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