X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/6e39d509471fe9b4e2660e0d1631b350d0c66f40..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/docs/source/ComboBox.html diff --git a/docs/source/ComboBox.html b/docs/source/ComboBox.html new file mode 100644 index 00000000..2d089727 --- /dev/null +++ b/docs/source/ComboBox.html @@ -0,0 +1,1022 @@ +
\ No newline at end of file/** + * @class Ext.form.field.ComboBox + * @extends Ext.form.field.Picker + * + * A combobox control with support for autocomplete, remote loading, and many other features. + * + * A ComboBox is like a combination of a traditional HTML text `<input>` field and a `<select>` + * field; the user is able to type freely into the field, and/or pick values from a dropdown selection + * list. The user can input any value by default, even if it does not appear in the selection list; + * to prevent free-form values and restrict them to items in the list, set {@link #forceSelection} to `true`. + * + * The selection list's options are populated from any {@link Ext.data.Store}, including remote + * stores. The data items in the store are mapped to each option's displayed text and backing value via + * the {@link #valueField} and {@link #displayField} configurations, respectively. + * + * If your store is not remote, i.e. it depends only on local data and is loaded up front, you should be + * sure to set the {@link #queryMode} to `'local'`, as this will improve responsiveness for the user. + * + * {@img Ext.form.ComboBox/Ext.form.ComboBox.png Ext.form.ComboBox component} + * + * ## Example usage: + * + * // The data store containing the list of states + * var states = Ext.create('Ext.data.Store', { + * fields: ['abbr', 'name'], + * data : [ + * {"abbr":"AL", "name":"Alabama"}, + * {"abbr":"AK", "name":"Alaska"}, + * {"abbr":"AZ", "name":"Arizona"} + * //... + * ] + * }); + * + * // Create the combo box, attached to the states data store + * Ext.create('Ext.form.ComboBox', { + * fieldLabel: 'Choose State', + * store: states, + * queryMode: 'local', + * displayField: 'name', + * valueField: 'abbr', + * renderTo: Ext.getBody() + * }); + * + * ## Events + * + * To do something when something in ComboBox is selected, configure the select event: + * + * var cb = Ext.create('Ext.form.ComboBox', { + * // all of your config options + * listeners:{ + * scope: yourScope, + * 'select': yourFunction + * } + * }); + * + * // Alternatively, you can assign events after the object is created: + * var cb = new Ext.form.field.ComboBox(yourOptions); + * cb.on('select', yourFunction, yourScope); + * + * ## Multiple Selection + * + * ComboBox also allows selection of multiple items from the list; to enable multi-selection set the + * {@link #multiSelect} config to `true`. + * + * @constructor + * Create a new ComboBox. + * @param {Object} config Configuration options + * @xtype combo + * @docauthor Jason Johnston <jason@sencha.com> + */ +Ext.define('Ext.form.field.ComboBox', { + extend:'Ext.form.field.Picker', + requires: ['Ext.util.DelayedTask', 'Ext.EventObject', 'Ext.view.BoundList', 'Ext.view.BoundListKeyNav', 'Ext.data.StoreManager'], + alternateClassName: 'Ext.form.ComboBox', + alias: ['widget.combobox', 'widget.combo'], + + /** + * @cfg {String} triggerCls + * An additional CSS class used to style the trigger button. The trigger will always get the + * {@link #triggerBaseCls} by default and <tt>triggerCls</tt> will be <b>appended</b> if specified. + * Defaults to 'x-form-arrow-trigger' for ComboBox. + */ + triggerCls: Ext.baseCSSPrefix + 'form-arrow-trigger', + + /** + * @cfg {Ext.data.Store/Array} store The data source to which this combo is bound (defaults to <tt>undefined</tt>). + * Acceptable values for this property are: + * <div class="mdetail-params"><ul> + * <li><b>any {@link Ext.data.Store Store} subclass</b></li> + * <li><b>an Array</b> : Arrays will be converted to a {@link Ext.data.Store} internally, + * automatically generating {@link Ext.data.Field#name field names} to work with all data components. + * <div class="mdetail-params"><ul> + * <li><b>1-dimensional array</b> : (e.g., <tt>['Foo','Bar']</tt>)<div class="sub-desc"> + * A 1-dimensional array will automatically be expanded (each array item will be used for both the combo + * {@link #valueField} and {@link #displayField})</div></li> + * <li><b>2-dimensional array</b> : (e.g., <tt>[['f','Foo'],['b','Bar']]</tt>)<div class="sub-desc"> + * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo + * {@link #valueField}, while the value at index 1 is assumed to be the combo {@link #displayField}. + * </div></li></ul></div></li></ul></div> + * <p>See also <tt>{@link #queryMode}</tt>.</p> + */ + + /** + * @cfg {Boolean} multiSelect + * If set to <tt>true</tt>, allows the combo field to hold more than one value at a time, and allows selecting + * multiple items from the dropdown list. The combo's text field will show all selected values separated by + * the {@link #delimiter}. (Defaults to <tt>false</tt>.) + */ + multiSelect: false, + + /** + * @cfg {String} delimiter + * The character(s) used to separate the {@link #displayField display values} of multiple selected items + * when <tt>{@link #multiSelect} = true</tt>. Defaults to <tt>', '</tt>. + */ + delimiter: ', ', + + /** + * @cfg {String} displayField The underlying {@link Ext.data.Field#name data field name} to bind to this + * ComboBox (defaults to 'text'). + * <p>See also <tt>{@link #valueField}</tt>.</p> + */ + displayField: 'text', + + /** + * @cfg {String} valueField + * @required + * The underlying {@link Ext.data.Field#name data value name} to bind to this ComboBox (defaults to match + * the value of the {@link #displayField} config). + * <p><b>Note</b>: use of a <tt>valueField</tt> requires the user to make a selection in order for a value to be + * mapped. See also <tt>{@link #displayField}</tt>.</p> + */ + + /** + * @cfg {String} triggerAction The action to execute when the trigger is clicked. + * <div class="mdetail-params"><ul> + * <li><b><tt>'all'</tt></b> : <b>Default</b> + * <p class="sub-desc">{@link #doQuery run the query} specified by the <tt>{@link #allQuery}</tt> config option</p></li> + * <li><b><tt>'query'</tt></b> : + * <p class="sub-desc">{@link #doQuery run the query} using the {@link Ext.form.field.Base#getRawValue raw value}.</p></li> + * </ul></div> + * <p>See also <code>{@link #queryParam}</code>.</p> + */ + triggerAction: 'all', + + /** + * @cfg {String} allQuery The text query to send to the server to return all records for the list + * with no filtering (defaults to '') + */ + allQuery: '', + + /** + * @cfg {String} queryParam Name of the query ({@link Ext.data.Store#baseParam baseParam} name for the store) + * as it will be passed on the querystring (defaults to <tt>'query'</tt>) + */ + queryParam: 'query', + + /** + * @cfg {String} queryMode + * The mode for queries. Acceptable values are: + * <div class="mdetail-params"><ul> + * <li><b><tt>'remote'</tt></b> : <b>Default</b> + * <p class="sub-desc">Automatically loads the <tt>{@link #store}</tt> the <b>first</b> time the trigger + * is clicked. If you do not want the store to be automatically loaded the first time the trigger is + * clicked, set to <tt>'local'</tt> and manually load the store. To force a requery of the store + * <b>every</b> time the trigger is clicked see <tt>{@link #lastQuery}</tt>.</p></li> + * <li><b><tt>'local'</tt></b> : + * <p class="sub-desc">ComboBox loads local data</p> + * <pre><code> +var combo = new Ext.form.field.ComboBox({ + renderTo: document.body, + queryMode: 'local', + store: new Ext.data.ArrayStore({ + id: 0, + fields: [ + 'myId', // numeric value is the key + 'displayText' + ], + data: [[1, 'item1'], [2, 'item2']] // data is local + }), + valueField: 'myId', + displayField: 'displayText', + triggerAction: 'all' +}); + * </code></pre></li> + * </ul></div> + */ + queryMode: 'remote', + + queryCaching: true, + + /** + * @cfg {Number} pageSize If greater than <tt>0</tt>, a {@link Ext.toolbar.Paging} is displayed in the + * footer of the dropdown list and the {@link #doQuery filter queries} will execute with page start and + * {@link Ext.toolbar.Paging#pageSize limit} parameters. Only applies when <tt>{@link #queryMode} = 'remote'</tt> + * (defaults to <tt>0</tt>). + */ + pageSize: 0, + + /** + * @cfg {Number} queryDelay The length of time in milliseconds to delay between the start of typing and + * sending the query to filter the dropdown list (defaults to <tt>500</tt> if <tt>{@link #queryMode} = 'remote'</tt> + * or <tt>10</tt> if <tt>{@link #queryMode} = 'local'</tt>) + */ + + /** + * @cfg {Number} minChars The minimum number of characters the user must type before autocomplete and + * {@link #typeAhead} activate (defaults to <tt>4</tt> if <tt>{@link #queryMode} = 'remote'</tt> or <tt>0</tt> if + * <tt>{@link #queryMode} = 'local'</tt>, does not apply if <tt>{@link Ext.form.field.Trigger#editable editable} = false</tt>). + */ + + /** + * @cfg {Boolean} autoSelect <tt>true</tt> to select the first result gathered by the data store (defaults + * to <tt>true</tt>). A false value would require a manual selection from the dropdown list to set the components value + * unless the value of ({@link #typeAhead}) were true. + */ + autoSelect: true, + + /** + * @cfg {Boolean} typeAhead <tt>true</tt> to populate and autoselect the remainder of the text being + * typed after a configurable delay ({@link #typeAheadDelay}) if it matches a known value (defaults + * to <tt>false</tt>) + */ + typeAhead: false, + + /** + * @cfg {Number} typeAheadDelay The length of time in milliseconds to wait until the typeahead text is displayed + * if <tt>{@link #typeAhead} = true</tt> (defaults to <tt>250</tt>) + */ + typeAheadDelay: 250, + + /** + * @cfg {Boolean} selectOnTab + * Whether the Tab key should select the currently highlighted item. Defaults to <tt>true</tt>. + */ + selectOnTab: true, + + /** + * @cfg {Boolean} forceSelection <tt>true</tt> to restrict the selected value to one of the values in the list, + * <tt>false</tt> to allow the user to set arbitrary text into the field (defaults to <tt>false</tt>) + */ + forceSelection: false, + + /** + * @cfg {String} valueNotFoundText When using a name/value combo, if the value passed to setValue is not found in + * the store, valueNotFoundText will be displayed as the field text if defined (defaults to undefined). If this + * default text is used, it means there is no value set and no validation will occur on this field. + */ + + /** + * The value of the match string used to filter the store. Delete this property to force a requery. + * Example use: + * <pre><code> +var combo = new Ext.form.field.ComboBox({ + ... + queryMode: 'remote', + listeners: { + // delete the previous query in the beforequery event or set + // combo.lastQuery = null (this will reload the store the next time it expands) + beforequery: function(qe){ + delete qe.combo.lastQuery; + } + } +}); + * </code></pre> + * To make sure the filter in the store is not cleared the first time the ComboBox trigger is used + * configure the combo with <tt>lastQuery=''</tt>. Example use: + * <pre><code> +var combo = new Ext.form.field.ComboBox({ + ... + queryMode: 'local', + triggerAction: 'all', + lastQuery: '' +}); + * </code></pre> + * @property lastQuery + * @type String + */ + + /** + * @cfg {Object} defaultListConfig + * Set of options that will be used as defaults for the user-configured {@link #listConfig} object. + */ + defaultListConfig: { + emptyText: '', + loadingText: 'Loading...', + loadingHeight: 70, + minWidth: 70, + maxHeight: 300, + shadow: 'sides' + }, + + /** + * @cfg {Mixed} transform + * The id, DOM node or {@link Ext.core.Element} of an existing HTML <tt><select></tt> element to + * convert into a ComboBox. The target select's options will be used to build the options in the ComboBox + * dropdown; a configured {@link #store} will take precedence over this. + */ + + /** + * @cfg {Object} listConfig + * <p>An optional set of configuration properties that will be passed to the {@link Ext.view.BoundList}'s + * constructor. Any configuration that is valid for BoundList can be included. Some of the more useful + * ones are:</p> + * <ul> + * <li>{@link Ext.view.BoundList#cls} - defaults to empty</li> + * <li>{@link Ext.view.BoundList#emptyText} - defaults to empty string</li> + * <li>{@link Ext.view.BoundList#getInnerTpl} - defaults to the template defined in BoundList</li> + * <li>{@link Ext.view.BoundList#itemSelector} - defaults to the value defined in BoundList</li> + * <li>{@link Ext.view.BoundList#loadingText} - defaults to <tt>'Loading...'</tt></li> + * <li>{@link Ext.view.BoundList#minWidth} - defaults to <tt>70</tt></li> + * <li>{@link Ext.view.BoundList#maxWidth} - defaults to <tt>undefined</tt></li> + * <li>{@link Ext.view.BoundList#maxHeight} - defaults to <tt>300</tt></li> + * <li>{@link Ext.view.BoundList#resizable} - defaults to <tt>false</tt></li> + * <li>{@link Ext.view.BoundList#shadow} - defaults to <tt>'sides'</tt></li> + * <li>{@link Ext.view.BoundList#width} - defaults to <tt>undefined</tt> (automatically set to the width + * of the ComboBox field if {@link #matchFieldWidth} is true)</li> + * </ul> + */ + + //private + ignoreSelection: 0, + + initComponent: function() { + var me = this, + isDefined = Ext.isDefined, + store = me.store, + transform = me.transform, + transformSelect, isLocalMode; + + //<debug> + if (!store && !transform) { + Ext.Error.raise('Either a valid store, or a HTML select to transform, must be configured on the combo.'); + } + if (me.typeAhead && me.multiSelect) { + Ext.Error.raise('typeAhead and multiSelect are mutually exclusive options -- please remove one of them.'); + } + if (me.typeAhead && !me.editable) { + Ext.Error.raise('If typeAhead is enabled the combo must be editable: true -- please change one of those settings.'); + } + if (me.selectOnFocus && !me.editable) { + Ext.Error.raise('If selectOnFocus is enabled the combo must be editable: true -- please change one of those settings.'); + } + //</debug> + + this.addEvents( + // TODO need beforeselect? + + /** + * @event beforequery + * Fires before all queries are processed. Return false to cancel the query or set the queryEvent's + * cancel property to true. + * @param {Object} queryEvent An object that has these properties:<ul> + * <li><code>combo</code> : Ext.form.field.ComboBox <div class="sub-desc">This combo box</div></li> + * <li><code>query</code> : String <div class="sub-desc">The query string</div></li> + * <li><code>forceAll</code> : Boolean <div class="sub-desc">True to force "all" query</div></li> + * <li><code>cancel</code> : Boolean <div class="sub-desc">Set to true to cancel the query</div></li> + * </ul> + */ + 'beforequery', + + /* + * @event select + * Fires when at least one list item is selected. + * @param {Ext.form.field.ComboBox} combo This combo box + * @param {Array} records The selected records + */ + 'select' + ); + + // Build store from 'transform' HTML select element's options + if (!store && transform) { + transformSelect = Ext.getDom(transform); + if (transformSelect) { + store = Ext.Array.map(Ext.Array.from(transformSelect.options), function(option) { + return [option.value, option.text]; + }); + if (!me.name) { + me.name = transformSelect.name; + } + if (!('value' in me)) { + me.value = transformSelect.value; + } + } + } + + me.bindStore(store, true); + store = me.store; + if (store.autoCreated) { + me.queryMode = 'local'; + me.valueField = me.displayField = 'field1'; + if (!store.expanded) { + me.displayField = 'field2'; + } + } + + + if (!isDefined(me.valueField)) { + me.valueField = me.displayField; + } + + isLocalMode = me.queryMode === 'local'; + if (!isDefined(me.queryDelay)) { + me.queryDelay = isLocalMode ? 10 : 500; + } + if (!isDefined(me.minChars)) { + me.minChars = isLocalMode ? 0 : 4; + } + + if (!me.displayTpl) { + me.displayTpl = Ext.create('Ext.XTemplate', + '<tpl for=".">' + + '{[typeof values === "string" ? values : values.' + me.displayField + ']}' + + '<tpl if="xindex < xcount">' + me.delimiter + '</tpl>' + + '</tpl>' + ); + } else if (Ext.isString(me.displayTpl)) { + me.displayTpl = Ext.create('Ext.XTemplate', me.displayTpl); + } + + me.callParent(); + + me.doQueryTask = Ext.create('Ext.util.DelayedTask', me.doRawQuery, me); + + // store has already been loaded, setValue + if (me.store.getCount() > 0) { + me.setValue(me.value); + } + + // render in place of 'transform' select + if (transformSelect) { + me.render(transformSelect.parentNode, transformSelect); + Ext.removeNode(transformSelect); + delete me.renderTo; + } + }, + + beforeBlur: function() { + var me = this; + me.doQueryTask.cancel(); + if (me.forceSelection) { + me.assertValue(); + } else { + me.collapse(); + } + }, + + // private + assertValue: function() { + var me = this, + value = me.getRawValue(), + rec; + + if (me.multiSelect) { + // For multiselect, check that the current displayed value matches the current + // selection, if it does not then revert to the most recent selection. + if (value !== me.getDisplayValue()) { + me.setValue(me.lastSelection); + } + } else { + // For single-select, match the displayed value to a record and select it, + // if it does not match a record then revert to the most recent selection. + rec = me.findRecordByDisplay(value); + if (rec) { + me.select(rec); + } else { + me.setValue(me.lastSelection); + } + } + me.collapse(); + }, + + onTypeAhead: function() { + var me = this, + displayField = me.displayField, + record = me.store.findRecord(displayField, me.getRawValue()), + boundList = me.getPicker(), + newValue, len, selStart; + + if (record) { + newValue = record.get(displayField); + len = newValue.length; + selStart = me.getRawValue().length; + + boundList.highlightItem(boundList.getNode(record)); + + if (selStart !== 0 && selStart !== len) { + me.setRawValue(newValue); + me.selectText(selStart, newValue.length); + } + } + }, + + // invoked when a different store is bound to this combo + // than the original + resetToDefault: function() { + + }, + + bindStore: function(store, initial) { + var me = this, + oldStore = me.store; + + // this code directly accesses this.picker, bc invoking getPicker + // would create it when we may be preping to destroy it + if (oldStore && !initial) { + if (oldStore !== store && oldStore.autoDestroy) { + oldStore.destroy(); + } else { + oldStore.un({ + scope: me, + load: me.onLoad, + exception: me.collapse + }); + } + if (!store) { + me.store = null; + if (me.picker) { + me.picker.bindStore(null); + } + } + } + if (store) { + if (!initial) { + me.resetToDefault(); + } + + me.store = Ext.data.StoreManager.lookup(store); + me.store.on({ + scope: me, + load: me.onLoad, + exception: me.collapse + }); + + if (me.picker) { + me.picker.bindStore(store); + } + } + }, + + onLoad: function() { + var me = this, + value = me.value; + + me.syncSelection(); + if (me.picker && !me.picker.getSelectionModel().hasSelection()) { + me.doAutoSelect(); + } + }, + + /** + * @private + * Execute the query with the raw contents within the textfield. + */ + doRawQuery: function() { + this.doQuery(this.getRawValue()); + }, + + /** + * Executes a query to filter the dropdown list. Fires the {@link #beforequery} event prior to performing the + * query allowing the query action to be canceled if needed. + * @param {String} queryString The SQL query to execute + * @param {Boolean} forceAll <tt>true</tt> to force the query to execute even if there are currently fewer + * characters in the field than the minimum specified by the <tt>{@link #minChars}</tt> config option. It + * also clears any filter previously saved in the current store (defaults to <tt>false</tt>) + * @return {Boolean} true if the query was permitted to run, false if it was cancelled by a {@link #beforequery} handler. + */ + doQuery: function(queryString, forceAll) { + queryString = queryString || ''; + + // store in object and pass by reference in 'beforequery' + // so that client code can modify values. + var me = this, + qe = { + query: queryString, + forceAll: forceAll, + combo: me, + cancel: false + }, + store = me.store, + isLocalMode = me.queryMode === 'local'; + + if (me.fireEvent('beforequery', qe) === false || qe.cancel) { + return false; + } + + // get back out possibly modified values + queryString = qe.query; + forceAll = qe.forceAll; + + // query permitted to run + if (forceAll || (queryString.length >= me.minChars)) { + // expand before starting query so LoadMask can position itself correctly + me.expand(); + + // make sure they aren't querying the same thing + if (!me.queryCaching || me.lastQuery !== queryString) { + me.lastQuery = queryString; + store.clearFilter(!forceAll); + if (isLocalMode) { + if (!forceAll) { + store.filter(me.displayField, queryString); + } + } else { + store.load({ + params: me.getParams(queryString) + }); + } + } + + // Clear current selection if it does not match the current value in the field + if (me.getRawValue() !== me.getDisplayValue()) { + me.ignoreSelection++; + me.picker.getSelectionModel().deselectAll(); + me.ignoreSelection--; + } + + if (isLocalMode) { + me.doAutoSelect(); + } + if (me.typeAhead) { + me.doTypeAhead(); + } + } + return true; + }, + + // private + getParams: function(queryString) { + var p = {}, + pageSize = this.pageSize; + p[this.queryParam] = queryString; + if (pageSize) { + p.start = 0; + p.limit = pageSize; + } + return p; + }, + + /** + * @private + * If the autoSelect config is true, and the picker is open, highlights the first item. + */ + doAutoSelect: function() { + var me = this, + picker = me.picker, + lastSelected, itemNode; + if (picker && me.autoSelect && me.store.getCount() > 0) { + // Highlight the last selected item and scroll it into view + lastSelected = picker.getSelectionModel().lastSelected; + itemNode = picker.getNode(lastSelected || 0); + if (itemNode) { + picker.highlightItem(itemNode); + picker.listEl.scrollChildIntoView(itemNode, false); + } + } + }, + + doTypeAhead: function() { + if (!this.typeAheadTask) { + this.typeAheadTask = Ext.create('Ext.util.DelayedTask', this.onTypeAhead, this); + } + if (this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE) { + this.typeAheadTask.delay(this.typeAheadDelay); + } + }, + + onTriggerClick: function() { + var me = this; + if (!me.readOnly && !me.disabled) { + if (me.isExpanded) { + me.collapse(); + } else { + me.onFocus({}); + if (me.triggerAction === 'all') { + me.doQuery(me.allQuery, true); + } else { + me.doQuery(me.getRawValue()); + } + } + me.inputEl.focus(); + } + }, + + + // store the last key and doQuery if relevant + onKeyUp: function(e, t) { + var me = this, + key = e.getKey(); + + if (!me.readOnly && !me.disabled && me.editable) { + me.lastKey = key; + // we put this in a task so that we can cancel it if a user is + // in and out before the queryDelay elapses + + // perform query w/ any normal key or backspace or delete + if (!e.isSpecialKey() || key == e.BACKSPACE || key == e.DELETE) { + me.doQueryTask.delay(me.queryDelay); + } + } + }, + + initEvents: function() { + var me = this; + me.callParent(); + + // setup keyboard handling + me.mon(me.inputEl, 'keyup', me.onKeyUp, me); + }, + + createPicker: function() { + var me = this, + picker, + menuCls = Ext.baseCSSPrefix + 'menu', + opts = Ext.apply({ + selModel: { + mode: me.multiSelect ? 'SIMPLE' : 'SINGLE' + }, + floating: true, + hidden: true, + ownerCt: me.ownerCt, + cls: me.el.up('.' + menuCls) ? menuCls : '', + store: me.store, + displayField: me.displayField, + focusOnToFront: false, + pageSize: me.pageSize + }, me.listConfig, me.defaultListConfig); + + picker = me.picker = Ext.create('Ext.view.BoundList', opts); + + me.mon(picker, { + itemclick: me.onItemClick, + refresh: me.onListRefresh, + scope: me + }); + + me.mon(picker.getSelectionModel(), { + selectionChange: me.onListSelectionChange, + scope: me + }); + + return picker; + }, + + onListRefresh: function() { + this.alignPicker(); + this.syncSelection(); + }, + + onItemClick: function(picker, record){ + /* + * If we're doing single selection, the selection change events won't fire when + * clicking on the selected element. Detect it here. + */ + var me = this, + lastSelection = me.lastSelection, + valueField = me.valueField, + selected; + + if (!me.multiSelect && lastSelection) { + selected = lastSelection[0]; + if (record.get(valueField) === selected.get(valueField)) { + me.collapse(); + } + } + }, + + onListSelectionChange: function(list, selectedRecords) { + var me = this; + // Only react to selection if it is not called from setValue, and if our list is + // expanded (ignores changes to the selection model triggered elsewhere) + if (!me.ignoreSelection && me.isExpanded) { + if (!me.multiSelect) { + Ext.defer(me.collapse, 1, me); + } + me.setValue(selectedRecords, false); + if (selectedRecords.length > 0) { + me.fireEvent('select', me, selectedRecords); + } + me.inputEl.focus(); + } + }, + + /** + * @private + * Enables the key nav for the BoundList when it is expanded. + */ + onExpand: function() { + var me = this, + keyNav = me.listKeyNav, + selectOnTab = me.selectOnTab, + picker = me.getPicker(); + + // Handle BoundList navigation from the input field. Insert a tab listener specially to enable selectOnTab. + if (keyNav) { + keyNav.enable(); + } else { + keyNav = me.listKeyNav = Ext.create('Ext.view.BoundListKeyNav', this.inputEl, { + boundList: picker, + forceKeyDown: true, + tab: function(e) { + if (selectOnTab) { + this.selectHighlighted(e); + me.triggerBlur(); + } + // Tab key event is allowed to propagate to field + return true; + } + }); + } + + // While list is expanded, stop tab monitoring from Ext.form.field.Trigger so it doesn't short-circuit selectOnTab + if (selectOnTab) { + me.ignoreMonitorTab = true; + } + + Ext.defer(keyNav.enable, 1, keyNav); //wait a bit so it doesn't react to the down arrow opening the picker + me.inputEl.focus(); + }, + + /** + * @private + * Disables the key nav for the BoundList when it is collapsed. + */ + onCollapse: function() { + var me = this, + keyNav = me.listKeyNav; + if (keyNav) { + keyNav.disable(); + me.ignoreMonitorTab = false; + } + }, + + /** + * Selects an item by a {@link Ext.data.Model Model}, or by a key value. + * @param r + */ + select: function(r) { + this.setValue(r, true); + }, + + /** + * Find the record by searching for a specific field/value combination + * Returns an Ext.data.Record or false + * @private + */ + findRecord: function(field, value) { + var ds = this.store, + idx = ds.findExact(field, value); + return idx !== -1 ? ds.getAt(idx) : false; + }, + findRecordByValue: function(value) { + return this.findRecord(this.valueField, value); + }, + findRecordByDisplay: function(value) { + return this.findRecord(this.displayField, value); + }, + + /** + * Sets the specified value(s) into the field. For each value, if a record is found in the {@link #store} that + * matches based on the {@link #valueField}, then that record's {@link #displayField} will be displayed in the + * field. If no match is found, and the {@link #valueNotFoundText} config option is defined, then that will be + * displayed as the default field text. Otherwise a blank value will be shown, although the value will still be set. + * @param {String|Array} value The value(s) to be set. Can be either a single String or {@link Ext.data.Model}, + * or an Array of Strings or Models. + * @return {Ext.form.field.Field} this + */ + setValue: function(value, doSelect) { + var me = this, + valueNotFoundText = me.valueNotFoundText, + inputEl = me.inputEl, + i, len, record, + models = [], + displayTplData = [], + processedValue = []; + + if (me.store.loading) { + // Called while the Store is loading. Ensure it is processed by the onLoad method. + me.value = value; + return me; + } + + // This method processes multi-values, so ensure value is an array. + value = Ext.Array.from(value); + + // Loop through values + for (i = 0, len = value.length; i < len; i++) { + record = value[i]; + if (!record || !record.isModel) { + record = me.findRecordByValue(record); + } + // record found, select it. + if (record) { + models.push(record); + displayTplData.push(record.data); + processedValue.push(record.get(me.valueField)); + } + // record was not found, this could happen because + // store is not loaded or they set a value not in the store + else { + // if valueNotFoundText is defined, display it, otherwise display nothing for this value + if (Ext.isDefined(valueNotFoundText)) { + displayTplData.push(valueNotFoundText); + } + processedValue.push(value[i]); + } + } + + // Set the value of this field. If we are multiselecting, then that is an array. + me.value = me.multiSelect ? processedValue : processedValue[0]; + if (!Ext.isDefined(me.value)) { + me.value = null; + } + me.displayTplData = displayTplData; //store for getDisplayValue method + me.lastSelection = me.valueModels = models; + + if (inputEl && me.emptyText && !Ext.isEmpty(value)) { + inputEl.removeCls(me.emptyCls); + } + + // Calculate raw value from the collection of Model data + me.setRawValue(me.getDisplayValue()); + me.checkChange(); + + if (doSelect !== false) { + me.syncSelection(); + } + me.applyEmptyText(); + + return me; + }, + + /** + * @private Generate the string value to be displayed in the text field for the currently stored value + */ + getDisplayValue: function() { + return this.displayTpl.apply(this.displayTplData); + }, + + getValue: function() { + // If the user has not changed the raw field value since a value was selected from the list, + // then return the structured value from the selection. If the raw field value is different + // than what would be displayed due to selection, return that raw value. + var me = this, + picker = me.picker, + rawValue = me.getRawValue(), //current value of text field + value = me.value; //stored value from last selection or setValue() call + + if (me.getDisplayValue() !== rawValue) { + value = rawValue; + me.value = me.displayTplData = me.valueModels = null; + if (picker) { + me.ignoreSelection++; + picker.getSelectionModel().deselectAll(); + me.ignoreSelection--; + } + } + + return value; + }, + + getSubmitValue: function() { + return this.getValue(); + }, + + isEqual: function(v1, v2) { + var fromArray = Ext.Array.from, + i, len; + + v1 = fromArray(v1); + v2 = fromArray(v2); + len = v1.length; + + if (len !== v2.length) { + return false; + } + + for(i = 0; i < len; i++) { + if (v2[i] !== v1[i]) { + return false; + } + } + + return true; + }, + + /** + * Clears any value currently set in the ComboBox. + */ + clearValue: function() { + this.setValue([]); + }, + + /** + * @private Synchronizes the selection in the picker to match the current value of the combobox. + */ + syncSelection: function() { + var me = this, + ExtArray = Ext.Array, + picker = me.picker, + selection, selModel; + if (picker) { + // From the value, find the Models that are in the store's current data + selection = []; + ExtArray.forEach(me.valueModels || [], function(value) { + if (value && value.isModel && me.store.indexOf(value) >= 0) { + selection.push(value); + } + }); + + // Update the selection to match + me.ignoreSelection++; + selModel = picker.getSelectionModel(); + selModel.deselectAll(); + if (selection.length) { + selModel.select(selection); + } + me.ignoreSelection--; + } + } +}); +