2020-10-20 00:58:15 +02:00

1003 lines
28 KiB
ActionScript
Executable File

/*
Copyright (c) 2009 Yahoo! Inc. All rights reserved.
The copyrights embodied in the content of this file are licensed under the BSD (revised) open source license
*/
package com.yahoo.astra.fl.controls
{
import com.yahoo.astra.fl.controls.carouselClasses.CarouselCellRenderer;
import com.yahoo.astra.fl.controls.carouselClasses.CarouselListData;
import com.yahoo.astra.fl.controls.carouselClasses.ICarouselLayoutRenderer;
import com.yahoo.astra.fl.controls.carouselClasses.SlidingCarouselRenderer;
import com.yahoo.astra.fl.controls.carouselClasses.StackCarouselRenderer;
import com.yahoo.astra.fl.utils.UIComponentUtil;
import com.yahoo.astra.fl.controls.carouselClasses.astra_carousel_internal;
import fl.controls.ScrollPolicy;
import fl.controls.SelectableList;
import fl.controls.listClasses.ICellRenderer;
import fl.core.InvalidationType;
import fl.core.UIComponent;
import fl.data.DataProvider;
import fl.events.ComponentEvent;
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.events.Event;
import flash.ui.Keyboard;
import flash.utils.Dictionary;
/**
* A list-like component that supports custom rendering engines for maximum
* creative flexibility.
*
* @see fl.controls.List
* @see http://developer.yahoo.com/ypatterns/pattern.php?pattern=carousel Carousel Pattern
*/
public class Carousel extends SelectableList
{
//--------------------------------------
// Static Properties
//--------------------------------------
/**
* @private
*/
private static var defaultStyles:Object =
{
cellRenderer: CarouselCellRenderer,
skin: "Carousel_skin",
contentPadding: 1
};
/**
* @private
*
* @langversion 3.0
* @playerversion Flash 9.0.28.0
*/
protected var _rowHeight:Number = 20;
/**
* Gets or sets the height of each row in the list, in pixels.
*
* @default 20
*
* @langversion 3.0
* @playerversion Flash 9.0.28.0
*/
public function get rowHeight():Number {
return _rowHeight;
}
/**
* @private
*
* @langversion 3.0
* @playerversion Flash 9.0.28.0
*/
public function set rowHeight(value:Number):void {
_rowHeight = value;
invalidate(InvalidationType.SIZE);
}
/**
* @private (protected)
*
*/
protected var _rowCount:uint = 0;
/**
* @copy fl.controls.SelectableList
*
* @default 1
*
*/
override public function get rowCount():uint
{
return 1;
}
/**
* @private (protected)
*
*/
protected var _columnWidth:Number = 50;
[Inspectable(defaultValue=50)]
/**
* Gets or sets the width that is applied to a column in the list, in pixels.
*
* @default 50
*
* @includeExample examples/TileList.columnWidth.1.as -noswf
*
* @see #rowHeight
*
*/
public function get columnWidth():Number {
return _columnWidth;
}
/**
* @private (setter)
*
* @langversion 3.0
* @playerversion Flash 9.0.28.0
*/
public function set columnWidth(value:Number):void {
if (_columnWidth == value) { return; }
_columnWidth = value;
invalidate(InvalidationType.SIZE);
}
/**
* @private (protected)
*/
protected var _columnCount:uint = 0;
[Inspectable(defaultValue=0, type="Number")]
/**
* Gets or sets the number of columns that are at least partially visible in the
* list.
*
* @default 0
*
* @see #rowCount
*
*/
public function get columnCount():uint {
var pad:Number = Number(getStyleValue("contentPadding"));
var cols:uint = Math.max(1,(_width-2*pad)/_columnWidth<<0);
var rows:uint = Math.max(1,(_height-2*pad)/_rowHeight<<0);
cols = Math.max(1,Math.ceil((_width-2*pad)/_columnWidth));
return cols;
}
/**
* @private
*/
public function set columnCount(value:uint):void
{
if (value == 0) { return; }
if (componentInspectorSetting) {
_columnCount = value;
return;
}
_columnCount = 0;
var pad:Number = Number(getStyleValue("contentPadding"));
width = columnWidth*value+2*pad;
}
/**
* @private
* We need to ensure that the layout classes are included.
*/
private static const DEPENDENCIES:Array = [StackCarouselRenderer, CellRendererSymbol];
/**
* @private
* A new invalidation type used by the Carousel related to layout.
*/
protected static const INVALIDATION_TYPE_LAYOUT:String = "layoutInvalid";
//--------------------------------------
// Static Methods
//--------------------------------------
/**
* @private
* @copy fl.core.UIComponent#getStyleDefinition()
*/
public static function getStyleDefinition():Object
{
return mergeStyles(defaultStyles, SelectableList.getStyleDefinition(), UIComponent.getStyleDefinition());
}
//--------------------------------------------------------------------------
//
// Class mixins
//
//--------------------------------------------------------------------------
/**
* Placeholder for mixin by CarouselAccImpl.
*/
public static var createAccessibilityImplementation:Function;
//--------------------------------------
// Constructor
//--------------------------------------
/**
* Constructor.
*/
public function Carousel()
{
super();
//no scroll bars
this.horizontalScrollPolicy = this.verticalScrollPolicy = ScrollPolicy.OFF;
}
//--------------------------------------
// Properties
//--------------------------------------
/**
* @private
* Saves a list connecting items to item renderers so that the
* same renderer my be reused the next time this component redraws.
* Useful for when renderers load external assets.
*/
private var _itemToRendererHash:Dictionary = new Dictionary();
/**
* @private
* Storage for the layout renderer property.
*/
private var _layoutRenderer:ICarouselLayoutRenderer;
/**
* An instance of ICarouselLayoutRenderer that handles the layout of
* cell renderers for this Carousel instance.
*/
public function get layoutRenderer():ICarouselLayoutRenderer
{
return this._layoutRenderer;
}
/**
* @private
*/
public function set layoutRenderer(value:ICarouselLayoutRenderer):void
{
if(this._layoutRenderer)
{
this._layoutRenderer.cleanUp();
this._layoutRenderer.removeEventListener(ComponentEvent.RESIZE, layoutRendererResizeHandler);
}
this._layoutRenderer = value;
this._layoutRenderer.addEventListener(ComponentEvent.RESIZE, layoutRendererResizeHandler, false, 0, true);
this.invalidate(INVALIDATION_TYPE_LAYOUT);
}
/**
* @private
* Storage for the labelField property.
*/
protected var _labelField:String = "label";
[Inspectable(defaultValue="label")]
/**
* Gets or sets the name of the field in the <code>dataProvider</code> object
* to be displayed as the label in the tabs.
*
* <p>By default, the component displays the <code>label</code> property
* of each <code>dataProvider</code> item. If the <code>dataProvider</code>
* items do not contain a <code>label</code> property, you can set the
* <code>labelField</code> property to use a different property.</p>
*
* <p><strong>Note:</strong> The <code>labelField</code> property is not used
* if the <code>labelFunction</code> property is set to a callback function.</p>
*
* @default "label"
*
* @see #labelFunction
*/
public function get labelField():String
{
return this._labelField;
}
/**
* @private
*/
public function set labelField(value:String):void
{
if(this._labelField != value)
{
this._labelField = value;
this.invalidate(InvalidationType.DATA);
}
}
/**
* @private
* Storage for the labelFunction property.
*/
protected var _labelFunction:Function = null;
/**
* Gets or sets the function to be used to obtain the label for the item.
*
* <p>By default, the component displays the <code>label</code> property
* for a <code>dataProvider</code> item. But some data sets may not have
* a <code>label</code> field or may not have a field whose value
* can be used as a label without modification. For example, a given data
* set might store full names but maintain them in <code>lastName</code> and
* <code>firstName</code> fields. In such a case, this property could be
* used to set a callback function that concatenates the values of the
* <code>lastName</code> and <code>firstName</code> fields into a full
* name string to be displayed.</p>
*
* <p><strong>Note:</strong> The <code>labelField</code> property is not used
* if the <code>labelFunction</code> property is set to a callback function.</p>
*
* @default null
*/
public function get labelFunction():Function
{
return this._labelFunction;
}
/**
* @private
*/
public function set labelFunction(value:Function):void
{
if(this._labelFunction != value)
{
this._labelFunction = value;
this.invalidate(InvalidationType.DATA);
}
}
/**
* @private
* Storage for the iconField property.
*/
private var _iconField:String = "icon";
/**
* Gets or sets the item field that provides the icon for a cell renderer.
*
* <p><strong>Note:</strong> The <code>iconField</code> is not used if the
* <code>iconFunction</code> property is set to a callback function.</p>
*
* @default "icon"
*
* @see #iconFunction
*/
public function get iconField():String
{
return this._iconField;
}
/**
* @private
*/
public function set iconField(value:String):void
{
if(this._iconField != value)
{
this._iconField = value;
this.invalidate(InvalidationType.DATA);
}
}
/**
* @private
* Storage for the iconFunction property.
*/
protected var _iconFunction:Function = null;
/**
* Gets or sets the function to be used to obtain the icon for the item.
*
* <p>By default, the component displays the <code>icon</code> property
* for a <code>dataProvider</code> item. But some data sets may not have
* a <code>icon</code> field or may not have a field whose value
* can be used as a icon without modification.</p>
*
* <p><strong>Note:</strong> The <code>iconField</code> property is not used
* if the <code>iconFunction</code> property is set to a callback function.</p>
*
* @default null
*/
public function get iconFunction():Function
{
return this._iconFunction;
}
/**
* @private
*/
public function set iconFunction(value:Function):void
{
if(this._iconFunction != value)
{
this._iconFunction = value;
this.invalidate(InvalidationType.DATA);
}
}
/**
* @private
* Storage for the sourceField property.
*/
private var _sourceField:String = "source";
[Inspectable(defaultValue="source")]
/**
* Gets or sets the item field that provides the source path for a cell renderer.
*
* <p><strong>Note:</strong> The <code>sourceField</code> is not used if the
* <code>sourceFunction</code> property is set to a callback function.</p>
*
* @default "source"
*
* @see #sourceFunction
*/
public function get sourceField():String
{
return this._sourceField;
}
/**
* @private
*/
public function set sourceField(value:String):void
{
if(this._sourceField != value)
{
this._sourceField = value;
this.invalidate(InvalidationType.DATA);
}
}
/**
* @private
* Storage for the sourceFunction property.
*/
protected var _sourceFunction:Function = null;
/**
* Gets or sets the function to be used to obtain the image source for the item.
*
* <p>By default, the component displays the <code>source</code> property
* for a <code>dataProvider</code> item. But some data sets may not have
* a <code>source</code> field or may not have a field whose value
* can be used as an image source without modification.</p>
*
* <p><strong>Note:</strong> The <code>sourceField</code> property is not used
* if the <code>sourceFunction</code> property is set to a callback function.</p>
*
* @default null
*/
public function get sourceFunction():Function
{
return this._sourceFunction;
}
/**
* @private
*/
public function set sourceFunction(value:Function):void
{
if(this._sourceFunction != value)
{
this._sourceFunction = value;
this.invalidate(InvalidationType.DATA);
}
}
[Collection(collectionClass="fl.data.DataProvider", collectionItem="fl.data.TileListCollectionItem", identifier="item")]
/**
* @private
*/
override public function set dataProvider(value:DataProvider):void
{
super.dataProvider = value;
if(this.dataProvider)
{
//make sure we have a valid selected index, which is required for the
//proper display of Carousel
if(this.dataProvider.length == 0)
{
this.selectedIndex = -1;
}
else if(this.selectedIndex < 0 || this.selectedIndex >= this.dataProvider.length)
{
this.selectedIndex = 0;
}
}
}
/**
* @private
* SelectableList accesses the list property, which we've removed,
* so let's remake UIComponent's set enabled.
*/
override public function set enabled(value:Boolean):void
{
if (value == this._enabled)
{
return;
}
this._enabled = value;
this.invalidate(InvalidationType.STATE);
}
/**
* @private
* Flag that indicates that the layout renderer is currently being
* redrawn.
*/
protected var isDrawingRenderer:Boolean = false;
//--------------------------------------
// Public Methods
//--------------------------------------
/**
* Retrieves the label text that the renderer displays for the given
* data object based on the <code>labelField</code> or <code>labelFunction</code>
* defined by the Carousel.
*/
override public function itemToLabel(item:Object):String
{
if(!item)
{
return "";
}
if(this.labelFunction != null)
{
return this.labelFunction(item);
}
else if(item.hasOwnProperty(this.labelField))
{
return item[this.labelField];
}
return "";
}
/**
* Retrieves the image source that the renderer displays for the given
* data object based on the <code>sourceField</code> or <code>sourceFunction</code>
* defined by the Carousel. Generally, this value is the URL to an image
* or SWF file to be displayed in a UILoader.
*/
public function itemToSource(item:Object):Object
{
if(!item)
{
return null;
}
if(this.sourceFunction != null)
{
return this.sourceFunction(item);
}
else if(item.hasOwnProperty(this.sourceField))
{
return item[this.sourceField];
}
return null;
}
/**
* Retrieves the icon that the renderer displays for the given
* data object based on the <code>iconField</code> or <code>iconFunction</code>
* defined by the Carousel.
*/
public function itemToIcon(item:Object):Object
{
if(!item)
{
return null;
}
if(this.iconFunction != null)
{
return this.iconFunction(item);
}
else if(item.hasOwnProperty(this.iconField))
{
return item[this.iconField];
}
return null;
}
//--------------------------------------
// Namespaced Methods
//--------------------------------------
/**
* Tells the Carousel that its cell renderers are invalid and that the
* layout renderer is beginning to draw.
*/
astra_carousel_internal function invalidateCellRenderers():void
{
this._layoutRenderer.cleanUp();
this.activeCellRenderers = [];
//save any cell renderers that are already showing
//items that appear in the current data provider
for(var item:Object in this._itemToRendererHash)
{
if(this.dataProvider.getItemIndex(item) < 0)
{
var renderer:ICellRenderer = ICellRenderer(this._itemToRendererHash[item]);
this.availableCellRenderers.push(renderer);
delete this._itemToRendererHash[item];
}
}
}
/**
* Informs the Carousel that the layout renderer has finished drawing
* and that it may perform any garbage collection needed.
*/
astra_carousel_internal function validateCellRenderers():void
{
//remove any cell renderers that we aren't using...
var rendererCount:int = this.availableCellRenderers.length;
for(var i:int = 0; i < rendererCount; i++)
{
var renderer:DisplayObject = DisplayObject(this.availableCellRenderers.shift());
renderer.parent.removeChild(renderer);
}
//...even if they're for data that we still use (but don't display)!
for each(renderer in this._itemToRendererHash)
{
renderer.parent.removeChild(renderer);
}
//create a hash of items to item renderers so that we can reuse the
//same renderer for each item the next time we redraw
this._itemToRendererHash = new Dictionary();
rendererCount = this.activeCellRenderers.length;
for(i = 0; i < rendererCount; i++)
{
var activeRenderer:ICellRenderer = ICellRenderer(this.activeCellRenderers[i]);
this._itemToRendererHash[activeRenderer.data] = activeRenderer;
}
}
/**
* Creates a cell renderer for use by the layout renderer.
*/
astra_carousel_internal function createCellRenderer(item:Object, parent:DisplayObjectContainer = null):ICellRenderer
{
/*
* Algorithm works as follows:
*
* 1) If we're requesting a cell renderer for data that was used the
* last time we redrew, reuse the same cell renderer.
*
* 2) Reuse a cell renderer for data that no longer exists in the
* data provider.
*
* 3) Create a new cell renderer if no cached renderers are
* available to be reused.
*
* fl.controls.List does the same thing.
*
*/
var renderer:ICellRenderer = this.itemToCellRenderer(item);
if(renderer)
{
//we've already created this renderer, reuse it.
return renderer;
}
if(!parent)
{
parent = DisplayObjectContainer(this.layoutRenderer);
}
if(this._itemToRendererHash[item])
{
//reuse a renderer if one already exists for this item
renderer = this._itemToRendererHash[item];
//we don't want to reuse it twice!
delete this._itemToRendererHash[item];
parent.setChildIndex(DisplayObject(renderer), parent.numChildren - 1);
}
else if(this.availableCellRenderers.length > 0)
{
renderer = ICellRenderer(this.availableCellRenderers.shift());
parent.setChildIndex(DisplayObject(renderer), parent.numChildren - 1);
}
else
{
var CellRendererType:Object = this.getStyleValue("cellRenderer");
renderer = UIComponentUtil.getDisplayObjectInstance(this, CellRendererType) as ICellRenderer;
if(!(renderer is ICellRenderer))
{
throw new Error("Cell renderers must implement the ICellRenderer interface.");
}
parent.addChild(DisplayObject(renderer));
}
var label:String = this.itemToLabel(item);
var icon:Object = this.itemToIcon(item);
var source:Object = this.itemToSource(item);
renderer.listData = new CarouselListData(label, source, icon, this, this.dataProvider.getItemIndex(item), 0, 0);
renderer.data = item;
renderer.selected = this.selectedItems.indexOf(item) >= 0;
//update styles
this.activeCellRenderers.push(renderer);
if(Object(renderer).hasOwnProperty("setStyle"))
{
for(var n:String in this.updatedRendererStyles)
{
Object(renderer).setStyle(n, this.updatedRendererStyles[n]);
}
}
return ICellRenderer(renderer);
}
//--------------------------------------
// Protected Methods
//--------------------------------------
/**
* @private (protected)
*
* We're not using the <code>list</code> variable due to layout being accomplished by composition.
* So we remove it to prevent a null reference error when changing data providers.
*
*/
override protected function _invalidateList():void
{
availableCellRenderers = [];
}
/**
* @private
*/
override protected function configUI():void
{
super.configUI();
if(!this.layoutRenderer)
{
this.layoutRenderer = new SlidingCarouselRenderer();
}
this.listHolder.removeChild(this.list);
this.list = null;
}
/**
* @private
*/
override protected function draw():void
{
this.isDrawingRenderer = false;
if(this.isInvalid(INVALIDATION_TYPE_LAYOUT))
{
if(this.layoutRenderer)
{
if(DisplayObject(this.layoutRenderer).parent != this.listHolder)
{
this.listHolder.addChild(DisplayObject(this.layoutRenderer));
}
this.layoutRenderer.carousel = this;
}
}
super.draw();
var contentPadding:Number = this.getStyleValue("contentPadding") as Number;
var rendererWidth:Number = this.width - 2 * contentPadding;
var rendererHeight:Number = this.height - 2 * contentPadding;
this.layoutRenderer.move(contentPadding, contentPadding);
this.layoutRenderer.setSize(rendererWidth, rendererHeight);
this.isDrawingRenderer = true;
this.layoutRenderer.drawNow();
this.isDrawingRenderer = false;
}
/**
* @private (protected)
* Moves the selection in a vertical direction in response
* to the user selecting items using the up-arrow or down-arrow
* Keys and modifiers such as the Shift and Ctrl keys.
*
* @param code The key that was pressed (e.g. Keyboard.DOWN)
*
* @param shiftKey <code>true</code> if the shift key was held down when
* the keyboard key was pressed.
*
* @param ctrlKey <code>true</code> if the ctrl key was held down when
* the keyboard key was pressed.
*
* @langversion 3.0
* @playerversion Flash 9.0.28.0
*/
override protected function moveSelectionVertically(code:uint, shiftKey:Boolean, ctrlKey:Boolean):void
{
var pageSize:int = Math.max(Math.floor(calculateAvailableHeight() / rowHeight), 1);
var newCaretIndex:int = -1;
var dir:int = 0;
switch(code)
{
case Keyboard.UP:
if (caretIndex > 0)
{
newCaretIndex = caretIndex - 1;
}
break;
case Keyboard.DOWN:
if (caretIndex < length - 1)
{
newCaretIndex = caretIndex + 1;
}
break;
case Keyboard.PAGE_UP:
if (caretIndex > 0)
{
newCaretIndex = Math.max(caretIndex - pageSize, 0);
}
break;
case Keyboard.PAGE_DOWN:
if (caretIndex < length - 1)
{
newCaretIndex = Math.min(caretIndex + pageSize, length - 1);
}
break;
case Keyboard.HOME:
if (caretIndex > 0)
{
newCaretIndex = 0;
}
break;
case Keyboard.END:
if (caretIndex < length - 1)
{
newCaretIndex = length - 1;
}
break;
}
if(newCaretIndex >= 0)
{
doKeySelection(newCaretIndex, shiftKey, ctrlKey);
scrollToSelected();
}
}
/**
* @inheritDoc
*/
override public function scrollToIndex(newCaretIndex:int):void
{
drawNow();
var lastVisibleItemIndex:uint = Math.floor((_verticalScrollPosition + availableHeight) / rowHeight) - 1;
var firstVisibleItemIndex:uint = Math.ceil(_verticalScrollPosition / rowHeight);
if(newCaretIndex < firstVisibleItemIndex)
{
verticalScrollPosition = newCaretIndex * rowHeight;
}
else if(newCaretIndex > lastVisibleItemIndex)
{
verticalScrollPosition = (newCaretIndex + 1) * rowHeight - availableHeight;
}
}
/**
* @inheritDoc
*/
override protected function moveSelectionHorizontally(code:uint, shiftKey:Boolean, ctrlKey:Boolean):void
{
var totalCols:uint = length;
var index:int;
switch(code)
{
case Keyboard.LEFT:
index = Math.max(0, selectedIndex - 1); break;
case Keyboard.RIGHT:
index = Math.min(length - 1, selectedIndex + 1); break;
case Keyboard.HOME:
index = 0; break;
case Keyboard.END:
index = length - 1; break;
case Keyboard.PAGE_UP:
var firstIndex:int = selectedIndex;
index = Math.max(0, Math.max(firstIndex, selectedIndex - length));
break;
case Keyboard.PAGE_DOWN:
var lastIndex:int = selectedIndex - selectedIndex % totalCols + totalCols -1;
index = Math.min(length - 1, Math.min(lastIndex, selectedIndex + totalCols)); break;
}
doKeySelection(index, shiftKey, ctrlKey);
scrollToSelected();
}
/**
* @private (protected)
*
* @langversion 3.0
* @playerversion Flash 9.0.28.0
*/
protected function doKeySelection(newCaretIndex:int, shiftKey:Boolean, ctrlKey:Boolean):void {
var selChanged:Boolean = false;
if(shiftKey) {
var i:int;
var selIndices:Array = [];
var startIndex:int = lastCaretIndex;
var endIndex:int = newCaretIndex;
if(startIndex == -1) {
startIndex = caretIndex != -1 ? caretIndex : newCaretIndex;
}
if(startIndex > endIndex) {
endIndex = startIndex;
startIndex = newCaretIndex;
}
for(i = startIndex; i <= endIndex; i++) {
selIndices.push(i);
}
selectedIndices = selIndices;
caretIndex = newCaretIndex;
selChanged = true;
} else {
selectedIndex = newCaretIndex;
caretIndex = lastCaretIndex = newCaretIndex;
selChanged = true;
}
if(selChanged) {
dispatchEvent(new Event(Event.CHANGE));
}
invalidate(InvalidationType.DATA);
}
/**
* @private
*/
protected function calculateAvailableHeight():Number
{
var pad:Number = Number(getStyleValue("contentPadding"));
return height-pad*2-((_horizontalScrollPolicy == ScrollPolicy.ON || (_horizontalScrollPolicy == ScrollPolicy.AUTO && _maxHorizontalScrollPosition > 0)) ? 15 : 0);
}
/**
* @inheritDoc
*/
override protected function initializeAccessibility():void
{
if (Carousel.createAccessibilityImplementation != null)
Carousel.createAccessibilityImplementation(this);
}
//--------------------------------------
// Private Event Handlers
//--------------------------------------
/**
* @private
* Resizes the carousel when the layout renderer resizes.
*/
private function layoutRendererResizeHandler(event:ComponentEvent):void
{
var contentPadding:Number = this.getStyleValue("contentPadding") as Number;
this.setSize(this.layoutRenderer.width + 2 * contentPadding, this.layoutRenderer.height + 2 * contentPadding);
if(this.isDrawingRenderer)
{
//this could lead to an infinite loop if the layout renderer
//is not doing its job. the renderer is expected to always check
//whether its width and height values have changed.
this.drawNow();
}
}
}
}