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