/**
 * A selection model for {@link Ext.grid.Grid grids} which allows you to select data in
 * a spreadsheet-like manner.
 *
 * Supported features:
 *
 *  - Single / Range / Multiple individual row selection
 *  - Single / Range cell selection
 *  - Column selection by click selecting column headers
 *  - Select / deselect all by clicking in the top-left, header
 *  - Adds row number column to enable row selection
 *  - Optionally you can enable row selection using checkboxes
 *
 * # Example usage
 *
 *     @example
 *     Ext.create({
 *         xtype: 'grid',
 *         title: 'Simpsons',
 *         store: [{
 *             name: 'Lisa',
 *             email: '[email protected]',
 *             phone: '555-111-1224'
 *         }, {
 *             name: 'Bart',
 *             email: '[email protected]',
 *             phone: '555-222-1234'
 *         }, {
 *             name: 'Homer',
 *             email: '[email protected]',
 *             phone: '555-222-1244'
 *         }],
 *         width: 400,
 *         height: 300,
 *         renderTo: Ext.getBody(),
 *         columns: [{
 *             text: 'Name',
 *             dataIndex: 'name'
 *         }, {
 *             text: 'Email',
 *             dataIndex: 'email',
 *             flex: 1
 *         }, {
 *             text: 'Phone',
 *             dataIndex: 'phone'
 *         }],
 *         selectable: {
 *             columns: false, // Can select cells and rows, but not columns
 *             extensible: true // Uses the draggable selection extender
 *         }
 *     });
 *
 * # Using {@link Ext.data.virtual.Store}s
 * It is very important to remember that a {@link Ext.data.virtual.Store} does *not* contain the
 * full dataset. The purpose of a VirtualStore is to only hold in the client, a range of
 * pages from the dataset that corresponds with what is currently visible in the grid
 * (plus a few pages above and below the visible range to allow fast scrolling).
 *
 * When using "select all" rows and a VirtualStore, an `allSelected` flag is set, and so all
 * records which are read into the client side cache will thenceforth be selected, and will
 * be rendered as selected in the grid.
 *
 * *But records which have not been read into the cache will obviously not be available
 * when interrogating selected records. What is stored as being selected is row index ranges.*
 *
 */
Ext.define('Ext.grid.selection.Model', {
    extend: 'Ext.dataview.selection.Model',
    requires: [
        'Ext.grid.Location',
        'Ext.grid.selection.*'
    ],
 
    alias: 'selmodel.grid',
 
    isGridSelectionModel: true,
 
    config: {
        /**
         * @cfg {Boolean} columns
         * Set to `true` to enable selection of columns.
         *
         * **NOTE**: This will disable sorting on header click and instead provide column
         * selection and deselection. Sorting is still available via column header menu.
         */
        columns: {
            $value: false,
            lazy: true
        },
 
        /**
         * @cfg {Boolean} cells
         * Set to `true` to enable selection of individual cells or a single rectangular
         * range of cells. This will provide cell range selection using click, and
         * potentially drag to select a rectangular range if (@link #cfg!drag} is `true`.
         * You can also use "SHIFT + arrow" key navigation to select a range of cells.
         */
        cells: {
            $value: false,
            lazy: true
        },
 
        /**
         * @cfg {Boolean} rows
         * Set to `true` to enable selection of rows by clicking on the selection model's
         * {@link #cfg!checkbox} column, {@link Ext.grid.Grid#cfg!rowNumbers row number column}
         * or, if {@link #cfg!drag} is `true`, by swiping down the
         * {@link Ext.grid.Grid#cfg!rowNumbers row number column}.
         */
        rows: {
            $value: true,
            lazy: true
        },
 
        /**
         * @cfg {Boolean} drag
         * Set to `true` to enables cell and row range selection by dragging.
         */
        drag: false,
 
        /**
         * @cfg {String} extensible
         * This configures whether this selection model is to implement a mouse based
         * dragging gesture to extend a *contiguous* selection.
         *
         * Note that if there are multiple, discontiguous selected rows or columns,
         * selection extension is not available.
         *
         * If set, then the bottom right corner of the contiguous selection will display a
         * drag handle. By dragging this, an extension area may be defined into which the
         * selection is extended.
         *
         * The {@link Ext.grid.Grid#beforeselectionextend beforeselectionextend} event fires
         * at the end of the drag though the owning grid. Event handlers may manipulate the
         * store data in any way.
         *
         * Possible values for this configuration are
         *
         *    - `"x"` Only allow extending the block to the left or right.
         *    - `"y"` Only allow extending the block above or below.
         *    - `"xy"` Allow extending the block in both dimensions.
         *    - `"both"` Allow extending the block in both dimensions.
         *    - `true` Allow extending the block in both dimensions.
         *    - `false` Disable the extensible feature
         *    - `null` Disable the extensible feature
         *
         * It's important to notice that setting this to `"both"`, `"xy"` or `true` will
         * allow you to extend the selection in both directions, but only one direction at
         * a time. It will NOT be possible to drag it diagonally.
         */
        extensible: {
            $value: false,
            lazy: true
        },
 
        /**
         * @cfg {Boolean} reducible
         * @since 6.6.0
         * This configures if the extensible config is also allowed to reduce its selection
         *
         * Note: This is only relevant if `extensible` is not `false` or `null`
         */
        reducible: true,
 
        /**
         * @cfg {Boolean} checkbox
         * Configure as `true` to include a checkbox to indicate selection of *Records*. The
         * checkbox cell plays no part in cell or column selection apart from being a selected
         * cell and part of any iteration through selections.
         *
         * See {@link #cfg!headerCheckbox} for inclusion of a "select all" checkbox in the
         * column header of the checkbox column.
         *
         * See {@link #cfg!checkboxDefaults} for how to influence the configuration of the
         * checkbox column header.
         */
        checkbox: false,
 
        /**
         * @cfg {Boolean} headerCheckbox
         * Configure as `false` to not display the header checkbox at the top of the checkbox
         * column when {@link #checkboxSelect} is set.
         */
        headerCheckbox: true,
 
        /**
         * @cfg {Object} checkboxDefaults
         * A config object to configure the checkbox column header if {@link #cfg!checkbox}
         * is set.
         */
        checkboxDefaults: {
            xtype: 'selectioncolumn',
            text: null,
            width: 30
        },
 
        showNumbererColumn: false
    },
 
    /**
     * @event selectionchange
     * Fired *by the grid* after the selection changes. Return `false` to veto the selection
     * extension.
     *
     * @param {Ext.grid.Panel} grid The grid whose selection has changed.
     * @param {Ext.dataview.selection.Selection} selection A subclass of
     * {@link Ext.dataview.selection.Selection} describing the new selection.
     */
 
    /**
     * @cfg {Boolean} checkboxSelect
     * Enables selection of the row via clicking on checkbox. Note: this feature will add
     * new column at position specified by {@link #checkboxColumnIndex}.
     */
    checkboxSelect: false,
 
    /**
     * @cfg {Number/String} checkboxColumnIndex
     * The index at which to insert the checkbox column.
     * Supported values are a numeric index, and the strings 'first' and 'last'. Only valid
     * when set before render.
     */
    checkboxColumnIndex: 0,
 
    mode: 'multi',
 
    columnSelectCls: Ext.baseCSSPrefix + 'selmodel-column-select',
    rowNumbererHeaderCls: Ext.baseCSSPrefix + 'selmodel-row-numberer-hd',
 
    /**
     * @member Ext.grid.Grid
     * @event beforeselectionextend An event fired when an extension block is extended
     * using a drag gesture. Only fired when the grid's
     * `{@link Ext.grid.Grid.selectable #cfg!selectable}` is configured with the
     * {@link Ext.grid.selection.Model#extensible extensible} config.
     *
     * @param {Ext.grid.Grid} grid The owning grid.
     * @param {Ext.dataview.selection.Selection} An object which encapsulates a contiguous
     * selection block.
     * @param {Object} extension An object describing the type and size of extension.
     * @param {String} extension.type `"rows"` or `"columns"`
     * @param {Ext.grid.Location} extension.start The start (top left) cell of the
     * extension area.
     * @param {Ext.grid.Location} extension.end The end (bottom right) cell of the
     * extension area.
     * @param {number} [extension.columns] The number of columns extended (-ve means on
     * the left side).
     * @param {number} [extension.rows] The number of rows extended (-ve means on the top
     * side).
     */
 
    /**
     * @member Ext.grid.Grid
     * @event selectionextenderdrag An event fired when an extension block is dragged to encompass
     * a new range. Only fired when the grid's `{@link Ext.grid.Grid.selectable #cfg!selectable}`
     * is configured with the {@link Ext.grid.selection.Model#extensible extensible} config.
     * @param {Ext.grid.Grid} grid The owning grid.
     * @param {Ext.dataview.selection.Selection} An object which encapsulates a contiguous
     * selection block.
     * @param {Object} extension An object describing the type and size of extension.
     * @param {String} extension.type `"rows"` or `"columns"`
     * @param {HTMLElement} extension.overCell The grid cell over which the mouse is being dragged.
     * @param {Ext.grid.Location} extension.start The start (top left) cell of the extension area.
     * @param {Ext.grid.Location} extension.end The end (bottom right) cell of the extension area.
     * @param {number} [extension.columns] The number of columns extended (-ve means on the
     * left side).
     * @param {number} [extension.rows] The number of rows extended (-ve means on the top side).
     */
 
    /**
     * @private
     */
    updateView: function(view, oldView) {
        var me = this,
            rowNumberer = me.numbererColumn = view.getRowNumbers(),
            checkbox = me.getCheckbox();
 
        me.callParent([view, oldView]);
 
        if (oldView) {
            me.navigationModel = null;
            Ext.destroy(me.viewListeners);
        }
 
        if (view) {
            // If there is a row numberer column we can use, add our classes to it so it can
            // get a different UI if the theme requires it. Also the header cursor is a
            // "select all" diagonal arrow.
            if (rowNumberer) {
                // If the grid shows a row numberer, add our class
                rowNumberer.setCell({
                    cls: me.rowNumbererCellCls
                });
                rowNumberer.setCls(me.rowNumbererHeaderCls);
            }
 
            if (checkbox) {
                view.registerColumn(checkbox);
            }
 
            me.viewListeners = view.on(me.getViewListeners());
        }
    },
 
    /**
     * @private
     * Called after the view has completed its initialization.
     * @param view
     */
    onViewCreated: function(view) {
        // Add class to add special cursor pointer to column headers
        if (this.getColumns()) {
            view.addCls(this.columnSelectCls);
        }
 
        this.updateHeaderState();
    },
 
    updateDrag: function(drag) {
        var view = this.getView(),
            viewListeners = {
                dragstart: 'onViewDragStart',
                delegate: view.eventDelegate,
                scope: this
            };
 
        // Start a drag on longpress if touch is supported.
        if (Ext.supports.Touch) {
            viewListeners.longpress = 'onViewLongpress';
        }
 
        view.innerCt[drag ? 'on' : 'un'](viewListeners);
    },
 
    /**
     * @param {"rows"/"records"/"cells"/"columns"} what The kind of object is to be selected.
     * @param {Boolean} reset 
     * @return {Ext.dataview.selection.Selection} A Selection object of the required type.
     * @private
     */
    getSelection: function(what, reset) {
        var config, result;
 
        // The two are interchangeable, to callers, but virtual stores use
        // row range selection as opposed to record collection.
        if (what === 'rows' || what === 'records') {
            what = this.getStore().isVirtualStore ? 'rows' : 'records';
        }
 
        result = this.callParent();
 
        // If called with a required type, ensure that the selection object
        // is of that type.
        if (what) {
            what = what.toLowerCase();
 
            if (!result || result.type !== what) {
                config = {
                    type: what
                };
 
                if (what === 'records') {
                    config.selected = this.getSelected();
                }
 
                this.setSelection(config);
 
                result = this.callParent();
            }
            else if (reset) {
                result.clear(true);
            }
        }
 
        return result;
    },
 
    /**
     * Retrieve a configuration to be used in a HeaderContainer.
     * This should be used when checkboxSelect is set to false.
     * @private
     */
    createCheckboxColumn: function(checkboxDefaults) {
        var me = this;
 
        return Ext.apply({
            headerCheckbox: me.getHeaderCheckbox() !== false
        }, checkboxDefaults);
    },
 
    /**
     * @private
     */
    onHeaderTap: function(headerCt, header, e) {
        var me = this,
            sel = me.getSelection(),
            isSelected = false,
            range, columns, i;
 
        // A click on the numberer column toggles all
        if (header === this.numbererColumn) {
            me.toggleAll(header, e);
        }
        // A column select click: exclude the checkbox column
        else if (me.getColumns() && header !== me.getCheckbox()) {
            // SHIFT means select range from last selected to here
            if (e.shiftKey && sel && sel.lastColumnSelected) {
 
                // CTRL means keep current selection
                if (!e.ctrlKey) {
                    sel.clear();
                }
 
                headerCt = me.getView().getHeaderContainer();
                columns = headerCt.getColumns();
 
                range = Ext.Array.sort([
                    headerCt.indexOfLeaf(sel.lastColumnSelected),
                    headerCt.indexOf(header)
                ], Ext.Array.numericSortFn);
 
                for (= range[0]; i <= range[1]; i++) {
                    me.selectColumn(columns[i], true);
                }
            }
            else {
                isSelected = me.isColumnSelected(header);
 
                if (!e.ctrlKey) {
                    sel.clear();
                }
                else if (isSelected) {
                    me.deselectColumn(header);
                    me.getSelection().lastColumnSelected = null;
                }
 
                if (!isSelected || !e.ctrlKey && e.pointerType !== 'touch') {
                    me.selectColumn(header, e.ctrlKey);
                    me.getSelection().lastColumnSelected = header;
                }
 
                me.updateSelectionExtender();
            }
        }
    },
 
    /**
     * @private
     */
    toggleAll: function(header, e) {
        var me = this,
            sel = me.getSelection();
 
        e.stopEvent();
 
        // Not all selected, select all
        if (!sel || !sel.isAllSelected()) {
            me.selectAll();
        }
        else {
            me.deselectAll();
        }
 
        me.updateHeaderState();
        me.lastColumnSelected = null;
    },
 
    selectByLocation: function(location) {
        //<debug>
        if (!location.isGridLocation) {
            Ext.raise('selectByLocation MUST be passed an Ext.grid.Location');
        }
        //</debug>
 
        // eslint-disable-next-line vars-on-top
        var me = this,
            record = location.record,
            column = location.column;
 
        if (me.getCells()) {
            me.selectCells(location, location);
        }
        else if (me.getRows() && record) {
            this.select(record);
        }
        else if (me.getColumns() && column) {
            me.selectColumn(column);
        }
    },
 
    /**
     * @private
     */
    updateHeaderState: function() {
        // check to see if all records are selected
        var me = this,
            store = me.getStore(),
            sel = me.getSelection(),
            isChecked = false,
            checkHd = me.getCheckbox(),
            storeCount;
 
        if (store && sel && sel.isRows) {
            storeCount = store.getCount();
 
            if (store.isBufferedStore) {
                isChecked = sel.allSelected;
            }
            else {
                isChecked = storeCount > 0 && (storeCount === sel.getCount());
            }
        }
 
        if (checkHd) {
            checkHd.setHeaderStatus(isChecked);
        }
    },
 
    /**
     * Intercepts the grid's updateColumns method.  Adds the checkbox header.
     * @param headerCt
     * @param {Object[]} columns 
     * @private
     */
    onColumnUpdate: function(headerCt, columns) {
        var me = this,
            checkColumn = me.getCheckbox();
 
        if (checkColumn) {
            // This is being called from a reconfigure operation - from updateColumns
            // so we have to preserve our column from destruction
            if (headerCt) {
                headerCt.remove(checkColumn, false);
            }
 
            columns.push(checkColumn);
        }
    },
 
    select: function(records, keepExisting, suppressEvent) {
        // API docs are inherited
        var me = this,
            sel = me.getSelection('records'),
            store = me.getStore(),
            len, i, record;
 
        if (!Ext.isArray(records)) {
            records = [records];
        }
 
        len = records.length;
 
        for (= 0; i < len; i++) {
            record = records[i];
 
            if (typeof record === 'number') {
                records[i] = record = store.getAt(record);
            }
        }
 
        // SelectionObject will call fireSelectionChange if necessary
        sel.add(records, keepExisting, suppressEvent);
    },
 
    deselect: function(records, suppressEvent) {
        // API docs are inherited
        var me = this,
            sel = me.getSelection('records'),
            store = me.getView().getStore(),
            len, i, record;
 
        if (sel && sel.isRecords) {
            if (!Ext.isArray(records)) {
                records = [records];
            }
 
            len = records.length;
 
            for (= 0; i < len; i++) {
                record = records[i];
 
                if (typeof record === 'number') {
                    records[i] = record = store.getAt(record);
                }
            }
        }
 
        // SelectionObject will call fireSelectionChange if necessary
        sel.remove(records, suppressEvent);
    },
 
    onCollectionRemove: function(selectedCollection, chunk) {
        this.updateHeaderState();
        this.callParent([selectedCollection, chunk]);
    },
 
    onCollectionAdd: function(selectedCollection, adds) {
        this.updateHeaderState();
        this.callParent([selectedCollection, adds]);
    },
 
    /**
     * This method allows programmatic selection of the cell range.
     *
     *     @example
     *     var store = Ext.create('Ext.data.Store', {
     *         fields  : ['name', 'email', 'phone'],
     *         data    : {
     *             items : [
     *                 { name : 'Lisa',  email : '[email protected]',  phone : '555-111-1224' },
     *                 { name : 'Bart',  email : '[email protected]',  phone : '555-222-1234' },
     *                 { name : 'Homer', email : '[email protected]', phone : '555-222-1244' },
     *                 { name : 'Marge', email : '[email protected]', phone : '555-222-1254' }
     *             ]
     *         },
     *         proxy   : {
     *             type   : 'memory',
     *             reader : {
     *                 type : 'json',
     *                 root : 'items'
     *             }
     *         }
     *     });
     *
     *     var grid = Ext.create('Ext.grid.Grid', {
     *         title    : 'Simpsons',
     *         store    : store,
     *         width    : 400,
     *         renderTo : Ext.getBody(),
     *         columns  : [
     *            columns: [
     *               { text: 'Name',  dataIndex: 'name' },
     *               { text: 'Email', dataIndex: 'email', flex: 1 },
     *               { text: 'Phone', dataIndex: 'phone', width:120 },
     *               {
     *                   text:'Combined', dataIndex: 'name', width : 300,
     *                   renderer: function(value, metaData, record, rowIndex,
     *                                       colIndex, store, view) {
     *                       console.log(arguments);
     *                       return value + ' has email: ' + record.get('email');
     *                   }
     *               }
     *           ],
     *         ],
     *         selType: 'spreadsheet'
     *     });
     *
     *     var selectable = grid.getSelectable();  // get selection model
     *
     *     // We will create range of 4 cells.
     *
     *     // Now set the range  and prevent rangeselect event from being fired.
     *     // We can use a simple array when we have no locked columns.
     *     selectable.selectCells([0, 0], [1, 1], true);
     *
     * @param rangeStart {Ext.grid.Location/Number[]} Range starting position. Can be
     * either Cell context or a `[rowIndex, columnIndex]` numeric array.
     *
     * Note that when a numeric array is used in a locking grid, the column indices are
     * relative to the outermost grid, encompassing locked *and* normal sides.
     * @param rangeEnd {Ext.grid.Location/Number[]} Range end position. Can be either Cell
     * context or a `[rowIndex, columnIndex]` numeric array.
     *
     * Note that when a numeric array is used in a locking grid, the column indices are
     * relative to the outermost grid, encompassing locked *and* normal sides.
     * @param {Boolean} [suppressEvent] Pass `true` to not fire the `{@link #selectionchange}`
     * event.
     */
    selectCells: function(rangeStart, rangeEnd, suppressEvent) {
        var me = this,
            view = me.getView(),
            sel;
 
        rangeStart = rangeStart.isGridLocation
            ? rangeStart.clone()
            : new Ext.grid.Location(view, {
                record: rangeStart[0],
                column: rangeStart[1]
            });
 
        rangeEnd = rangeEnd.isGridLocation
            ? rangeEnd.clone()
            : new Ext.grid.Location(view, {
                record: rangeEnd[0],
                column: rangeEnd[1]
            });
 
        me.resetSelection(true);
 
        sel = me.getSelection('cells');
        sel.setRangeStart(rangeStart);
        sel.setRangeEnd(rangeEnd);
 
        if (!suppressEvent) {
            me.fireSelectionChange();
        }
    },
 
    /**
     * Select all the data if possible.
     *
     * If {@link #rows} is `true`, then all *records* will be selected.
     *
     * If {@link #cells} is `true`, then all *rendered cells* will be selected.
     *
     * If {@link #columns} is `true`, then all *columns* will be selected.
     *
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    selectAll: function(suppressEvent) {
        var me = this,
            sel = me.getSelection(),
            doSelect;
 
        if (me.getRows()) {
            sel = me.getSelection('records');
            doSelect = true;
        }
        else if (me.getCells()) {
            sel = me.getSelection('cells');
            doSelect = true;
        }
        else if (me.getColumns()) {
            sel = me.getSelection('columns');
            doSelect = true;
        }
 
        if (doSelect) {
            sel.selectAll(suppressEvent); // populate the selection with the records
        }
    },
 
    /**
     * Clears the selection.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    deselectAll: function(suppressEvent) {
        var sel = this.getSelection();
 
        if (sel && sel.getCount()) {
            sel.clear(suppressEvent);
        }
    },
 
    /**
     * Select one or more rows.
     * @param rows {Ext.data.Model[]} Records to select.
     * @param {Boolean} [keepSelection=false] Pass `true` to keep previous selection.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    selectRows: function(rows, keepSelection, suppressEvent) {
        var sel = this.getSelection('records');
 
        if (!keepSelection) {
            this.resetSelection(true);
        }
 
        sel.add(rows, keepSelection, suppressEvent);
    },
 
    isSelected: function(record) {
        // API docs are inherited.
        return this.isRowSelected(record);
    },
 
    /**
     * Selects a column.
     * @param {Ext.grid.column.Column} column Column to select.
     * @param {Boolean} [keepSelection=false] Pass `true` to keep previous selection.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    selectColumn: function(column, keepSelection, suppressEvent) {
        var selData = this.getSelection('columns');
 
        if (!selData.isSelected(column)) {
            if (!keepSelection) {
                selData.clear(suppressEvent);
                selData.setRangeStart(column);
            }
            else {
                selData.add(column);
            }
        }
    },
 
    /**
     * Deselects a column.
     * @param {Ext.grid.column.Column} column Column to deselect.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    deselectColumn: function(column, suppressEvent) {
        var selData = this.getSelection();
 
        if (selData && selData.isColumns && selData.isSelected(column)) {
            selData.remove(column, suppressEvent);
        }
    },
 
    destroy: function() {
        var me = this,
            view = me.getView(),
            checkbox = me.checkbox;
 
        if (view && !view.destroying && checkbox) {
            view.unregisterColumn(checkbox, true);
        }
 
        Ext.destroy(me.viewListeners, me.getConfig('extensible', null, true));
        me.callParent();
    },
 
    //-------------------------------------------------------------------------
 
    privates: {
        /**
         * @property {Object} axesConfigs
         * Use when converting the extensible config into a SelectionExtender to create
         * its `axes` config to specify which axes it may extend.
         * @private
         */
        axesConfigs: {
            x: 1,
            y: 2,
            xy: 3,
            both: 3,
            "true": 3 // reserved word MUST be quoted when used an a property name
        },
 
        /**
         * @return {Object} 
         * @private
         */
        getViewListeners: function() {
            return {
                columnschanged: 'onColumnsChanged',
                columnmove: 'onColumnMove',
                scope: this,
                destroyable: true
            };
        },
 
        /**
         * @private
         */
        refreshSelection: function() {
            var selection = this.getSelection();
 
            if (selection.isRecords || selection.isRows) {
                this.callParent();
            }
            else {
                this.resetSelection();
            }
        },
 
        /**
         * @private
         */
        onColumnsChanged: function() {
            var me = this,
                selData = me.getSelection(),
                view, selectionChanged;
 
            // When columns have changed, we have to deselect *every* cell in the row range
            // because we do not know where the columns have gone to.
            if (selData) {
                view = selData.view;
 
                if (selData.isCells) {
                    if (view.visibleColumns().length) {
                        selData.eachCell(function(location) {
                            view.onCellDeselect(location);
                        });
                    }
                    else {
                        me.clearSelections();
                    }
                }
                // We have to deselect columns which have been hidden/removed
                else if (selData.isColumns) {
                    selectionChanged = false;
 
                    selData.eachColumn(function(column) {
                        if (!column.isVisible() || !view.isAncestor(column)) {
                            me.remove(column);
                            selectionChanged = true;
                        }
                    });
                }
            }
 
            // This event is fired directly from the HeaderContainer before the view updates.
            // So we have to wait until idle to update the selection UI.
            // NB: fireSelectionChange calls updateSelectionExtender after firing its event.
            Ext.on('idle', selectionChanged ? me.fireSelectionChange : me.updateSelectionExtender,
                   me, { single: true });
        },
 
        // The selection may have acquired or lost contiguity, so the replicator may need
        // enabling or disabling
        onColumnMove: function() {
            this.updateSelectionExtender();
        },
 
        /**
         * @private
         */
        resetSelection: function(suppressEvent) {
            var sel = this.getSelection();
 
            if (sel) {
                sel.clear(suppressEvent);
            }
        },
 
        onViewLongpress: function(e) {
            if (e.pointerType === 'touch') {
                e.startDrag();
            }
        },
 
        /**
         * Plumbing for drag selection of cell range
         * @private
         */
        onViewDragStart: function(e) {
            // For touch gestures, only initiate drags on longpress
            if (e.pointerType === 'touch' && !e.longpress) {
                return;
            }
 
            // eslint-disable-next-line vars-on-top
            var me = this,
                view = me.getView(),
                location = new Ext.grid.Location(view, e),
                header = location.column,
                viewLocation = view.getNavigationModel().getLocation(),
                isCheckClick = header === me.getCheckbox(),
                resumingSelection = false,
                sel;
 
            if (!location.cell) {
                return;
            }
 
            // Ignore right click, shift and alt modifiers.
            // Ignore when actionableMode is true so we can select the text inside an editor
 
            if (e.claimed || e.button > 0 || e.altKey ||
                (viewLocation && viewLocation.actionable) || !view.shouldSelectItem(e)) {
                return;
            }
 
            if (header) {
                e.claimGesture();
                me.mousedownPosition = location.clone();
 
                if (isCheckClick) {
                    me.checkCellClicked = location.cell.element.dom;
                }
 
                // Differentiate between row and cell selections.
                if (header === me.numbererColumn || isCheckClick || !me.getCells()) {
                    // Enforce rows setting
                    if (me.getRows()) {
                        // If checkOnly is set, and we're attempting to select a row 
                        // outside of the checkbox column then reject
                        if (e.shiftKey && me.isSelected(location.record)) {
                            resumingSelection = true;
                        }
                        else if (!e.shiftKey && !isCheckClick && me.checkboxOnly) {
                            return;
                        }
 
                        sel = me.getSelection('records');
 
                        if (!e.shiftKey && !e.ctrlKey && !isCheckClick) {
                            sel.clear();
                        }
                    }
                    else if (me.getColumns()) {
                        sel = me.getSelection('columns');
 
                        if (e.shiftKey && me.isColumnSelected(location.column)) {
                            resumingSelection = true;
                        }
                        else if (!e.shiftKey && !e.ctrlKey && !isCheckClick) {
                            sel.clear();
                        }
                    }
                    else {
                        return false;
                    }
                }
                else {
                    sel = me.getSelection('cells');
 
                    if (e.shiftKey &&
                        me.isCellSelected(location.recordIndex, location.columnIndex)) {
                        resumingSelection = true;
                    }
                    else if (!e.shiftKey) {
                        sel.clear();
                    }
                }
 
                if (!resumingSelection) {
                    me.lastDragLocation = null;
                }
 
                // If it was a lomgpress, begin selection now.
                // If it was a mousemove, then there will be a drag gesture coming right along.
                if (e.longpress) {
                    location.row.removeCls(view.pressedCls);
                    me.onViewSelectionDrag(e);
                }
 
                // Only begin the drag process if configured to select what they asked for
                if (sel) {
                    // Add the listener after the view has potentially been corrected
                    view.innerCt.on('dragend', me.onViewDragEnd, me, { single: true });
 
                    me.mousemoveListener = view.innerCt.on({
                        drag: 'onViewSelectionDrag',
                        scope: me,
                        delegate: view.eventDelegate,
                        destroyable: true
                    });
                }
            }
        },
 
        /**
         * Selects range based on mouse movements
         * @param e
         * @private
         */
        onViewSelectionDrag: function(e) {
            var me = this,
                view = me.getView(),
                newLocation, touch, realTarget;
 
            // The target of a Touch object remains unchanged from the touchstart target
            // even if the touch point moves outside of the original target.
            // We determine view Location from the "over" target, so polyfill using
            // the touch coordinates and document.elementFromPoint.
            if (e.changedTouches) {
                touch = e.changedTouches[0];
 
                // If the target does not contain the touch point, we have to correct it.
                if (touch && !Ext.fly(touch.target).getRegion().contains(touch.point)) {
                    realTarget = Ext.event.Event.resolveTextNode(
                        Ext.Element.fromPagePoint(touch.pageX, touch.pageY, true));
 
                    // Points can sometimes go negative and return no target.
                    if (realTarget) {
                        e.target = realTarget;
                    }
                }
            }
 
            me.stopAutoScroller();
 
            // Will fire when outside cells (on borders, row bodies and headers/footers).
            // We must only process cells.
            if (!Ext.fly(e.target).up(view.eventDelegate)) {
                me.scrollTowardsPointer(e, view);
 
                return;
            }
 
            newLocation = me.dragLocation = new Ext.grid.Location(view, e);
            me.changeSelectionRange(view, newLocation, e);
 
        },
 
        /**
         * @param view
         * @param location
         * @param e
         * @private
         */
        changeSelectionRange: function(view, location, e) {
            var me = this,
                overColumn = location.column,
                overRecord = location.record,
                overRowIdx = location.recordIndex,
                lastDragLocation = me.lastDragLocation,
                selData, lastOverRecord, lastOverColumn, recChange, colChange;
 
            e.claimGesture();
 
            if (lastDragLocation) {
                lastOverRecord = lastDragLocation.record;
                lastOverColumn = lastDragLocation.column;
            }
 
            // When the mousedown happens in a checkcolumn....
            if (me.checkCellClicked) {
                selData = me.getSelection('rows');
                selData.setRangeStart(me.getStore().indexOf(overRecord));
                me.checkCellClicked = null;
 
                return;
            }
 
            selData = me.getSelection();
 
            // Disable until a valid new selection is announced in fireSelectionChange
            if (me.getExtensible()) {
                me.getExtensible().disable();
            }
 
            if (overColumn) {
                recChange = overRecord !== lastOverRecord;
                colChange = overColumn !== lastOverColumn;
 
                // Initial mousedown was in rownumberer or checkbox overColumn
                if (selData.isRows || selData.isRecords) {
                    // Only react if we've changed row
                    if (recChange) {
                        if (lastOverRecord) {
                            selData.setRangeEnd(overRowIdx);
                        }
                        else {
                            selData.setRangeStart(overRowIdx);
                        }
                    }
                }
                // Selecting cells
                else if (selData.isCells) {
                    // Only react if we've changed row or overColumn
                    if (recChange || colChange) {
                        if (lastOverRecord) {
                            selData.setRangeEnd(location);
                        }
                        else {
                            selData.setRangeStart(location);
                        }
                    }
                }
                // Selecting columns
                else if (selData.isColumns) {
                    // Only react if we've changed overColumn
                    if (colChange) {
                        if (lastOverColumn) {
                            selData.setRangeEnd(location.column);
                        }
                        else {
                            selData.setRangeStart(location.column);
                        }
                    }
                }
 
                // Focus MUST follow the mouse.
                // Otherwise the focus may scroll out of the rendered range and revert to document
                if (recChange || colChange) {
                    view.getNavigationModel().setLocation(location);
                }
 
                me.lastDragLocation = location;
            }
        },
 
        /**
         * When dragging selection outside of the grid, this method will
         * scroll the view towards the pointer.
         * @param e
         * @param view
         * @private
         */
        scrollTowardsPointer: function(e, view) {
            var me = this,
                scrollClientRegion = view.getScrollable().getElement().getClientRegion(),
                point = e.getXY(),
                scrollTask = me.scrollTask || (me.scrollTask = Ext.util.TaskManager.newTask({
                    run: me.doAutoScroll,
                    args: [e, view],
                    scope: me,
                    interval: 10
                })),
                thresh = 25 * (window.devicePixelRatio || 1),
                scrollDelta = 3 * (window.devicePixelRatio || 1),
                scrollBy = me.scrollBy || (me.scrollBy = []);
 
            e.claimGesture();
 
            // Neart bottom of view
            if (point[1] > scrollClientRegion.bottom - thresh) {
                scrollBy[0] = 0;
                scrollBy[1] = scrollDelta;
                scrollTask.start();
            }
            // Near top of view
            else if (point[1] < scrollClientRegion.top + thresh) {
                scrollBy[0] = 0;
                scrollBy[1] = -scrollDelta;
                scrollTask.start();
            }
            // Near right edge of view
            else if (point[0] > scrollClientRegion.right - thresh) {
                scrollBy[0] = scrollDelta;
                scrollBy[1] = 0;
                scrollTask.start();
            }
            else if (point[0] < scrollClientRegion.left + thresh) {
                scrollBy[0] = -scrollDelta;
                scrollBy[1] = 0;
                scrollTask.start();
            }
        },
 
        doAutoScroll: function(e, view) {
            var me = this,
                scrollClientRegion = view.getScrollable().getElement().getClientRegion(),
                scroller = view.getScrollable(),
                xy = [],
                cell, location;
 
            // Bump the view in whatever direction was decided in the onDrag method.
            scroller.scrollBy.apply(scroller, me.scrollBy);
 
            if (me.scrollBy[0]) {
                xy[0] = me.scrollBy[0] > 0
                    ? scrollClientRegion.right - 5
                    : scrollClientRegion.left + 5;
            }
            else {
                xy[0] = e.getX();
            }
 
            if (me.scrollBy[1]) {
                xy[1] = me.scrollBy[1] > 0
                    ? scrollClientRegion.bottom - 5
                    : scrollClientRegion.top + 5;
            }
            else {
                xy[1] = e.getY();
            }
 
            cell = document.elementFromPoint.apply(document, xy);
 
            if (cell) {
                cell = Ext.fly(cell).up(view.cellSelector);
 
                if (!cell) {
                    me.stopAutoScroller();
 
                    return;
                }
 
                location = new Ext.grid.Location(view, cell);
 
                if (cell && !location.equals(me.lastDragLocation)) {
                    me.changeSelectionRange(view, location, e);
                }
            }
        },
 
        stopAutoScroller: function() {
            var me = this;
 
            if (me.scrollTask) {
                me.scrollBy[0] = me.scrollBy[1] = 0;
                me.scrollTask.stop();
                me.scrollTask = null;
            }
        },
 
        /**
         * Clean up mousemove event
         * @param e
         * @private
         */
        onViewDragEnd: function(e) {
            var me = this,
                view = me.getView(),
                dragLocation = me.dragLocation,
                changedCell = !dragLocation || !dragLocation.equals(me.mousedownPosition),
                location = e.location,
                sel;
 
            me.checkCellClicked = null;
 
            me.stopAutoScroller();
 
            if (view && !view.destroyed) {
                // If we catch the event before the View sees it and stamps a position in,
                // we need to know where they mouseupped.
                if (!location) {
                    e.location = new Ext.grid.Location(view, e);
                }
 
                // Disable until a valid new selection is announced in fireSelectionChange
                // unless it's a click
                if (me.getExtensible() && changedCell) {
                    me.getExtensible().disable();
                }
 
                me.mousemoveListener.destroy();
 
                // Copy the records encompassed by the drag range into the record collection
                if ((sel = me.getSelection()) && sel.isRows) {
                    sel.addRange(true);
                }
 
                // Fire selection change only if we have dragged - if the mouseup position
                // is different from the mousedown position.
                // If there has been no drag, the click handler will select the single row
                else if (changedCell) {
                    me.fireSelectionChange();
                }
            }
        },
 
        /**
         * Called when the grid's Navigation model detects navigation events (`mousedown`,
         * `click` and certain `keydown` events).
         * @param {Ext.event.Event} navigateEvent The event which caused navigation.
         * @private
         */
        onNavigate: function(navigateEvent) {
            var me = this,
                view = me.getView(),
                store = me.getStore(),
                selectingRows = me.getRows(),
                selectingCells = me.getCells(),
                selectingColumns = me.getColumns(),
                checkbox = me.getCheckbox(),
                checkboxOnly = me.checkboxOnly,
                mode = me.getMode(),
                location = navigateEvent.to,
                toColumn = location.column,
                record = location.record,
                sel = me.getSelection(),
                ctrlKey = navigateEvent.ctrlKey,
                shiftKey = navigateEvent.shiftKey,
                adding = true,
                isSpace = navigateEvent.getKey() === navigateEvent.SPACE,
                count, changedRow, selectionChanged, selected, lastSelected;
 
            // Honour the stopSelection flag which any prior handlers may set.
            // A SelectionColumn handles its own processing.
            if (navigateEvent.stopSelection || toColumn === me.checkboxColumn) {
                return;
            }
 
            // *key* navigation
            if (!navigateEvent.pointerType && !isSpace) {
                // CTRL/key just navigates, does not select
                if (ctrlKey) {
                    return;
                }
 
                // If within a row and not going to affect cell or column selection, then ignore.
                changedRow = !navigateEvent.from ||
                    (location.recordIndex !== navigateEvent.from.recordIndex);
 
                if (!changedRow && !(selectingCells || selectingColumns)) {
                    return;
                }
            }
 
            // Click is the mouseup at the end of a multi-cell/multi-column select swipe; reject.
            if (sel &&
                (sel.isCells || (sel.isColumns && selectingRows && !(ctrlKey || shiftKey))) &&
                sel.getCount() > 1 && !shiftKey && navigateEvent.type === 'click') {
                return;
            }
 
            // If all selection types are disabled, or it's not a selecting event, return
            if (!(selectingCells || selectingColumns || selectingRows) ||
                !record || navigateEvent.type === 'mousedown') {
                return;
            }
 
            // Ctrl/A key - Deselect current selection, or select all if no selection
            if (ctrlKey && navigateEvent.keyCode === navigateEvent.A && mode === 'multi') {
                // No selection, or only one, select all
                if (!sel || sel.getCount() < 2) {
                    me.selectAll();
                }
                else {
                    me.deselectAll();
                }
 
                me.updateHeaderState();
 
                return;
            }
 
            if (shiftKey && mode === 'multi') {
                // If the event is in one of the row selecting cells, or cell selecting is
                // turned off
                if (toColumn === me.numbererColumn || toColumn === me.checkColumn ||
                    !(selectingCells || selectingColumns) ||
                    (sel && (sel.isRows || sel.isRecords))) {
                    if (selectingRows) {
                        // If checkOnly is set, and we're attempting to select a row outside
                        // of the checkbox column, reject
                        if (toColumn !== checkbox && checkboxOnly) {
                            return;
                        }
 
                        // Ensure selection object is of the correct type
                        sel = me.getSelection('records');
 
                        // First shift
                        if (!sel.getRangeSize()) {
                            if (me.selectionStart == null) {
                                me.selectionStart = location.recordIndex;
                            }
 
                            sel.setRangeStart(me.selectionStart);
                        }
                        else {
                            // To have lastselected index to create range for multiple selection
                            lastSelected = Ext.isEmpty(sel.lastSelectedRecIndx)
                                ? new Ext.grid.Location(view, me.getLastSelected()).recordIndex
                                : sel.lastSelectedRecIndx;
 
                            // Because a range has already been started, and we are shift-selecting,
                            // we need to continue selection from the last selected location
                            sel.setRangeStart(lastSelected);
                        }
 
                        // end the range selection at the current location
                        sel.setRangeEnd(location.recordIndex);
                        selectionChanged = true;
                    }
                }
                // Navigate event in a normal cell
                else {
                    if (selectingCells) {
                        // Ensure selection object is of the correct type
                        sel = me.getSelection('cells');
                        count = sel.getCount();
 
                        // First shift
                        if (!sel.getRangeSize()) {
                            sel.setRangeStart(
                                navigateEvent.from ||
                                new Ext.grid.Location(me.getView(), { record: 0, column: 0 })
                            );
                        }
 
                        sel.setRangeEnd(location);
                        adding = count < sel.getCount();
                        selectionChanged = true;
                    }
                    else if (selectingColumns) {
                        // Ensure selection object is of the correct type
                        sel = me.getSelection('columns');
 
                        if (!sel.getCount()) {
                            sel.setRangeStart(toColumn);
                        }
 
                        sel.setRangeEnd(toColumn);
                        selectionChanged = true;
                    }
                }
            }
            else {
                me.selectionStart = null;
 
                if (sel && (mode !== 'multi' || !ctrlKey) && !isSpace) {
                    sel.clear(true);
                }
 
                // If we are selecting rows and (the event is in one of the row selecting
                // cells or we're *only* selecting rows) then select this row
                if (selectingRows && (toColumn === me.numbererColumn ||
                        toColumn === checkbox || !selectingCells)) {
                    // If checkOnly is set, and we're attempting to select a row outside
                    // of the checkbox column, reject
                    // Also reject if we're navigating by key within the same row.
                    if (toColumn !== checkbox && checkboxOnly || (navigateEvent.keyCode &&
                            navigateEvent.from && record === navigateEvent.from.record)) {
                        return;
                    }
 
                    // Ensure selection object is of the correct type
                    sel = me.getSelection('records');
 
                    if (sel.isSelected(record)) {
                        if (ctrlKey || toColumn === checkbox || me.getDeselectable()) {
                            sel.remove(record);
                            selectionChanged = true;
                        }
                    }
                    else {
                        sel.add(record, ctrlKey || toColumn === checkbox);
                        selectionChanged = true;
                    }
 
                    if (selectionChanged && (selected = sel.getSelected()) && selected.length) {
                        me.selectionStart = store.indexOf(selected.first());
                        sel.setRangeStart(me.selectionStart);
                    }
                }
                // Navigate event in a normal cell
                else {
                    // Prioritize cell selection over column selection
                    if (selectingCells) {
                        // Ensure selection object is of the correct type and cleared.
                        sel = me.getSelection('cells', true);
                        sel.setRangeStart(location);
                        selectionChanged = true;
                    }
                    else if (selectingColumns) {
                        // Ensure selection object is of the correct type
                        sel = me.getSelection('columns');
 
                        if (ctrlKey) {
                            if (sel.isSelected(toColumn)) {
                                sel.remove(toColumn);
                            }
                            else {
                                sel.add(toColumn);
                            }
                        }
                        else {
                            sel.setRangeStart(toColumn);
                        }
 
                        selectionChanged = true;
                    }
                }
            }
 
            // If our configuration allowed selection changes, update check header and fire event
            if (selectionChanged) {
                // Base class reacts to RecordSelection mutating its record Collection
                // It will fire the events and update the checked header state.
                if (!sel.isRecords) {
                    me.fireSelectionChange(null, adding);
                }
            }
 
            me.lastDragLocation = location;
        },
 
        /**
         * Check if given column is currently selected.
         *
         * @param {Ext.grid.column.Column} column 
         * @return {Boolean} 
         * @private
         */
        isColumnSelected: function(column) {
            var me = this,
                sel = me.getSelection(),
                ret = false;
 
            if (sel && sel.isColumns) {
                ret = sel.isSelected(column);
            }
 
            return ret;
        },
 
        /**
         * Returns true if specified cell within specified view is selected
         *
         * Used in {@link Ext.grid.Row} rendering to decide upon cell UI treatment.
         * @param {Number/Ext.grid.Location/Ext.data.Model} row - The Row index/record or
         * {@link Ext.grid.Location the grid Location} to test.
         * @param {Number} column - Column index to test.
         *
         * @return {Boolean} 
         * @private
         */
        isCellSelected: function(row, column) {
            var sel = this.getSelection();
 
            if (sel) {
                if (sel.isColumns) {
                    if (typeof column === 'number') {
                        column = this.getView().getVisibleColumns()[column];
                    }
 
                    return sel.isSelected(column);
                }
 
                if (sel.isCells) {
                    return sel.isSelected(row, column);
                }
 
                // We're selecting records or rows.
                // The cell is selected if the record is.
                return sel.isSelected(row);
            }
 
            return false;
        },
 
        /**
         * @private
         */
        updateSelection: function(selection, oldSelection) {
            var view = this.getView();
 
            // Destroy old selection.
            Ext.destroy(oldSelection);
 
            // Update the UI to match the new selection
            if (selection && selection.getCount()) {
                view = selection.view;
 
                // Rows; update each selection row
                if (selection.isRows) {
                    selection.eachRow(view.onRowSelect, view);
                }
                // Columns; update the selection columns for all rows
                else if (selection.isColumns) {
                    selection.eachCell(view.onCellSelect, view);
                }
                // Cells; update each selection cell
                else if (selection.isCells) {
                    selection.eachCell(view.onCellSelect, view);
                }
            }
        },
 
        /**
         * Show/hide the extra column headers depending upon rowSelection.
         * @private
         */
        updateRows: function(rows) {
            var sel;
 
            if (!rows) {
                // checkboxSelect depends on rowsSelect
                this.setCheckbox(false);
 
                sel = this.getSelection();
 
                if (sel && sel.isRows) {
                    sel.clear();
                }
            }
        },
 
        /**
         * Enable/disable the HeaderContainer's sortOnClick in line with column select on
         * column click.
         * @private
         */
        updateColumns: function(columns) {
            var me = this,
                view = me.getView(),
                sel = me.getSelection();
 
            if (!columns && sel && sel.isColumns) {
                sel.clear();
                me.fireSelectionChange();
            }
 
            view.toggleCls(me.columnSelectCls, !!columns);
        },
 
        /**
         * @private
         */
        updateCells: function(cells) {
            var me = this,
                sel = me.getSelection();
 
            if (!cells && sel && sel.isCells) {
                sel.clear();
                me.fireSelectionChange();
            }
        },
 
        updateMode: function(mode) {
            // If multi, we can use drag or not, so revert to initial value
            if (mode === 'multi') {
                this.setDrag(this.getInitialConfig().drag);
            }
            // With 'simple' or 'single', drag makes no sense
            else if (!this.isConfiguring) {
                this.setDrag(false);
            }
        },
 
        /**
         * @private
         * @param {Ext.data.Model[]} records. *ONLY* passed if called from the base class's
         * onCollectionAdd/Remove observers on the *record* collection.
         * @param {Boolean} selecting. *ONLY* passed if called from the base class's
         * onCollectionAdd/Remove observers on the *record* collection.
         */
        fireSelectionChange: function(records, selecting) {
            var me = this,
                view = me.getView(),
                sel = me.getSelection();
 
            // Inform selection object that we're done
            me.updateSelectionExtender();
 
            // Our own event
            me.fireEvent('selectionchange', view, me.getSelection());
 
            if (sel.isCells) {
                view.fireEvent('selectionchange', view, sel.getRange(), selecting, sel);
            }
            else {
                // Fire Grid's selectionchange event.
                // Only pass records if the selection type can yield them
                view.fireEvent('selectionchange', view,
                               sel.isRecords ? records : (sel.isCells ? sel.getRecords() : null),
                               selecting, me.getSelection());
            }
        },
 
        updateSelectionExtender: function() {
            var sel = this.getSelection();
 
            if (sel) {
                sel.onSelectionFinish();
            }
        },
 
        /**
         * Called when a selection has been made. The selection object's onSelectionFinish
         * calls back into this.
         * @param {Ext.dataview.selection.Selection} sel The selection object specific to
         * the selection performed.
         * @param {Ext.grid.Location} [firstCell] The left/top most selected cell.
         * Will be undefined if the selection is clear.
         * @param {Ext.grid.Location} [lastCell] The bottom/right most selected cell.
         * Will be undefined if the selection is clear.
         * @private
         */
        onSelectionFinish: function(sel, firstCell, lastCell) {
            var extensible = this.getExtensible();
 
            if (extensible) {
                extensible.setHandle(firstCell, lastCell);
            }
        },
 
        applyExtensible: function(extensible, oldExtensible) {
            var me = this,
                axes;
 
            // if extensible is false/null we should return undefined so the value
            // does not get set and we don't call updateExtensible
            if (!extensible) {
                return undefined;
            }
 
            if (extensible === true || typeof extensible === 'string') {
                axes = me.axesConfigs[extensible];
 
                // if we already have an extensible, just update it's config
                if (oldExtensible) {
                    oldExtensible.setAxes(axes);
 
                    return oldExtensible;
                }
 
                extensible = {
                    axes: axes
                };
            }
            else {
                extensible = Ext.Object.chain(extensible); // don't mutate the user's config
            }
 
            extensible.allowReduceSelection = me.getReducible();
            extensible.view = me.getView();
 
            // if this wasn't a simple axes update, destroy the old extensible
            // so we don't end up with multiple extensibles on the view.
            if (oldExtensible) {
                oldExtensible.destroy();
            }
 
            return new Ext.grid.selection.SelectionExtender(extensible);
        },
 
        applyReducible: function(reducible) {
            return !!reducible;
        },
 
        updateReducible: function(reducible) {
            var extensible;
 
            extensible = this.getConfig('extensible', null, true);
 
            if (extensible) {
                extensible.allowReduceSelection = reducible;
            }
        },
 
        applyCheckbox: function(checkbox) {
            var me = this;
 
            if (checkbox) {
                me.checkboxOnly = checkbox === 'only';
                me.checkboxColumn = checkbox = Ext.create(
                    me.createCheckboxColumn(me.getCheckboxDefaults())
                );
            }
 
            return checkbox;
        },
 
        updateCheckbox: function(checkbox, oldCheckbox) {
            var me = this,
                view;
 
            if (!me.isConfiguring) {
                view = me.getView();
 
                if (oldCheckbox) {
                    view.unregisterColumn(oldCheckbox, true);
                }
 
                if (checkbox) {
                    view.registerColumn(checkbox);
                    // rows selection is required so force it
                    me.setRows(true);
                }
            }
        },
 
        applyView: function(view) {
            // In a locking assembly, we talk to the owner
            return view.ownerGrid;
        },
 
        /**
         * Called when the SelectionExtender has the mouse released.
         * @param {Object} extension An object describing the type and size of extension.
         * @param {String} extension.type `"rows"` or `"columns"`
         * @param {Ext.grid.Location} extension.start The start (top left) cell of the
         * extension area.
         * @param {Ext.grid.Location} extension.end The end (bottom right) cell of the
         * extension area.
         * @param {number} [extension.columns] The number of columns extended (-ve means
         * on the left side).
         * @param {number} [extension.rows] The number of rows extended (-ve means on the
         * top side).
         * @private
         */
        extendSelection: function(extension) {
            var me = this,
                view = me.getView(),
                sel = me.getSelection(),
                action = extension.reduce ? 'reduce' : 'extend';
 
            // Announce that the selection is to be extended, and if no objections, extend it
            if (view.fireEvent('beforeselectionextend', view, sel, extension) !== false) {
                sel[action + 'Range'](extension);
 
                // Base class reacts to RowSelection mutating its record Collection
                // It will fire the events.
                if (!sel.isRows) {
                    me.fireSelectionChange();
                }
            }
        },
 
        /**
         * @private
         */
        onIdChanged: function(store, rec, oldId, newId) {
            var sel = this.getSelection();
 
            if (sel && sel.isRecords) {
                sel.getSelected().updateKey(rec, oldId);
            }
        },
 
        /**
         * @private
         */
        onSelectionStoreAdd: function() {
            this.callParent(arguments);
            this.updateHeaderState();
        },
 
        /**
         * @private
         */
        onSelectionStoreClear: function() {
            this.callParent(arguments);
            this.updateHeaderState();
        },
 
        /**
         * @private
         */
        onSelectionStoreLoad: function() {
            this.callParent(arguments);
            this.updateHeaderState();
        }
    }
 
}, function(GridModel) {
    var RowNumberer = Ext.ClassManager.get('Ext.grid.column.RowNumberer'),
        cellCls;
 
    if (RowNumberer) {
        cellCls = Ext.grid.column.RowNumberer.prototype.cellCls;
        // This class adds the e-resize cursor on hover to indicate availability of selection
        GridModel.prototype.rowNumbererCellCls =
            (cellCls ? (cellCls + ' ') : '') + Ext.baseCSSPrefix + 'selmodel-row-numberer-cell';
    }
});