Upgrade to ExtJS 3.3.1 - Released 11/30/2010
[extjs.git] / src / widgets / grid / GridView.js
1 /*!
2  * Ext JS Library 3.3.1
3  * Copyright(c) 2006-2010 Sencha Inc.
4  * licensing@sencha.com
5  * http://www.sencha.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      * <p>Defaults to <tt>false</tt>.  Specify <tt>true</tt> to have the column widths re-proportioned
121      * at <b>all times</b>.</p>
122      * <p>The {@link Ext.grid.Column#width initially configured width}</tt> of each
123      * column will be adjusted to fit the grid width and prevent horizontal scrolling. If columns are
124      * later resized (manually or programmatically), the other columns in the grid <b>will</b> be resized
125      * to fit the grid width.</p>
126      * <p>Columns which are configured with <code>fixed: true</code> are omitted from being resized.</p>
127      * <p>See <tt>{@link #autoFill}</tt>.</p>
128      */
129     forceFit : false,
130
131     /**
132      * @cfg {Array} sortClasses The CSS classes applied to a header when it is sorted. (defaults to <tt>['sort-asc', 'sort-desc']</tt>)
133      */
134     sortClasses : ['sort-asc', 'sort-desc'],
135
136     /**
137      * @cfg {String} sortAscText The text displayed in the 'Sort Ascending' menu item (defaults to <tt>'Sort Ascending'</tt>)
138      */
139     sortAscText : 'Sort Ascending',
140
141     /**
142      * @cfg {String} sortDescText The text displayed in the 'Sort Descending' menu item (defaults to <tt>'Sort Descending'</tt>)
143      */
144     sortDescText : 'Sort Descending',
145
146     /**
147      * @cfg {String} columnsText The text displayed in the 'Columns' menu item (defaults to <tt>'Columns'</tt>)
148      */
149     columnsText : 'Columns',
150
151     /**
152      * @cfg {String} selectedRowClass The CSS class applied to a selected row (defaults to <tt>'x-grid3-row-selected'</tt>). An
153      * example overriding the default styling:
154     <pre><code>
155     .x-grid3-row-selected {background-color: yellow;}
156     </code></pre>
157      * Note that this only controls the row, and will not do anything for the text inside it.  To style inner
158      * facets (like text) use something like:
159     <pre><code>
160     .x-grid3-row-selected .x-grid3-cell-inner {
161         color: #FFCC00;
162     }
163     </code></pre>
164      * @type String
165      */
166     selectedRowClass : 'x-grid3-row-selected',
167
168     // private
169     borderWidth : 2,
170     tdClass : 'x-grid3-cell',
171     hdCls : 'x-grid3-hd',
172     
173     
174     /**
175      * @cfg {Boolean} markDirty True to show the dirty cell indicator when a cell has been modified. Defaults to <tt>true</tt>.
176      */
177     markDirty : true,
178
179     /**
180      * @cfg {Number} cellSelectorDepth The number of levels to search for cells in event delegation (defaults to <tt>4</tt>)
181      */
182     cellSelectorDepth : 4,
183     
184     /**
185      * @cfg {Number} rowSelectorDepth The number of levels to search for rows in event delegation (defaults to <tt>10</tt>)
186      */
187     rowSelectorDepth : 10,
188
189     /**
190      * @cfg {Number} rowBodySelectorDepth The number of levels to search for row bodies in event delegation (defaults to <tt>10</tt>)
191      */
192     rowBodySelectorDepth : 10,
193
194     /**
195      * @cfg {String} cellSelector The selector used to find cells internally (defaults to <tt>'td.x-grid3-cell'</tt>)
196      */
197     cellSelector : 'td.x-grid3-cell',
198     
199     /**
200      * @cfg {String} rowSelector The selector used to find rows internally (defaults to <tt>'div.x-grid3-row'</tt>)
201      */
202     rowSelector : 'div.x-grid3-row',
203
204     /**
205      * @cfg {String} rowBodySelector The selector used to find row bodies internally (defaults to <tt>'div.x-grid3-row'</tt>)
206      */
207     rowBodySelector : 'div.x-grid3-row-body',
208
209     // private
210     firstRowCls: 'x-grid3-row-first',
211     lastRowCls: 'x-grid3-row-last',
212     rowClsRe: /(?:^|\s+)x-grid3-row-(first|last|alt)(?:\s+|$)/g,
213     
214     /**
215      * @cfg {String} headerMenuOpenCls The CSS class to add to the header cell when its menu is visible. Defaults to 'x-grid3-hd-menu-open'
216      */
217     headerMenuOpenCls: 'x-grid3-hd-menu-open',
218     
219     /**
220      * @cfg {String} rowOverCls The CSS class added to each row when it is hovered over. Defaults to 'x-grid3-row-over'
221      */
222     rowOverCls: 'x-grid3-row-over',
223
224     constructor : function(config) {
225         Ext.apply(this, config);
226         
227         // These events are only used internally by the grid components
228         this.addEvents(
229             /**
230              * @event beforerowremoved
231              * Internal UI Event. Fired before a row is removed.
232              * @param {Ext.grid.GridView} view
233              * @param {Number} rowIndex The index of the row to be removed.
234              * @param {Ext.data.Record} record The Record to be removed
235              */
236             'beforerowremoved',
237             
238             /**
239              * @event beforerowsinserted
240              * Internal UI Event. Fired before rows are inserted.
241              * @param {Ext.grid.GridView} view
242              * @param {Number} firstRow The index of the first row to be inserted.
243              * @param {Number} lastRow The index of the last row to be inserted.
244              */
245             'beforerowsinserted',
246             
247             /**
248              * @event beforerefresh
249              * Internal UI Event. Fired before the view is refreshed.
250              * @param {Ext.grid.GridView} view
251              */
252             'beforerefresh',
253             
254             /**
255              * @event rowremoved
256              * Internal UI Event. Fired after a row is removed.
257              * @param {Ext.grid.GridView} view
258              * @param {Number} rowIndex The index of the row that was removed.
259              * @param {Ext.data.Record} record The Record that was removed
260              */
261             'rowremoved',
262             
263             /**
264              * @event rowsinserted
265              * Internal UI Event. Fired after rows are inserted.
266              * @param {Ext.grid.GridView} view
267              * @param {Number} firstRow The index of the first inserted.
268              * @param {Number} lastRow The index of the last row inserted.
269              */
270             'rowsinserted',
271             
272             /**
273              * @event rowupdated
274              * Internal UI Event. Fired after a row has been updated.
275              * @param {Ext.grid.GridView} view
276              * @param {Number} firstRow The index of the row updated.
277              * @param {Ext.data.record} record The Record backing the row updated.
278              */
279             'rowupdated',
280             
281             /**
282              * @event refresh
283              * Internal UI Event. Fired after the GridView's body has been refreshed.
284              * @param {Ext.grid.GridView} view
285              */
286             'refresh'
287         );
288         
289         Ext.grid.GridView.superclass.constructor.call(this);
290     },
291
292     /* -------------------------------- UI Specific ----------------------------- */
293     
294     /**
295      * The master template to use when rendering the GridView. Has a default template
296      * @property Ext.Template
297      * @type masterTpl
298      */
299     masterTpl: new Ext.Template(
300         '<div class="x-grid3" hidefocus="true">',
301             '<div class="x-grid3-viewport">',
302                 '<div class="x-grid3-header">',
303                     '<div class="x-grid3-header-inner">',
304                         '<div class="x-grid3-header-offset" style="{ostyle}">{header}</div>',
305                     '</div>',
306                     '<div class="x-clear"></div>',
307                 '</div>',
308                 '<div class="x-grid3-scroller">',
309                     '<div class="x-grid3-body" style="{bstyle}">{body}</div>',
310                     '<a href="#" class="x-grid3-focus" tabIndex="-1"></a>',
311                 '</div>',
312             '</div>',
313             '<div class="x-grid3-resize-marker">&#160;</div>',
314             '<div class="x-grid3-resize-proxy">&#160;</div>',
315         '</div>'
316     ),
317     
318     /**
319      * The template to use when rendering headers. Has a default template
320      * @property headerTpl
321      * @type Ext.Template
322      */
323     headerTpl: new Ext.Template(
324         '<table border="0" cellspacing="0" cellpadding="0" style="{tstyle}">',
325             '<thead>',
326                 '<tr class="x-grid3-hd-row">{cells}</tr>',
327             '</thead>',
328         '</table>'
329     ),
330     
331     /**
332      * The template to use when rendering the body. Has a default template
333      * @property bodyTpl
334      * @type Ext.Template
335      */
336     bodyTpl: new Ext.Template('{rows}'),
337     
338     /**
339      * The template to use to render each cell. Has a default template
340      * @property cellTpl
341      * @type Ext.Template
342      */
343     cellTpl: new Ext.Template(
344         '<td class="x-grid3-col x-grid3-cell x-grid3-td-{id} {css}" style="{style}" tabIndex="0" {cellAttr}>',
345             '<div class="x-grid3-cell-inner x-grid3-col-{id}" unselectable="on" {attr}>{value}</div>',
346         '</td>'
347     ),
348     
349     /**
350      * @private
351      * Provides default templates if they are not given for this particular instance. Most of the templates are defined on
352      * the prototype, the ones defined inside this function are done so because they are based on Grid or GridView configuration
353      */
354     initTemplates : function() {
355         var templates = this.templates || {},
356             template, name,
357             
358             headerCellTpl = new Ext.Template(
359                 '<td class="x-grid3-hd x-grid3-cell x-grid3-td-{id} {css}" style="{style}">',
360                     '<div {tooltip} {attr} class="x-grid3-hd-inner x-grid3-hd-{id}" unselectable="on" style="{istyle}">', 
361                         this.grid.enableHdMenu ? '<a class="x-grid3-hd-btn" href="#"></a>' : '',
362                         '{value}',
363                         '<img alt="" class="x-grid3-sort-icon" src="', Ext.BLANK_IMAGE_URL, '" />',
364                     '</div>',
365                 '</td>'
366             ),
367         
368             rowBodyText = [
369                 '<tr class="x-grid3-row-body-tr" style="{bodyStyle}">',
370                     '<td colspan="{cols}" class="x-grid3-body-cell" tabIndex="0" hidefocus="on">',
371                         '<div class="x-grid3-row-body">{body}</div>',
372                     '</td>',
373                 '</tr>'
374             ].join(""),
375         
376             innerText = [
377                 '<table class="x-grid3-row-table" border="0" cellspacing="0" cellpadding="0" style="{tstyle}">',
378                      '<tbody>',
379                         '<tr>{cells}</tr>',
380                         this.enableRowBody ? rowBodyText : '',
381                      '</tbody>',
382                 '</table>'
383             ].join("");
384         
385         Ext.applyIf(templates, {
386             hcell   : headerCellTpl,
387             cell    : this.cellTpl,
388             body    : this.bodyTpl,
389             header  : this.headerTpl,
390             master  : this.masterTpl,
391             row     : new Ext.Template('<div class="x-grid3-row {alt}" style="{tstyle}">' + innerText + '</div>'),
392             rowInner: new Ext.Template(innerText)
393         });
394
395         for (name in templates) {
396             template = templates[name];
397             
398             if (template && Ext.isFunction(template.compile) && !template.compiled) {
399                 template.disableFormats = true;
400                 template.compile();
401             }
402         }
403
404         this.templates = templates;
405         this.colRe = new RegExp('x-grid3-td-([^\\s]+)', '');
406     },
407
408     /**
409      * @private
410      * Each GridView has its own private flyweight, accessed through this method
411      */
412     fly : function(el) {
413         if (!this._flyweight) {
414             this._flyweight = new Ext.Element.Flyweight(document.body);
415         }
416         this._flyweight.dom = el;
417         return this._flyweight;
418     },
419
420     // private
421     getEditorParent : function() {
422         return this.scroller.dom;
423     },
424
425     /**
426      * @private
427      * Finds and stores references to important elements
428      */
429     initElements : function() {
430         var Element  = Ext.Element,
431             el       = Ext.get(this.grid.getGridEl().dom.firstChild),
432             mainWrap = new Element(el.child('div.x-grid3-viewport')),
433             mainHd   = new Element(mainWrap.child('div.x-grid3-header')),
434             scroller = new Element(mainWrap.child('div.x-grid3-scroller'));
435         
436         if (this.grid.hideHeaders) {
437             mainHd.setDisplayed(false);
438         }
439         
440         if (this.forceFit) {
441             scroller.setStyle('overflow-x', 'hidden');
442         }
443         
444         /**
445          * <i>Read-only</i>. The GridView's body Element which encapsulates all rows in the Grid.
446          * This {@link Ext.Element Element} is only available after the GridPanel has been rendered.
447          * @type Ext.Element
448          * @property mainBody
449          */
450         
451         Ext.apply(this, {
452             el      : el,
453             mainWrap: mainWrap,
454             scroller: scroller,
455             mainHd  : mainHd,
456             innerHd : mainHd.child('div.x-grid3-header-inner').dom,
457             mainBody: new Element(Element.fly(scroller).child('div.x-grid3-body')),
458             focusEl : new Element(Element.fly(scroller).child('a')),
459             
460             resizeMarker: new Element(el.child('div.x-grid3-resize-marker')),
461             resizeProxy : new Element(el.child('div.x-grid3-resize-proxy'))
462         });
463         
464         this.focusEl.swallowEvent('click', true);
465     },
466
467     // private
468     getRows : function() {
469         return this.hasRows() ? this.mainBody.dom.childNodes : [];
470     },
471
472     // finder methods, used with delegation
473
474     // private
475     findCell : function(el) {
476         if (!el) {
477             return false;
478         }
479         return this.fly(el).findParent(this.cellSelector, this.cellSelectorDepth);
480     },
481
482     /**
483      * <p>Return the index of the grid column which contains the passed HTMLElement.</p>
484      * See also {@link #findRowIndex}
485      * @param {HTMLElement} el The target element
486      * @return {Number} The column index, or <b>false</b> if the target element is not within a row of this GridView.
487      */
488     findCellIndex : function(el, requiredCls) {
489         var cell = this.findCell(el),
490             hasCls;
491         
492         if (cell) {
493             hasCls = this.fly(cell).hasClass(requiredCls);
494             if (!requiredCls || hasCls) {
495                 return this.getCellIndex(cell);
496             }
497         }
498         return false;
499     },
500
501     // private
502     getCellIndex : function(el) {
503         if (el) {
504             var match = el.className.match(this.colRe);
505             
506             if (match && match[1]) {
507                 return this.cm.getIndexById(match[1]);
508             }
509         }
510         return false;
511     },
512
513     // private
514     findHeaderCell : function(el) {
515         var cell = this.findCell(el);
516         return cell && this.fly(cell).hasClass(this.hdCls) ? cell : null;
517     },
518
519     // private
520     findHeaderIndex : function(el){
521         return this.findCellIndex(el, this.hdCls);
522     },
523
524     /**
525      * Return the HtmlElement representing the grid row which contains the passed element.
526      * @param {HTMLElement} el The target HTMLElement
527      * @return {HTMLElement} The row element, or null if the target element is not within a row of this GridView.
528      */
529     findRow : function(el) {
530         if (!el) {
531             return false;
532         }
533         return this.fly(el).findParent(this.rowSelector, this.rowSelectorDepth);
534     },
535
536     /**
537      * Return the index of the grid row which contains the passed HTMLElement.
538      * See also {@link #findCellIndex}
539      * @param {HTMLElement} el The target HTMLElement
540      * @return {Number} The row index, or <b>false</b> if the target element is not within a row of this GridView.
541      */
542     findRowIndex : function(el) {
543         var row = this.findRow(el);
544         return row ? row.rowIndex : false;
545     },
546
547     /**
548      * Return the HtmlElement representing the grid row body which contains the passed element.
549      * @param {HTMLElement} el The target HTMLElement
550      * @return {HTMLElement} The row body element, or null if the target element is not within a row body of this GridView.
551      */
552     findRowBody : function(el) {
553         if (!el) {
554             return false;
555         }
556         
557         return this.fly(el).findParent(this.rowBodySelector, this.rowBodySelectorDepth);
558     },
559
560     // getter methods for fetching elements dynamically in the grid
561
562     /**
563      * Return the <tt>&lt;div></tt> HtmlElement which represents a Grid row for the specified index.
564      * @param {Number} index The row index
565      * @return {HtmlElement} The div element.
566      */
567     getRow : function(row) {
568         return this.getRows()[row];
569     },
570
571     /**
572      * Returns the grid's <tt>&lt;td></tt> HtmlElement at the specified coordinates.
573      * @param {Number} row The row index in which to find the cell.
574      * @param {Number} col The column index of the cell.
575      * @return {HtmlElement} The td at the specified coordinates.
576      */
577     getCell : function(row, col) {
578         return Ext.fly(this.getRow(row)).query(this.cellSelector)[col]; 
579     },
580
581     /**
582      * Return the <tt>&lt;td></tt> HtmlElement which represents the Grid's header cell for the specified column index.
583      * @param {Number} index The column index
584      * @return {HtmlElement} The td element.
585      */
586     getHeaderCell : function(index) {
587         return this.mainHd.dom.getElementsByTagName('td')[index];
588     },
589
590     // manipulating elements
591
592     // private - use getRowClass to apply custom row classes
593     addRowClass : function(rowId, cls) {
594         var row = this.getRow(rowId);
595         if (row) {
596             this.fly(row).addClass(cls);
597         }
598     },
599
600     // private
601     removeRowClass : function(row, cls) {
602         var r = this.getRow(row);
603         if(r){
604             this.fly(r).removeClass(cls);
605         }
606     },
607
608     // private
609     removeRow : function(row) {
610         Ext.removeNode(this.getRow(row));
611         this.syncFocusEl(row);
612     },
613
614     // private
615     removeRows : function(firstRow, lastRow) {
616         var bd = this.mainBody.dom,
617             rowIndex;
618             
619         for (rowIndex = firstRow; rowIndex <= lastRow; rowIndex++){
620             Ext.removeNode(bd.childNodes[firstRow]);
621         }
622         
623         this.syncFocusEl(firstRow);
624     },
625
626     /* ----------------------------------- Scrolling functions -------------------------------------------*/
627     
628     // private
629     getScrollState : function() {
630         var sb = this.scroller.dom;
631         
632         return {
633             left: sb.scrollLeft, 
634             top : sb.scrollTop
635         };
636     },
637
638     // private
639     restoreScroll : function(state) {
640         var sb = this.scroller.dom;
641         sb.scrollLeft = state.left;
642         sb.scrollTop  = state.top;
643     },
644
645     /**
646      * Scrolls the grid to the top
647      */
648     scrollToTop : function() {
649         var dom = this.scroller.dom;
650         
651         dom.scrollTop  = 0;
652         dom.scrollLeft = 0;
653     },
654
655     // private
656     syncScroll : function() {
657         this.syncHeaderScroll();
658         var mb = this.scroller.dom;
659         this.grid.fireEvent('bodyscroll', mb.scrollLeft, mb.scrollTop);
660     },
661
662     // private
663     syncHeaderScroll : function() {
664         var innerHd    = this.innerHd,
665             scrollLeft = this.scroller.dom.scrollLeft;
666         
667         innerHd.scrollLeft = scrollLeft;
668         innerHd.scrollLeft = scrollLeft; // second time for IE (1/2 time first fails, other browsers ignore)
669     },
670     
671     /**
672      * @private
673      * Ensures the given column has the given icon class
674      */
675     updateSortIcon : function(col, dir) {
676         var sortClasses = this.sortClasses,
677             sortClass   = sortClasses[dir == "DESC" ? 1 : 0],
678             headers     = this.mainHd.select('td').removeClass(sortClasses);
679         
680         headers.item(col).addClass(sortClass);
681     },
682
683     /**
684      * @private
685      * Updates the size of every column and cell in the grid
686      */
687     updateAllColumnWidths : function() {
688         var totalWidth = this.getTotalWidth(),
689             colCount   = this.cm.getColumnCount(),
690             rows       = this.getRows(),
691             rowCount   = rows.length,
692             widths     = [],
693             row, rowFirstChild, trow, i, j;
694         
695         for (i = 0; i < colCount; i++) {
696             widths[i] = this.getColumnWidth(i);
697             this.getHeaderCell(i).style.width = widths[i];
698         }
699         
700         this.updateHeaderWidth();
701         
702         for (i = 0; i < rowCount; i++) {
703             row = rows[i];
704             row.style.width = totalWidth;
705             rowFirstChild = row.firstChild;
706             
707             if (rowFirstChild) {
708                 rowFirstChild.style.width = totalWidth;
709                 trow = rowFirstChild.rows[0];
710                 
711                 for (j = 0; j < colCount; j++) {
712                     trow.childNodes[j].style.width = widths[j];
713                 }
714             }
715         }
716         
717         this.onAllColumnWidthsUpdated(widths, totalWidth);
718     },
719
720     /**
721      * @private
722      * Called after a column's width has been updated, this resizes all of the cells for that column in each row
723      * @param {Number} column The column index
724      */
725     updateColumnWidth : function(column, width) {
726         var columnWidth = this.getColumnWidth(column),
727             totalWidth  = this.getTotalWidth(),
728             headerCell  = this.getHeaderCell(column),
729             nodes       = this.getRows(),
730             nodeCount   = nodes.length,
731             row, i, firstChild;
732         
733         this.updateHeaderWidth();
734         headerCell.style.width = columnWidth;
735         
736         for (i = 0; i < nodeCount; i++) {
737             row = nodes[i];
738             firstChild = row.firstChild;
739             
740             row.style.width = totalWidth;
741             if (firstChild) {
742                 firstChild.style.width = totalWidth;
743                 firstChild.rows[0].childNodes[column].style.width = columnWidth;
744             }
745         }
746         
747         this.onColumnWidthUpdated(column, columnWidth, totalWidth);
748     },
749     
750     /**
751      * @private
752      * Sets the hidden status of a given column.
753      * @param {Number} col The column index
754      * @param {Boolean} hidden True to make the column hidden
755      */
756     updateColumnHidden : function(col, hidden) {
757         var totalWidth = this.getTotalWidth(),
758             display    = hidden ? 'none' : '',
759             headerCell = this.getHeaderCell(col),
760             nodes      = this.getRows(),
761             nodeCount  = nodes.length,
762             row, rowFirstChild, i;
763         
764         this.updateHeaderWidth();
765         headerCell.style.display = display;
766         
767         for (i = 0; i < nodeCount; i++) {
768             row = nodes[i];
769             row.style.width = totalWidth;
770             rowFirstChild = row.firstChild;
771             
772             if (rowFirstChild) {
773                 rowFirstChild.style.width = totalWidth;
774                 rowFirstChild.rows[0].childNodes[col].style.display = display;
775             }
776         }
777         
778         this.onColumnHiddenUpdated(col, hidden, totalWidth);
779         delete this.lastViewWidth; //recalc
780         this.layout();
781     },
782
783     /**
784      * @private
785      * Renders all of the rows to a string buffer and returns the string. This is called internally
786      * by renderRows and performs the actual string building for the rows - it does not inject HTML into the DOM.
787      * @param {Array} columns The column data acquired from getColumnData.
788      * @param {Array} records The array of records to render
789      * @param {Ext.data.Store} store The store to render the rows from
790      * @param {Number} startRow The index of the first row being rendered. Sometimes we only render a subset of
791      * the rows so this is used to maintain logic for striping etc
792      * @param {Number} colCount The total number of columns in the column model
793      * @param {Boolean} stripe True to stripe the rows
794      * @return {String} A string containing the HTML for the rendered rows
795      */
796     doRender : function(columns, records, store, startRow, colCount, stripe) {
797         var templates = this.templates,
798             cellTemplate = templates.cell,
799             rowTemplate = templates.row,
800             last = colCount - 1,
801             tstyle = 'width:' + this.getTotalWidth() + ';',
802             // buffers
803             rowBuffer = [],
804             colBuffer = [],
805             rowParams = {tstyle: tstyle},
806             meta = {},
807             len  = records.length,
808             alt,
809             column,
810             record, i, j, rowIndex;
811
812         //build up each row's HTML
813         for (j = 0; j < len; j++) {
814             record    = records[j];
815             colBuffer = [];
816
817             rowIndex = j + startRow;
818
819             //build up each column's HTML
820             for (i = 0; i < colCount; i++) {
821                 column = columns[i];
822                 
823                 meta.id    = column.id;
824                 meta.css   = i === 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : '');
825                 meta.attr  = meta.cellAttr = '';
826                 meta.style = column.style;
827                 meta.value = column.renderer.call(column.scope, record.data[column.name], meta, record, rowIndex, i, store);
828
829                 if (Ext.isEmpty(meta.value)) {
830                     meta.value = '&#160;';
831                 }
832
833                 if (this.markDirty && record.dirty && typeof record.modified[column.name] != 'undefined') {
834                     meta.css += ' x-grid3-dirty-cell';
835                 }
836
837                 colBuffer[colBuffer.length] = cellTemplate.apply(meta);
838             }
839
840             alt = [];
841             //set up row striping and row dirtiness CSS classes
842             if (stripe && ((rowIndex + 1) % 2 === 0)) {
843                 alt[0] = 'x-grid3-row-alt';
844             }
845
846             if (record.dirty) {
847                 alt[1] = ' x-grid3-dirty-row';
848             }
849
850             rowParams.cols = colCount;
851
852             if (this.getRowClass) {
853                 alt[2] = this.getRowClass(record, rowIndex, rowParams, store);
854             }
855
856             rowParams.alt   = alt.join(' ');
857             rowParams.cells = colBuffer.join('');
858
859             rowBuffer[rowBuffer.length] = rowTemplate.apply(rowParams);
860         }
861
862         return rowBuffer.join('');
863     },
864
865     /**
866      * @private
867      * Adds CSS classes and rowIndex to each row
868      * @param {Number} startRow The row to start from (defaults to 0)
869      */
870     processRows : function(startRow, skipStripe) {
871         if (!this.ds || this.ds.getCount() < 1) {
872             return;
873         }
874
875         var rows   = this.getRows(),
876             length = rows.length,
877             row, i;
878
879         skipStripe = skipStripe || !this.grid.stripeRows;
880         startRow   = startRow   || 0;
881
882         for (i = 0; i < length; i++) {
883             row = rows[i];
884             if (row) {
885                 row.rowIndex = i;
886                 if (!skipStripe) {
887                     row.className = row.className.replace(this.rowClsRe, ' ');
888                     if ((i + 1) % 2 === 0){
889                         row.className += ' x-grid3-row-alt';
890                     }
891                 }
892             }
893         }
894
895         // add first/last-row classes
896         if (startRow === 0) {
897             Ext.fly(rows[0]).addClass(this.firstRowCls);
898         }
899
900         Ext.fly(rows[length - 1]).addClass(this.lastRowCls);
901     },
902     
903     /**
904      * @private
905      */
906     afterRender : function() {
907         if (!this.ds || !this.cm) {
908             return;
909         }
910         
911         this.mainBody.dom.innerHTML = this.renderBody() || '&#160;';
912         this.processRows(0, true);
913
914         if (this.deferEmptyText !== true) {
915             this.applyEmptyText();
916         }
917         
918         this.grid.fireEvent('viewready', this.grid);
919     },
920     
921     /**
922      * @private
923      * This is always intended to be called after renderUI. Sets up listeners on the UI elements
924      * and sets up options like column menus, moving and resizing.
925      */
926     afterRenderUI: function() {
927         var grid = this.grid;
928         
929         this.initElements();
930
931         // get mousedowns early
932         Ext.fly(this.innerHd).on('click', this.handleHdDown, this);
933
934         this.mainHd.on({
935             scope    : this,
936             mouseover: this.handleHdOver,
937             mouseout : this.handleHdOut,
938             mousemove: this.handleHdMove
939         });
940
941         this.scroller.on('scroll', this.syncScroll,  this);
942         
943         if (grid.enableColumnResize !== false) {
944             this.splitZone = new Ext.grid.GridView.SplitDragZone(grid, this.mainHd.dom);
945         }
946
947         if (grid.enableColumnMove) {
948             this.columnDrag = new Ext.grid.GridView.ColumnDragZone(grid, this.innerHd);
949             this.columnDrop = new Ext.grid.HeaderDropZone(grid, this.mainHd.dom);
950         }
951
952         if (grid.enableHdMenu !== false) {
953             this.hmenu = new Ext.menu.Menu({id: grid.id + '-hctx'});
954             this.hmenu.add(
955                 {itemId:'asc',  text: this.sortAscText,  cls: 'xg-hmenu-sort-asc'},
956                 {itemId:'desc', text: this.sortDescText, cls: 'xg-hmenu-sort-desc'}
957             );
958
959             if (grid.enableColumnHide !== false) {
960                 this.colMenu = new Ext.menu.Menu({id:grid.id + '-hcols-menu'});
961                 this.colMenu.on({
962                     scope     : this,
963                     beforeshow: this.beforeColMenuShow,
964                     itemclick : this.handleHdMenuClick
965                 });
966                 this.hmenu.add('-', {
967                     itemId:'columns',
968                     hideOnClick: false,
969                     text: this.columnsText,
970                     menu: this.colMenu,
971                     iconCls: 'x-cols-icon'
972                 });
973             }
974
975             this.hmenu.on('itemclick', this.handleHdMenuClick, this);
976         }
977
978         if (grid.trackMouseOver) {
979             this.mainBody.on({
980                 scope    : this,
981                 mouseover: this.onRowOver,
982                 mouseout : this.onRowOut
983             });
984         }
985
986         if (grid.enableDragDrop || grid.enableDrag) {
987             this.dragZone = new Ext.grid.GridDragZone(grid, {
988                 ddGroup : grid.ddGroup || 'GridDD'
989             });
990         }
991
992         this.updateHeaderSortState();
993     },
994
995     /**
996      * @private
997      * Renders each of the UI elements in turn. This is called internally, once, by this.render. It does not
998      * render rows from the store, just the surrounding UI elements.
999      */
1000     renderUI : function() {
1001         var templates = this.templates;
1002
1003         return templates.master.apply({
1004             body  : templates.body.apply({rows:'&#160;'}),
1005             header: this.renderHeaders(),
1006             ostyle: 'width:' + this.getOffsetWidth() + ';',
1007             bstyle: 'width:' + this.getTotalWidth()  + ';'
1008         });
1009     },
1010
1011     // private
1012     processEvent : function(name, e) {
1013         var target = e.getTarget(),
1014             grid   = this.grid,
1015             header = this.findHeaderIndex(target),
1016             row, cell, col, body;
1017
1018         grid.fireEvent(name, e);
1019
1020         if (header !== false) {
1021             grid.fireEvent('header' + name, grid, header, e);
1022         } else {
1023             row = this.findRowIndex(target);
1024
1025 //          Grid's value-added events must bubble correctly to allow cancelling via returning false: cell->column->row
1026 //          We must allow a return of false at any of these levels to cancel the event processing.
1027 //          Particularly allowing rowmousedown to be cancellable by prior handlers which need to prevent selection.
1028             if (row !== false) {
1029                 cell = this.findCellIndex(target);
1030                 if (cell !== false) {
1031                     col = grid.colModel.getColumnAt(cell);
1032                     if (grid.fireEvent('cell' + name, grid, row, cell, e) !== false) {
1033                         if (!col || (col.processEvent && (col.processEvent(name, e, grid, row, cell) !== false))) {
1034                             grid.fireEvent('row' + name, grid, row, e);
1035                         }
1036                     }
1037                 } else {
1038                     if (grid.fireEvent('row' + name, grid, row, e) !== false) {
1039                         (body = this.findRowBody(target)) && grid.fireEvent('rowbody' + name, grid, row, e);
1040                     }
1041                 }
1042             } else {
1043                 grid.fireEvent('container' + name, grid, e);
1044             }
1045         }
1046     },
1047
1048     /**
1049      * @private
1050      * Sizes the grid's header and body elements
1051      */
1052     layout : function(initial) {
1053         if (!this.mainBody) {
1054             return; // not rendered
1055         }
1056
1057         var grid       = this.grid,
1058             gridEl     = grid.getGridEl(),
1059             gridSize   = gridEl.getSize(true),
1060             gridWidth  = gridSize.width,
1061             gridHeight = gridSize.height,
1062             scroller   = this.scroller,
1063             scrollStyle, headerHeight, scrollHeight;
1064         
1065         if (gridWidth < 20 || gridHeight < 20) {
1066             return;
1067         }
1068         
1069         if (grid.autoHeight) {
1070             scrollStyle = scroller.dom.style;
1071             scrollStyle.overflow = 'visible';
1072             
1073             if (Ext.isWebKit) {
1074                 scrollStyle.position = 'static';
1075             }
1076         } else {
1077             this.el.setSize(gridWidth, gridHeight);
1078             
1079             headerHeight = this.mainHd.getHeight();
1080             scrollHeight = gridHeight - headerHeight;
1081             
1082             scroller.setSize(gridWidth, scrollHeight);
1083             
1084             if (this.innerHd) {
1085                 this.innerHd.style.width = (gridWidth) + "px";
1086             }
1087         }
1088         
1089         if (this.forceFit || (initial === true && this.autoFill)) {
1090             if (this.lastViewWidth != gridWidth) {
1091                 this.fitColumns(false, false);
1092                 this.lastViewWidth = gridWidth;
1093             }
1094         } else {
1095             this.autoExpand();
1096             this.syncHeaderScroll();
1097         }
1098         
1099         this.onLayout(gridWidth, scrollHeight);
1100     },
1101
1102     // template functions for subclasses and plugins
1103     // these functions include precalculated values
1104     onLayout : function(vw, vh) {
1105         // do nothing
1106     },
1107
1108     onColumnWidthUpdated : function(col, w, tw) {
1109         //template method
1110     },
1111
1112     onAllColumnWidthsUpdated : function(ws, tw) {
1113         //template method
1114     },
1115
1116     onColumnHiddenUpdated : function(col, hidden, tw) {
1117         // template method
1118     },
1119
1120     updateColumnText : function(col, text) {
1121         // template method
1122     },
1123
1124     afterMove : function(colIndex) {
1125         // template method
1126     },
1127
1128     /* ----------------------------------- Core Specific -------------------------------------------*/
1129     // private
1130     init : function(grid) {
1131         this.grid = grid;
1132
1133         this.initTemplates();
1134         this.initData(grid.store, grid.colModel);
1135         this.initUI(grid);
1136     },
1137
1138     // private
1139     getColumnId : function(index){
1140         return this.cm.getColumnId(index);
1141     },
1142
1143     // private
1144     getOffsetWidth : function() {
1145         return (this.cm.getTotalWidth() + this.getScrollOffset()) + 'px';
1146     },
1147
1148     // private
1149     getScrollOffset: function() {
1150         return Ext.num(this.scrollOffset, Ext.getScrollBarWidth());
1151     },
1152
1153     /**
1154      * @private
1155      * Renders the header row using the 'header' template. Does not inject the HTML into the DOM, just
1156      * returns a string.
1157      * @return {String} Rendered header row
1158      */
1159     renderHeaders : function() {
1160         var colModel   = this.cm,
1161             templates  = this.templates,
1162             headerTpl  = templates.hcell,
1163             properties = {},
1164             colCount   = colModel.getColumnCount(),
1165             last       = colCount - 1,
1166             cells      = [],
1167             i, cssCls;
1168         
1169         for (i = 0; i < colCount; i++) {
1170             if (i == 0) {
1171                 cssCls = 'x-grid3-cell-first ';
1172             } else {
1173                 cssCls = i == last ? 'x-grid3-cell-last ' : '';
1174             }
1175             
1176             properties = {
1177                 id     : colModel.getColumnId(i),
1178                 value  : colModel.getColumnHeader(i) || '',
1179                 style  : this.getColumnStyle(i, true),
1180                 css    : cssCls,
1181                 tooltip: this.getColumnTooltip(i)
1182             };
1183             
1184             if (colModel.config[i].align == 'right') {
1185                 properties.istyle = 'padding-right: 16px;';
1186             } else {
1187                 delete properties.istyle;
1188             }
1189             
1190             cells[i] = headerTpl.apply(properties);
1191         }
1192         
1193         return templates.header.apply({
1194             cells : cells.join(""),
1195             tstyle: String.format("width: {0};", this.getTotalWidth())
1196         });
1197     },
1198
1199     /**
1200      * @private
1201      */
1202     getColumnTooltip : function(i) {
1203         var tooltip = this.cm.getColumnTooltip(i);
1204         if (tooltip) {
1205             if (Ext.QuickTips.isEnabled()) {
1206                 return 'ext:qtip="' + tooltip + '"';
1207             } else {
1208                 return 'title="' + tooltip + '"';
1209             }
1210         }
1211         
1212         return '';
1213     },
1214
1215     // private
1216     beforeUpdate : function() {
1217         this.grid.stopEditing(true);
1218     },
1219
1220     /**
1221      * @private
1222      * Re-renders the headers and ensures they are sized correctly
1223      */
1224     updateHeaders : function() {
1225         this.innerHd.firstChild.innerHTML = this.renderHeaders();
1226         
1227         this.updateHeaderWidth(false);
1228     },
1229     
1230     /**
1231      * @private
1232      * Ensures that the header is sized to the total width available to it
1233      * @param {Boolean} updateMain True to update the mainBody's width also (defaults to true)
1234      */
1235     updateHeaderWidth: function(updateMain) {
1236         var innerHdChild = this.innerHd.firstChild,
1237             totalWidth   = this.getTotalWidth();
1238         
1239         innerHdChild.style.width = this.getOffsetWidth();
1240         innerHdChild.firstChild.style.width = totalWidth;
1241         
1242         if (updateMain !== false) {
1243             this.mainBody.dom.style.width = totalWidth;
1244         }
1245     },
1246
1247     /**
1248      * Focuses the specified row.
1249      * @param {Number} row The row index
1250      */
1251     focusRow : function(row) {
1252         this.focusCell(row, 0, false);
1253     },
1254
1255     /**
1256      * Focuses the specified cell.
1257      * @param {Number} row The row index
1258      * @param {Number} col The column index
1259      */
1260     focusCell : function(row, col, hscroll) {
1261         this.syncFocusEl(this.ensureVisible(row, col, hscroll));
1262         
1263         var focusEl = this.focusEl;
1264         
1265         if (Ext.isGecko) {
1266             focusEl.focus();
1267         } else {
1268             focusEl.focus.defer(1, focusEl);
1269         }
1270     },
1271
1272     /**
1273      * @private
1274      * Finds the Elements corresponding to the given row and column indexes
1275      */
1276     resolveCell : function(row, col, hscroll) {
1277         if (!Ext.isNumber(row)) {
1278             row = row.rowIndex;
1279         }
1280         
1281         if (!this.ds) {
1282             return null;
1283         }
1284         
1285         if (row < 0 || row >= this.ds.getCount()) {
1286             return null;
1287         }
1288         col = (col !== undefined ? col : 0);
1289
1290         var rowEl    = this.getRow(row),
1291             colModel = this.cm,
1292             colCount = colModel.getColumnCount(),
1293             cellEl;
1294             
1295         if (!(hscroll === false && col === 0)) {
1296             while (col < colCount && colModel.isHidden(col)) {
1297                 col++;
1298             }
1299             
1300             cellEl = this.getCell(row, col);
1301         }
1302
1303         return {row: rowEl, cell: cellEl};
1304     },
1305
1306     /**
1307      * @private
1308      * Returns the XY co-ordinates of a given row/cell resolution (see {@link #resolveCell})
1309      * @return {Array} X and Y coords
1310      */
1311     getResolvedXY : function(resolved) {
1312         if (!resolved) {
1313             return null;
1314         }
1315         
1316         var cell = resolved.cell,
1317             row  = resolved.row;
1318         
1319         if (cell) {
1320             return Ext.fly(cell).getXY();
1321         } else {
1322             return [this.el.getX(), Ext.fly(row).getY()];
1323         }
1324     },
1325
1326     /**
1327      * @private
1328      * Moves the focus element to the x and y co-ordinates of the given row and column
1329      */
1330     syncFocusEl : function(row, col, hscroll) {
1331         var xy = row;
1332         
1333         if (!Ext.isArray(xy)) {
1334             row = Math.min(row, Math.max(0, this.getRows().length-1));
1335             
1336             if (isNaN(row)) {
1337                 return;
1338             }
1339             
1340             xy = this.getResolvedXY(this.resolveCell(row, col, hscroll));
1341         }
1342         
1343         this.focusEl.setXY(xy || this.scroller.getXY());
1344     },
1345
1346     /**
1347      * @private
1348      */
1349     ensureVisible : function(row, col, hscroll) {
1350         var resolved = this.resolveCell(row, col, hscroll);
1351         
1352         if (!resolved || !resolved.row) {
1353             return null;
1354         }
1355
1356         var rowEl  = resolved.row,
1357             cellEl = resolved.cell,
1358             c = this.scroller.dom,
1359             p = rowEl,
1360             ctop = 0,
1361             stop = this.el.dom;
1362
1363         while (p && p != stop) {
1364             ctop += p.offsetTop;
1365             p = p.offsetParent;
1366         }
1367
1368         ctop -= this.mainHd.dom.offsetHeight;
1369         stop = parseInt(c.scrollTop, 10);
1370
1371         var cbot = ctop + rowEl.offsetHeight,
1372             ch = c.clientHeight,
1373             sbot = stop + ch;
1374
1375
1376         if (ctop < stop) {
1377           c.scrollTop = ctop;
1378         } else if(cbot > sbot) {
1379             c.scrollTop = cbot-ch;
1380         }
1381
1382         if (hscroll !== false) {
1383             var cleft  = parseInt(cellEl.offsetLeft, 10),
1384                 cright = cleft + cellEl.offsetWidth,
1385                 sleft  = parseInt(c.scrollLeft, 10),
1386                 sright = sleft + c.clientWidth;
1387                 
1388             if (cleft < sleft) {
1389                 c.scrollLeft = cleft;
1390             } else if(cright > sright) {
1391                 c.scrollLeft = cright-c.clientWidth;
1392             }
1393         }
1394         
1395         return this.getResolvedXY(resolved);
1396     },
1397
1398     // private
1399     insertRows : function(dm, firstRow, lastRow, isUpdate) {
1400         var last = dm.getCount() - 1;
1401         if( !isUpdate && firstRow === 0 && lastRow >= last) {
1402             this.fireEvent('beforerowsinserted', this, firstRow, lastRow);
1403                 this.refresh();
1404             this.fireEvent('rowsinserted', this, firstRow, lastRow);
1405         } else {
1406             if (!isUpdate) {
1407                 this.fireEvent('beforerowsinserted', this, firstRow, lastRow);
1408             }
1409             var html = this.renderRows(firstRow, lastRow),
1410                 before = this.getRow(firstRow);
1411             if (before) {
1412                 if(firstRow === 0){
1413                     Ext.fly(this.getRow(0)).removeClass(this.firstRowCls);
1414                 }
1415                 Ext.DomHelper.insertHtml('beforeBegin', before, html);
1416             } else {
1417                 var r = this.getRow(last - 1);
1418                 if(r){
1419                     Ext.fly(r).removeClass(this.lastRowCls);
1420                 }
1421                 Ext.DomHelper.insertHtml('beforeEnd', this.mainBody.dom, html);
1422             }
1423             if (!isUpdate) {
1424                 this.processRows(firstRow);
1425                 this.fireEvent('rowsinserted', this, firstRow, lastRow);
1426             } else if (firstRow === 0 || firstRow >= last) {
1427                 //ensure first/last row is kept after an update.
1428                 Ext.fly(this.getRow(firstRow)).addClass(firstRow === 0 ? this.firstRowCls : this.lastRowCls);
1429             }
1430         }
1431         this.syncFocusEl(firstRow);
1432     },
1433
1434     /**
1435      * @private
1436      * DEPRECATED - this doesn't appear to be called anywhere in the library, remove in 4.0. 
1437      */
1438     deleteRows : function(dm, firstRow, lastRow) {
1439         if (dm.getRowCount() < 1) {
1440             this.refresh();
1441         } else {
1442             this.fireEvent('beforerowsdeleted', this, firstRow, lastRow);
1443
1444             this.removeRows(firstRow, lastRow);
1445
1446             this.processRows(firstRow);
1447             this.fireEvent('rowsdeleted', this, firstRow, lastRow);
1448         }
1449     },
1450
1451     /**
1452      * @private
1453      * Builds a CSS string for the given column index
1454      * @param {Number} colIndex The column index
1455      * @param {Boolean} isHeader True if getting the style for the column's header
1456      * @return {String} The CSS string
1457      */
1458     getColumnStyle : function(colIndex, isHeader) {
1459         var colModel  = this.cm,
1460             colConfig = colModel.config,
1461             style     = isHeader ? '' : colConfig[colIndex].css || '',
1462             align     = colConfig[colIndex].align;
1463         
1464         style += String.format("width: {0};", this.getColumnWidth(colIndex));
1465         
1466         if (colModel.isHidden(colIndex)) {
1467             style += 'display: none; ';
1468         }
1469         
1470         if (align) {
1471             style += String.format("text-align: {0};", align);
1472         }
1473         
1474         return style;
1475     },
1476
1477     /**
1478      * @private
1479      * Returns the width of a given column minus its border width
1480      * @return {Number} The column index
1481      * @return {String|Number} The width in pixels
1482      */
1483     getColumnWidth : function(column) {
1484         var columnWidth = this.cm.getColumnWidth(column),
1485             borderWidth = this.borderWidth;
1486         
1487         if (Ext.isNumber(columnWidth)) {
1488             if (Ext.isBorderBox || (Ext.isWebKit && !Ext.isSafari2)) {
1489                 return columnWidth + "px";
1490             } else {
1491                 return Math.max(columnWidth - borderWidth, 0) + "px";
1492             }
1493         } else {
1494             return columnWidth;
1495         }
1496     },
1497
1498     /**
1499      * @private
1500      * Returns the total width of all visible columns
1501      * @return {String} 
1502      */
1503     getTotalWidth : function() {
1504         return this.cm.getTotalWidth() + 'px';
1505     },
1506
1507     /**
1508      * @private
1509      * Resizes each column to fit the available grid width.
1510      * TODO: The second argument isn't even used, remove it in 4.0
1511      * @param {Boolean} preventRefresh True to prevent resizing of each row to the new column sizes (defaults to false)
1512      * @param {null} onlyExpand NOT USED, will be removed in 4.0
1513      * @param {Number} omitColumn The index of a column to leave at its current width. Defaults to undefined
1514      * @return {Boolean} True if the operation succeeded, false if not or undefined if the grid view is not yet initialized
1515      */
1516     fitColumns : function(preventRefresh, onlyExpand, omitColumn) {
1517         var grid          = this.grid,
1518             colModel      = this.cm,
1519             totalColWidth = colModel.getTotalWidth(false),
1520             gridWidth     = this.getGridInnerWidth(),
1521             extraWidth    = gridWidth - totalColWidth,
1522             columns       = [],
1523             extraCol      = 0,
1524             width         = 0,
1525             colWidth, fraction, i;
1526         
1527         // not initialized, so don't screw up the default widths
1528         if (gridWidth < 20 || extraWidth === 0) {
1529             return false;
1530         }
1531         
1532         var visibleColCount = colModel.getColumnCount(true),
1533             totalColCount   = colModel.getColumnCount(false),
1534             adjCount        = visibleColCount - (Ext.isNumber(omitColumn) ? 1 : 0);
1535         
1536         if (adjCount === 0) {
1537             adjCount = 1;
1538             omitColumn = undefined;
1539         }
1540         
1541         //FIXME: the algorithm used here is odd and potentially confusing. Includes this for loop and the while after it.
1542         for (i = 0; i < totalColCount; i++) {
1543             if (!colModel.isFixed(i) && i !== omitColumn) {
1544                 colWidth = colModel.getColumnWidth(i);
1545                 columns.push(i, colWidth);
1546                 
1547                 if (!colModel.isHidden(i)) {
1548                     extraCol = i;
1549                     width += colWidth;
1550                 }
1551             }
1552         }
1553         
1554         fraction = (gridWidth - colModel.getTotalWidth()) / width;
1555         
1556         while (columns.length) {
1557             colWidth = columns.pop();
1558             i        = columns.pop();
1559             
1560             colModel.setColumnWidth(i, Math.max(grid.minColumnWidth, Math.floor(colWidth + colWidth * fraction)), true);
1561         }
1562         
1563         //this has been changed above so remeasure now
1564         totalColWidth = colModel.getTotalWidth(false);
1565         
1566         if (totalColWidth > gridWidth) {
1567             var adjustCol = (adjCount == visibleColCount) ? extraCol : omitColumn,
1568                 newWidth  = Math.max(1, colModel.getColumnWidth(adjustCol) - (totalColWidth - gridWidth));
1569             
1570             colModel.setColumnWidth(adjustCol, newWidth, true);
1571         }
1572         
1573         if (preventRefresh !== true) {
1574             this.updateAllColumnWidths();
1575         }
1576         
1577         return true;
1578     },
1579
1580     /**
1581      * @private
1582      * Resizes the configured autoExpandColumn to take the available width after the other columns have 
1583      * been accounted for
1584      * @param {Boolean} preventUpdate True to prevent the resizing of all rows (defaults to false)
1585      */
1586     autoExpand : function(preventUpdate) {
1587         var grid             = this.grid,
1588             colModel         = this.cm,
1589             gridWidth        = this.getGridInnerWidth(),
1590             totalColumnWidth = colModel.getTotalWidth(false),
1591             autoExpandColumn = grid.autoExpandColumn;
1592         
1593         if (!this.userResized && autoExpandColumn) {
1594             if (gridWidth != totalColumnWidth) {
1595                 //if we are not already using all available width, resize the autoExpandColumn
1596                 var colIndex     = colModel.getIndexById(autoExpandColumn),
1597                     currentWidth = colModel.getColumnWidth(colIndex),
1598                     desiredWidth = gridWidth - totalColumnWidth + currentWidth,
1599                     newWidth     = Math.min(Math.max(desiredWidth, grid.autoExpandMin), grid.autoExpandMax);
1600                 
1601                 if (currentWidth != newWidth) {
1602                     colModel.setColumnWidth(colIndex, newWidth, true);
1603                     
1604                     if (preventUpdate !== true) {
1605                         this.updateColumnWidth(colIndex, newWidth);
1606                     }
1607                 }
1608             }
1609         }
1610     },
1611     
1612     /**
1613      * Returns the total internal width available to the grid, taking the scrollbar into account
1614      * @return {Number} The total width
1615      */
1616     getGridInnerWidth: function() {
1617         return this.grid.getGridEl().getWidth(true) - this.getScrollOffset();
1618     },
1619
1620     /**
1621      * @private
1622      * Returns an array of column configurations - one for each column
1623      * @return {Array} Array of column config objects. This includes the column name, renderer, id style and renderer
1624      */
1625     getColumnData : function() {
1626         var columns  = [],
1627             colModel = this.cm,
1628             colCount = colModel.getColumnCount(),
1629             fields   = this.ds.fields,
1630             i, name;
1631         
1632         for (i = 0; i < colCount; i++) {
1633             name = colModel.getDataIndex(i);
1634             
1635             columns[i] = {
1636                 name    : Ext.isDefined(name) ? name : (fields.get(i) ? fields.get(i).name : undefined),
1637                 renderer: colModel.getRenderer(i),
1638                 scope   : colModel.getRendererScope(i),
1639                 id      : colModel.getColumnId(i),
1640                 style   : this.getColumnStyle(i)
1641             };
1642         }
1643         
1644         return columns;
1645     },
1646
1647     /**
1648      * @private
1649      * Renders rows between start and end indexes
1650      * @param {Number} startRow Index of the first row to render
1651      * @param {Number} endRow Index of the last row to render
1652      */
1653     renderRows : function(startRow, endRow) {
1654         var grid     = this.grid,
1655             store    = grid.store,
1656             stripe   = grid.stripeRows,
1657             colModel = grid.colModel,
1658             colCount = colModel.getColumnCount(),
1659             rowCount = store.getCount(),
1660             records;
1661         
1662         if (rowCount < 1) {
1663             return '';
1664         }
1665         
1666         startRow = startRow || 0;
1667         endRow   = Ext.isDefined(endRow) ? endRow : rowCount - 1;
1668         records  = store.getRange(startRow, endRow);
1669         
1670         return this.doRender(this.getColumnData(), records, store, startRow, colCount, stripe);
1671     },
1672
1673     // private
1674     renderBody : function(){
1675         var markup = this.renderRows() || '&#160;';
1676         return this.templates.body.apply({rows: markup});
1677     },
1678
1679     /**
1680      * @private
1681      * Refreshes a row by re-rendering it. Fires the rowupdated event when done
1682      */
1683     refreshRow: function(record) {
1684         var store     = this.ds,
1685             colCount  = this.cm.getColumnCount(),
1686             columns   = this.getColumnData(),
1687             last      = colCount - 1,
1688             cls       = ['x-grid3-row'],
1689             rowParams = {
1690                 tstyle: String.format("width: {0};", this.getTotalWidth())
1691             },
1692             colBuffer = [],
1693             cellTpl   = this.templates.cell,
1694             rowIndex, row, column, meta, css, i;
1695         
1696         if (Ext.isNumber(record)) {
1697             rowIndex = record;
1698             record   = store.getAt(rowIndex);
1699         } else {
1700             rowIndex = store.indexOf(record);
1701         }
1702         
1703         //the record could not be found
1704         if (!record || rowIndex < 0) {
1705             return;
1706         }
1707         
1708         //builds each column in this row
1709         for (i = 0; i < colCount; i++) {
1710             column = columns[i];
1711             
1712             if (i == 0) {
1713                 css = 'x-grid3-cell-first';
1714             } else {
1715                 css = (i == last) ? 'x-grid3-cell-last ' : '';
1716             }
1717             
1718             meta = {
1719                 id      : column.id,
1720                 style   : column.style,
1721                 css     : css,
1722                 attr    : "",
1723                 cellAttr: ""
1724             };
1725             // Need to set this after, because we pass meta to the renderer
1726             meta.value = column.renderer.call(column.scope, record.data[column.name], meta, record, rowIndex, i, store);
1727             
1728             if (Ext.isEmpty(meta.value)) {
1729                 meta.value = '&#160;';
1730             }
1731             
1732             if (this.markDirty && record.dirty && typeof record.modified[column.name] != 'undefined') {
1733                 meta.css += ' x-grid3-dirty-cell';
1734             }
1735             
1736             colBuffer[i] = cellTpl.apply(meta);
1737         }
1738         
1739         row = this.getRow(rowIndex);
1740         row.className = '';
1741         
1742         if (this.grid.stripeRows && ((rowIndex + 1) % 2 === 0)) {
1743             cls.push('x-grid3-row-alt');
1744         }
1745         
1746         if (this.getRowClass) {
1747             rowParams.cols = colCount;
1748             cls.push(this.getRowClass(record, rowIndex, rowParams, store));
1749         }
1750         
1751         this.fly(row).addClass(cls).setStyle(rowParams.tstyle);
1752         rowParams.cells = colBuffer.join("");
1753         row.innerHTML = this.templates.rowInner.apply(rowParams);
1754         
1755         this.fireEvent('rowupdated', this, rowIndex, record);
1756     },
1757
1758     /**
1759      * Refreshs the grid UI
1760      * @param {Boolean} headersToo (optional) True to also refresh the headers
1761      */
1762     refresh : function(headersToo) {
1763         this.fireEvent('beforerefresh', this);
1764         this.grid.stopEditing(true);
1765
1766         var result = this.renderBody();
1767         this.mainBody.update(result).setWidth(this.getTotalWidth());
1768         if (headersToo === true) {
1769             this.updateHeaders();
1770             this.updateHeaderSortState();
1771         }
1772         this.processRows(0, true);
1773         this.layout();
1774         this.applyEmptyText();
1775         this.fireEvent('refresh', this);
1776     },
1777
1778     /**
1779      * @private
1780      * Displays the configured emptyText if there are currently no rows to display
1781      */
1782     applyEmptyText : function() {
1783         if (this.emptyText && !this.hasRows()) {
1784             this.mainBody.update('<div class="x-grid-empty">' + this.emptyText + '</div>');
1785         }
1786     },
1787
1788     /**
1789      * @private
1790      * Adds sorting classes to the column headers based on the bound store's sortInfo. Fires the 'sortchange' event
1791      * if the sorting has changed since this function was last run.
1792      */
1793     updateHeaderSortState : function() {
1794         var state = this.ds.getSortState();
1795         if (!state) {
1796             return;
1797         }
1798
1799         if (!this.sortState || (this.sortState.field != state.field || this.sortState.direction != state.direction)) {
1800             this.grid.fireEvent('sortchange', this.grid, state);
1801         }
1802
1803         this.sortState = state;
1804
1805         var sortColumn = this.cm.findColumnIndex(state.field);
1806         if (sortColumn != -1) {
1807             var sortDir = state.direction;
1808             this.updateSortIcon(sortColumn, sortDir);
1809         }
1810     },
1811
1812     /**
1813      * @private
1814      * Removes any sorting indicator classes from the column headers
1815      */
1816     clearHeaderSortState : function() {
1817         if (!this.sortState) {
1818             return;
1819         }
1820         this.grid.fireEvent('sortchange', this.grid, null);
1821         this.mainHd.select('td').removeClass(this.sortClasses);
1822         delete this.sortState;
1823     },
1824
1825     /**
1826      * @private
1827      * Destroys all objects associated with the GridView
1828      */
1829     destroy : function() {
1830         var me              = this,
1831             grid            = me.grid,
1832             gridEl          = grid.getGridEl(),
1833             dragZone        = me.dragZone,
1834             splitZone       = me.splitZone,
1835             columnDrag      = me.columnDrag,
1836             columnDrop      = me.columnDrop,
1837             scrollToTopTask = me.scrollToTopTask,
1838             columnDragData,
1839             columnDragProxy;
1840         
1841         if (scrollToTopTask && scrollToTopTask.cancel) {
1842             scrollToTopTask.cancel();
1843         }
1844         
1845         Ext.destroyMembers(me, 'colMenu', 'hmenu');
1846
1847         me.initData(null, null);
1848         me.purgeListeners();
1849         
1850         Ext.fly(me.innerHd).un("click", me.handleHdDown, me);
1851
1852         if (grid.enableColumnMove) {
1853             columnDragData = columnDrag.dragData;
1854             columnDragProxy = columnDrag.proxy;
1855             Ext.destroy(
1856                 columnDrag.el,
1857                 columnDragProxy.ghost,
1858                 columnDragProxy.el,
1859                 columnDrop.el,
1860                 columnDrop.proxyTop,
1861                 columnDrop.proxyBottom,
1862                 columnDragData.ddel,
1863                 columnDragData.header
1864             );
1865             
1866             if (columnDragProxy.anim) {
1867                 Ext.destroy(columnDragProxy.anim);
1868             }
1869             
1870             delete columnDragProxy.ghost;
1871             delete columnDragData.ddel;
1872             delete columnDragData.header;
1873             columnDrag.destroy();
1874             
1875             delete Ext.dd.DDM.locationCache[columnDrag.id];
1876             delete columnDrag._domRef;
1877
1878             delete columnDrop.proxyTop;
1879             delete columnDrop.proxyBottom;
1880             columnDrop.destroy();
1881             delete Ext.dd.DDM.locationCache["gridHeader" + gridEl.id];
1882             delete columnDrop._domRef;
1883             delete Ext.dd.DDM.ids[columnDrop.ddGroup];
1884         }
1885
1886         if (splitZone) { // enableColumnResize
1887             splitZone.destroy();
1888             delete splitZone._domRef;
1889             delete Ext.dd.DDM.ids["gridSplitters" + gridEl.id];
1890         }
1891
1892         Ext.fly(me.innerHd).removeAllListeners();
1893         Ext.removeNode(me.innerHd);
1894         delete me.innerHd;
1895
1896         Ext.destroy(
1897             me.el,
1898             me.mainWrap,
1899             me.mainHd,
1900             me.scroller,
1901             me.mainBody,
1902             me.focusEl,
1903             me.resizeMarker,
1904             me.resizeProxy,
1905             me.activeHdBtn,
1906             me._flyweight,
1907             dragZone,
1908             splitZone
1909         );
1910
1911         delete grid.container;
1912
1913         if (dragZone) {
1914             dragZone.destroy();
1915         }
1916
1917         Ext.dd.DDM.currentTarget = null;
1918         delete Ext.dd.DDM.locationCache[gridEl.id];
1919
1920         Ext.EventManager.removeResizeListener(me.onWindowResize, me);
1921     },
1922
1923     // private
1924     onDenyColumnHide : function() {
1925
1926     },
1927
1928     // private
1929     render : function() {
1930         if (this.autoFill) {
1931             var ct = this.grid.ownerCt;
1932             
1933             if (ct && ct.getLayout()) {
1934                 ct.on('afterlayout', function() {
1935                     this.fitColumns(true, true);
1936                     this.updateHeaders();
1937                     this.updateHeaderSortState();
1938                 }, this, {single: true});
1939             }
1940         } else if (this.forceFit) {
1941             this.fitColumns(true, false);
1942         } else if (this.grid.autoExpandColumn) {
1943             this.autoExpand(true);
1944         }
1945         
1946         this.grid.getGridEl().dom.innerHTML = this.renderUI();
1947         
1948         this.afterRenderUI();
1949     },
1950
1951     /* --------------------------------- Model Events and Handlers --------------------------------*/
1952     
1953     /**
1954      * @private
1955      * Binds a new Store and ColumnModel to this GridView. Removes any listeners from the old objects (if present)
1956      * and adds listeners to the new ones
1957      * @param {Ext.data.Store} newStore The new Store instance
1958      * @param {Ext.grid.ColumnModel} newColModel The new ColumnModel instance
1959      */
1960     initData : function(newStore, newColModel) {
1961         var me = this;
1962         
1963         if (me.ds) {
1964             var oldStore = me.ds;
1965             
1966             oldStore.un('add', me.onAdd, me);
1967             oldStore.un('load', me.onLoad, me);
1968             oldStore.un('clear', me.onClear, me);
1969             oldStore.un('remove', me.onRemove, me);
1970             oldStore.un('update', me.onUpdate, me);
1971             oldStore.un('datachanged', me.onDataChange, me);
1972             
1973             if (oldStore !== newStore && oldStore.autoDestroy) {
1974                 oldStore.destroy();
1975             }
1976         }
1977         
1978         if (newStore) {
1979             newStore.on({
1980                 scope      : me,
1981                 load       : me.onLoad,
1982                 add        : me.onAdd,
1983                 remove     : me.onRemove,
1984                 update     : me.onUpdate,
1985                 clear      : me.onClear,
1986                 datachanged: me.onDataChange
1987             });
1988         }
1989         
1990         if (me.cm) {
1991             var oldColModel = me.cm;
1992             
1993             oldColModel.un('configchange', me.onColConfigChange, me);
1994             oldColModel.un('widthchange',  me.onColWidthChange, me);
1995             oldColModel.un('headerchange', me.onHeaderChange, me);
1996             oldColModel.un('hiddenchange', me.onHiddenChange, me);
1997             oldColModel.un('columnmoved',  me.onColumnMove, me);
1998         }
1999         
2000         if (newColModel) {
2001             delete me.lastViewWidth;
2002             
2003             newColModel.on({
2004                 scope       : me,
2005                 configchange: me.onColConfigChange,
2006                 widthchange : me.onColWidthChange,
2007                 headerchange: me.onHeaderChange,
2008                 hiddenchange: me.onHiddenChange,
2009                 columnmoved : me.onColumnMove
2010             });
2011         }
2012         
2013         me.ds = newStore;
2014         me.cm = newColModel;
2015     },
2016
2017     // private
2018     onDataChange : function(){
2019         this.refresh(true);
2020         this.updateHeaderSortState();
2021         this.syncFocusEl(0);
2022     },
2023
2024     // private
2025     onClear : function() {
2026         this.refresh();
2027         this.syncFocusEl(0);
2028     },
2029
2030     // private
2031     onUpdate : function(store, record) {
2032         this.refreshRow(record);
2033     },
2034
2035     // private
2036     onAdd : function(store, records, index) {
2037         this.insertRows(store, index, index + (records.length-1));
2038     },
2039
2040     // private
2041     onRemove : function(store, record, index, isUpdate) {
2042         if (isUpdate !== true) {
2043             this.fireEvent('beforerowremoved', this, index, record);
2044         }
2045         
2046         this.removeRow(index);
2047         
2048         if (isUpdate !== true) {
2049             this.processRows(index);
2050             this.applyEmptyText();
2051             this.fireEvent('rowremoved', this, index, record);
2052         }
2053     },
2054
2055     /**
2056      * @private
2057      * Called when a store is loaded, scrolls to the top row
2058      */
2059     onLoad : function() {
2060         if (Ext.isGecko) {
2061             if (!this.scrollToTopTask) {
2062                 this.scrollToTopTask = new Ext.util.DelayedTask(this.scrollToTop, this);
2063             }
2064             this.scrollToTopTask.delay(1);
2065         } else {
2066             this.scrollToTop();
2067         }
2068     },
2069
2070     // private
2071     onColWidthChange : function(cm, col, width) {
2072         this.updateColumnWidth(col, width);
2073     },
2074
2075     // private
2076     onHeaderChange : function(cm, col, text) {
2077         this.updateHeaders();
2078     },
2079
2080     // private
2081     onHiddenChange : function(cm, col, hidden) {
2082         this.updateColumnHidden(col, hidden);
2083     },
2084
2085     // private
2086     onColumnMove : function(cm, oldIndex, newIndex) {
2087         this.indexMap = null;
2088         this.refresh(true);
2089         this.restoreScroll(this.getScrollState());
2090         
2091         this.afterMove(newIndex);
2092         this.grid.fireEvent('columnmove', oldIndex, newIndex);
2093     },
2094
2095     // private
2096     onColConfigChange : function() {
2097         delete this.lastViewWidth;
2098         this.indexMap = null;
2099         this.refresh(true);
2100     },
2101
2102     /* -------------------- UI Events and Handlers ------------------------------ */
2103     // private
2104     initUI : function(grid) {
2105         grid.on('headerclick', this.onHeaderClick, this);
2106     },
2107
2108     // private
2109     initEvents : Ext.emptyFn,
2110
2111     // private
2112     onHeaderClick : function(g, index) {
2113         if (this.headersDisabled || !this.cm.isSortable(index)) {
2114             return;
2115         }
2116         g.stopEditing(true);
2117         g.store.sort(this.cm.getDataIndex(index));
2118     },
2119
2120     /**
2121      * @private
2122      * Adds the hover class to a row when hovered over
2123      */
2124     onRowOver : function(e, target) {
2125         var row = this.findRowIndex(target);
2126         
2127         if (row !== false) {
2128             this.addRowClass(row, this.rowOverCls);
2129         }
2130     },
2131
2132     /**
2133      * @private
2134      * Removes the hover class from a row on mouseout
2135      */
2136     onRowOut : function(e, target) {
2137         var row = this.findRowIndex(target);
2138         
2139         if (row !== false && !e.within(this.getRow(row), true)) {
2140             this.removeRowClass(row, this.rowOverCls);
2141         }
2142     },
2143
2144     // private
2145     onRowSelect : function(row) {
2146         this.addRowClass(row, this.selectedRowClass);
2147     },
2148
2149     // private
2150     onRowDeselect : function(row) {
2151         this.removeRowClass(row, this.selectedRowClass);
2152     },
2153
2154     // private
2155     onCellSelect : function(row, col) {
2156         var cell = this.getCell(row, col);
2157         if (cell) {
2158             this.fly(cell).addClass('x-grid3-cell-selected');
2159         }
2160     },
2161
2162     // private
2163     onCellDeselect : function(row, col) {
2164         var cell = this.getCell(row, col);
2165         if (cell) {
2166             this.fly(cell).removeClass('x-grid3-cell-selected');
2167         }
2168     },
2169
2170     // private
2171     handleWheel : function(e) {
2172         e.stopPropagation();
2173     },
2174
2175     /**
2176      * @private
2177      * Called by the SplitDragZone when a drag has been completed. Resizes the columns
2178      */
2179     onColumnSplitterMoved : function(cellIndex, width) {
2180         this.userResized = true;
2181         this.grid.colModel.setColumnWidth(cellIndex, width, true);
2182
2183         if (this.forceFit) {
2184             this.fitColumns(true, false, cellIndex);
2185             this.updateAllColumnWidths();
2186         } else {
2187             this.updateColumnWidth(cellIndex, width);
2188             this.syncHeaderScroll();
2189         }
2190
2191         this.grid.fireEvent('columnresize', cellIndex, width);
2192     },
2193
2194     /**
2195      * @private
2196      * Click handler for the shared column dropdown menu, called on beforeshow. Builds the menu
2197      * which displays the list of columns for the user to show or hide.
2198      */
2199     beforeColMenuShow : function() {
2200         var colModel = this.cm,
2201             colCount = colModel.getColumnCount(),
2202             colMenu  = this.colMenu,
2203             i;
2204
2205         colMenu.removeAll();
2206
2207         for (i = 0; i < colCount; i++) {
2208             if (colModel.config[i].hideable !== false) {
2209                 colMenu.add(new Ext.menu.CheckItem({
2210                     text       : colModel.getColumnHeader(i),
2211                     itemId     : 'col-' + colModel.getColumnId(i),
2212                     checked    : !colModel.isHidden(i),
2213                     disabled   : colModel.config[i].hideable === false,
2214                     hideOnClick: false
2215                 }));
2216             }
2217         }
2218     },
2219     
2220     /**
2221      * @private
2222      * Attached as the 'itemclick' handler to the header menu and the column show/hide submenu (if available).
2223      * Performs sorting if the sorter buttons were clicked, otherwise hides/shows the column that was clicked.
2224      */
2225     handleHdMenuClick : function(item) {
2226         var store     = this.ds,
2227             dataIndex = this.cm.getDataIndex(this.hdCtxIndex);
2228
2229         switch (item.getItemId()) {
2230             case 'asc':
2231                 store.sort(dataIndex, 'ASC');
2232                 break;
2233             case 'desc':
2234                 store.sort(dataIndex, 'DESC');
2235                 break;
2236             default:
2237                 this.handleHdMenuClickDefault(item);
2238         }
2239         return true;
2240     },
2241     
2242     /**
2243      * Called by handleHdMenuClick if any button except a sort ASC/DESC button was clicked. The default implementation provides
2244      * the column hide/show functionality based on the check state of the menu item. A different implementation can be provided
2245      * if needed.
2246      * @param {Ext.menu.BaseItem} item The menu item that was clicked
2247      */
2248     handleHdMenuClickDefault: function(item) {
2249         var colModel = this.cm,
2250             itemId   = item.getItemId(),
2251             index    = colModel.getIndexById(itemId.substr(4));
2252
2253         if (index != -1) {
2254             if (item.checked && colModel.getColumnsBy(this.isHideableColumn, this).length <= 1) {
2255                 this.onDenyColumnHide();
2256                 return;
2257             }
2258             colModel.setHidden(index, item.checked);
2259         }
2260     },
2261
2262     /**
2263      * @private
2264      * Called when a header cell is clicked - shows the menu if the click happened over a trigger button
2265      */
2266     handleHdDown : function(e, target) {
2267         if (Ext.fly(target).hasClass('x-grid3-hd-btn')) {
2268             e.stopEvent();
2269             
2270             var colModel  = this.cm,
2271                 header    = this.findHeaderCell(target),
2272                 index     = this.getCellIndex(header),
2273                 sortable  = colModel.isSortable(index),
2274                 menu      = this.hmenu,
2275                 menuItems = menu.items,
2276                 menuCls   = this.headerMenuOpenCls;
2277             
2278             this.hdCtxIndex = index;
2279             
2280             Ext.fly(header).addClass(menuCls);
2281             menuItems.get('asc').setDisabled(!sortable);
2282             menuItems.get('desc').setDisabled(!sortable);
2283             
2284             menu.on('hide', function() {
2285                 Ext.fly(header).removeClass(menuCls);
2286             }, this, {single:true});
2287             
2288             menu.show(target, 'tl-bl?');
2289         }
2290     },
2291
2292     /**
2293      * @private
2294      * Attached to the headers' mousemove event. This figures out the CSS cursor to use based on where the mouse is currently
2295      * pointed. If the mouse is currently hovered over the extreme left or extreme right of any header cell and the cell next 
2296      * to it is resizable it is given the resize cursor, otherwise the cursor is set to an empty string.
2297      */
2298     handleHdMove : function(e) {
2299         var header = this.findHeaderCell(this.activeHdRef);
2300         
2301         if (header && !this.headersDisabled) {
2302             var handleWidth  = this.splitHandleWidth || 5,
2303                 activeRegion = this.activeHdRegion,
2304                 headerStyle  = header.style,
2305                 colModel     = this.cm,
2306                 cursor       = '',
2307                 pageX        = e.getPageX();
2308                 
2309             if (this.grid.enableColumnResize !== false) {
2310                 var activeHeaderIndex = this.activeHdIndex,
2311                     previousVisible   = this.getPreviousVisible(activeHeaderIndex),
2312                     currentResizable  = colModel.isResizable(activeHeaderIndex),
2313                     previousResizable = previousVisible && colModel.isResizable(previousVisible),
2314                     inLeftResizer     = pageX - activeRegion.left <= handleWidth,
2315                     inRightResizer    = activeRegion.right - pageX <= (!this.activeHdBtn ? handleWidth : 2);
2316                 
2317                 if (inLeftResizer && previousResizable) {
2318                     cursor = Ext.isAir ? 'move' : Ext.isWebKit ? 'e-resize' : 'col-resize'; // col-resize not always supported
2319                 } else if (inRightResizer && currentResizable) {
2320                     cursor = Ext.isAir ? 'move' : Ext.isWebKit ? 'w-resize' : 'col-resize';
2321                 }
2322             }
2323             
2324             headerStyle.cursor = cursor;
2325         }
2326     },
2327     
2328     /**
2329      * @private
2330      * Returns the index of the nearest currently visible header to the left of the given index.
2331      * @param {Number} index The header index
2332      * @return {Number/undefined} The index of the nearest visible header
2333      */
2334     getPreviousVisible: function(index) {
2335         while (index > 0) {
2336             if (!this.cm.isHidden(index - 1)) {
2337                 return index;
2338             }
2339             index--;
2340         }
2341         return undefined;
2342     },
2343
2344     /**
2345      * @private
2346      * Tied to the header element's mouseover event - adds the over class to the header cell if the menu is not disabled
2347      * for that cell
2348      */
2349     handleHdOver : function(e, target) {
2350         var header = this.findHeaderCell(target);
2351         
2352         if (header && !this.headersDisabled) {
2353             var fly = this.fly(header);
2354             
2355             this.activeHdRef = target;
2356             this.activeHdIndex = this.getCellIndex(header);
2357             this.activeHdRegion = fly.getRegion();
2358             
2359             if (!this.isMenuDisabled(this.activeHdIndex, fly)) {
2360                 fly.addClass('x-grid3-hd-over');
2361                 this.activeHdBtn = fly.child('.x-grid3-hd-btn');
2362                 
2363                 if (this.activeHdBtn) {
2364                     this.activeHdBtn.dom.style.height = (header.firstChild.offsetHeight - 1) + 'px';
2365                 }
2366             }
2367         }
2368     },
2369
2370     /**
2371      * @private
2372      * Tied to the header element's mouseout event. Removes the hover class from the header cell
2373      */
2374     handleHdOut : function(e, target) {
2375         var header = this.findHeaderCell(target);
2376         
2377         if (header && (!Ext.isIE || !e.within(header, true))) {
2378             this.activeHdRef = null;
2379             this.fly(header).removeClass('x-grid3-hd-over');
2380             header.style.cursor = '';
2381         }
2382     },
2383     
2384     /**
2385      * @private
2386      * Used by {@link #handleHdOver} to determine whether or not to show the header menu class on cell hover
2387      * @param {Number} cellIndex The header cell index
2388      * @param {Ext.Element} el The cell element currently being hovered over
2389      */
2390     isMenuDisabled: function(cellIndex, el) {
2391         return this.cm.isMenuDisabled(cellIndex);
2392     },
2393
2394     /**
2395      * @private
2396      * Returns true if there are any rows rendered into the GridView
2397      * @return {Boolean} True if any rows have been rendered
2398      */
2399     hasRows : function() {
2400         var fc = this.mainBody.dom.firstChild;
2401         return fc && fc.nodeType == 1 && fc.className != 'x-grid-empty';
2402     },
2403     
2404     /**
2405      * @private
2406      */
2407     isHideableColumn : function(c) {
2408         return !c.hidden;
2409     },
2410
2411     /**
2412      * @private
2413      * DEPRECATED - will be removed in Ext JS 5.0
2414      */
2415     bind : function(d, c) {
2416         this.initData(d, c);
2417     }
2418 });
2419
2420
2421 // private
2422 // This is a support class used internally by the Grid components
2423 Ext.grid.GridView.SplitDragZone = Ext.extend(Ext.dd.DDProxy, {
2424
2425     constructor: function(grid, hd){
2426         this.grid = grid;
2427         this.view = grid.getView();
2428         this.marker = this.view.resizeMarker;
2429         this.proxy = this.view.resizeProxy;
2430         Ext.grid.GridView.SplitDragZone.superclass.constructor.call(this, hd,
2431             'gridSplitters' + this.grid.getGridEl().id, {
2432             dragElId : Ext.id(this.proxy.dom), resizeFrame:false
2433         });
2434         this.scroll = false;
2435         this.hw = this.view.splitHandleWidth || 5;
2436     },
2437
2438     b4StartDrag : function(x, y){
2439         this.dragHeadersDisabled = this.view.headersDisabled;
2440         this.view.headersDisabled = true;
2441         var h = this.view.mainWrap.getHeight();
2442         this.marker.setHeight(h);
2443         this.marker.show();
2444         this.marker.alignTo(this.view.getHeaderCell(this.cellIndex), 'tl-tl', [-2, 0]);
2445         this.proxy.setHeight(h);
2446         var w = this.cm.getColumnWidth(this.cellIndex),
2447             minw = Math.max(w-this.grid.minColumnWidth, 0);
2448         this.resetConstraints();
2449         this.setXConstraint(minw, 1000);
2450         this.setYConstraint(0, 0);
2451         this.minX = x - minw;
2452         this.maxX = x + 1000;
2453         this.startPos = x;
2454         Ext.dd.DDProxy.prototype.b4StartDrag.call(this, x, y);
2455     },
2456
2457     allowHeaderDrag : function(e){
2458         return true;
2459     },
2460
2461     handleMouseDown : function(e){
2462         var t = this.view.findHeaderCell(e.getTarget());
2463         if(t && this.allowHeaderDrag(e)){
2464             var xy = this.view.fly(t).getXY(), 
2465                 x = xy[0],
2466                 exy = e.getXY(), 
2467                 ex = exy[0],
2468                 w = t.offsetWidth, 
2469                 adjust = false;
2470                 
2471             if((ex - x) <= this.hw){
2472                 adjust = -1;
2473             }else if((x+w) - ex <= this.hw){
2474                 adjust = 0;
2475             }
2476             if(adjust !== false){
2477                 this.cm = this.grid.colModel;
2478                 var ci = this.view.getCellIndex(t);
2479                 if(adjust == -1){
2480                   if (ci + adjust < 0) {
2481                     return;
2482                   }
2483                     while(this.cm.isHidden(ci+adjust)){
2484                         --adjust;
2485                         if(ci+adjust < 0){
2486                             return;
2487                         }
2488                     }
2489                 }
2490                 this.cellIndex = ci+adjust;
2491                 this.split = t.dom;
2492                 if(this.cm.isResizable(this.cellIndex) && !this.cm.isFixed(this.cellIndex)){
2493                     Ext.grid.GridView.SplitDragZone.superclass.handleMouseDown.apply(this, arguments);
2494                 }
2495             }else if(this.view.columnDrag){
2496                 this.view.columnDrag.callHandleMouseDown(e);
2497             }
2498         }
2499     },
2500
2501     endDrag : function(e){
2502         this.marker.hide();
2503         var v = this.view,
2504             endX = Math.max(this.minX, e.getPageX()),
2505             diff = endX - this.startPos,
2506             disabled = this.dragHeadersDisabled;
2507             
2508         v.onColumnSplitterMoved(this.cellIndex, this.cm.getColumnWidth(this.cellIndex)+diff);
2509         setTimeout(function(){
2510             v.headersDisabled = disabled;
2511         }, 50);
2512     },
2513
2514     autoOffset : function(){
2515         this.setDelta(0,0);
2516     }
2517 });