/* AutoComplete component based on Adobe original but heavily bug-fixed and stripped down http://www.adobe.com/cfusion/exchange/index.cfm?event=extensionDetail&extid=1047291 Enhancements to do: - up/down when field empty should show everything - up (to 0) when dropdown displayed should cause it to reset to previous typed value - down (past only item) when dropdown displayed should paste it - shouldn't be able to leave empty fields, or those which already exist */ package net.systemeD.controls { import flash.events.KeyboardEvent; import flash.events.Event; import flash.events.FocusEvent; import flash.events.MouseEvent; import flash.net.SharedObject; import flash.ui.Keyboard; import mx.core.UIComponent; import mx.controls.ComboBox; import mx.controls.DataGrid; import mx.controls.listClasses.ListBase; import mx.collections.ArrayCollection; import mx.collections.ListCollectionView; import mx.events.DropdownEvent; import mx.events.ListEvent; import mx.events.FlexEvent; import mx.managers.IFocusManagerComponent; [Event(name="filterFunctionChange", type="flash.events.Event")] [Event(name="typedTextChange", type="flash.events.Event")] [Exclude(name="editable", kind="property")] /** * The AutoComplete control is an enhanced * TextInput control which pops up a list of suggestions * based on characters entered by the user. These suggestions * are to be provided by setting the dataProvider * property of the control. * @mxml * *

The <fc:AutoComplete> tag inherits all the tag attributes * of its superclass, and adds the following tag attributes:

* *
	 *	<fc:AutoComplete
	 *	  Properties
	 *	  keepLocalHistory="false"
	 *	  typedText=""
	 *	  filterFunction="Internal filter function"
	 *
	 *	  Events
	 *	  filterFunctionChange="No default"
	 *	  typedTextChange="No default"
	 *	/>
	 *	
* * @includeExample ../../../../../../docs/com/adobe/flex/extras/controls/example/AutoCompleteCountriesData/AutoCompleteCountriesData.mxml * * @see mx.controls.ComboBox * */ public class AutoComplete extends ComboBox { //-------------------------------------------------------------------------- // Constructor //-------------------------------------------------------------------------- public function AutoComplete() { super(); //Make ComboBox look like a normal text field editable = true; setStyle("arrowButtonWidth",0); setStyle("fontWeight","normal"); setStyle("cornerRadius",0); setStyle("paddingLeft",0); setStyle("paddingRight",0); rowCount = 7; if (maxChars) textInput.maxChars=maxChars; } //-------------------------------------------------------------------------- // Variables //-------------------------------------------------------------------------- private var cursorPosition:Number=0; private var prevIndex:Number = -1; private var showDropdown:Boolean=false; private var showingDropdown:Boolean=false; private var tempCollection:Object; private var dropdownClosed:Boolean=true; public var maxChars:uint=0; //-------------------------------------------------------------------------- // Overridden Properties //-------------------------------------------------------------------------- override public function set editable(value:Boolean):void { //This is done to prevent user from resetting the value to false super.editable = true; } override public function set dataProvider(value:Object):void { super.dataProvider = value; tempCollection = value; // Big bug in Flex 3.5: // http://www.newtriks.com/?p=935 // http://forums.adobe.com/message/2952677 // https://bugs.adobe.com/jira/browse/SDK-25567 // https://bugs.adobe.com/jira/browse/SDK-25705 // http://stackoverflow.com/questions/3006291/adobe-flex-combobox-dataprovider // We can remove this workaround if we ever move to Flex 3.6 or Flex 4 var newDropDown:ListBase = dropdown; if(newDropDown) { validateSize(true); newDropDown.dataProvider = super.dataProvider; dropdown.addEventListener(ListEvent.ITEM_CLICK, itemClickHandler, false, 0, true); } } override public function set labelField(value:String):void { super.labelField = value; invalidateProperties(); invalidateDisplayList(); } //-------------------------------------------------------------------------- // Properties //-------------------------------------------------------------------------- private var _typedText:String=""; // text changed by user private var typedTextChanged:Boolean; [Bindable("typedTextChange")] [Inspectable(category="Data")] public function get typedText():String { return _typedText; } public function set typedText(input:String):void { _typedText = input; typedTextChanged = true; invalidateProperties(); invalidateDisplayList(); dispatchEvent(new Event("typedTextChange")); } //-------------------------------------------------------------------------- // New event listener to restore item-click //-------------------------------------------------------------------------- protected function itemClickHandler(event:ListEvent):void { typedTextChanged=false; textInput.text=itemToLabel(collection[event.rowIndex]); selectNextField(); } protected function selectNextField():void { if (this.parent.parent is DataGrid) { this.parent.parent.dispatchEvent(new FocusEvent("keyFocusChange",true,true,null,false,9)); } else { focusManager.getNextFocusManagerComponent(true).setFocus(); } } //-------------------------------------------------------------------------- // Overridden methods //-------------------------------------------------------------------------- override protected function commitProperties():void { super.commitProperties(); if (dropdown) { if (typedTextChanged) { cursorPosition = textInput.selectionBeginIndex; updateDataProvider(); if( collection.length==0 || typedText=="" || typedText==null ) { // no suggestions, so no dropdown dropdownClosed=true; showDropdown=false; showingDropdown=false; selectedIndex=-1; // nothing selected } else { // show dropdown showDropdown = true; selectedIndex = 0; // first item selected } } } else { selectedIndex=-1; } } override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void { super.updateDisplayList(unscaledWidth, unscaledHeight); if(selectedIndex == -1 && typedTextChanged && textInput.text!=typedText) { // not in menu // trace("not in menu"); trace("- restoring to "+typedText); textInput.text = typedText; textInput.setSelection(cursorPosition, cursorPosition); } else if (dropdown && typedTextChanged && textInput.text!=typedText) { // in menu, but user has typed // trace("in menu, but user has typed"); trace("- restoring to "+typedText); textInput.text = typedText; textInput.setSelection(cursorPosition, cursorPosition); } else if (showingDropdown && textInput.text==selectedLabel) { // force update if Flex has fucked up again // trace("should force update"); textInput.htmlText=selectedLabel; textInput.validateNow(); if (typedTextChanged) textInput.setSelection(cursorPosition, cursorPosition); } else if (showingDropdown && textInput.text!=selectedLabel && !typedTextChanged) { // in menu, user has navigated with cursor keys/mouse // trace("in menu, user has navigated with cursor keys/mouse"); textInput.text = selectedLabel; textInput.setSelection(0, textInput.text.length); } else if (textInput.text!="") { textInput.setSelection(cursorPosition, cursorPosition); } if (showDropdown && !dropdown.visible) { // controls the open duration of the dropdown super.open(); showDropdown = false; showingDropdown = true; dropdownClosed = false; } } override protected function keyDownHandler(event:KeyboardEvent):void { super.keyDownHandler(event); if (event.keyCode==Keyboard.UP || event.keyCode==Keyboard.DOWN) { typedTextChanged=false; } if (event.keyCode==Keyboard.ESCAPE && showingDropdown) { // ESCAPE cancels dropdown textInput.text = typedText; textInput.setSelection(textInput.text.length, textInput.text.length); showingDropdown = false; dropdownClosed=true; } else if (event.keyCode == Keyboard.ENTER) { // ENTER pressed, so select the topmost item (if it exists) if (selectedIndex>-1) { textInput.text = selectedLabel; } dropdownClosed=true; // and move on to the next field event.stopImmediatePropagation(); selectNextField(); } else if (event.ctrlKey && event.keyCode == Keyboard.UP) { dropdownClosed=true; } prevIndex = selectedIndex; } override public function getStyle(styleProp:String):* { if (styleProp != "openDuration") { return super.getStyle(styleProp); } else { if (dropdownClosed) return super.getStyle(styleProp); else return 0; } } override protected function textInput_changeHandler(event:Event):void { super.textInput_changeHandler(event); typedText = text; typedTextChanged = true; } override protected function measure():void { super.measure(); measuredWidth = mx.core.UIComponent.DEFAULT_MEASURED_WIDTH; } override public function set selectedIndex(value:int):void { var prevtext:String=text; super.selectedIndex=value; text=prevtext; } //---------------------------------- // filterFunction //---------------------------------- /** * A function that is used to select items that match the * function's criteria. * A filterFunction is expected to have the following signature: * *
f(item:~~, text:String):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, 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(item:~~, text:String):Boolean
		 *	{
		 *	   public var regExp:RegExp = new RegExp(text,"");
		 *	   return regExp.test(item);
		 *	}
		 *	
* */ private var _filterFunction:Function = defaultFilterFunction; private var filterFunctionChanged:Boolean = true; [Bindable("filterFunctionChange")] [Inspectable(category="General")] public function get filterFunction():Function { return _filterFunction; } public function set filterFunction(value:Function):void { //An empty filterFunction is allowed but not a null filterFunction if(value!=null) { _filterFunction = value; filterFunctionChanged = true; invalidateProperties(); invalidateDisplayList(); dispatchEvent(new Event("filterFunctionChange")); } else { _filterFunction = defaultFilterFunction; } } private function defaultFilterFunction(element:*, text:String):Boolean { if (!text || text=='') return false; var label:String = itemToLabel(element); return (label.toLowerCase().substring(0,text.length) == text.toLowerCase()); } private function templateFilterFunction(element:*):Boolean { var flag:Boolean=false; if(filterFunction!=null) flag=filterFunction(element,typedText); return flag; } // Updates the dataProvider used for showing suggestions private function updateDataProvider():void { dataProvider = tempCollection; collection.filterFunction = templateFilterFunction; collection.refresh(); } } }