/**
 * `tagfield` provides a combobox that removes the hassle of dealing with long and unruly select
 * options. The selected list is visually maintained in the value display area instead of
 * within the picker itself. Users may easily add or remove `tags` from the
 * display value area.
 *
 *    @example
 *    var shows = Ext.create('Ext.data.Store', {
 *        fields: ['id','show'],
 *        data: [
 *            {id: 0, show: 'Battlestar Galactica'},
 *            {id: 1, show: 'Doctor Who'},
 *            {id: 2, show: 'Farscape'},
 *            {id: 3, show: 'Firefly'},
 *            {id: 4, show: 'Star Trek'},
 *            {id: 5, show: 'Star Wars: Christmas Special'}
 *        ]
 *    });
 *
 *    Ext.create('Ext.form.Panel', {
 *        renderTo: Ext.getBody(),
 *        title: 'Sci-Fi Television',
 *        height: 200,
 *        width: 500,
 *        items: [{
 *            xtype: 'tagfield',
 *            fieldLabel: 'Select a Show',
 *            store: shows,
 *            displayField: 'show',
 *            valueField: 'id',
 *            queryMode: 'local',
 *            filterPickList: true
 *        }]
 *    });
 *
 * ### History
 *
 * Inspired by the [SuperBoxSelect component for ExtJS 3](http://technomedia.co.uk/SuperBoxSelect/examples3.html),
 * which in turn was inspired by the [BoxSelect component for ExtJS 2](http://efattal.fr/en/extjs/extuxboxselect/).
 *
 * Various contributions and suggestions made by many members of the ExtJS community which can be seen
 * in the [user extension forum post](http://www.sencha.com/forum/showthread.php?134751-Ext.ux.form.field.BoxSelect).
 *
 * By: kvee_iv http://www.sencha.com/forum/member.php?29437-kveeiv
 */
Ext.define('Ext.form.field.Tag', {
    extend:'Ext.form.field.ComboBox',
    requires: [
        'Ext.selection.Model',
        'Ext.data.Store'
    ],
 
    xtype: 'tagfield',
 
    // 
    // Begin configuration options related to the underlying store 
    // 
    
    /**
     * @cfg {String} valueParam 
     * The name of the parameter used to load unknown records into the store. If left unspecified, {@link #valueField}
     * will be used.
     */
 
    // 
    // End of configuration options related to the underlying store 
    // 
 
 
 
    // 
    // Begin configuration options related to selected values 
    // 
 
    /**
     * @cfg {Boolean}
     * 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 using the template
     * defined by {@link #labelTpl}.
     *
 
     */
    multiSelect: true,
 
    /**
     * @cfg {String/Ext.XTemplate} labelTpl
     * The [XTemplate](http://docs.sencha.com/ext-js/4-1/#!/api/Ext.XTemplate) to use for the inner
     * markup of the labelled items. Defaults to the configured {@link #displayField}
     */
 
    /**
     * @cfg
     * @inheritdoc
     *
     * When {@link #forceSelection} is `false`, new records can be created by the user as they
     * are typed. These records are **not** added to the combo's store. This creation
     * is triggered by typing the configured 'delimiter', and can be further configured using the
     * {@link #createNewOnEnter} and {@link #createNewOnBlur} configuration options.
     *
     * This functionality is primarily useful for things such as an email address.
     */
    forceSelection: true,
 
    /**
     * @cfg {Boolean}
     * Has no effect if {@link #forceSelection} is `true`.
     *
     * With {@link #createNewOnEnter} set to `true`, the creation described in
     * {@link #forceSelection} will also be triggered by the 'enter' key.
     */
    createNewOnEnter: false,
 
    /**
     * @cfg {Boolean}
     * Has no effect if {@link #forceSelection} is `true`.
     *
     * With {@link #createNewOnBlur} set to `true`, the creation described in
     * {@link #forceSelection} will also be triggered when the field loses focus.
     *
     * Please note that this behavior is also affected by the configuration options
     * {@link #autoSelect} and {@link #selectOnTab}. If those are true and an existing
     * item would have been selected as a result, the partial text the user has entered will
     * be discarded and the existing item will be added to the selection.
     */
    createNewOnBlur: false,
 
    /**
     * @cfg {Boolean}
     * Has no effect if {@link #multiSelect} is `false`.
     *
     * Controls the formatting of the form submit value of the field as returned by {@link #getSubmitValue}
     *
     * - `true` for the field value to submit as a json encoded array in a single GET/POST variable
     * - `false` for the field to submit as an array of GET/POST variables
     */
    encodeSubmitValue: false,
 
    // 
    // End of configuration options related to selected values 
    // 
 
 
 
    // 
    // Configuration options related to pick list behavior 
    // 
 
    /**
     * @cfg {Boolean}
     * `true` to activate the trigger when clicking in empty space in the field. Note that the
     * subsequent behavior of this is controlled by the field's {@link #triggerAction}.
     * This behavior is similar to that of a basic ComboBox with {@link #editable} `false`.
     */
    triggerOnClick: true,
 
    /**
     * @cfg {Boolean}
     * - `true` to have each selected value fill to the width of the form field
     * - `false to have each selected value size to its displayed contents
     */
    stacked: false,
 
    /**
     * @cfg {Boolean}
     * Has no effect if {@link #multiSelect} is `false`
     *
     * `true` to keep the pick list expanded after each selection from the pick list
     * `false` to automatically collapse the pick list after a selection is made
     */
    pinList: true,
 
    /**
     * @cfg {Boolean}
     * True to hide the currently selected values from the drop down list. These items are hidden via
     * css to maintain simplicity in store and filter management.
     *
     * - `true` to hide currently selected values from the drop down pick list
     * - `false` to keep the item in the pick list as a selected item
     */
    filterPickList: false,
 
    // 
    // End of configuration options related to pick list behavior 
    // 
 
 
 
    // 
    // Configuration options related to text field behavior 
    // 
 
    /**
     * @cfg
     * @inheritdoc
     */
    selectOnFocus: true,
 
    /**
     * @cfg {Boolean}
     *
     * `true` if this field should automatically grow and shrink vertically to its content.
     * Note that this overrides the natural trigger grow functionality, which is used to size
     * the field horizontally.
     */
    grow: true,
 
    /**
     * @cfg {Number/Boolean}
     * Has no effect if {@link #grow} is `false`
     *
     * The minimum height to allow when {@link #grow} is `true`, or `false` to allow for
     * natural vertical growth based on the current selected values. See also {@link #growMax}.
     */
    growMin: false,
 
    /**
     * @cfg {Number/Boolean}
     * Has no effect if {@link #grow} is `false`
     *
     * The maximum height to allow when {@link #grow} is `true`, or `false` to allow for
     * natural vertical growth based on the current selected values. See also {@link #growMin}.
     */
    growMax: false,
 
    /**
     * @cfg growAppend
     * @hide
     * Currently unsupported since this is used for horizontal growth and this component
     * only supports vertical growth.
     */
 
    /**
     * @cfg growToLongestValue
     * @hide
     * Currently unsupported since this is used for horizontal growth and this component
     * only supports vertical growth.
     */
 
    // 
    // End of configuration options related to text field behavior 
    // 
 
 
    // 
    // Event signatures 
    // 
 
    /**
     * @event autosize
     * Fires when the **{@link #autoSize}** function is triggered and the field is resized according to the
     * {@link #grow}/{@link #growMin}/{@link #growMax} configs as a result. This event provides a hook for the
     * developer to apply additional logic at runtime to resize the field if needed.
     * @param {Ext.form.field.Tag} this This field
     * @param {Number} height The new field height
     */
 
    // 
    // End of event signatures 
    // 
 
 
 
    // 
    // Configuration options that will break things if messed with 
    // 
 
    /**
     * @private
     */
    fieldSubTpl: [
        '<div id="{cmpId}-listWrapper" data-ref="listWrapper" class="' + Ext.baseCSSPrefix + 'tagfield {fieldCls} {typeCls} {typeCls}-{ui}">',
            '<ul id="{cmpId}-itemList" data-ref="itemList" class="' + Ext.baseCSSPrefix + 'tagfield-list">',
                '<li id="{cmpId}-inputElCt" data-ref="inputElCt" class="' + Ext.baseCSSPrefix + 'tagfield-input">',
                    '<div id="{cmpId}-emptyEl" data-ref="emptyEl" class="{emptyCls}">{emptyText}</div>',
                    '<input id="{cmpId}-inputEl" data-ref="inputEl" type="{type}" ',
                    '<tpl if="name">name="{name}" </tpl>',
                    '<tpl if="value"> value="{[Ext.util.Format.htmlEncode(values.value)]}"</tpl>',
                    '<tpl if="size">size="{size}" </tpl>',
                    '<tpl if="tabIdx != null">tabindex="{tabIdx}" </tpl>',
                    '<tpl if="disabled"> disabled="disabled"</tpl>',
                    'class="' + Ext.baseCSSPrefix + 'tagfield-input-field {inputElCls}" autocomplete="off">',
                '</li>',
            '</ul>',
        '</div>',
        {
            disableFormats: true
        }
    ],
 
    /**
     * @private
     */
    childEls: [ 'listWrapper', 'itemList', 'inputEl', 'inputElCt', 'emptyEl' ],
 
    /**
     * @private
     */
    emptyInputCls: Ext.baseCSSPrefix + 'tagfield-emptyinput',
 
    /**
     * @inheritdoc
     *
     * Initialize additional settings and enable simultaneous typeAhead and multiSelect support
     * @protected
     */
    initComponent: function() {
        var me = this,
            typeAhead = me.typeAhead,
            delimiter = me.delimiter;
 
        // <debug> 
        if (typeAhead && !me.editable) {
            Ext.Error.raise('If typeAhead is enabled the combo must be editable: true -- please change one of those settings.');
        }
        // </debug> 
 
        me.typeAhead = false;
 
        // Refresh happens upon select. 
        me.listConfig = Ext.apply({
            refreshSelmodelOnRefresh: false
        }, me.listConfig);
 
        // Create the selModel before calling parent, we need it to be available 
        // when we bind the store. 
        me.selectionModel = new Ext.selection.Model({
            mode: 'MULTI',
            lastFocused: null,
            onSelectChange: function(record, isSelected, suppressEvent, commitFn) {
                commitFn();
            }
        });
 
        me.callParent();
 
        me.typeAhead = typeAhead;
 
        if (delimiter && me.multiSelect) {
            me.delimiterRegexp = new RegExp(Ext.String.escapeRegex(delimiter));
        }
    },
 
    /**
     * Register events for management controls of labelled items
     * @protected
     */
    initEvents: function() {
        var me = this;
 
        me.callParent(arguments);
 
        if (!me.enableKeyEvents) {
            me.inputEl.on('keydown', me.onKeyDown, me);
        }
        me.inputEl.on('paste', me.onPaste, me);
        me.listWrapper.on('click', me.onItemListClick, me);
 
        // Relay these selection events passing the field instead of exposing the underlying selection model 
        me.selectionModel.on({
            scope: me,
            selectionchange: me.onSelectionChange,
            focuschange: me.onFocusChange
        });
    },
 
    /**
     * @inheritdoc
     *
     * Create a store for the records of our current value based on the main store's model
     * @protected
     */
    onBindStore: function(store) {
        var me = this,
            filter = me.selectedFilter,
            valueStore;
 
        if (store) {
            me.valueStore = valueStore = new Ext.data.Store({
                model: store.getModel()
            });
            valueStore.on('datachanged', me.onValueStoreChange, me);
            me.selectionModel.bindStore(valueStore);
 
            // Filter to hide from view, items which are already selected. 
            // Only enabled if filterPickList is false. 
            if (me.filterPickList) {
                if (!filter) {
                    me.selectedFilter =  filter = new Ext.util.Filter({
                        filterFn: function(rec) {
                            return !me.valueStore.contains(rec);
                        }
                    });
                }
                store.addFilter(filter);
            }
        }
    },
 
    /**
     * @inheritdoc
     *
     * Remove the selected value store and associated listeners
     * @protected
     */
    onUnbindStore: function(store) {
        var me = this,
            valueStore = me.valueStore;
 
        if (valueStore) {
            valueStore.destroy();
            me.valueStore = null;
        }
        if (me.filterPickList) {
            store.removeFilter(me.selectedFilter);
        }
        me.callParent(arguments);
    },
 
    onValueStoreChange: function() {
        // If they want to remove from view items which are already selected, apply store filters. 
        // We have a "selectedFilter" created which filters out selected items. 
        // This instance restores to view items removed from the value list 
        var me = this,
            filters = me.store.getFilters();
 
        if (me.filterPickList) {
            // Adding an existing item will trigger the filters to refresh 
            filters.add(me.selectedFilter);
        }
        me.applyMultiselectItemMarkup();
    },
 
    onSelectionChange: function(selModel, selectedRecs) {
        this.applyMultiselectItemMarkup();
        this.fireEvent('valueselectionchange', this, selectedRecs);
    },
 
    onFocusChange: function(selectionModel, oldFocused, newFocused) {
        this.fireEvent('valuefocuschange', this, oldFocused, newFocused);
    },
 
    /**
     * @inheritdoc
     *
     * Add refresh tracking to the picker for selection management
     * @protected
     */
    createPicker: function() {
        var me = this,
            picker = me.callParent(arguments);
 
        me.mon(picker, 'beforerefresh', me.onBeforeListRefresh, me);
        return picker;
    },
 
    /**
     * @inheritdoc
     *
     * Clean up selected values management controls
     * @protected
     */
    onDestroy: function() {
        this.selectionModel = Ext.destroy(this.selectionModel);
 
        // This will unbind the store, which will destroy the valueStore 
        this.callParent(arguments);
    },
 
    /**
     * Add empty text support to initial render.
     * @protected
     */
    getSubTplData: function(fieldData) {
        var me = this,
            data = me.callParent(arguments),
            emptyText = me.emptyText,
            emptyInputCls = me.emptyInputCls,
            isEmpty = emptyText && data.value.length < 1;
 
        data.value = '';
        data.emptyText = isEmpty ? emptyText : '';
        data.emptyCls = isEmpty ? me.emptyCls : emptyInputCls;
        data.inputElCls = isEmpty ? emptyInputCls : '';
 
        return data;
    },
 
    /**
     * @inheritdoc
     *
     * Overridden to avoid use of placeholder, as our main input field is often empty
     * @protected
     */
    afterRender: function() {
        var me = this,
            listWrapper = me.listWrapper,
            itemList = me.itemList,
            growMin = me.growMin,
            growMax = me.growMax;
 
        if (Ext.supports.Placeholder && me.inputEl && me.emptyText) {
            delete me.inputEl.dom.placeholder;
        }
 
        me.bodyEl.applyStyles('vertical-align:top');
 
        if (me.grow) {
            if (Ext.isNumber(growMin) && growMin > 0) {
                listWrapper.applyStyles('min-height:' + growMin + 'px');
            }
            if (Ext.isNumber(growMax) && growMax > 0) {
                listWrapper.applyStyles('max-height:' + growMax + 'px');
            }
        }
 
        if (me.stacked === true) {
            itemList.addCls(Ext.baseCSSPrefix + 'tagfield-stacked');
        }
 
        if (!me.multiSelect) {
            itemList.addCls(Ext.baseCSSPrefix + 'tagfield-singleselect');
        }
 
        me.applyMultiselectItemMarkup();
 
        me.callParent(arguments);
    },
 
    /**
     * Overridden to search entire unfiltered store since already selected values
     * can span across multiple store page loads and other filtering. Overlaps
     * some with {@link #isFilteredRecord}, but findRecord is used by the base component
     * for various logic so this logic is applied here as well.
     * @protected
     */
    findRecord: function(field, value) {
        var store = this.store,
            matches;
 
        if (store) {
            matches = store.queryBy(function(rec) {
                return rec.isEqual(rec.get(field), value);
            });
        }
 
        return matches || false;
    },
 
    /**
     * Overridden to map previously selected records to the "new" versions of the records
     * based on value field, if they are part of the new store load
     * @protected
     */
    onLoad: function() {
        var me = this,
            valueField = me.valueField,
            valueStore = me.valueStore,
            changed = false;
 
        if (valueStore) {
            if (!Ext.isEmpty(me.value) && (valueStore.getCount() === 0)) {
                me.setValue(me.value, false, true);
            }
 
            valueStore.suspendEvents();
            valueStore.each(function(rec) {
                var r = me.findRecord(valueField, rec.get(valueField)),
                i = r ? valueStore.indexOf(rec) : -1;
                if (>= 0) {
                    valueStore.removeAt(i);
                    valueStore.insert(i, r);
                    changed = true;
                }
            });
            valueStore.resumeEvents();
            if (changed) {
                me.onValueStoreChange();
            }
        }
 
        me.callParent(arguments);
    },
 
    /**
     * Used to determine if a record is filtered out of the current store's data set,
     * for determining if a currently selected value should be retained.
     *
     * Slightly complicated logic. A record is considered filtered and should be retained if:
     *
     * - It is not in the combo store and the store has no filter or it is in the filtered data set
     *   (Happens when our selected value is just part of a different load, page or query)
     * - It is not in the combo store and forceSelection is false and it is in the value store
     *   (Happens when our selected value was created manually)
     *
     * @private
     */
    isFilteredRecord: function(record) {
        var me = this,
            store = me.store,
            valueField = me.valueField,
            filtered = false,
            storeRecord;
 
        storeRecord = store.findExact(valueField, record.get(valueField));
 
        filtered = ((storeRecord === -1) && (!store.snapshot || (me.findRecord(valueField, record.get(valueField)) !== false)));
 
        filtered = filtered || (!filtered && (storeRecord === -1) && (me.forceSelection !== true) &&
            (me.valueStore.findExact(valueField, record.get(valueField)) >= 0));
 
        return filtered;
    },
 
    /**
     * @inheritdoc
     *
     * Overridden to allow for continued querying with multiSelect selections already made
     * @protected
     */
    doRawQuery: function() {
        var me = this,
            rawValue = me.inputEl.dom.value;
 
        if (me.multiSelect) {
            rawValue = rawValue.split(me.delimiter).pop();
        }
 
        me.doQuery(rawValue, false, true);
    },
 
    /**
     * When the picker is refreshing, we should ignore selection changes. Otherwise
     * the value of our field will be changing just because our view of the choices is.
     * @protected
     */
    onBeforeListRefresh: function() {
        this.ignoreSelection++;
    },
 
    /**
     * When the picker is refreshing, we should ignore selection changes. Otherwise
     * the value of our field will be changing just because our view of the choices is.
     * @protected
     */
    onListRefresh: function() {
        this.callParent(arguments);
        if (this.ignoreSelection > 0) {
            --this.ignoreSelection;
        }
    },
 
    /**
     * Overridden to preserve current labelled items when list is filtered/paged/loaded
     * and does not include our current value. See {@link #isFilteredRecord}
     * @private
     */
    onListSelectionChange: function(list, selectedRecords) {
        var me = this,
            valueStore = me.valueStore,
            mergedRecords = [],
            i;
 
        // Only react to selection if it is not called from setValue, and if our list is 
        // expanded (ignores changes to the selection model triggered elsewhere) 
        if ((me.ignoreSelection <= 0) && me.isExpanded) {
            // Pull forward records that were already selected or are now filtered out of the store 
            valueStore.each(function(rec) {
                if (Ext.Array.contains(selectedRecords, rec) || me.isFilteredRecord(rec)) {
                    mergedRecords.push(rec);
                }
            });
            mergedRecords = Ext.Array.merge(mergedRecords, selectedRecords);
 
            i = Ext.Array.intersect(mergedRecords, valueStore.getRange()).length;
            if ((!= mergedRecords.length) || (!= me.valueStore.getCount())) {
                me.setValue(mergedRecords, false);
                if (!me.multiSelect || !me.pinList) {
                    Ext.defer(me.collapse, 1, me);
                }
                if (valueStore.getCount() > 0) {
                    me.fireEvent('select', me, valueStore.getRange());
                }
            }
            if (!me.pinList) {
                me.inputEl.dom.value = '';
            }
            // If not using touch interactions, focus the input 
            if (!Ext.supports.TouchEvents) {
                me.inputEl.focus();
                if (me.selectOnFocus) {
                    me.inputEl.dom.select();
                }
            }
        }
    },
 
    /**
     * Overridden to use valueStore instead of valueModels, for inclusion of
     * filtered records. See {@link #isFilteredRecord}
     * @private
     */
    syncSelection: function() {
        var me = this,
            picker = me.picker,
            valueField = me.valueField,
            pickStore, selection, selModel;
 
        if (picker) {
            pickStore = picker.store;
 
            // From the value, find the Models that are in the store's current data 
            selection = [];
            if (me.valueStore) {
                me.valueStore.each(function(rec) {
                    var i = pickStore.findExact(valueField, rec.get(valueField));
                    if (>= 0) {
                        selection.push(pickStore.getAt(i));
                    }
                });
            }
 
            // Update the selection to match 
            me.ignoreSelection++;
            selModel = picker.getSelectionModel();
            selModel.deselectAll();
            if (selection.length > 0) {
                selModel.select(selection);
            }
            if (me.ignoreSelection > 0) {
                --me.ignoreSelection;
            }
        }
    },
 
    /**
     * Get the current cursor position in the input field, for key-based navigation
     * @private
     */
    getCursorPosition: function() {
        var cursorPos;
 
        if (document.selection) {
            cursorPos = document.selection.createRange();
            cursorPos.collapse(true);
            cursorPos.moveStart('character', -this.inputEl.dom.value.length);
            cursorPos = cursorPos.text.length;
        } else {
            cursorPos = this.inputEl.dom.selectionStart;
        }
        return cursorPos;
    },
 
    /**
     * Check to see if the input field has selected text, for key-based navigation
     * @private
     */
    hasSelectedText: function() {
        var inputEl = this.inputEl.dom,
            sel, range;
 
        if (document.selection) {
            sel = document.selection;
            range = sel.createRange();
            return (range.parentElement() === inputEl);
        } else {
            return inputEl.selectionStart !== inputEl.selectionEnd;
        }
    },
 
    /**
     * Handles keyDown processing of key-based selection of labelled items.
     * Supported keyboard controls:
     *
     * - If pick list is expanded
     *
     *     - `CTRL-A` will select all the items in the pick list
     *
     * - If the cursor is at the beginning of the input field and there are values present
     *
     *     - `CTRL-A` will highlight all the currently selected values
     *     - `BACKSPACE` and `DELETE` will remove any currently highlighted selected values
     *     - `RIGHT` and `LEFT` will move the current highlight in the appropriate direction
     *     - `SHIFT-RIGHT` and `SHIFT-LEFT` will add to the current highlight in the appropriate direction
     *
     * @protected
     */
    onKeyDown: function(e) {
        var me = this,
            key = e.getKey(),
            inputEl = me.inputEl,
            rawValue = inputEl.dom.value,
            valueStore = me.valueStore,
            selModel = me.selectionModel,
            stopEvent = false,
            lastSelectionIndex;
 
        if (me.readOnly || me.disabled || !me.editable) {
            return;
        }
 
        if (me.isExpanded && key === e.A && e.ctrlKey) {
            // CTRL-A when picker is expanded - add all items in current picker store page to current value 
            me.select(me.getStore().getRange());
            selModel.setLastFocused(null);
            selModel.deselectAll();
            me.collapse();
            inputEl.focus();
            stopEvent = true;
        }
        // We have some values and (no input text or cursor is at left of all text) 
        else if ((valueStore.getCount() > 0) &&
                ((rawValue === '') || ((me.getCursorPosition() === 0) && !me.hasSelectedText()))) {
            // Keyboard navigation of current values 
            lastSelectionIndex = (selModel.getCount() > 0) ? valueStore.indexOf(selModel.getLastSelected() || selModel.getLastFocused()) : -1;
 
            // Delete token 
            if (key === e.BACKSPACE || key === e.DELETE) {
                if (lastSelectionIndex > -1) {
                    if (selModel.getCount() > 1) {
                        lastSelectionIndex = -1;
                    }
                    valueStore.remove(selModel.getSelection());
                } else {
                    valueStore.remove(valueStore.last());
                }
                selModel.clearSelections();
                me.setValue(valueStore.getRange());
                if (lastSelectionIndex > 0) {
                    selModel.select(lastSelectionIndex - 1);
                }
                stopEvent = true;
            }
            // Navigate and select tokens 
            else if (key === e.RIGHT || key === e.LEFT) {
                if (lastSelectionIndex === -1 && key === e.LEFT) {
                    selModel.select(valueStore.last());
                    stopEvent = true;
                } else if (lastSelectionIndex > -1) {
                    if (key === e.RIGHT) {
                        if (lastSelectionIndex < (valueStore.getCount() - 1)) {
                            selModel.select(lastSelectionIndex + 1, e.shiftKey);
                            stopEvent = true;
                        } else if (!e.shiftKey) {
                            selModel.setLastFocused(null);
                            selModel.deselectAll();
                            stopEvent = true;
                        }
                    } else if (key === e.LEFT && (lastSelectionIndex > 0)) {
                        selModel.select(lastSelectionIndex - 1, e.shiftKey);
                        stopEvent = true;
                    }
                }
            }
            // Select all tokens 
            else if (key === e.A && e.ctrlKey) {
                selModel.selectAll();
                stopEvent = e.A;
            }
            inputEl.focus();
        }
 
        if (stopEvent) {
            me.preventKeyUpEvent = stopEvent;
            e.stopEvent();
            return;
        }
 
        // Prevent key up processing for enter if it is being handled by the picker 
        if (me.isExpanded && key === e.ENTER && me.picker.highlightedItem) {
            me.preventKeyUpEvent = true;
        }
 
        if (me.enableKeyEvents) {
            me.callParent(arguments);
        }
 
        if (!e.isSpecialKey() && !e.hasModifier()) {
            selModel.setLastFocused(null);
            selModel.deselectAll();
            inputEl.focus();
        }
    },
 
    /**
     * Handles auto-selection and creation of labelled items based on this field's
     * delimiter, as well as the keyUp processing of key-based selection of labelled items.
     * @protected
     */
    onKeyUp: function(e, t) {
        var me = this,
            inputEl = me.inputEl,
            rawValue = inputEl.dom.value,
            preventKeyUpEvent = me.preventKeyUpEvent;
 
        if (me.preventKeyUpEvent) {
            e.stopEvent();
            if (preventKeyUpEvent === true || e.getKey() === preventKeyUpEvent) {
                delete me.preventKeyUpEvent;
            }
            return;
        }
 
        if (me.multiSelect && me.delimiterRegexp && me.delimiterRegexp.test(rawValue) ||
                ((me.createNewOnEnter === true) && e.getKey() === e.ENTER)) {
            rawValue = Ext.Array.clean(rawValue.split(me.delimiterRegexp));
            inputEl.dom.value = '';
            me.setValue(me.valueStore.getRange().concat(rawValue));
            inputEl.focus();
        }
 
        me.callParent([e,t]);
    },
 
    /**
     * Handles auto-selection of labelled items based on this field's delimiter when pasting
     * a list of values in to the field (e.g., for email addresses)
     * @protected
     */
    onPaste: function(e) {
        var me = this,
            inputEl = me.inputEl,
            rawValue = inputEl.dom.value,
            clipboard = (&& e.browserEvent && e.browserEvent.clipboardData) ? e.browserEvent.clipboardData : false;
 
        if (me.multiSelect && me.delimiterRegexp && me.delimiterRegexp.test(rawValue)) {
            if (clipboard && clipboard.getData) {
                if (/text\/plain/.test(clipboard.types)) {
                    rawValue = clipboard.getData('text/plain');
                } else if (/text\/html/.test(clipboard.types)) {
                    rawValue = clipboard.getData('text/html');
                }
            }
 
            rawValue = Ext.Array.clean(rawValue.split(me.delimiterRegexp));
            inputEl.dom.value = '';
            me.setValue(me.valueStore.getRange().concat(rawValue));
            inputEl.focus();
        }
    },
 
    /**
     * Overridden to get and set the DOM value directly for type-ahead suggestion (bypassing get/setRawValue)
     * @protected
     */
    onTypeAhead: function() {
        var me = this,
            displayField = me.displayField,
            inputElDom = me.inputEl.dom,
            boundList = me.getPicker(),
            record = me.store.findRecord(displayField, inputElDom.value),
            newValue, len, selStart;
 
        if (record) {
            newValue = record.get(displayField);
            len = newValue.length;
            selStart = inputElDom.value.length;
            boundList.highlightItem(boundList.getNode(record));
            if (selStart !== 0 && selStart !== len) {
                inputElDom.value = newValue;
                me.selectText(selStart, newValue.length);
            }
        }
    },
 
    /**
     * Delegation control for selecting and removing labelled items or triggering list collapse/expansion
     * @protected
     */
    onItemListClick: function(evt) {
        var me = this,
            selectionModel = me.selectionModel,
            itemEl = evt.getTarget('.' + Ext.baseCSSPrefix + 'tagfield-item'),
            closeEl = itemEl ? evt.getTarget('.' + Ext.baseCSSPrefix + 'tagfield-item-close') : false;
 
        if (me.readOnly || me.disabled) {
            return;
        }
 
        evt.stopPropagation();
 
        if (itemEl) {
            if (closeEl) {
                me.removeByListItemNode(itemEl);
                if (me.valueStore.getCount() > 0) {
                    me.fireEvent('select', me, me.valueStore.getRange());
                }
            } else {
                me.toggleSelectionByListItemNode(itemEl, evt.shiftKey);
            }
            // If not using touch interactions, focus the input 
            if (!Ext.supports.TouchEvents) {
                me.inputEl.focus();
            }
        } else {
            if (selectionModel.getCount() > 0) {
                selectionModel.setLastFocused(null);
                selectionModel.deselectAll();
            }
            if (me.triggerOnClick) {
                me.onTriggerClick();
            }
        }
    },
 
    /**
     * Build the markup for the labelled items. Template must be built on demand due to ComboBox initComponent
     * lifecycle for the creation of on-demand stores (to account for automatic valueField/displayField setting)
     * @private
     */
    getMultiSelectItemMarkup: function() {
        var me = this;
 
        if (!me.multiSelectItemTpl) {
            if (!me.labelTpl) {
                me.labelTpl = '{' + me.displayField + '}';
            }
            me.labelTpl = me.getTpl('labelTpl');
 
            me.multiSelectItemTpl = new Ext.XTemplate([
                '<tpl for=".">',
                    '<li class="    ' + Ext.baseCSSPrefix + 'tagfield-item ',
                    '<tpl if="this.isSelected(values)">',
                    ' selected',
                    '</tpl>',
                    '{%',
                        'values = values.data;',
                    '%}',
                    '" qtip="{' + me.displayField + '}">' ,
                    '<div class="' + Ext.baseCSSPrefix + 'tagfield-item-text">{[this.getItemLabel(values)]}</div>',
                    '<div class="' + Ext.baseCSSPrefix + 'tagfield-item-close"></div>' ,
                    '</li>' ,
                '</tpl>',
                {
                    isSelected: function(rec) {
                        return me.selectionModel.isSelected(rec);
                    },
                    getItemLabel: function(values) {
                        return me.labelTpl.apply(values);
                    }
                }
            ]);
        }
        if (!me.multiSelectItemTpl.isTemplate) {
            me.multiSelectItemTpl = this.getTpl('multiSelectItemTpl');
        }
 
        return me.multiSelectItemTpl.apply(this.valueStore.getRange());
    },
 
    /**
     * Update the labelled items rendering
     * @private
     */
    applyMultiselectItemMarkup: function() {
        var me = this,
            itemList = me.itemList,
            item;
 
        if (itemList) {
            while ((item = me.inputElCt.prev())) {
                item.destroy();
            }
            me.inputElCt.insertHtml('beforeBegin', me.getMultiSelectItemMarkup());
        }
 
        // Before control returns from this event, align the picker and ensure the input area of 
        // this control is in view. 
        Ext.GlobalEvents.on({
            idle: function() {
                if (me.picker && me.isExpanded) {
                    me.alignPicker();
                }
                if (me.hasFocus && me.inputElCt && me.listWrapper) {
                    me.inputElCt.scrollIntoView(me.listWrapper);
                }
            },
            single: true
        });
    },
 
    /**
     * Returns the record from valueStore for the labelled item node
     */
    getRecordByListItemNode: function(itemEl) {
        var me = this,
            itemIdx = 0,
            searchEl = me.itemList.dom.firstChild;
 
        while (searchEl && searchEl.nextSibling) {
            if (searchEl == itemEl) {
                break;
            }
            itemIdx++;
            searchEl = searchEl.nextSibling;
        }
        itemIdx = (searchEl == itemEl) ? itemIdx : false;
 
        if (itemIdx === false) {
            return false;
        }
 
        return me.valueStore.getAt(itemIdx);
    },
 
    /**
     * Toggle of labelled item selection by node reference
     */
    toggleSelectionByListItemNode: function(itemEl, keepExisting) {
        var me = this,
            rec = me.getRecordByListItemNode(itemEl),
            selModel = me.selectionModel;
 
        if (rec) {
            if (selModel.isSelected(rec)) {
                if (selModel.isFocused(rec)) {
                    selModel.setLastFocused(null);
                }
                selModel.deselect(rec);
            } else {
                selModel.select(rec, keepExisting);
            }
        }
    },
 
    /**
     * Removal of labelled item by node reference
     */
    removeByListItemNode: function(itemEl) {
        var me = this,
            rec = me.getRecordByListItemNode(itemEl);
 
        if (rec) {
            me.valueStore.remove(rec);
            me.setValue(me.valueStore.getRange());
        }
    },
 
    /**
     * @inheritdoc
     * Intercept calls to getRawValue to pretend there is no inputEl for rawValue handling,
     * so that we can use inputEl for user input of just the current value.
     */
    getRawValue: function() {
        var me = this,
            inputEl = me.inputEl,
            result;
 
        me.inputEl = false;
        result = me.callParent(arguments);
        me.inputEl = inputEl;
        return result;
    },
 
    /**
     * @inheritdoc
     * Intercept calls to setRawValue to pretend there is no inputEl for rawValue handling,
     * so that we can use inputEl for user input of just the current value.
     */
    setRawValue: function(value) {
        var me = this,
            inputEl = me.inputEl,
            result;
 
        me.inputEl = false;
        result = me.callParent([value]);
        me.inputEl = inputEl;
 
        return result;
    },
 
    /**
     * Adds a value or values to the current value of the field
     * @param {Mixed} value The value or values to add to the current value, see {@link #setValue}
     */
    addValue: function(value) {
        var me = this;
        if (value) {
            me.setValue(Ext.Array.merge(me.value, Ext.Array.from(value)));
        }
    },
 
    /**
     * Removes a value or values from the current value of the field
     * @param {Mixed} value The value or values to remove from the current value, see {@link #setValue}
     */
    removeValue: function(value) {
        var me = this;
 
        if (value) {
            me.setValue(Ext.Array.difference(me.value, Ext.Array.from(value)));
        }
    },
 
    /**
     * Sets the specified value(s) into the field. The following value formats are recognised:
     *
     * - Single Values
     *
     *     - A string associated to this field's configured {@link #valueField}
     *     - A record containing at least this field's configured {@link #valueField} and {@link #displayField}
     *
     * - Multiple Values
     *
     *     - If {@link #multiSelect} is `true`, a string containing multiple strings as
     *       specified in the Single Values section above, concatenated in to one string
     *       with each entry separated by this field's configured {@link #delimiter}
     *     - An array of strings as specified in the Single Values section above
     *     - An array of records as specified in the Single Values section above
     *
     * In any of the string formats above, the following occurs if an associated record cannot be found:
     *
     * 1. If {@link #forceSelection} is `false`, a new record of the {@link #store}'s configured model type
     *    will be created using the given value as the {@link #displayField} and {@link #valueField}.
     *    This record will be added to the current value, but it will **not** be added to the store.
     * 2. If {@link #forceSelection} is `true` and {@link #queryMode} is `remote`, the list of unknown
     *    values will be submitted as a call to the {@link #store}'s load as a parameter named by
     *    the {@link #valueParam} with values separated by the configured {@link #delimiter}.
     *    ** This process will cause setValue to asynchronously process. ** This will only be attempted
     *    once. Any unknown values that the server does not return records for will be removed.
     * 3. Otherwise, unknown values will be removed.
     *
     * @param {Mixed} value The value(s) to be set, see method documentation for details
     * @return {Ext.form.field.Field/Boolean} this, or `false` if asynchronously querying for unknown values
     */
    setValue: function(value, doSelect, skipLoad) {
        var me = this,
            valueStore = me.valueStore,
            valueField = me.valueField,
            unknownValues = [],
            record, len, i, valueRecord, cls;
 
        if (Ext.isEmpty(value)) {
            value = null;
        }
        if (Ext.isString(value) && me.multiSelect) {
            value = value.split(me.delimiter);
        }
        value = Ext.Array.from(value, true);
 
        for (= 0, len = value.length; i < len; i++) {
            record = value[i];
            if (!record || !record.isModel) {
                valueRecord = valueStore.findExact(valueField, record);
                if (valueRecord >= 0) {
                    value[i] = valueStore.getAt(valueRecord);
                } else {
                    valueRecord = me.findRecord(valueField, record);
                    if (!valueRecord) {
                        if (me.forceSelection) {
                            unknownValues.push(record);
                        } else {
                            valueRecord = {};
                            valueRecord[me.valueField] = record;
                            valueRecord[me.displayField] = record;
 
                            cls = me.valueStore.getModel();
                            valueRecord = new cls(valueRecord);
                        }
                    }
                    if (valueRecord) {
                        value[i] = valueRecord;
                    }
                }
            }
        }
 
        if ((skipLoad !== true) && (unknownValues.length > 0) && (me.queryMode === 'remote')) {
            var params = {};
            params[me.valueParam || me.valueField] = unknownValues.join(me.delimiter);
            me.store.load({
                params: params,
                callback: function() {
                    if (me.itemList) {
                        me.itemList.unmask();
                    }
                    me.setValue(value, doSelect, true);
                    me.autoSize();
                    me.lastQuery = false;
                }
            });
            return false;
        }
 
        // For single-select boxes, use the last good (formal record) value if possible 
        if (!me.multiSelect && (value.length > 0)) {
            for (= value.length - 1; i >= 0; i--) {
                if (value[i].isModel) {
                    value = value[i];
                    break;
                }
            }
            if (Ext.isArray(value)) {
                value = value[value.length - 1];
            }
        }
 
        return me.callParent([value, doSelect]);
    },
 
    /**
     * Returns the records for the field's current value
     * @return {Array} The records for the field's current value
     */
    getValueRecords: function() {
        return this.valueStore.getRange();
    },
 
    /**
     * @inheritdoc
     * Overridden to optionally allow for submitting the field as a json encoded array.
     */
    getSubmitData: function() {
        var me = this,
            val = me.callParent(arguments);
 
        if (me.multiSelect && me.encodeSubmitValue && val && val[me.name]) {
            val[me.name] = Ext.encode(val[me.name]);
        }
 
        return val;
    },
 
    /**
     * Overridden to clear the input field if we are auto-setting a value as we blur.
     * @protected
     */
    mimicBlur: function() {
        var me = this;
 
        if (me.selectOnTab && me.picker && me.picker.highlightedItem) {
            me.inputEl.dom.value = '';
        }
 
        me.callParent(arguments);
    },
 
    /**
     * Overridden to handle partial-input selections more directly
     */
    assertValue: function() {
        var me = this,
            rawValue = me.inputEl.dom.value,
            rec = !Ext.isEmpty(rawValue) ? me.findRecordByDisplay(rawValue) : false,
            value = false;
 
        if (!rec && !me.forceSelection && me.createNewOnBlur && !Ext.isEmpty(rawValue)) {
            value = rawValue;
        } else if (rec) {
            value = rec;
        }
 
        if (value) {
            me.addValue(value);
        }
 
        me.inputEl.dom.value = '';
 
        me.collapse();
    },
 
    /**
     * Expand record values for evaluating change and fire change events for UI to respond to
     */
    checkChange: function() {
        if (!this.suspendCheckChange && !this.isDestroyed) {
            var me = this,
            valueStore = me.valueStore,
            lastValue = me.lastValue || '',
            valueField = me.valueField,
            newValue = Ext.Array.map(Ext.Array.from(me.value), function(val) {
                if (val.isModel) {
                    return val.get(valueField);
                }
                return val;
            }, this).join(this.delimiter),
            isEqual = me.isEqual(newValue, lastValue);
 
            if (!isEqual || ((newValue.length > 0 && valueStore.getCount() < me.value.length))) {
                valueStore.suspendEvents();
                valueStore.removeAll();
                if (Ext.isArray(me.valueModels)) {
                    valueStore.add(me.valueModels);
                }
                valueStore.resumeEvents();
                valueStore.fireEvent('datachanged', valueStore);
 
                if (!isEqual) {
                    me.lastValue = newValue;
                    me.fireEvent('change', me, newValue, lastValue);
                    me.onChange(newValue, lastValue);
                }
            }
        }
    },
 
    /**
     * Overridden to be more accepting of varied value types
     */
    isEqual: function(v1, v2) {
        var fromArray = Ext.Array.from,
            valueField = this.valueField,
            i, len, t1, t2;
 
        v1 = fromArray(v1);
        v2 = fromArray(v2);
        len = v1.length;
 
        if (len !== v2.length) {
            return false;
        }
 
        for(= 0; i < len; i++) {
            t1 = v1[i].isModel ? v1[i].get(valueField) : v1[i];
            t2 = v2[i].isModel ? v2[i].get(valueField) : v2[i];
            if (t1 !== t2) {
                return false;
            }
        }
 
        return true;
    },
 
    /**
     * Overridden to use value (selection) instead of raw value and to avoid the use of placeholder
     */
    applyEmptyText : function() {
        var me = this,
            emptyText = me.emptyText,
            emptyEl = me.emptyEl,
            inputEl = me.inputEl,
            listWrapper = me.listWrapper,
            emptyCls = me.emptyCls,
            emptyInputCls = me.emptyInputCls,
            isEmpty;
 
        if (me.rendered && emptyText) {
            isEmpty = Ext.isEmpty(me.value) && !me.hasFocus;
            if (isEmpty) {
                inputEl.dom.value = '';
                emptyEl.setHtml(emptyText);
                emptyEl.addCls(emptyCls);
                emptyEl.removeCls(emptyInputCls);
                listWrapper.addCls(emptyCls);
                inputEl.addCls(emptyInputCls);
            } else {
                emptyEl.addCls(emptyInputCls);
                emptyEl.removeCls(emptyCls);
                listWrapper.removeCls(emptyCls);
                inputEl.removeCls(emptyInputCls);
            }
            me.autoSize();
        }
    },
 
    /**
     * Overridden to use inputEl instead of raw value and to avoid the use of placeholder
     */
    preFocus : function(){
        var me = this,
            inputEl = me.inputEl,
            isEmpty = inputEl.dom.value === '';
 
        me.emptyEl.addCls(me.emptyInputCls);
        me.emptyEl.removeCls(me.emptyCls);
        me.listWrapper.removeCls(me.emptyCls);
        me.inputEl.removeCls(me.emptyInputCls);
 
        if (me.selectOnFocus || isEmpty) {
            inputEl.dom.select();
        }
    },
 
    /**
     * Intercept calls to onFocus to add focusCls, because the base field
     * classes assume this should be applied to inputEl
     */
    onFocus: function() {
        var me = this,
            focusCls = me.focusCls,
            itemList = me.itemList;
 
        if (focusCls && itemList) {
            itemList.addCls(focusCls);
        }
 
        me.callParent(arguments);
    },
 
    /**
     * Intercept calls to onBlur to remove focusCls, because the base field
     * classes assume this should be applied to inputEl
     */
    onBlur: function() {
        var me = this,
            focusCls = me.focusCls,
            itemList = me.itemList;
 
        if (focusCls && itemList) {
            itemList.removeCls(focusCls);
        }
 
        me.callParent(arguments);
    },
 
    /**
     * Intercept calls to renderActiveError to add invalidCls, because the base
     * field classes assume this should be applied to inputEl
     */
    renderActiveError: function() {
        var me = this,
            invalidCls = me.invalidCls,
            itemList = me.itemList,
            hasError = me.hasActiveError();
 
        if (invalidCls && itemList) {
            itemList[hasError ? 'addCls' : 'removeCls'](me.invalidCls + '-field');
        }
 
        me.callParent(arguments);
    },
 
    /**
     * Initiate auto-sizing for height based on {@link #grow}, if applicable.
     */
    autoSize: function() {
        var me = this;
 
        if (me.grow && me.rendered) {
            me.autoSizing = true;
            me.updateLayout();
        }
 
        return me;
    },
 
    /**
     * Track height change to fire {@link #event-autosize} event, when applicable.
     */
    afterComponentLayout: function() {
        var me = this,
            height;
 
        if (me.autoSizing) {
            height = me.getHeight();
            if (height !== me.lastInputHeight) {
                if (me.isExpanded) {
                    me.alignPicker();
                }
                me.fireEvent('autosize', me, height);
                me.lastInputHeight = height;
                me.autoSizing = false;
            }
        }
    }
});