/**
 * 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.grid.CellContext',
        'Ext.view.TableLayout',
        'Ext.view.NodeCache',
        'Ext.util.DelayedTask',
        'Ext.util.MixedCollection'
    ],

    componentLayout: 'tableview',

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

    unselectableCls: Ext.baseCSSPrefix + 'unselectable',

    /**
     * @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',
    beforeSelectedItemCls: Ext.baseCSSPrefix + 'grid-row-before-selected',
    selectedCellCls: Ext.baseCSSPrefix + 'grid-cell-selected',
    focusedItemCls: Ext.baseCSSPrefix + 'grid-row-focused',
    beforeFocusedItemCls: Ext.baseCSSPrefix + 'grid-row-before-focused',
    tableFocusedFirstCls: Ext.baseCSSPrefix + 'grid-table-focused-first',
    tableSelectedFirstCls: Ext.baseCSSPrefix + 'grid-table-selected-first',
    tableOverFirstCls: Ext.baseCSSPrefix + 'grid-table-over-first',
    overItemCls: Ext.baseCSSPrefix + 'grid-row-over',
    beforeOverItemCls: Ext.baseCSSPrefix + 'grid-row-before-over',
    altRowCls:   Ext.baseCSSPrefix + 'grid-row-alt',
    dirtyCls: Ext.baseCSSPrefix + 'grid-dirty-cell',
    rowClsRe: new RegExp('(?:^|\\s*)' + Ext.baseCSSPrefix + 'grid-row-(first|last|alt)(?:\\s+|$)', 'g'),
    cellRe: new RegExp(Ext.baseCSSPrefix + 'grid-cell-headerId-([^\\s]+)(?:\\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.
     */
     
    ariaRole: 'grid',

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

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

    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} tabIndex="-1" {ariaRowAttr}>',
            '<tpl for="columns">' +
                '{%',
                    'parent.view.renderCell(values, parent.record, parent.recordIndex, parent.rowIndex, xindex - 1, out, parent)',
                 '%}',
            '</tpl>',
        '</tr>',
        {
            priority: 0
        }
    ],

    cellTpl: [
        '<td class="{tdCls}" {tdAttr} {[Ext.aria ? "id=\\"" + Ext.id() + "\\"" : ""]} {ariaCellAttr}>',
            '<div {unselectableAttr} class="' + Ext.baseCSSPrefix + 'grid-cell-inner {innerCls}"',
                'style="text-align:{align};<tpl if="style">{style}</tpl>" {ariaCellInnerAttr}>{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-used to avoid GC churn during repeated rendering.
    rowValues: {
        itemClasses: [],
        rowClasses: []
    },
    cellValues: {
        classes: [
            Ext.baseCSSPrefix + 'grid-cell ' + Ext.baseCSSPrefix + 'grid-td' // for styles shared between cell and rowwrap
        ]
    },

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

    constructor: function(config) {
        // Adjust our base class if we are inside a TreePanel
        if (config.grid.isTree) {
            config.baseCls = Ext.baseCSSPrefix + 'tree-view';
        }
        this.callParent([config]);
    },

    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;
            me.beforeOverItemCls = 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.headerCt.view = me;

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

        me.itemSelector = me.getItemSelector();
        me.all = new Ext.view.NodeCache(me);
        me.callParent();
    },

    getVisibleColumnManager: function(){
        var owner = this.ownerCt,
            lock = owner.ownerLockable;

        return lock ? lock.getVisibleColumnManager() : owner.getVisibleColumnManager();

    },

    /**
     * @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 leaf columns used for rendering the grid rows.
     * @private
     */
    getGridColumns: function() {
        return this.ownerCt.getVisibleColumnManager().getColumns();
    },

    /**
     * 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.ownerCt.getVisibleColumnManager().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);
        }
    },

    renderTHead: function(values, out) {
        var headers = values.view.headerFns,
            len, i;

        if (headers) {
            for (i = 0, len = headers.length; i < len; ++i) {
                headers[i].call(this, values, out);
            }
        }
    },

    // Currently, we don't have ordering support for header/footer functions,
    // they will be pushed on at construction time. If the need does arise,
    // we can add this functionality in the future, but for now it's not
    // really necessary since currently only the summary feature uses this.
    addHeaderFn: function(){
        var headers = this.headerFns;
        if (!headers) {
            headers = this.headerFns = [];
        }
        headers.push(fn);
    },

    renderTFoot: function(values, out){
        var footers = values.view.footerFns,
            len, i;

        if (footers) {
            for (i = 0, len = footers.length; i < len; ++i) {
                footers[i].call(this, values, out);
            }
        }
    },

    addFooterFn: function(fn){
        var footers = this.footerFns;
        if (!footers) {
            footers = this.footerFns = [];
        }
        footers.push(fn);
    },

    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) {
        // We ignore scrolling caused by focusing
        if (!this.ignoreScroll) {
            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) {
        var me = this,
            recordIndex;

        // If store.destroyStore has been called before some delayed event fires on a node, we must ignore the event.
        if (me.store.isDestroyed) {
            return;
        }

        node = me.getNode(node);
        if (node) {
            // If we're grouping, the indexes may be off
            // because of collapsed groups, so just grab it by id
            if (!me.hasActiveGrouping()) {
                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 me.store.data.getAt(recordIndex);
                    }
                }
            }
            return me.store.getByInternalId(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 = node.isCollapsedPlaceholder ? this.getNode(node) : this.getNode(node, false);
        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,
            i;

        rowValues.view = this;
        rowValues.columns = this.ownerCt.getVisibleColumnManager().getColumns();

        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.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 role="presentation"><col role="presentation" class="', columns[i].getCellId(), '" 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;

        // recordIndex is index in true store (NOT the data source - possibly a GroupStore)
        rowValues.recordIndex = me.store.indexOf(record);

        // rowIndex is the row number in the view.
        rowValues.rowIndex = rowIdx;
        rowValues.rowId = me.getRowId(record);
        rowValues.itemCls = rowValues.rowCls = '';
        if (!rowValues.columns) {
            rowValues.columns = me.ownerCt.getVisibleColumnManager().getColumns();
        }

        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 (!me.ownerCt.disableSelection && selModel.isRowSelected) {
                // Selection class goes on the outermost row, so it goes into itemClasses
                if (selModel.isRowSelected(record)) {
                    itemClasses.push(me.selectedItemCls);
                }
                /* TODO: When merging into sencha-5.0.x branch. These classes should be pushed into the itemClasses array to go on the outermost view item element */
                // Ensure selection class is added to selected records, and to the record *before* any selected record
                // When looking ahead to see if the next record is selected, ensure we do not look past the end!
                if (me.rowValues.recordIndex < me.store.getTotalCount() - 1 && selModel.isRowSelected(me.rowValues.recordIndex + 1) && !me.isRowStyleFirst(rowIdx + 1)) {
                    rowClasses.push(me.beforeSelectedItemCls);
                }
            }

            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} rowIndex The row index (zero based within this view 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, rowIndex, columnIndex, out) {
        var me = this,
            selModel = me.selModel,
            cellValues = me.cellValues,
            classes = cellValues.classes,
            fieldValue = record.data[column.dataIndex],
            cellTpl = me.cellTpl,
            fullIndex, value, clsInsertPoint;

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

        if (column.renderer && column.renderer.call) {
            fullIndex = me.ownerCt.columnManager.getHeaderIndex(column);
            value = column.renderer.call(column.scope || me.ownerCt, fieldValue, cellValues, record, recordIndex, fullIndex, 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;
        }
        cellValues.value = (value == null || value === '') ? '&#160;' : value;

        // Calculate classes to add to cell
        classes[1] = column.getCellId();

        // On IE8, array[len] = 'foo' is twice as fast as array.push('foo')
        // So keep an insertion point and use assignment to help IE!
        clsInsertPoint = 2;

        if (column.tdCls) {
            classes[clsInsertPoint++] = column.tdCls;
        }
        if (me.markDirty && record.isModified(column.dataIndex)) {
            classes[clsInsertPoint++] = me.dirtyCls;
        }
        if (column.isFirstVisible) {
            classes[clsInsertPoint++] = me.firstCls;
        }
        if (column.isLastVisible) {
            classes[clsInsertPoint++] = me.lastCls;
        }
        if (!me.enableTextSelection) {
            classes[clsInsertPoint++] = me.unselectableCls;
        }
        if (cellValues.tdCls) {
            classes[clsInsertPoint++] = cellValues.tdCls;
        }
        if (selModel && selModel.isCellModel && selModel.isCellSelected(me, recordIndex, columnIndex)) {
            classes[clsInsertPoint++] = (me.selectedCellCls);
        }

        // Chop back array to only what we've set
        classes.length = clsInsertPoint;

        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;
    },

    /**
     * Returns the node given the passed Record, or index or node.
     * @param {HTMLElement/String/Number/Ext.data.Model} nodeInfo The node or record
     * @param {Boolean} [dataRow] `true` to return the data row (not the top level row if wrapped), `false`
     * to return the top level row.
     * @return {HTMLElement} The node or null if it wasn't found
     */
    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;
    },

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

    getNodeById: function(id, dataRow){
        id = this.constructRowId(id);
        return this.retrieveNode(id, dataRow);
    },

    getNodeByRecord: function(record, dataRow) {
        var id = this.getRowId(record);
        return this.retrieveNode(id, dataRow);
    },

    retrieveNode: function(id, dataRow){
        var result = this.el.getById(id, 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-headerId',

    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) {
        return header ? header.getCellSelector() : this.cellSelector; 
    },

    /*
     * 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);
        }
    },

    setHighlightedItem: function(item) {
        var me = this,
            highlighted = me.highlightedItem;

        if (highlighted && me.el.isAncestor(highlighted) && me.isRowStyleFirst(highlighted)) {
            me.toggleRowTableCls(highlighted, me.tableOverFirstCls, false);
        }

        item = me.getNode(item, false); // make sure item is not a "data row"

        if (item && me.isRowStyleFirst(item)) {
            me.toggleRowTableCls(item, me.tableOverFirstCls, true);
        }

        me.callParent(arguments);
    },

    // GridSelectionModel invokes onRowSelect as selection changes
    onRowSelect: function(rowIdx) {
        var me = this,
            beforeSelectedItemCls = me.beforeSelectedItemCls;

        me.addRowCls(rowIdx, me.selectedItemCls);
        if (me.isRowStyleFirst(rowIdx)) {
            me.toggleRowTableCls(rowIdx, me.tableSelectedFirstCls, true);
            if (rowIdx > 0) {
                me.removeRowCls(rowIdx - 1, beforeSelectedItemCls);
            }
        } else {
            me.addRowCls(rowIdx - 1, beforeSelectedItemCls);
        }
    },

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

        me.removeRowCls(rowIdx, [me.selectedItemCls, me.focusedItemCls]);
        if (me.isRowStyleFirst(rowIdx)) {
            me.toggleRowTableCls(rowIdx, [me.tableFocusedFirstCls, me.tableSelectedFirstCls], false);
        } else {
            me.removeRowCls(rowIdx - 1, [me.beforeFocusedItemCls, me.beforeSelectedItemCls]);
        }
    },

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

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

    },

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

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

    getFocusEl: function() {
        return this.focusEl;
    },

    // 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 (me.isRowStyleFirst(rowIdx)) {
                me.toggleRowTableCls(rowIdx, me.tableFocusedFirstCls, true);
            } else {
                me.addRowCls(rowIdx - 1, me.beforeFocusedItemCls);
            }
            if (!supressFocus) {
                me.focusRow(rowIdx);
            }
            //this.el.dom.setAttribute('aria-activedescendant', row.id);
        } else {
            me.removeRowCls(rowIdx, me.focusedItemCls);
            if (me.isRowStyleFirst(rowIdx)) {
                me.toggleRowTableCls(rowIdx, me.tableFocusedFirstCls, false);
            } else {
                me.removeRowCls(rowIdx - 1, me.beforeFocusedItemCls);
            }
        }

        if ((Ext.isIE6 || Ext.isIE7) && !me.ownerCt.rowLines) {
            me.repaintRow(rowIdx)
        }
    },

    /**
     * Focuses a particular row and brings it into view. Will fire the rowfocus event.
     * @param {HTMLElement/String/Number/Ext.data.Model} row An HTMLElement template node, index of a template node, the id of a template node or the
     * @param {Boolean/Number} [delay] Delay the focus this number of milliseconds (true for 10 milliseconds).
     * record associated with the node.
     */
    focusRow: function(row, delay) {
        var me = this,
            record,
            focusTask = me.getFocusTask();

        if (delay) {
            focusTask.delay(Ext.isNumber(delay) ? delay : 10, me.focusRow, me, [row, false]);
            return;
        }

        // An immediate focus call must cancel any outstanding delayed focus calls.
        focusTask.cancel();

        // Do not attempt to focus if hidden or within collapsed Panel
        // Maintainer: Note that to avoid an unnecessary call to me.getNode if not visible, or another, nested if test,
        // the assignment of the row var is embedded inside the condition expression.
        if (me.isVisible(true) && (row = me.getNode(row, true))) {
            me.scrollRowIntoView(row);
            record = me.getRecord(row);
            me.selModel.setLastFocused(record);
            me.doFocus(row);
            me.fireEvent('rowfocus', record, row, me.indexInStore(row));
        }
    },

    scrollRowIntoView: function(row) {
        row = this.getNode(row, true);
        if (row) {
            Ext.fly(row).scrollIntoView(this.el, false);
        }
    },

    /**
     * Focuses a particular cell and brings it into view. Will fire the rowfocus event.
     * @param {Ext.grid.CellContext} pos The cell to select
     * @param {Boolean/Number} [delay] Delay the focus this number of milliseconds (true for 10 milliseconds).
     */
    focusCell: function(position, delay) {
        var me = this,
            cell,
            focusTask = me.getFocusTask();

        if (delay) {
            focusTask.delay(Ext.isNumber(delay) ? delay : 10, me.focusCell, me, [position, false]);
            return;
        }

        // An immediate focus call must cancel any outstanding delayed focus calls.
        focusTask.cancel();

        // Do not attempt to focus if hidden or within collapsed Panel
        // Maintainer: Note that to avoid an unnecessary call to me.getCellByPosition if not visible, or another, nested if test,
        // the assignment of the cell var is embedded inside the condition expression.
        if (me.isVisible(true) && (cell = me.getCellByPosition(position))) {
            me.scrollCellIntoView(cell);
            me.doFocus(me.getNode(position.row));
            me.fireEvent('cellfocus', position.record, cell, position);
        }
    },

    // Private
    // Implementation which saves and restores scroll position on IE.
    doFocus: function(rowDom) {
        var me = this,
            saveScroll = Ext.isIE,
            scrollLeft;

        if (saveScroll) {
            scrollLeft = me.el.getScrollLeft();
            me.ignoreScroll = true;
        }

        (me.focusEl = Ext.get(rowDom)).focus();

        if (saveScroll) {
            me.el.setScrollLeft(scrollLeft);
            me.ignoreScroll = false;
        }
    },

    scrollCellIntoView: function(cell) {
        // Allow cell context object to be passed.
        // TODO: change to .isCellContext check when implement cell context class
        if (cell.row != null && cell.column != null) {
            cell = this.getCellByPosition(cell);
        }
        if (cell) {
            Ext.fly(cell).scrollIntoView(this.el, true);
        }
    },

    /**
     * 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;

        // 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) {
                        firstRow.setHeight(firstRowHeight);
                        secondRow.setHeight(firstRowHeight);
                    } else if (secondRowHeight > firstRowHeight) {
                        firstRow.setHeight(secondRowHeight);
                        secondRow.setHeight(secondRowHeight);
                    }
                }
            }
        }
    },

    onIdChanged: function(store, rec, oldId, newId, oldInternalId){
        var me = this,
            rowDom;

        if (me.viewReady) {
            rowDom = me.getNodeById(oldInternalId);
            if (rowDom) {
                rowDom.setAttribute('data-recordId', rec.internalId);
                rowDom.id = me.getRowId(rec);
            }
        }
    },

    // private
    onUpdate: function(store, record, operation, changedFieldNames) {
        var me = this,
            rowTpl = me.rowTpl,
            index,
            oldRow, oldRowDom, oldDataRow,
            newRowDom,
            newAttrs, attLen, attName, attrIndex,
            overItemCls, beforeOverItemCls,
            focusedItemCls, beforeFocusedItemCls,
            selectedItemCls, beforeSelectedItemCls,
            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;
                beforeOverItemCls = me.beforeOverItemCls;
                focusedItemCls = me.focusedItemCls;
                beforeFocusedItemCls = me.beforeFocusedItemCls;
                selectedItemCls = me.selectedItemCls;
                beforeSelectedItemCls = me.beforeSelectedItemCls;

                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(beforeOverItemCls)) {
                    Ext.fly(newRowDom).addCls(beforeOverItemCls);
                }
                if (oldRow.hasCls(focusedItemCls)) {
                    Ext.fly(newRowDom).addCls(focusedItemCls);
                }
                if (oldRow.hasCls(beforeFocusedItemCls)) {
                    Ext.fly(newRowDom).addCls(beforeFocusedItemCls);
                }
                if (oldRow.hasCls(selectedItemCls)) {
                    Ext.fly(newRowDom).addCls(selectedItemCls);
                }
                if (oldRow.hasCls(beforeSelectedItemCls)) {
                    Ext.fly(newRowDom).addCls(beforeSelectedItemCls);
                }
                columns = me.ownerCt.getVisibleColumnManager().getColumns();

                // Copy new row attributes across. Use IE-specific method if possible.
                // In IE10, there is a problem where the className will not get updated
                // in the view, even though the className on the dom element is correct.
                // See EXTJSIV-9462
                if (Ext.isIE9m && 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 && (oldDataRow = me.getNode(oldRowDom, true))) {
                    me.updateColumns(record, oldDataRow, 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(record, oldRowDom, newRowDom, columns, changedFieldNames) {
        var me = this,
            newAttrs, attLen, attName, attrIndex,
            colCount = columns.length,
            colIndex,
            column,
            oldCell, newCell,
            row,

            // See if our View has an editingPlugin, or if we are a locking twin, see if the top LockingView
            // has an editingPlugin.
            // We do not support one editing plugin on the top lockable and some other on the twinned views.
            editingPlugin = me.editingPlugin || (me.lockingPartner && me.ownerCt.ownerLockable.view.editingPlugin),

            // See if the found editing plugin is active.
            isEditing = editingPlugin && 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(record, 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(record, 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 no changeFieldNames array was passed, we have to assume that that information
        // is unknown and update all cells.
        if (column.hasCustomRenderer || !changedFieldNames) {
            return true;
        }

        if (changedFieldNames) {
            var len = changedFieldNames.length,
                i, field;

            for (i = 0; i < len; ++i) {
                field = changedFieldNames[i];
                if (field === column.dataIndex || field === record.idProperty) {
                    return true;
                }
            }
        }
        return false;
    },

    /**
     * Refreshes the grid view. Sets the sort state and focuses the previously focused row.
     */
    refresh: function() {
        var me = this;

        me.callParent(arguments);
        me.headerCt.setSortState();

        // Create horizontal stretcher element if no records in view and there is overflow of the header container.
        // Element will be transient and destroyed by the next refresh.
        if (me.el && !me.all.getCount() && me.headerCt && me.headerCt.tooNarrow) {
            me.el.createChild({
                role: 'presentation',
                style:'position:absolute;height:1px;width:1px;left:' + (me.headerCt.getFullWidth() - 1) + 'px'
            });
        }

        me.refreshSelection();
    },

    refreshSelection: function() {
        var me = this,
            selModel = me.selModel,
            selected, len, i;

        if (selModel.isRowModel) {
            selected = selModel.selected.items;
            len = selected.length;
            for (i = 0; i < len; i++) {
                me.onRowSelect(me.indexOf(me.getNode(selected[i])));
            }
        }
        me.selModel.onLastFocusChanged(null, me.selModel.lastFocused, true);
    },

    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, header;

            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.
            if (cell) {
                if (!cell.parentNode) {
                    // If we have no parentNode, the td has been removed from the DOM, probably via an update,
                    // so just jump out since the target for the event isn't valid
                    return false;
                }
                // We can't use the cellIndex because we don't render hidden columns
                header = me.getHeaderByCell(cell);

                // Find the index of the header in the *full* (including hidden columns) leaf column set.
                // Because In 4.0.0 we rendered hidden cells, and the cellIndex included the hidden ones.
                cellIndex = me.ownerCt.getColumnManager().getHeaderIndex(header);
            } else {
                cellIndex = -1;
            }

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

            if ((result === false || me.callParent(arguments) === false) && selModel.onVetoUIEvent) {
                selModel.onVetoUIEvent(type, me, cell, rowIndex, cellIndex, e, record, row);
                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} header 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,
            columnSizer = me.body.select(me.getColumnSizerSelector(header)),
            max = Math.max,
            widthAdjust = 0,
            maxWidth;

        if (ln > 0) {
            if (Ext.supports.ScrollWidthInlinePaddingBug) {
                widthAdjust += me.getCellPaddingAfter(cells[0]);
            }
            if (me.columnLines) {
                widthAdjust += Ext.fly(cells[0].parentNode).getBorderWidth('lr');
            }
        }

        // Set column width to 1px so we can detect the content width by measuring scrollWidth
        columnSizer.setWidth(1);

        // We are about to measure the offsetWidth of the textEl to determine how much
        // space the text occupies, but it will not report the correct width if the titleEl
        // has text-overflow:ellipsis.  Set text-overflow to 'clip' before proceeding to
        // ensure we get the correct measurement.
        header.titleEl.setStyle('text-overflow', 'clip');

        // Allow for padding round text of header
        maxWidth = header.textEl.dom.offsetWidth + header.titleEl.getPadding('lr');

        // revert to using text-overflow defined by the stylesheet
        header.titleEl.setStyle('text-overflow', '');

        for (; i < ln; i++) {
            maxWidth = max(maxWidth, cells[i].scrollWidth);
        }

        // in some browsers, the "after" padding is not accounted for in the scrollWidth
        maxWidth += widthAdjust;

        // 40 is the minimum column width.  TODO: should this be configurable?
        maxWidth = max(maxWidth, 40);

        // Set column width back to original width
        columnSizer.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 match = cell.className.match(this.cellRe);
            if (match && match[1]) {
                return this.ownerCt.getVisibleColumnManager().getHeaderById(match[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 {Ext.grid.CellContext} An object encapsulating the unique cell position.
     *
     * @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(),
            allCols      = me.ownerCt.getColumnManager(),
            visible      = me.ownerCt.getVisibleColumnManager(),
            firstIndex   = allCols.getHeaderIndex(visible.getFirst()),
            lastIndex    = allCols.getHeaderIndex(visible.getLast()),
            newRow       = row,
            newColumn    = column,
            activeHeader = allCols.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 === lastIndex) {
                    // if bottom row and last column, deny right
                    if (preventWrap || row === rowCount - 1) {
                        return false;
                    }
                    if (!e.ctrlKey) {
                        // otherwise wrap to nextRow and firstCol
                        newRow = me.walkRows(row, 1);
                        if (newRow !== row) {
                            newColumn = firstIndex;
                        }
                    }
                // go right
                } else {
                    if (!e.ctrlKey) {
                        newColumn = allCols.getHeaderIndex(visible.getNextSibling(activeHeader));
                    } else {
                        newColumn = lastIndex;
                    }
                }
                break;

            case 'left':
                // has the potential to wrap
                if (column === firstIndex) {
                    // if top row and first column, deny left
                    if (preventWrap || row === 0) {
                        return false;
                    }
                    if (!e.ctrlKey) {
                        // otherwise wrap to prevRow and lastIndex
                        newRow = me.walkRows(row, -1);
                        if (newRow !== row) {
                            newColumn = lastIndex;
                        }
                    }
                // go left
                } else {
                    if (!e.ctrlKey) {
                        newColumn = allCols.getHeaderIndex(visible.getPreviousSibling(activeHeader));
                    } else {
                        newColumn = firstIndex;
                    }
                }
                break;

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

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

        if (verifierFn && verifierFn.call(scope || me, {row: newRow, column: newColumn}) !== true) {
            return false;
        }

        newColumn = allCols.getHeaderAtIndex(newColumn);
        return new Ext.grid.CellContext(me).setPosition(newRow, newColumn);
    },

    /**
     * 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.buffered ? me.dataSource.getTotalCount() : 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.buffered ? me.store.getTotalCount() : 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.buffered ? me.dataSource.getTotalCount() : me.dataSource.getCount()),
            result = me.indexOf(me.all.first()) - 1;

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

    getLastVisibleRowIndex: function() {
        var me = this,
            result = me.indexOf(me.all.last());

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

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

    getPosition: function(record, header) {
        return new Ext.grid.CellContext(this).setPosition(record, header);
    },

    beforeDestroy: function() {
        var me = this;

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

    onDestroy: function() {
        var me = this,
            features = me.featuresMC,
            len,
            i;

        if (features) {
            for (i = 0, len = features.getCount(); i < len; ++i) {
                features.getAt(i).destroy();
            }
        }
        me.featuresMC = null;
        this.callParent(arguments);
    },

    // after adding a row stripe rows from then on
    onAdd: function(ds, records, index) {
        var me = this,
            selModel = me.selModel,
            nextIndex, isNextRowSelected, isNextRowFocused;

        me.callParent(arguments);
        me.doStripeRows(index);

        if (me.rendered && selModel.isRowModel && !records[0].isCollapsedPlaceholder) {
            nextIndex = index + records.length;
            isNextRowSelected = selModel.isRowSelected(nextIndex);
            isNextRowFocused = me.indexOf(selModel.lastFocused) === (nextIndex);
            if (isNextRowSelected || isNextRowFocused) {
                // calling onRowDeselect ensures before focus/selected border gets removed from
                // previous row if the row previously at this index was selected or focused.
                me.onRowDeselect(index);
            }
            if (isNextRowSelected) {
                me.onRowSelect(nextIndex);
            }
            if (selModel.isRowSelected(index)) {
                me.onRowSelect(index);
            }
        }
        me.selModel.onLastFocusChanged(null, me.selModel.lastFocused, true);
    },

    // after removing a row stripe rows from then on
    onRemove: function(ds, records, indexes) {
        var me = this,
            index,
            selModel = me.selModel,
            len = indexes.length,
            i = 0,
            delta = 0,
            record;

        // Ensure "beforeSelected" classes are maintained correctly.
        // Some of the removed rows my have been carrying the beforeSelected class.
        me.callParent(arguments);
        me.doStripeRows(indexes[0]);

        if (me.rendered && selModel.isRowModel && !records[0].isCollapsedPlaceholder) {
            for (; i < len; i++, delta++) {
                index = indexes[i] - delta;
                record = me.store.getAt(index);
                me.onRowDeselect(index);
                if (selModel.isRowSelected(record) && me.getNode(record)) {
                    me.onRowSelect(index);
                }
            }
        }
        selModel.onLastFocusChanged(null, selModel.lastFocused, true);
    },

    /**
     * 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);
                }
            }
        }
    },

    repaintRow: function(rowIdx) {
        var node = this.getNode(rowIdx),
            tds,
            i;

        if (node) {
            tds = node.childNodes;
            i = tds.length;
            while (i--) {
                tds[i].className = tds[i].className;
            }
        }
    },

    // private
    // returns the table that gains a top border when the first grid row is focused, selected,
    // or hovered.  Usually the main grid table but can be a sub-table, if a grouping
    // feature is being used.
    getRowStyleTableEl: function(item /* view item or row index */) {
       var me = this;

        if (!item.tagName) {
            item = this.getNode(item);
        }
        return (me.hasActiveGrouping() ? Ext.fly(item) : this.el).down('table.' + Ext.baseCSSPrefix + 'grid-table');
    },

    // private
    // adds or removes a css class to or from the table that gains a top border when the
    // first grid row is focused, selected, or hovered.  Usually the main grid table but
    // can be a sub-table, if a grouping feature is being used.
    toggleRowTableCls: function(item /* view item or row index */, cls, enabled) {
       var me = this,
           table, root;

        if (!item.tagName) {
            item = this.getNode(item);
        }

        root = me.isGrouping ? Ext.fly(item) : this.el;

        if (root) {
            table = root.down('table.' + Ext.baseCSSPrefix + 'grid-table');
        }

        if (table) {
            table[enabled ? 'addCls' : 'removeCls'](cls);
        }
    },

    // private
    // returns true if the row should be treated as the first row stylistically.  Typically
    // only returns true for the first row in a grid.  Returns true for the first row
    // in each group of a grouped grid.
    isRowStyleFirst: function(item /* view item or row index */) {
        var me = this,
            index;

        // An item not in the view
        if (item === -1) {
            return false;
        }

        if (!item.tagName) {
            index = item;
            item = this.getNode(item);
        } else {
            index = me.indexOf(item);
        }

        return (!index || me.hasActiveGrouping() && item && Ext.fly(item).hasCls(Ext.baseCSSPrefix + 'grid-group-row'));
    },

    hasActiveGrouping: function(){
        return this.isGrouping && this.store.isGrouped();
    },

    getCellPaddingAfter: function(cell) {
        return Ext.fly(cell).getPadding('r');
    },

    hasVerticalScroll: function(){
        var me = this,
            first;

        if (me.ownerCt.isLocked || !me.scrollFlags.y) {
            return false;
        }

        first = me.el.down('table');
        if (!first) {
            return false;
        }

        return this.getHeight() < first.getHeight(); 
    }

});