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