/**
 * 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
 * The {@link #multiSelect} config is deprecated.  For multiple selection use 
 * {@link Ext.form.field.Tag} or {@link Ext.view.MultiSelector}.
 *
 * # Filtered Stores
 *
 * If you have a local store that is already filtered, you can use the {@link #lastQuery}
 * config option to prevent the store from having the filter being cleared on first expand.
 *
 * ## Customized combobox
 *
 * Both the text shown in dropdown menu and text field can be easily customized:
 *
 *     @example
 *     var states = Ext.create('Ext.data.Store', {
 *         fields: ['abbr', 'name'],
 *         data : [
 *             {"abbr":"AL", "name":"Alabama"},
 *             {"abbr":"AK", "name":"Alaska"},
 *             {"abbr":"AZ", "name":"Arizona"}
 *         ]
 *     });
 *
 *     Ext.create('Ext.form.ComboBox', {
 *         fieldLabel: 'Choose State',
 *         store: states,
 *         queryMode: 'local',
 *         valueField: 'abbr',
 *         renderTo: Ext.getBody(),
 *         // Template for the dropdown menu.
 *         // Note the use of the "x-list-plain" and "x-boundlist-item" class,
 *         // this is required to make the items selectable.
 *         tpl: Ext.create('Ext.XTemplate',
 *             '<ul class="x-list-plain"><tpl for=".">',
 *                 '<li role="option" class="x-boundlist-item">{abbr} - {name}</li>',
 *             '</tpl></ul>'
 *         ),
 *         // template for the content inside text field
 *         displayTpl: Ext.create('Ext.XTemplate',
 *             '<tpl for=".">',
 *                 '{abbr} - {name}',
 *             '</tpl>'
 *         )
 *     });
 *
 * See also the {@link #listConfig} option for additional configuration of the dropdown.
 *
 */
Ext.define('Ext.form.field.ComboBox', {
    extend: 'Ext.form.field.Picker',
    alternateClassName: 'Ext.form.ComboBox',
    alias: ['widget.combobox', 'widget.combo'],
 
    requires: [
        'Ext.util.DelayedTask',
        'Ext.view.BoundList',
        'Ext.data.StoreManager'
    ],
 
    mixins: [
        'Ext.util.StoreHolder'
    ],
 
    config: {
        filters: null,
 
        /**
         * @cfg {Ext.data.Model} selection
         * The selected model. Typically used with {@link #bind binding}.
         */
        selection: null,
 
        /**
         * @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.
         */
        valueNotFoundText: null,
 
        /**
         * @cfg {String/String[]/Ext.XTemplate} displayTpl
         * The template to be used to display selected records inside the text field. An array
         * of the selected records' data will be passed to the template. Defaults to:
         *
         *     '<tpl for=".">' +
         *         '{[typeof values === "string" ? values : values["' + me.displayField + '"]]}' +
         *         '<tpl if="xindex < xcount">' + me.delimiter + '</tpl>' +
         *     '</tpl>'
         *
         * By default only the immediate data of the record is passed (no associated data).
         * The {@link #getRecordDisplayData} can be overridden to extend this.
         */
        displayTpl: null,
 
        /**
         * @cfg {String} delimiter
         * The character(s) used to separate the {@link #displayField display values} of multiple
         * selected items when `{@link #multiSelect} = true`.
         * @deprecated 5.1.0 For multiple selection use {@link Ext.form.field.Tag} or 
         * {@link Ext.view.MultiSelector}
         * @locale
         */
        delimiter: '',
 
        /**
         * @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 publishes
     * @inheritdoc
     */
    publishes: ['selection'],
 
    /**
     * @cfg twoWayBindable
     * @inheritdoc
     */
    twoWayBindable: ['selection'],
 
    /**
     * @cfg {String} triggerCls
     * An additional CSS class used to style the trigger button. The trigger will always get
     * the {@link Ext.form.trigger.Trigger#baseCls} 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: '',
 
    /**
     * @cfg {Boolean} [pinList=true]
     * Has no effect if {@link #multiSelect} is `false`
     *
     * Configure as `false` to automatically collapse the pick list after a selection is made.
     * @deprecated 5.1.0 For multiple selection use {@link Ext.form.field.Tag} or 
     * {@link Ext.view.MultiSelector}
     */
 
    /**
     * @cfg {Boolean} collapseOnSelect
     * Has no effect if {@link #multiSelect} is `false`
     *
     * Configure as true to automatically collapse the pick list after a selection is made.
     * @deprecated 5.1.0 For multiple selection use {@link Ext.form.field.Tag} or 
     * {@link Ext.view.MultiSelector}
     */
    collapseOnSelect: false,
 
    /**
     * @property {Ext.dom.Element} hiddenDataEl
     * @private
     */
 
    /**
     * @private
     * @cfg {String} hiddenDataCls
     * CSS class used to find the {@link #hiddenDataEl}
     */
    hiddenDataCls: Ext.baseCSSPrefix + 'hidden-display ' + Ext.baseCSSPrefix + 'form-data-hidden',
 
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'combobox',
 
    /**
     * @property autoDestroyBoundStore
     * @inheritdoc
     */
    autoDestroyBoundStore: true,
 
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: {
        'hiddenDataEl': true
    },
 
    /**
     * @property {Boolean} filtered
     * True if there are extra `filters` appllied to this component.
     * @private
     * @readonly
     * @since 5.0.0
     */
    filtered: false,
 
    afterRender: function() {
        var me = this;
 
        me.callParent(arguments);
 
        me.setHiddenValue(me.value);
    },
 
    /**
     * @cfg {Ext.data.Store/String/Array/Object} store (required)
     * The data source to which the combo / tagfield is bound. Acceptable values for this 
     * property are:
     *
     *   - **any {@link Ext.data.Store Store} class / subclass**
     *   - **an {@link Ext.data.Store#storeId ID of a store}**
     *   - **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}.
     *   - **a {@link Ext.data.Store Store} config object**.  When passing a config you can 
     *     specify the store type by alias.  Passing a config object with a store type will 
     *     dynamically create a new store of that type when the combo / tagfield is 
     *     instantiated.
     *
     *     Ext.define('MyApp.store.States', {
     *         extend: 'Ext.data.Store',
     *         alias: 'store.states',
     *         fields: ['name']
     *     });
     *     
     *     Ext.create({
     *         xtype: 'combobox',
     *         renderTo: document.body,
     *         store: {
     *             type: 'states',
     *             data: [{
     *                 name: 'California'
     *             }]
     *         },
     *         queryMode: 'local',
     *         displayField: 'name',
     *         valueField: 'name'
     *     });
     *
     * 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}.
     * @deprecated 5.1.0 Use {@link Ext.form.field.Tag} or {@link Ext.view.MultiSelector}
     */
    multiSelect: false,
 
    /**
     * @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
     *
     *   - **`'last'`**: {@link #doQuery run the query} using the
     * `{@link #lastQuery last query value}`.
     *
     *   - **`'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
     * 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 {Boolean} autoLoadOnValue
     * This option controls whether to *initially* load the store when a value is set so that
     * the display value can be determined from the appropriate record.
     * The store will only be loaded in a limited set of circumstances:
     * - The store is not currently loading.
     * - The store does not have a pending {@link Ext.data.Store#autoLoad}.
     * - The store has not been loaded before.
     */
    autoLoadOnValue: false,
 
    /**
     * @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} anyMatch
     * Configure as `true` to allow matching of the typed characters at any position in the
     * {@link #valueField}'s value.
     */
    anyMatch: false,
 
    /**
     * @cfg {Boolean} caseSensitive
     * Configure as `true` to make the filtering match with exact case matching
     */
    caseSensitive: 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} autoSelectLast
     * When `true`, the last selected record in the dropdown list will be re-selected
     * upon {@link #autoSelect}. Set to `false` to always select the first record in the
     * drop-down list. For accessible applications it is recommended to set this option
     * to `false`.
     */
    autoSelectLast: 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 {Boolean} clearFilterOnBlur
     * *When {@link #queryMode} is `'local'` only*
     * 
     * As text is entered, the underlying store is filtered to match the value. When this option
     * is `true`, any filtering applied by this field will be cleared when focus is removed
     * & reinstated on focus.
     * If `false`, the filters will be left in place.
     */
    clearFilterOnBlur: true,
 
    /**
     * @cfg {Boolean} enableRegEx
     * *When {@link #queryMode} is `'local'` only*
     *
     * Set to `true` to have the ComboBox use the typed value as a RegExp source to filter the store
     * to get possible matches. Invalid regex values will be ignored.
     */
 
    /**
     * @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.dom.Element} transform
     * The id, DOM node or {@link Ext.dom.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 {Boolean} transformInPlace
     * `true` to automatically render this combo box in place of the select element that is being 
     * {@link #transform transformed}. If `false`, this combo will be rendered using the normal
     * rendering, either as part of a layout, or using {@link #renderTo} or {@link #method-render}.
     */
    transformInPlace: true,
 
    /**
     * @cfg {Object} listConfig
     * An optional set of configuration properties that will be passed to the
     * {@link Ext.view.BoundList}'s constructor. Any configuration that is valid for BoundList
     * can be included. Some of the more useful ones are:
     *
     *   - {@link Ext.view.BoundList#cls cls} - defaults to empty
     *   - {@link Ext.view.BoundList#emptyText emptyText} - defaults to empty string
     *   - {@link Ext.view.BoundList#itemSelector itemSelector} - defaults to the value defined
     *     in BoundList
     *   - {@link Ext.view.BoundList#loadingText loadingText} - defaults to `'Loading...'`
     *   - {@link Ext.view.BoundList#minWidth minWidth} - defaults to `70`
     *   - {@link Ext.view.BoundList#maxWidth maxWidth} - defaults to `undefined`
     *   - {@link Ext.view.BoundList#maxHeight maxHeight} - defaults to `300`
     *   - {@link Ext.view.BoundList#resizable resizable} - defaults to `false`
     *   - {@link Ext.view.BoundList#shadow shadow} - defaults to `'sides'`
     *   - {@link Ext.view.BoundList#width width} - defaults to `undefined` (automatically set
     *     to the width of the ComboBox field if {@link #matchFieldWidth} is true)
     *   - {@link Ext.view.BoundList#getInnerTpl getInnerTpl} A function which returns a template
     *     string which renders the ComboBox's {@link #displayField} value in the dropdown.
     *     This defaults to just outputting the raw value, but may use any
     *     {@link Ext.XTemplate XTemplate} methods to produce output.
     *
     *     The running template is configured with some extra properties that provide some context:
     *         - field {@link Ext.form.field.ComboBox ComboBox} This combobox
     *         - store {@link Ext.data.Store Store} This combobox's data store
     */
 
    /**
     * @private
     */
    clearValueOnEmpty: true,
 
    newlineRe: /\r?\n/g,
 
    getGrowWidth: function() {
        var me = this,
            value = me.inputEl.dom.value,
            field, store, dataLn, currentLongestLength,
            i, item, itemLn;
 
        if (me.growToLongestValue) {
            field = me.displayField;
            store = me.store;
            dataLn = store.data.length;
            currentLongestLength = 0;
 
            for (= 0; i < dataLn; i++) {
                item = store.getAt(i).data[field];
                itemLn = item.length;
 
                // Compare the current item's length with the current longest length
                // and store the value.
                if (itemLn > currentLongestLength) {
                    currentLongestLength = itemLn;
                    value = item;
                }
            }
        }
 
        return value;
    },
 
    /**
     * @event beforequery
     * Fires before all queries are processed. Return false to cancel the query or set
     * the queryPlan's cancel property to true.
     *
     * @param {Object} queryPlan An object containing details about the query to be executed.
     * @param {Ext.form.field.ComboBox} queryPlan.combo A reference to this ComboBox.
     * @param {String} queryPlan.query The query value to be used to match against the ComboBox's
     * {@link #valueField}.
     * @param {Boolean} queryPlan.forceAll If `true`, causes the query to be executed even
     * if the minChars threshold is not met.
     * @param {Boolean} queryPlan.cancel A boolean value which, if set to `true` upon return,
     * causes the query not to be executed.
     * @param {Boolean} queryPlan.rawQuery If `true` indicates that the raw input field value
     * is being used, and upon store load,
     */
 
    /**
     * @event select
     * Fires when at least one list item is selected.
     * @param {Ext.form.field.ComboBox} combo This combo box
     * @param {Ext.data.Model/Ext.data.Model[]} record With {@link #multiSelect} 
     * `false`, the value will be a single record. With {@link #multiSelect} `true`, the 
     * value will be an array of records.
     */
 
    /**
     * @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
     */
 
    /**
     * @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
     */
 
    initComponent: function() {
        var me = this,
            isDefined = Ext.isDefined,
            store = me.store,
            transform = me.transform,
            transformSelect, isLocalMode;
 
        //<debug>
        if (me.typeAhead && me.multiSelect) {
            Ext.raise('typeAhead and multiSelect are mutually exclusive options -- ' +
                      'please remove one of them.');
        }
 
        if (me.typeAhead && !me.editable) {
            Ext.raise('If typeAhead is enabled the combo must be editable: true -- ' +
                      'please change one of those settings.');
        }
 
        if (me.selectOnFocus && !me.editable) {
            Ext.raise('If selectOnFocus is enabled the combo must be editable: true -- ' +
                      'please change one of those settings.');
        }
        //</debug>
 
        // Check for presence of deprecated pinList config, and convert it to collapseOnSelect
        if ('pinList' in me) {
            me.collapseOnSelect = !me.pinList;
        }
 
        // 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;
                }
            }
        }
 
        // Nothing configured, so generate one. This allows the user to
        // specify displayField in initComponent for extended classes.
        // It's necessary to check me.displayTpl.auto here because due to the
        // order of the config system, the displayTpl might be auto generated
        // before we get to use the user's config, so we use me.displayTpl.auto
        // to detect that it is auto generated.
        if (!me.displayTpl || me.displayTpl.auto) {
            me.setDisplayTpl(false);
        }
 
        me.bindStore(store || 'ext-empty-store', true, true);
 
        isLocalMode = me.queryMode === 'local';
 
        if (!isDefined(me.queryDelay)) {
            me.queryDelay = isLocalMode ? 10 : 500;
        }
 
        if (!isDefined(me.minChars)) {
            me.minChars = isLocalMode ? 0 : 4;
        }
 
        me.callParent();
 
        me.doQueryTask = new Ext.util.DelayedTask(me.doRawQuery, me);
 
        // render in place of 'transform' select
        if (transformSelect) {
            if (me.transformInPlace) {
                me.render(transformSelect.parentNode, transformSelect);
                delete me.renderTo;
            }
 
            Ext.removeNode(transformSelect);
        }
    },
 
    initEvents: function() {
        var me = this;
 
        me.callParent();
 
        // These key bindings need to have higher priority than BoundList keynav
        // so that Alt-Up/Down arrows would expand and collapse the picker without
        // highlighting items and/or changing selection
        me.altArrowKeyNav = new Ext.util.KeyNav({
            target: me.inputEl,
            forceKeyDown: true,
            priority: 1002, // BoundList keynav has 1001
            scope: me,
 
            down: {
                alt: true,
                handler: me.onAltDownArrow
            },
            up: {
                alt: true,
                handler: me.onAltUpArrow
            }
        });
    },
 
    getSubTplData: function(fieldData) {
        var me = this,
            id = me.id,
            data, ariaAttr;
 
        data = me.callParent([fieldData]);
 
        if (!me.ariaStaticRoles[me.ariaRole]) {
            ariaAttr = data.ariaElAttributes;
 
            if (ariaAttr) {
                ariaAttr['aria-owns'] = id + '-inputEl ' + id + '-picker-listEl';
 
                // TODO Change that to reflect the real behavior
                ariaAttr['aria-autocomplete'] = 'list';
            }
        }
 
        return data;
    },
 
    getSubTplMarkup: function(fieldData) {
        var me = this,
            hiddenDataElMarkup = '',
            markup = me.callParent(arguments);
 
        if (me.hiddenName) {
            hiddenDataElMarkup = '<div id="' + fieldData.id +
                                 '-hiddenDataEl" data-ref="hiddenDataEl" class="' +
                                 me.hiddenDataCls + '" role="presentation"></div>';
        }
 
        return hiddenDataElMarkup + markup;
    },
 
    applyDisplayTpl: function(displayTpl) {
        var me = this;
 
        if (!displayTpl) {
            /* eslint-disable indent, max-len */
            displayTpl = new Ext.XTemplate(
                '<tpl for=".">' +
                    '{[typeof values === "string" ? values : values["' + me.getDisplayField() + '"]]}' +
                    '<tpl if="xindex < xcount">' + me.getDelimiter() + '</tpl>' +
                '</tpl>'
            );
            /* eslint-enable indent, max-len */
 
            displayTpl.auto = true;
        }
        else if (!displayTpl.isTemplate) {
            displayTpl = new Ext.XTemplate(displayTpl);
        }
 
        return displayTpl;
    },
 
    applyFilters: function(filters, collection) {
        var me = this;
 
        if (filters === null || filters.isFilterCollection) {
            return filters;
        }
 
        if (filters) {
            if (!collection) {
                collection = this.getFilters();
            }
 
            collection.beginUpdate();
 
            collection.splice(0, collection.length, filters);
            collection.each(function(filter) {
                filter.ownerId = me.id;
            });
 
            collection.endUpdate();
        }
 
        return collection;
    },
 
    applyValueNotFoundText: function(v) {
        var me = this,
            displayField = me.getDisplayField(),
            valueField = me.valueField,
            valueNotFoundRecord = me.valueNotFoundRecord;
 
        if (!valueNotFoundRecord) {
            valueNotFoundRecord = me.valueNotFoundRecord = new Ext.data.Model();
        }
 
        valueNotFoundRecord.set(displayField, v);
 
        if (valueField && displayField !== valueField) {
            valueNotFoundRecord.set(valueField, v);
        }
 
        return v;
    },
 
    updateDisplayField: function(displayField) {
        // force displayTpl refresh when displayField changes
        if (displayField && !this.displayTpl || this.displayTpl.auto) {
            this.setDisplayTpl(false);
        }
    },
 
    /**
     * Returns the `Ext.util.FilterCollection`. Unless `autoCreate` is explicitly passed
     * as `false` this collection will be automatically created if it does not yet exist.
     * @param [autoCreate=true] Pass `false` to disable auto-creation of the collection.
     * @return {Ext.util.FilterCollection} The collection of filters.
     */
    getFilters: function(autoCreate) {
        var ret = this.filters;
 
        if (!ret && autoCreate !== false) {
            ret = new Ext.util.FilterCollection();
            this.setFilters(ret);
        }
 
        return ret;
    },
 
    updateFilters: function(newFilters, oldFilters) {
        var me = this;
 
        if (oldFilters) {
            oldFilters.un('endupdate', 'onEndUpdateFilters', me);
        }
 
        if (newFilters) {
            newFilters.on('endupdate', 'onEndUpdateFilters', me);
        }
 
        me.onEndUpdateFilters(newFilters);
    },
 
    onEndUpdateFilters: function(filters) {
        var me = this,
            was = me.filtered,
            is = !!filters && (filters.length > 0), // booleanize filters
            old, storeFilters;
 
        if (was || is) {
            me.filtered = is;
            old = [];
            storeFilters = me.store.getFilters();
 
            storeFilters.each(function(filter) {
                if (filter.ownerId === me.id && !filters.contains(filter)) {
                    old.push(filter);
                }
            });
 
            storeFilters.splice(0, old, filters.items);
        }
    },
 
    clearLocalFilter: function() {
        var me = this,
            filter = me.queryFilter;
 
        if (filter) {
            me.queryFilter = null;
            // Must set changingFilters flag for this.checkValueOnChange.
            // the suppressEvents flag does not affect the filterchange event
            me.changingFilters = true;
            me.store.removeFilter(filter, true);
            me.changingFilters = false;
        }
    },
 
    completeEdit: function(e) {
        var me = this;
 
        this.callParent([e]);
        me.doQueryTask.cancel();
        me.assertValue();
 
        if (me.queryFilter && me.queryMode === 'local' && me.clearFilterOnBlur) {
            me.clearLocalFilter();
        }
    },
 
    onFocus: function(e) {
        var me = this;
 
        me.callParent([e]);
 
        if (me.triggerAction !== 'all' && me.queryFilter && me.queryMode === 'local' &&
            me.clearFilterOnBlur) {
            delete me.lastQuery;
            me.doRawQuery();
        }
    },
 
    onAltDownArrow: function(e) {
        e.stopEvent();
 
        if (!this.isExpanded) {
            this.onDownArrow(e);
        }
 
        // Stop further keyNav processing
        return false;
    },
 
    onAltUpArrow: function(e) {
        e.stopEvent();
 
        if (this.isExpanded) {
            this.onEsc(e);
        }
 
        // Stop further keyNav processing
        return false;
    },
 
    /**
     * @private
     */
    assertValue: function() {
        var me = this,
            rawValue = me.getRawValue(),
            displayValue = me.getDisplayValue(),
            lastRecords = me.lastSelectedRecords,
            preventChange = false,
            needToSelect = false,
            value, 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 (rawValue !== displayValue) {
                    me.setRawValue(displayValue);
                }
            }
            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(rawValue);
 
                if (!rec) {
                    // if we have a custom displayTpl it's likely that findRecordByDisplay won't
                    // find the value based on RawValue, so we give it another try using the data
                    // stored in displayTplData if there is any.
                    if (me.displayTplData && me.displayTplData.length) {
                        rec = me.findRecordByValue(me.displayTplData[0][me.valueField]);
                    }
 
                    if (!rec && lastRecords && (!me.allowBlank || me.rawValue)) {
                        rec = lastRecords[0];
 
                        // Can't get rec from display value, so resetting previous one. 
                        // We should must fire select for this (As we will change display value)
                        needToSelect = true;
                    }
                }
                // Prevent an issue where we have duplicate display values with
                // different underlying values.
                else if (me.getDisplayValue([me.getRecordDisplayData(rec)]) === displayValue) {
                    rec = null;
                    preventChange = true;
                }
 
                if (rec) {
                    if (needToSelect || !(lastRecords && lastRecords[0] === rec)) {
                        me.select(rec, true);
                        me.fireEvent('select', me, rec);
                    }
                }
                else if (!preventChange) {
                    if (lastRecords) {
                        delete me.lastSelectedRecords;
                    }
 
                    // We need to reset any value that could have been set in the dom before
                    // or during a store load for remote combos. If we don't reset this,
                    // then ComboBox#getValue() will think that the value has changed and will
                    // then set `undefined` as the .value for forceSelection combos.  This then
                    // gets changed AGAIN to `null`, which will get set into the model field
                    // for editors. This is BAD.
                    me.setRawValue('');
                }
            }
        }
        // we can only call getValue() in this process if forceSelection is false
        // otherwise it will break the grid edit on tab
        // eslint-disable-next-line eqeqeq
        else if ((value = me.getValue()) && value == rawValue) {
            rec = me.findRecordByDisplay(value);
 
            if (rec && (rec !== (lastRecords && lastRecords[0]) ||
                me.displayField !== me.valueField)) {
                me.select(rec, true);
                me.fireEvent('select', me, rec);
            }
        }
 
        me.collapse();
    },
 
    onTypeAhead: function() {
        var me = this,
            displayField = me.displayField,
            record = me.store.findRecord(displayField, me.getRawValue()),
            newValue, len, selStart;
 
        if (record) {
            newValue = record.get(displayField);
            len = newValue.length;
            selStart = me.getRawValue().length;
 
            if (selStart !== 0 && selStart !== len) {
                // Setting the raw value will cause a field mutation event.
                // Prime the lastMutatedValue so that this does not cause a requery.
                me.lastMutatedValue = newValue;
 
                me.setRawValue(newValue);
                me.selectText(selStart, newValue.length);
            }
        }
    },
 
    // invoked when a different store is bound to this combo
    // than the original
    resetToDefault: Ext.emptyFn,
 
    beforeReset: function() {
        this.callParent();
        this.clearLocalFilter();
    },
 
    onUnbindStore: function() {
        var me = this,
            picker = me.picker;
 
        // If we'd added a local filter, remove it.
        // Listeners are unbound, so we don't need the changingFilters flag
        if (me.queryFilter && !me.store.destroyed) {
            me.clearLocalFilter();
        }
 
        if (picker) {
            picker.bindStore(null);
        }
 
        me.pickerSelectionModel.destroy();
    },
 
    onBindStore: function(store, initial) {
        var me = this,
            picker = me.picker,
            extraKeySpec,
            valueCollectionConfig;
 
        // We're being bound, not unbound...
        if (store) {
            // If store was created from a 2 dimensional array with generated field names
            // 'field1' and 'field2'
            if (store.autoCreated) {
                me.queryMode = 'local';
                me.valueField = me.displayField = 'field1';
 
                if (!store.expanded) {
                    me.displayField = 'field2';
                }
 
                // displayTpl config will need regenerating with the autogenerated displayField
                // name 'field1'
                if (me.getDisplayTpl().auto) {
                    me.setDisplayTpl(null);
                }
            }
 
            if (!Ext.isDefined(me.valueField)) {
                me.valueField = me.displayField;
            }
 
            // Add a byValue index to the store so that we can efficiently look up records
            // by the value field when setValue passes string value(s).
            // The two indices (Ext.util.CollectionKeys) are configured unique: false,
            // so that if duplicate keys are found, they are all returned by the get call.
            // This is so that findByText and findByValue are able to return the *FIRST*
            // matching value. By default, if unique is true, CollectionKey keeps the *last*
            // matching value.
            extraKeySpec = {
                byValue: {
                    rootProperty: 'data',
                    unique: false
                }
            };
 
            extraKeySpec.byValue.property = me.valueField;
            store.setExtraKeys(extraKeySpec);
 
            if (me.displayField === me.valueField) {
                store.byText = store.byValue;
            }
            else {
                extraKeySpec.byText = {
                    rootProperty: 'data',
                    unique: false
                };
                extraKeySpec.byText.property = me.displayField;
                store.setExtraKeys(extraKeySpec);
            }
 
            // We hold a collection of the values which have been selected, keyed by this field's
            // valueField. This collection also functions as the selected items collection
            // for the BoundList's selection model
            valueCollectionConfig = {
                rootProperty: 'data',
                extraKeys: {
                    byInternalId: {
                        property: 'internalId'
                    },
                    byValue: {
                        property: me.valueField,
                        rootProperty: 'data'
                    }
                },
                // Whenever this collection is changed by anyone, whether by this field
                // adding to it, or the BoundList operating, we must refresh our value.
                listeners: {
                    beginupdate: me.onValueCollectionBeginUpdate,
                    endupdate: me.onValueCollectionEndUpdate,
                    scope: me
                }
            };
 
            // This becomes our collection of selected records for the Field.
            me.valueCollection = new Ext.util.Collection(valueCollectionConfig);
 
            // This is the selection model we configure into the dropdown BoundList.
            // We use the selected Collection as our value collection and the basis
            // for rendering the tag list.
            me.pickerSelectionModel = new Ext.selection.DataViewModel({
                mode: me.multiSelect ? 'SIMPLE' : 'SINGLE',
                // There are situations when a row is selected on mousedown but then the mouse
                // is dragged to another row and released.  In these situations, the event target
                // for the click event won't be the row where the mouse was released
                // but the boundview. The view will then determine that it should fire a container
                // click, and the DataViewModel will then deselect all prior selections.
                // Setting `deselectOnContainerClick` here will prevent the model from deselecting.
                ordered: true,
                deselectOnContainerClick: false,
                enableInitialSelection: false,
                pruneRemoved: false,
                selected: me.valueCollection,
                store: store,
                listeners: {
                    scope: me,
                    lastselectedchanged: me.updateBindSelection
                }
            });
 
            if (!initial) {
                me.resetToDefault();
            }
 
            if (picker) {
                me.pickerSelectionModel.on({
                    scope: me,
                    beforeselect: me.onBeforeSelect,
                    beforedeselect: me.onBeforeDeselect
                });
 
                picker.setSelectionModel(me.pickerSelectionModel);
 
                if (picker.getStore() !== store) {
                    picker.bindStore(store);
                }
            }
        }
    },
 
    /**
     * Binds a store to this instance.
     * @param {Ext.data.AbstractStore/String} [store] The store to bind or ID of the store.
     * When no store given (or when `null` or `undefined` passed), unbinds the existing store.
     * @param {Boolean} [preventFilter] `true` to prevent any active filter from being activated
     * on the newly bound store. This is only valid when used with {@link #queryMode} `'local'`.
     * @param {Boolean} initial (private)
     */
    bindStore: function(store, preventFilter, initial) {
        var me = this,
            filter = me.queryFilter;
 
        me.mixins.storeholder.bindStore.call(me, store, initial);
        store = me.getStore();
 
        if (store && filter && !preventFilter) {
            store.getFilters().add(filter);
        }
 
        if (!initial && store && !store.isEmptyStore) {
            me.setValueOnData();
        }
    },
 
    getStoreListeners: function(store) {
        var me = this,
            result;
 
        // Don't bother with listeners on the dummy store that is provided for an unconfigured
        // ComboBox prior to a real store arriving from a ViewModel.
        // Nothing is ever going to be fired.
        if (!store.isEmptyStore) {
            result = {
                datachanged: me.onDataChanged,
                load: me.onLoad,
                exception: me.onException,
                update: me.onStoreUpdate,
                remove: me.checkValueOnChange
            };
 
            // If we are doing remote filtering, then mutating the store's filters should not
            // result in a re-evaluation of whether the current value is still present in the store.
            if (!store.getRemoteFilter()) {
                result.filterchange = me.checkValueOnChange;
            }
 
            return result;
        }
    },
 
    onDataChanged: function() {
        if (this.grow && this.growToLongestValue) {
            this.autoSize();
        }
    },
 
    checkValueOnChange: function() {
        var me = this;
 
        // Will be triggered by removal of filters upon destroy
        if (!me.destroying && me.getStore().isLoaded()) {
            // If multiselecting and the base store is modified, we may have to remove records
            // from the valueCollection if they have gone from the base store, or update
            // the rawValue if selected records are mutated.
            // TODO: 5.1.1: Use a ChainedStore for multiSelect so that selected records are not
            // filtered out of the base store and are able to be removed.
            // See https://sencha.jira.com/browse/EXTJS-16096
            if (me.multiSelect) {
                // TODO: Implement in 5.1.1 when selected records are available for modification
                // and not filtered out. valueCollection must be in sync with what's available
                // in the base store, and rendered rawValue/tags must match any updated data.
            }
            else {
                if (me.forceSelection && !me.changingFilters && !me.findRecordByValue(me.value)) {
                    // skip this if query mode is remote and the user is typing
                    // or is executing a page load
                    if (me.queryMode !== 'local' && (me.hasFocus || me.isPaging)) {
                        return;
                    }
 
                    me.setValue(null);
                }
            }
        }
    },
 
    onStoreUpdate: function(store, record) {
        // Ensure the rawValue is rendered correctly whenever a store record is mutated
        this.updateValue();
    },
 
    onException: function() {
        this.isPaging = false;
        this.collapse();
    },
 
    onLoad: function(store, records, success) {
        var me = this,
            // This flag is saying that we need to call setValue to match the value property
            // with the just loaded record set and update the valueCollection
            // (and thereby any bound ViewModel) with that matched record.
            needsValueUpdating = !me.valueCollection.byValue.get(me.value);
 
        // If not returning from a query, and the value was set from a raw data value,
        // unrelated to a record because the displayField was not honoured when calculating
        // the raw value, then we update the raw value.
        if (success && needsValueUpdating && !me.isPaging &&
            !(store.lastOptions && 'rawQuery' in store.lastOptions)) {
            me.setValueOnData();
        }
 
        // This synchronizes the value based upon contents of the store
        me.checkValueOnChange();
        me.isPaging = false;
    },
 
    setValueOnData: function() {
        var me = this;
 
        me.setValue(me.value);
 
        // Highlight the selected record
        if (me.isExpanded && me.getStore().getCount()) {
            me.doAutoSelect();
        }
    },
 
    /**
     * @private
     * Execute the query with the raw contents within the textfield.
     */
    doRawQuery: function() {
        var me = this,
            rawValue = me.inputEl.dom.value;
 
        // Use final bit after comma as query value if multiselecting
        if (me.multiSelect) {
            rawValue = rawValue.split(me.delimiter).pop();
        }
 
        me.doQuery(rawValue, 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 string to use to filter available items by matching
     * against the configured {@link #valueField}.
     * @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) {
        var me = this,
            store = me.getStore(),
            filters = store.getFilters(),
 
            // Decide if, and how we are going to query the store
            queryPlan = me.beforeQuery({
                lastQuery: me.lastQuery || '',
                query: queryString || '',
                rawQuery: rawQuery,
                forceAll: forceAll,
                combo: me,
                cancel: false
            }),
            refreshFilters;
 
        // Allow veto.
        if (queryPlan !== false && !queryPlan.cancel) {
            // if we have a queryString and we don't have a queryFilter or the queryFilter
            // has changed since the last query, we should run a query.
            refreshFilters = !!queryString && (!me.queryFilter || me.queryFilter &&
                             (filters.indexOf(me.queryFilter) < 0));
 
            // If they're using the same value as last time (and not being asked to query all), 
            // and the filters don't need to be refreshed, just show the dropdown
            if (me.queryCaching && !refreshFilters && queryPlan.query === me.lastQuery) {
                // The filter changing was done with events suppressed, so
                // refresh the picker DOM while hidden and it will layout on show.
                me.getPicker().refresh();
                me.expand();
                me.afterQuery(queryPlan);
            }
 
            // Otherwise filter or load the store
            else {
                me.lastQuery = queryPlan.query;
 
                if (me.queryMode === 'local') {
                    me.doLocalQuery(queryPlan);
 
                }
                else {
                    me.doRemoteQuery(queryPlan);
                }
            }
 
            return true;
        }
        // If the query was vetoed we still need to check the change
        // in case custom validators are used
        else {
            me.startCheckChangeTask();
        }
 
        return false;
    },
 
    /**
     * @template
     * A method which may modify aspects of how the store is to be filtered (if {@link #queryMode}
     * is `"local"`) of loaded (if {@link #queryMode} is `"remote"`).
     *
     * This is called by the {@link #doQuery} method, and may be overridden in subclasses to modify
     * the default behaviour.
     *
     * This method is passed an object containing information about the upcoming query operation
     * which it may modify before returning.
     *
     * @param {Object} queryPlan An object containing details about the query to be executed.
     * @param {String} queryPlan.query The query value to be used to match against the ComboBox's
     * {@link #valueField}.
     * @param {String} queryPlan.lastQuery The query value used the last time a store query
     * was made.
     * @param {Boolean} queryPlan.forceAll If `true`, causes the query to be executed even if
     * the minChars threshold is not met.
     * @param {Boolean} queryPlan.cancel A boolean value which, if set to `true` upon return,
     * causes the query not to be executed.
     * @param {Boolean} queryPlan.rawQuery If `true` indicates that the raw input field value
     * is being used, and upon store load, the input field value should **not** be overwritten.
     *
     */
    beforeQuery: function(queryPlan) {
        var me = this;
 
        // Allow beforequery event to veto by returning false
        if (me.fireEvent('beforequery', queryPlan) === false) {
            queryPlan.cancel = true;
        }
 
        // Allow beforequery event to veto by returning setting the cancel flag
        else if (!queryPlan.cancel) {
 
            // If the minChars threshold has not been met, and we're not forcing an "all" query,
            // cancel the query
            if (queryPlan.query.length < me.minChars && !queryPlan.forceAll) {
                queryPlan.cancel = true;
            }
        }
 
        return queryPlan;
    },
 
    doLocalQuery: function(queryPlan) {
        var me = this,
            queryString = queryPlan.query,
            store = me.getStore(),
            value = queryString,
            filter;
 
        me.clearLocalFilter();
 
        // Querying by a string...
        if (queryString) {
            // User can be typing a regex in here, if it's invalid
            // just swallow the exception and move on
            if (me.enableRegEx) {
                try {
                    value = new RegExp(value);
                }
                catch (e) {
                    value = null;
                }
            }
 
            if (value !== null) {
                // Must set changingFilters flag for this.checkValueOnChange.
                // the suppressEvents flag does not affect the filterchange event
                me.changingFilters = true;
 
                filter = me.queryFilter = new Ext.util.Filter({
                    id: me.id + '-filter',
                    anyMatch: me.anyMatch,
                    caseSensitive: me.caseSensitive,
                    root: 'data',
                    property: me.displayField,
                    value: value
                });
 
                store.addFilter(filter, true);
                me.changingFilters = false;
            }
        }
 
        // Expand after adjusting the filter if there are records or if emptyText is configured.
        if (me.store.getCount() || me.getPicker().emptyText) {
            // The filter changing was done with events suppressed, so
            // refresh the picker DOM while hidden and it will layout on show.
            me.getPicker().refresh();
            me.expand();
        }
        else {
            me.collapse();
        }
 
        me.afterQuery(queryPlan);
    },
 
    doRemoteQuery: function(queryPlan) {
        var me = this,
            loadCallback = function() {
                if (!me.destroyed) {
                    me.afterQuery(queryPlan);
                }
            };
 
        // expand before loading so LoadMask can position itself correctly
        me.expand();
 
        // 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, {
                rawQuery: queryPlan.rawQuery,
                callback: loadCallback
            });
        }
        else {
            me.store.load({
                params: me.getParams(queryPlan.query),
                rawQuery: queryPlan.rawQuery,
                callback: loadCallback
            });
        }
    },
 
    /**
     * @template
     * A method called when the filtering caused by the {@link #doQuery} call is complete
     * and the store has been either filtered locally (if {@link #queryMode} is `"local"`),
     * or has been loaded using the specified filtering.
     *
     * @param {Object} queryPlan An object containing details about the query was executed.
     * @param {String} queryPlan.query The query value to be used to match against the ComboBox's
     * {@link #valueField}.
     * @param {Boolean} queryPlan.forceAll If `true`, causes the query to be executed even if
     * the minChars threshold is not met.
     * @param {Boolean} queryPlan.cancel A boolean value which, if set to `true` upon return,
     * causes the query not to be executed.
     * @param {Boolean} queryPlan.rawQuery If `true` indicates that the raw input field value
     * is being used, and upon store load, the input field value should **not** be overwritten.
     */
    afterQuery: function(queryPlan) {
        var me = this;
 
        if (me.store.getCount()) {
            if (me.typeAhead) {
                me.doTypeAhead(queryPlan);
            }
 
            if (queryPlan.rawQuery) {
                if (me.picker && !me.picker.getSelectionModel().hasSelection()) {
                    me.doAutoSelect();
                }
            }
            else {
                me.doAutoSelect();
            }
        }
 
        // doQuery is called upon field mutation, so check for change
        // after the query has done its thing
        me.startCheckChangeTask();
    },
 
    loadPage: function(pageNum, options) {
        this.isPaging = true;
 
        this.store.loadPage(pageNum, Ext.apply({
            params: this.getParams(this.lastQuery)
        }, options));
    },
 
    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,
            store = me.store,
            picker = me.picker,
            itemNode = 0,
            selectionModel, lastSelected;
 
        if (picker && me.autoSelect && store.getCount() > 0) {
            if (me.autoSelectLast) {
                selectionModel = picker.getSelectionModel();
                lastSelected = selectionModel.lastSelected;
 
                // Highlight the last selected item and scroll it into view,
                // but only if it wasn't filtered out
                if (lastSelected && selectionModel.selected.length &&
                    store.indexOf(lastSelected) > -1) {
                    itemNode = lastSelected;
                }
            }
 
            picker.getNavigationModel().setPosition(itemNode);
        }
    },
 
    doTypeAhead: function(queryPlan) {
        var me = this;
 
        if (!me.typeAheadTask) {
            me.typeAheadTask = new Ext.util.DelayedTask(me.onTypeAhead, me);
        }
 
        // Only typeahead when user extends the query string, or it's a completely different query
        // If user is erasing, re-extending with typeahead is not wanted.
        if (queryPlan.query.length > queryPlan.lastQuery.length ||
            !Ext.String.startsWith(queryPlan.lastQuery, queryPlan.query)) {
            me.typeAheadTask.delay(me.typeAheadDelay);
        }
    },
 
    onTriggerClick: function(comboBox, trigger, e) {
        var me = this,
            oldAutoSelect;
 
        if (!me.readOnly && !me.disabled) {
            if (me.isExpanded) {
                me.collapse();
            }
            else {
                // Alt-Down arrow opens the picker but does not select items:
                // http://www.w3.org/TR/wai-aria-practices/#combobox
                if (&& e.type === 'keydown' && e.altKey) {
                    oldAutoSelect = me.autoSelect;
                    me.autoSelect = false;
                    me.expand();
                    me.autoSelect = oldAutoSelect;
                }
                else {
                    if (me.triggerAction === 'all') {
                        me.doQuery(me.allQuery, true);
                    }
                    else if (me.triggerAction === 'last') {
                        me.doQuery(me.lastQuery, true);
                    }
                    else {
                        me.doQuery(me.getRawValue(), false, true);
                    }
                }
            }
        }
    },
 
    onFieldMutation: function(e) {
        var me = this,
            key = e.getKey(),
            isDelete = key === e.BACKSPACE || key === e.DELETE,
            rawValue = me.inputEl.dom.value,
            len = rawValue.length;
 
        // Do not process two events for the same mutation.
        // For example an input event followed by the keyup that caused it.
        // We must process delete keyups.
        // Also, do not process TAB event which fires on arrival.
        if (!me.readOnly && (rawValue !== me.lastMutatedValue || isDelete) && key !== e.TAB) {
            me.lastMutatedValue = rawValue;
            me.refreshEmptyText();
 
            if (len && (e.type !== 'keyup' || (!e.isSpecialKey() || isDelete))) {
                me.doQueryTask.delay(me.queryDelay);
            }
            else {
                // We have *erased* back to empty if key is a delete,
                // or it is a non-key event (cut/copy)
                if (!len && (!key || isDelete)) {
                    // This portion of code may end up calling setValue will check for change.
                    // But since it's come from field mutations, we need to respect
                    // the checkChangeBuffer, so we suspend checks here, it will be handled
                    // by callParent
                    ++me.suspendCheckChange;
 
                    // Essentially a silent setValue.
                    // Clear our value, and the tplData used to construct a mathing raw value.
                    if (!me.multiSelect) {
                        me.value = null;
                        me.displayTplData = undefined;
                    }
 
                    // If the value is blank we can't have a value
                    if (me.clearValueOnEmpty) {
                        me.valueCollection.beginUpdate();
                        me.pickerSelectionModel.deselectAll();
                        me.valueCollection.removeAll();
                        me.valueCollection.endUpdate();
                    }
 
                    // Just erased back to empty. Hide the dropdown.
                    me.collapse();
 
                    // There may have been a local filter if we were querying locally.
                    // Clear the query filter and suppress the consequences
                    // (we do not want a list refresh).
                    if (me.queryFilter) {
                        me.clearLocalFilter();
                    }
 
                    // When queryCaching if the user deletes the value and then starts typing
                    // the same filter again, doQuery can erroneously expand the picker without
                    // filtering first.
                    me.lastQuery = null;
 
                    --me.suspendCheckChange;
                }
 
                me.callParent([e]);
            }
        }
    },
 
    doDestroy: function() {
        var me = this;
 
        me.doQueryTask.cancel();
 
        if (me.typeAheadTask) {
            me.typeAheadTask.cancel();
            me.typeAheadTask = null;
        }
 
        me.bindStore(null);
        Ext.destroy(me.altArrowKeyNav, me.valueCollection);
 
        me.callParent();
    },
 
    // The picker (the dropdown) must have its zIndex managed by the same ZIndexManager which is
    // providing the zIndex of our Container.
    onAdded: function() {
        var me = this;
 
        me.callParent(arguments);
 
        if (me.picker) {
            me.picker.ownerCt = me.up('[floating]');
            me.picker.registerWithOwnerCt();
        }
    },
 
    createPicker: function() {
        var me = this,
            picker,
            pickerCfg = Ext.apply({
                xtype: 'boundlist',
                id: me.id + '-picker',
                pickerField: me,
                selectionModel: me.pickerSelectionModel,
                floating: true,
                hidden: true,
                store: me.getPickerStore(),
                displayField: me.displayField,
                preserveScrollOnRefresh: true,
                pageSize: me.pageSize,
                tpl: me.tpl,
                ariaSelectable: me.ariaSelectable
            }, me.listConfig, me.defaultListConfig);
 
        picker = me.picker = Ext.widget(pickerCfg);
 
        if (me.pageSize) {
            picker.pagingToolbar.on('beforechange', me.onPageChange, me);
        }
 
        // We limit the height of the picker to fit in the space above
        // or below this field unless the picker has its own ideas about that.
        if (!picker.initialConfig.maxHeight) {
            picker.on({
                beforeshow: me.onBeforePickerShow,
                scope: me
            });
        }
 
        picker.getSelectionModel().on({
            beforeselect: me.onBeforeSelect,
            beforedeselect: me.onBeforeDeselect,
            focuschange: me.onFocusChange,
            scope: me
        });
 
        picker.getNavigationModel().navigateOnSpace = false;
 
        return picker;
    },
 
    getPickerStore: function() {
        return this.store;
    },
 
    onBeforePickerShow: function(picker) {
    // Just before we show the picker, set its maxHeight so it fits
    // either above or below, it will flip to the side where it fits
        var me = this,
            heightAbove = me.getPosition()[1] - Ext.getBody().getScroll().top,
            heightBelow = Ext.Element.getViewportHeight() - heightAbove - me.getHeight();
 
        // Then ensure that vertically, the dropdown will fit into the space
        // either above or below the inputEl.
        // Also have some leeway so we aren't flush against the window edge
        picker.maxHeight = Math.max(heightAbove, heightBelow) - 5;
    },
 
    onBeforeSelect: function(list, record, recordIndex) {
        return this.fireEvent('beforeselect', this, record, recordIndex);
    },
 
    onBeforeDeselect: function(list, record, recordIndex) {
        return this.fireEvent('beforedeselect', this, record, recordIndex);
    },
 
    onFocusChange: function(selModel, prevRecord, newRecord) {
        var picker = this.picker,
            inputEl = this.inputEl,
            el;
 
        if (newRecord) {
            el = picker.getNodeByRecord(newRecord);
 
            if (el) {
                // Need to make sure el has an id without leaving Ext.cache entry
                if (!el.id) {
                    el.id = Ext.id();
                }
 
                inputEl.dom.setAttribute('aria-activedescendant', el.id);
            }
            else {
                inputEl.dom.removeAttribute('aria-activedescendant');
            }
        }
    },
 
    /**
     * Returns the combobox's selection.
     * @return {Ext.data.Model} The selected record
     */
    getSelection: function() {
        var selModel = this.getPicker().getSelectionModel(),
            selection = selModel.getSelection();
 
        return selection.length ? selModel.getLastSelected() : null;
    },
 
    updateSelection: function(selection) {
        var me = this,
            sm;
 
        if (!me.ignoreNextSelection) {
            me.ignoreNextSelection = true;
            sm = me.getPicker().getSelectionModel();
 
            if (selection) {
                sm.select(selection);
                me.hasHadSelection = true;
            }
            else {
                sm.deselectAll();
            }
 
            me.ignoreNextSelection = false;
        }
    },
 
    updateBindSelection: function(selModel, selection) {
        var me = this,
            selected = null;
 
        if (!me.ignoreNextSelection) {
            me.ignoreNextSelection = true;
 
            if (selection.length) {
                selected = selModel.getLastSelected();
                me.hasHadSelection = true;
            }
 
            if (me.hasHadSelection) {
                me.setSelection(selected);
            }
 
            me.ignoreNextSelection = false;
        }
    },
 
    onValueCollectionBeginUpdate: Ext.emptyFn,
 
    onValueCollectionEndUpdate: function() {
        var me = this,
            store = me.store,
            selectedRecords = me.valueCollection.getRange(),
            selectedRecord = selectedRecords[0],
            selectionCount = selectedRecords.length;
 
        me.updateBindSelection(me.pickerSelectionModel, selectedRecords);
 
        if (me.isSelectionUpdating()) {
            return;
        }
 
        Ext.suspendLayouts();
 
        me.lastSelection = selectedRecords;
 
        if (selectionCount) {
            // Track the last selection with a value (non blank) for use in
            // assertValue
            me.lastSelectedRecords = selectedRecords;
        }
 
        me.updateValue();
 
        // If we have selected a value, and it's not possible to select any more values
        // or, we are configured to hide the picker each time, then collapse the picker.
        // eslint-disable-next-line max-len
        if (selectionCount && ((!me.multiSelect && store.contains(selectedRecord)) || me.collapseOnSelect || !store.getCount())) {
            me.updatingValue = true;
            me.collapse();
            me.updatingValue = false;
        }
 
        Ext.resumeLayouts(true);
 
        if (!me.suspendCheckChange) {
            if (!me.multiSelect) {
                selectedRecords = selectedRecord;
            }
 
            me.fireEvent('select', me, selectedRecords);
        }
    },
 
    isSelectionUpdating: function() {
        var selModel = this.pickerSelectionModel;
 
        return selModel.deselectingDuringSelect || selModel.refreshing;
    },
 
    /**
     * @private
     * Enables the key navs for the BoundList when it is expanded.
     */
    onExpand: function() {
        var me = this,
            picker = me.getPicker(),
            keyNav = picker.getNavigationModel(),
            node;
 
        if (keyNav) {
            keyNav.enable();
        }
 
        me.doAutoSelect();
 
        node = picker.highlightedItem;
 
        if (node) {
            // Need to make sure node has an id without leaving Ext.cache entry
            if (!node.id) {
                node.id = Ext.id();
            }
 
            me.inputEl.dom.setAttribute('aria-activedescendant', node.id);
        }
    },
 
    /**
     * @private
     * Disables the key navs for the BoundList when it is collapsed.
     */
    onCollapse: function() {