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