/*
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.autoCompleteClasses.AutoCompleteCellRenderer;
import com.yahoo.astra.fl.events.DropdownEvent;
import fl.controls.List;
import fl.controls.TextInput;
import fl.controls.listClasses.CellRenderer;
import fl.data.DataProvider;
import fl.events.ListEvent;
import fl.events.ScrollEvent;
import fl.transitions.easing.*;
import flash.display.DisplayObject;
import flash.events.Event;
import flash.events.FocusEvent;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.filters.BlurFilter;
import flash.geom.Point;
import flash.net.SharedObject;
import flash.text.TextFormat;
import flash.ui.Keyboard;
import flash.utils.*;
//--------------------------------------
// Events
//--------------------------------------
/**
* Dispatched when the filterFunction
property changes.
*
* You can listen for this event and update the component
* when the filterFunction
property changes.
typedText
property changes.
*
* You can listen for this event and update the component
* when the typedText
property changes.
*
* @eventType flash.events.Event
*/
[Event(name="typedTextChange", type="flash.events.Event")]
/**
* Dispatched when the completion box becomes visible.
*
* @eventType com.yahoo.astra.fl.events.DropdownEvent
*/
[Event(name="open", type="com.yahoo.astra.fl.events.DropdownEvent")]
/**
* Dispatched when the completion box becomes invisible.
*
* @eventType com.yahoo.astra.fl.events.DropdownEvent
*/
[Event(name="close", type="com.yahoo.astra.fl.events.DropdownEvent")]
//--------------------------------------
// Other metadata
//--------------------------------------
[IconFile("assets/AutoComplete.png")]
//--------------------------------------
// Class description
//--------------------------------------
/**
* The AutoComplete control works like a
* TextInput control, but can pop up a list of suggestions
* based on text entered. These suggestions
* are to be provided by setting the
* dataProvider
property.
*
* @author Alaric Cole
*/
public class AutoComplete extends TextInput
{
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*/
public function AutoComplete()
{
super();
addEventListener("unload", unloadHandler);
addEventListener("addedToStage", stageAdded);
textField.addEventListener(MouseEvent.MOUSE_DOWN, close);
}
//--------------------------------------------------------------------------
//
// Private variables
//
//--------------------------------------------------------------------------
/**
* @private
*/
private var showingDropdown:Boolean=false;
/**
* @private
*/
private var filteredData:Array;
/**
* @private
* A reference to the internal List that pops up to display a row
* for each dataProvider item.
*/
private var _dropdown:List;
/**
* @private
* A int to track the oldIndex, used when the dropdown is dismissed using the ESC key.
*/
private var _oldIndex:int;
/**
* @private
* Is the dropdown list currently shown?
*/
private var _showingDropdown:Boolean = false;
/**
* @private
*/
private function get isShowingDropdown():Boolean
{
return _showingDropdown;
}
/**
* @private
*/
private var _selectedIndexOnDropdown:int = -1;
/**
* @private
*/
private var bRemoveDropdown:Boolean = false;
/**
* @private
*/
private var inTween:Boolean = false;
/**
* @private
*/
private var bInKeyDown:Boolean = false;
/**
* @private
* Flag that will block default data/listData behavior
*/
private var selectedItemSet:Boolean;
/**
* @private
* Event that is causing the _dropdown to open or close.
*/
private var triggerEvent:Event;
/**
* @private
*/
private var usingLocalHistory:Boolean=false;
/**
* @private
*/
private var dropdownClosed:Boolean=true;
/**
* @private
*/
private var cursorPosition:Number = 0;
/**
* @private
*/
private var prevIndex:Number = -1;
/**
* @private
*/
private var removeHighlight:Boolean = false;
/**
* @private
*/
private var _showDropdown:Boolean=false;
/**
* @private
*/
private var _unfilteredDataProvider:DataProvider;
/**
* This holds the original data provider (without filtering based on what was typed).
*/
protected function get unfilteredDataProvider():DataProvider
{
return _unfilteredDataProvider;
}
/**
* This holds the data provider without filters.
*/
protected function set unfilteredDataProvider(value:DataProvider):void
{
_unfilteredDataProvider = value;
}
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
/**
* @private
* Storage for the popUpRenderer property.
*/
private var _defaultPopUpRenderer:Class = List ;
/**
* @private
* Storage for the popUpRenderer property.
*/
private var _popUpRenderer:Class = _defaultPopUpRenderer ;
/**
* The custom renderer that creates a List-derived instance to use
* as the drop-down.
* @default fl.controls.List
*
*/
public function get popUpRenderer():Class
{
return _popUpRenderer;
}
/**
* @private
*/
public function set popUpRenderer(value:Class):void
{
_popUpRenderer = value;
//remove all previous instances of dropdowns
}
/**
* @private
* Method for creating the Accessibility class.
* This method is called from UIComponent.
*/
public static var createAccessibilityImplementation:Function;
//----------------------------------
// dataProvider
//----------------------------------
/**
* @private
*/
private var _dataProvider:DataProvider;
/**
* Gets or sets the data model of the list of items to be viewed.
* A data provider can be shared by multiple list-based components.
* Changes to the data provider are immediately available to all components that use it as a data source.
*
* @default null
*/
public function get dataProvider():DataProvider
{
return _dataProvider;
}
[Collection(collectionClass="fl.data.DataProvider", collectionItem="fl.data.SimpleCollectionItem", identifier="item")]
public function set dataProvider(value:DataProvider):void
{
_dataProvider = value;
dropdown.dataProvider = value;
unfilteredDataProvider = value.clone();
}
//----------------------------------
// selectedIndex
//----------------------------------
/**
* Index of the selected item in the drop-down list.
* Setting this property sets the current index and displays
* the associated label in the TextInput portion.
* The default value is -1, but it set to 0
* when a dataProvider
is assigned, unless there is a prompt.
* If the control is editable, and the user types in the TextInput portion,
* the value of the selectedIndex
property becomes
* -1. If the value of the selectedIndex
* property is out of range, the selectedIndex
property is set to the last
* item in the dataProvider
.
The default value is -1, but it set to 0
* when a dataProvider
is assigned, unless there is a prompt.
* If the control is editable, and the user types in the TextInput portion,
* the value of the selectedIndex
property becomes
* -1. If the value of the selectedIndex
* property is out of range, the selectedIndex
property is set to the last
* item in the dataProvider
.
The default value is 100 or the width of AutoComplete component
* in the dataProvider
, whichever is greater.
dataProvider
* Array to display as the label in the TextInput portion and drop-down list.
* By default, the control uses a property named label
* on each Array object and displays it.
* However, if the dataProvider
items do not contain
* a label
property, you can set the labelField
* property to use a different property.
label
* on each dataProvider
item to determine its label.
* However, some data sets do not have a label
property,
* or do not have another property that can be used for displaying
* as a label.
* An example is a data set that has lastName
and
* firstName
fields but you want to display full names.
* You use labelFunction
to specify a callback function
* that uses the appropriate fields and return a displayable String.
The labelFunction takes a single argument which is the item * in the dataProvider and returns a String:
** myLabelFunction(item:Object):String ** */ public function get labelFunction():Function { return _labelFunction; } /** * @private */ public function set labelFunction(value:Function):void { _labelFunction = value; labelFunctionChanged = true; invalidate(); dispatchEvent(new Event("labelFunctionChanged")); } //---------------------------------- // rowCount //---------------------------------- /** * @private * Storage for the rowCount property. */ private var _rowCount:int = 5; /** * Maximum number of rows visible in the AutoComplete control list. * If there are fewer items in the * dataProvider, the AutoComplete shows only as many items as * there are in the dataProvider. * * @default 5 */ public function get rowCount():int { return Math.max(1, Math.min(dropdown.dataProvider.length, _rowCount)); } /** * @private */ public function set rowCount(value:int):void { _rowCount = value; if (_dropdown) _dropdown.rowCount = value; } //---------------------------------- // selectedLabel //---------------------------------- /** * The String displayed in the TextInput portion of the AutoComplete. It * is calculated from the data by using the
labelField
* or labelFunction
.
*/
public function get selectedLabel():String
{
var item:Object = selectedItem;
return itemToLabel(item);
}
//----------------------------------
// filterFunction
//----------------------------------
/**
* @private
* Storage for the filterFunction property.
*/
private var _filterFunction:Function = defaultFilterFunction;
/**
* @private
*/
private var filterFunctionChanged:Boolean = true;
/**
* A function that is used to select items that match the
* function's criteria.
* A filterFunction is expected to have the following signature:
*
* f(element:~~, index:int, arr:Array):Boolean* * where the return value is
true
if the specified item
* should displayed as a suggestion.
* Whenever there is a change in text in the AutoComplete control, this
* filterFunction is run on each item in the dataProvider
.
*
* The default implementation for filterFunction works as follows:
* If "AB" has been typed with or without leading or trailing whitespace,
* it will display all the items matching
* "AB~~" (ABaa, ABcc, abAc etc.).
An example usage of a customized filterFunction is when text typed * is a regular expression and we want to display all the * items which come in the set.
* * @example ** public function myFilterFunction(element:~~, index:int = 0, arr:Array = null):Boolean * { * public var regExp:RegExp = new RegExp(text,""); * return regExp.test(element); * } ** */ public function get filterFunction():Function { return _filterFunction; } /** * @private */ public function set filterFunction(value:Function):void { //An empty filterFunction is allowed but not a null filterFunction if(value!=null) { _filterFunction = value; filterFunctionChanged = true; invalidate(); dispatchEvent(new Event("filterFunctionChange")); } else _filterFunction = defaultFilterFunction; } /** * @private * Storage for the keepLocalHistory property. */ private var _keepLocalHistory:Boolean = false; /** * @private */ private var keepLocalHistoryChanged:Boolean = true; [Inspectable(category="General")] /** * When true, this causes the control to keep track of the * entries that are typed into the control, and saves the * history as a local shared object. When true, the * completionFunction and dataProvider are ignored. * * @default false */ public function get keepLocalHistory():Boolean { return _keepLocalHistory; } /** * @private */ public function set keepLocalHistory(value:Boolean):void { _keepLocalHistory = value; if(value) { addEventListener("focusOut",focusOutHandler); } else { removeEventListener("focusOut",focusOutHandler); } } //---------------------------------- // autoFillEnabled //---------------------------------- /** * @private * Storage for the autoFillEnabled property. */ private var _autoFillEnabled:Boolean; /** * @private */ private var autoFillEnabledChanged:Boolean; [Inspectable] /** * Whether to complete the text in the text field * with the first item in the drop down list or not. * * @default false */ public function get autoFillEnabled():Boolean { return _autoFillEnabled; } /** * @private */ public function set autoFillEnabled(value:Boolean):void { _autoFillEnabled = value; autoFillEnabledChanged = true; } /** * @private * Storage */ private var _minCharsForCompletion:int = 0; [Inspectable] /** * The minimum number of characters that must be typed in to display the completion list * @default 0 */ public function get minCharsForCompletion():int { return _minCharsForCompletion; } public function set minCharsForCompletion(value:int):void { _minCharsForCompletion = value; } [Inspectable] /** * A flag that indicates whether to loop the selection in the dropdown when * using the arrow keys. * * @default true */ public var loopSelection:Boolean = true; /** * @private */ private var continuation:Boolean = false; //---------------------------------- // typedText //---------------------------------- /** * @private * Storage for the typedText property. */ private var _typedText:String=""; /** * @private */ protected var typedTextChanged:Boolean; /** * A String to keep track of the text changed as * a result of user interaction. */ protected function get typedText():String { return _typedText; } /** * @private */ protected function set typedText(input:String):void { _typedText = input; typedTextChanged = true; updateDataProvider(); if(autoFillEnabled && dropdown.visible && typedText!="") { var lbl:String = itemToLabel(dataProvider.getItemAt(0)); var index:Number = lbl.toLowerCase().indexOf(text.toLowerCase()); if(index==0) { var t:String = lbl.substr(text.length); if(!removeHighlight) { text = text + t; setSelection(text.length,_typedText.length); textField.scrollH = 0; } else { setSelection(text.length,text.length); //keeping the autoFillEnabled highlighted text from being scrolled to //This should highlight the text but keep the original text in the same place horizontally textField.scrollH = 0; } removeHighlight = false; } } dispatchEvent(new Event("typedTextChange")); } /** * @private * Storage for the property. */ private var _dropdownEnabled:Boolean = true; [Inspectable] /** * Allows completion of a word or phrase with a dropdown list. * Setting to false could allow this to be used as an autofill/lookahead only component * @default true */ public function get dropdownEnabled():Boolean { return _dropdownEnabled; } public function set dropdownEnabled(value:Boolean):void { _dropdownEnabled = value; } /** * @private * Storage for the property. */ private var _emphasizeMatch:Boolean = false; [Inspectable] /** * Highlights the characters or words in each item in the dropdown list * that match the characters the user typed in the input field. * * @default false */ public function get emphasizeMatch():Boolean { return _emphasizeMatch; } public function set emphasizeMatch(value:Boolean):void { _emphasizeMatch = value; } /** * @private */ protected function defaultFilterFunction(element:*, index:int = 0, arr:Array = null):Boolean { var label:String = itemToLabel(element); var input:String = text.toLowerCase(); var potentialMatch:String = label.toLowerCase().substring(0,input.length); var b:Boolean = potentialMatch == input; return b; } /** * @private */ private function templateFilterFunction(element:*):Boolean { var flag:Boolean=false; if(filterFunction!=null) flag=filterFunction(element,typedText); return flag; } //-------------------------------------------------------------------------- // // Overridden methods // //-------------------------------------------------------------------------- /** * @private * Make sure the drop-down width is the same as the rest of the AutoComplete */ override protected function draw():void { super.draw(); if (!_showingDropdown && inTween) { bRemoveDropdown = true; } if (labelFieldChanged) { if (_dropdown) _dropdown.labelField = _labelField; } if (labelFunctionChanged) { if (_dropdown) _dropdown.labelFunction = _labelFunction; } } //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * @private * If keepLocalHistory is enabled, stores the text typed * by the user in the local history on the client machine */ private function addToLocalHistory():void { if ( text != null && text != "") { var so:SharedObject = SharedObject.getLocal(name + "AutoCompleteData"); var savedData : Array = so.data.suggestions; //No shared object has been created so far if (savedData == null) savedData = new Array(); var i:Number=0; var flag:Boolean=false; //Check if this entry is there in the previously saved shared object data for(i=0;i
item
parameter.
*
* This method checks in the following order to find a value to return:
* *labelFunction
property,
* returns the result of passing the item to the function.labelField
property, returns the contents of the property.