    /**
     * @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.
     */
    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',

        /**
         * @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',

        /**
         * @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: '',

        /**
         * @private
         * @cfg {String}
         * CSS class used to find the {@link #hiddenDataEl}
         */
        hiddenDataCls: Ext.baseCSSPrefix + 'hide-display ' + Ext.baseCSSPrefix + 'form-data-hidden',

        /**
         * @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);
        },

        /**
         * @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 `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
         * `{@link #multiSelect} = true`.
         */
        //<locale>
        delimiter: ', ',
        //</locale>

        /**
         * @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)
         * 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.
         */

        /**
         * @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
         */
        allQuery: '',

        /**
         * @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 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',

        /**
         * @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,

        /**
         * @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 `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 `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
         * `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
         * `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 `{@link #typeAhead} = true`
         */
        typeAheadDelay: 250,

        /**
         * @cfg {Boolean} selectOnTab
         * Whether the Tab key should select the currently highlighted item.
         */
        selectOnTab: true,

        /**
         * @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 {Boolean} growToLongestValue
         * `false` to not allow the component to resize itself when its data changes
         * (and its {@link #grow} property is `true`)
         */
        growToLongestValue: true,

        /**
         * @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.
         */

        /**
         * @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: ''
         *     });
         */

        /**
         * @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'
        },

        /**
         * @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
         * 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
        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(
                /**
                 * @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',

                /**
                 * @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',

                /**
                 * @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 (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;
            }
        },

        /**
         * 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: function() {

        },

        onUnbindStore: function(store) {
            var picker = this.picker;
            if (!store && picker) {
                picker.bindStore(null);
            }
        },

        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);
                    }
                }
            }
        },

        /**
         * @private
         * Execute the query with the raw contents within the textfield.
         */
        doRawQuery: function() {
            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.
         *
         * @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;
        },

        /**
         * 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,
                filters = store.filters,
                filter = this.activeFilter,
                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;
        },

        /**
         * @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,
                menuCls = Ext.baseCSSPrefix + 'menu',
                pickerCfg = Ext.apply({
                    xtype: 'boundlist',
                    pickerField: me,
                    selModel: {
                        mode: me.multiSelect ? 'SIMPLE' : 'SINGLE'
                    },
                    floating: true,
                    hidden: true,

                    // The picker (the dropdown) must have its zIndex managed by the same ZIndexManager which is
                    // providing the zIndex of our Container.
                    ownerCt: me.up('[floating]'),
                    cls: me.el && me.el.up('.' + menuCls) ? menuCls : '',
                    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();
            }
        },

        /**
         * @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();
        },

        /**
         * @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 {Object} r
         */
        select: function(r) {
            this.setValue(r, true);
        },

        /**
         * 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);
        },

        /**
         * 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;
        },

        /**
         * @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];
            }
        },

        /**
         * @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;
        },

        /**
         * 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,
                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);
            }
        }
    });