Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / view / Table.js
1 /**
2  * @class Ext.view.Table
3  * @extends Ext.view.View
4
5 This class encapsulates the user interface for a tabular data set.
6 It acts as a centralized manager for controlling the various interface
7 elements of the view. This includes handling events, such as row and cell
8 level based DOM events. It also reacts to events from the underlying {@link Ext.selection.Model}
9 to provide visual feedback to the user. 
10
11 This class does not provide ways to manipulate the underlying data of the configured
12 {@link Ext.data.Store}.
13
14 This is the base class for both {@link Ext.grid.View} and {@link Ext.tree.View} and is not
15 to be used directly.
16
17  * @markdown
18  * @abstract
19  * @xtype tableview
20  * @author Nicolas Ferrero
21  */
22 Ext.define('Ext.view.Table', {
23     extend: 'Ext.view.View',
24     alias: 'widget.tableview',
25     uses: [
26         'Ext.view.TableChunker',
27         'Ext.util.DelayedTask',
28         'Ext.util.MixedCollection'
29     ],
30
31     cls: Ext.baseCSSPrefix + 'grid-view',
32
33     // row
34     itemSelector: '.' + Ext.baseCSSPrefix + 'grid-row',
35     // cell
36     cellSelector: '.' + Ext.baseCSSPrefix + 'grid-cell',
37
38     selectedItemCls: Ext.baseCSSPrefix + 'grid-row-selected',
39     selectedCellCls: Ext.baseCSSPrefix + 'grid-cell-selected',
40     focusedItemCls: Ext.baseCSSPrefix + 'grid-row-focused',
41     overItemCls: Ext.baseCSSPrefix + 'grid-row-over',
42     altRowCls:   Ext.baseCSSPrefix + 'grid-row-alt',
43     rowClsRe: /(?:^|\s*)grid-row-(first|last|alt)(?:\s+|$)/g,
44     cellRe: new RegExp('x-grid-cell-([^\\s]+) ', ''),
45
46     // cfg docs inherited
47     trackOver: true,
48
49     /**
50      * Override this function to apply custom CSS classes to rows during rendering.  You can also supply custom
51      * parameters to the row template for the current row to customize how it is rendered using the <b>rowParams</b>
52      * parameter.  This function should return the CSS class name (or empty string '' for none) that will be added
53      * to the row's wrapping div.  To apply multiple class names, simply return them space-delimited within the string
54      * (e.g., 'my-class another-class'). Example usage:
55     <pre><code>
56 viewConfig: {
57     forceFit: true,
58     showPreview: true, // custom property
59     enableRowBody: true, // required to create a second, full-width row to show expanded Record data
60     getRowClass: function(record, rowIndex, rp, ds){ // rp = rowParams
61         if(this.showPreview){
62             rp.body = '&lt;p>'+record.data.excerpt+'&lt;/p>';
63             return 'x-grid3-row-expanded';
64         }
65         return 'x-grid3-row-collapsed';
66     }
67 },
68     </code></pre>
69      * @param {Model} model The {@link Ext.data.Model} corresponding to the current row.
70      * @param {Number} index The row index.
71      * @param {Object} rowParams (DEPRECATED) A config object that is passed to the row template during rendering that allows
72      * customization of various aspects of a grid row.
73      * <p>If {@link #enableRowBody} is configured <b><tt></tt>true</b>, then the following properties may be set
74      * by this function, and will be used to render a full-width expansion row below each grid row:</p>
75      * <ul>
76      * <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>
77      * <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>
78      * </ul>
79      * The following property will be passed in, and may be appended to:
80      * <ul>
81      * <li><code>tstyle</code> : String <div class="sub-desc">A CSS style specification that willl be applied to the &lt;table> element which encapsulates
82      * both the standard grid row, and any expansion row.</div></li>
83      * </ul>
84      * @param {Store} store The {@link Ext.data.Store} this grid is bound to
85      * @method getRowClass
86      * @return {String} a CSS class name to add to the row.
87      */
88     getRowClass: null,
89
90     initComponent: function() {
91         var me = this;
92         
93         me.scrollState = {};
94         me.selModel.view = me;
95         me.headerCt.view = me;
96         me.initFeatures();
97         me.tpl = '<div></div>';
98         me.callParent();
99         me.mon(me.store, {
100             load: me.onStoreLoad,
101             scope: me
102         });
103
104         // this.addEvents(
105         //     /**
106         //      * @event rowfocus
107         //      * @param {Ext.data.Record} record
108         //      * @param {HTMLElement} row
109         //      * @param {Number} rowIdx
110         //      */
111         //     'rowfocus'
112         // );
113     },
114
115     // scroll to top of the grid when store loads
116     onStoreLoad: function(){
117         var me = this;
118         
119         if (me.invalidateScrollerOnRefresh) {
120             if (Ext.isGecko) {
121                 if (!me.scrollToTopTask) {
122                     me.scrollToTopTask = Ext.create('Ext.util.DelayedTask', me.scrollToTop, me);
123                 }
124                 me.scrollToTopTask.delay(1);
125             } else {
126                 me    .scrollToTop();
127             }
128         }
129     },
130
131     // scroll the view to the top
132     scrollToTop: Ext.emptyFn,
133     
134     /**
135      * Add a listener to the main view element. It will be destroyed with the view.
136      * @private
137      */
138     addElListener: function(eventName, fn, scope){
139         this.mon(this, eventName, fn, scope, {
140             element: 'el'
141         });
142     },
143     
144     /**
145      * Get the columns used for generating a template via TableChunker.
146      * See {@link Ext.grid.header.Container#getGridColumns}.
147      * @private
148      */
149     getGridColumns: function() {
150         return this.headerCt.getGridColumns();    
151     },
152     
153     /**
154      * Get a leaf level header by index regardless of what the nesting
155      * structure is.
156      * @private
157      * @param {Number} index The index
158      */
159     getHeaderAtIndex: function(index) {
160         return this.headerCt.getHeaderAtIndex(index);
161     },
162     
163     /**
164      * Get the cell (td) for a particular record and column.
165      * @param {Ext.data.Model} record
166      * @param {Ext.grid.column.Colunm} column
167      * @private
168      */
169     getCell: function(record, column) {
170         var row = this.getNode(record);
171         return Ext.fly(row).down(column.getCellSelector());
172     },
173
174     /**
175      * Get a reference to a feature
176      * @param {String} id The id of the feature
177      * @return {Ext.grid.feature.Feature} The feature. Undefined if not found
178      */
179     getFeature: function(id) {
180         var features = this.featuresMC;
181         if (features) {
182             return features.get(id);
183         }
184     },
185
186     /**
187      * Initializes each feature and bind it to this view.
188      * @private
189      */
190     initFeatures: function() {
191         var me = this,
192             i = 0,
193             features,
194             len;
195             
196         me.features = me.features || [];
197         features = me.features;
198         len = features.length;
199
200         me.featuresMC = Ext.create('Ext.util.MixedCollection');
201         for (; i < len; i++) {
202             // ensure feature hasnt already been instantiated
203             if (!features[i].isFeature) {
204                 features[i] = Ext.create('feature.' + features[i].ftype, features[i]);
205             }
206             // inject a reference to view
207             features[i].view = me;
208             me.featuresMC.add(features[i]);
209         }
210     },
211
212     /**
213      * Gives features an injection point to attach events to the markup that
214      * has been created for this view.
215      * @private
216      */
217     attachEventsForFeatures: function() {
218         var features = this.features,
219             ln       = features.length,
220             i        = 0;
221
222         for (; i < ln; i++) {
223             if (features[i].isFeature) {
224                 features[i].attachEvents();
225             }
226         }
227     },
228
229     afterRender: function() {
230         var me = this;
231         
232         me.callParent();
233         me.mon(me.el, {
234             scroll: me.fireBodyScroll,
235             scope: me
236         });
237         me.el.unselectable();
238         me.attachEventsForFeatures();
239     },
240
241     fireBodyScroll: function(e, t) {
242         this.fireEvent('bodyscroll', e, t);
243     },
244
245     // TODO: Refactor headerCt dependency here to colModel
246     /**
247      * Uses the headerCt to transform data from dataIndex keys in a record to
248      * headerId keys in each header and then run them through each feature to
249      * get additional data for variables they have injected into the view template.
250      * @private
251      */
252     prepareData: function(data, idx, record) {
253         var me       = this,
254             orig     = me.headerCt.prepareData(data, idx, record, me, me.ownerCt),
255             features = me.features,
256             ln       = features.length,
257             i        = 0,
258             node, feature;
259
260         for (; i < ln; i++) {
261             feature = features[i];
262             if (feature.isFeature) {
263                 Ext.apply(orig, feature.getAdditionalData(data, idx, record, orig, me));
264             }
265         }
266
267         return orig;
268     },
269
270     // TODO: Refactor headerCt dependency here to colModel
271     collectData: function(records, startIndex) {
272         var preppedRecords = this.callParent(arguments),
273             headerCt  = this.headerCt,
274             fullWidth = headerCt.getFullWidth(),
275             features  = this.features,
276             ln = features.length,
277             o = {
278                 rows: preppedRecords,
279                 fullWidth: fullWidth
280             },
281             i  = 0,
282             feature,
283             j = 0,
284             jln,
285             rowParams;
286
287         jln = preppedRecords.length;
288         // process row classes, rowParams has been deprecated and has been moved
289         // to the individual features that implement the behavior. 
290         if (this.getRowClass) {
291             for (; j < jln; j++) {
292                 rowParams = {};
293                 preppedRecords[j]['rowCls'] = this.getRowClass(records[j], j, rowParams, this.store);
294                 //<debug>
295                 if (rowParams.alt) {
296                     Ext.Error.raise("The getRowClass alt property is no longer supported.");
297                 }
298                 if (rowParams.tstyle) {
299                     Ext.Error.raise("The getRowClass tstyle property is no longer supported.");
300                 }
301                 if (rowParams.cells) {
302                     Ext.Error.raise("The getRowClass cells property is no longer supported.");
303                 }
304                 if (rowParams.body) {
305                     Ext.Error.raise("The getRowClass body property is no longer supported. Use the getAdditionalData method of the rowbody feature.");
306                 }
307                 if (rowParams.bodyStyle) {
308                     Ext.Error.raise("The getRowClass bodyStyle property is no longer supported.");
309                 }
310                 if (rowParams.cols) {
311                     Ext.Error.raise("The getRowClass cols property is no longer supported.");
312                 }
313                 //</debug>
314             }
315         }
316         // currently only one feature may implement collectData. This is to modify
317         // what's returned to the view before its rendered
318         for (; i < ln; i++) {
319             feature = features[i];
320             if (feature.isFeature && feature.collectData && !feature.disabled) {
321                 o = feature.collectData(records, preppedRecords, startIndex, fullWidth, o);
322                 break;
323             }
324         }
325         return o;
326     },
327
328     // TODO: Refactor header resizing to column resizing
329     /**
330      * When a header is resized, setWidth on the individual columns resizer class,
331      * the top level table, save/restore scroll state, generate a new template and
332      * restore focus to the grid view's element so that keyboard navigation
333      * continues to work.
334      * @private
335      */
336     onHeaderResize: function(header, w, suppressFocus) {
337         var me = this,
338             el = me.el;
339         if (el) {
340             me.saveScrollState();
341             // Grab the col and set the width, css
342             // class is generated in TableChunker.
343             // Select composites because there may be several chunks.
344             el.select('.' + Ext.baseCSSPrefix + 'grid-col-resizer-'+header.id).setWidth(w);
345             el.select('.' + Ext.baseCSSPrefix + 'grid-table-resizer').setWidth(me.headerCt.getFullWidth());
346             me.restoreScrollState();
347             me.setNewTemplate();
348             if (!suppressFocus) {
349                 me.el.focus();
350             }
351         }
352     },
353
354     /**
355      * When a header is shown restore its oldWidth if it was previously hidden.
356      * @private
357      */
358     onHeaderShow: function(headerCt, header, suppressFocus) {
359         // restore headers that were dynamically hidden
360         if (header.oldWidth) {
361             this.onHeaderResize(header, header.oldWidth, suppressFocus);
362             delete header.oldWidth;
363         // flexed headers will have a calculated size set
364         // this additional check has to do with the fact that
365         // defaults: {width: 100} will fight with a flex value
366         } else if (header.width && !header.flex) {
367             this.onHeaderResize(header, header.width, suppressFocus);
368         }
369         this.setNewTemplate();
370     },
371
372     /**
373      * When the header hides treat it as a resize to 0.
374      * @private
375      */
376     onHeaderHide: function(headerCt, header, suppressFocus) {
377         this.onHeaderResize(header, 0, suppressFocus);
378     },
379
380     /**
381      * Set a new template based on the current columns displayed in the
382      * grid.
383      * @private
384      */
385     setNewTemplate: function() {
386         var me = this,
387             columns = me.headerCt.getColumnsForTpl(true);
388             
389         me.tpl = me.getTableChunker().getTableTpl({
390             columns: columns,
391             features: me.features
392         });
393     },
394
395     /**
396      * Get the configured chunker or default of Ext.view.TableChunker
397      */
398     getTableChunker: function() {
399         return this.chunker || Ext.view.TableChunker;
400     },
401
402     /**
403      * Add a CSS Class to a specific row.
404      * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model representing this row
405      * @param {String} cls
406      */
407     addRowCls: function(rowInfo, cls) {
408         var row = this.getNode(rowInfo);
409         if (row) {
410             Ext.fly(row).addCls(cls);
411         }
412     },
413
414     /**
415      * Remove a CSS Class from a specific row.
416      * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model representing this row
417      * @param {String} cls
418      */
419     removeRowCls: function(rowInfo, cls) {
420         var row = this.getNode(rowInfo);
421         if (row) {
422             Ext.fly(row).removeCls(cls);
423         }
424     },
425
426     // GridSelectionModel invokes onRowSelect as selection changes
427     onRowSelect : function(rowIdx) {
428         this.addRowCls(rowIdx, this.selectedItemCls);
429     },
430
431     // GridSelectionModel invokes onRowDeselect as selection changes
432     onRowDeselect : function(rowIdx) {
433         var me = this;
434         
435         me.removeRowCls(rowIdx, me.selectedItemCls);
436         me.removeRowCls(rowIdx, me.focusedItemCls);
437     },
438     
439     onCellSelect: function(position) {
440         var cell = this.getCellByPosition(position);
441         if (cell) {
442             cell.addCls(this.selectedCellCls);
443         }
444     },
445     
446     onCellDeselect: function(position) {
447         var cell = this.getCellByPosition(position);
448         if (cell) {
449             cell.removeCls(this.selectedCellCls);
450         }
451         
452     },
453     
454     onCellFocus: function(position) {
455         //var cell = this.getCellByPosition(position);
456         this.focusCell(position);
457     },
458     
459     getCellByPosition: function(position) {
460         var row    = position.row,
461             column = position.column,
462             store  = this.store,
463             node   = this.getNode(row),
464             header = this.headerCt.getHeaderAtIndex(column),
465             cellSelector,
466             cell = false;
467             
468         if (header && node) {
469             cellSelector = header.getCellSelector();
470             cell = Ext.fly(node).down(cellSelector);
471         }
472         return cell;
473     },
474
475     // GridSelectionModel invokes onRowFocus to 'highlight'
476     // the last row focused
477     onRowFocus: function(rowIdx, highlight, supressFocus) {
478         var me = this,
479             row = me.getNode(rowIdx);
480
481         if (highlight) {
482             me.addRowCls(rowIdx, me.focusedItemCls);
483             if (!supressFocus) {
484                 me.focusRow(rowIdx);
485             }
486             //this.el.dom.setAttribute('aria-activedescendant', row.id);
487         } else {
488             me.removeRowCls(rowIdx, me.focusedItemCls);
489         }
490     },
491
492     /**
493      * Focus a particular row and bring it into view. Will fire the rowfocus event.
494      * @cfg {Mixed} An HTMLElement template node, index of a template node, the
495      * id of a template node or the record associated with the node.
496      */
497     focusRow: function(rowIdx) {
498         var me         = this,
499             row        = me.getNode(rowIdx),
500             el         = me.el,
501             adjustment = 0,
502             panel      = me.ownerCt,
503             rowRegion,
504             elRegion,
505             record;
506             
507         if (row && el) {
508             elRegion  = el.getRegion();
509             rowRegion = Ext.fly(row).getRegion();
510             // row is above
511             if (rowRegion.top < elRegion.top) {
512                 adjustment = rowRegion.top - elRegion.top;
513             // row is below
514             } else if (rowRegion.bottom > elRegion.bottom) {
515                 adjustment = rowRegion.bottom - elRegion.bottom;
516             }
517             record = me.getRecord(row);
518             rowIdx = me.store.indexOf(record);
519
520             if (adjustment) {
521                 // scroll the grid itself, so that all gridview's update.
522                 panel.scrollByDeltaY(adjustment);
523             }
524             me.fireEvent('rowfocus', record, row, rowIdx);
525         }
526     },
527
528     focusCell: function(position) {
529         var me          = this,
530             cell        = me.getCellByPosition(position),
531             el          = me.el,
532             adjustmentY = 0,
533             adjustmentX = 0,
534             elRegion    = el.getRegion(),
535             panel       = me.ownerCt,
536             cellRegion,
537             record;
538
539         if (cell) {
540             cellRegion = cell.getRegion();
541             // cell is above
542             if (cellRegion.top < elRegion.top) {
543                 adjustmentY = cellRegion.top - elRegion.top;
544             // cell is below
545             } else if (cellRegion.bottom > elRegion.bottom) {
546                 adjustmentY = cellRegion.bottom - elRegion.bottom;
547             }
548
549             // cell is left
550             if (cellRegion.left < elRegion.left) {
551                 adjustmentX = cellRegion.left - elRegion.left;
552             // cell is right
553             } else if (cellRegion.right > elRegion.right) {
554                 adjustmentX = cellRegion.right - elRegion.right;
555             }
556
557             if (adjustmentY) {
558                 // scroll the grid itself, so that all gridview's update.
559                 panel.scrollByDeltaY(adjustmentY);
560             }
561             if (adjustmentX) {
562                 panel.scrollByDeltaX(adjustmentX);
563             }
564             el.focus();
565             me.fireEvent('cellfocus', record, cell, position);
566         }
567     },
568
569     /**
570      * Scroll by delta. This affects this individual view ONLY and does not
571      * synchronize across views or scrollers.
572      * @param {Number} delta
573      * @param {String} dir (optional) Valid values are scrollTop and scrollLeft. Defaults to scrollTop.
574      * @private
575      */
576     scrollByDelta: function(delta, dir) {
577         dir = dir || 'scrollTop';
578         var elDom = this.el.dom;
579         elDom[dir] = (elDom[dir] += delta);
580     },
581
582     onUpdate: function(ds, index) {
583         this.callParent(arguments);
584     },
585
586     /**
587      * Save the scrollState in a private variable.
588      * Must be used in conjunction with restoreScrollState
589      */
590     saveScrollState: function() {
591         var dom = this.el.dom,
592             state = this.scrollState;
593
594         state.left = dom.scrollLeft;
595         state.top = dom.scrollTop;
596     },
597
598     /**
599      * Restore the scrollState.
600      * Must be used in conjunction with saveScrollState
601      * @private
602      */
603     restoreScrollState: function() {
604         var dom = this.el.dom,
605             state = this.scrollState,
606             headerEl = this.headerCt.el.dom;
607
608         headerEl.scrollLeft = dom.scrollLeft = state.left;
609         dom.scrollTop = state.top;
610     },
611
612     /**
613      * Refresh the grid view.
614      * Saves and restores the scroll state, generates a new template, stripes rows
615      * and invalidates the scrollers.
616      * @param {Boolean} firstPass This is a private flag for internal use only.
617      */
618     refresh: function(firstPass) {
619         var me = this,
620             table;
621
622         //this.saveScrollState();
623         me.setNewTemplate();
624         
625         me.callParent(arguments);
626
627         //this.restoreScrollState();
628
629         if (me.rendered && !firstPass) {
630             // give focus back to gridview
631             //me.el.focus();
632         }
633     },
634
635     processItemEvent: function(record, row, rowIndex, e) {
636         var me = this,
637             cell = e.getTarget(me.cellSelector, row),
638             cellIndex = cell ? cell.cellIndex : -1,
639             map = me.statics().EventMap,
640             selModel = me.getSelectionModel(),
641             type = e.type,
642             result;
643
644         if (type == 'keydown' && !cell && selModel.getCurrentPosition) {
645             // CellModel, otherwise we can't tell which cell to invoke
646             cell = me.getCellByPosition(selModel.getCurrentPosition());
647             if (cell) {
648                 cell = cell.dom;
649                 cellIndex = cell.cellIndex;
650             }
651         }
652
653         result = me.fireEvent('uievent', type, me, cell, rowIndex, cellIndex, e);
654
655         if (result === false || me.callParent(arguments) === false) {
656             return false;
657         }
658
659         // Don't handle cellmouseenter and cellmouseleave events for now
660         if (type == 'mouseover' || type == 'mouseout') {
661             return true;
662         }
663
664         return !(
665             // We are adding cell and feature events  
666             (me['onBeforeCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
667             (me.fireEvent('beforecell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false) ||
668             (me['onCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
669             (me.fireEvent('cell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false)
670         );
671     },
672
673     processSpecialEvent: function(e) {
674         var me = this,
675             map = me.statics().EventMap,
676             features = me.features,
677             ln = features.length,
678             type = e.type,
679             i, feature, prefix, featureTarget,
680             beforeArgs, args,
681             panel = me.ownerCt;
682
683         me.callParent(arguments);
684
685         if (type == 'mouseover' || type == 'mouseout') {
686             return;
687         }
688
689         for (i = 0; i < ln; i++) {
690             feature = features[i];
691             if (feature.hasFeatureEvent) {
692                 featureTarget = e.getTarget(feature.eventSelector, me.getTargetEl());
693                 if (featureTarget) {
694                     prefix = feature.eventPrefix;
695                     // allows features to implement getFireEventArgs to change the
696                     // fireEvent signature
697                     beforeArgs = feature.getFireEventArgs('before' + prefix + type, me, featureTarget, e);
698                     args = feature.getFireEventArgs(prefix + type, me, featureTarget, e);
699                     
700                     if (
701                         // before view event
702                         (me.fireEvent.apply(me, beforeArgs) === false) ||
703                         // panel grid event
704                         (panel.fireEvent.apply(panel, beforeArgs) === false) ||
705                         // view event
706                         (me.fireEvent.apply(me, args) === false) ||
707                         // panel event
708                         (panel.fireEvent.apply(panel, args) === false)
709                     ) {
710                         return false;
711                     }
712                 }
713             }
714         }
715         return true;
716     },
717
718     onCellMouseDown: Ext.emptyFn,
719     onCellMouseUp: Ext.emptyFn,
720     onCellClick: Ext.emptyFn,
721     onCellDblClick: Ext.emptyFn,
722     onCellContextMenu: Ext.emptyFn,
723     onCellKeyDown: Ext.emptyFn,
724     onBeforeCellMouseDown: Ext.emptyFn,
725     onBeforeCellMouseUp: Ext.emptyFn,
726     onBeforeCellClick: Ext.emptyFn,
727     onBeforeCellDblClick: Ext.emptyFn,
728     onBeforeCellContextMenu: Ext.emptyFn,
729     onBeforeCellKeyDown: Ext.emptyFn,
730
731     /**
732      * Expand a particular header to fit the max content width.
733      * This will ONLY expand, not contract.
734      * @private
735      */
736     expandToFit: function(header) {
737         var maxWidth = this.getMaxContentWidth(header);
738         delete header.flex;
739         header.setWidth(maxWidth);
740     },
741
742     /**
743      * Get the max contentWidth of the header's text and all cells
744      * in the grid under this header.
745      * @private
746      */
747     getMaxContentWidth: function(header) {
748         var cellSelector = header.getCellInnerSelector(),
749             cells        = this.el.query(cellSelector),
750             i = 0,
751             ln = cells.length,
752             maxWidth = header.el.dom.scrollWidth,
753             scrollWidth;
754
755         for (; i < ln; i++) {
756             scrollWidth = cells[i].scrollWidth;
757             if (scrollWidth > maxWidth) {
758                 maxWidth = scrollWidth;
759             }
760         }
761         return maxWidth;
762     },
763
764     getPositionByEvent: function(e) {
765         var me       = this,
766             cellNode = e.getTarget(me.cellSelector),
767             rowNode  = e.getTarget(me.itemSelector),
768             record   = me.getRecord(rowNode),
769             header   = me.getHeaderByCell(cellNode);
770
771         return me.getPosition(record, header);
772     },
773
774     getHeaderByCell: function(cell) {
775         if (cell) {
776             var m = cell.className.match(this.cellRe);
777             if (m && m[1]) {
778                 return Ext.getCmp(m[1]);
779             }
780         }
781         return false;
782     },
783
784     /**
785      * @param {Object} position The current row and column: an object containing the following properties:<ul>
786      * <li>row<div class="sub-desc"> The row <b>index</b></div></li>
787      * <li>column<div class="sub-desc">The column <b>index</b></div></li>
788      * </ul>
789      * @param {String} direction 'up', 'down', 'right' and 'left'
790      * @param {Ext.EventObject} e event
791      * @param {Boolean} preventWrap Set to true to prevent wrap around to the next or previous row.
792      * @param {Function} verifierFn A function to verify the validity of the calculated position. When using this function, you must return true to allow the newPosition to be returned.
793      * @param {Scope} scope Scope to run the verifierFn in
794      * @returns {Object} newPosition An object containing the following properties:<ul>
795      * <li>row<div class="sub-desc"> The row <b>index</b></div></li>
796      * <li>column<div class="sub-desc">The column <b>index</b></div></li>
797      * </ul>
798      * @private
799      */
800     walkCells: function(pos, direction, e, preventWrap, verifierFn, scope) {
801         var me       = this,
802             row      = pos.row,
803             column   = pos.column,
804             rowCount = me.store.getCount(),
805             firstCol = me.getFirstVisibleColumnIndex(),
806             lastCol  = me.getLastVisibleColumnIndex(),
807             newPos   = {row: row, column: column},
808             activeHeader = me.headerCt.getHeaderAtIndex(column);
809
810         // no active header or its currently hidden
811         if (!activeHeader || activeHeader.hidden) {
812             return false;
813         }
814
815         e = e || {};
816         direction = direction.toLowerCase();
817         switch (direction) {
818             case 'right':
819                 // has the potential to wrap if its last
820                 if (column === lastCol) {
821                     // if bottom row and last column, deny right
822                     if (preventWrap || row === rowCount - 1) {
823                         return false;
824                     }
825                     if (!e.ctrlKey) {
826                         // otherwise wrap to nextRow and firstCol
827                         newPos.row = row + 1;
828                         newPos.column = firstCol;
829                     }
830                 // go right
831                 } else {
832                     if (!e.ctrlKey) {
833                         newPos.column = column + me.getRightGap(activeHeader);
834                     } else {
835                         newPos.column = lastCol;
836                     }
837                 }
838                 break;
839
840             case 'left':
841                 // has the potential to wrap
842                 if (column === firstCol) {
843                     // if top row and first column, deny left
844                     if (preventWrap || row === 0) {
845                         return false;
846                     }
847                     if (!e.ctrlKey) {
848                         // otherwise wrap to prevRow and lastCol
849                         newPos.row = row - 1;
850                         newPos.column = lastCol;
851                     }
852                 // go left
853                 } else {
854                     if (!e.ctrlKey) {
855                         newPos.column = column + me.getLeftGap(activeHeader);
856                     } else {
857                         newPos.column = firstCol;
858                     }
859                 }
860                 break;
861
862             case 'up':
863                 // if top row, deny up
864                 if (row === 0) {
865                     return false;
866                 // go up
867                 } else {
868                     if (!e.ctrlKey) {
869                         newPos.row = row - 1;
870                     } else {
871                         newPos.row = 0;
872                     }
873                 }
874                 break;
875
876             case 'down':
877                 // if bottom row, deny down
878                 if (row === rowCount - 1) {
879                     return false;
880                 // go down
881                 } else {
882                     if (!e.ctrlKey) {
883                         newPos.row = row + 1;
884                     } else {
885                         newPos.row = rowCount - 1;
886                     }
887                 }
888                 break;
889         }
890
891         if (verifierFn && verifierFn.call(scope || window, newPos) !== true) {
892             return false;
893         } else {
894             return newPos;
895         }
896     },
897     getFirstVisibleColumnIndex: function() {
898         var headerCt   = this.getHeaderCt(),
899             allColumns = headerCt.getGridColumns(),
900             visHeaders = Ext.ComponentQuery.query(':not([hidden])', allColumns),
901             firstHeader = visHeaders[0];
902
903         return headerCt.getHeaderIndex(firstHeader);
904     },
905
906     getLastVisibleColumnIndex: function() {
907         var headerCt   = this.getHeaderCt(),
908             allColumns = headerCt.getGridColumns(),
909             visHeaders = Ext.ComponentQuery.query(':not([hidden])', allColumns),
910             lastHeader = visHeaders[visHeaders.length - 1];
911
912         return headerCt.getHeaderIndex(lastHeader);
913     },
914
915     getHeaderCt: function() {
916         return this.headerCt;
917     },
918
919     getPosition: function(record, header) {
920         var me = this,
921             store = me.store,
922             gridCols = me.headerCt.getGridColumns();
923
924         return {
925             row: store.indexOf(record),
926             column: Ext.Array.indexOf(gridCols, header)
927         };
928     },
929
930     /**
931      * Determines the 'gap' between the closest adjacent header to the right
932      * that is not hidden.
933      * @private
934      */
935     getRightGap: function(activeHeader) {
936         var headerCt        = this.getHeaderCt(),
937             headers         = headerCt.getGridColumns(),
938             activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
939             i               = activeHeaderIdx + 1,
940             nextIdx;
941
942         for (; i <= headers.length; i++) {
943             if (!headers[i].hidden) {
944                 nextIdx = i;
945                 break;
946             }
947         }
948
949         return nextIdx - activeHeaderIdx;
950     },
951
952     beforeDestroy: function() {
953         if (this.rendered) {
954             this.el.removeAllListeners();
955         }
956         this.callParent(arguments);
957     },
958
959     /**
960      * Determines the 'gap' between the closest adjacent header to the left
961      * that is not hidden.
962      * @private
963      */
964     getLeftGap: function(activeHeader) {
965         var headerCt        = this.getHeaderCt(),
966             headers         = headerCt.getGridColumns(),
967             activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
968             i               = activeHeaderIdx - 1,
969             prevIdx;
970
971         for (; i >= 0; i--) {
972             if (!headers[i].hidden) {
973                 prevIdx = i;
974                 break;
975             }
976         }
977
978         return prevIdx - activeHeaderIdx;
979     }
980 });