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