Give AutoComplete teh snappy
[potlatch2.git] / net / systemeD / controls / AutoComplete.as
1 /*
2         AutoComplete component
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
5
6         Enhancements to do:
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
11 */
12
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 flash.utils.*;
21         import mx.core.UIComponent;
22         import mx.controls.ComboBox;
23         import mx.controls.DataGrid;
24         import mx.controls.TextInput;
25         import mx.controls.listClasses.ListBase;
26         import mx.collections.ArrayCollection;
27         import mx.collections.ListCollectionView;
28         import mx.events.DropdownEvent;
29         import mx.events.ListEvent;
30         import mx.events.FlexEvent;
31         import mx.managers.IFocusManagerComponent;
32
33         [Event(name="filterFunctionChange", type="flash.events.Event")]
34         [Event(name="typedTextChange", type="flash.events.Event")]
35
36         [Exclude(name="editable", kind="property")]
37
38         /**
39          *      The AutoComplete control is an enhanced 
40          *      TextInput control which pops up a list of suggestions 
41          *      based on characters entered by the user. These suggestions
42          *      are to be provided by setting the <code>dataProvider
43          *      </code> property of the control.
44          *      @mxml
45          *
46          *      <p>The <code>&lt;fc:AutoComplete&gt;</code> tag inherits all the tag attributes
47          *      of its superclass, and adds the following tag attributes:</p>
48          *
49          *      <pre>
50          *      &lt;fc:AutoComplete
51          *        <b>Properties</b>
52          *        keepLocalHistory="false"
53          *        typedText=""
54          *        filterFunction="<i>Internal filter function</i>"
55          *
56          *        <b>Events</b>
57          *        filterFunctionChange="<i>No default</i>"
58          *        typedTextChange="<i>No default</i>"
59          *      /&gt;
60          *      </pre>
61          *
62          *      @includeExample ../../../../../../docs/com/adobe/flex/extras/controls/example/AutoCompleteCountriesData/AutoCompleteCountriesData.mxml
63          *
64          *      @see mx.controls.ComboBox
65          *
66          */
67         public class AutoComplete extends ComboBox 
68         {
69
70                 //--------------------------------------------------------------------------
71                 //      Constructor
72                 //--------------------------------------------------------------------------
73
74                 public function AutoComplete() {
75                         super();
76
77                         //Make ComboBox look like a normal text field
78                         editable = true;
79
80                         setStyle("arrowButtonWidth",0);
81                         setStyle("fontWeight","normal");
82                         setStyle("cornerRadius",0);
83                         setStyle("paddingLeft",0);
84                         setStyle("paddingRight",0);
85                         setStyle("openDuration",0);
86                         rowCount = 7;
87                         
88                         if (maxChars) textInput.maxChars=maxChars;
89                 }
90                 
91                 //--------------------------------------------------------------------------
92                 //      Variables
93                 //--------------------------------------------------------------------------
94
95                 private var cursorPosition:Number=0;
96                 private var prevIndex:Number = -1;
97                 private var showDropdown:Boolean=false;
98                 private var showingDropdown:Boolean=false;
99                 private var tempCollection:Object;
100                 private var dropdownClosed:Boolean=true;
101                 private var dropdownTimer:uint;
102                 public var maxChars:uint=0;
103
104                 //--------------------------------------------------------------------------
105                 //      Overridden Properties
106                 //--------------------------------------------------------------------------
107
108                 override public function set editable(value:Boolean):void {
109                         //This is done to prevent user from resetting the value to false
110                         super.editable = true;
111                 }
112                 override public function set dataProvider(value:Object):void {
113                         super.dataProvider = value;
114                         tempCollection = value;
115
116                         // Big bug in Flex 3.5:
117                         //  http://www.newtriks.com/?p=935
118                         //  http://forums.adobe.com/message/2952677
119                         //  https://bugs.adobe.com/jira/browse/SDK-25567
120                         //  https://bugs.adobe.com/jira/browse/SDK-25705
121                         //  http://stackoverflow.com/questions/3006291/adobe-flex-combobox-dataprovider
122                         // We can remove this workaround if we ever move to Flex 3.6 or Flex 4
123                         var newDropDown:ListBase = dropdown;
124                         if(newDropDown) {
125                                 validateSize(true);
126                                 newDropDown.dataProvider = super.dataProvider;
127
128                                 dropdown.addEventListener(ListEvent.ITEM_CLICK, itemClickHandler, false, 0, true);
129                         }
130                 }
131
132                 override public function set labelField(value:String):void {
133                         super.labelField = value;
134                         invalidateProperties();
135                         invalidateDisplayList();
136                 }
137
138
139                 //--------------------------------------------------------------------------
140                 //      Properties
141                 //--------------------------------------------------------------------------
142
143                 private var _typedText:String="";                       // text changed by user
144                 private var typedTextChanged:Boolean;
145
146                 [Bindable("typedTextChange")]
147                 [Inspectable(category="Data")]
148                 public function get typedText():String { return _typedText; }
149
150                 public function set typedText(input:String):void {
151                         _typedText = input;
152                         typedTextChanged = true;
153                         
154                         invalidateProperties();
155                         invalidateDisplayList();
156                         dispatchEvent(new Event("typedTextChange"));
157                 }
158
159                 //--------------------------------------------------------------------------
160                 //      New event listener to restore item-click
161                 //--------------------------------------------------------------------------
162
163                 protected function itemClickHandler(event:ListEvent):void {
164                         typedTextChanged=false;
165                         textInput.text=itemToLabel(collection[event.rowIndex]);
166                         selectNextField();
167                 }
168
169                 protected function selectNextField():void {
170                         if (this.parent.parent is DataGrid) {
171                                 this.parent.parent.dispatchEvent(new FocusEvent("keyFocusChange",true,true,null,false,9));
172                         } else {
173                                 focusManager.getNextFocusManagerComponent(true).setFocus();
174                         }
175                 }
176
177                 //--------------------------------------------------------------------------
178                 //      Overridden methods
179                 //--------------------------------------------------------------------------
180
181                 override protected function commitProperties():void {
182                         super.commitProperties();
183
184                         if (dropdown) {
185                                 if (typedTextChanged) {
186                                         cursorPosition = TextInput(textInput).selectionBeginIndex;
187                                         updateDataProvider();
188
189                                         if( collection.length==0 || typedText=="" || typedText==null ) {
190                                                 // no suggestions, so no dropdown
191                                                 dropdownClosed=true;
192                                                 showDropdown=false;
193                                                 showingDropdown=false;
194                                                 selectedIndex=-1;               // nothing selected
195                                         } else {
196                                                 // show dropdown
197                                                 showDropdown = true;
198                                                 selectedIndex = 0;              // first item selected
199                                         }
200                                 }
201                         } else {
202                                 selectedIndex=-1;
203                         }
204                 }
205                 
206                 override protected function updateDisplayList(unscaledWidth:Number, 
207                                                                   unscaledHeight:Number):void {
208
209                         super.updateDisplayList(unscaledWidth, unscaledHeight);
210                         
211                         if(selectedIndex == -1 && typedTextChanged && textInput.text!=typedText) { 
212                                 // not in menu
213                                 textInput.text = typedText;
214                                 textInput.validateNow();
215                                 textInput.selectRange(cursorPosition, cursorPosition);
216                         } else if (dropdown && typedTextChanged && textInput.text!=typedText) {
217                                 // in menu, but user has typed
218                                 textInput.text = typedText;
219                                 textInput.validateNow();
220                                 textInput.selectRange(cursorPosition, cursorPosition);
221                         } else if (showingDropdown && textInput.text==selectedLabel) {
222                                 // force update if Flex has fucked up again
223                                 TextInput(textInput).htmlText=selectedLabel;
224                                 textInput.validateNow();
225                                 if (typedTextChanged) textInput.selectRange(cursorPosition, cursorPosition);
226                         } else if (showingDropdown && textInput.text!=selectedLabel && !typedTextChanged) {
227                                 // in menu, user has navigated with cursor keys/mouse
228                                 textInput.text = selectedLabel;
229                                 textInput.validateNow();
230                                 textInput.selectRange(0, textInput.text.length);
231                         } else if (textInput.text!="") {
232                                 textInput.selectRange(cursorPosition, cursorPosition);
233                         }
234
235                         if (showDropdown && !dropdown.visible) {
236                                 // controls the open duration of the dropdown
237                                 showDropdown = false;
238                                 clearTimeout(dropdownTimer);
239                                 dropdownTimer=setTimeout(openDropdown,100);
240                         }
241                 }
242
243                 private function openDropdown():void {
244                         clearTimeout(dropdownTimer);
245                         super.open();
246                         showingDropdown = true;
247                         dropdownClosed = false;
248                 }
249         
250                 override protected function keyDownHandler(event:KeyboardEvent):void {
251                         super.keyDownHandler(event);
252
253                         if (event.keyCode==Keyboard.UP || event.keyCode==Keyboard.DOWN) {
254                                 typedTextChanged=false;
255                         }
256
257                         if (event.keyCode==Keyboard.ESCAPE && showingDropdown) {
258                                 // ESCAPE cancels dropdown
259                                 textInput.text = typedText;
260                                 textInput.selectRange(textInput.text.length, textInput.text.length);
261                                 showingDropdown = false;
262                                 dropdownClosed=true;
263
264                         } else if (event.keyCode == Keyboard.ENTER) {
265                                 // ENTER pressed, so select the topmost item (if it exists)
266                                 if (selectedIndex>-1) { textInput.text = selectedLabel; }
267                                 dropdownClosed=true;
268                                 
269                                 // and move on to the next field
270                                 event.stopImmediatePropagation();
271                                 selectNextField();
272
273                         } else if (event.ctrlKey && event.keyCode == Keyboard.UP) {
274                                 dropdownClosed=true;
275                         }
276                 
277                         prevIndex = selectedIndex;
278                 }
279         
280                 override public function getStyle(styleProp:String):* {
281                         if (styleProp != "openDuration") {
282                                 return super.getStyle(styleProp);
283                         } else {
284                                 if (dropdownClosed) return super.getStyle(styleProp);
285                                 else return 0;
286                         }
287                 }
288
289                 override protected function textInput_changeHandler(event:Event):void {
290                         super.textInput_changeHandler(event);
291                         typedText = text;
292                         typedTextChanged = true;
293                 }
294
295                 override protected function measure():void {
296                         super.measure();
297                         measuredWidth = mx.core.UIComponent.DEFAULT_MEASURED_WIDTH;
298                 }
299
300                 override public function set selectedIndex(value:int):void {
301                         var prevtext:String=text;
302                         super.selectedIndex=value;
303                         text=prevtext;
304                 }
305
306
307                 //----------------------------------
308                 //      filterFunction
309                 //----------------------------------
310                 /**
311                  *      A function that is used to select items that match the
312                  *      function's criteria. 
313                  *      A filterFunction is expected to have the following signature:
314                  *
315                  *      <pre>f(item:~~, text:String):Boolean</pre>
316                  *
317                  *      where the return value is <code>true</code> if the specified item
318                  *      should displayed as a suggestion. 
319                  *      Whenever there is a change in text in the AutoComplete control, this 
320                  *      filterFunction is run on each item in the <code>dataProvider</code>.
321                  *      
322                  *      <p>The default implementation for filterFunction works as follows:<br>
323                  *      If "AB" has been typed, it will display all the items matching 
324                  *      "AB~~" (ABaa, ABcc, abAc etc.).</p>
325                  *
326                  *      <p>An example usage of a customized filterFunction is when text typed
327                  *      is a regular expression and we want to display all the
328                  *      items which come in the set.</p>
329                  *
330                  *      @example
331                  *      <pre>
332                  *      public function myFilterFunction(item:~~, text:String):Boolean
333                  *      {
334                  *         public var regExp:RegExp = new RegExp(text,"");
335                  *         return regExp.test(item);
336                  *      }
337                  *      </pre>
338                  *
339                  */
340
341                 private var _filterFunction:Function = defaultFilterFunction;
342                 private var filterFunctionChanged:Boolean = true;
343
344                 [Bindable("filterFunctionChange")]
345                 [Inspectable(category="General")]
346
347                 public function get filterFunction():Function {
348                         return _filterFunction;
349                 }
350
351                 public function set filterFunction(value:Function):void {
352                         //An empty filterFunction is allowed but not a null filterFunction
353                         if(value!=null) {
354                                 _filterFunction = value;
355                                 filterFunctionChanged = true;
356
357                                 invalidateProperties();
358                                 invalidateDisplayList();
359         
360                                 dispatchEvent(new Event("filterFunctionChange"));
361                         } else {
362                                 _filterFunction = defaultFilterFunction;
363                         }
364                 }
365                                 
366                 private function defaultFilterFunction(element:*, text:String):Boolean {
367                         if (!text || text=='') return false;
368                         var label:String = itemToLabel(element);
369                         return (label.toLowerCase().substring(0,text.length) == text.toLowerCase());
370                 }
371
372                 private function templateFilterFunction(element:*):Boolean {
373                         var flag:Boolean=false;
374                         if(filterFunction!=null)
375                                 flag=filterFunction(element,typedText);
376                         return flag;
377                 }
378
379                 // Updates the dataProvider used for showing suggestions
380                 private function updateDataProvider():void {
381                         dataProvider = tempCollection;
382                         collection.filterFunction = templateFilterFunction;
383                         collection.refresh();
384                 }
385         }       
386 }