blob: aaba442a8790495c92e8a91c4090624492b706f4 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package spark.components.supportClasses
{
import flash.display.DisplayObject;
import flash.events.Event;
import flash.events.FocusEvent;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.geom.Point;
import mx.collections.IList;
import mx.core.InteractionMode;
import mx.events.CollectionEvent;
import mx.events.FlexEvent;
import spark.components.List;
import spark.core.NavigationUnit;
import spark.events.DropDownEvent;
import spark.events.IndexChangeEvent;
import mx.core.mx_internal;
use namespace mx_internal;
//--------------------------------------
// Styles
//--------------------------------------
/**
* The radius of the corners for this component.
*
* @default 4
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="cornerRadius", type="Number", format="Length", inherit="no", theme="spark")]
/**
* Controls the visibility of the drop shadow for this component.
*
* @default true
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="dropShadowVisible", type="Boolean", inherit="no", theme="spark")]
//--------------------------------------
// Events
//--------------------------------------
/**
* Dispatched when the drop-down list closes for any reason, such when
* the user:
* <ul>
* <li>Selects an item in the drop-down list.</li>
* <li>Clicks outside of the drop-down list.</li>
* <li>Clicks the anchor button while the drop-down list is
* displayed.</li>
* </ul>
*
* @eventType spark.events.DropDownEvent.CLOSE
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Event(name="close", type="spark.events.DropDownEvent")]
/**
* Dispatched when the user clicks the anchor button
* to display the drop-down list.
* It is also dispatched if the user
* uses Control-Down to open the dropDown.
*
* @eventType spark.events.DropDownEvent.OPEN
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Event(name="open", type="spark.events.DropDownEvent")]
//--------------------------------------
// SkinStates
//--------------------------------------
/**
* Skin state for the open state of the DropDownListBase control.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[SkinState("open")]
//--------------------------------------
// Excluded APIs
//--------------------------------------
[Exclude(name="allowMultipleSelection", kind="property")]
[Exclude(name="dragEnabled", kind="property")]
[Exclude(name="dragMoveEnabled", kind="property")]
[Exclude(name="dropEnabled", kind="property")]
[Exclude(name="selectedIndices", kind="property")]
[Exclude(name="selectedItems", kind="property")]
//--------------------------------------
// Other metadata
//--------------------------------------
[AccessibilityClass(implementation="spark.accessibility.DropDownListBaseAccImpl")]
/**
* The DropDownListBase control contains a drop-down list
* from which the user can select a single value.
* Its functionality is very similar to that of the
* SELECT form element in HTML.
*
* <p>The DropDownListBase control consists of the anchor button,
* and drop-down-list.
* Use the anchor button to open and close the drop-down-list.
* </p>
*
* <p>When the drop-down list is open:</p>
* <ul>
* <li>Clicking the anchor button closes the drop-down list
* and commits the currently selected data item.</li>
* <li>Clicking outside of the drop-down list closes the drop-down list
* and commits the currently selected data item.</li>
* <li>Clicking on a data item selects that item and closes the drop-down list.</li>
* <li>If the <code>requireSelection</code> property is <code>false</code>,
* clicking on a data item while pressing the Control key deselects
* the item and closes the drop-down list.</li>
* </ul>
*
* @mxml <p>The <code>&lt;s:DropDownListBase&gt;</code> tag inherits all of the tag
* attributes of its superclass and adds the following attributes:</p>
*
* <pre>
* &lt;s:DropDownListBase
* <strong>Styles</strong>
* cornerRadius="4"
* dropShadowVisible="true"
*
* <strong>Events</strong>
* closed="<i>No default</i>"
* open="<i>No default</i>"
* /&gt;
* </pre>
*
* @see spark.skins.spark.DropDownListSkin
* @see spark.components.supportClasses.DropDownController
*
* @includeExample examples/DropDownListExample.mxml
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public class DropDownListBase extends List
{
include "../../core/Version.as";
//--------------------------------------------------------------------------
//
// Class mixins
//
//--------------------------------------------------------------------------
/**
* @private
* Placeholder for mixin by DropDownListBaseAccImpl.
*/
mx_internal static var createAccessibilityImplementation:Function;
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function DropDownListBase()
{
super();
dropDownController = new DropDownController();
}
//--------------------------------------------------------------------------
//
// Skin parts
//
//--------------------------------------------------------------------------
//----------------------------------
// dropDown
//----------------------------------
[SkinPart(required="false")]
/**
* A skin part that defines the drop-down list area. When the DropDownListBase is open,
* clicking anywhere outside of the dropDown skin part closes the
* drop-down list.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public var dropDown:DisplayObject;
//----------------------------------
// openButton
//----------------------------------
[SkinPart(required="true")]
/**
* A skin part that defines the anchor button.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public var openButton:ButtonBase;
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
*/
private var labelChanged:Boolean = false;
// Stores the user selected index until the dropDown closes
/**
* @private
*/
mx_internal static var PAGE_SIZE:int = 5;
//--------------------------------------------------------------------------
//
// Overridden properties
//
//--------------------------------------------------------------------------
//----------------------------------
// allowMultipleSelection
//----------------------------------
/**
* @private
*/
override public function set allowMultipleSelection(value:Boolean):void
{
// Don't allow this value to be set. If the multiple
// selection related properties are set and
// allowMultipleSelection is false, List will
// select the first item passed in.
return;
}
//----------------------------------
// dataProvider
//----------------------------------
[Inspectable(category="Data")]
/**
* @private
* Update the label if the dataProvider has changed
*/
override public function set dataProvider(value:IList):void
{
if (dataProvider === value)
return;
super.dataProvider = value;
labelChanged = true;
invalidateProperties();
}
//----------------------------------
// dragEnabled
//----------------------------------
/**
* @private
* Excluded property
*/
override public function set dragEnabled(value:Boolean):void
{
}
//----------------------------------
// dragMoveEnabled
//----------------------------------
/**
* @private
* Excluded property
*/
override public function set dragMoveEnabled(value:Boolean):void
{
}
//----------------------------------
// dropEnabled
//----------------------------------
/**
* @private
* Excluded property
*/
override public function set dropEnabled(value:Boolean):void
{
}
//----------------------------------
// labelField
//----------------------------------
[Inspectable(category="Data", defaultValue="label")]
/**
* @private
*/
override public function set labelField(value:String):void
{
if (labelField == value)
return;
super.labelField = value;
labelChanged = true;
invalidateProperties();
}
//----------------------------------
// labelFunction
//----------------------------------
[Inspectable(category="Data")]
/**
* @private
*/
override public function set labelFunction(value:Function):void
{
if (labelFunction == value)
return;
super.labelFunction = value;
labelChanged = true;
invalidateProperties();
}
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// dropDownController
//----------------------------------
/**
* @private
*/
private var _dropDownController:DropDownController;
/**
* Instance of the DropDownController class that handles all of the mouse, keyboard
* and focus user interactions.
*
* Flex calls the <code>initializeDropDownController()</code> method after
* the DropDownController instance is created in the constructor.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
protected function get dropDownController():DropDownController
{
return _dropDownController;
}
/**
* @private
*/
protected function set dropDownController(value:DropDownController):void
{
if (_dropDownController == value)
return;
_dropDownController = value;
_dropDownController.addEventListener(DropDownEvent.OPEN, dropDownController_openHandler);
_dropDownController.addEventListener(DropDownEvent.CLOSE, dropDownController_closeHandler);
_dropDownController.closeOnResize = _closeDropDownOnResize;
if (openButton)
_dropDownController.openButton = openButton;
if (dropDown)
_dropDownController.dropDown = dropDown;
}
//----------------------------------
// isDropDownOpen
//----------------------------------
/**
* @copy spark.components.supportClasses.DropDownController#isOpen
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get isDropDownOpen():Boolean
{
if (dropDownController)
return dropDownController.isOpen;
else
return false;
}
//----------------------------------
// closeDropDownOnResize
//----------------------------------
/**
* @private
*
* cached value: getStyle("interactionMode") == InteractionMode.TOUCH
*/
private var isTouchInteractionMode:Boolean = false;
/**
* @private
*
* do not know what default value for closeDropDownOnResize will be until styles have been applied,
* but user may set property before that. this flag indicates whether property has been explicitly set,
* and so will not need to be determined from css when stylesInitialized() or styleChanged() is called.
*/
private var isCloseDropDownOnResizeExplicitlySet:Boolean = false;
/**
* @private
*/
protected var _closeDropDownOnResize:Boolean = true;
[Inspectable(category="General", enumeration="true,false", defaultValue="true")]
/**
* When <code>true</code>, resizing the system manager
* closes the drop down.
* For mobile applications, this property is set
* to <code>false</code> so that the drop down stays open when the
* page orientation changes.
*
* @default true
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.12
*/
public function get closeDropDownOnResize():Boolean
{
return _closeDropDownOnResize;
}
/**
* @private
*/
public function set closeDropDownOnResize(value:Boolean):void
{
setCloseDropDownOnResize(value, true);
}
/**
* @private
*/
private function setCloseDropDownOnResize(value:Boolean, explicitlySet:Boolean):void
{
_closeDropDownOnResize = value;
isCloseDropDownOnResizeExplicitlySet ||= explicitlySet;
if (dropDownController)
dropDownController.closeOnResize = _closeDropDownOnResize;
}
//----------------------------------
// userProposedSelectedIndex
//----------------------------------
/**
* @private
*/
private var _userProposedSelectedIndex:Number = NO_SELECTION;
/**
* @private
*/
mx_internal function set userProposedSelectedIndex(value:Number):void
{
_userProposedSelectedIndex = value;
}
/**
* @private
*/
mx_internal function get userProposedSelectedIndex():Number
{
return _userProposedSelectedIndex;
}
//--------------------------------------------------------------------------
//
// Overridden methods
//
//--------------------------------------------------------------------------
/**
* @private
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.12
*/
override public function stylesInitialized():void
{
super.stylesInitialized();
setInteractionMode();
}
/**
* @private
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.12
*/
override public function styleChanged(styleProp:String):void
{
super.styleChanged(styleProp);
if (!styleProp || styleProp == "styleName" || styleProp == "interactionMode")
setInteractionMode();
}
/**
* @private
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.12
*/
private function setInteractionMode():void
{
isTouchInteractionMode = getStyle("interactionMode") == InteractionMode.TOUCH;
if (!isCloseDropDownOnResizeExplicitlySet)
setCloseDropDownOnResize(!isTouchInteractionMode, false);
}
/**
* @private
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.12
*/
override public function setSelectedIndex(rowIndex:int, dispatchChangeEvent:Boolean = false, changeCaret:Boolean = true):void
{
super.setSelectedIndex(rowIndex, dispatchChangeEvent, changeCaret);
if (isTouchInteractionMode)
{
userProposedSelectedIndex = rowIndex;
closeDropDown(true);
}
}
/**
* @private
* Called by the initialize() method of UIComponent
* to hook in the accessibility code.
*/
override protected function initializeAccessibility():void
{
if (DropDownListBase.createAccessibilityImplementation != null)
DropDownListBase.createAccessibilityImplementation(this);
}
/**
* @private
*/
override protected function commitProperties():void
{
super.commitProperties();
if (labelChanged)
{
labelChanged = false;
updateLabelDisplay();
}
}
/**
* @private
*/
override protected function partAdded(partName:String, instance:Object):void
{
super.partAdded(partName, instance);
if (instance == openButton)
{
if (dropDownController)
dropDownController.openButton = openButton;
}
else if (instance == dropDown && dropDownController)
{
dropDownController.dropDown = dropDown;
}
}
/**
* @private
*/
override protected function partRemoved(partName:String, instance:Object):void
{
if (dropDownController)
{
if (instance == openButton)
dropDownController.openButton = null;
if (instance == dropDown)
dropDownController.dropDown = null;
}
super.partRemoved(partName, instance);
}
/**
* @private
*/
override protected function getCurrentSkinState():String
{
return !enabled ? "disabled" : isDropDownOpen ? "open" : "normal";
}
/**
* @private
*/
override protected function commitSelection(dispatchChangedEvents:Boolean = true):Boolean
{
var retVal:Boolean = super.commitSelection(dispatchChangedEvents);
updateLabelDisplay();
return retVal;
}
/**
* @private
* In updateRenderer, we want to select the proposedSelectedIndex
*/
override mx_internal function isItemIndexSelected(index:int):Boolean
{
return userProposedSelectedIndex == index;
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* Open the drop-down list and dispatch
* a <code>DropdownEvent.OPEN</code> event.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function openDropDown():void
{
dropDownController.openDropDown();
}
/**
* Close the drop-down list and dispatch a <code>DropDownEvent.CLOSE</code> event.
*
* @param commit If <code>true</code>, commit the selected
* data item.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function closeDropDown(commit:Boolean):void
{
dropDownController.closeDropDown(commit);
}
/**
* @private
* Called whenever we need to update the text passed to the labelDisplay skin part
*/
// TODO (jszeto): Make this protected and make the name more generic (passing data to skin)
mx_internal function updateLabelDisplay(displayItem:* = undefined):void
{
// DropDownList and ComboBox will override this function
}
/**
* @private
* Called whenever we need to change the highlighted selection while the dropDown is open
* ComboBox overrides this behavior
*/
mx_internal function changeHighlightedSelection(newIndex:int, scrollToTop:Boolean = false):void
{
// Store the selection in userProposedSelectedIndex because we
// don't want to update selectedIndex until the dropdown closes
itemSelected(userProposedSelectedIndex, false);
userProposedSelectedIndex = newIndex;
itemSelected(userProposedSelectedIndex, true);
positionIndexInView(userProposedSelectedIndex, scrollToTop ? 0 : NaN);
var e:IndexChangeEvent = new IndexChangeEvent(IndexChangeEvent.CARET_CHANGE);
e.oldIndex = caretIndex;
setCurrentCaretIndex(userProposedSelectedIndex);
e.newIndex = caretIndex;
dispatchEvent(e);
}
/**
* @private
*/
mx_internal function positionIndexInView(index:int, topOffset:Number = NaN,
bottomOffset:Number = NaN,
leftOffset:Number = NaN,
rightOffset:Number = NaN):void
{
if (!layout || dataGroup == null)
return;
var spDelta:Point =
dataGroup.layout.getScrollPositionDeltaToElementHelper(index, topOffset, bottomOffset,
leftOffset, rightOffset);
if (spDelta)
{
dataGroup.horizontalScrollPosition += spDelta.x;
dataGroup.verticalScrollPosition += spDelta.y;
}
}
/**
* @private
*/
override mx_internal function findKey(eventCode:int):Boolean
{
if (!dataProvider || dataProvider.length == 0)
return false;
if (eventCode >= 33 && eventCode <= 126)
{
var matchingIndex:Number;
var keyString:String = String.fromCharCode(eventCode);
var startIndex:int = isDropDownOpen ? userProposedSelectedIndex + 1 : selectedIndex + 1;
startIndex = Math.max(0, startIndex);
matchingIndex = findStringLoop(keyString, startIndex, dataProvider.length);
// We didn't find the item, loop back to the top
if (matchingIndex == -1)
{
matchingIndex = findStringLoop(keyString, 0, startIndex);
}
if (matchingIndex != -1)
{
if (isDropDownOpen)
changeHighlightedSelection(matchingIndex);
else
setSelectedIndex(matchingIndex, true);
return true;
}
}
return false;
}
//--------------------------------------------------------------------------
//
// Event handlers
//
//--------------------------------------------------------------------------
/**
* @private
*/
override protected function dataProvider_collectionChangeHandler(event:Event):void
{
super.dataProvider_collectionChangeHandler(event);
if (event is CollectionEvent)
{
labelChanged = true;
invalidateProperties();
}
}
/**
* @private
*/
override protected function item_mouseDownHandler(event:MouseEvent):void
{
super.item_mouseDownHandler(event);
if (!isTouchInteractionMode)
{
userProposedSelectedIndex = selectedIndex;
closeDropDown(true);
}
}
/**
* @private
*/
override protected function keyDownHandler(event:KeyboardEvent) : void
{
if (!enabled)
return;
if (!dropDownController.processKeyDown(event))
{
// If rtl layout, need to swap Keyboard.LEFT and Keyboard.RIGHT.
var navigationUnit:uint = mapKeycodeForLayoutDirection(event);
if (findKey(event.charCode))
{
event.preventDefault();
return;
}
if (!NavigationUnit.isNavigationUnit(navigationUnit))
return;
var proposedNewIndex:int = NO_SELECTION;
var currentIndex:int;
if (isDropDownOpen)
{
// Normalize the proposed index for getNavigationDestinationIndex
currentIndex = userProposedSelectedIndex < NO_SELECTION ? NO_SELECTION : userProposedSelectedIndex;
proposedNewIndex = layout.getNavigationDestinationIndex(currentIndex, navigationUnit, arrowKeysWrapFocus);
if (proposedNewIndex != NO_SELECTION)
{
changeHighlightedSelection(proposedNewIndex);
event.preventDefault();
}
}
else if (dataProvider)
{
var maxIndex:int = dataProvider.length - 1;
// Normalize the proposed index for getNavigationDestinationIndex
currentIndex = caretIndex < NO_SELECTION ? NO_SELECTION : caretIndex;
switch (navigationUnit)
{
case NavigationUnit.UP:
{
if (arrowKeysWrapFocus &&
(currentIndex == 0 ||
currentIndex == NO_SELECTION ||
currentIndex == CUSTOM_SELECTED_ITEM))
proposedNewIndex = maxIndex;
else
proposedNewIndex = currentIndex - 1;
event.preventDefault();
break;
}
case NavigationUnit.DOWN:
{
if (arrowKeysWrapFocus &&
(currentIndex == maxIndex ||
currentIndex == NO_SELECTION ||
currentIndex == CUSTOM_SELECTED_ITEM))
proposedNewIndex = 0;
else
proposedNewIndex = currentIndex + 1;
event.preventDefault();
break;
}
case NavigationUnit.PAGE_UP:
{
proposedNewIndex = currentIndex == NO_SELECTION ?
NO_SELECTION : Math.max(currentIndex - PAGE_SIZE, 0);
event.preventDefault();
break;
}
case NavigationUnit.PAGE_DOWN:
{
proposedNewIndex = currentIndex == NO_SELECTION ?
PAGE_SIZE : (currentIndex + PAGE_SIZE);
event.preventDefault();
break;
}
case NavigationUnit.HOME:
{
proposedNewIndex = 0;
event.preventDefault();
break;
}
case NavigationUnit.END:
{
proposedNewIndex = maxIndex;
event.preventDefault();
break;
}
}
proposedNewIndex = Math.min(proposedNewIndex, maxIndex);
if (proposedNewIndex >= 0)
{
userProposedSelectedIndex = proposedNewIndex;
setSelectedIndex(proposedNewIndex, true);
}
}
}
else
{
event.preventDefault();
}
}
/**
* @private
*/
override protected function focusOutHandler(event:FocusEvent):void
{
if (isOurFocus(DisplayObject(event.target)))
dropDownController.processFocusOut(event);
super.focusOutHandler(event);
}
/**
* @private
* Event handler for the <code>dropDownController</code>
* <code>DropDownEvent.OPEN</code> event. Updates the skin's state and
* ensures that the selectedItem is visible.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
mx_internal function dropDownController_openHandler(event:DropDownEvent):void
{
addEventListener(FlexEvent.UPDATE_COMPLETE, open_updateCompleteHandler);
userProposedSelectedIndex = selectedIndex;
invalidateSkinState();
}
/**
* @private
*/
mx_internal function open_updateCompleteHandler(event:FlexEvent):void
{
removeEventListener(FlexEvent.UPDATE_COMPLETE, open_updateCompleteHandler);
positionIndexInView(selectedIndex, 0);
dispatchEvent(new DropDownEvent(DropDownEvent.OPEN));
}
/**
* @private
* Event handler for the <code>dropDownController</code>
* <code>DropDownEvent.CLOSE</code> event. Updates the skin's state.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
protected function dropDownController_closeHandler(event:DropDownEvent):void
{
addEventListener(FlexEvent.UPDATE_COMPLETE, close_updateCompleteHandler);
invalidateSkinState();
if (!event.isDefaultPrevented())
{
// Even if the dropDown was programmatically closed, assume the selection
// changed as a result of a previous user interaction
setSelectedIndex(userProposedSelectedIndex, true);
}
else
{
changeHighlightedSelection(selectedIndex);
}
}
/**
* @private
*/
private function close_updateCompleteHandler(event:FlexEvent):void
{
removeEventListener(FlexEvent.UPDATE_COMPLETE, close_updateCompleteHandler);
dispatchEvent(new DropDownEvent(DropDownEvent.CLOSE));
}
}
}