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