X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/3789b528d8dd8aad4558e38e22d775bcab1cbd36..f562e4c6e5fac7bcb445985b99acbea4d706e6f0:/docs/source/ComboBox.html diff --git a/docs/source/ComboBox.html b/docs/source/ComboBox.html index 46feb821..d11631f8 100644 --- a/docs/source/ComboBox.html +++ b/docs/source/ComboBox.html @@ -3,8 +3,8 @@
/** - * @class Ext.form.field.ComboBox - * @extends Ext.form.field.Picker +/** + * @docauthor Jason Johnston <jason@sencha.com> * * A combobox control with support for autocomplete, remote loading, and many other features. * @@ -33,10 +32,9 @@ * 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: + * # Example usage: * + * @example * // The data store containing the list of states * var states = Ext.create('Ext.data.Store', { * fields: ['abbr', 'name'], @@ -58,7 +56,7 @@ * renderTo: Ext.getBody() * }); * - * ## Events + * # Events * * To do something when something in ComboBox is selected, configure the select event: * @@ -74,16 +72,10 @@ * var cb = new Ext.form.field.ComboBox(yourOptions); * cb.on('select', yourFunction, yourScope); * - * ## Multiple Selection + * # 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', @@ -92,209 +84,267 @@ Ext.define('Ext.form.field.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. + * @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', + * @private + * @cfg {String} + * CSS class used to find the {@link #hiddenDataEl} + */ + hiddenDataCls: Ext.baseCSSPrefix + 'hide-display ' + Ext.baseCSSPrefix + 'form-data-hidden', + + /** + * @override + */ + fieldSubTpl: [ + '<div class="{hiddenDataCls}" role="presentation"></div>', + '<input id="{id}" type="{type}" ', + '<tpl if="size">size="{size}" </tpl>', + '<tpl if="tabIdx">tabIndex="{tabIdx}" </tpl>', + 'class="{fieldCls} {typeCls}" autocomplete="off" />', + '<div id="{cmpId}-triggerWrap" class="{triggerWrapCls}" role="presentation">', + '{triggerEl}', + '<div class="{clearCls}" role="presentation"></div>', + '</div>', + { + 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); + }, + /** - * @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 {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}. */ /** * @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>.) + * 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, /** * @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>. + * The character(s) used to separate the {@link #displayField display values} of multiple selected items when + * `{@link #multiSelect} = true`. */ 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> + * @cfg {String} displayField + * The underlying {@link Ext.data.Field#name data field name} to bind to this ComboBox. + * + * See also `{@link #valueField}`. */ displayField: 'text', /** - * @cfg {String} valueField - * @required + * @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> + * + * **Note**: use of a `valueField` requires the user to make a selection in order for a value to be mapped. See also + * `{@link #displayField}`. */ /** - * @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> + * @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', /** - * @cfg {String} allQuery The text query to send to the server to return all records for the list - * with no filtering (defaults to '') + * @cfg {String} allQuery + * The text query to send to the server to return all records for the list with no filtering */ allQuery: '', /** - * @cfg {String} queryParam Name of the query ({@link Ext.data.proxy.Proxy#extraParam extraParam} name for the store) - * as it will be passed on the querystring (defaults to <tt>'query'</tt>). If explicitly set to a falsey value it will - * not be send. + * @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', /** * @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> + * 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#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', 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>). + * @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, /** - * @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} 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'`) */ /** - * @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 {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`). */ /** - * @cfg {Boolean} autoSelect <tt>true</tt> to automatically highlight the first result gathered by the data store - * in the dropdown list when it is opened. (Defaults to <tt>true</tt>). 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. + * @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, /** - * @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>) + * @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, /** - * @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>) + * @cfg {Number} typeAheadDelay + * The length of time in milliseconds to wait until the typeahead text is displayed if `{@link #typeAhead} = true` */ typeAheadDelay: 250, /** * @cfg {Boolean} selectOnTab - * Whether the Tab key should select the currently highlighted item. Defaults to <tt>true</tt>. + * Whether the Tab key should select the currently highlighted item. */ 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>) + * @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, /** - * @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. + * @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. */ /** - * 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 + * @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: '' + * }); */ /** @@ -311,31 +361,28 @@ var combo = new Ext.form.field.ComboBox({ }, /** - * @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 {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. */ /** * @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> + * 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} - defaults to empty + * - {@link Ext.view.BoundList#emptyText} - defaults to empty string + * - {@link Ext.view.BoundList#itemSelector} - defaults to the value defined in BoundList + * - {@link Ext.view.BoundList#loadingText} - defaults to `'Loading...'` + * - {@link Ext.view.BoundList#minWidth} - defaults to `70` + * - {@link Ext.view.BoundList#maxWidth} - defaults to `undefined` + * - {@link Ext.view.BoundList#maxHeight} - defaults to `300` + * - {@link Ext.view.BoundList#resizable} - defaults to `false` + * - {@link Ext.view.BoundList#shadow} - defaults to `'sides'` + * - {@link Ext.view.BoundList#width} - defaults to `undefined` (automatically set to the width of the ComboBox + * field if {@link #matchFieldWidth} is true) */ //private @@ -348,10 +395,11 @@ var combo = new Ext.form.field.ComboBox({ transform = me.transform, transformSelect, isLocalMode; + Ext.applyIf(me.renderSelectors, { + hiddenDataEl: '.' + me.hiddenDataCls.split(' ').join('.') + }); + //<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.'); } @@ -364,32 +412,60 @@ var combo = new Ext.form.field.ComboBox({ //</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> + * 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', - /* - * @event select + /** + * @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' + 'select', + + /** + * @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', + + /** + * @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 (!store && transform) { + if (transform) { transformSelect = Ext.getDom(transform); if (transformSelect) { store = Ext.Array.map(Ext.Array.from(transformSelect.options), function(option) { @@ -404,7 +480,7 @@ var combo = new Ext.form.field.ComboBox({ } } - me.bindStore(store, true); + me.bindStore(store || 'ext-empty-store', true); store = me.store; if (store.autoCreated) { me.queryMode = 'local'; @@ -430,7 +506,7 @@ var combo = new Ext.form.field.ComboBox({ if (!me.displayTpl) { me.displayTpl = Ext.create('Ext.XTemplate', '<tpl for=".">' + - '{[typeof values === "string" ? values : values.' + me.displayField + ']}' + + '{[typeof values === "string" ? values : values["' + me.displayField + '"]]}' + '<tpl if="xindex < xcount">' + me.delimiter + '</tpl>' + '</tpl>' ); @@ -455,14 +531,17 @@ var combo = new Ext.form.field.ComboBox({ } }, + /** + * Returns the store associated with this ComboBox. + * @return {Ext.data.Store} The store + */ + getStore : function(){ + return this.store; + }, + beforeBlur: function() { - var me = this; - me.doQueryTask.cancel(); - if (me.forceSelection) { - me.assertValue(); - } else { - me.collapse(); - } + this.doQueryTask.cancel(); + this.assertValue(); }, // private @@ -471,20 +550,22 @@ var combo = new Ext.form.field.ComboBox({ 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); + 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 { - me.setValue(me.lastSelection); + // 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(); @@ -525,7 +606,7 @@ var combo = new Ext.form.field.ComboBox({ // would create it when we may be preping to destroy it if (oldStore && !initial) { if (oldStore !== store && oldStore.autoDestroy) { - oldStore.destroy(); + oldStore.destroyStore(); } else { oldStore.un({ scope: me, @@ -562,9 +643,28 @@ var combo = new Ext.form.field.ComboBox({ var me = this, value = me.value; - me.syncSelection(); - if (me.picker && !me.picker.getSelectionModel().hasSelection()) { - me.doAutoSelect(); + // 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.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 { + me.setValue(''); + } + } } }, @@ -573,19 +673,23 @@ var combo = new Ext.form.field.ComboBox({ * Execute the query with the raw contents within the textfield. */ doRawQuery: function() { - this.doQuery(this.getRawValue()); + this.doQuery(this.getRawValue(), false, true); }, /** - * 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. + * 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. + * @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) { + doQuery: function(queryString, forceAll, rawQuery) { queryString = queryString || ''; // store in object and pass by reference in 'beforequery' @@ -616,15 +720,30 @@ var combo = new Ext.form.field.ComboBox({ // 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) { + // forceAll means no filtering - show whole dataset. + if (forceAll) { + store.clearFilter(); + } else { + // Clear filter, but supress event so that the BoundList is not immediately updated. + store.clearFilter(true); store.filter(me.displayField, queryString); } } else { - store.load({ - params: me.getParams(queryString) - }); + // 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) + }); + } } } @@ -645,21 +764,31 @@ var combo = new Ext.form.field.ComboBox({ return true; }, + 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 p = {}, - pageSize = this.pageSize, + var params = {}, param = this.queryParam; - + if (param) { - p[param] = queryString; + params[param] = queryString; } - - if (pageSize) { - p.start = 0; - p.limit = pageSize; - } - return p; + return params; }, /** @@ -700,7 +829,7 @@ var combo = new Ext.form.field.ComboBox({ if (me.triggerAction === 'all') { me.doQuery(me.allQuery, true); } else { - me.doQuery(me.getRawValue()); + me.doQuery(me.getRawValue(), false, true); } } me.inputEl.focus(); @@ -723,7 +852,7 @@ var combo = new Ext.form.field.ComboBox({ me.doQueryTask.delay(me.queryDelay); } } - + if (me.enableKeyEvents) { me.callParent(arguments); } @@ -734,19 +863,25 @@ var combo = new Ext.form.field.ComboBox({ me.callParent(); /* - * Setup keyboard handling. If enableKeyEvents is true, we already have + * 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(); + }, createPicker: function() { var me = this, picker, menuCls = Ext.baseCSSPrefix + 'menu', opts = Ext.apply({ + pickerField: me, selModel: { mode: me.multiSelect ? 'SIMPLE' : 'SINGLE' }, @@ -757,10 +892,14 @@ var combo = new Ext.form.field.ComboBox({ store: me.store, displayField: me.displayField, focusOnToFront: false, - pageSize: me.pageSize + pageSize: me.pageSize, + tpl: me.tpl }, me.listConfig, me.defaultListConfig); picker = me.picker = Ext.create('Ext.view.BoundList', opts); + if (me.pageSize) { + picker.pagingToolbar.on('beforechange', me.onPageChange, me); + } me.mon(picker, { itemclick: me.onItemClick, @@ -769,18 +908,34 @@ var combo = new Ext.form.field.ComboBox({ }); me.mon(picker.getSelectionModel(), { - selectionChange: me.onListSelectionChange, + 'beforeselect': me.onBeforeSelect, + 'beforedeselect': me.onBeforeDeselect, + 'selectionchange': me.onListSelectionChange, scope: me }); return picker; }, + alignPicker: function(){ + var me = this, + picker = me.picker, + heightAbove = me.getPosition()[1] - Ext.getBody().getScroll().top, + heightBelow = Ext.Element.getViewHeight() - heightAbove - me.getHeight(), + space = Math.max(heightAbove, heightBelow); + + me.callParent(); + if (picker.getHeight() > space) { + picker.setHeight(space - 5); // have some leeway so we aren't flush against + me.doAlign(); + } + }, + onListRefresh: function() { this.alignPicker(); this.syncSelection(); }, - + onItemClick: function(picker, record){ /* * If we're doing single selection, the selection change events won't fire when @@ -790,25 +945,45 @@ var combo = new Ext.form.field.ComboBox({ lastSelection = me.lastSelection, valueField = me.valueField, selected; - + if (!me.multiSelect && lastSelection) { selected = lastSelection[0]; - if (record.get(valueField) === selected.get(valueField)) { + if (selected && (record.get(valueField) === selected.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; + 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 (!me.multiSelect) { + if (!isMulti) { Ext.defer(me.collapse, 1, me); } - me.setValue(selectedRecords, false); - if (selectedRecords.length > 0) { + /* + * 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(); @@ -867,25 +1042,38 @@ var combo = new Ext.form.field.ComboBox({ /** * Selects an item by a {@link Ext.data.Model Model}, or by a key value. - * @param r + * @param {Object} 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 + * 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; }, + + /** + * 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); }, + + /** + * 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); }, @@ -893,9 +1081,9 @@ var combo = new Ext.form.field.ComboBox({ /** * 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 + * 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}, + * @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 */ @@ -911,6 +1099,7 @@ var combo = new Ext.form.field.ComboBox({ 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; } @@ -932,15 +1121,20 @@ var combo = new Ext.form.field.ComboBox({ // 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)) { + // If we are allowing insertion of values not represented in the Store, then set the value, and the display value + if (!me.forceSelection) { + displayTplData.push(value[i]); + processedValue.push(value[i]); + } + // Else, if valueNotFoundText is defined, display it, otherwise display nothing for this value + else 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.setHiddenValue(processedValue); me.value = me.multiSelect ? processedValue : processedValue[0]; if (!Ext.isDefined(me.value)) { me.value = null; @@ -964,8 +1158,43 @@ var combo = new Ext.form.field.ComboBox({ return me; }, + /** + * @private + * Set the value of {@link #hiddenDataEl} + * Dynamically adds and removes input[type=hidden] elements + */ + setHiddenValue: function(values){ + var me = this, i; + if (!me.hiddenDataEl) { + return; + } + values = Ext.Array.from(values); + var 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:me.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]; + } + }, + /** - * @private Generate the string value to be displayed in the text field for the currently stored value + * @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);