3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @docauthor Jason Johnston <jason@sencha.com>
18 * A combobox control with support for autocomplete, remote loading, and many other features.
20 * A ComboBox is like a combination of a traditional HTML text `<input>` field and a `<select>`
21 * field; the user is able to type freely into the field, and/or pick values from a dropdown selection
22 * list. The user can input any value by default, even if it does not appear in the selection list;
23 * to prevent free-form values and restrict them to items in the list, set {@link #forceSelection} to `true`.
25 * The selection list's options are populated from any {@link Ext.data.Store}, including remote
26 * stores. The data items in the store are mapped to each option's displayed text and backing value via
27 * the {@link #valueField} and {@link #displayField} configurations, respectively.
29 * If your store is not remote, i.e. it depends only on local data and is loaded up front, you should be
30 * sure to set the {@link #queryMode} to `'local'`, as this will improve responsiveness for the user.
35 * // The data store containing the list of states
36 * var states = Ext.create('Ext.data.Store', {
37 * fields: ['abbr', 'name'],
39 * {"abbr":"AL", "name":"Alabama"},
40 * {"abbr":"AK", "name":"Alaska"},
41 * {"abbr":"AZ", "name":"Arizona"}
46 * // Create the combo box, attached to the states data store
47 * Ext.create('Ext.form.ComboBox', {
48 * fieldLabel: 'Choose State',
51 * displayField: 'name',
53 * renderTo: Ext.getBody()
58 * To do something when something in ComboBox is selected, configure the select event:
60 * var cb = Ext.create('Ext.form.ComboBox', {
61 * // all of your config options
64 * 'select': yourFunction
68 * // Alternatively, you can assign events after the object is created:
69 * var cb = new Ext.form.field.ComboBox(yourOptions);
70 * cb.on('select', yourFunction, yourScope);
72 * # Multiple Selection
74 * ComboBox also allows selection of multiple items from the list; to enable multi-selection set the
75 * {@link #multiSelect} config to `true`.
77 Ext.define('Ext.form.field.ComboBox', {
78 extend:'Ext.form.field.Picker',
79 requires: ['Ext.util.DelayedTask', 'Ext.EventObject', 'Ext.view.BoundList', 'Ext.view.BoundListKeyNav', 'Ext.data.StoreManager'],
80 alternateClassName: 'Ext.form.ComboBox',
81 alias: ['widget.combobox', 'widget.combo'],
84 * @cfg {String} [triggerCls='x-form-arrow-trigger']
85 * An additional CSS class used to style the trigger button. The trigger will always get the {@link #triggerBaseCls}
86 * by default and `triggerCls` will be **appended** if specified.
88 triggerCls: Ext.baseCSSPrefix + 'form-arrow-trigger',
93 * CSS class used to find the {@link #hiddenDataEl}
95 hiddenDataCls: Ext.baseCSSPrefix + 'hide-display ' + Ext.baseCSSPrefix + 'form-data-hidden',
101 '<div class="{hiddenDataCls}" role="presentation"></div>',
102 '<input id="{id}" type="{type}" ',
103 '<tpl if="size">size="{size}" </tpl>',
104 '<tpl if="tabIdx">tabIndex="{tabIdx}" </tpl>',
105 'class="{fieldCls} {typeCls}" autocomplete="off" />',
106 '<div id="{cmpId}-triggerWrap" class="{triggerWrapCls}" role="presentation">',
108 '<div class="{clearCls}" role="presentation"></div>',
116 getSubTplData: function(){
118 Ext.applyIf(me.subTplData, {
119 hiddenDataCls: me.hiddenDataCls
121 return me.callParent(arguments);
124 afterRender: function(){
126 me.callParent(arguments);
127 me.setHiddenValue(me.value);
131 * @cfg {Ext.data.Store/Array} store
132 * The data source to which this combo is bound. Acceptable values for this property are:
134 * - **any {@link Ext.data.Store Store} subclass**
135 * - **an Array** : Arrays will be converted to a {@link Ext.data.Store} internally, automatically generating
136 * {@link Ext.data.Field#name field names} to work with all data components.
138 * - **1-dimensional array** : (e.g., `['Foo','Bar']`)
140 * A 1-dimensional array will automatically be expanded (each array item will be used for both the combo
141 * {@link #valueField} and {@link #displayField})
143 * - **2-dimensional array** : (e.g., `[['f','Foo'],['b','Bar']]`)
145 * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
146 * {@link #valueField}, while the value at index 1 is assumed to be the combo {@link #displayField}.
148 * See also {@link #queryMode}.
152 * @cfg {Boolean} multiSelect
153 * If set to `true`, allows the combo field to hold more than one value at a time, and allows selecting multiple
154 * items from the dropdown list. The combo's text field will show all selected values separated by the
155 * {@link #delimiter}.
160 * @cfg {String} delimiter
161 * The character(s) used to separate the {@link #displayField display values} of multiple selected items when
162 * `{@link #multiSelect} = true`.
167 * @cfg {String} displayField
168 * The underlying {@link Ext.data.Field#name data field name} to bind to this ComboBox.
170 * See also `{@link #valueField}`.
172 displayField: 'text',
175 * @cfg {String} valueField (required)
176 * The underlying {@link Ext.data.Field#name data value name} to bind to this ComboBox (defaults to match
177 * the value of the {@link #displayField} config).
179 * **Note**: use of a `valueField` requires the user to make a selection in order for a value to be mapped. See also
180 * `{@link #displayField}`.
184 * @cfg {String} triggerAction
185 * The action to execute when the trigger is clicked.
189 * {@link #doQuery run the query} specified by the `{@link #allQuery}` config option
193 * {@link #doQuery run the query} using the {@link Ext.form.field.Base#getRawValue raw value}.
195 * See also `{@link #queryParam}`.
197 triggerAction: 'all',
200 * @cfg {String} allQuery
201 * The text query to send to the server to return all records for the list with no filtering
206 * @cfg {String} queryParam
207 * Name of the parameter used by the Store to pass the typed string when the ComboBox is configured with
208 * `{@link #queryMode}: 'remote'`. If explicitly set to a falsy value it will not be sent.
213 * @cfg {String} queryMode
214 * The mode in which the ComboBox uses the configured Store. Acceptable values are:
218 * In `queryMode: 'remote'`, the ComboBox loads its Store dynamically based upon user interaction.
220 * This is typically used for "autocomplete" type inputs, and after the user finishes typing, the Store is {@link
221 * Ext.data.Store#load load}ed.
223 * A parameter containing the typed string is sent in the load request. The default parameter name for the input
224 * string is `query`, but this can be configured using the {@link #queryParam} config.
226 * In `queryMode: 'remote'`, the Store may be configured with `{@link Ext.data.Store#remoteFilter remoteFilter}:
227 * true`, and further filters may be _programatically_ added to the Store which are then passed with every load
228 * request which allows the server to further refine the returned dataset.
230 * Typically, in an autocomplete situation, {@link #hideTrigger} is configured `true` because it has no meaning for
235 * ComboBox loads local data
237 * var combo = new Ext.form.field.ComboBox({
238 * renderTo: document.body,
239 * queryMode: 'local',
240 * store: new Ext.data.ArrayStore({
243 * 'myId', // numeric value is the key
246 * data: [[1, 'item1'], [2, 'item2']] // data is local
248 * valueField: 'myId',
249 * displayField: 'displayText',
250 * triggerAction: 'all'
258 * @cfg {Number} pageSize
259 * If greater than `0`, a {@link Ext.toolbar.Paging} is displayed in the footer of the dropdown list and the
260 * {@link #doQuery filter queries} will execute with page start and {@link Ext.view.BoundList#pageSize limit}
261 * parameters. Only applies when `{@link #queryMode} = 'remote'`.
266 * @cfg {Number} queryDelay
267 * The length of time in milliseconds to delay between the start of typing and sending the query to filter the
268 * dropdown list (defaults to `500` if `{@link #queryMode} = 'remote'` or `10` if `{@link #queryMode} = 'local'`)
272 * @cfg {Number} minChars
273 * The minimum number of characters the user must type before autocomplete and {@link #typeAhead} activate (defaults
274 * to `4` if `{@link #queryMode} = 'remote'` or `0` if `{@link #queryMode} = 'local'`, does not apply if
275 * `{@link Ext.form.field.Trigger#editable editable} = false`).
279 * @cfg {Boolean} autoSelect
280 * `true` to automatically highlight the first result gathered by the data store in the dropdown list when it is
281 * opened. A false value would cause nothing in the list to be highlighted automatically, so
282 * the user would have to manually highlight an item before pressing the enter or {@link #selectOnTab tab} key to
283 * select it (unless the value of ({@link #typeAhead}) were true), or use the mouse to select a value.
288 * @cfg {Boolean} typeAhead
289 * `true` to populate and autoselect the remainder of the text being typed after a configurable delay
290 * ({@link #typeAheadDelay}) if it matches a known value.
295 * @cfg {Number} typeAheadDelay
296 * The length of time in milliseconds to wait until the typeahead text is displayed if `{@link #typeAhead} = true`
301 * @cfg {Boolean} selectOnTab
302 * Whether the Tab key should select the currently highlighted item.
307 * @cfg {Boolean} forceSelection
308 * `true` to restrict the selected value to one of the values in the list, `false` to allow the user to set
309 * arbitrary text into the field.
311 forceSelection: false,
314 * @cfg {String} valueNotFoundText
315 * When using a name/value combo, if the value passed to setValue is not found in the store, valueNotFoundText will
316 * be displayed as the field text if defined. If this default text is used, it means there
317 * is no value set and no validation will occur on this field.
321 * @property {String} lastQuery
322 * The value of the match string used to filter the store. Delete this property to force a requery. Example use:
324 * var combo = new Ext.form.field.ComboBox({
326 * queryMode: 'remote',
328 * // delete the previous query in the beforequery event or set
329 * // combo.lastQuery = null (this will reload the store the next time it expands)
330 * beforequery: function(qe){
331 * delete qe.combo.lastQuery;
336 * To make sure the filter in the store is not cleared the first time the ComboBox trigger is used configure the
337 * combo with `lastQuery=''`. Example use:
339 * var combo = new Ext.form.field.ComboBox({
341 * queryMode: 'local',
342 * triggerAction: 'all',
348 * @cfg {Object} defaultListConfig
349 * Set of options that will be used as defaults for the user-configured {@link #listConfig} object.
353 loadingText: 'Loading...',
361 * @cfg {String/HTMLElement/Ext.Element} transform
362 * The id, DOM node or {@link Ext.Element} of an existing HTML `<select>` element to convert into a ComboBox. The
363 * target select's options will be used to build the options in the ComboBox dropdown; a configured {@link #store}
364 * will take precedence over this.
368 * @cfg {Object} listConfig
369 * An optional set of configuration properties that will be passed to the {@link Ext.view.BoundList}'s constructor.
370 * Any configuration that is valid for BoundList can be included. Some of the more useful ones are:
372 * - {@link Ext.view.BoundList#cls} - defaults to empty
373 * - {@link Ext.view.BoundList#emptyText} - defaults to empty string
374 * - {@link Ext.view.BoundList#itemSelector} - defaults to the value defined in BoundList
375 * - {@link Ext.view.BoundList#loadingText} - defaults to `'Loading...'`
376 * - {@link Ext.view.BoundList#minWidth} - defaults to `70`
377 * - {@link Ext.view.BoundList#maxWidth} - defaults to `undefined`
378 * - {@link Ext.view.BoundList#maxHeight} - defaults to `300`
379 * - {@link Ext.view.BoundList#resizable} - defaults to `false`
380 * - {@link Ext.view.BoundList#shadow} - defaults to `'sides'`
381 * - {@link Ext.view.BoundList#width} - defaults to `undefined` (automatically set to the width of the ComboBox
382 * field if {@link #matchFieldWidth} is true)
388 initComponent: function() {
390 isDefined = Ext.isDefined,
392 transform = me.transform,
393 transformSelect, isLocalMode;
395 Ext.applyIf(me.renderSelectors, {
396 hiddenDataEl: '.' + me.hiddenDataCls.split(' ').join('.')
400 if (me.typeAhead && me.multiSelect) {
401 Ext.Error.raise('typeAhead and multiSelect are mutually exclusive options -- please remove one of them.');
403 if (me.typeAhead && !me.editable) {
404 Ext.Error.raise('If typeAhead is enabled the combo must be editable: true -- please change one of those settings.');
406 if (me.selectOnFocus && !me.editable) {
407 Ext.Error.raise('If selectOnFocus is enabled the combo must be editable: true -- please change one of those settings.');
414 * Fires before all queries are processed. Return false to cancel the query or set the queryEvent's cancel
417 * @param {Object} queryEvent An object that has these properties:
419 * - `combo` : Ext.form.field.ComboBox
427 * - `forceAll` : Boolean
429 * True to force "all" query
431 * - `cancel` : Boolean
433 * Set to true to cancel the query
439 * Fires when at least one list item is selected.
440 * @param {Ext.form.field.ComboBox} combo This combo box
441 * @param {Array} records The selected records
446 * @event beforeselect
447 * Fires before the selected item is added to the collection
448 * @param {Ext.form.field.ComboBox} combo This combo box
449 * @param {Ext.data.Record} record The selected record
450 * @param {Number} index The index of the selected record
455 * @event beforedeselect
456 * Fires before the deselected item is removed from the collection
457 * @param {Ext.form.field.ComboBox} combo This combo box
458 * @param {Ext.data.Record} record The deselected record
459 * @param {Number} index The index of the deselected record
464 // Build store from 'transform' HTML select element's options
466 transformSelect = Ext.getDom(transform);
467 if (transformSelect) {
468 store = Ext.Array.map(Ext.Array.from(transformSelect.options), function(option) {
469 return [option.value, option.text];
472 me.name = transformSelect.name;
474 if (!('value' in me)) {
475 me.value = transformSelect.value;
480 me.bindStore(store || 'ext-empty-store', true);
482 if (store.autoCreated) {
483 me.queryMode = 'local';
484 me.valueField = me.displayField = 'field1';
485 if (!store.expanded) {
486 me.displayField = 'field2';
491 if (!isDefined(me.valueField)) {
492 me.valueField = me.displayField;
495 isLocalMode = me.queryMode === 'local';
496 if (!isDefined(me.queryDelay)) {
497 me.queryDelay = isLocalMode ? 10 : 500;
499 if (!isDefined(me.minChars)) {
500 me.minChars = isLocalMode ? 0 : 4;
503 if (!me.displayTpl) {
504 me.displayTpl = Ext.create('Ext.XTemplate',
506 '{[typeof values === "string" ? values : values["' + me.displayField + '"]]}' +
507 '<tpl if="xindex < xcount">' + me.delimiter + '</tpl>' +
510 } else if (Ext.isString(me.displayTpl)) {
511 me.displayTpl = Ext.create('Ext.XTemplate', me.displayTpl);
516 me.doQueryTask = Ext.create('Ext.util.DelayedTask', me.doRawQuery, me);
518 // store has already been loaded, setValue
519 if (me.store.getCount() > 0) {
520 me.setValue(me.value);
523 // render in place of 'transform' select
524 if (transformSelect) {
525 me.render(transformSelect.parentNode, transformSelect);
526 Ext.removeNode(transformSelect);
532 * Returns the store associated with this ComboBox.
533 * @return {Ext.data.Store} The store
535 getStore : function(){
539 beforeBlur: function() {
540 this.doQueryTask.cancel();
545 assertValue: function() {
547 value = me.getRawValue(),
550 if (me.forceSelection) {
551 if (me.multiSelect) {
552 // For multiselect, check that the current displayed value matches the current
553 // selection, if it does not then revert to the most recent selection.
554 if (value !== me.getDisplayValue()) {
555 me.setValue(me.lastSelection);
558 // For single-select, match the displayed value to a record and select it,
559 // if it does not match a record then revert to the most recent selection.
560 rec = me.findRecordByDisplay(value);
564 me.setValue(me.lastSelection);
571 onTypeAhead: function() {
573 displayField = me.displayField,
574 record = me.store.findRecord(displayField, me.getRawValue()),
575 boundList = me.getPicker(),
576 newValue, len, selStart;
579 newValue = record.get(displayField);
580 len = newValue.length;
581 selStart = me.getRawValue().length;
583 boundList.highlightItem(boundList.getNode(record));
585 if (selStart !== 0 && selStart !== len) {
586 me.setRawValue(newValue);
587 me.selectText(selStart, newValue.length);
592 // invoked when a different store is bound to this combo
594 resetToDefault: function() {
598 bindStore: function(store, initial) {
602 // this code directly accesses this.picker, bc invoking getPicker
603 // would create it when we may be preping to destroy it
604 if (oldStore && !initial) {
605 if (oldStore !== store && oldStore.autoDestroy) {
606 oldStore.destroyStore();
611 exception: me.collapse
617 me.picker.bindStore(null);
626 me.store = Ext.data.StoreManager.lookup(store);
630 exception: me.collapse
634 me.picker.bindStore(store);
643 // If performing a remote query upon the raw value...
647 if (me.picker && !me.picker.getSelectionModel().hasSelection()) {
651 // If store initial load or triggerAction: 'all' trigger click.
653 // Set the value on load
655 me.setValue(me.value);
658 // Highlight the first item in the list if autoSelect: true
659 if (me.store.getCount()) {
670 * Execute the query with the raw contents within the textfield.
672 doRawQuery: function() {
673 this.doQuery(this.getRawValue(), false, true);
677 * Executes a query to filter the dropdown list. Fires the {@link #beforequery} event prior to performing the query
678 * allowing the query action to be canceled if needed.
680 * @param {String} queryString The SQL query to execute
681 * @param {Boolean} [forceAll=false] `true` to force the query to execute even if there are currently fewer characters in
682 * the field than the minimum specified by the `{@link #minChars}` config option. It also clears any filter
683 * previously saved in the current store.
684 * @param {Boolean} [rawQuery=false] Pass as true if the raw typed value is being used as the query string. This causes the
685 * resulting store load to leave the raw value undisturbed.
686 * @return {Boolean} true if the query was permitted to run, false if it was cancelled by a {@link #beforequery}
689 doQuery: function(queryString, forceAll, rawQuery) {
690 queryString = queryString || '';
692 // store in object and pass by reference in 'beforequery'
693 // so that client code can modify values.
702 isLocalMode = me.queryMode === 'local';
704 if (me.fireEvent('beforequery', qe) === false || qe.cancel) {
708 // get back out possibly modified values
709 queryString = qe.query;
710 forceAll = qe.forceAll;
712 // query permitted to run
713 if (forceAll || (queryString.length >= me.minChars)) {
714 // expand before starting query so LoadMask can position itself correctly
717 // make sure they aren't querying the same thing
718 if (!me.queryCaching || me.lastQuery !== queryString) {
719 me.lastQuery = queryString;
722 // forceAll means no filtering - show whole dataset.
726 // Clear filter, but supress event so that the BoundList is not immediately updated.
727 store.clearFilter(true);
728 store.filter(me.displayField, queryString);
731 // Set flag for onLoad handling to know how the Store was loaded
732 me.rawQuery = rawQuery;
734 // In queryMode: 'remote', we assume Store filters are added by the developer as remote filters,
735 // and these are automatically passed as params with every load call, so we do *not* call clearFilter.
737 // if we're paging, we've changed the query so start at page 1.
741 params: me.getParams(queryString)
747 // Clear current selection if it does not match the current value in the field
748 if (me.getRawValue() !== me.getDisplayValue()) {
749 me.ignoreSelection++;
750 me.picker.getSelectionModel().deselectAll();
751 me.ignoreSelection--;
764 loadPage: function(pageNum){
765 this.store.loadPage(pageNum, {
766 params: this.getParams(this.lastQuery)
770 onPageChange: function(toolbar, newPage){
772 * Return false here so we can call load ourselves and inject the query param.
773 * We don't want to do this for every store load since the developer may load
774 * the store through some other means so we won't add the query param.
776 this.loadPage(newPage);
781 getParams: function(queryString) {
783 param = this.queryParam;
786 params[param] = queryString;
793 * If the autoSelect config is true, and the picker is open, highlights the first item.
795 doAutoSelect: function() {
798 lastSelected, itemNode;
799 if (picker && me.autoSelect && me.store.getCount() > 0) {
800 // Highlight the last selected item and scroll it into view
801 lastSelected = picker.getSelectionModel().lastSelected;
802 itemNode = picker.getNode(lastSelected || 0);
804 picker.highlightItem(itemNode);
805 picker.listEl.scrollChildIntoView(itemNode, false);
810 doTypeAhead: function() {
811 if (!this.typeAheadTask) {
812 this.typeAheadTask = Ext.create('Ext.util.DelayedTask', this.onTypeAhead, this);
814 if (this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE) {
815 this.typeAheadTask.delay(this.typeAheadDelay);
819 onTriggerClick: function() {
821 if (!me.readOnly && !me.disabled) {
826 if (me.triggerAction === 'all') {
827 me.doQuery(me.allQuery, true);
829 me.doQuery(me.getRawValue(), false, true);
837 // store the last key and doQuery if relevant
838 onKeyUp: function(e, t) {
842 if (!me.readOnly && !me.disabled && me.editable) {
844 // we put this in a task so that we can cancel it if a user is
845 // in and out before the queryDelay elapses
847 // perform query w/ any normal key or backspace or delete
848 if (!e.isSpecialKey() || key == e.BACKSPACE || key == e.DELETE) {
849 me.doQueryTask.delay(me.queryDelay);
853 if (me.enableKeyEvents) {
854 me.callParent(arguments);
858 initEvents: function() {
863 * Setup keyboard handling. If enableKeyEvents is true, we already have
864 * a listener on the inputEl for keyup, so don't create a second.
866 if (!me.enableKeyEvents) {
867 me.mon(me.inputEl, 'keyup', me.onKeyUp, me);
871 onDestroy: function(){
872 this.bindStore(null);
876 createPicker: function() {
879 menuCls = Ext.baseCSSPrefix + 'menu',
883 mode: me.multiSelect ? 'SIMPLE' : 'SINGLE'
888 cls: me.el.up('.' + menuCls) ? menuCls : '',
890 displayField: me.displayField,
891 focusOnToFront: false,
892 pageSize: me.pageSize,
894 }, me.listConfig, me.defaultListConfig);
896 picker = me.picker = Ext.create('Ext.view.BoundList', opts);
898 picker.pagingToolbar.on('beforechange', me.onPageChange, me);
902 itemclick: me.onItemClick,
903 refresh: me.onListRefresh,
907 me.mon(picker.getSelectionModel(), {
908 'beforeselect': me.onBeforeSelect,
909 'beforedeselect': me.onBeforeDeselect,
910 'selectionchange': me.onListSelectionChange,
917 alignPicker: function(){
920 heightAbove = me.getPosition()[1] - Ext.getBody().getScroll().top,
921 heightBelow = Ext.Element.getViewHeight() - heightAbove - me.getHeight(),
922 space = Math.max(heightAbove, heightBelow);
925 if (picker.getHeight() > space) {
926 picker.setHeight(space - 5); // have some leeway so we aren't flush against
931 onListRefresh: function() {
933 this.syncSelection();
936 onItemClick: function(picker, record){
938 * If we're doing single selection, the selection change events won't fire when
939 * clicking on the selected element. Detect it here.
942 lastSelection = me.lastSelection,
943 valueField = me.valueField,
946 if (!me.multiSelect && lastSelection) {
947 selected = lastSelection[0];
948 if (selected && (record.get(valueField) === selected.get(valueField))) {
949 // Make sure we also update the display value if it's only partial
950 me.displayTplData = [record.data];
951 me.setRawValue(me.getDisplayValue());
957 onBeforeSelect: function(list, record) {
958 return this.fireEvent('beforeselect', this, record, record.index);
961 onBeforeDeselect: function(list, record) {
962 return this.fireEvent('beforedeselect', this, record, record.index);
965 onListSelectionChange: function(list, selectedRecords) {
967 isMulti = me.multiSelect,
968 hasRecords = selectedRecords.length > 0;
969 // Only react to selection if it is not called from setValue, and if our list is
970 // expanded (ignores changes to the selection model triggered elsewhere)
971 if (!me.ignoreSelection && me.isExpanded) {
973 Ext.defer(me.collapse, 1, me);
976 * Only set the value here if we're in multi selection mode or we have
977 * a selection. Otherwise setValue will be called with an empty value
978 * which will cause the change event to fire twice.
980 if (isMulti || hasRecords) {
981 me.setValue(selectedRecords, false);
984 me.fireEvent('select', me, selectedRecords);
992 * Enables the key nav for the BoundList when it is expanded.
994 onExpand: function() {
996 keyNav = me.listKeyNav,
997 selectOnTab = me.selectOnTab,
998 picker = me.getPicker();
1000 // Handle BoundList navigation from the input field. Insert a tab listener specially to enable selectOnTab.
1004 keyNav = me.listKeyNav = Ext.create('Ext.view.BoundListKeyNav', this.inputEl, {
1009 this.selectHighlighted(e);
1012 // Tab key event is allowed to propagate to field
1018 // While list is expanded, stop tab monitoring from Ext.form.field.Trigger so it doesn't short-circuit selectOnTab
1020 me.ignoreMonitorTab = true;
1023 Ext.defer(keyNav.enable, 1, keyNav); //wait a bit so it doesn't react to the down arrow opening the picker
1029 * Disables the key nav for the BoundList when it is collapsed.
1031 onCollapse: function() {
1033 keyNav = me.listKeyNav;
1036 me.ignoreMonitorTab = false;
1041 * Selects an item by a {@link Ext.data.Model Model}, or by a key value.
1044 select: function(r) {
1045 this.setValue(r, true);
1049 * Finds the record by searching for a specific field/value combination.
1050 * @param {String} field The name of the field to test.
1051 * @param {Object} value The value to match the field against.
1052 * @return {Ext.data.Model} The matched record or false.
1054 findRecord: function(field, value) {
1055 var ds = this.store,
1056 idx = ds.findExact(field, value);
1057 return idx !== -1 ? ds.getAt(idx) : false;
1061 * Finds the record by searching values in the {@link #valueField}.
1062 * @param {Object} value The value to match the field against.
1063 * @return {Ext.data.Model} The matched record or false.
1065 findRecordByValue: function(value) {
1066 return this.findRecord(this.valueField, value);
1070 * Finds the record by searching values in the {@link #displayField}.
1071 * @param {Object} value The value to match the field against.
1072 * @return {Ext.data.Model} The matched record or false.
1074 findRecordByDisplay: function(value) {
1075 return this.findRecord(this.displayField, value);
1079 * Sets the specified value(s) into the field. For each value, if a record is found in the {@link #store} that
1080 * matches based on the {@link #valueField}, then that record's {@link #displayField} will be displayed in the
1081 * field. If no match is found, and the {@link #valueNotFoundText} config option is defined, then that will be
1082 * displayed as the default field text. Otherwise a blank value will be shown, although the value will still be set.
1083 * @param {String/String[]} value The value(s) to be set. Can be either a single String or {@link Ext.data.Model},
1084 * or an Array of Strings or Models.
1085 * @return {Ext.form.field.Field} this
1087 setValue: function(value, doSelect) {
1089 valueNotFoundText = me.valueNotFoundText,
1090 inputEl = me.inputEl,
1093 displayTplData = [],
1094 processedValue = [];
1096 if (me.store.loading) {
1097 // Called while the Store is loading. Ensure it is processed by the onLoad method.
1099 me.setHiddenValue(me.value);
1103 // This method processes multi-values, so ensure value is an array.
1104 value = Ext.Array.from(value);
1106 // Loop through values
1107 for (i = 0, len = value.length; i < len; i++) {
1109 if (!record || !record.isModel) {
1110 record = me.findRecordByValue(record);
1112 // record found, select it.
1114 models.push(record);
1115 displayTplData.push(record.data);
1116 processedValue.push(record.get(me.valueField));
1118 // record was not found, this could happen because
1119 // store is not loaded or they set a value not in the store
1121 // If we are allowing insertion of values not represented in the Store, then set the value, and the display value
1122 if (!me.forceSelection) {
1123 displayTplData.push(value[i]);
1124 processedValue.push(value[i]);
1126 // Else, if valueNotFoundText is defined, display it, otherwise display nothing for this value
1127 else if (Ext.isDefined(valueNotFoundText)) {
1128 displayTplData.push(valueNotFoundText);
1133 // Set the value of this field. If we are multiselecting, then that is an array.
1134 me.setHiddenValue(processedValue);
1135 me.value = me.multiSelect ? processedValue : processedValue[0];
1136 if (!Ext.isDefined(me.value)) {
1139 me.displayTplData = displayTplData; //store for getDisplayValue method
1140 me.lastSelection = me.valueModels = models;
1142 if (inputEl && me.emptyText && !Ext.isEmpty(value)) {
1143 inputEl.removeCls(me.emptyCls);
1146 // Calculate raw value from the collection of Model data
1147 me.setRawValue(me.getDisplayValue());
1150 if (doSelect !== false) {
1153 me.applyEmptyText();
1160 * Set the value of {@link #hiddenDataEl}
1161 * Dynamically adds and removes input[type=hidden] elements
1163 setHiddenValue: function(values){
1165 if (!me.hiddenDataEl) {
1168 values = Ext.Array.from(values);
1169 var dom = me.hiddenDataEl.dom,
1170 childNodes = dom.childNodes,
1171 input = childNodes[0],
1172 valueCount = values.length,
1173 childrenCount = childNodes.length;
1175 if (!input && valueCount > 0) {
1176 me.hiddenDataEl.update(Ext.DomHelper.markup({tag:'input', type:'hidden', name:me.name}));
1178 input = dom.firstChild;
1180 while (childrenCount > valueCount) {
1181 dom.removeChild(childNodes[0]);
1184 while (childrenCount < valueCount) {
1185 dom.appendChild(input.cloneNode(true));
1188 for (i = 0; i < valueCount; i++) {
1189 childNodes[i].value = values[i];
1194 * @private Generates the string value to be displayed in the text field for the currently stored value
1196 getDisplayValue: function() {
1197 return this.displayTpl.apply(this.displayTplData);
1200 getValue: function() {
1201 // If the user has not changed the raw field value since a value was selected from the list,
1202 // then return the structured value from the selection. If the raw field value is different
1203 // than what would be displayed due to selection, return that raw value.
1206 rawValue = me.getRawValue(), //current value of text field
1207 value = me.value; //stored value from last selection or setValue() call
1209 if (me.getDisplayValue() !== rawValue) {
1211 me.value = me.displayTplData = me.valueModels = null;
1213 me.ignoreSelection++;
1214 picker.getSelectionModel().deselectAll();
1215 me.ignoreSelection--;
1222 getSubmitValue: function() {
1223 return this.getValue();
1226 isEqual: function(v1, v2) {
1227 var fromArray = Ext.Array.from,
1234 if (len !== v2.length) {
1238 for(i = 0; i < len; i++) {
1239 if (v2[i] !== v1[i]) {
1248 * Clears any value currently set in the ComboBox.
1250 clearValue: function() {
1255 * @private Synchronizes the selection in the picker to match the current value of the combobox.
1257 syncSelection: function() {
1259 ExtArray = Ext.Array,
1261 selection, selModel;
1263 // From the value, find the Models that are in the store's current data
1265 ExtArray.forEach(me.valueModels || [], function(value) {
1266 if (value && value.isModel && me.store.indexOf(value) >= 0) {
1267 selection.push(value);
1271 // Update the selection to match
1272 me.ignoreSelection++;
1273 selModel = picker.getSelectionModel();
1274 selModel.deselectAll();
1275 if (selection.length) {
1276 selModel.select(selection);
1278 me.ignoreSelection--;