Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / form / field / ComboBox.js
1 /**
2  * @class Ext.form.field.ComboBox
3  * @extends Ext.form.field.Picker
4  *
5  * A combobox control with support for autocomplete, remote loading, and many other features.
6  *
7  * A ComboBox is like a combination of a traditional HTML text `<input>` field and a `<select>`
8  * field; the user is able to type freely into the field, and/or pick values from a dropdown selection
9  * list. The user can input any value by default, even if it does not appear in the selection list;
10  * to prevent free-form values and restrict them to items in the list, set {@link #forceSelection} to `true`.
11  *
12  * The selection list's options are populated from any {@link Ext.data.Store}, including remote
13  * stores. The data items in the store are mapped to each option's displayed text and backing value via
14  * the {@link #valueField} and {@link #displayField} configurations, respectively.
15  *
16  * If your store is not remote, i.e. it depends only on local data and is loaded up front, you should be
17  * sure to set the {@link #queryMode} to `'local'`, as this will improve responsiveness for the user.
18  *
19  * {@img Ext.form.ComboBox/Ext.form.ComboBox.png Ext.form.ComboBox component}
20  *
21  * ## Example usage:
22  *
23  *     // The data store containing the list of states
24  *     var states = Ext.create('Ext.data.Store', {
25  *         fields: ['abbr', 'name'],
26  *         data : [
27  *             {"abbr":"AL", "name":"Alabama"},
28  *             {"abbr":"AK", "name":"Alaska"},
29  *             {"abbr":"AZ", "name":"Arizona"}
30  *             //...
31  *         ]
32  *     });
33  *
34  *     // Create the combo box, attached to the states data store
35  *     Ext.create('Ext.form.ComboBox', {
36  *         fieldLabel: 'Choose State',
37  *         store: states,
38  *         queryMode: 'local',
39  *         displayField: 'name',
40  *         valueField: 'abbr',
41  *         renderTo: Ext.getBody()
42  *     });
43  *
44  * ## Events
45  *
46  * To do something when something in ComboBox is selected, configure the select event:
47  *
48  *     var cb = Ext.create('Ext.form.ComboBox', {
49  *         // all of your config options
50  *         listeners:{
51  *              scope: yourScope,
52  *              'select': yourFunction
53  *         }
54  *     });
55  *
56  *     // Alternatively, you can assign events after the object is created:
57  *     var cb = new Ext.form.field.ComboBox(yourOptions);
58  *     cb.on('select', yourFunction, yourScope);
59  *
60  * ## Multiple Selection
61  *
62  * ComboBox also allows selection of multiple items from the list; to enable multi-selection set the
63  * {@link #multiSelect} config to `true`.
64  *
65  * @constructor
66  * Create a new ComboBox.
67  * @param {Object} config Configuration options
68  * @xtype combo
69  * @docauthor Jason Johnston <jason@sencha.com>
70  */
71 Ext.define('Ext.form.field.ComboBox', {
72     extend:'Ext.form.field.Picker',
73     requires: ['Ext.util.DelayedTask', 'Ext.EventObject', 'Ext.view.BoundList', 'Ext.view.BoundListKeyNav', 'Ext.data.StoreManager'],
74     alternateClassName: 'Ext.form.ComboBox',
75     alias: ['widget.combobox', 'widget.combo'],
76
77     /**
78      * @cfg {String} triggerCls
79      * An additional CSS class used to style the trigger button. The trigger will always get the
80      * {@link #triggerBaseCls} by default and <tt>triggerCls</tt> will be <b>appended</b> if specified.
81      * Defaults to 'x-form-arrow-trigger' for ComboBox.
82      */
83     triggerCls: Ext.baseCSSPrefix + 'form-arrow-trigger',
84
85     /**
86      * @cfg {Ext.data.Store/Array} store The data source to which this combo is bound (defaults to <tt>undefined</tt>).
87      * Acceptable values for this property are:
88      * <div class="mdetail-params"><ul>
89      * <li><b>any {@link Ext.data.Store Store} subclass</b></li>
90      * <li><b>an Array</b> : Arrays will be converted to a {@link Ext.data.Store} internally,
91      * automatically generating {@link Ext.data.Field#name field names} to work with all data components.
92      * <div class="mdetail-params"><ul>
93      * <li><b>1-dimensional array</b> : (e.g., <tt>['Foo','Bar']</tt>)<div class="sub-desc">
94      * A 1-dimensional array will automatically be expanded (each array item will be used for both the combo
95      * {@link #valueField} and {@link #displayField})</div></li>
96      * <li><b>2-dimensional array</b> : (e.g., <tt>[['f','Foo'],['b','Bar']]</tt>)<div class="sub-desc">
97      * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
98      * {@link #valueField}, while the value at index 1 is assumed to be the combo {@link #displayField}.
99      * </div></li></ul></div></li></ul></div>
100      * <p>See also <tt>{@link #queryMode}</tt>.</p>
101      */
102
103     /**
104      * @cfg {Boolean} multiSelect
105      * If set to <tt>true</tt>, allows the combo field to hold more than one value at a time, and allows selecting
106      * multiple items from the dropdown list. The combo's text field will show all selected values separated by
107      * the {@link #delimiter}. (Defaults to <tt>false</tt>.)
108      */
109     multiSelect: false,
110
111     /**
112      * @cfg {String} delimiter
113      * The character(s) used to separate the {@link #displayField display values} of multiple selected items
114      * when <tt>{@link #multiSelect} = true</tt>. Defaults to <tt>', '</tt>.
115      */
116     delimiter: ', ',
117
118     /**
119      * @cfg {String} displayField The underlying {@link Ext.data.Field#name data field name} to bind to this
120      * ComboBox (defaults to 'text').
121      * <p>See also <tt>{@link #valueField}</tt>.</p>
122      */
123     displayField: 'text',
124
125     /**
126      * @cfg {String} valueField
127      * @required
128      * The underlying {@link Ext.data.Field#name data value name} to bind to this ComboBox (defaults to match
129      * the value of the {@link #displayField} config).
130      * <p><b>Note</b>: use of a <tt>valueField</tt> requires the user to make a selection in order for a value to be
131      * mapped. See also <tt>{@link #displayField}</tt>.</p>
132      */
133
134     /**
135      * @cfg {String} triggerAction The action to execute when the trigger is clicked.
136      * <div class="mdetail-params"><ul>
137      * <li><b><tt>'all'</tt></b> : <b>Default</b>
138      * <p class="sub-desc">{@link #doQuery run the query} specified by the <tt>{@link #allQuery}</tt> config option</p></li>
139      * <li><b><tt>'query'</tt></b> :
140      * <p class="sub-desc">{@link #doQuery run the query} using the {@link Ext.form.field.Base#getRawValue raw value}.</p></li>
141      * </ul></div>
142      * <p>See also <code>{@link #queryParam}</code>.</p>
143      */
144     triggerAction: 'all',
145
146     /**
147      * @cfg {String} allQuery The text query to send to the server to return all records for the list
148      * with no filtering (defaults to '')
149      */
150     allQuery: '',
151
152     /**
153      * @cfg {String} queryParam Name of the query ({@link Ext.data.proxy.Proxy#extraParam extraParam} name for the store)
154      * as it will be passed on the querystring (defaults to <tt>'query'</tt>). If explicitly set to a falsey value it will
155      * not be send.
156      */
157     queryParam: 'query',
158
159     /**
160      * @cfg {String} queryMode
161      * The mode for queries. Acceptable values are:
162      * <div class="mdetail-params"><ul>
163      * <li><b><tt>'remote'</tt></b> : <b>Default</b>
164      * <p class="sub-desc">Automatically loads the <tt>{@link #store}</tt> the <b>first</b> time the trigger
165      * is clicked. If you do not want the store to be automatically loaded the first time the trigger is
166      * clicked, set to <tt>'local'</tt> and manually load the store.  To force a requery of the store
167      * <b>every</b> time the trigger is clicked see <tt>{@link #lastQuery}</tt>.</p></li>
168      * <li><b><tt>'local'</tt></b> :
169      * <p class="sub-desc">ComboBox loads local data</p>
170      * <pre><code>
171 var combo = new Ext.form.field.ComboBox({
172     renderTo: document.body,
173     queryMode: 'local',
174     store: new Ext.data.ArrayStore({
175         id: 0,
176         fields: [
177             'myId',  // numeric value is the key
178             'displayText'
179         ],
180         data: [[1, 'item1'], [2, 'item2']]  // data is local
181     }),
182     valueField: 'myId',
183     displayField: 'displayText',
184     triggerAction: 'all'
185 });
186      * </code></pre></li>
187      * </ul></div>
188      */
189     queryMode: 'remote',
190
191     queryCaching: true,
192
193     /**
194      * @cfg {Number} pageSize If greater than <tt>0</tt>, a {@link Ext.toolbar.Paging} is displayed in the
195      * footer of the dropdown list and the {@link #doQuery filter queries} will execute with page start and
196      * {@link Ext.toolbar.Paging#pageSize limit} parameters. Only applies when <tt>{@link #queryMode} = 'remote'</tt>
197      * (defaults to <tt>0</tt>).
198      */
199     pageSize: 0,
200
201     /**
202      * @cfg {Number} queryDelay The length of time in milliseconds to delay between the start of typing and
203      * sending the query to filter the dropdown list (defaults to <tt>500</tt> if <tt>{@link #queryMode} = 'remote'</tt>
204      * or <tt>10</tt> if <tt>{@link #queryMode} = 'local'</tt>)
205      */
206
207     /**
208      * @cfg {Number} minChars The minimum number of characters the user must type before autocomplete and
209      * {@link #typeAhead} activate (defaults to <tt>4</tt> if <tt>{@link #queryMode} = 'remote'</tt> or <tt>0</tt> if
210      * <tt>{@link #queryMode} = 'local'</tt>, does not apply if <tt>{@link Ext.form.field.Trigger#editable editable} = false</tt>).
211      */
212
213     /**
214      * @cfg {Boolean} autoSelect <tt>true</tt> to automatically highlight the first result gathered by the data store
215      * in the dropdown list when it is opened. (Defaults to <tt>true</tt>). A false value would cause nothing in the
216      * list to be highlighted automatically, so the user would have to manually highlight an item before pressing
217      * the enter or {@link #selectOnTab tab} key to select it (unless the value of ({@link #typeAhead}) were true),
218      * or use the mouse to select a value.
219      */
220     autoSelect: true,
221
222     /**
223      * @cfg {Boolean} typeAhead <tt>true</tt> to populate and autoselect the remainder of the text being
224      * typed after a configurable delay ({@link #typeAheadDelay}) if it matches a known value (defaults
225      * to <tt>false</tt>)
226      */
227     typeAhead: false,
228
229     /**
230      * @cfg {Number} typeAheadDelay The length of time in milliseconds to wait until the typeahead text is displayed
231      * if <tt>{@link #typeAhead} = true</tt> (defaults to <tt>250</tt>)
232      */
233     typeAheadDelay: 250,
234
235     /**
236      * @cfg {Boolean} selectOnTab
237      * Whether the Tab key should select the currently highlighted item. Defaults to <tt>true</tt>.
238      */
239     selectOnTab: true,
240
241     /**
242      * @cfg {Boolean} forceSelection <tt>true</tt> to restrict the selected value to one of the values in the list,
243      * <tt>false</tt> to allow the user to set arbitrary text into the field (defaults to <tt>false</tt>)
244      */
245     forceSelection: false,
246
247     /**
248      * @cfg {String} valueNotFoundText When using a name/value combo, if the value passed to setValue is not found in
249      * the store, valueNotFoundText will be displayed as the field text if defined (defaults to undefined). If this
250      * default text is used, it means there is no value set and no validation will occur on this field.
251      */
252
253     /**
254      * The value of the match string used to filter the store. Delete this property to force a requery.
255      * Example use:
256      * <pre><code>
257 var combo = new Ext.form.field.ComboBox({
258     ...
259     queryMode: 'remote',
260     listeners: {
261         // delete the previous query in the beforequery event or set
262         // combo.lastQuery = null (this will reload the store the next time it expands)
263         beforequery: function(qe){
264             delete qe.combo.lastQuery;
265         }
266     }
267 });
268      * </code></pre>
269      * To make sure the filter in the store is not cleared the first time the ComboBox trigger is used
270      * configure the combo with <tt>lastQuery=''</tt>. Example use:
271      * <pre><code>
272 var combo = new Ext.form.field.ComboBox({
273     ...
274     queryMode: 'local',
275     triggerAction: 'all',
276     lastQuery: ''
277 });
278      * </code></pre>
279      * @property lastQuery
280      * @type String
281      */
282
283     /**
284      * @cfg {Object} defaultListConfig
285      * Set of options that will be used as defaults for the user-configured {@link #listConfig} object.
286      */
287     defaultListConfig: {
288         emptyText: '',
289         loadingText: 'Loading...',
290         loadingHeight: 70,
291         minWidth: 70,
292         maxHeight: 300,
293         shadow: 'sides'
294     },
295
296     /**
297      * @cfg {Mixed} transform
298      * The id, DOM node or {@link Ext.core.Element} of an existing HTML <tt>&lt;select&gt;</tt> element to
299      * convert into a ComboBox. The target select's options will be used to build the options in the ComboBox
300      * dropdown; a configured {@link #store} will take precedence over this.
301      */
302
303     /**
304      * @cfg {Object} listConfig
305      * <p>An optional set of configuration properties that will be passed to the {@link Ext.view.BoundList}'s
306      * constructor. Any configuration that is valid for BoundList can be included. Some of the more useful
307      * ones are:</p>
308      * <ul>
309      *     <li>{@link Ext.view.BoundList#cls} - defaults to empty</li>
310      *     <li>{@link Ext.view.BoundList#emptyText} - defaults to empty string</li>
311      *     <li>{@link Ext.view.BoundList#getInnerTpl} - defaults to the template defined in BoundList</li>
312      *     <li>{@link Ext.view.BoundList#itemSelector} - defaults to the value defined in BoundList</li>
313      *     <li>{@link Ext.view.BoundList#loadingText} - defaults to <tt>'Loading...'</tt></li>
314      *     <li>{@link Ext.view.BoundList#minWidth} - defaults to <tt>70</tt></li>
315      *     <li>{@link Ext.view.BoundList#maxWidth} - defaults to <tt>undefined</tt></li>
316      *     <li>{@link Ext.view.BoundList#maxHeight} - defaults to <tt>300</tt></li>
317      *     <li>{@link Ext.view.BoundList#resizable} - defaults to <tt>false</tt></li>
318      *     <li>{@link Ext.view.BoundList#shadow} - defaults to <tt>'sides'</tt></li>
319      *     <li>{@link Ext.view.BoundList#width} - defaults to <tt>undefined</tt> (automatically set to the width
320      *         of the ComboBox field if {@link #matchFieldWidth} is true)</li>
321      * </ul>
322      */
323
324     //private
325     ignoreSelection: 0,
326
327     initComponent: function() {
328         var me = this,
329             isDefined = Ext.isDefined,
330             store = me.store,
331             transform = me.transform,
332             transformSelect, isLocalMode;
333
334         //<debug>
335         if (!store && !transform) {
336             Ext.Error.raise('Either a valid store, or a HTML select to transform, must be configured on the combo.');
337         }
338         if (me.typeAhead && me.multiSelect) {
339             Ext.Error.raise('typeAhead and multiSelect are mutually exclusive options -- please remove one of them.');
340         }
341         if (me.typeAhead && !me.editable) {
342             Ext.Error.raise('If typeAhead is enabled the combo must be editable: true -- please change one of those settings.');
343         }
344         if (me.selectOnFocus && !me.editable) {
345             Ext.Error.raise('If selectOnFocus is enabled the combo must be editable: true -- please change one of those settings.');
346         }
347         //</debug>
348
349         this.addEvents(
350             // TODO need beforeselect?
351
352             /**
353              * @event beforequery
354              * Fires before all queries are processed. Return false to cancel the query or set the queryEvent's
355              * cancel property to true.
356              * @param {Object} queryEvent An object that has these properties:<ul>
357              * <li><code>combo</code> : Ext.form.field.ComboBox <div class="sub-desc">This combo box</div></li>
358              * <li><code>query</code> : String <div class="sub-desc">The query string</div></li>
359              * <li><code>forceAll</code> : Boolean <div class="sub-desc">True to force "all" query</div></li>
360              * <li><code>cancel</code> : Boolean <div class="sub-desc">Set to true to cancel the query</div></li>
361              * </ul>
362              */
363             'beforequery',
364
365             /*
366              * @event select
367              * Fires when at least one list item is selected.
368              * @param {Ext.form.field.ComboBox} combo This combo box
369              * @param {Array} records The selected records
370              */
371             'select'
372         );
373
374         // Build store from 'transform' HTML select element's options
375         if (!store && transform) {
376             transformSelect = Ext.getDom(transform);
377             if (transformSelect) {
378                 store = Ext.Array.map(Ext.Array.from(transformSelect.options), function(option) {
379                     return [option.value, option.text];
380                 });
381                 if (!me.name) {
382                     me.name = transformSelect.name;
383                 }
384                 if (!('value' in me)) {
385                     me.value = transformSelect.value;
386                 }
387             }
388         }
389
390         me.bindStore(store, true);
391         store = me.store;
392         if (store.autoCreated) {
393             me.queryMode = 'local';
394             me.valueField = me.displayField = 'field1';
395             if (!store.expanded) {
396                 me.displayField = 'field2';
397             }
398         }
399
400
401         if (!isDefined(me.valueField)) {
402             me.valueField = me.displayField;
403         }
404
405         isLocalMode = me.queryMode === 'local';
406         if (!isDefined(me.queryDelay)) {
407             me.queryDelay = isLocalMode ? 10 : 500;
408         }
409         if (!isDefined(me.minChars)) {
410             me.minChars = isLocalMode ? 0 : 4;
411         }
412
413         if (!me.displayTpl) {
414             me.displayTpl = Ext.create('Ext.XTemplate',
415                 '<tpl for=".">' +
416                     '{[typeof values === "string" ? values : values.' + me.displayField + ']}' +
417                     '<tpl if="xindex < xcount">' + me.delimiter + '</tpl>' +
418                 '</tpl>'
419             );
420         } else if (Ext.isString(me.displayTpl)) {
421             me.displayTpl = Ext.create('Ext.XTemplate', me.displayTpl);
422         }
423
424         me.callParent();
425
426         me.doQueryTask = Ext.create('Ext.util.DelayedTask', me.doRawQuery, me);
427
428         // store has already been loaded, setValue
429         if (me.store.getCount() > 0) {
430             me.setValue(me.value);
431         }
432
433         // render in place of 'transform' select
434         if (transformSelect) {
435             me.render(transformSelect.parentNode, transformSelect);
436             Ext.removeNode(transformSelect);
437             delete me.renderTo;
438         }
439     },
440
441     beforeBlur: function() {
442         var me = this;
443         me.doQueryTask.cancel();
444         if (me.forceSelection) {
445             me.assertValue();
446         } else {
447             me.collapse();
448         }
449     },
450
451     // private
452     assertValue: function() {
453         var me = this,
454             value = me.getRawValue(),
455             rec;
456
457         if (me.multiSelect) {
458             // For multiselect, check that the current displayed value matches the current
459             // selection, if it does not then revert to the most recent selection.
460             if (value !== me.getDisplayValue()) {
461                 me.setValue(me.lastSelection);
462             }
463         } else {
464             // For single-select, match the displayed value to a record and select it,
465             // if it does not match a record then revert to the most recent selection.
466             rec = me.findRecordByDisplay(value);
467             if (rec) {
468                 me.select(rec);
469             } else {
470                 me.setValue(me.lastSelection);
471             }
472         }
473         me.collapse();
474     },
475
476     onTypeAhead: function() {
477         var me = this,
478             displayField = me.displayField,
479             record = me.store.findRecord(displayField, me.getRawValue()),
480             boundList = me.getPicker(),
481             newValue, len, selStart;
482
483         if (record) {
484             newValue = record.get(displayField);
485             len = newValue.length;
486             selStart = me.getRawValue().length;
487
488             boundList.highlightItem(boundList.getNode(record));
489
490             if (selStart !== 0 && selStart !== len) {
491                 me.setRawValue(newValue);
492                 me.selectText(selStart, newValue.length);
493             }
494         }
495     },
496
497     // invoked when a different store is bound to this combo
498     // than the original
499     resetToDefault: function() {
500
501     },
502
503     bindStore: function(store, initial) {
504         var me = this,
505             oldStore = me.store;
506
507         // this code directly accesses this.picker, bc invoking getPicker
508         // would create it when we may be preping to destroy it
509         if (oldStore && !initial) {
510             if (oldStore !== store && oldStore.autoDestroy) {
511                 oldStore.destroy();
512             } else {
513                 oldStore.un({
514                     scope: me,
515                     load: me.onLoad,
516                     exception: me.collapse
517                 });
518             }
519             if (!store) {
520                 me.store = null;
521                 if (me.picker) {
522                     me.picker.bindStore(null);
523                 }
524             }
525         }
526         if (store) {
527             if (!initial) {
528                 me.resetToDefault();
529             }
530
531             me.store = Ext.data.StoreManager.lookup(store);
532             me.store.on({
533                 scope: me,
534                 load: me.onLoad,
535                 exception: me.collapse
536             });
537
538             if (me.picker) {
539                 me.picker.bindStore(store);
540             }
541         }
542     },
543
544     onLoad: function() {
545         var me = this,
546             value = me.value;
547
548         me.syncSelection();
549         if (me.picker && !me.picker.getSelectionModel().hasSelection()) {
550             me.doAutoSelect();
551         }
552     },
553
554     /**
555      * @private
556      * Execute the query with the raw contents within the textfield.
557      */
558     doRawQuery: function() {
559         this.doQuery(this.getRawValue());
560     },
561
562     /**
563      * Executes a query to filter the dropdown list. Fires the {@link #beforequery} event prior to performing the
564      * query allowing the query action to be canceled if needed.
565      * @param {String} queryString The SQL query to execute
566      * @param {Boolean} forceAll <tt>true</tt> to force the query to execute even if there are currently fewer
567      * characters in the field than the minimum specified by the <tt>{@link #minChars}</tt> config option.  It
568      * also clears any filter previously saved in the current store (defaults to <tt>false</tt>)
569      * @return {Boolean} true if the query was permitted to run, false if it was cancelled by a {@link #beforequery} handler.
570      */
571     doQuery: function(queryString, forceAll) {
572         queryString = queryString || '';
573
574         // store in object and pass by reference in 'beforequery'
575         // so that client code can modify values.
576         var me = this,
577             qe = {
578                 query: queryString,
579                 forceAll: forceAll,
580                 combo: me,
581                 cancel: false
582             },
583             store = me.store,
584             isLocalMode = me.queryMode === 'local';
585
586         if (me.fireEvent('beforequery', qe) === false || qe.cancel) {
587             return false;
588         }
589
590         // get back out possibly modified values
591         queryString = qe.query;
592         forceAll = qe.forceAll;
593
594         // query permitted to run
595         if (forceAll || (queryString.length >= me.minChars)) {
596             // expand before starting query so LoadMask can position itself correctly
597             me.expand();
598
599             // make sure they aren't querying the same thing
600             if (!me.queryCaching || me.lastQuery !== queryString) {
601                 me.lastQuery = queryString;
602                 store.clearFilter(!forceAll);
603                 if (isLocalMode) {
604                     if (!forceAll) {
605                         store.filter(me.displayField, queryString);
606                     }
607                 } else {
608                     store.load({
609                         params: me.getParams(queryString)
610                     });
611                 }
612             }
613
614             // Clear current selection if it does not match the current value in the field
615             if (me.getRawValue() !== me.getDisplayValue()) {
616                 me.ignoreSelection++;
617                 me.picker.getSelectionModel().deselectAll();
618                 me.ignoreSelection--;
619             }
620
621             if (isLocalMode) {
622                 me.doAutoSelect();
623             }
624             if (me.typeAhead) {
625                 me.doTypeAhead();
626             }
627         }
628         return true;
629     },
630
631     // private
632     getParams: function(queryString) {
633         var p = {},
634             pageSize = this.pageSize,
635             param = this.queryParam;
636             
637         if (param) {
638             p[param] = queryString;
639         }
640         
641         if (pageSize) {
642             p.start = 0;
643             p.limit = pageSize;
644         }
645         return p;
646     },
647
648     /**
649      * @private
650      * If the autoSelect config is true, and the picker is open, highlights the first item.
651      */
652     doAutoSelect: function() {
653         var me = this,
654             picker = me.picker,
655             lastSelected, itemNode;
656         if (picker && me.autoSelect && me.store.getCount() > 0) {
657             // Highlight the last selected item and scroll it into view
658             lastSelected = picker.getSelectionModel().lastSelected;
659             itemNode = picker.getNode(lastSelected || 0);
660             if (itemNode) {
661                 picker.highlightItem(itemNode);
662                 picker.listEl.scrollChildIntoView(itemNode, false);
663             }
664         }
665     },
666
667     doTypeAhead: function() {
668         if (!this.typeAheadTask) {
669             this.typeAheadTask = Ext.create('Ext.util.DelayedTask', this.onTypeAhead, this);
670         }
671         if (this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE) {
672             this.typeAheadTask.delay(this.typeAheadDelay);
673         }
674     },
675
676     onTriggerClick: function() {
677         var me = this;
678         if (!me.readOnly && !me.disabled) {
679             if (me.isExpanded) {
680                 me.collapse();
681             } else {
682                 me.onFocus({});
683                 if (me.triggerAction === 'all') {
684                     me.doQuery(me.allQuery, true);
685                 } else {
686                     me.doQuery(me.getRawValue());
687                 }
688             }
689             me.inputEl.focus();
690         }
691     },
692
693
694     // store the last key and doQuery if relevant
695     onKeyUp: function(e, t) {
696         var me = this,
697             key = e.getKey();
698
699         if (!me.readOnly && !me.disabled && me.editable) {
700             me.lastKey = key;
701             // we put this in a task so that we can cancel it if a user is
702             // in and out before the queryDelay elapses
703
704             // perform query w/ any normal key or backspace or delete
705             if (!e.isSpecialKey() || key == e.BACKSPACE || key == e.DELETE) {
706                 me.doQueryTask.delay(me.queryDelay);
707             }
708         }
709         
710         if (me.enableKeyEvents) {
711             me.callParent(arguments);
712         }
713     },
714
715     initEvents: function() {
716         var me = this;
717         me.callParent();
718
719         /*
720          * Setup keyboard handling. If enableKeyEvents is true, we already have 
721          * a listener on the inputEl for keyup, so don't create a second.
722          */
723         if (!me.enableKeyEvents) {
724             me.mon(me.inputEl, 'keyup', me.onKeyUp, me);
725         }
726     },
727
728     createPicker: function() {
729         var me = this,
730             picker,
731             menuCls = Ext.baseCSSPrefix + 'menu',
732             opts = Ext.apply({
733                 selModel: {
734                     mode: me.multiSelect ? 'SIMPLE' : 'SINGLE'
735                 },
736                 floating: true,
737                 hidden: true,
738                 ownerCt: me.ownerCt,
739                 cls: me.el.up('.' + menuCls) ? menuCls : '',
740                 store: me.store,
741                 displayField: me.displayField,
742                 focusOnToFront: false,
743                 pageSize: me.pageSize
744             }, me.listConfig, me.defaultListConfig);
745
746         picker = me.picker = Ext.create('Ext.view.BoundList', opts);
747
748         me.mon(picker, {
749             itemclick: me.onItemClick,
750             refresh: me.onListRefresh,
751             scope: me
752         });
753
754         me.mon(picker.getSelectionModel(), {
755             selectionChange: me.onListSelectionChange,
756             scope: me
757         });
758
759         return picker;
760     },
761
762     onListRefresh: function() {
763         this.alignPicker();
764         this.syncSelection();
765     },
766     
767     onItemClick: function(picker, record){
768         /*
769          * If we're doing single selection, the selection change events won't fire when
770          * clicking on the selected element. Detect it here.
771          */
772         var me = this,
773             lastSelection = me.lastSelection,
774             valueField = me.valueField,
775             selected;
776         
777         if (!me.multiSelect && lastSelection) {
778             selected = lastSelection[0];
779             if (record.get(valueField) === selected.get(valueField)) {
780                 me.collapse();
781             }
782         }   
783     },
784
785     onListSelectionChange: function(list, selectedRecords) {
786         var me = this;
787         // Only react to selection if it is not called from setValue, and if our list is
788         // expanded (ignores changes to the selection model triggered elsewhere)
789         if (!me.ignoreSelection && me.isExpanded) {
790             if (!me.multiSelect) {
791                 Ext.defer(me.collapse, 1, me);
792             }
793             me.setValue(selectedRecords, false);
794             if (selectedRecords.length > 0) {
795                 me.fireEvent('select', me, selectedRecords);
796             }
797             me.inputEl.focus();
798         }
799     },
800
801     /**
802      * @private
803      * Enables the key nav for the BoundList when it is expanded.
804      */
805     onExpand: function() {
806         var me = this,
807             keyNav = me.listKeyNav,
808             selectOnTab = me.selectOnTab,
809             picker = me.getPicker();
810
811         // Handle BoundList navigation from the input field. Insert a tab listener specially to enable selectOnTab.
812         if (keyNav) {
813             keyNav.enable();
814         } else {
815             keyNav = me.listKeyNav = Ext.create('Ext.view.BoundListKeyNav', this.inputEl, {
816                 boundList: picker,
817                 forceKeyDown: true,
818                 tab: function(e) {
819                     if (selectOnTab) {
820                         this.selectHighlighted(e);
821                         me.triggerBlur();
822                     }
823                     // Tab key event is allowed to propagate to field
824                     return true;
825                 }
826             });
827         }
828
829         // While list is expanded, stop tab monitoring from Ext.form.field.Trigger so it doesn't short-circuit selectOnTab
830         if (selectOnTab) {
831             me.ignoreMonitorTab = true;
832         }
833
834         Ext.defer(keyNav.enable, 1, keyNav); //wait a bit so it doesn't react to the down arrow opening the picker
835         me.inputEl.focus();
836     },
837
838     /**
839      * @private
840      * Disables the key nav for the BoundList when it is collapsed.
841      */
842     onCollapse: function() {
843         var me = this,
844             keyNav = me.listKeyNav;
845         if (keyNav) {
846             keyNav.disable();
847             me.ignoreMonitorTab = false;
848         }
849     },
850
851     /**
852      * Selects an item by a {@link Ext.data.Model Model}, or by a key value.
853      * @param r
854      */
855     select: function(r) {
856         this.setValue(r, true);
857     },
858
859     /**
860      * Find the record by searching for a specific field/value combination
861      * Returns an Ext.data.Record or false
862      * @private
863      */
864     findRecord: function(field, value) {
865         var ds = this.store,
866             idx = ds.findExact(field, value);
867         return idx !== -1 ? ds.getAt(idx) : false;
868     },
869     findRecordByValue: function(value) {
870         return this.findRecord(this.valueField, value);
871     },
872     findRecordByDisplay: function(value) {
873         return this.findRecord(this.displayField, value);
874     },
875
876     /**
877      * Sets the specified value(s) into the field. For each value, if a record is found in the {@link #store} that
878      * matches based on the {@link #valueField}, then that record's {@link #displayField} will be displayed in the
879      * field.  If no match is found, and the {@link #valueNotFoundText} config option is defined, then that will be
880      * displayed as the default field text. Otherwise a blank value will be shown, although the value will still be set.
881      * @param {String|Array} value The value(s) to be set. Can be either a single String or {@link Ext.data.Model},
882      * or an Array of Strings or Models.
883      * @return {Ext.form.field.Field} this
884      */
885     setValue: function(value, doSelect) {
886         var me = this,
887             valueNotFoundText = me.valueNotFoundText,
888             inputEl = me.inputEl,
889             i, len, record,
890             models = [],
891             displayTplData = [],
892             processedValue = [];
893
894         if (me.store.loading) {
895             // Called while the Store is loading. Ensure it is processed by the onLoad method.
896             me.value = value;
897             return me;
898         }
899
900         // This method processes multi-values, so ensure value is an array.
901         value = Ext.Array.from(value);
902
903         // Loop through values
904         for (i = 0, len = value.length; i < len; i++) {
905             record = value[i];
906             if (!record || !record.isModel) {
907                 record = me.findRecordByValue(record);
908             }
909             // record found, select it.
910             if (record) {
911                 models.push(record);
912                 displayTplData.push(record.data);
913                 processedValue.push(record.get(me.valueField));
914             }
915             // record was not found, this could happen because
916             // store is not loaded or they set a value not in the store
917             else {
918                 // if valueNotFoundText is defined, display it, otherwise display nothing for this value
919                 if (Ext.isDefined(valueNotFoundText)) {
920                     displayTplData.push(valueNotFoundText);
921                 }
922                 processedValue.push(value[i]);
923             }
924         }
925
926         // Set the value of this field. If we are multiselecting, then that is an array.
927         me.value = me.multiSelect ? processedValue : processedValue[0];
928         if (!Ext.isDefined(me.value)) {
929             me.value = null;
930         }
931         me.displayTplData = displayTplData; //store for getDisplayValue method
932         me.lastSelection = me.valueModels = models;
933
934         if (inputEl && me.emptyText && !Ext.isEmpty(value)) {
935             inputEl.removeCls(me.emptyCls);
936         }
937
938         // Calculate raw value from the collection of Model data
939         me.setRawValue(me.getDisplayValue());
940         me.checkChange();
941
942         if (doSelect !== false) {
943             me.syncSelection();
944         }
945         me.applyEmptyText();
946
947         return me;
948     },
949
950     /**
951      * @private Generate the string value to be displayed in the text field for the currently stored value
952      */
953     getDisplayValue: function() {
954         return this.displayTpl.apply(this.displayTplData);
955     },
956
957     getValue: function() {
958         // If the user has not changed the raw field value since a value was selected from the list,
959         // then return the structured value from the selection. If the raw field value is different
960         // than what would be displayed due to selection, return that raw value.
961         var me = this,
962             picker = me.picker,
963             rawValue = me.getRawValue(), //current value of text field
964             value = me.value; //stored value from last selection or setValue() call
965
966         if (me.getDisplayValue() !== rawValue) {
967             value = rawValue;
968             me.value = me.displayTplData = me.valueModels = null;
969             if (picker) {
970                 me.ignoreSelection++;
971                 picker.getSelectionModel().deselectAll();
972                 me.ignoreSelection--;
973             }
974         }
975
976         return value;
977     },
978
979     getSubmitValue: function() {
980         return this.getValue();
981     },
982
983     isEqual: function(v1, v2) {
984         var fromArray = Ext.Array.from,
985             i, len;
986
987         v1 = fromArray(v1);
988         v2 = fromArray(v2);
989         len = v1.length;
990
991         if (len !== v2.length) {
992             return false;
993         }
994
995         for(i = 0; i < len; i++) {
996             if (v2[i] !== v1[i]) {
997                 return false;
998             }
999         }
1000
1001         return true;
1002     },
1003
1004     /**
1005      * Clears any value currently set in the ComboBox.
1006      */
1007     clearValue: function() {
1008         this.setValue([]);
1009     },
1010
1011     /**
1012      * @private Synchronizes the selection in the picker to match the current value of the combobox.
1013      */
1014     syncSelection: function() {
1015         var me = this,
1016             ExtArray = Ext.Array,
1017             picker = me.picker,
1018             selection, selModel;
1019         if (picker) {
1020             // From the value, find the Models that are in the store's current data
1021             selection = [];
1022             ExtArray.forEach(me.valueModels || [], function(value) {
1023                 if (value && value.isModel && me.store.indexOf(value) >= 0) {
1024                     selection.push(value);
1025                 }
1026             });
1027
1028             // Update the selection to match
1029             me.ignoreSelection++;
1030             selModel = picker.getSelectionModel();
1031             selModel.deselectAll();
1032             if (selection.length) {
1033                 selModel.select(selection);
1034             }
1035             me.ignoreSelection--;
1036         }
1037     }
1038 });