Upgrade to ExtJS 3.2.2 - Released 06/02/2010
[extjs.git] / src / widgets / grid / GridView.js
1 /*!
2  * Ext JS Library 3.2.2
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.isFixed(i) && i !== omitColumn){
1276                 w = cm.getColumnWidth(i);
1277                 cols.push(i, w);
1278                 if(!cm.isHidden(i)){
1279                     extraCol = i;
1280                     width += w;
1281                 }
1282             }
1283         }
1284         var frac = (aw - cm.getTotalWidth())/width;
1285         while (cols.length){
1286             w = cols.pop();
1287             i = cols.pop();
1288             cm.setColumnWidth(i, Math.max(this.grid.minColumnWidth, Math.floor(w + w*frac)), true);
1289         }
1290
1291         if((tw = cm.getTotalWidth(false)) > aw){
1292             var adjustCol = ac != vc ? omitColumn : extraCol;
1293              cm.setColumnWidth(adjustCol, Math.max(1,
1294                      cm.getColumnWidth(adjustCol)- (tw-aw)), true);
1295         }
1296
1297         if(preventRefresh !== true){
1298             this.updateAllColumnWidths();
1299         }
1300
1301
1302         return true;
1303     },
1304
1305     // private
1306     autoExpand : function(preventUpdate){
1307         var g = this.grid, cm = this.cm;
1308         if(!this.userResized && g.autoExpandColumn){
1309             var tw = cm.getTotalWidth(false);
1310             var aw = this.grid.getGridEl().getWidth(true)-this.getScrollOffset();
1311             if(tw != aw){
1312                 var ci = cm.getIndexById(g.autoExpandColumn);
1313                 var currentWidth = cm.getColumnWidth(ci);
1314                 var cw = Math.min(Math.max(((aw-tw)+currentWidth), g.autoExpandMin), g.autoExpandMax);
1315                 if(cw != currentWidth){
1316                     cm.setColumnWidth(ci, cw, true);
1317                     if(preventUpdate !== true){
1318                         this.updateColumnWidth(ci, cw);
1319                     }
1320                 }
1321             }
1322         }
1323     },
1324
1325     /**
1326      * @private
1327      * Returns an array of column configurations - one for each column
1328      * @return {Array} Array of column config objects. This includes the column name, renderer, id style and renderer
1329      */
1330     getColumnData : function(){
1331         // build a map for all the columns
1332         var cs       = [],
1333             cm       = this.cm,
1334             colCount = cm.getColumnCount();
1335
1336         for (var i = 0; i < colCount; i++) {
1337             var name = cm.getDataIndex(i);
1338
1339             cs[i] = {
1340                 name    : (!Ext.isDefined(name) ? this.ds.fields.get(i).name : name),
1341                 renderer: cm.getRenderer(i),
1342                 scope   : cm.getRendererScope(i),
1343                 id      : cm.getColumnId(i),
1344                 style   : this.getColumnStyle(i)
1345             };
1346         }
1347
1348         return cs;
1349     },
1350
1351     // private
1352     renderRows : function(startRow, endRow){
1353         // pull in all the crap needed to render rows
1354         var g = this.grid, cm = g.colModel, ds = g.store, stripe = g.stripeRows;
1355         var colCount = cm.getColumnCount();
1356
1357         if(ds.getCount() < 1){
1358             return '';
1359         }
1360
1361         var cs = this.getColumnData();
1362
1363         startRow = startRow || 0;
1364         endRow = !Ext.isDefined(endRow) ? ds.getCount()-1 : endRow;
1365
1366         // records to render
1367         var rs = ds.getRange(startRow, endRow);
1368
1369         return this.doRender(cs, rs, ds, startRow, colCount, stripe);
1370     },
1371
1372     // private
1373     renderBody : function(){
1374         var markup = this.renderRows() || '&#160;';
1375         return this.templates.body.apply({rows: markup});
1376     },
1377
1378     // private
1379     refreshRow : function(record){
1380         var ds = this.ds, index;
1381         if(Ext.isNumber(record)){
1382             index = record;
1383             record = ds.getAt(index);
1384             if(!record){
1385                 return;
1386             }
1387         }else{
1388             index = ds.indexOf(record);
1389             if(index < 0){
1390                 return;
1391             }
1392         }
1393         this.insertRows(ds, index, index, true);
1394         this.getRow(index).rowIndex = index;
1395         this.onRemove(ds, record, index+1, true);
1396         this.fireEvent('rowupdated', this, index, record);
1397     },
1398
1399     /**
1400      * Refreshs the grid UI
1401      * @param {Boolean} headersToo (optional) True to also refresh the headers
1402      */
1403     refresh : function(headersToo){
1404         this.fireEvent('beforerefresh', this);
1405         this.grid.stopEditing(true);
1406
1407         var result = this.renderBody();
1408         this.mainBody.update(result).setWidth(this.getTotalWidth());
1409         if(headersToo === true){
1410             this.updateHeaders();
1411             this.updateHeaderSortState();
1412         }
1413         this.processRows(0, true);
1414         this.layout();
1415         this.applyEmptyText();
1416         this.fireEvent('refresh', this);
1417     },
1418
1419     /**
1420      * @private
1421      * Displays the configured emptyText if there are currently no rows to display
1422      */
1423     applyEmptyText : function(){
1424         if(this.emptyText && !this.hasRows()){
1425             this.mainBody.update('<div class="x-grid-empty">' + this.emptyText + '</div>');
1426         }
1427     },
1428
1429     /**
1430      * @private
1431      * Adds sorting classes to the column headers based on the bound store's sortInfo. Fires the 'sortchange' event
1432      * if the sorting has changed since this function was last run.
1433      */
1434     updateHeaderSortState : function(){
1435         var state = this.ds.getSortState();
1436         if (!state) {
1437             return;
1438         }
1439
1440         if (!this.sortState || (this.sortState.field != state.field || this.sortState.direction != state.direction)) {
1441             this.grid.fireEvent('sortchange', this.grid, state);
1442         }
1443
1444         this.sortState = state;
1445
1446         var sortColumn = this.cm.findColumnIndex(state.field);
1447         if (sortColumn != -1){
1448             var sortDir = state.direction;
1449             this.updateSortIcon(sortColumn, sortDir);
1450         }
1451     },
1452
1453     /**
1454      * @private
1455      * Removes any sorting indicator classes from the column headers
1456      */
1457     clearHeaderSortState : function(){
1458         if (!this.sortState) {
1459             return;
1460         }
1461         this.grid.fireEvent('sortchange', this.grid, null);
1462         this.mainHd.select('td').removeClass(this.sortClasses);
1463         delete this.sortState;
1464     },
1465
1466     // private
1467     destroy : function(){
1468         if (this.scrollToTopTask && this.scrollToTopTask.cancel){
1469             this.scrollToTopTask.cancel();
1470         }
1471         if(this.colMenu){
1472             Ext.menu.MenuMgr.unregister(this.colMenu);
1473             this.colMenu.destroy();
1474             delete this.colMenu;
1475         }
1476         if(this.hmenu){
1477             Ext.menu.MenuMgr.unregister(this.hmenu);
1478             this.hmenu.destroy();
1479             delete this.hmenu;
1480         }
1481
1482         this.initData(null, null);
1483         this.purgeListeners();
1484         Ext.fly(this.innerHd).un("click", this.handleHdDown, this);
1485
1486         if(this.grid.enableColumnMove){
1487             Ext.destroy(
1488                 this.columnDrag.el,
1489                 this.columnDrag.proxy.ghost,
1490                 this.columnDrag.proxy.el,
1491                 this.columnDrop.el,
1492                 this.columnDrop.proxyTop,
1493                 this.columnDrop.proxyBottom,
1494                 this.columnDrag.dragData.ddel,
1495                 this.columnDrag.dragData.header
1496             );
1497             if (this.columnDrag.proxy.anim) {
1498                 Ext.destroy(this.columnDrag.proxy.anim);
1499             }
1500             delete this.columnDrag.proxy.ghost;
1501             delete this.columnDrag.dragData.ddel;
1502             delete this.columnDrag.dragData.header;
1503             this.columnDrag.destroy();
1504             delete Ext.dd.DDM.locationCache[this.columnDrag.id];
1505             delete this.columnDrag._domRef;
1506
1507             delete this.columnDrop.proxyTop;
1508             delete this.columnDrop.proxyBottom;
1509             this.columnDrop.destroy();
1510             delete Ext.dd.DDM.locationCache["gridHeader" + this.grid.getGridEl().id];
1511             delete this.columnDrop._domRef;
1512             delete Ext.dd.DDM.ids[this.columnDrop.ddGroup];
1513         }
1514
1515         if (this.splitZone){ // enableColumnResize
1516             this.splitZone.destroy();
1517             delete this.splitZone._domRef;
1518             delete Ext.dd.DDM.ids["gridSplitters" + this.grid.getGridEl().id];
1519         }
1520
1521         Ext.fly(this.innerHd).removeAllListeners();
1522         Ext.removeNode(this.innerHd);
1523         delete this.innerHd;
1524
1525         Ext.destroy(
1526             this.el,
1527             this.mainWrap,
1528             this.mainHd,
1529             this.scroller,
1530             this.mainBody,
1531             this.focusEl,
1532             this.resizeMarker,
1533             this.resizeProxy,
1534             this.activeHdBtn,
1535             this.dragZone,
1536             this.splitZone,
1537             this._flyweight
1538         );
1539
1540         delete this.grid.container;
1541
1542         if(this.dragZone){
1543             this.dragZone.destroy();
1544         }
1545
1546         Ext.dd.DDM.currentTarget = null;
1547         delete Ext.dd.DDM.locationCache[this.grid.getGridEl().id];
1548
1549         Ext.EventManager.removeResizeListener(this.onWindowResize, this);
1550     },
1551
1552     // private
1553     onDenyColumnHide : function(){
1554
1555     },
1556
1557     // private
1558     render : function(){
1559         if(this.autoFill){
1560             var ct = this.grid.ownerCt;
1561             if (ct && ct.getLayout()){
1562                 ct.on('afterlayout', function(){
1563                     this.fitColumns(true, true);
1564                     this.updateHeaders();
1565                 }, this, {single: true});
1566             }else{
1567                 this.fitColumns(true, true);
1568             }
1569         }else if(this.forceFit){
1570             this.fitColumns(true, false);
1571         }else if(this.grid.autoExpandColumn){
1572             this.autoExpand(true);
1573         }
1574
1575         this.renderUI();
1576     },
1577
1578     /* --------------------------------- Model Events and Handlers --------------------------------*/
1579     // private
1580     initData : function(ds, cm){
1581         if(this.ds){
1582             this.ds.un('load', this.onLoad, this);
1583             this.ds.un('datachanged', this.onDataChange, this);
1584             this.ds.un('add', this.onAdd, this);
1585             this.ds.un('remove', this.onRemove, this);
1586             this.ds.un('update', this.onUpdate, this);
1587             this.ds.un('clear', this.onClear, this);
1588             if(this.ds !== ds && this.ds.autoDestroy){
1589                 this.ds.destroy();
1590             }
1591         }
1592         if(ds){
1593             ds.on({
1594                 scope: this,
1595                 load: this.onLoad,
1596                 datachanged: this.onDataChange,
1597                 add: this.onAdd,
1598                 remove: this.onRemove,
1599                 update: this.onUpdate,
1600                 clear: this.onClear
1601             });
1602         }
1603         this.ds = ds;
1604
1605         if(this.cm){
1606             this.cm.un('configchange', this.onColConfigChange, this);
1607             this.cm.un('widthchange', this.onColWidthChange, this);
1608             this.cm.un('headerchange', this.onHeaderChange, this);
1609             this.cm.un('hiddenchange', this.onHiddenChange, this);
1610             this.cm.un('columnmoved', this.onColumnMove, this);
1611         }
1612         if(cm){
1613             delete this.lastViewWidth;
1614             cm.on({
1615                 scope: this,
1616                 configchange: this.onColConfigChange,
1617                 widthchange: this.onColWidthChange,
1618                 headerchange: this.onHeaderChange,
1619                 hiddenchange: this.onHiddenChange,
1620                 columnmoved: this.onColumnMove
1621             });
1622         }
1623         this.cm = cm;
1624     },
1625
1626     // private
1627     onDataChange : function(){
1628         this.refresh();
1629         this.updateHeaderSortState();
1630         this.syncFocusEl(0);
1631     },
1632
1633     // private
1634     onClear : function(){
1635         this.refresh();
1636         this.syncFocusEl(0);
1637     },
1638
1639     // private
1640     onUpdate : function(ds, record){
1641         this.refreshRow(record);
1642     },
1643
1644     // private
1645     onAdd : function(ds, records, index){
1646         this.insertRows(ds, index, index + (records.length-1));
1647     },
1648
1649     // private
1650     onRemove : function(ds, record, index, isUpdate){
1651         if(isUpdate !== true){
1652             this.fireEvent('beforerowremoved', this, index, record);
1653         }
1654         this.removeRow(index);
1655         if(isUpdate !== true){
1656             this.processRows(index);
1657             this.applyEmptyText();
1658             this.fireEvent('rowremoved', this, index, record);
1659         }
1660     },
1661
1662     // private
1663     onLoad : function(){
1664         if (Ext.isGecko){
1665             if (!this.scrollToTopTask) {
1666                 this.scrollToTopTask = new Ext.util.DelayedTask(this.scrollToTop, this);
1667             }
1668             this.scrollToTopTask.delay(1);
1669         }else{
1670             this.scrollToTop();
1671         }
1672     },
1673
1674     // private
1675     onColWidthChange : function(cm, col, width){
1676         this.updateColumnWidth(col, width);
1677     },
1678
1679     // private
1680     onHeaderChange : function(cm, col, text){
1681         this.updateHeaders();
1682     },
1683
1684     // private
1685     onHiddenChange : function(cm, col, hidden){
1686         this.updateColumnHidden(col, hidden);
1687     },
1688
1689     // private
1690     onColumnMove : function(cm, oldIndex, newIndex){
1691         this.indexMap = null;
1692         var s = this.getScrollState();
1693         this.refresh(true);
1694         this.restoreScroll(s);
1695         this.afterMove(newIndex);
1696         this.grid.fireEvent('columnmove', oldIndex, newIndex);
1697     },
1698
1699     // private
1700     onColConfigChange : function(){
1701         delete this.lastViewWidth;
1702         this.indexMap = null;
1703         this.refresh(true);
1704     },
1705
1706     /* -------------------- UI Events and Handlers ------------------------------ */
1707     // private
1708     initUI : function(grid){
1709         grid.on('headerclick', this.onHeaderClick, this);
1710     },
1711
1712     // private
1713     initEvents : function(){
1714     },
1715
1716     // private
1717     onHeaderClick : function(g, index){
1718         if(this.headersDisabled || !this.cm.isSortable(index)){
1719             return;
1720         }
1721         g.stopEditing(true);
1722         g.store.sort(this.cm.getDataIndex(index));
1723     },
1724
1725     // private
1726     onRowOver : function(e, t){
1727         var row;
1728         if((row = this.findRowIndex(t)) !== false){
1729             this.addRowClass(row, 'x-grid3-row-over');
1730         }
1731     },
1732
1733     // private
1734     onRowOut : function(e, t){
1735         var row;
1736         if((row = this.findRowIndex(t)) !== false && !e.within(this.getRow(row), true)){
1737             this.removeRowClass(row, 'x-grid3-row-over');
1738         }
1739     },
1740
1741     // private
1742     handleWheel : function(e){
1743         e.stopPropagation();
1744     },
1745
1746     // private
1747     onRowSelect : function(row){
1748         this.addRowClass(row, this.selectedRowClass);
1749     },
1750
1751     // private
1752     onRowDeselect : function(row){
1753         this.removeRowClass(row, this.selectedRowClass);
1754     },
1755
1756     // private
1757     onCellSelect : function(row, col){
1758         var cell = this.getCell(row, col);
1759         if(cell){
1760             this.fly(cell).addClass('x-grid3-cell-selected');
1761         }
1762     },
1763
1764     // private
1765     onCellDeselect : function(row, col){
1766         var cell = this.getCell(row, col);
1767         if(cell){
1768             this.fly(cell).removeClass('x-grid3-cell-selected');
1769         }
1770     },
1771
1772     // private
1773     onColumnSplitterMoved : function(i, w){
1774         this.userResized = true;
1775         var cm = this.grid.colModel;
1776         cm.setColumnWidth(i, w, true);
1777
1778         if(this.forceFit){
1779             this.fitColumns(true, false, i);
1780             this.updateAllColumnWidths();
1781         }else{
1782             this.updateColumnWidth(i, w);
1783             this.syncHeaderScroll();
1784         }
1785
1786         this.grid.fireEvent('columnresize', i, w);
1787     },
1788
1789     // private
1790     handleHdMenuClick : function(item){
1791         var index = this.hdCtxIndex,
1792             cm = this.cm,
1793             ds = this.ds,
1794             id = item.getItemId();
1795         switch(id){
1796             case 'asc':
1797                 ds.sort(cm.getDataIndex(index), 'ASC');
1798                 break;
1799             case 'desc':
1800                 ds.sort(cm.getDataIndex(index), 'DESC');
1801                 break;
1802             default:
1803                 index = cm.getIndexById(id.substr(4));
1804                 if(index != -1){
1805                     if(item.checked && cm.getColumnsBy(this.isHideableColumn, this).length <= 1){
1806                         this.onDenyColumnHide();
1807                         return false;
1808                     }
1809                     cm.setHidden(index, item.checked);
1810                 }
1811         }
1812         return true;
1813     },
1814
1815     // private
1816     isHideableColumn : function(c){
1817         return !c.hidden;
1818     },
1819
1820     // private
1821     beforeColMenuShow : function(){
1822         var cm = this.cm,  colCount = cm.getColumnCount();
1823         this.colMenu.removeAll();
1824         for(var i = 0; i < colCount; i++){
1825             if(cm.config[i].hideable !== false){
1826                 this.colMenu.add(new Ext.menu.CheckItem({
1827                     itemId: 'col-'+cm.getColumnId(i),
1828                     text: cm.getColumnHeader(i),
1829                     checked: !cm.isHidden(i),
1830                     hideOnClick:false,
1831                     disabled: cm.config[i].hideable === false
1832                 }));
1833             }
1834         }
1835     },
1836
1837     // private
1838     handleHdDown : function(e, t){
1839         if(Ext.fly(t).hasClass('x-grid3-hd-btn')){
1840             e.stopEvent();
1841             var hd = this.findHeaderCell(t);
1842             Ext.fly(hd).addClass('x-grid3-hd-menu-open');
1843             var index = this.getCellIndex(hd);
1844             this.hdCtxIndex = index;
1845             var ms = this.hmenu.items, cm = this.cm;
1846             ms.get('asc').setDisabled(!cm.isSortable(index));
1847             ms.get('desc').setDisabled(!cm.isSortable(index));
1848             this.hmenu.on('hide', function(){
1849                 Ext.fly(hd).removeClass('x-grid3-hd-menu-open');
1850             }, this, {single:true});
1851             this.hmenu.show(t, 'tl-bl?');
1852         }
1853     },
1854
1855     // private
1856     handleHdOver : function(e, t){
1857         var hd = this.findHeaderCell(t);
1858         if(hd && !this.headersDisabled){
1859             this.activeHdRef = t;
1860             this.activeHdIndex = this.getCellIndex(hd);
1861             var fly = this.fly(hd);
1862             this.activeHdRegion = fly.getRegion();
1863             if(!this.cm.isMenuDisabled(this.activeHdIndex)){
1864                 fly.addClass('x-grid3-hd-over');
1865                 this.activeHdBtn = fly.child('.x-grid3-hd-btn');
1866                 if(this.activeHdBtn){
1867                     this.activeHdBtn.dom.style.height = (hd.firstChild.offsetHeight-1)+'px';
1868                 }
1869             }
1870         }
1871     },
1872
1873     // private
1874     handleHdMove : function(e, t){
1875         var hd = this.findHeaderCell(this.activeHdRef);
1876         if(hd && !this.headersDisabled){
1877             var hw = this.splitHandleWidth || 5,
1878                 r = this.activeHdRegion,
1879                 x = e.getPageX(),
1880                 ss = hd.style,
1881                 cur = '';
1882             if(this.grid.enableColumnResize !== false){
1883                 if(x - r.left <= hw && this.cm.isResizable(this.activeHdIndex-1)){
1884                     cur = Ext.isAir ? 'move' : Ext.isWebKit ? 'e-resize' : 'col-resize'; // col-resize not always supported
1885                 }else if(r.right - x <= (!this.activeHdBtn ? hw : 2) && this.cm.isResizable(this.activeHdIndex)){
1886                     cur = Ext.isAir ? 'move' : Ext.isWebKit ? 'w-resize' : 'col-resize';
1887                 }
1888             }
1889             ss.cursor = cur;
1890         }
1891     },
1892
1893     // private
1894     handleHdOut : function(e, t){
1895         var hd = this.findHeaderCell(t);
1896         if(hd && (!Ext.isIE || !e.within(hd, true))){
1897             this.activeHdRef = null;
1898             this.fly(hd).removeClass('x-grid3-hd-over');
1899             hd.style.cursor = '';
1900         }
1901     },
1902
1903     // private
1904     hasRows : function(){
1905         var fc = this.mainBody.dom.firstChild;
1906         return fc && fc.nodeType == 1 && fc.className != 'x-grid-empty';
1907     },
1908
1909     // back compat
1910     bind : function(d, c){
1911         this.initData(d, c);
1912     }
1913 });
1914
1915
1916 // private
1917 // This is a support class used internally by the Grid components
1918 Ext.grid.GridView.SplitDragZone = Ext.extend(Ext.dd.DDProxy, {
1919     
1920     constructor: function(grid, hd){
1921         this.grid = grid;
1922         this.view = grid.getView();
1923         this.marker = this.view.resizeMarker;
1924         this.proxy = this.view.resizeProxy;
1925         Ext.grid.GridView.SplitDragZone.superclass.constructor.call(this, hd,
1926             'gridSplitters' + this.grid.getGridEl().id, {
1927             dragElId : Ext.id(this.proxy.dom), resizeFrame:false
1928         });
1929         this.scroll = false;
1930         this.hw = this.view.splitHandleWidth || 5;
1931     },
1932
1933     b4StartDrag : function(x, y){
1934         this.dragHeadersDisabled = this.view.headersDisabled;
1935         this.view.headersDisabled = true;
1936         var h = this.view.mainWrap.getHeight();
1937         this.marker.setHeight(h);
1938         this.marker.show();
1939         this.marker.alignTo(this.view.getHeaderCell(this.cellIndex), 'tl-tl', [-2, 0]);
1940         this.proxy.setHeight(h);
1941         var w = this.cm.getColumnWidth(this.cellIndex),
1942             minw = Math.max(w-this.grid.minColumnWidth, 0);
1943         this.resetConstraints();
1944         this.setXConstraint(minw, 1000);
1945         this.setYConstraint(0, 0);
1946         this.minX = x - minw;
1947         this.maxX = x + 1000;
1948         this.startPos = x;
1949         Ext.dd.DDProxy.prototype.b4StartDrag.call(this, x, y);
1950     },
1951
1952     allowHeaderDrag : function(e){
1953         return true;
1954     },
1955
1956     handleMouseDown : function(e){
1957         var t = this.view.findHeaderCell(e.getTarget());
1958         if(t && this.allowHeaderDrag(e)){
1959             var xy = this.view.fly(t).getXY(), 
1960                 x = xy[0], 
1961                 y = xy[1],
1962                 exy = e.getXY(), ex = exy[0],
1963                 w = t.offsetWidth, adjust = false;
1964                 
1965             if((ex - x) <= this.hw){
1966                 adjust = -1;
1967             }else if((x+w) - ex <= this.hw){
1968                 adjust = 0;
1969             }
1970             if(adjust !== false){
1971                 this.cm = this.grid.colModel;
1972                 var ci = this.view.getCellIndex(t);
1973                 if(adjust == -1){
1974                   if (ci + adjust < 0) {
1975                     return;
1976                   }
1977                     while(this.cm.isHidden(ci+adjust)){
1978                         --adjust;
1979                         if(ci+adjust < 0){
1980                             return;
1981                         }
1982                     }
1983                 }
1984                 this.cellIndex = ci+adjust;
1985                 this.split = t.dom;
1986                 if(this.cm.isResizable(this.cellIndex) && !this.cm.isFixed(this.cellIndex)){
1987                     Ext.grid.GridView.SplitDragZone.superclass.handleMouseDown.apply(this, arguments);
1988                 }
1989             }else if(this.view.columnDrag){
1990                 this.view.columnDrag.callHandleMouseDown(e);
1991             }
1992         }
1993     },
1994
1995     endDrag : function(e){
1996         this.marker.hide();
1997         var v = this.view,
1998             endX = Math.max(this.minX, e.getPageX()),
1999             diff = endX - this.startPos,
2000             disabled = this.dragHeadersDisabled;
2001             
2002         v.onColumnSplitterMoved(this.cellIndex, this.cm.getColumnWidth(this.cellIndex)+diff);
2003         setTimeout(function(){
2004             v.headersDisabled = disabled;
2005         }, 50);
2006     },
2007
2008     autoOffset : function(){
2009         this.setDelta(0,0);
2010     }
2011 });