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