X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/530ef4b6c5b943cfa68b779d11cf7de29aa878bf..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 @@ +Sencha Documentation Project
/**
+ * @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>&lt;select&gt;</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--;
+        }
+    }
+});
+
\ No newline at end of file