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