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