<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>The source code</title> <link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" /> <script type="text/javascript" src="../resources/prettify/prettify.js"></script> <style type="text/css"> .highlight { display: block; background-color: #ddd; } </style> <script type="text/javascript"> function highlight() { document.getElementById(location.hash.replace(/#/, "")).className = "highlight"; } </script> </head> <body onload="prettyPrint(); highlight();"> <pre class="prettyprint lang-js"><span id='Ext-form-field-ComboBox'>/** </span> * @docauthor Jason Johnston <jason@sencha.com> * * 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. * * # Example usage: * * @example * // 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`. * * # Filtered Stores * * If you have a local store that is already filtered, you can use the {@link #lastQuery} config option * to prevent the store from having the filter being cleared on first expand. * * ## Customized combobox * * Both the text shown in dropdown menu and text field can be easily customized: * * @example * var states = Ext.create('Ext.data.Store', { * fields: ['abbr', 'name'], * data : [ * {"abbr":"AL", "name":"Alabama"}, * {"abbr":"AK", "name":"Alaska"}, * {"abbr":"AZ", "name":"Arizona"} * ] * }); * * Ext.create('Ext.form.ComboBox', { * fieldLabel: 'Choose State', * store: states, * queryMode: 'local', * valueField: 'abbr', * renderTo: Ext.getBody(), * // Template for the dropdown menu. * // Note the use of "x-boundlist-item" class, * // this is required to make the items selectable. * tpl: Ext.create('Ext.XTemplate', * '<tpl for=".">', * '<div class="x-boundlist-item">{abbr} - {name}</div>', * '</tpl>' * ), * // template for the content inside text field * displayTpl: Ext.create('Ext.XTemplate', * '<tpl for=".">', * '{abbr} - {name}', * '</tpl>' * ) * }); * * See also the {@link #listConfig} option for additional configuration of the dropdown. * */ 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', 'Ext.layout.component.field.ComboBox'], alternateClassName: 'Ext.form.ComboBox', alias: ['widget.combobox', 'widget.combo'], mixins: { bindable: 'Ext.util.Bindable' }, componentLayout: 'combobox', <span id='Ext-form-field-ComboBox-cfg-triggerCls'> /** </span> * @cfg {String} [triggerCls='x-form-arrow-trigger'] * An additional CSS class used to style the trigger button. The trigger will always get the {@link #triggerBaseCls} * by default and `triggerCls` will be **appended** if specified. */ triggerCls: Ext.baseCSSPrefix + 'form-arrow-trigger', <span id='Ext-form-field-ComboBox-cfg-hiddenName'> /** </span> * @cfg {String} [hiddenName=""] * The name of an underlying hidden field which will be synchronized with the underlying value of the combo. * This option is useful if the combo is part of a form element doing a regular form post. The hidden field * will not be created unless a hiddenName is specified. */ hiddenName: '', <span id='Ext-form-field-ComboBox-property-hiddenDataEl'> /** </span> * @property {Ext.Element} hiddenDataEl * @private */ <span id='Ext-form-field-ComboBox-cfg-hiddenDataCls'> /** </span> * @private * @cfg {String} * CSS class used to find the {@link #hiddenDataEl} */ hiddenDataCls: Ext.baseCSSPrefix + 'hide-display ' + Ext.baseCSSPrefix + 'form-data-hidden', <span id='Ext-form-field-ComboBox-cfg-fieldSubTpl'> /** </span> * @cfg * @inheritdoc */ fieldSubTpl: [ '<div class="{hiddenDataCls}" role="presentation"></div>', '<input id="{id}" type="{type}" {inputAttrTpl} class="{fieldCls} {typeCls}" autocomplete="off"', '<tpl if="value"> value="{[Ext.util.Format.htmlEncode(values.value)]}"</tpl>', '<tpl if="name"> name="{name}"</tpl>', '<tpl if="placeholder"> placeholder="{placeholder}"</tpl>', '<tpl if="size"> size="{size}"</tpl>', '<tpl if="maxLength !== undefined"> maxlength="{maxLength}"</tpl>', '<tpl if="readOnly"> readonly="readonly"</tpl>', '<tpl if="disabled"> disabled="disabled"</tpl>', '<tpl if="tabIdx"> tabIndex="{tabIdx}"</tpl>', '<tpl if="fieldStyle"> style="{fieldStyle}"</tpl>', '/>', { compiled: true, disableFormats: true } ], getSubTplData: function(){ var me = this; Ext.applyIf(me.subTplData, { hiddenDataCls: me.hiddenDataCls }); return me.callParent(arguments); }, afterRender: function(){ var me = this; me.callParent(arguments); me.setHiddenValue(me.value); }, <span id='Ext-form-field-ComboBox-cfg-store'> /** </span> * @cfg {Ext.data.Store/Array} store * The data source to which this combo is bound. Acceptable values for this property are: * * - **any {@link Ext.data.Store Store} subclass** * - **an Array** : 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. * * - **1-dimensional array** : (e.g., `['Foo','Bar']`) * * A 1-dimensional array will automatically be expanded (each array item will be used for both the combo * {@link #valueField} and {@link #displayField}) * * - **2-dimensional array** : (e.g., `[['f','Foo'],['b','Bar']]`) * * 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}. * * See also {@link #queryMode}. */ <span id='Ext-form-field-ComboBox-cfg-multiSelect'> /** </span> * @cfg {Boolean} multiSelect * If set to `true`, 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}. */ multiSelect: false, //<locale> <span id='Ext-form-field-ComboBox-cfg-delimiter'> /** </span> * @cfg {String} delimiter * The character(s) used to separate the {@link #displayField display values} of multiple selected items when * `{@link #multiSelect} = true`. */ delimiter: ', ', //</locale> <span id='Ext-form-field-ComboBox-cfg-displayField'> /** </span> * @cfg {String} displayField * The underlying {@link Ext.data.Field#name data field name} to bind to this ComboBox. * * See also `{@link #valueField}`. */ displayField: 'text', <span id='Ext-form-field-ComboBox-cfg-valueField'> /** </span> * @cfg {String} valueField (required) * The underlying {@link Ext.data.Field#name data value name} to bind to this ComboBox. * * **Note**: use of a `valueField` requires the user to make a selection in order for a value to be mapped. See also * `{@link #displayField}`. * * Defaults to match the value of the {@link #displayField} config. */ <span id='Ext-form-field-ComboBox-cfg-triggerAction'> /** </span> * @cfg {String} triggerAction * The action to execute when the trigger is clicked. * * - **`'all'`** : * * {@link #doQuery run the query} specified by the `{@link #allQuery}` config option * * - **`'query'`** : * * {@link #doQuery run the query} using the {@link Ext.form.field.Base#getRawValue raw value}. * * See also `{@link #queryParam}`. */ triggerAction: 'all', <span id='Ext-form-field-ComboBox-cfg-allQuery'> /** </span> * @cfg {String} allQuery * The text query to send to the server to return all records for the list with no filtering */ allQuery: '', <span id='Ext-form-field-ComboBox-cfg-queryParam'> /** </span> * @cfg {String} queryParam * Name of the parameter used by the Store to pass the typed string when the ComboBox is configured with * `{@link #queryMode}: 'remote'`. If explicitly set to a falsy value it will not be sent. */ queryParam: 'query', <span id='Ext-form-field-ComboBox-cfg-queryMode'> /** </span> * @cfg {String} queryMode * The mode in which the ComboBox uses the configured Store. Acceptable values are: * * - **`'remote'`** : * * In `queryMode: 'remote'`, the ComboBox loads its Store dynamically based upon user interaction. * * This is typically used for "autocomplete" type inputs, and after the user finishes typing, the Store is {@link * Ext.data.Store#method-load load}ed. * * A parameter containing the typed string is sent in the load request. The default parameter name for the input * string is `query`, but this can be configured using the {@link #queryParam} config. * * In `queryMode: 'remote'`, the Store may be configured with `{@link Ext.data.Store#remoteFilter remoteFilter}: * true`, and further filters may be _programatically_ added to the Store which are then passed with every load * request which allows the server to further refine the returned dataset. * * Typically, in an autocomplete situation, {@link #hideTrigger} is configured `true` because it has no meaning for * autocomplete. * * - **`'local'`** : * * ComboBox loads local data * * 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' * }); */ queryMode: 'remote', <span id='Ext-form-field-ComboBox-cfg-queryCaching'> /** </span> * @cfg {Boolean} [queryCaching=true] * When true, this prevents the combo from re-querying (either locally or remotely) when the current query * is the same as the previous query. */ queryCaching: true, <span id='Ext-form-field-ComboBox-cfg-pageSize'> /** </span> * @cfg {Number} pageSize * If greater than `0`, 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.view.BoundList#pageSize limit} * parameters. Only applies when `{@link #queryMode} = 'remote'`. */ pageSize: 0, <span id='Ext-form-field-ComboBox-cfg-queryDelay'> /** </span> * @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 `500` if `{@link #queryMode} = 'remote'` or `10` if `{@link #queryMode} = 'local'` */ <span id='Ext-form-field-ComboBox-cfg-minChars'> /** </span> * @cfg {Number} minChars * The minimum number of characters the user must type before autocomplete and {@link #typeAhead} activate. * * Defaults to `4` if `{@link #queryMode} = 'remote'` or `0` if `{@link #queryMode} = 'local'`, * does not apply if `{@link Ext.form.field.Trigger#editable editable} = false`. */ <span id='Ext-form-field-ComboBox-cfg-autoSelect'> /** </span> * @cfg {Boolean} autoSelect * `true` to automatically highlight the first result gathered by the data store in the dropdown list when it is * opened. A false value would cause nothing in the list to be highlighted automatically, so * the user would have to manually highlight an item before pressing the enter or {@link #selectOnTab tab} key to * select it (unless the value of ({@link #typeAhead}) were true), or use the mouse to select a value. */ autoSelect: true, <span id='Ext-form-field-ComboBox-cfg-typeAhead'> /** </span> * @cfg {Boolean} typeAhead * `true` to populate and autoselect the remainder of the text being typed after a configurable delay * ({@link #typeAheadDelay}) if it matches a known value. */ typeAhead: false, <span id='Ext-form-field-ComboBox-cfg-typeAheadDelay'> /** </span> * @cfg {Number} typeAheadDelay * The length of time in milliseconds to wait until the typeahead text is displayed if `{@link #typeAhead} = true` */ typeAheadDelay: 250, <span id='Ext-form-field-ComboBox-cfg-selectOnTab'> /** </span> * @cfg {Boolean} selectOnTab * Whether the Tab key should select the currently highlighted item. */ selectOnTab: true, <span id='Ext-form-field-ComboBox-cfg-forceSelection'> /** </span> * @cfg {Boolean} forceSelection * `true` to restrict the selected value to one of the values in the list, `false` to allow the user to set * arbitrary text into the field. */ forceSelection: false, <span id='Ext-form-field-ComboBox-cfg-growToLongestValue'> /** </span> * @cfg {Boolean} growToLongestValue * `false` to not allow the component to resize itself when its data changes * (and its {@link #grow} property is `true`) */ growToLongestValue: true, <span id='Ext-form-field-ComboBox-cfg-valueNotFoundText'> /** </span> * @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. If this default text is used, it means there * is no value set and no validation will occur on this field. */ <span id='Ext-form-field-ComboBox-property-lastQuery'> /** </span> * @property {String} lastQuery * The value of the match string used to filter the store. Delete this property to force a requery. Example use: * * 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; * } * } * }); * * To make sure the filter in the store is not cleared the first time the ComboBox trigger is used configure the * combo with `lastQuery=''`. Example use: * * var combo = new Ext.form.field.ComboBox({ * ... * queryMode: 'local', * triggerAction: 'all', * lastQuery: '' * }); */ <span id='Ext-form-field-ComboBox-cfg-defaultListConfig'> /** </span> * @cfg {Object} defaultListConfig * Set of options that will be used as defaults for the user-configured {@link #listConfig} object. */ defaultListConfig: { loadingHeight: 70, minWidth: 70, maxHeight: 300, shadow: 'sides' }, <span id='Ext-form-field-ComboBox-cfg-transform'> /** </span> * @cfg {String/HTMLElement/Ext.Element} transform * The id, DOM node or {@link Ext.Element} of an existing HTML `<select>` 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. */ <span id='Ext-form-field-ComboBox-cfg-listConfig'> /** </span> * @cfg {Object} listConfig * 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: * * - {@link Ext.view.BoundList#cls cls} - defaults to empty * - {@link Ext.view.BoundList#emptyText emptyText} - defaults to empty string * - {@link Ext.view.BoundList#itemSelector itemSelector} - defaults to the value defined in BoundList * - {@link Ext.view.BoundList#loadingText loadingText} - defaults to `'Loading...'` * - {@link Ext.view.BoundList#minWidth minWidth} - defaults to `70` * - {@link Ext.view.BoundList#maxWidth maxWidth} - defaults to `undefined` * - {@link Ext.view.BoundList#maxHeight maxHeight} - defaults to `300` * - {@link Ext.view.BoundList#resizable resizable} - defaults to `false` * - {@link Ext.view.BoundList#shadow shadow} - defaults to `'sides'` * - {@link Ext.view.BoundList#width width} - defaults to `undefined` (automatically set to the width of the ComboBox * field if {@link #matchFieldWidth} is true) */ //private ignoreSelection: 0, //private, tells the layout to recalculate its startingWidth when a record is removed from its bound store removingRecords: null, //private helper resizeComboToGrow: function () { var me = this; return me.grow && me.growToLongestValue; }, initComponent: function() { var me = this, isDefined = Ext.isDefined, store = me.store, transform = me.transform, transformSelect, isLocalMode; Ext.applyIf(me.renderSelectors, { hiddenDataEl: '.' + me.hiddenDataCls.split(' ').join('.') }); //<debug> 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( <span id='Ext-form-field-ComboBox-event-beforequery'> /** </span> * @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: * * - `combo` : Ext.form.field.ComboBox * * This combo box * * - `query` : String * * The query string * * - `forceAll` : Boolean * * True to force "all" query * * - `cancel` : Boolean * * Set to true to cancel the query */ 'beforequery', <span id='Ext-form-field-ComboBox-event-select'> /** </span> * @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', <span id='Ext-form-field-ComboBox-event-beforeselect'> /** </span> * @event beforeselect * Fires before the selected item is added to the collection * @param {Ext.form.field.ComboBox} combo This combo box * @param {Ext.data.Record} record The selected record * @param {Number} index The index of the selected record */ 'beforeselect', <span id='Ext-form-field-ComboBox-event-beforedeselect'> /** </span> * @event beforedeselect * Fires before the deselected item is removed from the collection * @param {Ext.form.field.ComboBox} combo This combo box * @param {Ext.data.Record} record The deselected record * @param {Number} index The index of the deselected record */ 'beforedeselect' ); // Build store from 'transform' HTML select element's options if (transform) { transformSelect = Ext.getDom(transform); if (transformSelect) { if (!me.store) { 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 || 'ext-empty-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 = new 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 = new Ext.XTemplate(me.displayTpl); } me.callParent(); me.doQueryTask = new 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; } }, <span id='Ext-form-field-ComboBox-method-getStore'> /** </span> * Returns the store associated with this ComboBox. * @return {Ext.data.Store} The store */ getStore : function(){ return this.store; }, beforeBlur: function() { this.doQueryTask.cancel(); this.assertValue(); }, // private assertValue: function() { var me = this, value = me.getRawValue(), rec; if (me.forceSelection) { 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: Ext.emptyFn, beforeReset: function() { this.callParent(); this.clearFilter(); }, onUnbindStore: function(store) { var picker = this.picker; if (!store && picker) { picker.bindStore(null); } this.clearFilter(); }, onBindStore: function(store, initial) { var picker = this.picker; if (!initial) { this.resetToDefault(); } if (picker) { picker.bindStore(store); } }, getStoreListeners: function() { var me = this; return { beforeload: me.onBeforeLoad, clear: me.onClear, datachanged: me.onDataChanged, load: me.onLoad, exception: me.onException, remove: me.onRemove }; }, onBeforeLoad: function(){ // If we're remote loading, the load mask will show which will trigger a deslectAll. // This selection change will trigger the collapse in onListSelectionChange. As such // we'll veto it for now and restore selection listeners when we've loaded. ++this.ignoreSelection; }, onDataChanged: function() { var me = this; if (me.resizeComboToGrow()) { me.updateLayout(); } }, onClear: function() { var me = this; if (me.resizeComboToGrow()) { me.removingRecords = true; me.onDataChanged(); } }, onRemove: function() { var me = this; if (me.resizeComboToGrow()) { me.removingRecords = true; } }, onException: function(){ if (this.ignoreSelection > 0) { --this.ignoreSelection; } this.collapse(); }, onLoad: function() { var me = this, value = me.value; if (me.ignoreSelection > 0) { --me.ignoreSelection; } // If performing a remote query upon the raw value... if (me.rawQuery) { me.rawQuery = false; me.syncSelection(); if (me.picker && !me.picker.getSelectionModel().hasSelection()) { me.doAutoSelect(); } } // If store initial load or triggerAction: 'all' trigger click. else { // Set the value on load if (me.value || me.value === 0) { me.setValue(me.value); } else { // There's no value. // Highlight the first item in the list if autoSelect: true if (me.store.getCount()) { me.doAutoSelect(); } else { // assign whatever empty value we have to prevent change from firing me.setValue(me.value); } } } }, <span id='Ext-form-field-ComboBox-method-doRawQuery'> /** </span> * @private * Execute the query with the raw contents within the textfield. */ doRawQuery: function() { this.doQuery(this.getRawValue(), false, true); }, <span id='Ext-form-field-ComboBox-method-doQuery'> /** </span> * 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=false] `true` to force the query to execute even if there are currently fewer characters in * the field than the minimum specified by the `{@link #minChars}` config option. It also clears any filter * previously saved in the current store. * @param {Boolean} [rawQuery=false] Pass as true if the raw typed value is being used as the query string. This causes the * resulting store load to leave the raw value undisturbed. * @return {Boolean} true if the query was permitted to run, false if it was cancelled by a {@link #beforequery} * handler. */ doQuery: function(queryString, forceAll, rawQuery) { 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', needsRefresh; 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; if (isLocalMode) { // forceAll means no filtering - show whole dataset. store.suspendEvents(); needsRefresh = me.clearFilter(); if (queryString || !forceAll) { me.activeFilter = new Ext.util.Filter({ root: 'data', property: me.displayField, value: queryString }); store.filter(me.activeFilter); needsRefresh = true; } else { delete me.activeFilter; } store.resumeEvents(); if (me.rendered && needsRefresh) { me.getPicker().refresh(); } } else { // Set flag for onLoad handling to know how the Store was loaded me.rawQuery = rawQuery; // In queryMode: 'remote', we assume Store filters are added by the developer as remote filters, // and these are automatically passed as params with every load call, so we do *not* call clearFilter. if (me.pageSize) { // if we're paging, we've changed the query so start at page 1. me.loadPage(1); } 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; }, <span id='Ext-form-field-ComboBox-method-clearFilter'> /** </span> * Clears any previous filters applied by the combo to the store * @private * @return {Boolean} True if a filter was removed */ clearFilter: function() { var store = this.store, filter = this.activeFilter, filters = store.filters, remaining; if (filter) { if (filters.getCount() > 1) { // More than 1 existing filter filters.remove(filter); remaining = filters.getRange(); } store.clearFilter(true); if (remaining) { store.filter(remaining); } } return !!filter; }, loadPage: function(pageNum){ this.store.loadPage(pageNum, { params: this.getParams(this.lastQuery) }); }, onPageChange: function(toolbar, newPage){ /* * Return false here so we can call load ourselves and inject the query param. * We don't want to do this for every store load since the developer may load * the store through some other means so we won't add the query param. */ this.loadPage(newPage); return false; }, // private getParams: function(queryString) { var params = {}, param = this.queryParam; if (param) { params[param] = queryString; } return params; }, <span id='Ext-form-field-ComboBox-method-doAutoSelect'> /** </span> * @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 = new 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(), false, true); } } 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); } } if (me.enableKeyEvents) { me.callParent(arguments); } }, initEvents: function() { var me = this; me.callParent(); /* * Setup keyboard handling. If enableKeyEvents is true, we already have * a listener on the inputEl for keyup, so don't create a second. */ if (!me.enableKeyEvents) { me.mon(me.inputEl, 'keyup', me.onKeyUp, me); } }, onDestroy: function() { this.bindStore(null); this.callParent(); }, // The picker (the dropdown) must have its zIndex managed by the same ZIndexManager which is // providing the zIndex of our Container. onAdded: function() { var me = this; me.callParent(arguments); if (me.picker) { me.picker.ownerCt = me.up('[floating]'); me.picker.registerWithOwnerCt(); } }, createPicker: function() { var me = this, picker, pickerCfg = Ext.apply({ xtype: 'boundlist', pickerField: me, selModel: { mode: me.multiSelect ? 'SIMPLE' : 'SINGLE' }, floating: true, hidden: true, store: me.store, displayField: me.displayField, focusOnToFront: false, pageSize: me.pageSize, tpl: me.tpl }, me.listConfig, me.defaultListConfig); picker = me.picker = Ext.widget(pickerCfg); if (me.pageSize) { picker.pagingToolbar.on('beforechange', me.onPageChange, me); } me.mon(picker, { itemclick: me.onItemClick, refresh: me.onListRefresh, scope: me }); me.mon(picker.getSelectionModel(), { beforeselect: me.onBeforeSelect, beforedeselect: me.onBeforeDeselect, selectionchange: me.onListSelectionChange, scope: me }); return picker; }, alignPicker: function(){ var me = this, picker = me.getPicker(), heightAbove = me.getPosition()[1] - Ext.getBody().getScroll().top, heightBelow = Ext.Element.getViewHeight() - heightAbove - me.getHeight(), space = Math.max(heightAbove, heightBelow); // Allow the picker to height itself naturally. if (picker.height) { delete picker.height; picker.updateLayout(); } // Then ensure that vertically, the dropdown will fit into the space either above or below the inputEl. if (picker.getHeight() > space - 5) { picker.setHeight(space - 5); // have some leeway so we aren't flush against } me.callParent(); }, 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, selection = me.picker.getSelectionModel().getSelection(), valueField = me.valueField; if (!me.multiSelect && selection.length) { if (record.get(valueField) === selection[0].get(valueField)) { // Make sure we also update the display value if it's only partial me.displayTplData = [record.data]; me.setRawValue(me.getDisplayValue()); me.collapse(); } } }, onBeforeSelect: function(list, record) { return this.fireEvent('beforeselect', this, record, record.index); }, onBeforeDeselect: function(list, record) { return this.fireEvent('beforedeselect', this, record, record.index); }, onListSelectionChange: function(list, selectedRecords) { var me = this, isMulti = me.multiSelect, hasRecords = selectedRecords.length > 0; // 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 (!isMulti) { Ext.defer(me.collapse, 1, me); } /* * Only set the value here if we're in multi selection mode or we have * a selection. Otherwise setValue will be called with an empty value * which will cause the change event to fire twice. */ if (isMulti || hasRecords) { me.setValue(selectedRecords, false); } if (hasRecords) { me.fireEvent('select', me, selectedRecords); } me.inputEl.focus(); } }, <span id='Ext-form-field-ComboBox-method-onExpand'> /** </span> * @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 = new 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(); }, <span id='Ext-form-field-ComboBox-method-onCollapse'> /** </span> * @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; } }, <span id='Ext-form-field-ComboBox-method-select'> /** </span> * Selects an item by a {@link Ext.data.Model Model}, or by a key value. * @param {Object} r */ select: function(r) { this.setValue(r, true); }, <span id='Ext-form-field-ComboBox-method-findRecord'> /** </span> * Finds the record by searching for a specific field/value combination. * @param {String} field The name of the field to test. * @param {Object} value The value to match the field against. * @return {Ext.data.Model} The matched record or false. */ findRecord: function(field, value) { var ds = this.store, idx = ds.findExact(field, value); return idx !== -1 ? ds.getAt(idx) : false; }, <span id='Ext-form-field-ComboBox-method-findRecordByValue'> /** </span> * Finds the record by searching values in the {@link #valueField}. * @param {Object} value The value to match the field against. * @return {Ext.data.Model} The matched record or false. */ findRecordByValue: function(value) { return this.findRecord(this.valueField, value); }, <span id='Ext-form-field-ComboBox-method-findRecordByDisplay'> /** </span> * Finds the record by searching values in the {@link #displayField}. * @param {Object} value The value to match the field against. * @return {Ext.data.Model} The matched record or false. */ findRecordByDisplay: function(value) { return this.findRecord(this.displayField, value); }, <span id='Ext-form-field-ComboBox-method-setValue'> /** </span> * 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/String[]} 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, dataObj, matchedRecords = [], displayTplData = [], processedValue = []; if (me.store.loading) { // Called while the Store is loading. Ensure it is processed by the onLoad method. me.value = value; me.setHiddenValue(me.value); return me; } // This method processes multi-values, so ensure value is an array. value = Ext.Array.from(value); // Loop through values, matching each from the Store, and collecting matched records 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) { matchedRecords.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 we are allowing insertion of values not represented in the Store, then push the value and // create a fake record data object to push as a display value for use by the displayTpl if (!me.forceSelection) { processedValue.push(value[i]); dataObj = {}; dataObj[me.displayField] = value[i]; displayTplData.push(dataObj); // TODO: Add config to create new records on selection of a value that has no match in the Store } // Else, if valueNotFoundText is defined, display it, otherwise display nothing for this value else if (Ext.isDefined(valueNotFoundText)) { displayTplData.push(valueNotFoundText); } } } // Set the value of this field. If we are multiselecting, then that is an array. me.setHiddenValue(processedValue); 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 = matchedRecords; 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; }, <span id='Ext-form-field-ComboBox-method-setHiddenValue'> /** </span> * @private * Set the value of {@link #hiddenDataEl} * Dynamically adds and removes input[type=hidden] elements */ setHiddenValue: function(values){ var me = this, name = me.hiddenName, i, dom, childNodes, input, valueCount, childrenCount; if (!me.hiddenDataEl || !name) { return; } values = Ext.Array.from(values); dom = me.hiddenDataEl.dom; childNodes = dom.childNodes; input = childNodes[0]; valueCount = values.length; childrenCount = childNodes.length; if (!input && valueCount > 0) { me.hiddenDataEl.update(Ext.DomHelper.markup({ tag: 'input', type: 'hidden', name: name })); childrenCount = 1; input = dom.firstChild; } while (childrenCount > valueCount) { dom.removeChild(childNodes[0]); -- childrenCount; } while (childrenCount < valueCount) { dom.appendChild(input.cloneNode(true)); ++ childrenCount; } for (i = 0; i < valueCount; i++) { childNodes[i].value = values[i]; } }, <span id='Ext-form-field-ComboBox-method-getDisplayValue'> /** </span> * @private Generates 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; }, <span id='Ext-form-field-ComboBox-method-clearValue'> /** </span> * Clears any value currently set in the ComboBox. */ clearValue: function() { this.setValue([]); }, <span id='Ext-form-field-ComboBox-method-syncSelection'> /** </span> * @private Synchronizes the selection in the picker to match the current value of the combobox. */ syncSelection: function() { var me = this, picker = me.picker, selection, selModel, values = me.valueModels || [], vLen = values.length, v, value; if (picker) { // From the value, find the Models that are in the store's current data selection = []; for (v = 0; v < vLen; v++) { value = values[v]; 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--; } }, onEditorTab: function(e){ var keyNav = this.listKeyNav; if (this.selectOnTab && keyNav) { keyNav.selectHighlighted(e); } } }); </pre> </body> </html>