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