Upgrade to ExtJS 3.0.3 - Released 10/11/2009
[extjs.git] / src / widgets / grid / GridView.js
1 /*!
2  * Ext JS Library 3.0.3
3  * Copyright(c) 2006-2009 Ext JS, LLC
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**
8  * @class Ext.grid.GridView
9  * @extends Ext.util.Observable
10  * <p>This class encapsulates the user interface of an {@link Ext.grid.GridPanel}.
11  * Methods of this class may be used to access user interface elements to enable
12  * special display effects. Do not change the DOM structure of the user interface.</p>
13  * <p>This class does not provide ways to manipulate the underlying data. The data
14  * model of a Grid is held in an {@link Ext.data.Store}.</p>
15  * @constructor
16  * @param {Object} config
17  */
18 Ext.grid.GridView = function(config){
19     Ext.apply(this, config);
20     // These events are only used internally by the grid components
21     this.addEvents(
22         /**
23          * @event beforerowremoved
24          * Internal UI Event. Fired before a row is removed.
25          * @param {Ext.grid.GridView} view
26          * @param {Number} rowIndex The index of the row to be removed.
27          * @param {Ext.data.Record} record The Record to be removed
28          */
29         'beforerowremoved',
30         /**
31          * @event beforerowsinserted
32          * Internal UI Event. Fired before rows are inserted.
33          * @param {Ext.grid.GridView} view
34          * @param {Number} firstRow The index of the first row to be inserted.
35          * @param {Number} lastRow The index of the last row to be inserted.
36          */
37         'beforerowsinserted',
38         /**
39          * @event beforerefresh
40          * Internal UI Event. Fired before the view is refreshed.
41          * @param {Ext.grid.GridView} view
42          */
43         'beforerefresh',
44         /**
45          * @event rowremoved
46          * Internal UI Event. Fired after a row is removed.
47          * @param {Ext.grid.GridView} view
48          * @param {Number} rowIndex The index of the row that was removed.
49          * @param {Ext.data.Record} record The Record that was removed
50          */
51         'rowremoved',
52         /**
53          * @event rowsinserted
54          * Internal UI Event. Fired after rows are inserted.
55          * @param {Ext.grid.GridView} view
56          * @param {Number} firstRow The index of the first inserted.
57          * @param {Number} lastRow The index of the last row inserted.
58          */
59         'rowsinserted',
60         /**
61          * @event rowupdated
62          * Internal UI Event. Fired after a row has been updated.
63          * @param {Ext.grid.GridView} view
64          * @param {Number} firstRow The index of the row updated.
65          * @param {Ext.data.record} record The Record backing the row updated.
66          */
67         'rowupdated',
68         /**
69          * @event refresh
70          * Internal UI Event. Fired after the GridView's body has been refreshed.
71          * @param {Ext.grid.GridView} view
72          */
73         'refresh'
74     );
75     Ext.grid.GridView.superclass.constructor.call(this);
76 };
77
78 Ext.extend(Ext.grid.GridView, Ext.util.Observable, {
79     /**
80      * Override this function to apply custom CSS classes to rows during rendering.  You can also supply custom
81      * parameters to the row template for the current row to customize how it is rendered using the <b>rowParams</b>
82      * parameter.  This function should return the CSS class name (or empty string '' for none) that will be added
83      * to the row's wrapping div.  To apply multiple class names, simply return them space-delimited within the string
84      * (e.g., 'my-class another-class'). Example usage:
85     <pre><code>
86 viewConfig: {
87     forceFit: true,
88     showPreview: true, // custom property
89     enableRowBody: true, // required to create a second, full-width row to show expanded Record data
90     getRowClass: function(record, rowIndex, rp, ds){ // rp = rowParams
91         if(this.showPreview){
92             rp.body = '&lt;p>'+record.data.excerpt+'&lt;/p>';
93             return 'x-grid3-row-expanded';
94         }
95         return 'x-grid3-row-collapsed';
96     }
97 },     
98     </code></pre>
99      * @param {Record} record The {@link Ext.data.Record} corresponding to the current row.
100      * @param {Number} index The row index.
101      * @param {Object} rowParams A config object that is passed to the row template during rendering that allows
102      * customization of various aspects of a grid row.
103      * <p>If {@link #enableRowBody} is configured <b><tt></tt>true</b>, then the following properties may be set
104      * by this function, and will be used to render a full-width expansion row below each grid row:</p>
105      * <ul>
106      * <li><code>body</code> : String <div class="sub-desc">An HTML fragment to be used as the expansion row's body content (defaults to '').</div></li>
107      * <li><code>bodyStyle</code> : String <div class="sub-desc">A CSS style specification that will be applied to the expansion row's &lt;tr> element. (defaults to '').</div></li>
108      * </ul>
109      * The following property will be passed in, and may be appended to:
110      * <ul>
111      * <li><code>tstyle</code> : String <div class="sub-desc">A CSS style specification that willl be applied to the &lt;table> element which encapsulates
112      * both the standard grid row, and any expansion row.</div></li>
113      * </ul>
114      * @param {Store} store The {@link Ext.data.Store} this grid is bound to
115      * @method getRowClass
116      * @return {String} a CSS class name to add to the row.
117      */
118     /**
119      * @cfg {Boolean} enableRowBody True to add a second TR element per row that can be used to provide a row body
120      * that spans beneath the data row.  Use the {@link #getRowClass} method's rowParams config to customize the row body.
121      */
122     /**
123      * @cfg {String} emptyText Default text (html tags are accepted) to display in the grid body when no rows
124      * are available (defaults to ''). This value will be used to update the <tt>{@link #mainBody}</tt>:
125     <pre><code>
126     this.mainBody.update('&lt;div class="x-grid-empty">' + this.emptyText + '&lt;/div>');
127     </code></pre>
128      */
129     /**
130      * @cfg {Boolean} headersDisabled True to disable the grid column headers (defaults to <tt>false</tt>). 
131      * Use the {@link Ext.grid.ColumnModel ColumnModel} <tt>{@link Ext.grid.ColumnModel#menuDisabled menuDisabled}</tt>
132      * config to disable the <i>menu</i> for individual columns.  While this config is true the
133      * following will be disabled:<div class="mdetail-params"><ul>
134      * <li>clicking on header to sort</li>
135      * <li>the trigger to reveal the menu.</li>
136      * </ul></div>
137      */
138     /**
139      * <p>A customized implementation of a {@link Ext.dd.DragZone DragZone} which provides default implementations
140      * of the template methods of DragZone to enable dragging of the selected rows of a GridPanel.
141      * See {@link Ext.grid.GridDragZone} for details.</p>
142      * <p>This will <b>only</b> be present:<div class="mdetail-params"><ul>
143      * <li><i>if</i> the owning GridPanel was configured with {@link Ext.grid.GridPanel#enableDragDrop enableDragDrop}: <tt>true</tt>.</li>
144      * <li><i>after</i> the owning GridPanel has been rendered.</li>
145      * </ul></div>
146      * @property dragZone
147      * @type {Ext.grid.GridDragZone}
148      */
149     /**
150      * @cfg {Boolean} deferEmptyText True to defer <tt>{@link #emptyText}</tt> being applied until the store's
151      * first load (defaults to <tt>true</tt>).
152      */
153     deferEmptyText : true,
154     /**
155      * @cfg {Number} scrollOffset The amount of space to reserve for the vertical scrollbar
156      * (defaults to <tt>undefined</tt>). If an explicit value isn't specified, this will be automatically
157      * calculated.
158      */
159     scrollOffset : undefined,
160     /**
161      * @cfg {Boolean} autoFill
162      * Defaults to <tt>false</tt>.  Specify <tt>true</tt> to have the column widths re-proportioned
163      * when the grid is <b>initially rendered</b>.  The 
164      * {@link Ext.grid.Column#width initially configured width}</tt> of each column will be adjusted
165      * to fit the grid width and prevent horizontal scrolling. If columns are later resized (manually
166      * or programmatically), the other columns in the grid will <b>not</b> be resized to fit the grid width.
167      * See <tt>{@link #forceFit}</tt> also.
168      */
169     autoFill : false,
170     /**
171      * @cfg {Boolean} forceFit
172      * Defaults to <tt>false</tt>.  Specify <tt>true</tt> to have the column widths re-proportioned
173      * at <b>all times</b>.  The {@link Ext.grid.Column#width initially configured width}</tt> of each
174      * column will be adjusted to fit the grid width and prevent horizontal scrolling. If columns are
175      * later resized (manually or programmatically), the other columns in the grid <b>will</b> be resized
176      * to fit the grid width. See <tt>{@link #autoFill}</tt> also.
177      */
178     forceFit : false,
179     /**
180      * @cfg {Array} sortClasses The CSS classes applied to a header when it is sorted. (defaults to <tt>['sort-asc', 'sort-desc']</tt>)
181      */
182     sortClasses : ['sort-asc', 'sort-desc'],
183     /**
184      * @cfg {String} sortAscText The text displayed in the 'Sort Ascending' menu item (defaults to <tt>'Sort Ascending'</tt>)
185      */
186     sortAscText : 'Sort Ascending',
187     /**
188      * @cfg {String} sortDescText The text displayed in the 'Sort Descending' menu item (defaults to <tt>'Sort Descending'</tt>)
189      */
190     sortDescText : 'Sort Descending',
191     /**
192      * @cfg {String} columnsText The text displayed in the 'Columns' menu item (defaults to <tt>'Columns'</tt>)
193      */
194     columnsText : 'Columns',
195
196     /**
197      * @cfg {String} selectedRowClass The CSS class applied to a selected row (defaults to <tt>'x-grid3-row-selected'</tt>). An
198      * example overriding the default styling:
199     <pre><code>
200     .x-grid3-row-selected {background-color: yellow;}
201     </code></pre>
202      * Note that this only controls the row, and will not do anything for the text inside it.  To style inner
203      * facets (like text) use something like:
204     <pre><code>
205     .x-grid3-row-selected .x-grid3-cell-inner {
206         color: #FFCC00;
207     }
208     </code></pre>
209      * @type String
210      */
211     selectedRowClass : 'x-grid3-row-selected',
212
213     // private
214     borderWidth : 2,
215     tdClass : 'x-grid3-cell',
216     hdCls : 'x-grid3-hd',
217     markDirty : true,
218
219     /**
220      * @cfg {Number} cellSelectorDepth The number of levels to search for cells in event delegation (defaults to <tt>4</tt>)
221      */
222     cellSelectorDepth : 4,
223     /**
224      * @cfg {Number} rowSelectorDepth The number of levels to search for rows in event delegation (defaults to <tt>10</tt>)
225      */
226     rowSelectorDepth : 10,
227
228     /**
229      * @cfg {String} cellSelector The selector used to find cells internally (defaults to <tt>'td.x-grid3-cell'</tt>)
230      */
231     cellSelector : 'td.x-grid3-cell',
232     /**
233      * @cfg {String} rowSelector The selector used to find rows internally (defaults to <tt>'div.x-grid3-row'</tt>)
234      */
235     rowSelector : 'div.x-grid3-row',
236     
237     // private
238     firstRowCls: 'x-grid3-row-first',
239     lastRowCls: 'x-grid3-row-last',
240     rowClsRe: /(?:^|\s+)x-grid3-row-(first|last|alt)(?:\s+|$)/g,
241
242     /* -------------------------------- UI Specific ----------------------------- */
243
244     // private
245     initTemplates : function(){
246         var ts = this.templates || {};
247         if(!ts.master){
248             ts.master = new Ext.Template(
249                     '<div class="x-grid3" hidefocus="true">',
250                         '<div class="x-grid3-viewport">',
251                             '<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>',
252                             '<div class="x-grid3-scroller"><div class="x-grid3-body" style="{bstyle}">{body}</div><a href="#" class="x-grid3-focus" tabIndex="-1"></a></div>',
253                         '</div>',
254                         '<div class="x-grid3-resize-marker">&#160;</div>',
255                         '<div class="x-grid3-resize-proxy">&#160;</div>',
256                     '</div>'
257                     );
258         }
259
260         if(!ts.header){
261             ts.header = new Ext.Template(
262                     '<table border="0" cellspacing="0" cellpadding="0" style="{tstyle}">',
263                     '<thead><tr class="x-grid3-hd-row">{cells}</tr></thead>',
264                     '</table>'
265                     );
266         }
267
268         if(!ts.hcell){
269             ts.hcell = new Ext.Template(
270                     '<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>' : '',
271                     '{value}<img class="x-grid3-sort-icon" src="', Ext.BLANK_IMAGE_URL, '" />',
272                     '</div></td>'
273                     );
274         }
275
276         if(!ts.body){
277             ts.body = new Ext.Template('{rows}');
278         }
279
280         if(!ts.row){
281             ts.row = new Ext.Template(
282                     '<div class="x-grid3-row {alt}" style="{tstyle}"><table class="x-grid3-row-table" border="0" cellspacing="0" cellpadding="0" style="{tstyle}">',
283                     '<tbody><tr>{cells}</tr>',
284                     (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>' : ''),
285                     '</tbody></table></div>'
286                     );
287         }
288
289         if(!ts.cell){
290             ts.cell = new Ext.Template(
291                     '<td class="x-grid3-col x-grid3-cell x-grid3-td-{id} {css}" style="{style}" tabIndex="0" {cellAttr}>',
292                     '<div class="x-grid3-cell-inner x-grid3-col-{id}" unselectable="on" {attr}>{value}</div>',
293                     '</td>'
294                     );
295         }
296
297         for(var k in ts){
298             var t = ts[k];
299             if(t && typeof t.compile == 'function' && !t.compiled){
300                 t.disableFormats = true;
301                 t.compile();
302             }
303         }
304
305         this.templates = ts;
306         this.colRe = new RegExp('x-grid3-td-([^\\s]+)', '');
307     },
308
309     // private
310     fly : function(el){
311         if(!this._flyweight){
312             this._flyweight = new Ext.Element.Flyweight(document.body);
313         }
314         this._flyweight.dom = el;
315         return this._flyweight;
316     },
317
318     // private
319     getEditorParent : function(){
320         return this.scroller.dom;
321     },
322
323     // private
324     initElements : function(){
325         var E = Ext.Element;
326
327         var el = this.grid.getGridEl().dom.firstChild;
328         var cs = el.childNodes;
329
330         this.el = new E(el);
331
332         this.mainWrap = new E(cs[0]);
333         this.mainHd = new E(this.mainWrap.dom.firstChild);
334
335         if(this.grid.hideHeaders){
336             this.mainHd.setDisplayed(false);
337         }
338
339         this.innerHd = this.mainHd.dom.firstChild;
340         this.scroller = new E(this.mainWrap.dom.childNodes[1]);
341         if(this.forceFit){
342             this.scroller.setStyle('overflow-x', 'hidden');
343         }
344         /**
345          * <i>Read-only</i>. The GridView's body Element which encapsulates all rows in the Grid.
346          * This {@link Ext.Element Element} is only available after the GridPanel has been rendered.
347          * @type Ext.Element
348          * @property mainBody
349          */
350         this.mainBody = new E(this.scroller.dom.firstChild);
351
352         this.focusEl = new E(this.scroller.dom.childNodes[1]);
353         this.focusEl.swallowEvent('click', true);
354
355         this.resizeMarker = new E(cs[1]);
356         this.resizeProxy = new E(cs[2]);
357     },
358
359     // private
360     getRows : function(){
361         return this.hasRows() ? this.mainBody.dom.childNodes : [];
362     },
363
364     // finder methods, used with delegation
365
366     // private
367     findCell : function(el){
368         if(!el){
369             return false;
370         }
371         return this.fly(el).findParent(this.cellSelector, this.cellSelectorDepth);
372     },
373
374     /**
375      * <p>Return the index of the grid column which contains the passed HTMLElement.</p>
376      * See also {@link #findRowIndex}
377      * @param {HTMLElement} el The target element
378      * @return {Number} The column index, or <b>false</b> if the target element is not within a row of this GridView.
379      */
380     findCellIndex : function(el, requiredCls){
381         var cell = this.findCell(el);
382         if(cell && (!requiredCls || this.fly(cell).hasClass(requiredCls))){
383             return this.getCellIndex(cell);
384         }
385         return false;
386     },
387
388     // private
389     getCellIndex : function(el){
390         if(el){
391             var m = el.className.match(this.colRe);
392             if(m && m[1]){
393                 return this.cm.getIndexById(m[1]);
394             }
395         }
396         return false;
397     },
398
399     // private
400     findHeaderCell : function(el){
401         var cell = this.findCell(el);
402         return cell && this.fly(cell).hasClass(this.hdCls) ? cell : null;
403     },
404
405     // private
406     findHeaderIndex : function(el){
407         return this.findCellIndex(el, this.hdCls);
408     },
409
410     /**
411      * Return the HtmlElement representing the grid row which contains the passed element.
412      * @param {HTMLElement} el The target HTMLElement
413      * @return {HTMLElement} The row element, or null if the target element is not within a row of this GridView.
414      */
415     findRow : function(el){
416         if(!el){
417             return false;
418         }
419         return this.fly(el).findParent(this.rowSelector, this.rowSelectorDepth);
420     },
421
422     /**
423      * <p>Return the index of the grid row which contains the passed HTMLElement.</p>
424      * See also {@link #findCellIndex}
425      * @param {HTMLElement} el The target HTMLElement
426      * @return {Number} The row index, or <b>false</b> if the target element is not within a row of this GridView.
427      */
428     findRowIndex : function(el){
429         var r = this.findRow(el);
430         return r ? r.rowIndex : false;
431     },
432
433     // getter methods for fetching elements dynamically in the grid
434
435     /**
436      * Return the <tt>&lt;div></tt> HtmlElement which represents a Grid row for the specified index.
437      * @param {Number} index The row index
438      * @return {HtmlElement} The div element.
439      */
440     getRow : function(row){
441         return this.getRows()[row];
442     },
443
444     /**
445      * Returns the grid's <tt>&lt;td></tt> HtmlElement at the specified coordinates.
446      * @param {Number} row The row index in which to find the cell.
447      * @param {Number} col The column index of the cell.
448      * @return {HtmlElement} The td at the specified coordinates.
449      */
450     getCell : function(row, col){
451         return this.getRow(row).getElementsByTagName('td')[col];
452     },
453
454     /**
455      * Return the <tt>&lt;td></tt> HtmlElement which represents the Grid's header cell for the specified column index.
456      * @param {Number} index The column index
457      * @return {HtmlElement} The td element.
458      */
459     getHeaderCell : function(index){
460       return this.mainHd.dom.getElementsByTagName('td')[index];
461     },
462
463     // manipulating elements
464
465     // private - use getRowClass to apply custom row classes
466     addRowClass : function(row, cls){
467         var r = this.getRow(row);
468         if(r){
469             this.fly(r).addClass(cls);
470         }
471     },
472
473     // private
474     removeRowClass : function(row, cls){
475         var r = this.getRow(row);
476         if(r){
477             this.fly(r).removeClass(cls);
478         }
479     },
480
481     // private
482     removeRow : function(row){
483         Ext.removeNode(this.getRow(row));
484         this.syncFocusEl(row);
485     },
486     
487     // private
488     removeRows : function(firstRow, lastRow){
489         var bd = this.mainBody.dom;
490         for(var rowIndex = firstRow; rowIndex <= lastRow; rowIndex++){
491             Ext.removeNode(bd.childNodes[firstRow]);
492         }
493         this.syncFocusEl(firstRow);
494     },
495
496     // scrolling stuff
497
498     // private
499     getScrollState : function(){
500         var sb = this.scroller.dom;
501         return {left: sb.scrollLeft, top: sb.scrollTop};
502     },
503
504     // private
505     restoreScroll : function(state){
506         var sb = this.scroller.dom;
507         sb.scrollLeft = state.left;
508         sb.scrollTop = state.top;
509     },
510
511     /**
512      * Scrolls the grid to the top
513      */
514     scrollToTop : function(){
515         this.scroller.dom.scrollTop = 0;
516         this.scroller.dom.scrollLeft = 0;
517     },
518
519     // private
520     syncScroll : function(){
521       this.syncHeaderScroll();
522       var mb = this.scroller.dom;
523         this.grid.fireEvent('bodyscroll', mb.scrollLeft, mb.scrollTop);
524     },
525
526     // private
527     syncHeaderScroll : function(){
528         var mb = this.scroller.dom;
529         this.innerHd.scrollLeft = mb.scrollLeft;
530         this.innerHd.scrollLeft = mb.scrollLeft; // second time for IE (1/2 time first fails, other browsers ignore)
531     },
532
533     // private
534     updateSortIcon : function(col, dir){
535         var sc = this.sortClasses;
536         var hds = this.mainHd.select('td').removeClass(sc);
537         hds.item(col).addClass(sc[dir == 'DESC' ? 1 : 0]);
538     },
539
540     // private
541     updateAllColumnWidths : function(){
542         var tw = this.getTotalWidth(),
543             clen = this.cm.getColumnCount(),
544             ws = [],
545             len,
546             i;
547         for(i = 0; i < clen; i++){
548             ws[i] = this.getColumnWidth(i);
549         }
550         this.innerHd.firstChild.style.width = this.getOffsetWidth();
551         this.innerHd.firstChild.firstChild.style.width = tw;
552         this.mainBody.dom.style.width = tw;
553         for(i = 0; i < clen; i++){
554             var hd = this.getHeaderCell(i);
555             hd.style.width = ws[i];
556         }
557
558         var ns = this.getRows(), row, trow;
559         for(i = 0, len = ns.length; i < len; i++){
560             row = ns[i];
561             row.style.width = tw;
562             if(row.firstChild){
563                 row.firstChild.style.width = tw;
564                 trow = row.firstChild.rows[0];
565                 for (var j = 0; j < clen; j++) {
566                    trow.childNodes[j].style.width = ws[j];
567                 }
568             }
569         }
570
571         this.onAllColumnWidthsUpdated(ws, tw);
572     },
573
574     // private
575     updateColumnWidth : function(col, width){
576         var w = this.getColumnWidth(col);
577         var tw = this.getTotalWidth();
578         this.innerHd.firstChild.style.width = this.getOffsetWidth();
579         this.innerHd.firstChild.firstChild.style.width = tw;
580         this.mainBody.dom.style.width = tw;
581         var hd = this.getHeaderCell(col);
582         hd.style.width = w;
583
584         var ns = this.getRows(), row;
585         for(var i = 0, len = ns.length; i < len; i++){
586             row = ns[i];
587             row.style.width = tw;
588             if(row.firstChild){
589                 row.firstChild.style.width = tw;
590                 row.firstChild.rows[0].childNodes[col].style.width = w;
591             }
592         }
593
594         this.onColumnWidthUpdated(col, w, tw);
595     },
596
597     // private
598     updateColumnHidden : function(col, hidden){
599         var tw = this.getTotalWidth();
600         this.innerHd.firstChild.style.width = this.getOffsetWidth();
601         this.innerHd.firstChild.firstChild.style.width = tw;
602         this.mainBody.dom.style.width = tw;
603         var display = hidden ? 'none' : '';
604
605         var hd = this.getHeaderCell(col);
606         hd.style.display = display;
607
608         var ns = this.getRows(), row;
609         for(var i = 0, len = ns.length; i < len; i++){
610             row = ns[i];
611             row.style.width = tw;
612             if(row.firstChild){
613                 row.firstChild.style.width = tw;
614                 row.firstChild.rows[0].childNodes[col].style.display = display;
615             }
616         }
617
618         this.onColumnHiddenUpdated(col, hidden, tw);
619         delete this.lastViewWidth; // force recalc
620         this.layout();
621     },
622
623     // private
624     doRender : function(cs, rs, ds, startRow, colCount, stripe){
625         var ts = this.templates, ct = ts.cell, rt = ts.row, last = colCount-1;
626         var tstyle = 'width:'+this.getTotalWidth()+';';
627         // buffers
628         var buf = [], cb, c, p = {}, rp = {tstyle: tstyle}, r;
629         for(var j = 0, len = rs.length; j < len; j++){
630             r = rs[j]; cb = [];
631             var rowIndex = (j+startRow);
632             for(var i = 0; i < colCount; i++){
633                 c = cs[i];
634                 p.id = c.id;
635                 p.css = i === 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : '');
636                 p.attr = p.cellAttr = '';
637                 p.value = c.renderer(r.data[c.name], p, r, rowIndex, i, ds);
638                 p.style = c.style;
639                 if(Ext.isEmpty(p.value)){
640                     p.value = '&#160;';
641                 }
642                 if(this.markDirty && r.dirty && typeof r.modified[c.name] !== 'undefined'){
643                     p.css += ' x-grid3-dirty-cell';
644                 }
645                 cb[cb.length] = ct.apply(p);
646             }
647             var alt = [];
648             if(stripe && ((rowIndex+1) % 2 === 0)){
649                 alt[0] = 'x-grid3-row-alt';
650             }
651             if(r.dirty){
652                 alt[1] = ' x-grid3-dirty-row';
653             }
654             rp.cols = colCount;
655             if(this.getRowClass){
656                 alt[2] = this.getRowClass(r, rowIndex, rp, ds);
657             }
658             rp.alt = alt.join(' ');
659             rp.cells = cb.join('');
660             buf[buf.length] =  rt.apply(rp);
661         }
662         return buf.join('');
663     },
664
665     // private
666     processRows : function(startRow, skipStripe){
667         if(!this.ds || this.ds.getCount() < 1){
668             return;
669         }
670         var rows = this.getRows();
671         skipStripe = skipStripe || !this.grid.stripeRows;
672         startRow = startRow || 0;
673         Ext.each(rows, function(row, idx){
674             row.rowIndex = idx;
675             if(!skipStripe){
676                 row.className = row.className.replace(this.rowClsRe, ' ');
677                 if ((idx + 1) % 2 === 0){
678                     row.className += ' x-grid3-row-alt';
679                 }
680             }
681         }, this);
682         // add first/last-row classes
683         if(startRow === 0){
684             Ext.fly(rows[0]).addClass(this.firstRowCls);
685         }
686         Ext.fly(rows[rows.length - 1]).addClass(this.lastRowCls);
687     },
688
689     afterRender : function(){
690         if(!this.ds || !this.cm){
691             return;
692         }
693         this.mainBody.dom.innerHTML = this.renderRows() || '&#160;';
694         this.processRows(0, true);
695
696         if(this.deferEmptyText !== true){
697             this.applyEmptyText();
698         }
699     },
700
701     // private
702     renderUI : function(){
703
704         var header = this.renderHeaders();
705         var body = this.templates.body.apply({rows:'&#160;'});
706
707
708         var html = this.templates.master.apply({
709             body: body,
710             header: header,
711             ostyle: 'width:'+this.getOffsetWidth()+';',
712             bstyle: 'width:'+this.getTotalWidth()+';'
713         });
714
715         var g = this.grid;
716
717         g.getGridEl().dom.innerHTML = html;
718
719         this.initElements();
720
721         // get mousedowns early
722         Ext.fly(this.innerHd).on('click', this.handleHdDown, this);
723         this.mainHd.on({
724             scope: this,
725             mouseover: this.handleHdOver,
726             mouseout: this.handleHdOut,
727             mousemove: this.handleHdMove
728         });
729
730         this.scroller.on('scroll', this.syncScroll,  this);
731         if(g.enableColumnResize !== false){
732             this.splitZone = new Ext.grid.GridView.SplitDragZone(g, this.mainHd.dom);
733         }
734
735         if(g.enableColumnMove){
736             this.columnDrag = new Ext.grid.GridView.ColumnDragZone(g, this.innerHd);
737             this.columnDrop = new Ext.grid.HeaderDropZone(g, this.mainHd.dom);
738         }
739
740         if(g.enableHdMenu !== false){
741             this.hmenu = new Ext.menu.Menu({id: g.id + '-hctx'});
742             this.hmenu.add(
743                 {itemId:'asc', text: this.sortAscText, cls: 'xg-hmenu-sort-asc'},
744                 {itemId:'desc', text: this.sortDescText, cls: 'xg-hmenu-sort-desc'}
745             );
746             if(g.enableColumnHide !== false){
747                 this.colMenu = new Ext.menu.Menu({id:g.id + '-hcols-menu'});
748                 this.colMenu.on({
749                     scope: this,
750                     beforeshow: this.beforeColMenuShow,
751                     itemclick: this.handleHdMenuClick
752                 });
753                 this.hmenu.add('-', {
754                     itemId:'columns',
755                     hideOnClick: false,
756                     text: this.columnsText,
757                     menu: this.colMenu,
758                     iconCls: 'x-cols-icon'
759                 });
760             }
761             this.hmenu.on('itemclick', this.handleHdMenuClick, this);
762         }
763
764         if(g.trackMouseOver){
765             this.mainBody.on({
766                 scope: this,
767                 mouseover: this.onRowOver,
768                 mouseout: this.onRowOut
769             });
770         }
771
772         if(g.enableDragDrop || g.enableDrag){
773             this.dragZone = new Ext.grid.GridDragZone(g, {
774                 ddGroup : g.ddGroup || 'GridDD'
775             });
776         }
777
778         this.updateHeaderSortState();
779
780     },
781
782     // private
783     layout : function(){
784         if(!this.mainBody){
785             return; // not rendered
786         }
787         var g = this.grid;
788         var c = g.getGridEl();
789         var csize = c.getSize(true);
790         var vw = csize.width;
791
792         if(!g.hideHeaders && (vw < 20 || csize.height < 20)){ // display: none?
793             return;
794         }
795         
796         if(g.autoHeight){
797             this.scroller.dom.style.overflow = 'visible';
798             if(Ext.isWebKit){
799                 this.scroller.dom.style.position = 'static';
800             }
801         }else{
802             this.el.setSize(csize.width, csize.height);
803
804             var hdHeight = this.mainHd.getHeight();
805             var vh = csize.height - (hdHeight);
806
807             this.scroller.setSize(vw, vh);
808             if(this.innerHd){
809                 this.innerHd.style.width = (vw)+'px';
810             }
811         }
812         if(this.forceFit){
813             if(this.lastViewWidth != vw){
814                 this.fitColumns(false, false);
815                 this.lastViewWidth = vw;
816             }
817         }else {
818             this.autoExpand();
819             this.syncHeaderScroll();
820         }
821         this.onLayout(vw, vh);
822     },
823
824     // template functions for subclasses and plugins
825     // these functions include precalculated values
826     onLayout : function(vw, vh){
827         // do nothing
828     },
829
830     onColumnWidthUpdated : function(col, w, tw){
831         //template method
832     },
833
834     onAllColumnWidthsUpdated : function(ws, tw){
835         //template method
836     },
837
838     onColumnHiddenUpdated : function(col, hidden, tw){
839         // template method
840     },
841
842     updateColumnText : function(col, text){
843         // template method
844     },
845
846     afterMove : function(colIndex){
847         // template method
848     },
849
850     /* ----------------------------------- Core Specific -------------------------------------------*/
851     // private
852     init : function(grid){
853         this.grid = grid;
854
855         this.initTemplates();
856         this.initData(grid.store, grid.colModel);
857         this.initUI(grid);
858     },
859
860     // private
861     getColumnId : function(index){
862       return this.cm.getColumnId(index);
863     },
864     
865     // private 
866     getOffsetWidth : function() {
867         return (this.cm.getTotalWidth() + this.getScrollOffset()) + 'px';
868     },
869     
870     getScrollOffset: function(){
871         return Ext.num(this.scrollOffset, Ext.getScrollBarWidth());
872     },
873
874     // private
875     renderHeaders : function(){
876         var cm = this.cm, 
877             ts = this.templates,
878             ct = ts.hcell,
879             cb = [], 
880             p = {},
881             len = cm.getColumnCount(),
882             last = len - 1;
883             
884         for(var i = 0; i < len; i++){
885             p.id = cm.getColumnId(i);
886             p.value = cm.getColumnHeader(i) || '';
887             p.style = this.getColumnStyle(i, true);
888             p.tooltip = this.getColumnTooltip(i);
889             p.css = i === 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : '');
890             if(cm.config[i].align == 'right'){
891                 p.istyle = 'padding-right:16px';
892             } else {
893                 delete p.istyle;
894             }
895             cb[cb.length] = ct.apply(p);
896         }
897         return ts.header.apply({cells: cb.join(''), tstyle:'width:'+this.getTotalWidth()+';'});
898     },
899
900     // private
901     getColumnTooltip : function(i){
902         var tt = this.cm.getColumnTooltip(i);
903         if(tt){
904             if(Ext.QuickTips.isEnabled()){
905                 return 'ext:qtip="'+tt+'"';
906             }else{
907                 return 'title="'+tt+'"';
908             }
909         }
910         return '';
911     },
912
913     // private
914     beforeUpdate : function(){
915         this.grid.stopEditing(true);
916     },
917
918     // private
919     updateHeaders : function(){
920         this.innerHd.firstChild.innerHTML = this.renderHeaders();
921         this.innerHd.firstChild.style.width = this.getOffsetWidth();
922         this.innerHd.firstChild.firstChild.style.width = this.getTotalWidth();
923     },
924
925     /**
926      * Focuses the specified row.
927      * @param {Number} row The row index
928      */
929     focusRow : function(row){
930         this.focusCell(row, 0, false);
931     },
932
933     /**
934      * Focuses the specified cell.
935      * @param {Number} row The row index
936      * @param {Number} col The column index
937      */
938     focusCell : function(row, col, hscroll){
939         this.syncFocusEl(this.ensureVisible(row, col, hscroll));
940         if(Ext.isGecko){
941             this.focusEl.focus();
942         }else{
943             this.focusEl.focus.defer(1, this.focusEl);
944         }
945     },
946
947     resolveCell : function(row, col, hscroll){
948         if(typeof row != "number"){
949             row = row.rowIndex;
950         }
951         if(!this.ds){
952             return null;
953         }
954         if(row < 0 || row >= this.ds.getCount()){
955             return null;
956         }
957         col = (col !== undefined ? col : 0);
958
959         var rowEl = this.getRow(row),
960             cm = this.cm,
961             colCount = cm.getColumnCount(),
962             cellEl;
963         if(!(hscroll === false && col === 0)){
964             while(col < colCount && cm.isHidden(col)){
965                 col++;
966             }
967             cellEl = this.getCell(row, col);
968         }
969
970         return {row: rowEl, cell: cellEl};
971     },
972
973     getResolvedXY : function(resolved){
974         if(!resolved){
975             return null;
976         }
977         var s = this.scroller.dom, c = resolved.cell, r = resolved.row;
978         return c ? Ext.fly(c).getXY() : [this.el.getX(), Ext.fly(r).getY()];
979     },
980
981     syncFocusEl : function(row, col, hscroll){
982         var xy = row;
983         if(!Ext.isArray(xy)){
984             row = Math.min(row, Math.max(0, this.getRows().length-1));
985             xy = this.getResolvedXY(this.resolveCell(row, col, hscroll));
986         }
987         this.focusEl.setXY(xy||this.scroller.getXY());
988     },
989
990     ensureVisible : function(row, col, hscroll){
991         var resolved = this.resolveCell(row, col, hscroll);
992         if(!resolved || !resolved.row){
993             return;
994         }
995
996         var rowEl = resolved.row, 
997             cellEl = resolved.cell,
998             c = this.scroller.dom,
999             ctop = 0,
1000             p = rowEl, 
1001             stop = this.el.dom;
1002             
1003         while(p && p != stop){
1004             ctop += p.offsetTop;
1005             p = p.offsetParent;
1006         }
1007         
1008         ctop -= this.mainHd.dom.offsetHeight;
1009         stop = parseInt(c.scrollTop, 10);
1010         
1011         var cbot = ctop + rowEl.offsetHeight,
1012             ch = c.clientHeight,
1013             sbot = stop + ch;
1014         
1015
1016         if(ctop < stop){
1017           c.scrollTop = ctop;
1018         }else if(cbot > sbot){
1019             c.scrollTop = cbot-ch;
1020         }
1021
1022         if(hscroll !== false){
1023             var cleft = parseInt(cellEl.offsetLeft, 10);
1024             var cright = cleft + cellEl.offsetWidth;
1025
1026             var sleft = parseInt(c.scrollLeft, 10);
1027             var sright = sleft + c.clientWidth;
1028             if(cleft < sleft){
1029                 c.scrollLeft = cleft;
1030             }else if(cright > sright){
1031                 c.scrollLeft = cright-c.clientWidth;
1032             }
1033         }
1034         return this.getResolvedXY(resolved);
1035     },
1036
1037     // private
1038     insertRows : function(dm, firstRow, lastRow, isUpdate){
1039         var last = dm.getCount() - 1;
1040         if(!isUpdate && firstRow === 0 && lastRow >= last){
1041             this.refresh();
1042         }else{
1043             if(!isUpdate){
1044                 this.fireEvent('beforerowsinserted', this, firstRow, lastRow);
1045             }
1046             var html = this.renderRows(firstRow, lastRow),
1047                 before = this.getRow(firstRow);
1048             if(before){
1049                 if(firstRow === 0){
1050                     Ext.fly(this.getRow(0)).removeClass(this.firstRowCls);
1051                 }
1052                 Ext.DomHelper.insertHtml('beforeBegin', before, html);
1053             }else{
1054                 var r = this.getRow(last - 1);
1055                 if(r){
1056                     Ext.fly(r).removeClass(this.lastRowCls);
1057                 }
1058                 Ext.DomHelper.insertHtml('beforeEnd', this.mainBody.dom, html);
1059             }
1060             if(!isUpdate){
1061                 this.fireEvent('rowsinserted', this, firstRow, lastRow);
1062                 this.processRows(firstRow);
1063             }else if(firstRow === 0 || firstRow >= last){
1064                 //ensure first/last row is kept after an update.
1065                 Ext.fly(this.getRow(firstRow)).addClass(firstRow === 0 ? this.firstRowCls : this.lastRowCls);
1066             }
1067         }
1068         this.syncFocusEl(firstRow);
1069     },
1070
1071     // private
1072     deleteRows : function(dm, firstRow, lastRow){
1073         if(dm.getRowCount()<1){
1074             this.refresh();
1075         }else{
1076             this.fireEvent('beforerowsdeleted', this, firstRow, lastRow);
1077
1078             this.removeRows(firstRow, lastRow);
1079
1080             this.processRows(firstRow);
1081             this.fireEvent('rowsdeleted', this, firstRow, lastRow);
1082         }
1083     },
1084
1085     // private
1086     getColumnStyle : function(col, isHeader){
1087         var style = !isHeader ? (this.cm.config[col].css || '') : '';
1088         style += 'width:'+this.getColumnWidth(col)+';';
1089         if(this.cm.isHidden(col)){
1090             style += 'display:none;';
1091         }
1092         var align = this.cm.config[col].align;
1093         if(align){
1094             style += 'text-align:'+align+';';
1095         }
1096         return style;
1097     },
1098
1099     // private
1100     getColumnWidth : function(col){
1101         var w = this.cm.getColumnWidth(col);
1102         if(typeof w == 'number'){
1103             return (Ext.isBorderBox || (Ext.isWebKit && !Ext.isSafari2) ? w : (w - this.borderWidth > 0 ? w - this.borderWidth : 0)) + 'px';
1104         }
1105         return w;
1106     },
1107
1108     // private
1109     getTotalWidth : function(){
1110         return this.cm.getTotalWidth()+'px';
1111     },
1112
1113     // private
1114     fitColumns : function(preventRefresh, onlyExpand, omitColumn){
1115         var cm = this.cm, i;
1116         var tw = cm.getTotalWidth(false);
1117         var aw = this.grid.getGridEl().getWidth(true)-this.getScrollOffset();
1118
1119         if(aw < 20){ // not initialized, so don't screw up the default widths
1120             return;
1121         }
1122         var extra = aw - tw;
1123
1124         if(extra === 0){
1125             return false;
1126         }
1127
1128         var vc = cm.getColumnCount(true);
1129         var ac = vc-(typeof omitColumn == 'number' ? 1 : 0);
1130         if(ac === 0){
1131             ac = 1;
1132             omitColumn = undefined;
1133         }
1134         var colCount = cm.getColumnCount();
1135         var cols = [];
1136         var extraCol = 0;
1137         var width = 0;
1138         var w;
1139         for (i = 0; i < colCount; i++){
1140             if(!cm.isHidden(i) && !cm.isFixed(i) && i !== omitColumn){
1141                 w = cm.getColumnWidth(i);
1142                 cols.push(i);
1143                 extraCol = i;
1144                 cols.push(w);
1145                 width += w;
1146             }
1147         }
1148         var frac = (aw - cm.getTotalWidth())/width;
1149         while (cols.length){
1150             w = cols.pop();
1151             i = cols.pop();
1152             cm.setColumnWidth(i, Math.max(this.grid.minColumnWidth, Math.floor(w + w*frac)), true);
1153         }
1154
1155         if((tw = cm.getTotalWidth(false)) > aw){
1156             var adjustCol = ac != vc ? omitColumn : extraCol;
1157              cm.setColumnWidth(adjustCol, Math.max(1,
1158                      cm.getColumnWidth(adjustCol)- (tw-aw)), true);
1159         }
1160
1161         if(preventRefresh !== true){
1162             this.updateAllColumnWidths();
1163         }
1164
1165
1166         return true;
1167     },
1168
1169     // private
1170     autoExpand : function(preventUpdate){
1171         var g = this.grid, cm = this.cm;
1172         if(!this.userResized && g.autoExpandColumn){
1173             var tw = cm.getTotalWidth(false);
1174             var aw = this.grid.getGridEl().getWidth(true)-this.getScrollOffset();
1175             if(tw != aw){
1176                 var ci = cm.getIndexById(g.autoExpandColumn);
1177                 var currentWidth = cm.getColumnWidth(ci);
1178                 var cw = Math.min(Math.max(((aw-tw)+currentWidth), g.autoExpandMin), g.autoExpandMax);
1179                 if(cw != currentWidth){
1180                     cm.setColumnWidth(ci, cw, true);
1181                     if(preventUpdate !== true){
1182                         this.updateColumnWidth(ci, cw);
1183                     }
1184                 }
1185             }
1186         }
1187     },
1188
1189     // private
1190     getColumnData : function(){
1191         // build a map for all the columns
1192         var cs = [], cm = this.cm, colCount = cm.getColumnCount();
1193         for(var i = 0; i < colCount; i++){
1194             var name = cm.getDataIndex(i);
1195             cs[i] = {
1196                 name : (typeof name == 'undefined' ? this.ds.fields.get(i).name : name),
1197                 renderer : cm.getRenderer(i),
1198                 id : cm.getColumnId(i),
1199                 style : this.getColumnStyle(i)
1200             };
1201         }
1202         return cs;
1203     },
1204
1205     // private
1206     renderRows : function(startRow, endRow){
1207         // pull in all the crap needed to render rows
1208         var g = this.grid, cm = g.colModel, ds = g.store, stripe = g.stripeRows;
1209         var colCount = cm.getColumnCount();
1210
1211         if(ds.getCount() < 1){
1212             return '';
1213         }
1214
1215         var cs = this.getColumnData();
1216
1217         startRow = startRow || 0;
1218         endRow = typeof endRow == "undefined"? ds.getCount()-1 : endRow;
1219
1220         // records to render
1221         var rs = ds.getRange(startRow, endRow);
1222
1223         return this.doRender(cs, rs, ds, startRow, colCount, stripe);
1224     },
1225
1226     // private
1227     renderBody : function(){
1228         var markup = this.renderRows() || '&#160;';
1229         return this.templates.body.apply({rows: markup});
1230     },
1231
1232     // private
1233     refreshRow : function(record){
1234         var ds = this.ds, index;
1235         if(typeof record == 'number'){
1236             index = record;
1237             record = ds.getAt(index);
1238             if(!record){
1239                 return;
1240             }
1241         }else{
1242             index = ds.indexOf(record);
1243             if(index < 0){
1244                 return;
1245             }
1246         }
1247         this.insertRows(ds, index, index, true);
1248         this.getRow(index).rowIndex = index;
1249         this.onRemove(ds, record, index+1, true);
1250         this.fireEvent('rowupdated', this, index, record);
1251     },
1252
1253     /**
1254      * Refreshs the grid UI
1255      * @param {Boolean} headersToo (optional) True to also refresh the headers
1256      */
1257     refresh : function(headersToo){
1258         this.fireEvent('beforerefresh', this);
1259         this.grid.stopEditing(true);
1260
1261         var result = this.renderBody();
1262         this.mainBody.update(result).setWidth(this.getTotalWidth());
1263         if(headersToo === true){
1264             this.updateHeaders();
1265             this.updateHeaderSortState();
1266         }
1267         this.processRows(0, true);
1268         this.layout();
1269         this.applyEmptyText();
1270         this.fireEvent('refresh', this);
1271     },
1272
1273     // private
1274     applyEmptyText : function(){
1275         if(this.emptyText && !this.hasRows()){
1276             this.mainBody.update('<div class="x-grid-empty">' + this.emptyText + '</div>');
1277         }
1278     },
1279
1280     // private
1281     updateHeaderSortState : function(){
1282         var state = this.ds.getSortState();
1283         if(!state){
1284             return;
1285         }
1286         if(!this.sortState || (this.sortState.field != state.field || this.sortState.direction != state.direction)){
1287             this.grid.fireEvent('sortchange', this.grid, state);
1288         }
1289         this.sortState = state;
1290         var sortColumn = this.cm.findColumnIndex(state.field);
1291         if(sortColumn != -1){
1292             var sortDir = state.direction;
1293             this.updateSortIcon(sortColumn, sortDir);
1294         }
1295     },
1296
1297     // private
1298     destroy : function(){
1299         if(this.colMenu){
1300             Ext.menu.MenuMgr.unregister(this.colMenu);
1301             this.colMenu.destroy();
1302             delete this.colMenu;
1303         }
1304         if(this.hmenu){
1305             Ext.menu.MenuMgr.unregister(this.hmenu);
1306             this.hmenu.destroy();
1307             delete this.hmenu;
1308         }
1309
1310         this.initData(null, null);
1311         this.purgeListeners();
1312         Ext.fly(this.innerHd).un("click", this.handleHdDown, this);
1313
1314         if(this.grid.enableColumnMove){
1315             Ext.destroy(
1316                 this.columnDrag.el,
1317                 this.columnDrag.proxy.ghost,
1318                 this.columnDrag.proxy.el,
1319                 this.columnDrop.el,
1320                 this.columnDrop.proxyTop,
1321                 this.columnDrop.proxyBottom,
1322                 this.columnDrag.dragData.ddel,
1323                 this.columnDrag.dragData.header
1324             );
1325             if (this.columnDrag.proxy.anim) {
1326                 Ext.destroy(this.columnDrag.proxy.anim);
1327             }
1328             delete this.columnDrag.proxy.ghost;
1329             delete this.columnDrag.dragData.ddel;
1330             delete this.columnDrag.dragData.header;
1331             this.columnDrag.destroy();
1332             delete Ext.dd.DDM.locationCache[this.columnDrag.id];
1333             delete this.columnDrag._domRef;
1334
1335             delete this.columnDrop.proxyTop;
1336             delete this.columnDrop.proxyBottom;
1337             this.columnDrop.destroy();
1338             delete Ext.dd.DDM.locationCache["gridHeader" + this.grid.getGridEl().id];
1339             delete this.columnDrop._domRef;
1340             delete Ext.dd.DDM.ids[this.columnDrop.ddGroup];
1341         }
1342
1343         if (this.splitone){ // enableColumnResize
1344             this.splitone.destroy();
1345             delete this.splitone._domRef;
1346             delete Ext.dd.DDM.ids["gridSplitters" + this.grid.getGridEl().id];
1347         }
1348
1349         Ext.fly(this.innerHd).removeAllListeners();
1350         Ext.removeNode(this.innerHd);
1351         delete this.innerHd;
1352
1353         Ext.destroy(
1354             this.el,
1355             this.mainWrap,
1356             this.mainHd,
1357             this.scroller,
1358             this.mainBody,
1359             this.focusEl,
1360             this.resizeMarker,
1361             this.resizeProxy,
1362             this.activeHdBtn,
1363             this.dragZone,
1364             this.splitZone,
1365             this._flyweight
1366         );
1367
1368         delete this.grid.container;
1369
1370         if(this.dragZone){
1371             this.dragZone.destroy();
1372         }
1373
1374         Ext.dd.DDM.currentTarget = null;
1375         delete Ext.dd.DDM.locationCache[this.grid.getGridEl().id];
1376
1377         Ext.EventManager.removeResizeListener(this.onWindowResize, this);
1378     },
1379
1380     // private
1381     onDenyColumnHide : function(){
1382
1383     },
1384
1385     // private
1386     render : function(){
1387         if(this.autoFill){
1388             var ct = this.grid.ownerCt;
1389             if (ct && ct.getLayout()){
1390                 ct.on('afterlayout', function(){ 
1391                     this.fitColumns(true, true);
1392                     this.updateHeaders(); 
1393                 }, this, {single: true}); 
1394             }else{ 
1395                 this.fitColumns(true, true); 
1396             }
1397         }else if(this.forceFit){
1398             this.fitColumns(true, false);
1399         }else if(this.grid.autoExpandColumn){
1400             this.autoExpand(true);
1401         }
1402
1403         this.renderUI();
1404     },
1405
1406     /* --------------------------------- Model Events and Handlers --------------------------------*/
1407     // private
1408     initData : function(ds, cm){
1409         if(this.ds){
1410             this.ds.un('load', this.onLoad, this);
1411             this.ds.un('datachanged', this.onDataChange, this);
1412             this.ds.un('add', this.onAdd, this);
1413             this.ds.un('remove', this.onRemove, this);
1414             this.ds.un('update', this.onUpdate, this);
1415             this.ds.un('clear', this.onClear, this);
1416             if(this.ds !== ds && this.ds.autoDestroy){
1417                 this.ds.destroy();
1418             }
1419         }
1420         if(ds){
1421             ds.on({
1422                 scope: this,
1423                 load: this.onLoad,
1424                 datachanged: this.onDataChange,
1425                 add: this.onAdd,
1426                 remove: this.onRemove,
1427                 update: this.onUpdate,
1428                 clear: this.onClear
1429             });
1430         }
1431         this.ds = ds;
1432
1433         if(this.cm){
1434             this.cm.un('configchange', this.onColConfigChange, this);
1435             this.cm.un('widthchange', this.onColWidthChange, this);
1436             this.cm.un('headerchange', this.onHeaderChange, this);
1437             this.cm.un('hiddenchange', this.onHiddenChange, this);
1438             this.cm.un('columnmoved', this.onColumnMove, this);
1439         }
1440         if(cm){
1441             delete this.lastViewWidth;
1442             cm.on({
1443                 scope: this,
1444                 configchange: this.onColConfigChange,
1445                 widthchange: this.onColWidthChange,
1446                 headerchange: this.onHeaderChange,
1447                 hiddenchange: this.onHiddenChange,
1448                 columnmoved: this.onColumnMove
1449             });
1450         }
1451         this.cm = cm;
1452     },
1453
1454     // private
1455     onDataChange : function(){
1456         this.refresh();
1457         this.updateHeaderSortState();
1458         this.syncFocusEl(0);
1459     },
1460
1461     // private
1462     onClear : function(){
1463         this.refresh();
1464         this.syncFocusEl(0);
1465     },
1466
1467     // private
1468     onUpdate : function(ds, record){
1469         this.refreshRow(record);
1470     },
1471
1472     // private
1473     onAdd : function(ds, records, index){
1474         this.insertRows(ds, index, index + (records.length-1));
1475     },
1476
1477     // private
1478     onRemove : function(ds, record, index, isUpdate){
1479         if(isUpdate !== true){
1480             this.fireEvent('beforerowremoved', this, index, record);
1481         }
1482         this.removeRow(index);
1483         if(isUpdate !== true){
1484             this.processRows(index);
1485             this.applyEmptyText();
1486             this.fireEvent('rowremoved', this, index, record);
1487         }
1488     },
1489
1490     // private
1491     onLoad : function(){
1492         this.scrollToTop.defer(Ext.isGecko ? 1 : 0, this);
1493     },
1494
1495     // private
1496     onColWidthChange : function(cm, col, width){
1497         this.updateColumnWidth(col, width);
1498     },
1499
1500     // private
1501     onHeaderChange : function(cm, col, text){
1502         this.updateHeaders();
1503     },
1504
1505     // private
1506     onHiddenChange : function(cm, col, hidden){
1507         this.updateColumnHidden(col, hidden);
1508     },
1509
1510     // private
1511     onColumnMove : function(cm, oldIndex, newIndex){
1512         this.indexMap = null;
1513         var s = this.getScrollState();
1514         this.refresh(true);
1515         this.restoreScroll(s);
1516         this.afterMove(newIndex);
1517         this.grid.fireEvent('columnmove', oldIndex, newIndex);
1518     },
1519
1520     // private
1521     onColConfigChange : function(){
1522         delete this.lastViewWidth;
1523         this.indexMap = null;
1524         this.refresh(true);
1525     },
1526
1527     /* -------------------- UI Events and Handlers ------------------------------ */
1528     // private
1529     initUI : function(grid){
1530         grid.on('headerclick', this.onHeaderClick, this);
1531     },
1532
1533     // private
1534     initEvents : function(){
1535     },
1536
1537     // private
1538     onHeaderClick : function(g, index){
1539         if(this.headersDisabled || !this.cm.isSortable(index)){
1540             return;
1541         }
1542         g.stopEditing(true);
1543         g.store.sort(this.cm.getDataIndex(index));
1544     },
1545
1546     // private
1547     onRowOver : function(e, t){
1548         var row;
1549         if((row = this.findRowIndex(t)) !== false){
1550             this.addRowClass(row, 'x-grid3-row-over');
1551         }
1552     },
1553
1554     // private
1555     onRowOut : function(e, t){
1556         var row;
1557         if((row = this.findRowIndex(t)) !== false && !e.within(this.getRow(row), true)){
1558             this.removeRowClass(row, 'x-grid3-row-over');
1559         }
1560     },
1561
1562     // private
1563     handleWheel : function(e){
1564         e.stopPropagation();
1565     },
1566
1567     // private
1568     onRowSelect : function(row){
1569         this.addRowClass(row, this.selectedRowClass);
1570     },
1571
1572     // private
1573     onRowDeselect : function(row){
1574         this.removeRowClass(row, this.selectedRowClass);
1575     },
1576
1577     // private
1578     onCellSelect : function(row, col){
1579         var cell = this.getCell(row, col);
1580         if(cell){
1581             this.fly(cell).addClass('x-grid3-cell-selected');
1582         }
1583     },
1584
1585     // private
1586     onCellDeselect : function(row, col){
1587         var cell = this.getCell(row, col);
1588         if(cell){
1589             this.fly(cell).removeClass('x-grid3-cell-selected');
1590         }
1591     },
1592
1593     // private
1594     onColumnSplitterMoved : function(i, w){
1595         this.userResized = true;
1596         var cm = this.grid.colModel;
1597         cm.setColumnWidth(i, w, true);
1598
1599         if(this.forceFit){
1600             this.fitColumns(true, false, i);
1601             this.updateAllColumnWidths();
1602         }else{
1603             this.updateColumnWidth(i, w);
1604             this.syncHeaderScroll();
1605         }
1606
1607         this.grid.fireEvent('columnresize', i, w);
1608     },
1609
1610     // private
1611     handleHdMenuClick : function(item){
1612         var index = this.hdCtxIndex,
1613             cm = this.cm, 
1614             ds = this.ds,
1615             id = item.getItemId();
1616         switch(id){
1617             case 'asc':
1618                 ds.sort(cm.getDataIndex(index), 'ASC');
1619                 break;
1620             case 'desc':
1621                 ds.sort(cm.getDataIndex(index), 'DESC');
1622                 break;
1623             default:
1624                 index = cm.getIndexById(id.substr(4));
1625                 if(index != -1){
1626                     if(item.checked && cm.getColumnsBy(this.isHideableColumn, this).length <= 1){
1627                         this.onDenyColumnHide();
1628                         return false;
1629                     }
1630                     cm.setHidden(index, item.checked);
1631                 }
1632         }
1633         return true;
1634     },
1635
1636     // private
1637     isHideableColumn : function(c){
1638         return !c.hidden && !c.fixed;
1639     },
1640
1641     // private
1642     beforeColMenuShow : function(){
1643         var cm = this.cm,  colCount = cm.getColumnCount();
1644         this.colMenu.removeAll();
1645         for(var i = 0; i < colCount; i++){
1646             if(cm.config[i].fixed !== true && cm.config[i].hideable !== false){
1647                 this.colMenu.add(new Ext.menu.CheckItem({
1648                     itemId: 'col-'+cm.getColumnId(i),
1649                     text: cm.getColumnHeader(i),
1650                     checked: !cm.isHidden(i),
1651                     hideOnClick:false,
1652                     disabled: cm.config[i].hideable === false
1653                 }));
1654             }
1655         }
1656     },
1657
1658     // private
1659     handleHdDown : function(e, t){
1660         if(Ext.fly(t).hasClass('x-grid3-hd-btn')){
1661             e.stopEvent();
1662             var hd = this.findHeaderCell(t);
1663             Ext.fly(hd).addClass('x-grid3-hd-menu-open');
1664             var index = this.getCellIndex(hd);
1665             this.hdCtxIndex = index;
1666             var ms = this.hmenu.items, cm = this.cm;
1667             ms.get('asc').setDisabled(!cm.isSortable(index));
1668             ms.get('desc').setDisabled(!cm.isSortable(index));
1669             this.hmenu.on('hide', function(){
1670                 Ext.fly(hd).removeClass('x-grid3-hd-menu-open');
1671             }, this, {single:true});
1672             this.hmenu.show(t, 'tl-bl?');
1673         }
1674     },
1675
1676     // private
1677     handleHdOver : function(e, t){
1678         var hd = this.findHeaderCell(t);
1679         if(hd && !this.headersDisabled){
1680             this.activeHdRef = t;
1681             this.activeHdIndex = this.getCellIndex(hd);
1682             var fly = this.fly(hd);
1683             this.activeHdRegion = fly.getRegion();
1684             if(!this.cm.isMenuDisabled(this.activeHdIndex)){
1685                 fly.addClass('x-grid3-hd-over');
1686                 this.activeHdBtn = fly.child('.x-grid3-hd-btn');
1687                 if(this.activeHdBtn){
1688                     this.activeHdBtn.dom.style.height = (hd.firstChild.offsetHeight-1)+'px';
1689                 }
1690             }
1691         }
1692     },
1693
1694     // private
1695     handleHdMove : function(e, t){
1696         var hd = this.findHeaderCell(this.activeHdRef);
1697         if(hd && !this.headersDisabled){
1698             var hw = this.splitHandleWidth || 5,
1699                 r = this.activeHdRegion,
1700                 x = e.getPageX(),
1701                 ss = hd.style,
1702                 cur = '';
1703             if(this.grid.enableColumnResize !== false){
1704                 if(x - r.left <= hw && this.cm.isResizable(this.activeHdIndex-1)){
1705                     cur = Ext.isAir ? 'move' : Ext.isWebKit ? 'e-resize' : 'col-resize'; // col-resize not always supported
1706                 }else if(r.right - x <= (!this.activeHdBtn ? hw : 2) && this.cm.isResizable(this.activeHdIndex)){
1707                     cur = Ext.isAir ? 'move' : Ext.isWebKit ? 'w-resize' : 'col-resize';
1708                 }
1709             }
1710             ss.cursor = cur;
1711         }
1712     },
1713
1714     // private
1715     handleHdOut : function(e, t){
1716         var hd = this.findHeaderCell(t);
1717         if(hd && (!Ext.isIE || !e.within(hd, true))){
1718             this.activeHdRef = null;
1719             this.fly(hd).removeClass('x-grid3-hd-over');
1720             hd.style.cursor = '';
1721         }
1722     },
1723
1724     // private
1725     hasRows : function(){
1726         var fc = this.mainBody.dom.firstChild;
1727         return fc && fc.nodeType == 1 && fc.className != 'x-grid-empty';
1728     },
1729
1730     // back compat
1731     bind : function(d, c){
1732         this.initData(d, c);
1733     }
1734 });
1735
1736
1737 // private
1738 // This is a support class used internally by the Grid components
1739 Ext.grid.GridView.SplitDragZone = function(grid, hd){
1740     this.grid = grid;
1741     this.view = grid.getView();
1742     this.marker = this.view.resizeMarker;
1743     this.proxy = this.view.resizeProxy;
1744     Ext.grid.GridView.SplitDragZone.superclass.constructor.call(this, hd,
1745         'gridSplitters' + this.grid.getGridEl().id, {
1746         dragElId : Ext.id(this.proxy.dom), resizeFrame:false
1747     });
1748     this.scroll = false;
1749     this.hw = this.view.splitHandleWidth || 5;
1750 };
1751 Ext.extend(Ext.grid.GridView.SplitDragZone, Ext.dd.DDProxy, {
1752
1753     b4StartDrag : function(x, y){
1754         this.view.headersDisabled = true;
1755         var h = this.view.mainWrap.getHeight();
1756         this.marker.setHeight(h);
1757         this.marker.show();
1758         this.marker.alignTo(this.view.getHeaderCell(this.cellIndex), 'tl-tl', [-2, 0]);
1759         this.proxy.setHeight(h);
1760         var w = this.cm.getColumnWidth(this.cellIndex);
1761         var minw = Math.max(w-this.grid.minColumnWidth, 0);
1762         this.resetConstraints();
1763         this.setXConstraint(minw, 1000);
1764         this.setYConstraint(0, 0);
1765         this.minX = x - minw;
1766         this.maxX = x + 1000;
1767         this.startPos = x;
1768         Ext.dd.DDProxy.prototype.b4StartDrag.call(this, x, y);
1769     },
1770
1771
1772     handleMouseDown : function(e){
1773         var t = this.view.findHeaderCell(e.getTarget());
1774         if(t){
1775             var xy = this.view.fly(t).getXY(), x = xy[0], y = xy[1];
1776             var exy = e.getXY(), ex = exy[0];
1777             var w = t.offsetWidth, adjust = false;
1778             if((ex - x) <= this.hw){
1779                 adjust = -1;
1780             }else if((x+w) - ex <= this.hw){
1781                 adjust = 0;
1782             }
1783             if(adjust !== false){
1784                 this.cm = this.grid.colModel;
1785                 var ci = this.view.getCellIndex(t);
1786                 if(adjust == -1){
1787                   if (ci + adjust < 0) {
1788                     return;
1789                   }
1790                     while(this.cm.isHidden(ci+adjust)){
1791                         --adjust;
1792                         if(ci+adjust < 0){
1793                             return;
1794                         }
1795                     }
1796                 }
1797                 this.cellIndex = ci+adjust;
1798                 this.split = t.dom;
1799                 if(this.cm.isResizable(this.cellIndex) && !this.cm.isFixed(this.cellIndex)){
1800                     Ext.grid.GridView.SplitDragZone.superclass.handleMouseDown.apply(this, arguments);
1801                 }
1802             }else if(this.view.columnDrag){
1803                 this.view.columnDrag.callHandleMouseDown(e);
1804             }
1805         }
1806     },
1807
1808     endDrag : function(e){
1809         this.marker.hide();
1810         var v = this.view;
1811         var endX = Math.max(this.minX, e.getPageX());
1812         var diff = endX - this.startPos;
1813         v.onColumnSplitterMoved(this.cellIndex, this.cm.getColumnWidth(this.cellIndex)+diff);
1814         setTimeout(function(){
1815             v.headersDisabled = false;
1816         }, 50);
1817     },
1818
1819     autoOffset : function(){
1820         this.setDelta(0,0);
1821     }
1822 });