/**
 * 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; if the {@link #cfg!editable} config is `true`, then 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 which are applied to the list
 * via the {@link #cfg!itemTpl}.
 *
 * If your store is not remote, i.e. it depends only on local data and is loaded up front, you MUST
 * set the {@link #queryMode} to `'local'`.
 *
 * # Example usage:
 *
 *      @example
 *      Ext.create({
 *          fullscreen: true,
 *          xtype: 'container',
 *          padding: 50,
 *          layout: 'vbox',
 *          items: [{
 *              xtype: 'combobox',
 *              label: 'Choose State',
 *              queryMode: 'local',
 *              displayField: 'name',
 *              valueField: 'abbr',
 *
 *              store: [
 *                  { abbr: 'AL', name: 'Alabama' },
 *                  { abbr: 'AK', name: 'Alaska' },
 *                  { abbr: 'AZ', name: 'Arizona' }
 *              ]
 *          }]
 *      });
 *
 * # Events
 *
 * ComboBox fires a select event if an item is chosen from the associated list.  If
 * the ComboBox is configured with {@link #forceSelection}: true, an action event is fired
 * when the user has typed the ENTER key while editing the field, and a change event on
 * each keystroke.
 *
 * ## Customized combobox
 *
 * Both the text shown in dropdown list and text field can be easily customized:
 *
 *      @example
 *      Ext.create({
 *          fullscreen: true,
 *          xtype: 'container',
 *          padding: 50,
 *          layout: 'vbox',
 *          items: [{
 *              xtype: 'combobox',
 *              label: 'Choose State',
 *              queryMode: 'local',
 *              displayField: 'name',
 *              valueField: 'abbr',
 *
 *              // For the dropdown list
 *              itemTpl: '<span role="option" class="x-boundlist-item">{abbr} - {name}</span>',
 *
 *              // For the content of the text field
 *              displayTpl: '{abbr} - {name}',
 *
 *              editable: false,  // disable typing in the text field
 *
 *              store: [
 *                  { abbr: 'AL', name: 'Alabama' },
 *                  { abbr: 'AK', name: 'Alaska' },
 *                  { abbr: 'AZ', name: 'Arizona' }
 *              ]
 *          }]
 *      });
 *
 * See also the {@link #cfg!floatedPicker} and {@link #cfg!edgePicker} options for additional
 * configuration of the options list.
 *
 * @since 6.5.0
 */
Ext.define('Ext.field.ComboBox', {
    extend: 'Ext.field.Select',
    xtype: [
        'combobox',
        'comboboxfield'
    ],
 
    alternateClassName: [
        'Ext.form.field.ComboBox' // classic compat 
    ],
 
    requires: [
        'Ext.dataview.BoundListNavigationModel'
    ],
 
    config: {
        /**
         * @private
         * @readonly
         * The filter instance used to filter the store on input field mutation by typing
         * or pasting.
         *
         * This may be a filter config object which specifies a filter which uses the
         * {@link #cfg!store}'s fields.
         *
         * {@link Ext.util.Filter Filters} may also be instantiated using a custom `filterFn`
         * to allow a developer to specify complex matching. For example, a combobox developer
         * might allow a user to filter using either the {@link #cfg!valueField} or
         * {@link #cfg!displayField} by using:
         *
         *     primaryFilter: {
         *         filterFn: function(candidateRecord) {
         *             // This is a method on a Filter instance, we have this config
         *             var value = this.getValue();
         *
         *             return Ext.String.startsWith(candidateRecord.get('stateName', value, true) ||
         *                    Ext.String.startsWith(candidateRecord.get('abbreviation', value, true);
         *         }
         *     }
         */
        primaryFilter: true,
 
        /**
         * @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:
         *
         *   - **`'local'`** : In this mode, the ComboBox assumes the store is fully loaded and will query it directly.
         *
         *   - **`'remote'`** : In this mode 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 #cfg!queryParam} config.
         *
         *     In `queryMode: 'remote'`, the Store may be configured with `{@link Ext.data.Store#cfg!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 #cfg!hideTrigger} is configured `true` because it has no meaning for
         *     autocomplete.
         */
        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 {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'`
         */
        queryDelay: true,
 
        /**
         * @cfg {Number} minChars 
         * The minimum number of characters the user must type before autocomplete and
         * {@link #typeAhead} activate.
         *
         * Defaults to `4` if {@link #queryMode} is `'remote'` or `0` if {@link #queryMode}
         * is `'local'`, does not apply if {@link Ext.form.field.Trigger#editable editable}
         * is `false`.
         */
        minChars: null,
 
        /**
         * @cfg {Boolean} anyMatch 
         * * Only valid when {@link #cfg!queryMode} is `'local'`.*
         * Configure as `true` to cause the {@link #cfg!primaryFilter} to match the typed
         * characters at any position in the {@link #displayField}'s value when filtering
         * *locally*.
         */
        anyMatch: false,
 
        /**
         * @cfg {Boolean} caseSensitive 
         * * Only valid when {@link #cfg!queryMode} is `'local'`.*
         * Configure as `true` to cause the {@link #cfg!primaryFilter} to match with
         * exact case matching.
         */
        caseSensitive: false,
 
        /**
         * @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} is `true`.
         */
        typeAheadDelay: 250,
 
        /**
         * @cfg {String} triggerAction 
         * The action to execute when the trigger is clicked.
         *
         *   - **`'all'`** :
         *
         *     {@link #doFilter run the query} specified by the `{@link #cfg!allQuery}` config option
         *
         *   - **`'last'`** :
         *
         *     {@link #doFilter run the query} using the `{@link #lastQuery last query value}`.
         *
         *   - **`'query'`** :
         *
         *     {@link #doFilter 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 use to filter the store when the trigger element is tapped
         * (or expansion is requested by a keyboard gesture). By default, this is `null`
         * causing no filtering to occur.
         */
        allQuery: null,
 
        /**
         * @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.
         */
        enableRegEx: null
    },
 
    /**
     * @cfg {Boolean} autoSelect 
     * `true` to auto select the first value in the {@link #store} or {@link #options} when
     * they are changed. Only happens when the {@link #value} is set to `null`.
     */
    autoSelect: false,
 
    /**
     * @cfg editable
     * @inheritdoc Ext.field.Text#cfg-editable
     */
    editable: true,
 
    /**
     * @cfg {Boolean} forceSelection 
     * Set to `true` to restrict the selected value to one of the values in the list, or
     * `false` to allow the user to set arbitrary text into the field.
     */
    forceSelection: false,
 
    /**
     * @event beforepickercreate
     * Fires before the pop-up picker is created to give a developer a chance to configure it.
     * @param {Ext.field.ComboBox} this
     * @param {Object} newValue The config object for the picker.
     */
 
    /**
     * @event pickercreate
     * Fires after the pop-up picker is created to give a developer a chance to configure it.
     * @param {Ext.field.ComboBox} this
     * @param {Ext.dataview.List/Ext.Component} picker The instantiated picker.
     */
 
    /**
     * @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.force 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 {Object} [queryPlan.lastQuery] The queryPlan object used in the previous query.
     */
 
    /**
     * @event select
     * Fires when the user has selected an item from the associated picker.
     * @param {Ext.field.ComboBox} this This field
     * @param {Ext.data.Model} newValue The corresponding record for the new value
     */
 
    /**
     * @event change
     * Fires when the field is changed, or if forceSelection is false, on keystroke.
     * @param {Ext.field.ComboBox} this This field
     * @param {Ext.data.Model} newValue The new value
     * @param {Ext.data.Model} oldValue The original value
     */
 
    // Start with value on prototype. 
    lastQuery: {},
 
    picker: 'floated',
 
    onInput: function (e) {
        var me = this,
            filterTask = me.doFilterTask,
            value = me.inputElement.dom.value,
            filters = me.getStore().getFilters();
 
        if (!me.getForceSelection() || (value === '' && !me.getRequired())) {
            me.callParent([ e ]);
        }
        else {
            // Keep our config up to date: 
            me._inputValue = value;
 
            me.syncEmptyState();
        }
 
        if (value.length) {
            if (!filterTask) {
                filterTask = me.doFilterTask = new Ext.util.DelayedTask(me.doRawFilter, me);
            }
            filterTask.delay(me.getQueryDelay());
        } else {
            me.collapse();
            filters.beginUpdate();
            me.getPrimaryFilter().setDisabled(true);
            filters.endUpdate();
        }
    },
 
    /**
     * @private
     * Execute the query with the raw contents within the textfield.
     */
    doRawFilter: function () {
        var me = this,
            rawValue = me.inputElement.dom.value,
            lastQuery = me.lastQuery.query,
            isErase = lastQuery && lastQuery.length > rawValue.length;
 
        me.doFilter({
            query: rawValue,
            isErase: isErase
        });
    },
 
    /**
     * @private
     * Show the dropdown based upon triggerAction and allQuery
     */
    onExpandTap: function () {
        var me = this,
            triggerAction = me.getTriggerAction();
 
        // TODO: Keyboard operation 
        // Alt-Down arrow opens the picker but does not select items: 
        // http://www.w3.org/TR/wai-aria-practices/#combobox 
 
        if (me.expanded) {
            me.collapse();
        }
        else if (!me.getReadOnly() && !me.getDisabled()) {
            if (triggerAction === 'all') {
                me.doFilter({
                    query: me.getAllQuery(),
                    force: true // overrides the minChars test 
                });
            } else if (triggerAction === 'last') {
                me.doFilter({
                    query: me.lastQuery.query,
                    force: true // overrides the minChars test 
                });
            } else {
                me.doFilter({
                    query: me.inputElement.dom.value
                });
            }
        }
    },
 
    clearValue: function () {
        var me = this,
            inputMask = me.getInputMask();
 
        if (inputMask) {
            // show empty mask and move caret to first editable position 
            // inputMask.showEmptyMask(me, true); 
            // TODO make inputMask work 
        } else {
            me.setValue(null);
            me.setFieldDisplay();
        }
 
        me.syncEmptyState();
    },
 
    /**
     * 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 {Object} query An object containing details about the query to be executed.
     * @param {String} [query.query] The query value to be used to match against the
     * ComboBox's {@link #textField}. If not present, the primary {@link #cfg!textfield}
     * filter is disabled.
     * @param {Boolean} query.force If `true`, causes the query to be executed even if
     * the {@link #cfg!minChars} threshold is not met.
     * @returns {Boolean} `true` if the query resulted in picker expansion.
     */
    doFilter: function (query) {
        var me = this,
            isLocal = me.getQueryMode() === 'local',
            lastQuery = me.lastQuery,
            store = me.getStore() && me._pickerStore,
            filter = me.getPrimaryFilter(),
            filters = store.getFilters(),
            // Decide if, and how we are going to query the store 
            queryPlan = me.beforeFilter(Ext.apply({
                filterGeneration: filter.generation,
                lastQuery: lastQuery || {},
                combo: me,
                cancel: false
            }, query)),
            picker;
 
        // Allow veto. 
        if (store && queryPlan !== false && !queryPlan.cancel) {
            // User can be typing a regex in here, if it's invalid 
            // just swallow the exception and move on 
            if (me.getEnableRegEx()) {
                try {
                    queryPlan.query = new RegExp(queryPlan.query);
                } catch(e) {
                    queryPlan.query = null;
                }
            }
 
            // Update the value. 
            filter.setValue(queryPlan.query);
 
            // If we are not caching previous queries, or the filter has changed in any way 
            // (value, or matching criteria etc), or the force flag is different, then we 
            // must re-filter. Otherwise, we just drop through to expand. 
            if (!me.getQueryCaching() || filter.generation !== lastQuery.filterGeneration ||
                    query.force) {
                // If there is a query string to filter against, enable the filter now and prime its value 
                // Filtering will occur when the store's FilterCollection broadcasts its endUpdate signal. 
                if (Ext.isEmpty(queryPlan.query)) {
                    filter.setDisabled(true);
                } else {
                    filter.setDisabled(false);
 
                    // If we are doing remote filtering, set a flag to 
                    // indicate to onStoreLoad that the load is the result of filering. 
                    me.isFiltering = !isLocal;
                }
 
                me.lastQuery = queryPlan;
 
                // Firing the ensUpdate event will cause the store to refilter if local filtering 
                // or reload starting at page 1 if remote. 
                filters.beginUpdate();
                filters.endUpdate();
            }
 
            if (me.getTypeAhead()) {
                me.doTypeAhead(queryPlan);
            }
 
            // If the query result is non-zero length, or there is empty text to display 
            // we must expand. 
            // Note that edge pickers do not have an emptyText config. 
            picker = me.getPicker();
 
            // If it's a remote store, we must expand now, so that the picker will show its loading mask 
            // to show that some activity is happening. 
            if (!isLocal || store.getCount() || (picker.getEmptyText && picker.getEmptyText())) {
                me.expand();
                return true;
            }
            // The result of the filtering is no records and there's no emptyText... 
            // if it's a local query, hide the picker. If it's remote, we do not 
            // know the result size yet, so the loading mask must stay visible. 
            else {
                me.collapse();
            }
        }
 
        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 #doFilter 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 #textField}.
     * If not present, the primary {@link #cfg!textfield} filter is disabled.
     * @param {String} queryPlan.lastQuery The query value used the last time a store query was made.
     * @param {Boolean} queryPlan.force 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.
     *
     */
    beforeFilter: function (queryPlan) {
        var me = this,
            query = queryPlan.query,
            len;
 
        // 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) {
            len = query && query.length;
            // If the minChars threshold has not been met, and we're not forcing a query, cancel the query 
            if (!queryPlan.force && len && len < me._getMinChars()) {
                queryPlan.cancel = true;
            }
        }
 
        return queryPlan;
    },
 
    completeEdit: function() {
        var me = this,
            inputValue = me.getInputValue();
 
        // Don't want to callParent here, we need custom handling 
 
        if (me.doFilterTask) {
            me.doFilterTask.cancel();
        }
 
        if (inputValue) {
            me.syncMode = 'input';
            me.syncValue();
        }
 
        if (me.getTypeAhead()) {
            me.select(inputValue ? inputValue.length : 0);
        }
    },
 
    /**
     * @private
     * Called when the internal {@link #store}'s data has changed.
     * This may be in response to filtering. At this point, if we are expanded, we must
     * ensure that the List's NavigationModel is either focused on the first item that is
     * in the selection, or if no selections, on tgeh first item.
     */
    onStoreDataChanged: function (store) {
        this.callParent([store]);
 
        //\\ TODO: If expanded, navigate into the List if we are configured to do so. 
        //\\ TODO: What is that config? 
        //\\ TODO: What if any record in selections has been removed from the unfiltered source collection? 
    },
 
    /**
     * @private
     * Called when local filtering is being used.
     * Only effective when NOT actively using the primary filter
     */
    onStoreFilterChange: function() {
        var me = this,
            store = me.getStore(),
            selection = me.getSelection() || null,
            toRemove = [];
 
        // If we are not in the middle of doing a primary filter, then prune no longer 
        // present value(s) 
        if (selection && !me.destroying && store && store.isLoaded() &&
                me.getPrimaryFilter().getDisabled()) {
            if (!selection.isEntered && !store.contains(selection)) {
                toRemove.push(selection);
            }
 
            // Prune out values which are no longer in the source store 
            if (toRemove.length) {
                this.getValueCollection().remove(toRemove);
            }
        }
    },
 
    //\\ TODO: Decide on an EdgePicker 
 
    onListSelect: Ext.emptyFn,
 
    applyQueryDelay: function (queryDelay) {
        if (queryDelay === true) {
            queryDelay = this.getQueryMode() === 'local' ? 10 : 500;
        }
 
        return queryDelay;
    },
 
    applyPrimaryFilter: function (filter, oldFilter) {
        var me = this,
            store = me.getStore() && me._pickerStore,
            isInstance = filter && filter.isFilter;
 
        // If we have to remove the oldFilter, or reconfigure it... 
        if (store && oldFilter) {
            // We are replacing the old filter 
            if (filter) {
                if (isInstance) {
                    store.removeFilter(oldFilter, true);
                } else {
                    oldFilter.setConfig(filter);
                    return;
                }
            }
            // We are removing the old filter 
            else if (!store.destroyed) {
                store.getFilters().remove(oldFilter);
            }
        }
 
        // There is a new filter 
        if (filter) {
            if (filter === true) {
                filter = {
                    id: me.id + '-primary-filter',
                    anyMatch: me.getAnyMatch(),
                    caseSensitive: me.getCaseSensitive(),
                    root: 'data',
                    property: me.getDisplayField(),
                    value: me.inputElement.dom.value,
                    disabled: true
                };
            }
 
            // Ensure it's promoted to an instance 
            if (!filter.isFilter) {
                filter = new Ext.util.Filter(filter);
            }
 
            // Primary filter serialized as simple value by default 
            filter.serialize = function () {
                return me.serializePrimaryFilter(this);
            };
 
            // Add filter if we have a store already 
            if (store) {
                store.addFilter(filter, true);
            }
        }
 
        return filter;
    },
 
    updateOptions: function(options, oldOptions) {
        if (options) {
            this.setQueryMode('local');
        }
        this.callParent([options, oldOptions]);
    },
 
    updatePicker: function(picker, oldPicker) {
        if (picker) {
            picker.getSelectable().ignoredFilter = this.getPrimaryFilter();
        }
 
        this.callParent([picker, oldPicker]);
    },
 
    updateStore: function(store, oldStore) {
        var me = this,
            isRemote = me.getQueryMode() === 'remote',
            primaryFilter,
            proxy, oldFilters;
 
        // Tweak the proxy to encode the primaryFilter's parameter as documented for ComboBox 
        if (isRemote) {
            store.setRemoteFilter(true);
 
            // Set the Proxy's filterParam name to our queryParam name if it is a ServerProxy which encodes params 
            proxy = store.getProxy();
            if (proxy.setFilterParam) {
                proxy.setFilterParam(me.getQueryParam());
            }
        }
 
        // Superclass ensures that there's a ChainedStore in the _pickerStore 
        // property if we are going to be adding our own local filters to it. 
        me.callParent([store, oldStore]);
 
        // The primaryFilter (Our typing filter) will add itself to the _pickerStore. 
        primaryFilter = me.getPrimaryFilter();
 
        if (primaryFilter) {
            // Remove primaryFilter from the outgoing store. 
            // It will only be there if the outgoing store was remoteFilter. 
            if (oldStore && !oldStore.destroyed) {
                oldFilters = oldStore.getFilters();
 
                // Filter collection might not exist. 
                oldFilters && oldFilters.remove(primaryFilter);
            }
 
            // Add the primary filter to the (possibly new, but possibly just 
            // re-attached to the incoming store) pickerStore. 
            // See Ext.field.Select#updateStore, and its call to updatePickerStore. 
            me._pickerStore.addFilter(primaryFilter, true);
        }
 
        // If we are doing remote filtering, then mutating the store's filters should not 
        // result in a re-evaluation of whether the current value(s) is/are still present in the store. 
        // For local filtering, if a new filter is added (must not be done while typing is taking place) 
        // then the current selection is pruned to remove no longer valid entries. 
        if (me.getQueryMode() === 'local') {
            store.on({
                filterchange: 'onStoreFilterChange',
                scope: me
            });
        }
    },
 
    /**
     * @template
     * A method - that may be overridden in a ComboBox subclass - which serializes the primary filter
     * which is the filter that passes the typed value for transmission to the server in the {@link #cfg!queryParam}.
     *
     * The provided implementation simply passes the filter's textual value as the {@link #cfg!queryParam} value.
     *
     * @param {Ext.util.Filter} filter The {@link #cfg!primaryFilter} of this ComboBox which
     * encapsulates the typed value and the matching rules.
     * @return {String/Object} A value which, when encoded as an HTML parameter, your server will understand/
     */
    serializePrimaryFilter: function(filter) {
        return filter.getValue();
    },
 
    doDestroy: function() {
        var me = this;
 
        me.setPrimaryFilter(null);
 
        if (me.typeAheadTask) {
            me.typeAheadTask = me.typeAheadTask.cancel();
        }
 
        me.callParent();
    },
 
    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.lastQuery.query || !queryPlan.query || 
                queryPlan.query.length > queryPlan.lastQuery.query.length) ||
            !Ext.String.startsWith(queryPlan.lastQuery.query, queryPlan.query)
        ) {
            me.typeAheadTask.delay(me.getTypeAheadDelay());
        }
    },
 
    onTypeAhead: function () {
        var me = this,
            displayField = me.getDisplayField(),
            inputEl = me.inputElement.dom,
            rawValue = inputEl.value,
            store = me.getStore(),
            record = store.findRecord(displayField, rawValue),
            newValue, len, selStart;
 
        if (record) {
            newValue = record.get(displayField);
            len = newValue.length;
            selStart = rawValue.length;
 
            if (selStart !== 0 && selStart !== len) {
                inputEl.value = me._inputValue = newValue;
 
                me.select(selStart, len);
            }
        }
    },
 
    privates: {
        _getMinChars: function() {
            var result = this.getMinChars();
 
            if (result == null) {
                result = this.getQueryMode() === 'remote' ? 4 : 0;
            }
            return result;
        },
 
        setFieldDisplay: function (selection) {
            var me = this,
                inputValue;
 
            me.callParent([ selection ]);
 
            if (me.getTypeAhead()) {
                inputValue = me.getInputValue();
 
                me.select(inputValue ? inputValue.length : 0);
            }
        }
    }
});