Upgrade to ExtJS 3.2.0 - Released 03/30/2010
[extjs.git] / src / widgets / grid / GridView.js
index 407b7aa..eabb487 100644 (file)
@@ -1,6 +1,6 @@
 /*!
- * Ext JS Library 3.1.0
- * Copyright(c) 2006-2009 Ext JS, LLC
+ * Ext JS Library 3.2.0
+ * Copyright(c) 2006-2010 Ext JS, Inc.
  * licensing@extjs.com
  * http://www.extjs.com/license
  */
@@ -34,7 +34,7 @@ viewConfig: {
         }
         return 'x-grid3-row-collapsed';
     }
-},     
+},
     </code></pre>
      * @param {Record} record The {@link Ext.data.Record} corresponding to the current row.
      * @param {Number} index The row index.
@@ -55,10 +55,12 @@ viewConfig: {
      * @method getRowClass
      * @return {String} a CSS class name to add to the row.
      */
+
     /**
      * @cfg {Boolean} enableRowBody True to add a second TR element per row that can be used to provide a row body
      * that spans beneath the data row.  Use the {@link #getRowClass} method's rowParams config to customize the row body.
      */
+
     /**
      * @cfg {String} emptyText Default text (html tags are accepted) to display in the grid body when no rows
      * are available (defaults to ''). This value will be used to update the <tt>{@link #mainBody}</tt>:
@@ -66,8 +68,9 @@ viewConfig: {
     this.mainBody.update('&lt;div class="x-grid-empty">' + this.emptyText + '&lt;/div>');
     </code></pre>
      */
+
     /**
-     * @cfg {Boolean} headersDisabled True to disable the grid column headers (defaults to <tt>false</tt>). 
+     * @cfg {Boolean} headersDisabled True to disable the grid column headers (defaults to <tt>false</tt>).
      * Use the {@link Ext.grid.ColumnModel ColumnModel} <tt>{@link Ext.grid.ColumnModel#menuDisabled menuDisabled}</tt>
      * config to disable the <i>menu</i> for individual columns.  While this config is true the
      * following will be disabled:<div class="mdetail-params"><ul>
@@ -75,6 +78,7 @@ viewConfig: {
      * <li>the trigger to reveal the menu.</li>
      * </ul></div>
      */
+
     /**
      * <p>A customized implementation of a {@link Ext.dd.DragZone DragZone} which provides default implementations
      * of the template methods of DragZone to enable dragging of the selected rows of a GridPanel.
@@ -86,27 +90,31 @@ viewConfig: {
      * @property dragZone
      * @type {Ext.grid.GridDragZone}
      */
+
     /**
      * @cfg {Boolean} deferEmptyText True to defer <tt>{@link #emptyText}</tt> being applied until the store's
      * first load (defaults to <tt>true</tt>).
      */
     deferEmptyText : true,
+
     /**
      * @cfg {Number} scrollOffset The amount of space to reserve for the vertical scrollbar
      * (defaults to <tt>undefined</tt>). If an explicit value isn't specified, this will be automatically
      * calculated.
      */
     scrollOffset : undefined,
+
     /**
      * @cfg {Boolean} autoFill
      * Defaults to <tt>false</tt>.  Specify <tt>true</tt> to have the column widths re-proportioned
-     * when the grid is <b>initially rendered</b>.  The 
+     * when the grid is <b>initially rendered</b>.  The
      * {@link Ext.grid.Column#width initially configured width}</tt> of each column will be adjusted
      * to fit the grid width and prevent horizontal scrolling. If columns are later resized (manually
      * or programmatically), the other columns in the grid will <b>not</b> be resized to fit the grid width.
      * See <tt>{@link #forceFit}</tt> also.
      */
     autoFill : false,
+
     /**
      * @cfg {Boolean} forceFit
      * Defaults to <tt>false</tt>.  Specify <tt>true</tt> to have the column widths re-proportioned
@@ -116,18 +124,22 @@ viewConfig: {
      * to fit the grid width. See <tt>{@link #autoFill}</tt> also.
      */
     forceFit : false,
+
     /**
      * @cfg {Array} sortClasses The CSS classes applied to a header when it is sorted. (defaults to <tt>['sort-asc', 'sort-desc']</tt>)
      */
     sortClasses : ['sort-asc', 'sort-desc'],
+
     /**
      * @cfg {String} sortAscText The text displayed in the 'Sort Ascending' menu item (defaults to <tt>'Sort Ascending'</tt>)
      */
     sortAscText : 'Sort Ascending',
+
     /**
      * @cfg {String} sortDescText The text displayed in the 'Sort Descending' menu item (defaults to <tt>'Sort Descending'</tt>)
      */
     sortDescText : 'Sort Descending',
+
     /**
      * @cfg {String} columnsText The text displayed in the 'Columns' menu item (defaults to <tt>'Columns'</tt>)
      */
@@ -164,7 +176,7 @@ viewConfig: {
      * @cfg {Number} rowSelectorDepth The number of levels to search for rows in event delegation (defaults to <tt>10</tt>)
      */
     rowSelectorDepth : 10,
-    
+
     /**
      * @cfg {Number} rowBodySelectorDepth The number of levels to search for row bodies in event delegation (defaults to <tt>10</tt>)
      */
@@ -178,75 +190,75 @@ viewConfig: {
      * @cfg {String} rowSelector The selector used to find rows internally (defaults to <tt>'div.x-grid3-row'</tt>)
      */
     rowSelector : 'div.x-grid3-row',
-    
+
     /**
      * @cfg {String} rowBodySelector The selector used to find row bodies internally (defaults to <tt>'div.x-grid3-row'</tt>)
      */
     rowBodySelector : 'div.x-grid3-row-body',
-    
+
     // private
     firstRowCls: 'x-grid3-row-first',
     lastRowCls: 'x-grid3-row-last',
     rowClsRe: /(?:^|\s+)x-grid3-row-(first|last|alt)(?:\s+|$)/g,
-    
+
     constructor : function(config){
         Ext.apply(this, config);
-           // These events are only used internally by the grid components
-           this.addEvents(
-               /**
-                * @event beforerowremoved
-                * Internal UI Event. Fired before a row is removed.
-                * @param {Ext.grid.GridView} view
-                * @param {Number} rowIndex The index of the row to be removed.
-                * @param {Ext.data.Record} record The Record to be removed
-                */
-               'beforerowremoved',
-               /**
-                * @event beforerowsinserted
-                * Internal UI Event. Fired before rows are inserted.
-                * @param {Ext.grid.GridView} view
-                * @param {Number} firstRow The index of the first row to be inserted.
-                * @param {Number} lastRow The index of the last row to be inserted.
-                */
-               'beforerowsinserted',
-               /**
-                * @event beforerefresh
-                * Internal UI Event. Fired before the view is refreshed.
-                * @param {Ext.grid.GridView} view
-                */
-               'beforerefresh',
-               /**
-                * @event rowremoved
-                * Internal UI Event. Fired after a row is removed.
-                * @param {Ext.grid.GridView} view
-                * @param {Number} rowIndex The index of the row that was removed.
-                * @param {Ext.data.Record} record The Record that was removed
-                */
-               'rowremoved',
-               /**
-                * @event rowsinserted
-                * Internal UI Event. Fired after rows are inserted.
-                * @param {Ext.grid.GridView} view
-                * @param {Number} firstRow The index of the first inserted.
-                * @param {Number} lastRow The index of the last row inserted.
-                */
-               'rowsinserted',
-               /**
-                * @event rowupdated
-                * Internal UI Event. Fired after a row has been updated.
-                * @param {Ext.grid.GridView} view
-                * @param {Number} firstRow The index of the row updated.
-                * @param {Ext.data.record} record The Record backing the row updated.
-                */
-               'rowupdated',
-               /**
-                * @event refresh
-                * Internal UI Event. Fired after the GridView's body has been refreshed.
-                * @param {Ext.grid.GridView} view
-                */
-               'refresh'
-           );
-           Ext.grid.GridView.superclass.constructor.call(this);    
+        // These events are only used internally by the grid components
+        this.addEvents(
+            /**
+             * @event beforerowremoved
+             * Internal UI Event. Fired before a row is removed.
+             * @param {Ext.grid.GridView} view
+             * @param {Number} rowIndex The index of the row to be removed.
+             * @param {Ext.data.Record} record The Record to be removed
+             */
+            'beforerowremoved',
+            /**
+             * @event beforerowsinserted
+             * Internal UI Event. Fired before rows are inserted.
+             * @param {Ext.grid.GridView} view
+             * @param {Number} firstRow The index of the first row to be inserted.
+             * @param {Number} lastRow The index of the last row to be inserted.
+             */
+            'beforerowsinserted',
+            /**
+             * @event beforerefresh
+             * Internal UI Event. Fired before the view is refreshed.
+             * @param {Ext.grid.GridView} view
+             */
+            'beforerefresh',
+            /**
+             * @event rowremoved
+             * Internal UI Event. Fired after a row is removed.
+             * @param {Ext.grid.GridView} view
+             * @param {Number} rowIndex The index of the row that was removed.
+             * @param {Ext.data.Record} record The Record that was removed
+             */
+            'rowremoved',
+            /**
+             * @event rowsinserted
+             * Internal UI Event. Fired after rows are inserted.
+             * @param {Ext.grid.GridView} view
+             * @param {Number} firstRow The index of the first inserted.
+             * @param {Number} lastRow The index of the last row inserted.
+             */
+            'rowsinserted',
+            /**
+             * @event rowupdated
+             * Internal UI Event. Fired after a row has been updated.
+             * @param {Ext.grid.GridView} view
+             * @param {Number} firstRow The index of the row updated.
+             * @param {Ext.data.record} record The Record backing the row updated.
+             */
+            'rowupdated',
+            /**
+             * @event refresh
+             * Internal UI Event. Fired after the GridView's body has been refreshed.
+             * @param {Ext.grid.GridView} view
+             */
+            'refresh'
+        );
+        Ext.grid.GridView.superclass.constructor.call(this);
     },
 
     /* -------------------------------- UI Specific ----------------------------- */
@@ -256,31 +268,31 @@ viewConfig: {
         var ts = this.templates || {};
         if(!ts.master){
             ts.master = new Ext.Template(
-                    '<div class="x-grid3" hidefocus="true">',
-                        '<div class="x-grid3-viewport">',
-                            '<div class="x-grid3-header"><div class="x-grid3-header-inner"><div class="x-grid3-header-offset" style="{ostyle}">{header}</div></div><div class="x-clear"></div></div>',
-                            '<div class="x-grid3-scroller"><div class="x-grid3-body" style="{bstyle}">{body}</div><a href="#" class="x-grid3-focus" tabIndex="-1"></a></div>',
-                        '</div>',
-                        '<div class="x-grid3-resize-marker">&#160;</div>',
-                        '<div class="x-grid3-resize-proxy">&#160;</div>',
-                    '</div>'
-                    );
+                '<div class="x-grid3" hidefocus="true">',
+                    '<div class="x-grid3-viewport">',
+                        '<div class="x-grid3-header"><div class="x-grid3-header-inner"><div class="x-grid3-header-offset" style="{ostyle}">{header}</div></div><div class="x-clear"></div></div>',
+                        '<div class="x-grid3-scroller"><div class="x-grid3-body" style="{bstyle}">{body}</div><a href="#" class="x-grid3-focus" tabIndex="-1"></a></div>',
+                    '</div>',
+                    '<div class="x-grid3-resize-marker">&#160;</div>',
+                    '<div class="x-grid3-resize-proxy">&#160;</div>',
+                '</div>'
+            );
         }
 
         if(!ts.header){
             ts.header = new Ext.Template(
-                    '<table border="0" cellspacing="0" cellpadding="0" style="{tstyle}">',
-                    '<thead><tr class="x-grid3-hd-row">{cells}</tr></thead>',
-                    '</table>'
-                    );
+                '<table border="0" cellspacing="0" cellpadding="0" style="{tstyle}">',
+                '<thead><tr class="x-grid3-hd-row">{cells}</tr></thead>',
+                '</table>'
+            );
         }
 
         if(!ts.hcell){
             ts.hcell = new Ext.Template(
-                    '<td class="x-grid3-hd x-grid3-cell x-grid3-td-{id} {css}" style="{style}"><div {tooltip} {attr} class="x-grid3-hd-inner x-grid3-hd-{id}" unselectable="on" style="{istyle}">', this.grid.enableHdMenu ? '<a class="x-grid3-hd-btn" href="#"></a>' : '',
-                    '{value}<img class="x-grid3-sort-icon" src="', Ext.BLANK_IMAGE_URL, '" />',
-                    '</div></td>'
-                    );
+                '<td class="x-grid3-hd x-grid3-cell x-grid3-td-{id} {css}" style="{style}"><div {tooltip} {attr} class="x-grid3-hd-inner x-grid3-hd-{id}" unselectable="on" style="{istyle}">', this.grid.enableHdMenu ? '<a class="x-grid3-hd-btn" href="#"></a>' : '',
+                '{value}<img class="x-grid3-sort-icon" src="', Ext.BLANK_IMAGE_URL, '" />',
+                '</div></td>'
+            );
         }
 
         if(!ts.body){
@@ -289,11 +301,11 @@ viewConfig: {
 
         if(!ts.row){
             ts.row = new Ext.Template(
-                    '<div class="x-grid3-row {alt}" style="{tstyle}"><table class="x-grid3-row-table" border="0" cellspacing="0" cellpadding="0" style="{tstyle}">',
-                    '<tbody><tr>{cells}</tr>',
-                    (this.enableRowBody ? '<tr class="x-grid3-row-body-tr" style="{bodyStyle}"><td colspan="{cols}" class="x-grid3-body-cell" tabIndex="0" hidefocus="on"><div class="x-grid3-row-body">{body}</div></td></tr>' : ''),
-                    '</tbody></table></div>'
-                    );
+                '<div class="x-grid3-row {alt}" style="{tstyle}"><table class="x-grid3-row-table" border="0" cellspacing="0" cellpadding="0" style="{tstyle}">',
+                '<tbody><tr>{cells}</tr>',
+                (this.enableRowBody ? '<tr class="x-grid3-row-body-tr" style="{bodyStyle}"><td colspan="{cols}" class="x-grid3-body-cell" tabIndex="0" hidefocus="on"><div class="x-grid3-row-body">{body}</div></td></tr>' : ''),
+                '</tbody></table></div>'
+            );
         }
 
         if(!ts.cell){
@@ -439,7 +451,7 @@ viewConfig: {
         var r = this.findRow(el);
         return r ? r.rowIndex : false;
     },
-    
+
     /**
      * Return the HtmlElement representing the grid row body which contains the passed element.
      * @param {HTMLElement} el The target HTMLElement
@@ -479,7 +491,7 @@ viewConfig: {
      * @return {HtmlElement} The td element.
      */
     getHeaderCell : function(index){
-      return this.mainHd.dom.getElementsByTagName('td')[index];
+        return this.mainHd.dom.getElementsByTagName('td')[index];
     },
 
     // manipulating elements
@@ -505,7 +517,7 @@ viewConfig: {
         Ext.removeNode(this.getRow(row));
         this.syncFocusEl(row);
     },
-    
+
     // private
     removeRows : function(firstRow, lastRow){
         var bd = this.mainBody.dom;
@@ -540,8 +552,8 @@ viewConfig: {
 
     // private
     syncScroll : function(){
-      this.syncHeaderScroll();
-      var mb = this.scroller.dom;
+        this.syncHeaderScroll();
+        var mb = this.scroller.dom;
         this.grid.fireEvent('bodyscroll', mb.scrollLeft, mb.scrollTop);
     },
 
@@ -561,17 +573,20 @@ viewConfig: {
 
     // private
     updateAllColumnWidths : function(){
-        var tw = this.getTotalWidth(),
+        var tw   = this.getTotalWidth(),
             clen = this.cm.getColumnCount(),
-            ws = [],
+            ws   = [],
             len,
             i;
+
         for(i = 0; i < clen; i++){
             ws[i] = this.getColumnWidth(i);
         }
+
         this.innerHd.firstChild.style.width = this.getOffsetWidth();
         this.innerHd.firstChild.firstChild.style.width = tw;
         this.mainBody.dom.style.width = tw;
+
         for(i = 0; i < clen; i++){
             var hd = this.getHeaderCell(i);
             hd.style.width = ws[i];
@@ -642,75 +657,120 @@ viewConfig: {
         this.layout();
     },
 
-    // private
-    doRender : function(cs, rs, ds, startRow, colCount, stripe){
-        var ts = this.templates, ct = ts.cell, rt = ts.row, last = colCount-1;
-        var tstyle = 'width:'+this.getTotalWidth()+';';
+    /**
+     * @private
+     * Renders all of the rows to a string buffer and returns the string. This is called internally
+     * by renderRows and performs the actual string building for the rows - it does not inject HTML into the DOM.
+     * @param {Array} columns The column data acquired from getColumnData.
+     * @param {Array} records The array of records to render
+     * @param {Ext.data.Store} store The store to render the rows from
+     * @param {Number} startRow The index of the first row being rendered. Sometimes we only render a subset of
+     * the rows so this is used to maintain logic for striping etc
+     * @param {Number} colCount The total number of columns in the column model
+     * @param {Boolean} stripe True to stripe the rows
+     * @return {String} A string containing the HTML for the rendered rows
+     */
+    doRender : function(columns, records, store, startRow, colCount, stripe) {
+        var templates    = this.templates,
+            cellTemplate = templates.cell,
+            rowTemplate  = templates.row,
+            last         = colCount - 1;
+
+        var tstyle = 'width:' + this.getTotalWidth() + ';';
+
         // buffers
-        var buf = [], cb, c, p = {}, rp = {tstyle: tstyle}, r;
-        for(var j = 0, len = rs.length; j < len; j++){
-            r = rs[j]; cb = [];
-            var rowIndex = (j+startRow);
-            for(var i = 0; i < colCount; i++){
-                c = cs[i];
-                p.id = c.id;
-                p.css = i === 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : '');
-                p.attr = p.cellAttr = '';
-                p.value = c.renderer.call(c.scope, r.data[c.name], p, r, rowIndex, i, ds);
-                p.style = c.style;
-                if(Ext.isEmpty(p.value)){
-                    p.value = '&#160;';
+        var rowBuffer = [],
+            colBuffer = [],
+            rowParams = {tstyle: tstyle},
+            meta      = {},
+            column,
+            record;
+
+        //build up each row's HTML
+        for (var j = 0, len = records.length; j < len; j++) {
+            record    = records[j];
+            colBuffer = [];
+
+            var rowIndex = j + startRow;
+
+            //build up each column's HTML
+            for (var i = 0; i < colCount; i++) {
+                column = columns[i];
+
+                meta.id    = column.id;
+                meta.css   = i === 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : '');
+                meta.attr  = meta.cellAttr = '';
+                meta.style = column.style;
+                meta.value = column.renderer.call(column.scope, record.data[column.name], meta, record, rowIndex, i, store);
+
+                if (Ext.isEmpty(meta.value)) {
+                    meta.value = '&#160;';
                 }
-                if(this.markDirty && r.dirty && Ext.isDefined(r.modified[c.name])){
-                    p.css += ' x-grid3-dirty-cell';
+
+                if (this.markDirty && record.dirty && Ext.isDefined(record.modified[column.name])) {
+                    meta.css += ' x-grid3-dirty-cell';
                 }
-                cb[cb.length] = ct.apply(p);
+
+                colBuffer[colBuffer.length] = cellTemplate.apply(meta);
             }
+
+            //set up row striping and row dirtiness CSS classes
             var alt = [];
-            if(stripe && ((rowIndex+1) % 2 === 0)){
+
+            if (stripe && ((rowIndex + 1) % 2 === 0)) {
                 alt[0] = 'x-grid3-row-alt';
             }
-            if(r.dirty){
+
+            if (record.dirty) {
                 alt[1] = ' x-grid3-dirty-row';
             }
-            rp.cols = colCount;
-            if(this.getRowClass){
-                alt[2] = this.getRowClass(r, rowIndex, rp, ds);
+
+            rowParams.cols = colCount;
+
+            if (this.getRowClass) {
+                alt[2] = this.getRowClass(record, rowIndex, rowParams, store);
             }
-            rp.alt = alt.join(' ');
-            rp.cells = cb.join('');
-            buf[buf.length] =  rt.apply(rp);
+
+            rowParams.alt   = alt.join(' ');
+            rowParams.cells = colBuffer.join('');
+
+            rowBuffer[rowBuffer.length] = rowTemplate.apply(rowParams);
         }
-        return buf.join('');
+
+        return rowBuffer.join('');
     },
 
     // private
-    processRows : function(startRow, skipStripe){
-        if(!this.ds || this.ds.getCount() < 1){
+    processRows : function(startRow, skipStripe) {
+        if (!this.ds || this.ds.getCount() < 1) {
             return;
         }
+
         var rows = this.getRows(),
-            len = rows.length,
+            len  = rows.length,
             i, r;
-            
+
         skipStripe = skipStripe || !this.grid.stripeRows;
-        startRow = startRow || 0;
-        for(i = 0; i<len; i++) {
+        startRow   = startRow   || 0;
+
+        for (i = 0; i<len; i++) {
             r = rows[i];
-            if(r) {
+            if (r) {
                 r.rowIndex = i;
-                if(!skipStripe){
+                if (!skipStripe) {
                     r.className = r.className.replace(this.rowClsRe, ' ');
                     if ((i + 1) % 2 === 0){
                         r.className += ' x-grid3-row-alt';
                     }
-                }   
-            }          
+                }
+            }
         }
+
         // add first/last-row classes
-        if(startRow === 0){
+        if (startRow === 0) {
             Ext.fly(rows[0]).addClass(this.firstRowCls);
         }
+
         Ext.fly(rows[rows.length - 1]).addClass(this.lastRowCls);
     },
 
@@ -727,18 +787,22 @@ viewConfig: {
         this.grid.fireEvent('viewready', this.grid);
     },
 
-    // private
-    renderUI : function(){
-
-        var header = this.renderHeaders();
-        var body = this.templates.body.apply({rows:'&#160;'});
-
+    /**
+     * @private
+     * Renders each of the UI elements in turn. This is called internally, once, by this.render. It does not
+     * render rows from the store, just the surrounding UI elements. It also sets up listeners on the UI elements
+     * and sets up options like column menus, moving and resizing.
+     */
+    renderUI : function() {
+        var templates = this.templates,
+            header    = this.renderHeaders(),
+            body      = templates.body.apply({rows:'&#160;'});
 
-        var html = this.templates.master.apply({
-            body: body,
+        var html = templates.master.apply({
+            body  : body,
             header: header,
-            ostyle: 'width:'+this.getOffsetWidth()+';',
-            bstyle: 'width:'+this.getTotalWidth()+';'
+            ostyle: 'width:' + this.getOffsetWidth() + ';',
+            bstyle: 'width:' + this.getTotalWidth()  + ';'
         });
 
         var g = this.grid;
@@ -749,35 +813,37 @@ viewConfig: {
 
         // get mousedowns early
         Ext.fly(this.innerHd).on('click', this.handleHdDown, this);
+
         this.mainHd.on({
-            scope: this,
+            scope    : this,
             mouseover: this.handleHdOver,
-            mouseout: this.handleHdOut,
+            mouseout : this.handleHdOut,
             mousemove: this.handleHdMove
         });
 
         this.scroller.on('scroll', this.syncScroll,  this);
-        if(g.enableColumnResize !== false){
+        if (g.enableColumnResize !== false) {
             this.splitZone = new Ext.grid.GridView.SplitDragZone(g, this.mainHd.dom);
         }
 
-        if(g.enableColumnMove){
+        if (g.enableColumnMove) {
             this.columnDrag = new Ext.grid.GridView.ColumnDragZone(g, this.innerHd);
             this.columnDrop = new Ext.grid.HeaderDropZone(g, this.mainHd.dom);
         }
 
-        if(g.enableHdMenu !== false){
+        if (g.enableHdMenu !== false) {
             this.hmenu = new Ext.menu.Menu({id: g.id + '-hctx'});
             this.hmenu.add(
-                {itemId:'asc', text: this.sortAscText, cls: 'xg-hmenu-sort-asc'},
+                {itemId:'asc',  text: this.sortAscText,  cls: 'xg-hmenu-sort-asc'},
                 {itemId:'desc', text: this.sortDescText, cls: 'xg-hmenu-sort-desc'}
             );
-            if(g.enableColumnHide !== false){
+
+            if (g.enableColumnHide !== false) {
                 this.colMenu = new Ext.menu.Menu({id:g.id + '-hcols-menu'});
                 this.colMenu.on({
-                    scope: this,
+                    scope     : this,
                     beforeshow: this.beforeColMenuShow,
-                    itemclick: this.handleHdMenuClick
+                    itemclick : this.handleHdMenuClick
                 });
                 this.hmenu.add('-', {
                     itemId:'columns',
@@ -787,32 +853,58 @@ viewConfig: {
                     iconCls: 'x-cols-icon'
                 });
             }
+
             this.hmenu.on('itemclick', this.handleHdMenuClick, this);
         }
 
-        if(g.trackMouseOver){
+        if (g.trackMouseOver) {
             this.mainBody.on({
-                scope: this,
+                scope    : this,
                 mouseover: this.onRowOver,
-                mouseout: this.onRowOut
+                mouseout : this.onRowOut
             });
         }
 
-        if(g.enableDragDrop || g.enableDrag){
+        if (g.enableDragDrop || g.enableDrag) {
             this.dragZone = new Ext.grid.GridDragZone(g, {
                 ddGroup : g.ddGroup || 'GridDD'
             });
         }
 
         this.updateHeaderSortState();
-
     },
-    
+
     // private
-    processEvent: Ext.emptyFn,
+    processEvent : function(name, e) {
+        var t = e.getTarget(),
+            g = this.grid,
+            header = this.findHeaderIndex(t);
+        g.fireEvent(name, e);
+        if (header !== false) {
+            g.fireEvent('header' + name, g, header, e);
+        } else {
+            var row = this.findRowIndex(t),
+                cell,
+                body;
+            if (row !== false) {
+                g.fireEvent('row' + name, g, row, e);
+                cell = this.findCellIndex(t);
+                if (cell !== false) {
+                    g.fireEvent('cell' + name, g, row, cell, e);
+                } else {
+                    body = this.findRowBody(t);
+                    if (body) {
+                        g.fireEvent('rowbody' + name, g, row, e);
+                    }
+                }
+            } else {
+                g.fireEvent('container' + name, g, e);
+            }
+        }
+    },
 
     // private
-    layout : function(){
+    layout : function() {
         if(!this.mainBody){
             return; // not rendered
         }
@@ -824,7 +916,7 @@ viewConfig: {
         if(!g.hideHeaders && (vw < 20 || csize.height < 20)){ // display: none?
             return;
         }
-        
+
         if(g.autoHeight){
             this.scroller.dom.style.overflow = 'visible';
             if(Ext.isWebKit){
@@ -893,33 +985,39 @@ viewConfig: {
     getColumnId : function(index){
       return this.cm.getColumnId(index);
     },
-    
-    // private 
+
+    // private
     getOffsetWidth : function() {
         return (this.cm.getTotalWidth() + this.getScrollOffset()) + 'px';
     },
-    
+
     getScrollOffset: function(){
         return Ext.num(this.scrollOffset, Ext.getScrollBarWidth());
     },
 
-    // private
-    renderHeaders : function(){
-        var cm = this.cm, 
-            ts = this.templates,
-            ct = ts.hcell,
-            cb = [], 
-            p = {},
-            len = cm.getColumnCount(),
+    /**
+     * @private
+     * Renders the header row using the 'header' template. Does not inject the HTML into the DOM, just
+     * returns a string.
+     * @return {String} Rendered header row
+     */
+    renderHeaders : function() {
+        var cm   = this.cm,
+            ts   = this.templates,
+            ct   = ts.hcell,
+            cb   = [],
+            p    = {},
+            len  = cm.getColumnCount(),
             last = len - 1;
-            
-        for(var i = 0; i < len; i++){
+
+        for (var i = 0; i < len; i++) {
             p.id = cm.getColumnId(i);
             p.value = cm.getColumnHeader(i) || '';
             p.style = this.getColumnStyle(i, true);
             p.tooltip = this.getColumnTooltip(i);
             p.css = i === 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : '');
-            if(cm.config[i].align == 'right'){
+
+            if (cm.config[i].align == 'right') {
                 p.istyle = 'padding-right:16px';
             } else {
                 delete p.istyle;
@@ -1014,6 +1112,9 @@ viewConfig: {
         var xy = row;
         if(!Ext.isArray(xy)){
             row = Math.min(row, Math.max(0, this.getRows().length-1));
+            if (isNaN(row)) {
+                return;
+            }
             xy = this.getResolvedXY(this.resolveCell(row, col, hscroll));
         }
         this.focusEl.setXY(xy||this.scroller.getXY());
@@ -1025,25 +1126,25 @@ viewConfig: {
             return;
         }
 
-        var rowEl = resolved.row, 
+        var rowEl = resolved.row,
             cellEl = resolved.cell,
             c = this.scroller.dom,
             ctop = 0,
-            p = rowEl, 
+            p = rowEl,
             stop = this.el.dom;
-            
+
         while(p && p != stop){
             ctop += p.offsetTop;
             p = p.offsetParent;
         }
-        
+
         ctop -= this.mainHd.dom.offsetHeight;
         stop = parseInt(c.scrollTop, 10);
-        
+
         var cbot = ctop + rowEl.offsetHeight,
             ch = c.clientHeight,
             sbot = stop + ch;
-        
+
 
         if(ctop < stop){
           c.scrollTop = ctop;
@@ -1067,34 +1168,34 @@ viewConfig: {
     },
 
     // private
-    insertRows : function(dm, firstRow, lastRow, isUpdate){
+    insertRows : function(dm, firstRow, lastRow, isUpdate) {
         var last = dm.getCount() - 1;
-        if(!isUpdate && firstRow === 0 && lastRow >= last){
-           this.fireEvent('beforerowsinserted', this, firstRow, lastRow);
-            this.refresh();
-           this.fireEvent('rowsinserted', this, firstRow, lastRow);
-        }else{
-            if(!isUpdate){
+        if( !isUpdate && firstRow === 0 && lastRow >= last) {
+            this.fireEvent('beforerowsinserted', this, firstRow, lastRow);
+                this.refresh();
+            this.fireEvent('rowsinserted', this, firstRow, lastRow);
+        } else {
+            if (!isUpdate) {
                 this.fireEvent('beforerowsinserted', this, firstRow, lastRow);
             }
             var html = this.renderRows(firstRow, lastRow),
                 before = this.getRow(firstRow);
-            if(before){
+            if (before) {
                 if(firstRow === 0){
                     Ext.fly(this.getRow(0)).removeClass(this.firstRowCls);
                 }
                 Ext.DomHelper.insertHtml('beforeBegin', before, html);
-            }else{
+            } else {
                 var r = this.getRow(last - 1);
                 if(r){
                     Ext.fly(r).removeClass(this.lastRowCls);
                 }
                 Ext.DomHelper.insertHtml('beforeEnd', this.mainBody.dom, html);
             }
-            if(!isUpdate){
+            if (!isUpdate) {
                 this.fireEvent('rowsinserted', this, firstRow, lastRow);
                 this.processRows(firstRow);
-            }else if(firstRow === 0 || firstRow >= last){
+            } else if (firstRow === 0 || firstRow >= last) {
                 //ensure first/last row is kept after an update.
                 Ext.fly(this.getRow(firstRow)).addClass(firstRow === 0 ? this.firstRowCls : this.lastRowCls);
             }
@@ -1220,20 +1321,29 @@ viewConfig: {
         }
     },
 
-    // private
+    /**
+     * @private
+     * Returns an array of column configurations - one for each column
+     * @return {Array} Array of column config objects. This includes the column name, renderer, id style and renderer
+     */
     getColumnData : function(){
         // build a map for all the columns
-        var cs = [], cm = this.cm, colCount = cm.getColumnCount();
-        for(var i = 0; i < colCount; i++){
+        var cs       = [],
+            cm       = this.cm,
+            colCount = cm.getColumnCount();
+
+        for (var i = 0; i < colCount; i++) {
             var name = cm.getDataIndex(i);
+
             cs[i] = {
-                name : (!Ext.isDefined(name) ? this.ds.fields.get(i).name : name),
-                renderer : cm.getRenderer(i),
-                scope: cm.getRendererScope(i),
-                id : cm.getColumnId(i),
-                style : this.getColumnStyle(i)
+                name    : (!Ext.isDefined(name) ? this.ds.fields.get(i).name : name),
+                renderer: cm.getRenderer(i),
+                scope   : cm.getRendererScope(i),
+                id      : cm.getColumnId(i),
+                style   : this.getColumnStyle(i)
             };
         }
+
         return cs;
     },
 
@@ -1305,33 +1415,46 @@ viewConfig: {
         this.fireEvent('refresh', this);
     },
 
-    // private
+    /**
+     * @private
+     * Displays the configured emptyText if there are currently no rows to display
+     */
     applyEmptyText : function(){
         if(this.emptyText && !this.hasRows()){
             this.mainBody.update('<div class="x-grid-empty">' + this.emptyText + '</div>');
         }
     },
 
-    // private
+    /**
+     * @private
+     * Adds sorting classes to the column headers based on the bound store's sortInfo. Fires the 'sortchange' event
+     * if the sorting has changed since this function was last run.
+     */
     updateHeaderSortState : function(){
         var state = this.ds.getSortState();
-        if(!state){
+        if (!state) {
             return;
         }
-        if(!this.sortState || (this.sortState.field != state.field || this.sortState.direction != state.direction)){
+
+        if (!this.sortState || (this.sortState.field != state.field || this.sortState.direction != state.direction)) {
             this.grid.fireEvent('sortchange', this.grid, state);
         }
+
         this.sortState = state;
+
         var sortColumn = this.cm.findColumnIndex(state.field);
-        if(sortColumn != -1){
+        if (sortColumn != -1){
             var sortDir = state.direction;
             this.updateSortIcon(sortColumn, sortDir);
         }
     },
 
-    // private
+    /**
+     * @private
+     * Removes any sorting indicator classes from the column headers
+     */
     clearHeaderSortState : function(){
-        if(!this.sortState){
+        if (!this.sortState) {
             return;
         }
         this.grid.fireEvent('sortchange', this.grid, null);
@@ -1341,6 +1464,9 @@ viewConfig: {
 
     // private
     destroy : function(){
+        if (this.scrollToTopTask && this.scrollToTopTask.cancel){
+            this.scrollToTopTask.cancel();
+        }
         if(this.colMenu){
             Ext.menu.MenuMgr.unregister(this.colMenu);
             this.colMenu.destroy();
@@ -1432,12 +1558,12 @@ viewConfig: {
         if(this.autoFill){
             var ct = this.grid.ownerCt;
             if (ct && ct.getLayout()){
-                ct.on('afterlayout', function(){ 
+                ct.on('afterlayout', function(){
                     this.fitColumns(true, true);
-                    this.updateHeaders(); 
-                }, this, {single: true}); 
-            }else{ 
-                this.fitColumns(true, true); 
+                    this.updateHeaders();
+                }, this, {single: true});
+            }else{
+                this.fitColumns(true, true);
             }
         }else if(this.forceFit){
             this.fitColumns(true, false);
@@ -1516,7 +1642,6 @@ viewConfig: {
 
     // private
     onAdd : function(ds, records, index){
-       
         this.insertRows(ds, index, index + (records.length-1));
     },
 
@@ -1535,7 +1660,14 @@ viewConfig: {
 
     // private
     onLoad : function(){
-        this.scrollToTop.defer(Ext.isGecko ? 1 : 0, this);
+        if (Ext.isGecko){
+            if (!this.scrollToTopTask) {
+                this.scrollToTopTask = new Ext.util.DelayedTask(this.scrollToTop, this);
+            }
+            this.scrollToTopTask.delay(1);
+        }else{
+            this.scrollToTop();
+        }
     },
 
     // private
@@ -1656,7 +1788,7 @@ viewConfig: {
     // private
     handleHdMenuClick : function(item){
         var index = this.hdCtxIndex,
-            cm = this.cm, 
+            cm = this.cm,
             ds = this.ds,
             id = item.getItemId();
         switch(id){
@@ -1681,7 +1813,7 @@ viewConfig: {
 
     // private
     isHideableColumn : function(c){
-        return !c.hidden && !c.fixed;
+        return !c.hidden;
     },
 
     // private
@@ -1689,7 +1821,7 @@ viewConfig: {
         var cm = this.cm,  colCount = cm.getColumnCount();
         this.colMenu.removeAll();
         for(var i = 0; i < colCount; i++){
-            if(cm.config[i].fixed !== true && cm.config[i].hideable !== false){
+            if(cm.config[i].hideable !== false){
                 this.colMenu.add(new Ext.menu.CheckItem({
                     itemId: 'col-'+cm.getColumnId(i),
                     text: cm.getColumnHeader(i),
@@ -1782,29 +1914,31 @@ viewConfig: {
 
 // private
 // This is a support class used internally by the Grid components
-Ext.grid.GridView.SplitDragZone = function(grid, hd){
-    this.grid = grid;
-    this.view = grid.getView();
-    this.marker = this.view.resizeMarker;
-    this.proxy = this.view.resizeProxy;
-    Ext.grid.GridView.SplitDragZone.superclass.constructor.call(this, hd,
-        'gridSplitters' + this.grid.getGridEl().id, {
-        dragElId : Ext.id(this.proxy.dom), resizeFrame:false
-    });
-    this.scroll = false;
-    this.hw = this.view.splitHandleWidth || 5;
-};
-Ext.extend(Ext.grid.GridView.SplitDragZone, Ext.dd.DDProxy, {
+Ext.grid.GridView.SplitDragZone = Ext.extend(Ext.dd.DDProxy, {
+    
+    constructor: function(grid, hd){
+        this.grid = grid;
+        this.view = grid.getView();
+        this.marker = this.view.resizeMarker;
+        this.proxy = this.view.resizeProxy;
+        Ext.grid.GridView.SplitDragZone.superclass.constructor.call(this, hd,
+            'gridSplitters' + this.grid.getGridEl().id, {
+            dragElId : Ext.id(this.proxy.dom), resizeFrame:false
+        });
+        this.scroll = false;
+        this.hw = this.view.splitHandleWidth || 5;
+    },
 
     b4StartDrag : function(x, y){
+        this.dragHeadersDisabled = this.view.headersDisabled;
         this.view.headersDisabled = true;
         var h = this.view.mainWrap.getHeight();
         this.marker.setHeight(h);
         this.marker.show();
         this.marker.alignTo(this.view.getHeaderCell(this.cellIndex), 'tl-tl', [-2, 0]);
         this.proxy.setHeight(h);
-        var w = this.cm.getColumnWidth(this.cellIndex);
-        var minw = Math.max(w-this.grid.minColumnWidth, 0);
+        var w = this.cm.getColumnWidth(this.cellIndex),
+            minw = Math.max(w-this.grid.minColumnWidth, 0);
         this.resetConstraints();
         this.setXConstraint(minw, 1000);
         this.setYConstraint(0, 0);
@@ -1813,18 +1947,20 @@ Ext.extend(Ext.grid.GridView.SplitDragZone, Ext.dd.DDProxy, {
         this.startPos = x;
         Ext.dd.DDProxy.prototype.b4StartDrag.call(this, x, y);
     },
-    
+
     allowHeaderDrag : function(e){
         return true;
     },
 
-
     handleMouseDown : function(e){
         var t = this.view.findHeaderCell(e.getTarget());
         if(t && this.allowHeaderDrag(e)){
-            var xy = this.view.fly(t).getXY(), x = xy[0], y = xy[1];
-            var exy = e.getXY(), ex = exy[0];
-            var w = t.offsetWidth, adjust = false;
+            var xy = this.view.fly(t).getXY(), 
+                x = xy[0], 
+                y = xy[1];
+                exy = e.getXY(), ex = exy[0],
+                w = t.offsetWidth, adjust = false;
+                
             if((ex - x) <= this.hw){
                 adjust = -1;
             }else if((x+w) - ex <= this.hw){
@@ -1857,12 +1993,14 @@ Ext.extend(Ext.grid.GridView.SplitDragZone, Ext.dd.DDProxy, {
 
     endDrag : function(e){
         this.marker.hide();
-        var v = this.view;
-        var endX = Math.max(this.minX, e.getPageX());
-        var diff = endX - this.startPos;
+        var v = this.view,
+            endX = Math.max(this.minX, e.getPageX()),
+            diff = endX - this.startPos,
+            disabled = this.dragHeadersDisabled;
+            
         v.onColumnSplitterMoved(this.cellIndex, this.cm.getColumnWidth(this.cellIndex)+diff);
         setTimeout(function(){
-            v.headersDisabled = false;
+            v.headersDisabled = disabled;
         }, 50);
     },