3 based on Adobe original but heavily bug-fixed and stripped down
4 http://www.adobe.com/cfusion/exchange/index.cfm?event=extensionDetail&extid=1047291
7 - up/down when field empty should show everything
8 - up (to 0) when dropdown displayed should cause it to reset to previous typed value
9 - down (past only item) when dropdown displayed should paste it
10 - shouldn't be able to leave empty fields, or those which already exist
13 package net.systemeD.controls {
14 import flash.events.KeyboardEvent;
15 import flash.events.Event;
16 import flash.events.FocusEvent;
17 import flash.events.MouseEvent;
18 import flash.net.SharedObject;
19 import flash.ui.Keyboard;
20 import mx.core.UIComponent;
21 import mx.controls.ComboBox;
22 import mx.controls.DataGrid;
23 import mx.controls.listClasses.ListBase;
24 import mx.collections.ArrayCollection;
25 import mx.collections.ListCollectionView;
26 import mx.events.DropdownEvent;
27 import mx.events.ListEvent;
28 import mx.events.FlexEvent;
29 import mx.managers.IFocusManagerComponent;
31 [Event(name="filterFunctionChange", type="flash.events.Event")]
32 [Event(name="typedTextChange", type="flash.events.Event")]
34 [Exclude(name="editable", kind="property")]
37 * The AutoComplete control is an enhanced
38 * TextInput control which pops up a list of suggestions
39 * based on characters entered by the user. These suggestions
40 * are to be provided by setting the <code>dataProvider
41 * </code> property of the control.
44 * <p>The <code><fc:AutoComplete></code> tag inherits all the tag attributes
45 * of its superclass, and adds the following tag attributes:</p>
50 * keepLocalHistory="false"
52 * filterFunction="<i>Internal filter function</i>"
55 * filterFunctionChange="<i>No default</i>"
56 * typedTextChange="<i>No default</i>"
60 * @includeExample ../../../../../../docs/com/adobe/flex/extras/controls/example/AutoCompleteCountriesData/AutoCompleteCountriesData.mxml
62 * @see mx.controls.ComboBox
65 public class AutoComplete extends ComboBox
68 //--------------------------------------------------------------------------
70 //--------------------------------------------------------------------------
72 public function AutoComplete() {
75 //Make ComboBox look like a normal text field
78 setStyle("arrowButtonWidth",0);
79 setStyle("fontWeight","normal");
80 setStyle("cornerRadius",0);
81 setStyle("paddingLeft",0);
82 setStyle("paddingRight",0);
85 if (maxChars) textInput.maxChars=maxChars;
88 //--------------------------------------------------------------------------
90 //--------------------------------------------------------------------------
92 private var cursorPosition:Number=0;
93 private var prevIndex:Number = -1;
94 private var showDropdown:Boolean=false;
95 private var showingDropdown:Boolean=false;
96 private var tempCollection:Object;
97 private var dropdownClosed:Boolean=true;
98 public var maxChars:uint=0;
100 //--------------------------------------------------------------------------
101 // Overridden Properties
102 //--------------------------------------------------------------------------
104 override public function set editable(value:Boolean):void {
105 //This is done to prevent user from resetting the value to false
106 super.editable = true;
108 override public function set dataProvider(value:Object):void {
109 super.dataProvider = value;
110 tempCollection = value;
112 // Big bug in Flex 3.5:
113 // http://www.newtriks.com/?p=935
114 // http://forums.adobe.com/message/2952677
115 // https://bugs.adobe.com/jira/browse/SDK-25567
116 // https://bugs.adobe.com/jira/browse/SDK-25705
117 // http://stackoverflow.com/questions/3006291/adobe-flex-combobox-dataprovider
118 // We can remove this workaround if we ever move to Flex 3.6 or Flex 4
119 var newDropDown:ListBase = dropdown;
122 newDropDown.dataProvider = super.dataProvider;
124 dropdown.addEventListener(ListEvent.ITEM_CLICK, itemClickHandler, false, 0, true);
128 override public function set labelField(value:String):void {
129 super.labelField = value;
130 invalidateProperties();
131 invalidateDisplayList();
135 //--------------------------------------------------------------------------
137 //--------------------------------------------------------------------------
139 private var _typedText:String=""; // text changed by user
140 private var typedTextChanged:Boolean;
142 [Bindable("typedTextChange")]
143 [Inspectable(category="Data")]
144 public function get typedText():String { return _typedText; }
146 public function set typedText(input:String):void {
148 typedTextChanged = true;
150 invalidateProperties();
151 invalidateDisplayList();
152 dispatchEvent(new Event("typedTextChange"));
155 //--------------------------------------------------------------------------
156 // New event listener to restore item-click
157 //--------------------------------------------------------------------------
159 protected function itemClickHandler(event:ListEvent):void {
160 typedTextChanged=false;
161 textInput.text=itemToLabel(collection[event.rowIndex]);
165 protected function selectNextField():void {
166 if (this.parent.parent is DataGrid) {
167 this.parent.parent.dispatchEvent(new FocusEvent("keyFocusChange",true,true,null,false,9));
169 focusManager.getNextFocusManagerComponent(true).setFocus();
173 //--------------------------------------------------------------------------
174 // Overridden methods
175 //--------------------------------------------------------------------------
177 override protected function commitProperties():void {
178 super.commitProperties();
181 if (typedTextChanged) {
182 cursorPosition = textInput.selectionBeginIndex;
183 updateDataProvider();
185 if( collection.length==0 || typedText=="" || typedText==null ) {
186 // no suggestions, so no dropdown
189 showingDropdown=false;
190 selectedIndex=-1; // nothing selected
194 selectedIndex = 0; // first item selected
202 override protected function updateDisplayList(unscaledWidth:Number,
203 unscaledHeight:Number):void {
205 super.updateDisplayList(unscaledWidth, unscaledHeight);
207 if(selectedIndex == -1 && typedTextChanged && textInput.text!=typedText) {
209 // trace("not in menu"); trace("- restoring to "+typedText);
210 textInput.text = typedText;
211 textInput.setSelection(cursorPosition, cursorPosition);
212 } else if (dropdown && typedTextChanged && textInput.text!=typedText) {
213 // in menu, but user has typed
214 // trace("in menu, but user has typed"); trace("- restoring to "+typedText);
215 textInput.text = typedText;
216 textInput.setSelection(cursorPosition, cursorPosition);
217 } else if (showingDropdown && textInput.text==selectedLabel) {
218 // force update if Flex has fucked up again
219 // trace("should force update");
220 textInput.htmlText=selectedLabel;
221 textInput.validateNow();
222 if (typedTextChanged) textInput.setSelection(cursorPosition, cursorPosition);
223 } else if (showingDropdown && textInput.text!=selectedLabel && !typedTextChanged) {
224 // in menu, user has navigated with cursor keys/mouse
225 // trace("in menu, user has navigated with cursor keys/mouse");
226 textInput.text = selectedLabel;
227 textInput.setSelection(0, textInput.text.length);
228 } else if (textInput.text!="") {
229 textInput.setSelection(cursorPosition, cursorPosition);
232 if (showDropdown && !dropdown.visible) {
233 // controls the open duration of the dropdown
235 showDropdown = false;
236 showingDropdown = true;
237 dropdownClosed = false;
241 override protected function keyDownHandler(event:KeyboardEvent):void {
242 super.keyDownHandler(event);
244 if (event.keyCode==Keyboard.UP || event.keyCode==Keyboard.DOWN) {
245 typedTextChanged=false;
248 if (event.keyCode==Keyboard.ESCAPE && showingDropdown) {
249 // ESCAPE cancels dropdown
250 textInput.text = typedText;
251 textInput.setSelection(textInput.text.length, textInput.text.length);
252 showingDropdown = false;
255 } else if (event.keyCode == Keyboard.ENTER) {
256 // ENTER pressed, so select the topmost item (if it exists)
257 if (selectedIndex>-1) { textInput.text = selectedLabel; }
260 // and move on to the next field
261 event.stopImmediatePropagation();
264 } else if (event.ctrlKey && event.keyCode == Keyboard.UP) {
268 prevIndex = selectedIndex;
271 override public function getStyle(styleProp:String):* {
272 if (styleProp != "openDuration") {
273 return super.getStyle(styleProp);
275 if (dropdownClosed) return super.getStyle(styleProp);
280 override protected function textInput_changeHandler(event:Event):void {
281 super.textInput_changeHandler(event);
283 typedTextChanged = true;
286 override protected function measure():void {
288 measuredWidth = mx.core.UIComponent.DEFAULT_MEASURED_WIDTH;
291 override public function set selectedIndex(value:int):void {
292 var prevtext:String=text;
293 super.selectedIndex=value;
298 //----------------------------------
300 //----------------------------------
302 * A function that is used to select items that match the
303 * function's criteria.
304 * A filterFunction is expected to have the following signature:
306 * <pre>f(item:~~, text:String):Boolean</pre>
308 * where the return value is <code>true</code> if the specified item
309 * should displayed as a suggestion.
310 * Whenever there is a change in text in the AutoComplete control, this
311 * filterFunction is run on each item in the <code>dataProvider</code>.
313 * <p>The default implementation for filterFunction works as follows:<br>
314 * If "AB" has been typed, it will display all the items matching
315 * "AB~~" (ABaa, ABcc, abAc etc.).</p>
317 * <p>An example usage of a customized filterFunction is when text typed
318 * is a regular expression and we want to display all the
319 * items which come in the set.</p>
323 * public function myFilterFunction(item:~~, text:String):Boolean
325 * public var regExp:RegExp = new RegExp(text,"");
326 * return regExp.test(item);
332 private var _filterFunction:Function = defaultFilterFunction;
333 private var filterFunctionChanged:Boolean = true;
335 [Bindable("filterFunctionChange")]
336 [Inspectable(category="General")]
338 public function get filterFunction():Function {
339 return _filterFunction;
342 public function set filterFunction(value:Function):void {
343 //An empty filterFunction is allowed but not a null filterFunction
345 _filterFunction = value;
346 filterFunctionChanged = true;
348 invalidateProperties();
349 invalidateDisplayList();
351 dispatchEvent(new Event("filterFunctionChange"));
353 _filterFunction = defaultFilterFunction;
357 private function defaultFilterFunction(element:*, text:String):Boolean {
358 if (!text || text=='') return false;
359 var label:String = itemToLabel(element);
360 return (label.toLowerCase().substring(0,text.length) == text.toLowerCase());
363 private function templateFilterFunction(element:*):Boolean {
364 var flag:Boolean=false;
365 if(filterFunction!=null)
366 flag=filterFunction(element,typedText);
370 // Updates the dataProvider used for showing suggestions
371 private function updateDataProvider():void {
372 dataProvider = tempCollection;
373 collection.filterFunction = templateFilterFunction;
374 collection.refresh();