commit extjs-2.2.1
[extjs.git] / source / widgets / form / Combo.js
1 /*\r
2  * Ext JS Library 2.2.1\r
3  * Copyright(c) 2006-2009, Ext JS, LLC.\r
4  * licensing@extjs.com\r
5  * \r
6  * http://extjs.com/license\r
7  */\r
8 \r
9 /**\r
10  * @class Ext.form.ComboBox\r
11  * @extends Ext.form.TriggerField\r
12  * <p>A combobox control with support for autocomplete, remote-loading, paging and many other features.</p>\r
13  * A ComboBox works in a similar manner to a traditional HTML &lt;select> field. The difference is that to submit the\r
14  * {@link #valueField}, you must specify a {@link #hiddenName} to create a hidden input field to hold the\r
15  * value of the valueField. The <i>{@link #displayField}</i> is shown in the text field which is named\r
16  * according to the {@link #name}.\r
17  * @constructor\r
18  * Create a new ComboBox.\r
19  * @param {Object} config Configuration options\r
20  */\r
21 Ext.form.ComboBox = Ext.extend(Ext.form.TriggerField, {\r
22     /**\r
23      * @cfg {Mixed} transform The id, DOM node or element of an existing HTML SELECT to convert to a ComboBox.\r
24      * Note that if you specify this and the combo is going to be in a {@link Ext.form.BasicForm} or\r
25      * {@link Ext.form.FormPanel}, you must also set {@link #lazyRender} = true.\r
26      */\r
27     /**\r
28      * @cfg {Boolean} lazyRender True to prevent the ComboBox from rendering until requested (should always be used when\r
29      * rendering into an Ext.Editor, defaults to false).\r
30      */\r
31     /**\r
32      * @cfg {Boolean/Object} autoCreate A DomHelper element spec, or true for a default element spec (defaults to:\r
33      * {tag: "input", type: "text", size: "24", autocomplete: "off"})\r
34      */\r
35     /**\r
36      * @cfg {Ext.data.Store/Array} store The data source to which this combo is bound (defaults to undefined).  This can be\r
37      * any {@link Ext.data.Store} subclass, a 1-dimensional array (e.g., ['Foo','Bar']) or a 2-dimensional array (e.g.,\r
38      * [['f','Foo'],['b','Bar']]).  Arrays will be converted to a {@link Ext.data.SimpleStore} internally.\r
39      * 1-dimensional arrays will automatically be expanded (each array item will be the combo value and text) and\r
40      * for multi-dimensional arrays, the value in index 0 of each item will be assumed to be the combo value, while\r
41      * the value at index 1 is assumed to be the combo text.\r
42      */\r
43     /**\r
44      * @cfg {String} title If supplied, a header element is created containing this text and added into the top of\r
45      * the dropdown list (defaults to undefined, with no header element)\r
46      */\r
47 \r
48     // private\r
49     defaultAutoCreate : {tag: "input", type: "text", size: "24", autocomplete: "off"},\r
50     /**\r
51      * @cfg {Number} listWidth The width in pixels of the dropdown list (defaults to the width of the ComboBox field)\r
52      */\r
53     /**\r
54      * @cfg {String} displayField The underlying data field name to bind to this ComboBox (defaults to undefined if\r
55      * mode = 'remote' or 'text' if transforming a select)\r
56      */\r
57     /**\r
58      * @cfg {String} valueField The underlying data value name to bind to this ComboBox (defaults to undefined if\r
59      * mode = 'remote' or 'value' if transforming a select) Note: use of a valueField requires the user to make a selection\r
60      * in order for a value to be mapped.\r
61      */\r
62     /**\r
63      * @cfg {String} hiddenName If specified, a hidden form field with this name is dynamically generated to store the\r
64      * field's data value (defaults to the underlying DOM element's name). Required for the combo's value to automatically\r
65      * post during a form submission.  Note that the hidden field's id will also default to this name if {@link #hiddenId}\r
66      * is not specified.  The combo's id and the hidden field's ids should be different, since no two DOM nodes should\r
67      * share the same id, so if the combo and hidden names are the same, you should specify a unique hiddenId.\r
68      */\r
69     /**\r
70      * @cfg {String} hiddenId If {@link #hiddenName} is specified, hiddenId can also be provided to give the hidden field\r
71      * a unique id (defaults to the hiddenName).  The hiddenId and combo {@link #id} should be different, since no two DOM\r
72      * nodes should share the same id.\r
73      */\r
74     /**\r
75      * @cfg {String} listClass CSS class to apply to the dropdown list element (defaults to '')\r
76      */\r
77     listClass: '',\r
78     /**\r
79      * @cfg {String} selectedClass CSS class to apply to the selected item in the dropdown list (defaults to 'x-combo-selected')\r
80      */\r
81     selectedClass: 'x-combo-selected',\r
82     /**\r
83      * @cfg {String} triggerClass An additional CSS class used to style the trigger button.  The trigger will always get the\r
84      * class 'x-form-trigger' and triggerClass will be <b>appended</b> if specified (defaults to 'x-form-arrow-trigger'\r
85      * which displays a downward arrow icon).\r
86      */\r
87     triggerClass : 'x-form-arrow-trigger',\r
88     /**\r
89      * @cfg {Boolean/String} shadow True or "sides" for the default effect, "frame" for 4-way shadow, and "drop" for bottom-right\r
90      */\r
91     shadow:'sides',\r
92     /**\r
93      * @cfg {String} listAlign A valid anchor position value. See {@link Ext.Element#alignTo} for details on supported\r
94      * anchor positions (defaults to 'tl-bl')\r
95      */\r
96     listAlign: 'tl-bl?',\r
97     /**\r
98      * @cfg {Number} maxHeight The maximum height in pixels of the dropdown list before scrollbars are shown (defaults to 300)\r
99      */\r
100     maxHeight: 300,\r
101     /**\r
102      * @cfg {Number} minHeight The minimum height in pixels of the dropdown list when the list is constrained by its\r
103      * distance to the viewport edges (defaults to 90)\r
104      */\r
105     minHeight: 90,\r
106     /**\r
107      * @cfg {String} triggerAction The action to execute when the trigger is clicked.  Use 'all' to run the\r
108      * query specified by the allQuery config option (defaults to 'query')\r
109      */\r
110     triggerAction: 'query',\r
111     /**\r
112      * @cfg {Number} minChars The minimum number of characters the user must type before autocomplete and typeahead activate\r
113      * (defaults to 4 if remote or 0 if local, does not apply if editable = false)\r
114      */\r
115     minChars : 4,\r
116     /**\r
117      * @cfg {Boolean} typeAhead True to populate and autoselect the remainder of the text being typed after a configurable\r
118      * delay ({@link #typeAheadDelay}) if it matches a known value (defaults to false)\r
119      */\r
120     typeAhead: false,\r
121     /**\r
122      * @cfg {Number} queryDelay The length of time in milliseconds to delay between the start of typing and sending the\r
123      * query to filter the dropdown list (defaults to 500 if mode = 'remote' or 10 if mode = 'local')\r
124      */\r
125     queryDelay: 500,\r
126     /**\r
127      * @cfg {Number} pageSize If greater than 0, a paging toolbar is displayed in the footer of the dropdown list and the\r
128      * filter queries will execute with page start and limit parameters.  Only applies when mode = 'remote' (defaults to 0)\r
129      */\r
130     pageSize: 0,\r
131     /**\r
132      * @cfg {Boolean} selectOnFocus True to select any existing text in the field immediately on focus.  Only applies\r
133      * when editable = true (defaults to false)\r
134      */\r
135     selectOnFocus:false,\r
136     /**\r
137      * @cfg {String} queryParam Name of the query as it will be passed on the querystring (defaults to 'query')\r
138      */\r
139     queryParam: 'query',\r
140     /**\r
141      * @cfg {String} loadingText The text to display in the dropdown list while data is loading.  Only applies\r
142      * when mode = 'remote' (defaults to 'Loading...')\r
143      */\r
144     loadingText: 'Loading...',\r
145     /**\r
146      * @cfg {Boolean} resizable True to add a resize handle to the bottom of the dropdown list (defaults to false)\r
147      */\r
148     resizable: false,\r
149     /**\r
150      * @cfg {Number} handleHeight The height in pixels of the dropdown list resize handle if resizable = true (defaults to 8)\r
151      */\r
152     handleHeight : 8,\r
153     /**\r
154      * @cfg {Boolean} editable False to prevent the user from typing text directly into the field, just like a\r
155      * traditional select (defaults to true)\r
156      */\r
157     editable: true,\r
158     /**\r
159      * @cfg {String} allQuery The text query to send to the server to return all records for the list with no filtering (defaults to '')\r
160      */\r
161     allQuery: '',\r
162     /**\r
163      * @cfg {String} mode Set to 'local' if the ComboBox loads local data (defaults to 'remote' which loads from the server)\r
164      */\r
165     mode: 'remote',\r
166     /**\r
167      * @cfg {Number} minListWidth The minimum width of the dropdown list in pixels (defaults to 70, will be ignored if\r
168      * listWidth has a higher value)\r
169      */\r
170     minListWidth : 70,\r
171     /**\r
172      * @cfg {Boolean} forceSelection True to restrict the selected value to one of the values in the list, false to\r
173      * allow the user to set arbitrary text into the field (defaults to false)\r
174      */\r
175     forceSelection:false,\r
176     /**\r
177      * @cfg {Number} typeAheadDelay The length of time in milliseconds to wait until the typeahead text is displayed\r
178      * if typeAhead = true (defaults to 250)\r
179      */\r
180     typeAheadDelay : 250,\r
181     /**\r
182      * @cfg {String} valueNotFoundText When using a name/value combo, if the value passed to setValue is not found in\r
183      * the store, valueNotFoundText will be displayed as the field text if defined (defaults to undefined). If this\r
184      * defaut text is used, it means there is no value set and no validation will occur on this field.\r
185      */\r
186 \r
187     /**\r
188      * @cfg {Boolean} lazyInit True to not initialize the list for this combo until the field is focused (defaults to true)\r
189      */\r
190     lazyInit : true,\r
191 \r
192     /**\r
193      * The value of the match string used to filter the store. Delete this property to force a requery.\r
194      * @property lastQuery\r
195      * @type String\r
196      */\r
197 \r
198     // private\r
199     initComponent : function(){\r
200         Ext.form.ComboBox.superclass.initComponent.call(this);\r
201         this.addEvents(\r
202             /**\r
203              * @event expand\r
204              * Fires when the dropdown list is expanded\r
205              * @param {Ext.form.ComboBox} combo This combo box\r
206              */\r
207             'expand',\r
208             /**\r
209              * @event collapse\r
210              * Fires when the dropdown list is collapsed\r
211              * @param {Ext.form.ComboBox} combo This combo box\r
212              */\r
213             'collapse',\r
214             /**\r
215              * @event beforeselect\r
216              * Fires before a list item is selected. Return false to cancel the selection.\r
217              * @param {Ext.form.ComboBox} combo This combo box\r
218              * @param {Ext.data.Record} record The data record returned from the underlying store\r
219              * @param {Number} index The index of the selected item in the dropdown list\r
220              */\r
221             'beforeselect',\r
222             /**\r
223              * @event select\r
224              * Fires when a list item is selected\r
225              * @param {Ext.form.ComboBox} combo This combo box\r
226              * @param {Ext.data.Record} record The data record returned from the underlying store\r
227              * @param {Number} index The index of the selected item in the dropdown list\r
228              */\r
229             'select',\r
230             /**\r
231              * @event beforequery\r
232              * Fires before all queries are processed. Return false to cancel the query or set the queryEvent's\r
233              * cancel property to true.\r
234              * @param {Object} queryEvent An object that has these properties:<ul>\r
235              * <li><code>combo</code> : Ext.form.ComboBox <div class="sub-desc">This combo box</div></li>\r
236              * <li><code>query</code> : String <div class="sub-desc">The query</div></li>\r
237              * <li><code>forceAll</code> : Boolean <div class="sub-desc">True to force "all" query</div></li>\r
238              * <li><code>cancel</code> : Boolean <div class="sub-desc">Set to true to cancel the query</div></li>\r
239              * </ul>\r
240              */\r
241             'beforequery'\r
242         );\r
243         if(this.transform){\r
244             this.allowDomMove = false;\r
245             var s = Ext.getDom(this.transform);\r
246             if(!this.hiddenName){\r
247                 this.hiddenName = s.name;\r
248             }\r
249             if(!this.store){\r
250                 this.mode = 'local';\r
251                 var d = [], opts = s.options;\r
252                 for(var i = 0, len = opts.length;i < len; i++){\r
253                     var o = opts[i];\r
254                     var value = (Ext.isIE ? o.getAttributeNode('value').specified : o.hasAttribute('value')) ? o.value : o.text;\r
255                     if(o.selected) {\r
256                         this.value = value;\r
257                     }\r
258                     d.push([value, o.text]);\r
259                 }\r
260                 this.store = new Ext.data.SimpleStore({\r
261                     'id': 0,\r
262                     fields: ['value', 'text'],\r
263                     data : d\r
264                 });\r
265                 this.valueField = 'value';\r
266                 this.displayField = 'text';\r
267             }\r
268             s.name = Ext.id(); // wipe out the name in case somewhere else they have a reference\r
269             if(!this.lazyRender){\r
270                 this.target = true;\r
271                 this.el = Ext.DomHelper.insertBefore(s, this.autoCreate || this.defaultAutoCreate);\r
272                 Ext.removeNode(s); // remove it\r
273                 this.render(this.el.parentNode);\r
274             }else{\r
275                 Ext.removeNode(s); // remove it\r
276             }\r
277         }\r
278         //auto-configure store from local array data\r
279         else if(Ext.isArray(this.store)){\r
280             if (Ext.isArray(this.store[0])){\r
281                 this.store = new Ext.data.SimpleStore({\r
282                     fields: ['value','text'],\r
283                     data: this.store\r
284                 });\r
285                 this.valueField = 'value';\r
286             }else{\r
287                 this.store = new Ext.data.SimpleStore({\r
288                     fields: ['text'],\r
289                     data: this.store,\r
290                     expandData: true\r
291                 });\r
292                 this.valueField = 'text';\r
293             }\r
294             this.displayField = 'text';\r
295             this.mode = 'local';\r
296         }\r
297 \r
298         this.selectedIndex = -1;\r
299         if(this.mode == 'local'){\r
300             if(this.initialConfig.queryDelay === undefined){\r
301                 this.queryDelay = 10;\r
302             }\r
303             if(this.initialConfig.minChars === undefined){\r
304                 this.minChars = 0;\r
305             }\r
306         }\r
307     },\r
308 \r
309     // private\r
310     onRender : function(ct, position){\r
311         Ext.form.ComboBox.superclass.onRender.call(this, ct, position);\r
312         if(this.hiddenName){\r
313             this.hiddenField = this.el.insertSibling({tag:'input', type:'hidden', name: this.hiddenName,\r
314                     id: (this.hiddenId||this.hiddenName)}, 'before', true);\r
315 \r
316             // prevent input submission\r
317             this.el.dom.removeAttribute('name');\r
318         }\r
319         if(Ext.isGecko){\r
320             this.el.dom.setAttribute('autocomplete', 'off');\r
321         }\r
322 \r
323         if(!this.lazyInit){\r
324             this.initList();\r
325         }else{\r
326             this.on('focus', this.initList, this, {single: true});\r
327         }\r
328 \r
329         if(!this.editable){\r
330             this.editable = true;\r
331             this.setEditable(false);\r
332         }\r
333     },\r
334 \r
335     // private\r
336     initValue : function(){\r
337         Ext.form.ComboBox.superclass.initValue.call(this);\r
338         if(this.hiddenField){\r
339             this.hiddenField.value =\r
340                 this.hiddenValue !== undefined ? this.hiddenValue :\r
341                 this.value !== undefined ? this.value : '';\r
342         }\r
343     },\r
344 \r
345     // private\r
346     initList : function(){\r
347         if(!this.list){\r
348             var cls = 'x-combo-list';\r
349 \r
350             this.list = new Ext.Layer({\r
351                 shadow: this.shadow, cls: [cls, this.listClass].join(' '), constrain:false\r
352             });\r
353 \r
354             var lw = this.listWidth || Math.max(this.wrap.getWidth(), this.minListWidth);\r
355             this.list.setWidth(lw);\r
356             this.list.swallowEvent('mousewheel');\r
357             this.assetHeight = 0;\r
358 \r
359             if(this.title){\r
360                 this.header = this.list.createChild({cls:cls+'-hd', html: this.title});\r
361                 this.assetHeight += this.header.getHeight();\r
362             }\r
363 \r
364             this.innerList = this.list.createChild({cls:cls+'-inner'});\r
365             this.innerList.on('mouseover', this.onViewOver, this);\r
366             this.innerList.on('mousemove', this.onViewMove, this);\r
367             this.innerList.setWidth(lw - this.list.getFrameWidth('lr'));\r
368 \r
369             if(this.pageSize){\r
370                 this.footer = this.list.createChild({cls:cls+'-ft'});\r
371                 this.pageTb = new Ext.PagingToolbar({\r
372                     store:this.store,\r
373                     pageSize: this.pageSize,\r
374                     renderTo:this.footer\r
375                 });\r
376                 this.assetHeight += this.footer.getHeight();\r
377             }\r
378 \r
379             if(!this.tpl){\r
380                 /**\r
381                 * @cfg {String/Ext.XTemplate} tpl The template string, or {@link Ext.XTemplate}\r
382                 * instance to use to display each item in the dropdown list. Use\r
383                 * this to create custom UI layouts for items in the list.\r
384                 * <p>\r
385                 * If you wish to preserve the default visual look of list items, add the CSS\r
386                 * class name <pre>x-combo-list-item</pre> to the template's container element.\r
387                 * <p>\r
388                 * <b>The template must contain one or more substitution parameters using field\r
389                 * names from the Combo's</b> {@link #store Store}. An example of a custom template\r
390                 * would be adding an <pre>ext:qtip</pre> attribute which might display other fields\r
391                 * from the Store.\r
392                 * <p>\r
393                 * The dropdown list is displayed in a DataView. See {@link Ext.DataView} for details.\r
394                 */\r
395                 this.tpl = '<tpl for="."><div class="'+cls+'-item">{' + this.displayField + '}</div></tpl>';\r
396                 /**\r
397                  * @cfg {String} itemSelector\r
398                  * <b>This setting is required if a custom XTemplate has been specified in {@link #tpl}\r
399                  * which assigns a class other than <pre>'x-combo-list-item'</pre> to dropdown list items</b>.\r
400                  * A simple CSS selector (e.g. div.some-class or span:first-child) that will be\r
401                  * used to determine what nodes the DataView which handles the dropdown display will\r
402                  * be working with.\r
403                  */\r
404             }\r
405 \r
406             /**\r
407             * The {@link Ext.DataView DataView} used to display the ComboBox's options.\r
408             * @type Ext.DataView\r
409             */\r
410             this.view = new Ext.DataView({\r
411                 applyTo: this.innerList,\r
412                 tpl: this.tpl,\r
413                 singleSelect: true,\r
414                 selectedClass: this.selectedClass,\r
415                 itemSelector: this.itemSelector || '.' + cls + '-item'\r
416             });\r
417 \r
418             this.view.on('click', this.onViewClick, this);\r
419 \r
420             this.bindStore(this.store, true);\r
421 \r
422             if(this.resizable){\r
423                 this.resizer = new Ext.Resizable(this.list,  {\r
424                    pinned:true, handles:'se'\r
425                 });\r
426                 this.resizer.on('resize', function(r, w, h){\r
427                     this.maxHeight = h-this.handleHeight-this.list.getFrameWidth('tb')-this.assetHeight;\r
428                     this.listWidth = w;\r
429                     this.innerList.setWidth(w - this.list.getFrameWidth('lr'));\r
430                     this.restrictHeight();\r
431                 }, this);\r
432                 this[this.pageSize?'footer':'innerList'].setStyle('margin-bottom', this.handleHeight+'px');\r
433             }\r
434         }\r
435     },\r
436     \r
437     /**\r
438      * Returns the store associated with this combo.\r
439      * @return {Ext.data.Store} The store\r
440      */\r
441     getStore : function(){\r
442         return this.store;\r
443     },\r
444 \r
445     // private\r
446     bindStore : function(store, initial){\r
447         if(this.store && !initial){\r
448             this.store.un('beforeload', this.onBeforeLoad, this);\r
449             this.store.un('load', this.onLoad, this);\r
450             this.store.un('loadexception', this.collapse, this);\r
451             if(!store){\r
452                 this.store = null;\r
453                 if(this.view){\r
454                     this.view.setStore(null);\r
455                 }\r
456             }\r
457         }\r
458         if(store){\r
459             this.store = Ext.StoreMgr.lookup(store);\r
460 \r
461             this.store.on('beforeload', this.onBeforeLoad, this);\r
462             this.store.on('load', this.onLoad, this);\r
463             this.store.on('loadexception', this.collapse, this);\r
464 \r
465             if(this.view){\r
466                 this.view.setStore(store);\r
467             }\r
468         }\r
469     },\r
470 \r
471     // private\r
472     initEvents : function(){\r
473         Ext.form.ComboBox.superclass.initEvents.call(this);\r
474 \r
475         this.keyNav = new Ext.KeyNav(this.el, {\r
476             "up" : function(e){\r
477                 this.inKeyMode = true;\r
478                 this.selectPrev();\r
479             },\r
480 \r
481             "down" : function(e){\r
482                 if(!this.isExpanded()){\r
483                     this.onTriggerClick();\r
484                 }else{\r
485                     this.inKeyMode = true;\r
486                     this.selectNext();\r
487                 }\r
488             },\r
489 \r
490             "enter" : function(e){\r
491                 this.onViewClick();\r
492                 this.delayedCheck = true;\r
493                 this.unsetDelayCheck.defer(10, this);\r
494             },\r
495 \r
496             "esc" : function(e){\r
497                 this.collapse();\r
498             },\r
499 \r
500             "tab" : function(e){\r
501                 this.onViewClick(false);\r
502                 return true;\r
503             },\r
504 \r
505             scope : this,\r
506 \r
507             doRelay : function(foo, bar, hname){\r
508                 if(hname == 'down' || this.scope.isExpanded()){\r
509                    return Ext.KeyNav.prototype.doRelay.apply(this, arguments);\r
510                 }\r
511                 return true;\r
512             },\r
513 \r
514             forceKeyDown : true\r
515         });\r
516         this.queryDelay = Math.max(this.queryDelay || 10,\r
517                 this.mode == 'local' ? 10 : 250);\r
518         this.dqTask = new Ext.util.DelayedTask(this.initQuery, this);\r
519         if(this.typeAhead){\r
520             this.taTask = new Ext.util.DelayedTask(this.onTypeAhead, this);\r
521         }\r
522         if(this.editable !== false){\r
523             this.el.on("keyup", this.onKeyUp, this);\r
524         }\r
525         if(this.forceSelection){\r
526             this.on('blur', this.doForce, this);\r
527         }\r
528     },\r
529 \r
530     // private\r
531     onDestroy : function(){\r
532         if(this.view){\r
533             Ext.destroy(this.view);\r
534         }\r
535         if(this.list){\r
536             if(this.innerList){\r
537                 this.innerList.un('mouseover', this.onViewOver, this);\r
538                 this.innerList.un('mousemove', this.onViewMove, this);\r
539             }\r
540             this.list.destroy();\r
541         }\r
542         if (this.dqTask){\r
543             this.dqTask.cancel();\r
544             this.dqTask = null;\r
545         }\r
546         this.bindStore(null);\r
547         Ext.form.ComboBox.superclass.onDestroy.call(this);\r
548     },\r
549 \r
550     // private\r
551     unsetDelayCheck : function(){\r
552         delete this.delayedCheck;\r
553     },\r
554 \r
555     // private\r
556     fireKey : function(e){\r
557         if(e.isNavKeyPress() && !this.isExpanded() && !this.delayedCheck){\r
558             this.fireEvent("specialkey", this, e);\r
559         }\r
560     },\r
561 \r
562     // private\r
563     onResize: function(w, h){\r
564         Ext.form.ComboBox.superclass.onResize.apply(this, arguments);\r
565         if(this.list && this.listWidth === undefined){\r
566             var lw = Math.max(w, this.minListWidth);\r
567             this.list.setWidth(lw);\r
568             this.innerList.setWidth(lw - this.list.getFrameWidth('lr'));\r
569         }\r
570     },\r
571 \r
572     // private\r
573     onEnable: function(){\r
574         Ext.form.ComboBox.superclass.onEnable.apply(this, arguments);\r
575         if(this.hiddenField){\r
576             this.hiddenField.disabled = false;\r
577         }\r
578     },\r
579 \r
580     // private\r
581     onDisable: function(){\r
582         Ext.form.ComboBox.superclass.onDisable.apply(this, arguments);\r
583         if(this.hiddenField){\r
584             this.hiddenField.disabled = true;\r
585         }\r
586     },\r
587 \r
588     /**\r
589      * Allow or prevent the user from directly editing the field text.  If false is passed,\r
590      * the user will only be able to select from the items defined in the dropdown list.  This method\r
591      * is the runtime equivalent of setting the 'editable' config option at config time.\r
592      * @param {Boolean} value True to allow the user to directly edit the field text\r
593      */\r
594     setEditable : function(value){\r
595         if(value == this.editable){\r
596             return;\r
597         }\r
598         this.editable = value;\r
599         if(!value){\r
600             this.el.dom.setAttribute('readOnly', true);\r
601             this.el.on('mousedown', this.onTriggerClick,  this);\r
602             this.el.addClass('x-combo-noedit');\r
603         }else{\r
604             this.el.dom.removeAttribute('readOnly');\r
605             this.el.un('mousedown', this.onTriggerClick,  this);\r
606             this.el.removeClass('x-combo-noedit');\r
607         }\r
608     },\r
609 \r
610     // private\r
611     onBeforeLoad : function(){\r
612         if(!this.hasFocus){\r
613             return;\r
614         }\r
615         this.innerList.update(this.loadingText ?\r
616                '<div class="loading-indicator">'+this.loadingText+'</div>' : '');\r
617         this.restrictHeight();\r
618         this.selectedIndex = -1;\r
619     },\r
620 \r
621     // private\r
622     onLoad : function(){\r
623         if(!this.hasFocus){\r
624             return;\r
625         }\r
626         if(this.store.getCount() > 0){\r
627             this.expand();\r
628             this.restrictHeight();\r
629             if(this.lastQuery == this.allQuery){\r
630                 if(this.editable){\r
631                     this.el.dom.select();\r
632                 }\r
633                 if(!this.selectByValue(this.value, true)){\r
634                     this.select(0, true);\r
635                 }\r
636             }else{\r
637                 this.selectNext();\r
638                 if(this.typeAhead && this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE){\r
639                     this.taTask.delay(this.typeAheadDelay);\r
640                 }\r
641             }\r
642         }else{\r
643             this.onEmptyResults();\r
644         }\r
645         //this.el.focus();\r
646     },\r
647 \r
648     // private\r
649     onTypeAhead : function(){\r
650         if(this.store.getCount() > 0){\r
651             var r = this.store.getAt(0);\r
652             var newValue = r.data[this.displayField];\r
653             var len = newValue.length;\r
654             var selStart = this.getRawValue().length;\r
655             if(selStart != len){\r
656                 this.setRawValue(newValue);\r
657                 this.selectText(selStart, newValue.length);\r
658             }\r
659         }\r
660     },\r
661 \r
662     // private\r
663     onSelect : function(record, index){\r
664         if(this.fireEvent('beforeselect', this, record, index) !== false){\r
665             this.setValue(record.data[this.valueField || this.displayField]);\r
666             this.collapse();\r
667             this.fireEvent('select', this, record, index);\r
668         }\r
669     },\r
670 \r
671     /**\r
672      * Returns the currently selected field value or empty string if no value is set.\r
673      * @return {String} value The selected value\r
674      */\r
675     getValue : function(){\r
676         if(this.valueField){\r
677             return typeof this.value != 'undefined' ? this.value : '';\r
678         }else{\r
679             return Ext.form.ComboBox.superclass.getValue.call(this);\r
680         }\r
681     },\r
682 \r
683     /**\r
684      * Clears any text/value currently set in the field\r
685      */\r
686     clearValue : function(){\r
687         if(this.hiddenField){\r
688             this.hiddenField.value = '';\r
689         }\r
690         this.setRawValue('');\r
691         this.lastSelectionText = '';\r
692         this.applyEmptyText();\r
693         this.value = '';\r
694     },\r
695 \r
696     /**\r
697      * Sets the specified value into the field.  If the value finds a match, the corresponding record text\r
698      * will be displayed in the field.  If the value does not match the data value of an existing item,\r
699      * and the valueNotFoundText config option is defined, it will be displayed as the default field text.\r
700      * Otherwise the field will be blank (although the value will still be set).\r
701      * @param {String} value The value to match\r
702      */\r
703     setValue : function(v){\r
704         var text = v;\r
705         if(this.valueField){\r
706             var r = this.findRecord(this.valueField, v);\r
707             if(r){\r
708                 text = r.data[this.displayField];\r
709             }else if(this.valueNotFoundText !== undefined){\r
710                 text = this.valueNotFoundText;\r
711             }\r
712         }\r
713         this.lastSelectionText = text;\r
714         if(this.hiddenField){\r
715             this.hiddenField.value = v;\r
716         }\r
717         Ext.form.ComboBox.superclass.setValue.call(this, text);\r
718         this.value = v;\r
719     },\r
720 \r
721     // private\r
722     findRecord : function(prop, value){\r
723         var record;\r
724         if(this.store.getCount() > 0){\r
725             this.store.each(function(r){\r
726                 if(r.data[prop] == value){\r
727                     record = r;\r
728                     return false;\r
729                 }\r
730             });\r
731         }\r
732         return record;\r
733     },\r
734 \r
735     // private\r
736     onViewMove : function(e, t){\r
737         this.inKeyMode = false;\r
738     },\r
739 \r
740     // private\r
741     onViewOver : function(e, t){\r
742         if(this.inKeyMode){ // prevent key nav and mouse over conflicts\r
743             return;\r
744         }\r
745         var item = this.view.findItemFromChild(t);\r
746         if(item){\r
747             var index = this.view.indexOf(item);\r
748             this.select(index, false);\r
749         }\r
750     },\r
751 \r
752     // private\r
753     onViewClick : function(doFocus){\r
754         var index = this.view.getSelectedIndexes()[0];\r
755         var r = this.store.getAt(index);\r
756         if(r){\r
757             this.onSelect(r, index);\r
758         }\r
759         if(doFocus !== false){\r
760             this.el.focus();\r
761         }\r
762     },\r
763 \r
764     // private\r
765     restrictHeight : function(){\r
766         this.innerList.dom.style.height = '';\r
767         var inner = this.innerList.dom;\r
768         var pad = this.list.getFrameWidth('tb')+(this.resizable?this.handleHeight:0)+this.assetHeight;\r
769         var h = Math.max(inner.clientHeight, inner.offsetHeight, inner.scrollHeight);\r
770         var ha = this.getPosition()[1]-Ext.getBody().getScroll().top;\r
771         var hb = Ext.lib.Dom.getViewHeight()-ha-this.getSize().height;\r
772         var space = Math.max(ha, hb, this.minHeight || 0)-this.list.shadowOffset-pad-5;\r
773         h = Math.min(h, space, this.maxHeight);\r
774 \r
775         this.innerList.setHeight(h);\r
776         this.list.beginUpdate();\r
777         this.list.setHeight(h+pad);\r
778         this.list.alignTo(this.wrap, this.listAlign);\r
779         this.list.endUpdate();\r
780     },\r
781 \r
782     // private\r
783     onEmptyResults : function(){\r
784         this.collapse();\r
785     },\r
786 \r
787     /**\r
788      * Returns true if the dropdown list is expanded, else false.\r
789      */\r
790     isExpanded : function(){\r
791         return this.list && this.list.isVisible();\r
792     },\r
793 \r
794     /**\r
795      * Select an item in the dropdown list by its data value. This function does NOT cause the select event to fire.\r
796      * The store must be loaded and the list expanded for this function to work, otherwise use setValue.\r
797      * @param {String} value The data value of the item to select\r
798      * @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the\r
799      * selected item if it is not currently in view (defaults to true)\r
800      * @return {Boolean} True if the value matched an item in the list, else false\r
801      */\r
802     selectByValue : function(v, scrollIntoView){\r
803         if(v !== undefined && v !== null){\r
804             var r = this.findRecord(this.valueField || this.displayField, v);\r
805             if(r){\r
806                 this.select(this.store.indexOf(r), scrollIntoView);\r
807                 return true;\r
808             }\r
809         }\r
810         return false;\r
811     },\r
812 \r
813     /**\r
814      * Select an item in the dropdown list by its numeric index in the list. This function does NOT cause the select event to fire.\r
815      * The store must be loaded and the list expanded for this function to work, otherwise use setValue.\r
816      * @param {Number} index The zero-based index of the list item to select\r
817      * @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the\r
818      * selected item if it is not currently in view (defaults to true)\r
819      */\r
820     select : function(index, scrollIntoView){\r
821         this.selectedIndex = index;\r
822         this.view.select(index);\r
823         if(scrollIntoView !== false){\r
824             var el = this.view.getNode(index);\r
825             if(el){\r
826                 this.innerList.scrollChildIntoView(el, false);\r
827             }\r
828         }\r
829     },\r
830 \r
831     // private\r
832     selectNext : function(){\r
833         var ct = this.store.getCount();\r
834         if(ct > 0){\r
835             if(this.selectedIndex == -1){\r
836                 this.select(0);\r
837             }else if(this.selectedIndex < ct-1){\r
838                 this.select(this.selectedIndex+1);\r
839             }\r
840         }\r
841     },\r
842 \r
843     // private\r
844     selectPrev : function(){\r
845         var ct = this.store.getCount();\r
846         if(ct > 0){\r
847             if(this.selectedIndex == -1){\r
848                 this.select(0);\r
849             }else if(this.selectedIndex != 0){\r
850                 this.select(this.selectedIndex-1);\r
851             }\r
852         }\r
853     },\r
854 \r
855     // private\r
856     onKeyUp : function(e){\r
857         if(this.editable !== false && !e.isSpecialKey()){\r
858             this.lastKey = e.getKey();\r
859             this.dqTask.delay(this.queryDelay);\r
860         }\r
861     },\r
862 \r
863     // private\r
864     validateBlur : function(){\r
865         return !this.list || !this.list.isVisible();\r
866     },\r
867 \r
868     // private\r
869     initQuery : function(){\r
870         this.doQuery(this.getRawValue());\r
871     },\r
872 \r
873     // private\r
874     doForce : function(){\r
875         if(this.el.dom.value.length > 0){\r
876             this.el.dom.value =\r
877                 this.lastSelectionText === undefined ? '' : this.lastSelectionText;\r
878             this.applyEmptyText();\r
879         }\r
880     },\r
881 \r
882     /**\r
883      * Execute a query to filter the dropdown list.  Fires the {@link #beforequery} event prior to performing the\r
884      * query allowing the query action to be canceled if needed.\r
885      * @param {String} query The SQL query to execute\r
886      * @param {Boolean} forceAll True to force the query to execute even if there are currently fewer characters\r
887      * in the field than the minimum specified by the minChars config option.  It also clears any filter previously\r
888      * saved in the current store (defaults to false)\r
889      */\r
890     doQuery : function(q, forceAll){\r
891         if(q === undefined || q === null){\r
892             q = '';\r
893         }\r
894         var qe = {\r
895             query: q,\r
896             forceAll: forceAll,\r
897             combo: this,\r
898             cancel:false\r
899         };\r
900         if(this.fireEvent('beforequery', qe)===false || qe.cancel){\r
901             return false;\r
902         }\r
903         q = qe.query;\r
904         forceAll = qe.forceAll;\r
905         if(forceAll === true || (q.length >= this.minChars)){\r
906             if(this.lastQuery !== q){\r
907                 this.lastQuery = q;\r
908                 if(this.mode == 'local'){\r
909                     this.selectedIndex = -1;\r
910                     if(forceAll){\r
911                         this.store.clearFilter();\r
912                     }else{\r
913                         this.store.filter(this.displayField, q);\r
914                     }\r
915                     this.onLoad();\r
916                 }else{\r
917                     this.store.baseParams[this.queryParam] = q;\r
918                     this.store.load({\r
919                         params: this.getParams(q)\r
920                     });\r
921                     this.expand();\r
922                 }\r
923             }else{\r
924                 this.selectedIndex = -1;\r
925                 this.onLoad();\r
926             }\r
927         }\r
928     },\r
929 \r
930     // private\r
931     getParams : function(q){\r
932         var p = {};\r
933         //p[this.queryParam] = q;\r
934         if(this.pageSize){\r
935             p.start = 0;\r
936             p.limit = this.pageSize;\r
937         }\r
938         return p;\r
939     },\r
940 \r
941     /**\r
942      * Hides the dropdown list if it is currently expanded. Fires the {@link #collapse} event on completion.\r
943      */\r
944     collapse : function(){\r
945         if(!this.isExpanded()){\r
946             return;\r
947         }\r
948         this.list.hide();\r
949         Ext.getDoc().un('mousewheel', this.collapseIf, this);\r
950         Ext.getDoc().un('mousedown', this.collapseIf, this);\r
951         this.fireEvent('collapse', this);\r
952     },\r
953 \r
954     // private\r
955     collapseIf : function(e){\r
956         if(!e.within(this.wrap) && !e.within(this.list)){\r
957             this.collapse();\r
958         }\r
959     },\r
960 \r
961     /**\r
962      * Expands the dropdown list if it is currently hidden. Fires the {@link #expand} event on completion.\r
963      */\r
964     expand : function(){\r
965         if(this.isExpanded() || !this.hasFocus){\r
966             return;\r
967         }\r
968         this.list.alignTo(this.wrap, this.listAlign);\r
969         this.list.show();\r
970         this.innerList.setOverflow('auto'); // necessary for FF 2.0/Mac\r
971         Ext.getDoc().on('mousewheel', this.collapseIf, this);\r
972         Ext.getDoc().on('mousedown', this.collapseIf, this);\r
973         this.fireEvent('expand', this);\r
974     },\r
975 \r
976     /**\r
977      * @method onTriggerClick\r
978      * @hide\r
979      */\r
980     // private\r
981     // Implements the default empty TriggerField.onTriggerClick function\r
982     onTriggerClick : function(){\r
983         if(this.disabled){\r
984             return;\r
985         }\r
986         if(this.isExpanded()){\r
987             this.collapse();\r
988             this.el.focus();\r
989         }else {\r
990             this.onFocus({});\r
991             if(this.triggerAction == 'all') {\r
992                 this.doQuery(this.allQuery, true);\r
993             } else {\r
994                 this.doQuery(this.getRawValue());\r
995             }\r
996             this.el.focus();\r
997         }\r
998     }\r
999 \r
1000     /**\r
1001      * @hide\r
1002      * @method autoSize\r
1003      */\r
1004     /**\r
1005      * @cfg {Boolean} grow @hide\r
1006      */\r
1007     /**\r
1008      * @cfg {Number} growMin @hide\r
1009      */\r
1010     /**\r
1011      * @cfg {Number} growMax @hide\r
1012      */\r
1013 \r
1014 });\r
1015 Ext.reg('combo', Ext.form.ComboBox);