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