    /**
     * This class encapsulates the user interface for a tabular data set.
     * It acts as a centralized manager for controlling the various interface
     * elements of the view. This includes handling events, such as row and cell
     * level based DOM events. It also reacts to events from the underlying {@link Ext.selection.Model}
     * to provide visual feedback to the user.
     *
     * This class does not provide ways to manipulate the underlying data of the configured
     * {@link Ext.data.Store}.
     *
     * This is the base class for both {@link Ext.grid.View} and {@link Ext.tree.View} and is not
     * to be used directly.
     */
    Ext.define('Ext.view.Table', {
        extend: 'Ext.view.View',
        alias: 'widget.tableview',
        requires: [
            'Ext.view.TableLayout',
            'Ext.view.NodeCache',
            'Ext.util.DelayedTask',
            'Ext.util.MixedCollection'
        ],

        componentLayout: 'tableview',

        baseCls: Ext.baseCSSPrefix + 'grid-view',

        /**
         * @cfg {String} [firstCls='x-grid-cell-first']
         * A CSS class to add to the *first* cell in every row to enable special styling for the first column.
         * If no styling is needed on the first column, this may be configured as `null`.
         */
        firstCls: Ext.baseCSSPrefix + 'grid-cell-first',

        /**
         * @cfg {String} [lastCls='x-grid-cell-last']
         * A CSS class to add to the *last* cell in every row to enable special styling for the last column.
         * If no styling is needed on the last column, this may be configured as `null`.
         */
        lastCls: Ext.baseCSSPrefix + 'grid-cell-last',

        headerRowSelector: 'tr.' + Ext.baseCSSPrefix + 'grid-header-row',

        selectedItemCls: Ext.baseCSSPrefix + 'grid-row-selected',
        selectedCellCls: Ext.baseCSSPrefix + 'grid-cell-selected',
        focusedItemCls: Ext.baseCSSPrefix + 'grid-row-focused',
        overItemCls: Ext.baseCSSPrefix + 'grid-row-over',
        altRowCls:   Ext.baseCSSPrefix + 'grid-row-alt',
        rowClsRe: new RegExp('(?:^|\\s*)' + Ext.baseCSSPrefix + 'grid-row-(first|last|alt)(?:\\s+|$)', 'g'),
        cellRe: new RegExp(Ext.baseCSSPrefix + 'grid-cell-([^\\s]+) ', ''),
        positionBody: true,

        // cfg docs inherited
        trackOver: true,

        /**
         * Override this function to apply custom CSS classes to rows during rendering. This function should return the
         * CSS class name (or empty string '' for none) that will be added to the row's wrapping div. To apply multiple
         * class names, simply return them space-delimited within the string (e.g. 'my-class another-class').
         * Example usage:
         *
         *     viewConfig: {
         *         getRowClass: function(record, rowIndex, rowParams, store){
         *             return record.get("valid") ? "row-valid" : "row-error";
         *         }
         *     }
         *
         * @param {Ext.data.Model} record The record corresponding to the current row.
         * @param {Number} index The row index.
         * @param {Object} rowParams **DEPRECATED.** For row body use the
         * {@link Ext.grid.feature.RowBody#getAdditionalData getAdditionalData} method of the rowbody feature.
         * @param {Ext.data.Store} store The store this grid is bound to
         * @return {String} a CSS class name to add to the row.
         * @method
         */
        getRowClass: null,

        /**
         * @cfg {Boolean} stripeRows
         * True to stripe the rows.
         *
         * This causes the CSS class **`x-grid-row-alt`** to be added to alternate rows of
         * the grid. A default CSS rule is provided which sets a background color, but you can override this
         * with a rule which either overrides the **background-color** style using the `!important`
         * modifier, or which uses a CSS selector of higher specificity.
         */
        stripeRows: true,

        /**
         * @cfg {Boolean} markDirty
         * True to show the dirty cell indicator when a cell has been modified.
         */
        markDirty : true,

        /**
         * @cfg {Boolean} enableTextSelection
         * True to enable text selections.
         */

        /**
         * @private
         * Simple initial tpl for TableView just to satisfy the validation within AbstractView.initComponent.
         */
        tpl: '{%values.view.tableTpl.applyOut(values, out)%}',

        tableTpl: [
            '<table id="{view.id}-table" class="' + Ext.baseCSSPrefix + '{view.id}-table ' + Ext.baseCSSPrefix + 'grid-table" border="0" cellspacing="0" cellpadding="0" style="{tableStyle}">',
                '{[values.view.renderColumnSizer(out)]}',
                '{[this.renderTHead(values, out)]}',
                '{[this.renderTFoot(values, out)]}',
                '<tbody id="{view.id}-body">',
                '{%',
                    'values.view.renderRows(values.rows, values.viewStartIndex, out);',
                '%}',
                '</tbody>',
            '</table>',
            {
                priority: 0,
                renderTHead: Ext.emptyFn,
                renderTFoot: Ext.emptyFn
            }
        ],

        rowTpl: [
            '{%',
                'var dataRowCls = values.recordIndex === -1 ? "" : " ' + Ext.baseCSSPrefix + 'grid-data-row";',
            '%}',
            '<tr {[values.rowId ? ("id=\\"" + values.rowId + "\\"") : ""]} ',
                'data-boundView="{view.id}" ',
                'data-recordId="{record.internalId}" ',
                'data-recordIndex="{recordIndex}" ',
                'class="{[values.itemClasses.join(" ")]} {[values.rowClasses.join(" ")]}{[dataRowCls]}" ',
                '{rowAttr:attributes}>',
                '<tpl for="columns">' +
                    '{%',
                        'parent.view.renderCell(values, parent.record, parent.recordIndex, xindex - 1, out, parent)',
                     '%}',
                '</tpl>',
            '</tr>',
            {
                priority: 0
            }
        ],

        cellTpl: [
            '<td class="{tdCls}" {tdAttr}>',
                '<div {unselectableAttr} class="' + Ext.baseCSSPrefix + 'grid-cell-inner"',
                    'style="text-align:{align};<tpl if="style">{style}</tpl>">{value}</div>',
            '</td>', {
                priority: 0
            }
        ],

        /**
         * @private
         * Flag to disable refreshing SelectionModel on view refresh. Table views render rows with selected CSS class already added if necessary.
         */
        refreshSelmodelOnRefresh: false,

        tableValues: {},

        // Private properties used during the row and cell render process.
        // They are allocated here on the prototype, and cleared/re-sued to avoid GC churn during repeated rendering.
        rowValues: {
            itemClasses: [],
            rowClasses: []
        },
        cellValues: {
            classes: []
        },

        /// Private used for buffered rendering
        renderBuffer: document.createElement('div'),

        initComponent: function() {
            var me = this,
                scroll = me.scroll;

            this.addEvents(
                /**
                 * @event beforecellclick
                 * Fired before the cell click is processed. Return false to cancel the default action.
                 * @param {Ext.view.Table} this
                 * @param {HTMLElement} td The TD element for the cell.
                 * @param {Number} cellIndex
                 * @param {Ext.data.Model} record
                 * @param {HTMLElement} tr The TR element for the cell.
                 * @param {Number} rowIndex
                 * @param {Ext.EventObject} e
                 */
                'beforecellclick',
                /**
                 * @event cellclick
                 * Fired when table cell is clicked.
                 * @param {Ext.view.Table} this
                 * @param {HTMLElement} td The TD element for the cell.
                 * @param {Number} cellIndex
                 * @param {Ext.data.Model} record
                 * @param {HTMLElement} tr The TR element for the cell.
                 * @param {Number} rowIndex
                 * @param {Ext.EventObject} e
                 */
                'cellclick',
                /**
                 * @event beforecelldblclick
                 * Fired before the cell double click is processed. Return false to cancel the default action.
                 * @param {Ext.view.Table} this
                 * @param {HTMLElement} td The TD element for the cell.
                 * @param {Number} cellIndex
                 * @param {Ext.data.Model} record
                 * @param {HTMLElement} tr The TR element for the cell.
                 * @param {Number} rowIndex
                 * @param {Ext.EventObject} e
                 */
                'beforecelldblclick',
                /**
                 * @event celldblclick
                 * Fired when table cell is double clicked.
                 * @param {Ext.view.Table} this
                 * @param {HTMLElement} td The TD element for the cell.
                 * @param {Number} cellIndex
                 * @param {Ext.data.Model} record
                 * @param {HTMLElement} tr The TR element for the cell.
                 * @param {Number} rowIndex
                 * @param {Ext.EventObject} e
                 */
                'celldblclick',
                /**
                 * @event beforecellcontextmenu
                 * Fired before the cell right click is processed. Return false to cancel the default action.
                 * @param {Ext.view.Table} this
                 * @param {HTMLElement} td The TD element for the cell.
                 * @param {Number} cellIndex
                 * @param {Ext.data.Model} record
                 * @param {HTMLElement} tr The TR element for the cell.
                 * @param {Number} rowIndex
                 * @param {Ext.EventObject} e
                 */
                'beforecellcontextmenu',
                /**
                 * @event cellcontextmenu
                 * Fired when table cell is right clicked.
                 * @param {Ext.view.Table} this
                 * @param {HTMLElement} td The TD element for the cell.
                 * @param {Number} cellIndex
                 * @param {Ext.data.Model} record
                 * @param {HTMLElement} tr The TR element for the cell.
                 * @param {Number} rowIndex
                 * @param {Ext.EventObject} e
                 */
                'cellcontextmenu',
                /**
                 * @event beforecellmousedown
                 * Fired before the cell mouse down is processed. Return false to cancel the default action.
                 * @param {Ext.view.Table} this
                 * @param {HTMLElement} td The TD element for the cell.
                 * @param {Number} cellIndex
                 * @param {Ext.data.Model} record
                 * @param {HTMLElement} tr The TR element for the cell.
                 * @param {Number} rowIndex
                 * @param {Ext.EventObject} e
                 */
                'beforecellmousedown',
                /**
                 * @event cellmousedown
                 * Fired when the mousedown event is captured on the cell.
                 * @param {Ext.view.Table} this
                 * @param {HTMLElement} td The TD element for the cell.
                 * @param {Number} cellIndex
                 * @param {Ext.data.Model} record
                 * @param {HTMLElement} tr The TR element for the cell.
                 * @param {Number} rowIndex
                 * @param {Ext.EventObject} e
                 */
                'cellmousedown',
                /**
                 * @event beforecellmouseup
                 * Fired before the cell mouse up is processed. Return false to cancel the default action.
                 * @param {Ext.view.Table} this
                 * @param {HTMLElement} td The TD element for the cell.
                 * @param {Number} cellIndex
                 * @param {Ext.data.Model} record
                 * @param {HTMLElement} tr The TR element for the cell.
                 * @param {Number} rowIndex
                 * @param {Ext.EventObject} e
                 */
                'beforecellmouseup',
                /**
                 * @event cellmouseup
                 * Fired when the mouseup event is captured on the cell.
                 * @param {Ext.view.Table} this
                 * @param {HTMLElement} td The TD element for the cell.
                 * @param {Number} cellIndex
                 * @param {Ext.data.Model} record
                 * @param {HTMLElement} tr The TR element for the cell.
                 * @param {Number} rowIndex
                 * @param {Ext.EventObject} e
                 */
                'cellmouseup',
                /**
                 * @event beforecellkeydown
                 * Fired before the cell key down is processed. Return false to cancel the default action.
                 * @param {Ext.view.Table} this
                 * @param {HTMLElement} td The TD element for the cell.
                 * @param {Number} cellIndex
                 * @param {Ext.data.Model} record
                 * @param {HTMLElement} tr The TR element for the cell.
                 * @param {Number} rowIndex
                 * @param {Ext.EventObject} e
                 */
                'beforecellkeydown',
                /**
                 * @event cellkeydown
                 * Fired when the keydown event is captured on the cell.
                 * @param {Ext.view.Table} this
                 * @param {HTMLElement} td The TD element for the cell.
                 * @param {Number} cellIndex
                 * @param {Ext.data.Model} record
                 * @param {HTMLElement} tr The TR element for the cell.
                 * @param {Number} rowIndex
                 * @param {Ext.EventObject} e
                 */
                'cellkeydown'
            );

            /**
             * @private
             * @property {Ext.dom.AbstractElement.Fly} body
             * A flyweight Ext.Element which encapsulates a reference to the view's main row containing element.
             * *Note that the `dom` reference will not be present until the first data refresh*
             */
            me.body = new Ext.dom.Element.Fly();
            me.body.id = me.id + 'gridBody';

            // Scrolling within a TableView is controlled by the scroll config of its owning GridPanel
            // It must see undefined in this property in order to leave the scroll styles alone at afterRender time
            me.autoScroll = undefined;

            // If trackOver has been turned off, null out the overCls because documented behaviour
            // in AbstractView is to turn trackOver on if overItemCls is set.
            if (!me.trackOver) {
                me.overItemCls = null;
            }

            // Convert grid scroll config to standard Component scrolling configurations.
            if (scroll === true || scroll === 'both') {
                me.autoScroll = true;
            } else if (scroll === 'horizontal') {
                me.overflowX = 'auto';
            } else if (scroll === 'vertical') {
                me.overflowY = 'auto';
            }
            me.selModel.view = me;
            me.headerCt.view = me;
            me.headerCt.markDirty = me.markDirty;

            // Features need a reference to the grid.
            // Grid needs an immediate reference to its view so that the view cabn reliably be got from the grid during initialization
            me.grid.view = me;
            me.initFeatures(me.grid);
            delete me.grid;

            // The real tpl is generated, but AbstractView.initComponent insists upon the presence of a fully instantiated XTemplate at construction time.
            me.tpl = me.getTpl('tpl');
            me.itemSelector = me.getItemSelector();
            me.all = new Ext.view.NodeCache(me);
            me.callParent();
        },

        /**
         * @private
         * Move a grid column from one position to another
         * @param {Number} fromIdx The index from which to move columns
         * @param {Number} toIdx The index at which to insert columns.
         * @param {Number} [colsToMove=1] The number of columns to move beginning at the `fromIdx`
         */
        moveColumn: function(fromIdx, toIdx, colsToMove) {
            var me = this,
                fragment = (colsToMove > 1) ? document.createDocumentFragment() : undefined,
                destinationCellIdx = toIdx,
                colCount = me.getGridColumns().length,
                lastIndex = colCount - 1,
                doFirstLastClasses = (me.firstCls || me.lastCls) && (toIdx === 0 || toIdx == colCount || fromIdx === 0 || fromIdx == lastIndex),
                i,
                j,
                rows, len, tr, cells,
                tables;

            // Dragging between locked and unlocked side first refreshes the view, and calls onHeaderMoved with
            // fromIndex and toIndex the same.
            if (me.rendered && toIdx !== fromIdx) {
                // Grab all rows which have column cells in.
                // That is data rows and column sizing rows.
                rows = me.el.query(me.getDataRowSelector());

                if (toIdx > fromIdx && fragment) {
                    destinationCellIdx -= colsToMove;
                }

                for (i = 0, len = rows.length; i < len; i++) {
                    tr = rows[i];
                    cells = tr.childNodes;

                    // Keep first cell class and last cell class correct *only if needed*
                    if (doFirstLastClasses) {

                        if (cells.length === 1) {
                            Ext.fly(cells[0]).addCls(me.firstCls);
                            Ext.fly(cells[0]).addCls(me.lastCls);
                            continue;
                        }
                        if (fromIdx === 0) {
                            Ext.fly(cells[0]).removeCls(me.firstCls);
                            Ext.fly(cells[1]).addCls(me.firstCls);
                        } else if (fromIdx === lastIndex) {
                            Ext.fly(cells[lastIndex]).removeCls(me.lastCls);
                            Ext.fly(cells[lastIndex - 1]).addCls(me.lastCls);
                        }
                        if (toIdx === 0) {
                            Ext.fly(cells[0]).removeCls(me.firstCls);
                            Ext.fly(cells[fromIdx]).addCls(me.firstCls);
                        } else if (toIdx === colCount) {
                            Ext.fly(cells[lastIndex]).removeCls(me.lastCls);
                            Ext.fly(cells[fromIdx]).addCls(me.lastCls);
                        }
                    }

                    if (fragment) {
                        for (j = 0; j < colsToMove; j++) {
                            fragment.appendChild(cells[fromIdx]);
                        }
                        tr.insertBefore(fragment, cells[destinationCellIdx] || null);
                    } else {
                        tr.insertBefore(cells[fromIdx], cells[destinationCellIdx] || null);
                    }
                }

                // Shuffle the <colgroup> elements at the ta=op of all <tables> in the grid
                tables = me.el.query(me.getBodySelector());
                for (i = 0, len = tables.length; i < len; i++) {
                    tr = tables[i];
                    if (fragment) {
                        for (j = 0; j < colsToMove; j++) {
                            fragment.appendChild(tr.childNodes[fromIdx]);
                        }
                        tr.insertBefore(fragment, tr.childNodes[destinationCellIdx] || null);
                    } else {
                        tr.insertBefore(tr.childNodes[fromIdx], tr.childNodes[destinationCellIdx] || null);
                    }
                }
            }
        },

        // scroll the view to the top
        scrollToTop: Ext.emptyFn,

        /**
         * Add a listener to the main view element. It will be destroyed with the view.
         * @private
         */
        addElListener: function(eventName, fn, scope){
            this.mon(this, eventName, fn, scope, {
                element: 'el'
            });
        },

        /**
         * Get the columns used for generating a template via TableChunker.
         * See {@link Ext.grid.header.Container#getGridColumns}.
         * @private
         */
        getGridColumns: function() {
            return this.headerCt.getGridColumns();
        },

        /**
         * Get a leaf level header by index regardless of what the nesting
         * structure is.
         * @private
         * @param {Number} index The index
         */
        getHeaderAtIndex: function(index) {
            return this.headerCt.getHeaderAtIndex(index);
        },

        /**
         * Get the cell (td) for a particular record and column.
         * @param {Ext.data.Model} record
         * @param {Ext.grid.column.Column} column
         * @private
         */
        getCell: function(record, column) {
            var row = this.getNode(record, true);
            return Ext.fly(row).down(column.getCellSelector());
        },

        /**
         * Get a reference to a feature
         * @param {String} id The id of the feature
         * @return {Ext.grid.feature.Feature} The feature. Undefined if not found
         */
        getFeature: function(id) {
            var features = this.featuresMC;
            if (features) {
                return features.get(id);
            }
        },

        // @private
        // Finds a features by ftype in the features array
        findFeature: function(ftype) {
            if (this.features) {
                return Ext.Array.findBy(this.features, function(feature) {
                    if (feature.ftype === ftype) {
                        return true;
                    }
                });
            }
        },

        /**
         * Initializes each feature and bind it to this view.
         * @private
         */
        initFeatures: function(grid) {
            var me = this,
                i,
                features,
                feature,
                len;

            me.tableTpl = Ext.XTemplate.getTpl(this, 'tableTpl');
            me.rowTpl   = Ext.XTemplate.getTpl(this, 'rowTpl');
            me.cellTpl  = Ext.XTemplate.getTpl(this, 'cellTpl');

            me.featuresMC = new Ext.util.MixedCollection();
            features = me.features = me.constructFeatures();
            len = features ? features.length : 0;
            for (i = 0; i < len; i++) {
                feature = features[i];

                // inject a reference to view and grid - Features need both
                feature.view = me;
                feature.grid = grid;
                me.featuresMC.add(feature);
                feature.init(grid);
            }
        },

        addTableTpl: function(newTpl) {
            return this.addTpl('tableTpl', newTpl);
        },

        addRowTpl: function(newTpl) {
            return this.addTpl('rowTpl', newTpl);
        },

        addCellTpl: function(newTpl) {
            return this.addTpl('cellTpl', newTpl);
        },

        addTpl: function(which, newTpl) {
            var me = this,
                tpl,
                prevTpl;

            newTpl = Ext.Object.chain(newTpl);

            // If we have been passed an object of the form
            // {
            //      before: fn
            //      after: fn
            // }
            if (!newTpl.isTemplate) {
                newTpl.applyOut = me.tplApplyOut;
            }

            // Stop at the first TPL who's priority is less than the passed rowTpl
            for (tpl = me[which]; newTpl.priority < tpl.priority; tpl = tpl.nextTpl) {
                prevTpl = tpl;
            }

            // If we had skipped over some, link the previous one to the passed rowTpl
            if (prevTpl) {
                prevTpl.nextTpl = newTpl;
            }
            // First one
            else {
                me[which] = newTpl;
            }
            newTpl.nextTpl = tpl;
            return newTpl;
        },

        tplApplyOut: function(values, out) {
            if (this.before) {
                if (this.before(values, out) === false) {
                    return;
                }
            }
            this.nextTpl.applyOut(values, out);
            if (this.after) {
                this.after(values, out);
            }
        },

        /**
         * @private
         * Converts the features array as configured, into an array of instantiated Feature objects.
         *
         * Must have no side effects other than Feature instantiation.
         *
         * MUST NOT update the this.features property, and MUST NOT update the instantiated Features.
         */
        constructFeatures: function() {
            var me = this,
                features = me.features,
                feature,
                result,
                i = 0, len;

            if (features) {
                result = [];
                len = features.length;
                for (; i < len; i++) {
                    feature = features[i];
                    if (!feature.isFeature) {
                        feature = Ext.create('feature.' + feature.ftype, feature);
                    }
                    result[i] = feature;
                }
            }
            return result;
        },

        beforeRender: function() {
            var me = this;
            me.callParent();

            if (!me.enableTextSelection) {
                me.protoEl.unselectable();
            }
        },

        // Private template method implemented starting at the AbstractView class.
        onViewScroll: function(e, t) {
            this.callParent(arguments);
            this.fireEvent('bodyscroll', e, t);
        },

        // private
        // Create the DOM element which enapsulates the passed record.
        // Used when updating existing rows, so drills down into resulting structure .
        createRowElement: function(record, index) {
            var me = this,
                div = me.renderBuffer;

            me.tpl.overwrite(div, me.collectData([record], index));
            // Return first element within node containing element
            return Ext.fly(div).down(me.getNodeContainerSelector(), true).firstChild;
        },

        // private
        // Override so that we can use a quicker way to access the row nodes.
        // They are simply all child nodes of the TBODY element.
        bufferRender: function(records, index) {
            var me = this,
                div = me.renderBuffer;

            me.tpl.overwrite(div, me.collectData(records, index));
            return Ext.Array.toArray(Ext.fly(div).down(me.getNodeContainerSelector(), true).childNodes);
        },

        collectData: function(records, startIndex) {
            this.rowValues.view = this;

            return {
                view: this,
                rows: records,
                viewStartIndex: startIndex,
                tableStyle: this.bufferedRenderer ? ('position:absolute;top:' + this.bufferedRenderer.bodyTop) : ''
            };
        },

        // Overridden implementation.
        // Called by refresh to collect the view item nodes.
        // Note that these may be wrapping rows which *contain* rows which map to records
        collectNodes: function(targetEl) {
            this.all.fill(this.getNodeContainer().childNodes, this.all.startIndex);
        },

        // Private. Called when the table changes height.
        // For example, see examples/grid/group-summary-grid.html
        // If we have flexed column headers, we need to update the header layout
        // because it may have to accommodate (or cease to accommodate) a vertical scrollbar.
        // Only do this on platforms which have a space-consuming scrollbar.
        // Only do it when vertical scrolling is enabled.
        refreshSize: function() {
            var me = this,
                grid,
                bodySelector = me.getBodySelector();

            // On every update of the layout system due to data update, capture the view's main element in our private flyweight.
            // IF there *is* a main element. Some TplFactories emit naked rows.
            if (bodySelector) {
                me.body.attach(me.el.child(bodySelector, true));
            }

            if (!me.hasLoadingHeight) {
                grid = me.up('tablepanel');

                // Suspend layouts in case the superclass requests a layout. We might too, so they
                // must be coalescsed.
                Ext.suspendLayouts();

                me.callParent();

                // Since columns and tables are not sized by generated CSS rules any more, EVERY table refresh
                // has to be followed by a layout to ensure correct table and column sizing.
                grid.updateLayout();

                Ext.resumeLayouts(true);
            }
        },

        statics: {
            getBoundView: function(node) {
                return Ext.getCmp(node.getAttribute('data-boundView'));
            }
        },

        getRecord: function(node) {
            node = this.getNode(node);
            if (node) {
                var recordIndex = node.getAttribute('data-recordIndex');
                if (recordIndex) {
                    recordIndex = parseInt(recordIndex, 10);
                    if (recordIndex > -1) {
                        // The index is the index in the original Store, not in a GroupStore
                        // The Grouping Feature increments the index to skip over unrendered records in collapsed groups
                        return this.store.data.getAt(recordIndex);
                    }
                }
                return this.dataSource.data.get(node.getAttribute('data-recordId'));
            }
        },

        indexOf: function(node) {
            node = this.getNode(node, false);
            if (!node && node !== 0) {
                return -1;
            }
            return this.all.indexOf(node);
        },

        indexInStore: function(node) {
            node = this.getNode(node, true);
            if (!node && node !== 0) {
                return -1;
            }
            var recordIndex = node.getAttribute('data-recordIndex');
            if (recordIndex) {
                return parseInt(recordIndex, 10);
            }
            return this.dataSource.indexOf(this.getRecord(node));
        },

        renderRows: function(rows, viewStartIndex, out) {
            var rowValues = this.rowValues,
                rowCount = rows.length,
                headerCt = this.getHeaderCt(),
                i;

            rowValues.view = this;
            rowValues.columns = headerCt.getGridColumns(true);
            rowValues.visibleColumns = headerCt.getVisibleGridColumns();

            for (i = 0; i < rowCount; i++, viewStartIndex++) {
                rowValues.itemClasses.length = rowValues.rowClasses.length = 0;
                this.renderRow(rows[i], viewStartIndex, out);
            }

            // Dereference objects since rowValues is a persistent on our prototype
            rowValues.view = rowValues.columns = rowValues.visibleColumns = rowValues.record = null;
        },

        /* Alternative column sizer element renderer.
        renderTHeadColumnSizer: function(out) {
            var columns = this.getGridColumns(),
                len = columns.length, i,
                column, width;

            out.push('<thead><tr class="' + Ext.baseCSSPrefix + 'grid-header-row">');
            for (i = 0; i < len; i++) {
                column = columns[i];
                width = column.hidden ? 0 : (column.lastBox ? column.lastBox.width : Ext.grid.header.Container.prototype.defaultWidth);
                out.push('<th class="', Ext.baseCSSPrefix, 'grid-cell-', columns[i].getItemId(), '" style="width:' + width + 'px"></th>');
            }
            out.push('</tr></thead>');
        },
        */

        renderColumnSizer: function(out) {
            var columns = this.getGridColumns(),
                len = columns.length, i,
                column, width;

            for (i = 0; i < len; i++) {
                column = columns[i];
                width = column.hidden ? 0 : (column.lastBox ? column.lastBox.width : Ext.grid.header.Container.prototype.defaultWidth);
                out.push('<colgroup><col class="', Ext.baseCSSPrefix, 'grid-cell-', columns[i].getItemId(), '" style="width:' + width + 'px"></colgroup>');
            }
        },

        /**
         * @private
         * Renders the HTML markup string for a single row into the passed array as a sequence of strings, or
         * returns the HTML markup for a single row.
         *
         * @param {Ext.data.Model} record The record to render.
         * @param {String[]} [out] A string array onto which to append the resulting HTML string. If omitted,
         * the resulting HTML string is returned.
         * @return {String} **only when the out parameter is omitted** The resulting HTML string.
         */
        renderRow: function(record, rowIdx, out) {
            var me = this,
                isMetadataRecord = rowIdx === -1,
                selModel = me.selModel,
                rowValues = me.rowValues,
                itemClasses = rowValues.itemClasses,
                rowClasses = rowValues.rowClasses,
                cls,
                rowTpl = me.rowTpl;

            // Set up mandatory properties on rowValues
            rowValues.record = record;
            rowValues.recordId = record.internalId;
            rowValues.recordIndex = rowIdx;
            rowValues.rowId = me.getRowId(record);
            rowValues.itemCls = rowValues.rowCls = '';
            if (!rowValues.columns) {
                rowValues.columns = me.headerCt.getGridColumns(true);
            }

            itemClasses.length = rowClasses.length = 0;

            // If it's a metadata record such as a summary record.
            // So do not decorate it with the regular CSS.
            // The Feature which renders it must know how to decorate it.
            if (!isMetadataRecord) {
                itemClasses[0] = Ext.baseCSSPrefix + "grid-row";
                if (selModel && selModel.isRowSelected && selModel.isRowSelected(record, rowIdx)) {
                    itemClasses.push(me.selectedItemCls);
                }

                if (me.stripeRows && rowIdx % 2 !== 0) {
                    rowClasses.push(me.altRowCls);
                }

                if (me.getRowClass) {
                    cls = me.getRowClass(record, rowIdx, null, me.dataSource);
                    if (cls) {
                        rowClasses.push(cls);
                    }
                }
            }

            if (out) {
                rowTpl.applyOut(rowValues, out);
            } else {
                return rowTpl.apply(rowValues);
            }
        },

        /**
         * @private
         * Emits the HTML representing a single grid cell into the passed output stream (which is an array of strings).
         *
         * @param {Ext.grid.column.Column} column The column definition for which to render a cell.
         * @param {Number} recordIndex The row index (zero based within the {@link #store}) for which to render the cell.
         * @param {Number} columnIndex The column index (zero based) for which to render the cell.
         * @param {String[]} out The output stream into which the HTML strings are appended.
         */
        renderCell: function(column, record, recordIndex, columnIndex, out) {
            var me = this,
                selModel = me.selModel,
                cellValues = me.cellValues,
                classes = cellValues.classes,
                fieldValue = record.data[column.dataIndex],
                cellTpl = me.cellTpl,
                value;

            cellValues.record = record;
            cellValues.column = column;
            cellValues.recordIndex = recordIndex;
            cellValues.columnIndex = columnIndex;
            cellValues.align = column.align;
            cellValues.tdCls = cellValues.style = cellValues.tdAttr = "";
            cellValues.unselectableAttr = me.enableTextSelection ? '' : 'unselectable="on"';

            if (column.renderer && column.renderer.call) {
                value = column.renderer.call(column.scope || me.ownerCt, fieldValue, cellValues, record, recordIndex, columnIndex, me.dataSource, me);
                if (cellValues.css) {
                    // This warning attribute is used by the compat layer
                    // TODO: remove when compat layer becomes deprecated
                    record.cssWarning = true;
                    cellValues.tdCls += ' ' + cellValues.css;
                    delete cellValues.css;
                }
            } else {
                value = fieldValue;
            }
            if (value === undefined || value === null || value === '') {
                value = '&#160;';
            }
            cellValues.value = value;

            // Calculate rows to add to cell
            classes.length = 0;
            classes.push(
                (column.tdCls ||''),
                Ext.baseCSSPrefix + 'grid-cell ' + Ext.baseCSSPrefix + 'grid-cell-' + column.getItemId()
            );

            if (record.isModified(column.dataIndex)) {
                classes.push(this.headerCt.dirtyCls);
            }
            if (column.isFirstVisible) {
                classes.push(me.firstCls);
            }
            if (column.isLastVisible) {
                classes.push(me.lastCls);
            }
            if (!this.enableTextSelection) {
                classes.push(Ext.baseCSSPrefix + 'unselectable');
            }

            classes.push(cellValues.tdCls);
            if (selModel && selModel.isCellSelected && selModel.isCellSelected(me, recordIndex, columnIndex)) {
                classes.push(me.selectedCellCls);
            }
            cellValues.tdCls = classes.join(' ');

            cellTpl.applyOut(cellValues, out);

            // Dereference objects since cellValues is a persistent var in the XTemplate's scope chain
            cellValues.column = null;
        },

        /**
         * @override
         * Returns the node given the passed Record, or index or node.
         * @param nodeInfo {HTMLElement/String/Number/Ext.data.Model} The node or record
         * @param dataRow `true` to return the data row (not the top level row if wrapped), `false`
         * to return the top level row.
         */
        getNode: function(nodeInfo, dataRow) {
            var fly,
                result = this.callParent(arguments);

            if (result && result.tagName) {
                if (dataRow) {
                    if (!(fly = Ext.fly(result)).is(this.dataRowSelector)) {
                        return fly.down(this.dataRowSelector, true);
                    }
                } else if (dataRow === false) {
                    if (!(fly = Ext.fly(result)).is(this.itemSelector)) {
                        return fly.up(this.itemSelector, null, true);
                    }
                }
            }
            return result;
        },

        getRowId: function(record){
            return this.id + '-record-' + record.internalId;
        },

        getNodeByRecord: function(record, dataRow) {
            var result = this.el.getById(this.getRowId(record), true),
                itemSelector = this.itemSelector,
                fly;

            if (dataRow === false && result) {
                if (!(fly = Ext.fly(result)).is(itemSelector)) {
                    return fly.up(itemSelector, null, true);
                }
            }
            return result;
        },

        // Links back from grid rows are installed by the XTemplate as data attributes
        updateIndexes: Ext.emptyFn,

        // Outer table
        bodySelector: 'table',

        // Element which contains rows
        nodeContainerSelector: 'tbody',

        // view item (may be a wrapper)
        itemSelector: 'tr.' + Ext.baseCSSPrefix + 'grid-row',

        // row which contains cells as opposed to wrapping rows
        dataRowSelector: 'tr.' + Ext.baseCSSPrefix + 'grid-data-row',

        // cell
        cellSelector: 'td.' + Ext.baseCSSPrefix + 'grid-cell',

        // <column sizer>
        sizerSelector: 'col.' + Ext.baseCSSPrefix + 'grid-cell',

        innerSelector: 'div.' + Ext.baseCSSPrefix + 'grid-cell-inner',

        getNodeContainer: function() {
            return this.el.down(this.nodeContainerSelector, true);
        },

        /**
         * Returns a CSS selector which selects the outermost element(s) in this view.
         */
        getBodySelector: function() {
            return this.bodySelector + '.' + Ext.baseCSSPrefix + this.id + '-table';
        },

        /**
         * Returns a CSS selector which selects the element which contains record nodes.
         */
        getNodeContainerSelector: function() {
            return this.nodeContainerSelector;
        },

        /**
         * Returns a CSS selector which selects the element(s) which define the width of a column.
         *
         * This is used by the {@link Ext.view.TableLayout} when resizing columns.
         *
         */
        getColumnSizerSelector: function(header) {
            return this.sizerSelector + '-' + header.getItemId();
        },

        /**
         * Returns a CSS selector which selects items of the view rendered by the rowTpl
         */
        getItemSelector: function() {
            return this.itemSelector;
        },

        /**
         * Returns a CSS selector which selects a row which contains cells.
         *
         * These *may not* correspond to actual records in the store. This selector may be used
         * to identify things like total rows or header rows as injected by features which modify
         * the rowTpl.
         *
         */
        getDataRowSelector: function() {
            return this.dataRowSelector;
        },

        /**
         * Returns a CSS selector which selects a particular column if the desired header is passed,
         * or a general cell selector is no parameter is passed.
         *
         * @param {Ext.grid.column.Column} [header] The column for which to return the selector. If
         * omitted, the general cell selector which matches **ant cell** will be returned.
         *
         */
        getCellSelector: function(header) {
            var result = this.cellSelector;
            if (header) {
                result += '-' + header.getItemId();
            }
            return result;
        },

        /*
         * Returns a CSS selector which selects the content carrying element within cells.
         */
        getCellInnerSelector: function(header) {
            return this.getCellSelector(header) + ' ' + this.innerSelector;
        },

        /**
         * Adds a CSS Class to a specific row.
         * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model
         * representing this row
         * @param {String} cls
         */
        addRowCls: function(rowInfo, cls) {
            var row = this.getNode(rowInfo, false);
            if (row) {
                Ext.fly(row).addCls(cls);
            }
        },

        /**
         * Removes a CSS Class from a specific row.
         * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model
         * representing this row
         * @param {String} cls
         */
        removeRowCls: function(rowInfo, cls) {
            var row = this.getNode(rowInfo, false);
            if (row) {
                Ext.fly(row).removeCls(cls);
            }
        },

        // GridSelectionModel invokes onRowSelect as selection changes
        onRowSelect : function(rowIdx) {
            this.addRowCls(rowIdx, this.selectedItemCls);
        },

        // GridSelectionModel invokes onRowDeselect as selection changes
        onRowDeselect : function(rowIdx) {
            var me = this;

            me.removeRowCls(rowIdx, me.selectedItemCls);
            me.removeRowCls(rowIdx, me.focusedItemCls);
        },

        onCellSelect: function(position) {
            var cell = this.getCellByPosition(position, true);
            if (cell) {
                Ext.fly(cell).addCls(this.selectedCellCls);
            }
        },

        onCellDeselect: function(position) {
            var cell = this.getCellByPosition(position, true);
            if (cell) {
                Ext.fly(cell).removeCls(this.selectedCellCls);
            }

        },

        onCellFocus: function(position) {
            this.focusCell(position);
        },

        getCellByPosition: function(position, returnDom) {
            if (position) {
                var node   = this.getNode(position.row, true),
                    header = this.headerCt.getHeaderAtIndex(position.column);

                if (header && node) {
                    return Ext.fly(node).down(this.getCellSelector(header), returnDom);
                }
            }
            return false;
        },

        // GridSelectionModel invokes onRowFocus to 'highlight'
        // the last row focused
        onRowFocus: function(rowIdx, highlight, supressFocus) {
            var me = this;

            if (highlight) {
                me.addRowCls(rowIdx, me.focusedItemCls);
                if (!supressFocus) {
                    me.focusRow(rowIdx);
                }
                //this.el.dom.setAttribute('aria-activedescendant', row.id);
            } else {
                me.removeRowCls(rowIdx, me.focusedItemCls);
            }
        },

        /**
         * Focuses a particular row and brings it into view. Will fire the rowfocus event.
         * @param {HTMLElement/String/Number/Ext.data.Model} rowIdx
         * An HTMLElement template node, index of a template node, the id of a template node or the
         * record associated with the node.
         */
        focusRow: function(rowIdx) {
            var me         = this,
                row        = me.getNode(rowIdx, true),
                el         = me.el,
                adjustment = 0,
                panel      = me.ownerCt,
                rowRegion,
                elTop,
                elBottom,
                record;

            if (row && el) {
                // Viewable region must not include scrollbars, so use
                // DOM clientHeight to determine height
                elTop = el.getY();
                elBottom = elTop + el.dom.clientHeight;
                rowRegion = Ext.fly(row).getRegion();
                // row is above
                if (rowRegion.top < elTop) {
                    adjustment = rowRegion.top - elTop;
                // row is below
                } else if (rowRegion.bottom > elBottom) {
                    adjustment = rowRegion.bottom - elBottom;
                }
                record = me.getRecord(row);
                rowIdx = me.indexInStore(row);

                if (adjustment) {
                    panel.scrollByDeltaY(adjustment);
                }
                el.focus();
                me.fireEvent('rowfocus', record, row, rowIdx);
            }
        },

        focusCell: function(position) {
            var me          = this,
                cell        = me.getCellByPosition(position),
                record      = me.getRecord(position.row),
                el          = me.el,
                adjustmentY = 0,
                adjustmentX = 0,
                elRegion    = el.getRegion(),
                panel       = me.ownerCt,
                cellRegion;

            // Viewable region must not include scrollbars, so use
            // DOM client dimensions
            elRegion.bottom = elRegion.top + el.dom.clientHeight;
            elRegion.right = elRegion.left + el.dom.clientWidth;
            if (cell) {
                cellRegion = cell.getRegion();
                // cell is above
                if (cellRegion.top < elRegion.top) {
                    adjustmentY = cellRegion.top - elRegion.top;
                // cell is below
                } else if (cellRegion.bottom > elRegion.bottom) {
                    adjustmentY = cellRegion.bottom - elRegion.bottom;
                }

                // cell is left
                if (cellRegion.left < elRegion.left) {
                    adjustmentX = cellRegion.left - elRegion.left;
                // cell is right
                } else if (cellRegion.right > elRegion.right) {
                    adjustmentX = cellRegion.right - elRegion.right;
                }

                if (adjustmentY) {
                    panel.scrollByDeltaY(adjustmentY);
                }
                if (adjustmentX) {
                    panel.scrollByDeltaX(adjustmentX);
                }
                el.focus();
                me.fireEvent('cellfocus', record, cell, position);
            }
        },

        /**
         * Scrolls by delta. This affects this individual view ONLY and does not
         * synchronize across views or scrollers.
         * @param {Number} delta
         * @param {String} [dir] Valid values are scrollTop and scrollLeft. Defaults to scrollTop.
         * @private
         */
        scrollByDelta: function(delta, dir) {
            dir = dir || 'scrollTop';
            var elDom = this.el.dom;
            elDom[dir] = (elDom[dir] += delta);
        },

        /**
         * @private
         * Used to test if a row being updated is a basic data row as opposed to containing extra markup
         * provided by a Feature, eg a wrapping row containing extra elements such as group header, group summary,
         * row body etc.
         *
         * If A row being updated *is not* a data row, then the elements within it which are not valid data rows
         * must be copied.
         * @param row
         * @return {Boolean} `true` if the passed element is a basic data row.
         */
        isDataRow: function(row) {
            return Ext.fly(row).hasCls(Ext.baseCSSPrefix + 'grid-data-row');
        },

        syncRowHeights: function(firstRow, secondRow) {
            firstRow = Ext.get(firstRow);
            secondRow = Ext.get(secondRow);
            firstRow.dom.style.height = secondRow.dom.style.height = '';
            var me = this,
                rowTpl = me.rowTpl,
                firstRowHeight = firstRow.dom.offsetHeight,
                secondRowHeight = secondRow.dom.offsetHeight,
                // Hack for legacy IE browsers which add cell borders onto the TR height.
                incr = Ext.isBorderBox ? 0 : -2;

            // If the two rows *need* syncing...
            if (firstRowHeight !== secondRowHeight) {

                // loop thru all of rowTpls asking them to sync the two row heights if they know how to.
                while (rowTpl) {
                    if (rowTpl.syncRowHeights) {
                        // If any rowTpl in the chain returns false, quit processing
                        if (rowTpl.syncRowHeights(firstRow, secondRow) === false) {
                            break;
                        }
                    }
                    rowTpl = rowTpl.nextTpl;
                }

                // If that did not fix it, see if we have nested data rows, and equalize the data row heights
                firstRowHeight = firstRow.dom.offsetHeight;
                secondRowHeight = secondRow.dom.offsetHeight;
                if (firstRowHeight !== secondRowHeight) {

                    // See if the real data row has been nested deeper by a Feature.
                    firstRow = firstRow.down('[data-recordId]') || firstRow;
                    secondRow = secondRow.down('[data-recordId]') || secondRow;

                    // Yes, there's a nested data row on each side. Sync the heights of the two.
                    if (firstRow && secondRow) {
                        firstRow.dom.style.height = secondRow.dom.style.height = '';
                        firstRowHeight = firstRow.dom.offsetHeight;
                        secondRowHeight = secondRow.dom.offsetHeight;

                        if (firstRowHeight > secondRowHeight) {
                            secondRow.setHeight(firstRowHeight + incr);
                        } else if (secondRowHeight > firstRowHeight) {
                            firstRow.setHeight(secondRowHeight + incr);
                        }
                    }
                }
            }
        },

        // private
        onUpdate : function(store, record, operation, changedFieldNames) {
            var me = this,
                rowTpl = me.rowTpl,
                index,
                oldRow, oldRowDom,
                newRowDom,
                newAttrs, attLen, attName, attrIndex,
                overItemCls,
                focusedItemCls,
                columns;

            if (me.viewReady) {
                // Table row being updated
                oldRowDom = me.getNodeByRecord(record, false);

                // Row might not be rendered due to buffered rendering or being part of a collapsed group...
                if (oldRowDom) {
                    overItemCls = me.overItemCls;
                    focusedItemCls = me.focusedItemCls;

                    index = me.indexInStore(record);
                    oldRow = Ext.fly(oldRowDom, '_internal');
                    newRowDom = me.createRowElement(record, index);
                    if (oldRow.hasCls(overItemCls)) {
                        Ext.fly(newRowDom).addCls(overItemCls);
                    }
                    if (oldRow.hasCls(focusedItemCls)) {
                        Ext.fly(newRowDom).addCls(focusedItemCls);
                    }
                    columns = me.headerCt.getGridColumns();

                    // Copy new row attributes across. Use IE-specific method if possible.
                    if (oldRowDom.mergeAttributes) {
                        oldRowDom.mergeAttributes(newRowDom, true);
                    } else {
                        newAttrs = newRowDom.attributes;
                        attLen = newAttrs.length;
                        for (attrIndex = 0; attrIndex < attLen; attrIndex++) {
                            attName = newAttrs[attrIndex].name;
                            if (attName !== 'id') {
                                oldRowDom.setAttribute(attName, newAttrs[attrIndex].value);
                            }
                        }
                    }

                    // If we have columns which may *need* updating (think locked side of lockable grid with all columns unlocked)
                    // and the changed record is within our view, then update the view
                    if (columns.length) {
                        me.updateColumns(me.getNode(oldRowDom, true), me.getNode(newRowDom, true), columns, changedFieldNames);
                    }

                    // loop thru all of rowTpls asking them to sync the content they are responsible for if any.
                    while (rowTpl) {
                        if (rowTpl.syncContent) {
                            if (rowTpl.syncContent(oldRowDom, newRowDom) === false) {
                                break;
                            }
                        }
                        rowTpl = rowTpl.nextTpl;
                    }

                    // Since we don't actually replace the row, we need to fire the event with the old row
                    // because it's the thing that is still in the DOM
                    me.fireEvent('itemupdate', record, index, oldRowDom);
                    me.refreshSize();
                }
            }
        },

        updateColumns: function(oldRowDom, newRowDom, columns, changedFieldNames) {
            var me = this,
                newAttrs, attLen, attName, attrIndex,
                colCount = columns.length,
                colIndex,
                column,
                oldCell, newCell,
                row,
                // See if an editing plugin is active.
                isEditing = me.editingPlugin && me.editingPlugin.editing,
                cellSelector = me.getCellSelector();

                // Copy new row attributes across. Use IE-specific method if possible.
                // Must do again at this level because the row DOM passed here may be the nested row in a row wrap.
                if (oldRowDom.mergeAttributes) {
                    oldRowDom.mergeAttributes(newRowDom, true);
                } else {
                    newAttrs = newRowDom.attributes;
                    attLen = newAttrs.length;
                    for (attrIndex = 0; attrIndex < attLen; attrIndex++) {
                        attName = newAttrs[attrIndex].name;
                        if (attName !== 'id') {
                            oldRowDom.setAttribute(attName, newAttrs[attrIndex].value);
                        }
                    }
                }

            // Replace changed cells in the existing row structure with the new version from the rendered row.
            for (colIndex = 0; colIndex < colCount; colIndex++) {
                column = columns[colIndex];

                // If the field at this column index was changed, or column has a custom renderer
                // (which means value could rely on any other changed field) the update the cell's content.
                if (me.shouldUpdateCell(column, changedFieldNames)) {

                    // Pluck out cells using the column's unique cell selector.
                    // Becuse in a wrapped row, there may be several TD elements.
                    cellSelector = me.getCellSelector(column);
                    oldCell = Ext.DomQuery.selectNode(cellSelector, oldRowDom);
                    newCell = Ext.DomQuery.selectNode(cellSelector, newRowDom);

                    // If an editor plugin is active, we carefully replace just the *contents* of the cell.
                    if (isEditing) {
                        Ext.fly(oldCell).syncContent(newCell);
                    }
                    // Otherwise, we simply replace whole TDs with a new version
                    else {
                        // Use immediate parentNode when replacing in case the main row was a wrapping row
                        row = oldCell.parentNode;
                        row.insertBefore(newCell, oldCell);
                        row.removeChild(oldCell);
                    }
                }
            }
        },

        shouldUpdateCell: function(column, changedFieldNames){
            // Though this may not be the most efficient, a renderer could be dependent on any field in the
            // store, so we must always update the cell
            if (column.hasCustomRenderer) {
                return true;
            }
            return !changedFieldNames || Ext.Array.contains(changedFieldNames, column.dataIndex);
        },

        /**
         * Refreshes the grid view. Saves and restores the scroll state, generates a new template, stripes rows and
         * invalidates the scrollers.
         */
        refresh: function() {
            var me = this;
            me.callParent(arguments);
            me.headerCt.setSortState();
            me.selModel.onLastFocusChanged(null, me.selModel.lastFocused);

        },

        processItemEvent: function(record, row, rowIndex, e) {
            // Only process the event if it occurred within a row which maps to a record in the store
            if (this.indexInStore(row) !== -1) {
                var me = this,
                    cell = e.getTarget(me.getCellSelector(), row),
                    cellIndex,
                    map = me.statics().EventMap,
                    selModel = me.getSelectionModel(),
                    type = e.type,
                    features = me.features,
                    len = features.length,
                    i, result, feature;

                if (type == 'keydown' && !cell && selModel.getCurrentPosition) {
                    // CellModel, otherwise we can't tell which cell to invoke
                    cell = me.getCellByPosition(selModel.getCurrentPosition(), true);
                }

                // cellIndex is an attribute only of TD elements. Other TplFactories must use the data-cellIndex attribute.
                cellIndex = cell ? (cell.cellIndex || Number(cell.getAttribute('data-cellIndex'))) : -1;

                result = me.fireEvent('uievent', type, me, cell, rowIndex, cellIndex, e, record, row);

                if (result === false || me.callParent(arguments) === false) {
                    return false;
                }

                for (i = 0; i < len; ++i) {
                    feature = features[i];
                    // In some features, the first/last row might be wrapped to contain extra info,
                    // such as grouping or summary, so we may need to stop the event
                    if (feature.wrapsItem) {
                        if (feature.vetoEvent(record, row, rowIndex, e) === false) {
                            // If the feature is vetoing the event, there's a good chance that
                            // it's for some feature action in the wrapped row.
                            me.processSpecialEvent(e);
                            return false;
                        }
                    }
                }

                // Don't handle cellmouseenter and cellmouseleave events for now
                if (type == 'mouseover' || type == 'mouseout') {
                    return true;
                }

                if(!cell) {
                    // if the element whose event is being processed is not an actual cell (for example if using a rowbody
                    // feature and the rowbody element's event is being processed) then do not fire any "cell" events
                    return true;
                }

                return !(
                    // We are adding cell and feature events
                    (me['onBeforeCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
                    (me.fireEvent('beforecell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false) ||
                    (me['onCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
                    (me.fireEvent('cell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false)
                );
            } else {
                // If it's not in the store, it could be a feature event, so check here
                this.processSpecialEvent(e);
                return false;
            }
        },

        processSpecialEvent: function(e) {
            var me = this,
                features = me.features,
                ln = features.length,
                type = e.type,
                i, feature, prefix, featureTarget,
                beforeArgs, args,
                panel = me.ownerCt;

            me.callParent(arguments);

            if (type == 'mouseover' || type == 'mouseout') {
                return;
            }

            for (i = 0; i < ln; i++) {
                feature = features[i];
                if (feature.hasFeatureEvent) {
                    featureTarget = e.getTarget(feature.eventSelector, me.getTargetEl());
                    if (featureTarget) {
                        prefix = feature.eventPrefix;
                        // allows features to implement getFireEventArgs to change the
                        // fireEvent signature
                        beforeArgs = feature.getFireEventArgs('before' + prefix + type, me, featureTarget, e);
                        args = feature.getFireEventArgs(prefix + type, me, featureTarget, e);

                        if (
                            // before view event
                            (me.fireEvent.apply(me, beforeArgs) === false) ||
                            // panel grid event
                            (panel.fireEvent.apply(panel, beforeArgs) === false) ||
                            // view event
                            (me.fireEvent.apply(me, args) === false) ||
                            // panel event
                            (panel.fireEvent.apply(panel, args) === false)
                        ) {
                            return false;
                        }
                    }
                }
            }
            return true;
        },

        onCellMouseDown: Ext.emptyFn,
        onCellMouseUp: Ext.emptyFn,
        onCellClick: Ext.emptyFn,
        onCellDblClick: Ext.emptyFn,
        onCellContextMenu: Ext.emptyFn,
        onCellKeyDown: Ext.emptyFn,
        onBeforeCellMouseDown: Ext.emptyFn,
        onBeforeCellMouseUp: Ext.emptyFn,
        onBeforeCellClick: Ext.emptyFn,
        onBeforeCellDblClick: Ext.emptyFn,
        onBeforeCellContextMenu: Ext.emptyFn,
        onBeforeCellKeyDown: Ext.emptyFn,

        /**
         * Expands a particular header to fit the max content width.
         * @deprecated Use {@link #autoSizeColumn} instead.
         */
        expandToFit: function(header) {
            this.autoSizeColumn(header);
        },

        /**
         * Sizes the passed header to fit the max content width.
         * *Note that group columns shrinkwrap around the size of leaf columns. Auto sizing a group column
         * autosizes descendant leaf columns.*
         * @param {Ext.grid.column.Column/Number} The header (or index of header) to auto size.
         */
        autoSizeColumn: function(header) {
            if (Ext.isNumber(header)) {
                header = this.getGridColumns[header];
            }
            if (header) {
                if (header.isGroupHeader) {
                    header.autoSize();
                    return;
                }
                delete header.flex;
                header.setWidth(this.getMaxContentWidth(header));
            }
        },

        /**
         * Returns the max contentWidth of the header's text and all cells
         * in the grid under this header.
         * @private
         */
        getMaxContentWidth: function(header) {
            var me = this,
                cells = me.el.query(header.getCellInnerSelector()),
                originalWidth = header.getWidth(),
                i = 0,
                ln = cells.length,
                maxWidth;

            // Set column width back to the minimum header width
            me.body.select(me.getColumnSizerSelector(header)).setWidth(40);

            // Allow for padding round text of header
            maxWidth = header.textEl.dom.offsetWidth + header.titleEl.getPadding('lr');
            for (; i < ln; i++) {
                // Add 2 to avoid text-overflow:ellipsis truncation
                maxWidth = Math.max(maxWidth, cells[i].scrollWidth + 2);
            }
            // Set column width back to original width
            me.body.select(me.getColumnSizerSelector(header)).setWidth(originalWidth);

            return maxWidth;
        },

        getPositionByEvent: function(e) {
            var me       = this,
                cellNode = e.getTarget(me.cellSelector),
                rowNode  = e.getTarget(me.itemSelector),
                record   = me.getRecord(rowNode),
                header   = me.getHeaderByCell(cellNode);

            return me.getPosition(record, header);
        },

        getHeaderByCell: function(cell) {
            if (cell) {
                var m = cell.className.match(this.cellRe);
                if (m && m[1]) {
                    return Ext.getCmp(m[1]);
                }
            }
            return false;
        },

        /**
         * @param {Object} position The current row and column: an object containing the following properties:
         *
         * - row - The row index
         * - column - The column index
         *
         * @param {String} direction 'up', 'down', 'right' and 'left'
         * @param {Ext.EventObject} e event
         * @param {Boolean} preventWrap Set to true to prevent wrap around to the next or previous row.
         * @param {Function} verifierFn A function to verify the validity of the calculated position.
         * When using this function, you must return true to allow the newPosition to be returned.
         * @param {Object} scope Scope to run the verifierFn in
         * @returns {Object} newPosition An object containing the following properties:
         *
         * - row - The row index
         * - column - The column index
         *
         * @private
         */
        walkCells: function(pos, direction, e, preventWrap, verifierFn, scope) {

            // Caller (probably CellModel) had no current position. This can happen
            // if the main el is focused and any navigation key is presssed.
            if (!pos) {
                return false;
            }

            var me       = this,
                row      = pos.row,
                column   = pos.column,
                rowCount = me.dataSource.getCount(),
                firstCol = me.getFirstVisibleColumnIndex(),
                lastCol  = me.getLastVisibleColumnIndex(),
                newPos   = {row: row, column: column},
                activeHeader = me.headerCt.getHeaderAtIndex(column);

            // no active header or its currently hidden
            if (!activeHeader || activeHeader.hidden || !rowCount) {
                return false;
            }

            e = e || {};
            direction = direction.toLowerCase();
            switch (direction) {
                case 'right':
                    // has the potential to wrap if its last
                    if (column === lastCol) {
                        // if bottom row and last column, deny right
                        if (preventWrap || row === rowCount - 1) {
                            return false;
                        }
                        if (!e.ctrlKey) {
                            // otherwise wrap to nextRow and firstCol
                            newPos.row = me.walkRows(row, 1);
                            if (newPos.row !== row) {
                                newPos.column = firstCol;
                            }
                        }
                    // go right
                    } else {
                        if (!e.ctrlKey) {
                            newPos.column = column + me.getRightGap(activeHeader);
                        } else {
                            newPos.column = lastCol;
                        }
                    }
                    break;

                case 'left':
                    // has the potential to wrap
                    if (column === firstCol) {
                        // if top row and first column, deny left
                        if (preventWrap || row === 0) {
                            return false;
                        }
                        if (!e.ctrlKey) {
                            // otherwise wrap to prevRow and lastCol
                            newPos.row = me.walkRows(row, -1);
                            if (newPos.row !== row) {
                                newPos.column = lastCol;
                            }
                        }
                    // go left
                    } else {
                        if (!e.ctrlKey) {
                            newPos.column = column + me.getLeftGap(activeHeader);
                        } else {
                            newPos.column = firstCol;
                        }
                    }
                    break;

                case 'up':
                    // if top row, deny up
                    if (row === 0) {
                        return false;
                    // go up
                    } else {
                        if (!e.ctrlKey) {
                            newPos.row = me.walkRows(row, -1);
                        } else {
                            // Go to first row by walking down from row -1
                            newPos.row = me.walkRows(-1, 1);
                        }
                    }
                    break;

                case 'down':
                    // if bottom row, deny down
                    if (row === rowCount - 1) {
                        return false;
                    // go down
                    } else {
                        if (!e.ctrlKey) {
                            newPos.row = me.walkRows(row, 1);
                        } else {
                            // Go to first row by walking up from beyond the last row
                            newPos.row = me.walkRows(rowCount, -1);
                        }
                    }
                    break;
            }

            if (verifierFn && verifierFn.call(scope || window, newPos) !== true) {
                return false;
            } else {
                return newPos;
            }
        },

        /**
         * Increments the passed row index by the passed increment which may be +ve or -ve
         *
         * Skips hidden rows.
         *
         * If no row is visible in the specified direction, returns the input row index unchanged.
         * @param {Number} startRow The zero-based row index to start from.
         * @param {Number} distance The distance to move the row by. May be +ve or -ve.
         */
        walkRows: function(startRow, distance) {
            // Note that we use the **dataSource** here because row indices mean view row indices
            // so records in collapsed groups must be omitted.
            var me = this,
                moved = 0,
                lastValid = startRow,
                node,
                last = me.dataSource.getCount() - 1,
                limit = (distance < 0) ? 0 : last,
                increment = limit ? 1 : -1,
                result = startRow;

            do {
                // Walked off the end: return the last encountered valid row
                if (limit ? result >= limit : result <= 0) {
                    return lastValid || limit;
                }

                // Move the result pointer on by one position. We have to count intervening VISIBLE nodes
                result += increment;

                // Stepped onto VISIBLE record: Increment the moved count.
                // We must not count stepping onto a non-rendered record as a move.
                if ((node = Ext.fly(me.getNode(result, true))) && node.isVisible(true)) {
                    moved += increment;
                    lastValid = result;
                }
            } while (moved !== distance);
            return result;
        },

        /**
         * Navigates from the passed record by the passed increment which may be +ve or -ve
         *
         * Skips hidden records.
         *
         * If no record is visible in the specified direction, returns the starting record index unchanged.
         * @param {Ext.data.Model} startRec The Record to start from.
         * @param {Number} distance The distance to move from the record. May be +ve or -ve.
         */
        walkRecs: function(startRec, distance) {
            // Note that we use the **store** to access the records by index because the dataSource omits records in collapsed groups.
            // This is used by selection models which use the **store**
            var me = this,
                moved = 0,
                lastValid = startRec,
                node,
                last = me.store.getCount() - 1,
                limit = (distance < 0) ? 0 : last,
                increment = limit ? 1 : -1,
                testIndex = me.store.indexOf(startRec),
                rec;

            do {
                // Walked off the end: return the last encountered valid record
                if (limit ? testIndex >= limit : testIndex <= 0) {
                    return lastValid;
                }

                // Move the result pointer on by one position. We have to count intervening VISIBLE nodes
                testIndex += increment;

                // Stepped onto VISIBLE record: Increment the moved count.
                // We must not count stepping onto a non-rendered record as a move.
                rec = me.store.getAt(testIndex);
                if ((node = Ext.fly(me.getNodeByRecord(rec, true))) && node.isVisible(true)) {
                    moved += increment;
                    lastValid = rec;
                }
            } while (moved !== distance);
            return lastValid;
        },

        getFirstVisibleRowIndex: function() {
            var me = this,
                count = me.dataSource.getCount(),
                result = 0;

            do {
                result += 1;
                if (result === count) {
                    return;
                }
            } while (!Ext.fly(me.getNode(result, true)).isVisible(true));
            return result;
        },

        getLastVisibleRowIndex: function() {
            var me = this,
                count = me.dataSource.getCount(),
                result = count;

            do {
                result -= 1;
                if (result === -1) {
                    return;
                }
            } while (!Ext.fly(me.getNode(result, true)).isVisible(true));
            return result;
        },

        getFirstVisibleColumnIndex: function() {
            var firstVisibleHeader = this.getHeaderCt().getVisibleGridColumns()[0];

            return firstVisibleHeader ? firstVisibleHeader.getIndex() : -1;
        },

        getLastVisibleColumnIndex: function() {
            var visHeaders = this.getHeaderCt().getVisibleGridColumns(),
                lastHeader = visHeaders[visHeaders.length - 1];

            return lastHeader.getIndex();
        },

        getHeaderCt: function() {
            return this.headerCt;
        },

        // TODO: have this use the new Ext.grid.CellContext class
        getPosition: function(record, header) {
            return {
                row: this.dataSource.indexOf(record, true),
                column: Ext.Array.indexOf(this.headerCt.getGridColumns(), header)
            };
        },

        /**
         * Determines the 'gap' between the closest adjacent header to the right
         * that is not hidden.
         * @private
         */
        getRightGap: function(activeHeader) {
            var headerCt        = this.getHeaderCt(),
                headers         = headerCt.getGridColumns(),
                activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
                i               = activeHeaderIdx + 1,
                nextIdx;

            for (; i <= headers.length; i++) {
                if (!headers[i].hidden) {
                    nextIdx = i;
                    break;
                }
            }

            return nextIdx - activeHeaderIdx;
        },

        beforeDestroy: function() {
            var me = this;

            if (me.rendered) {
                me.el.removeAllListeners();
            }
            me.callParent(arguments);
        },

        /**
         * Determines the 'gap' between the closest adjacent header to the left
         * that is not hidden.
         * @private
         */
        getLeftGap: function(activeHeader) {
            var headerCt        = this.getHeaderCt(),
                headers         = headerCt.getGridColumns(),
                activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
                i               = activeHeaderIdx - 1,
                prevIdx;

            for (; i >= 0; i--) {
                if (!headers[i].hidden) {
                    prevIdx = i;
                    break;
                }
            }

            return prevIdx - activeHeaderIdx;
        },

        // after adding a row stripe rows from then on
        onAdd: function(ds, records, index) {
            this.callParent(arguments);
            this.doStripeRows(index);
        },

        // after removing a row stripe rows from then on
        onRemove: function(ds, records, indexes) {
            this.callParent(arguments);
            this.doStripeRows(indexes[0]);
        },

        /**
         * Stripes rows from a particular row index.
         * @param {Number} startRow
         * @param {Number} [endRow] argument specifying the last row to process.
         * By default process up to the last row.
         * @private
         */
        doStripeRows: function(startRow, endRow) {
            var me = this,
                rows,
                rowsLn,
                i,
                row;

            // ensure stripeRows configuration is turned on
            if (me.rendered && me.stripeRows) {
                rows = me.getNodes(startRow, endRow);

                for (i = 0, rowsLn = rows.length; i < rowsLn; i++) {
                    row = rows[i];
                    // Remove prior applied row classes.
                    row.className = row.className.replace(me.rowClsRe, ' ');
                    startRow++;
                    // Every odd row will get an additional cls
                    if (startRow % 2 === 0) {
                        row.className += (' ' + me.altRowCls);
                    }
                }
            }
        }
    });