Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / grid / header / Container.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.grid.header.Container
17  * @extends Ext.container.Container
18  *
19  * Container which holds headers and is docked at the top or bottom of a TablePanel.
20  * The HeaderContainer drives resizing/moving/hiding of columns within the TableView.
21  * As headers are hidden, moved or resized the headercontainer is responsible for
22  * triggering changes within the view.
23  */
24 Ext.define('Ext.grid.header.Container', {
25     extend: 'Ext.container.Container',
26     uses: [
27         'Ext.grid.ColumnLayout',
28         'Ext.grid.column.Column',
29         'Ext.menu.Menu',
30         'Ext.menu.CheckItem',
31         'Ext.menu.Separator',
32         'Ext.grid.plugin.HeaderResizer',
33         'Ext.grid.plugin.HeaderReorderer'
34     ],
35     border: true,
36
37     alias: 'widget.headercontainer',
38
39     baseCls: Ext.baseCSSPrefix + 'grid-header-ct',
40     dock: 'top',
41
42     /**
43      * @cfg {Number} weight
44      * HeaderContainer overrides the default weight of 0 for all docked items to 100.
45      * This is so that it has more priority over things like toolbars.
46      */
47     weight: 100,
48     defaultType: 'gridcolumn',
49     /**
50      * @cfg {Number} defaultWidth
51      * Width of the header if no width or flex is specified. Defaults to 100.
52      */
53     defaultWidth: 100,
54
55
56     sortAscText: 'Sort Ascending',
57     sortDescText: 'Sort Descending',
58     sortClearText: 'Clear Sort',
59     columnsText: 'Columns',
60
61     lastHeaderCls: Ext.baseCSSPrefix + 'column-header-last',
62     firstHeaderCls: Ext.baseCSSPrefix + 'column-header-first',
63     headerOpenCls: Ext.baseCSSPrefix + 'column-header-open',
64
65     // private; will probably be removed by 4.0
66     triStateSort: false,
67
68     ddLock: false,
69
70     dragging: false,
71
72     /**
73      * <code>true</code> if this HeaderContainer is in fact a group header which contains sub headers.
74      * @type Boolean
75      * @property isGroupHeader
76      */
77
78     /**
79      * @cfg {Boolean} sortable
80      * Provides the default sortable state for all Headers within this HeaderContainer.
81      * Also turns on or off the menus in the HeaderContainer. Note that the menu is
82      * shared across every header and therefore turning it off will remove the menu
83      * items for every header.
84      */
85     sortable: true,
86
87     initComponent: function() {
88         var me = this;
89
90         me.headerCounter = 0;
91         me.plugins = me.plugins || [];
92
93         // TODO: Pass in configurations to turn on/off dynamic
94         //       resizing and disable resizing all together
95
96         // Only set up a Resizer and Reorderer for the topmost HeaderContainer.
97         // Nested Group Headers are themselves HeaderContainers
98         if (!me.isHeader) {
99             me.resizer   = Ext.create('Ext.grid.plugin.HeaderResizer');
100             me.reorderer = Ext.create('Ext.grid.plugin.HeaderReorderer');
101             if (!me.enableColumnResize) {
102                 me.resizer.disable();
103             }
104             if (!me.enableColumnMove) {
105                 me.reorderer.disable();
106             }
107             me.plugins.push(me.reorderer, me.resizer);
108         }
109
110         // Base headers do not need a box layout
111         if (me.isHeader && !me.items) {
112             me.layout = 'auto';
113         }
114         // HeaderContainer and Group header needs a gridcolumn layout.
115         else {
116             me.layout = {
117                 type: 'gridcolumn',
118                 availableSpaceOffset: me.availableSpaceOffset,
119                 align: 'stretchmax',
120                 resetStretch: true
121             };
122         }
123         me.defaults = me.defaults || {};
124         Ext.applyIf(me.defaults, {
125             width: me.defaultWidth,
126             triStateSort: me.triStateSort,
127             sortable: me.sortable
128         });
129         me.callParent();
130         me.addEvents(
131             /**
132              * @event columnresize
133              * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
134              * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
135              * @param {Number} width
136              */
137             'columnresize',
138
139             /**
140              * @event headerclick
141              * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
142              * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
143              * @param {Ext.EventObject} e
144              * @param {HTMLElement} t
145              */
146             'headerclick',
147
148             /**
149              * @event headertriggerclick
150              * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
151              * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
152              * @param {Ext.EventObject} e
153              * @param {HTMLElement} t
154              */
155             'headertriggerclick',
156
157             /**
158              * @event columnmove
159              * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
160              * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
161              * @param {Number} fromIdx
162              * @param {Number} toIdx
163              */
164             'columnmove',
165             /**
166              * @event columnhide
167              * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
168              * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
169              */
170             'columnhide',
171             /**
172              * @event columnshow
173              * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
174              * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
175              */
176             'columnshow',
177             /**
178              * @event sortchange
179              * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
180              * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
181              * @param {String} direction
182              */
183             'sortchange',
184             /**
185              * @event menucreate
186              * Fired immediately after the column header menu is created.
187              * @param {Ext.grid.header.Container} ct This instance
188              * @param {Ext.menu.Menu} menu The Menu that was created
189              */
190             'menucreate'
191         );
192     },
193
194     onDestroy: function() {
195         Ext.destroy(this.resizer, this.reorderer);
196         this.callParent();
197     },
198     
199     applyDefaults: function(config){
200         /*
201          * Ensure header.Container defaults don't get applied to a RowNumberer 
202          * if an xtype is supplied. This isn't an ideal solution however it's 
203          * much more likely that a RowNumberer with no options will be created, 
204          * wanting to use the defaults specified on the class as opposed to 
205          * those setup on the Container.
206          */
207         if (config && !config.isComponent && config.xtype == 'rownumberer') {
208             return config;
209         }
210         return this.callParent([config]);
211     },
212
213     applyColumnsState: function(columns) {
214         if (!columns || !columns.length) {
215             return;
216         }
217
218         var me = this,
219             i = 0,
220             index,
221             col;
222
223         Ext.each(columns, function (columnState) {
224             col = me.down('gridcolumn[headerId=' + columnState.id + ']');
225             if (col) {
226                 index = me.items.indexOf(col);
227                 if (i !== index) {
228                     me.moveHeader(index, i);
229                 }
230
231                 if (col.applyColumnState) {
232                     col.applyColumnState(columnState);
233                 }
234                 ++i;
235             }
236         });
237     },
238
239     getColumnsState: function () {
240         var me = this,
241             columns = [],
242             state;
243
244         me.items.each(function (col) {
245             state = col.getColumnState && col.getColumnState();
246             if (state) {
247                 columns.push(state);
248             }
249         });
250
251         return columns;
252     },
253
254     // Invalidate column cache on add
255     // We cannot refresh the View on every add because this method is called
256     // when the HeaderDropZone moves Headers around, that will also refresh the view
257     onAdd: function(c) {
258         var me = this;
259         if (!c.headerId) {
260             c.headerId = c.initialConfig.id || ('h' + (++me.headerCounter));
261         }
262         //<debug warn>
263         if (Ext.global.console && Ext.global.console.warn) {
264             if (!me._usedIDs) me._usedIDs = {};
265             if (me._usedIDs[c.headerId]) {
266                 Ext.global.console.warn(this.$className, 'attempted to reuse an existing id', c.headerId);
267             }
268             me._usedIDs[c.headerId] = true;
269         }
270         //</debug>
271         me.callParent(arguments);
272         me.purgeCache();
273     },
274
275     // Invalidate column cache on remove
276     // We cannot refresh the View on every remove because this method is called
277     // when the HeaderDropZone moves Headers around, that will also refresh the view
278     onRemove: function(c) {
279         var me = this;
280         me.callParent(arguments);
281         me.purgeCache();
282     },
283
284     afterRender: function() {
285         this.callParent();
286         var store   = this.up('[store]').store,
287             sorters = store.sorters,
288             first   = sorters.first(),
289             hd;
290
291         if (first) {
292             hd = this.down('gridcolumn[dataIndex=' + first.property  +']');
293             if (hd) {
294                 hd.setSortState(first.direction, false, true);
295             }
296         }
297     },
298
299     afterLayout: function() {
300         if (!this.isHeader) {
301             var me = this,
302                 topHeaders = me.query('>gridcolumn:not([hidden])'),
303                 viewEl,
304                 firstHeaderEl,
305                 lastHeaderEl;
306
307             me.callParent(arguments);
308
309             if (topHeaders.length) {
310                 firstHeaderEl = topHeaders[0].el;
311                 if (firstHeaderEl !== me.pastFirstHeaderEl) {
312                     if (me.pastFirstHeaderEl) {
313                         me.pastFirstHeaderEl.removeCls(me.firstHeaderCls);
314                     }
315                     firstHeaderEl.addCls(me.firstHeaderCls);
316                     me.pastFirstHeaderEl = firstHeaderEl;
317                 }
318
319                 lastHeaderEl = topHeaders[topHeaders.length - 1].el;
320                 if (lastHeaderEl !== me.pastLastHeaderEl) {
321                     if (me.pastLastHeaderEl) {
322                         me.pastLastHeaderEl.removeCls(me.lastHeaderCls);
323                     }
324                     lastHeaderEl.addCls(me.lastHeaderCls);
325                     me.pastLastHeaderEl = lastHeaderEl;
326                 }
327             }
328         }
329
330     },
331
332     onHeaderShow: function(header, preventLayout) {
333         // Pass up to the GridSection
334         var me = this,
335             gridSection = me.ownerCt,
336             menu = me.getMenu(),
337             topItems, topItemsVisible,
338             colCheckItem,
339             itemToEnable,
340             len, i;
341
342         if (menu) {
343
344             colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
345             if (colCheckItem) {
346                 colCheckItem.setChecked(true, true);
347             }
348
349             // There's more than one header visible, and we've disabled some checked items... re-enable them
350             topItems = menu.query('#columnItem>menucheckitem[checked]');
351             topItemsVisible = topItems.length;
352             if ((me.getVisibleGridColumns().length > 1) && me.disabledMenuItems && me.disabledMenuItems.length) {
353                 if (topItemsVisible == 1) {
354                     Ext.Array.remove(me.disabledMenuItems, topItems[0]);
355                 }
356                 for (i = 0, len = me.disabledMenuItems.length; i < len; i++) {
357                     itemToEnable = me.disabledMenuItems[i];
358                     if (!itemToEnable.isDestroyed) {
359                         itemToEnable[itemToEnable.menu ? 'enableCheckChange' : 'enable']();
360                     }
361                 }
362                 if (topItemsVisible == 1) {
363                     me.disabledMenuItems = topItems;
364                 } else {
365                     me.disabledMenuItems = [];
366                 }
367             }
368         }
369
370         // Only update the grid UI when we are notified about base level Header shows;
371         // Group header shows just cause a layout of the HeaderContainer
372         if (!header.isGroupHeader) {
373             if (me.view) {
374                 me.view.onHeaderShow(me, header, true);
375             }
376             if (gridSection) {
377                 gridSection.onHeaderShow(me, header);
378             }
379         }
380         me.fireEvent('columnshow', me, header);
381
382         // The header's own hide suppresses cascading layouts, so lay the headers out now
383         if (preventLayout !== true) {
384             me.doLayout();
385         }
386     },
387
388     doComponentLayout: function(){
389         var me = this;
390         if (me.view && me.view.saveScrollState) {
391             me.view.saveScrollState();
392         }
393         me.callParent(arguments);
394         if (me.view && me.view.restoreScrollState) {
395             me.view.restoreScrollState();
396         }
397     },
398
399     onHeaderHide: function(header, suppressLayout) {
400         // Pass up to the GridSection
401         var me = this,
402             gridSection = me.ownerCt,
403             menu = me.getMenu(),
404             colCheckItem;
405
406         if (menu) {
407
408             // If the header was hidden programmatically, sync the Menu state
409             colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
410             if (colCheckItem) {
411                 colCheckItem.setChecked(false, true);
412             }
413             me.setDisabledItems();
414         }
415
416         // Only update the UI when we are notified about base level Header hides;
417         if (!header.isGroupHeader) {
418             if (me.view) {
419                 me.view.onHeaderHide(me, header, true);
420             }
421             if (gridSection) {
422                 gridSection.onHeaderHide(me, header);
423             }
424
425             // The header's own hide suppresses cascading layouts, so lay the headers out now
426             if (!suppressLayout) {
427                 me.doLayout();
428             }
429         }
430         me.fireEvent('columnhide', me, header);
431     },
432
433     setDisabledItems: function(){
434         var me = this,
435             menu = me.getMenu(),
436             i = 0,
437             len,
438             itemsToDisable,
439             itemToDisable;
440
441         // Find what to disable. If only one top level item remaining checked, we have to disable stuff.
442         itemsToDisable = menu.query('#columnItem>menucheckitem[checked]');
443         if ((itemsToDisable.length === 1)) {
444             if (!me.disabledMenuItems) {
445                 me.disabledMenuItems = [];
446             }
447
448             // If down to only one column visible, also disable any descendant checkitems
449             if ((me.getVisibleGridColumns().length === 1) && itemsToDisable[0].menu) {
450                 itemsToDisable = itemsToDisable.concat(itemsToDisable[0].menu.query('menucheckitem[checked]'));
451             }
452
453             len = itemsToDisable.length;
454             // Disable any further unchecking at any level.
455             for (i = 0; i < len; i++) {
456                 itemToDisable = itemsToDisable[i];
457                 if (!Ext.Array.contains(me.disabledMenuItems, itemToDisable)) {
458
459                     // If we only want to disable check change: it might be a disabled item, so enable it prior to
460                     // setting its correct disablement level.
461                     itemToDisable.disabled = false;
462                     itemToDisable[itemToDisable.menu ? 'disableCheckChange' : 'disable']();
463                     me.disabledMenuItems.push(itemToDisable);
464                 }
465             }
466         }
467     },
468
469     /**
470      * Temporarily lock the headerCt. This makes it so that clicking on headers
471      * don't trigger actions like sorting or opening of the header menu. This is
472      * done because extraneous events may be fired on the headers after interacting
473      * with a drag drop operation.
474      * @private
475      */
476     tempLock: function() {
477         this.ddLock = true;
478         Ext.Function.defer(function() {
479             this.ddLock = false;
480         }, 200, this);
481     },
482
483     onHeaderResize: function(header, w, suppressFocus) {
484         this.tempLock();
485         if (this.view && this.view.rendered) {
486             this.view.onHeaderResize(header, w, suppressFocus);
487         }
488     },
489
490     onHeaderClick: function(header, e, t) {
491         this.fireEvent("headerclick", this, header, e, t);
492     },
493
494     onHeaderTriggerClick: function(header, e, t) {
495         // generate and cache menu, provide ability to cancel/etc
496         if (this.fireEvent("headertriggerclick", this, header, e, t) !== false) {
497             this.showMenuBy(t, header);
498         }
499     },
500
501     showMenuBy: function(t, header) {
502         var menu = this.getMenu(),
503             ascItem  = menu.down('#ascItem'),
504             descItem = menu.down('#descItem'),
505             sortableMth;
506
507         menu.activeHeader = menu.ownerCt = header;
508         menu.setFloatParent(header);
509         // TODO: remove coupling to Header's titleContainer el
510         header.titleContainer.addCls(this.headerOpenCls);
511
512         // enable or disable asc & desc menu items based on header being sortable
513         sortableMth = header.sortable ? 'enable' : 'disable';
514         if (ascItem) {
515             ascItem[sortableMth]();
516         }
517         if (descItem) {
518             descItem[sortableMth]();
519         }
520         menu.showBy(t);
521     },
522
523     // remove the trigger open class when the menu is hidden
524     onMenuDeactivate: function() {
525         var menu = this.getMenu();
526         // TODO: remove coupling to Header's titleContainer el
527         menu.activeHeader.titleContainer.removeCls(this.headerOpenCls);
528     },
529
530     moveHeader: function(fromIdx, toIdx) {
531
532         // An automatically expiring lock
533         this.tempLock();
534         this.onHeaderMoved(this.move(fromIdx, toIdx), fromIdx, toIdx);
535     },
536
537     purgeCache: function() {
538         var me = this;
539         // Delete column cache - column order has changed.
540         delete me.gridDataColumns;
541         delete me.hideableColumns;
542
543         // Menu changes when columns are moved. It will be recreated.
544         if (me.menu) {
545             me.menu.destroy();
546             delete me.menu;
547         }
548     },
549
550     onHeaderMoved: function(header, fromIdx, toIdx) {
551         var me = this,
552             gridSection = me.ownerCt;
553
554         if (gridSection && gridSection.onHeaderMove) {
555             gridSection.onHeaderMove(me, header, fromIdx, toIdx);
556         }
557         me.fireEvent("columnmove", me, header, fromIdx, toIdx);
558     },
559
560     /**
561      * Gets the menu (and will create it if it doesn't already exist)
562      * @private
563      */
564     getMenu: function() {
565         var me = this;
566
567         if (!me.menu) {
568             me.menu = Ext.create('Ext.menu.Menu', {
569                 hideOnParentHide: false,  // Persists when owning ColumnHeader is hidden
570                 items: me.getMenuItems(),
571                 listeners: {
572                     deactivate: me.onMenuDeactivate,
573                     scope: me
574                 }
575             });
576             me.setDisabledItems();
577             me.fireEvent('menucreate', me, me.menu);
578         }
579         return me.menu;
580     },
581
582     /**
583      * Returns an array of menu items to be placed into the shared menu
584      * across all headers in this header container.
585      * @returns {Array} menuItems
586      */
587     getMenuItems: function() {
588         var me = this,
589             menuItems = [],
590             hideableColumns = me.enableColumnHide ? me.getColumnMenu(me) : null;
591
592         if (me.sortable) {
593             menuItems = [{
594                 itemId: 'ascItem',
595                 text: me.sortAscText,
596                 cls: Ext.baseCSSPrefix + 'hmenu-sort-asc',
597                 handler: me.onSortAscClick,
598                 scope: me
599             },{
600                 itemId: 'descItem',
601                 text: me.sortDescText,
602                 cls: Ext.baseCSSPrefix + 'hmenu-sort-desc',
603                 handler: me.onSortDescClick,
604                 scope: me
605             }];
606         }
607         if (hideableColumns && hideableColumns.length) {
608             menuItems.push('-', {
609                 itemId: 'columnItem',
610                 text: me.columnsText,
611                 cls: Ext.baseCSSPrefix + 'cols-icon',
612                 menu: hideableColumns
613             });
614         }
615         return menuItems;
616     },
617
618     // sort asc when clicking on item in menu
619     onSortAscClick: function() {
620         var menu = this.getMenu(),
621             activeHeader = menu.activeHeader;
622
623         activeHeader.setSortState('ASC');
624     },
625
626     // sort desc when clicking on item in menu
627     onSortDescClick: function() {
628         var menu = this.getMenu(),
629             activeHeader = menu.activeHeader;
630
631         activeHeader.setSortState('DESC');
632     },
633
634     /**
635      * Returns an array of menu CheckItems corresponding to all immediate children of the passed Container which have been configured as hideable.
636      */
637     getColumnMenu: function(headerContainer) {
638         var menuItems = [],
639             i = 0,
640             item,
641             items = headerContainer.query('>gridcolumn[hideable]'),
642             itemsLn = items.length,
643             menuItem;
644
645         for (; i < itemsLn; i++) {
646             item = items[i];
647             menuItem = Ext.create('Ext.menu.CheckItem', {
648                 text: item.text,
649                 checked: !item.hidden,
650                 hideOnClick: false,
651                 headerId: item.id,
652                 menu: item.isGroupHeader ? this.getColumnMenu(item) : undefined,
653                 checkHandler: this.onColumnCheckChange,
654                 scope: this
655             });
656             if (itemsLn === 1) {
657                 menuItem.disabled = true;
658             }
659             menuItems.push(menuItem);
660
661             // If the header is ever destroyed - for instance by dragging out the last remaining sub header,
662             // then the associated menu item must also be destroyed.
663             item.on({
664                 destroy: Ext.Function.bind(menuItem.destroy, menuItem)
665             });
666         }
667         return menuItems;
668     },
669
670     onColumnCheckChange: function(checkItem, checked) {
671         var header = Ext.getCmp(checkItem.headerId);
672         header[checked ? 'show' : 'hide']();
673     },
674
675     /**
676      * Get the columns used for generating a template via TableChunker.
677      * Returns an array of all columns and their
678      *  - dataIndex
679      *  - align
680      *  - width
681      *  - id
682      *  - columnId - used to create an identifying CSS class
683      *  - cls The tdCls configuration from the Column object
684      *  @private
685      */
686     getColumnsForTpl: function(flushCache) {
687         var cols    = [],
688             headers   = this.getGridColumns(flushCache),
689             headersLn = headers.length,
690             i = 0,
691             header,
692             width;
693
694         for (; i < headersLn; i++) {
695             header = headers[i];
696
697             if (header.hidden || header.up('headercontainer[hidden=true]')) {
698                 width = 0;
699             } else {
700                 width = header.getDesiredWidth();
701                 // IE6 and IE7 bug.
702                 // Setting the width of the first TD does not work - ends up with a 1 pixel discrepancy.
703                 // We need to increment the passed with in this case.
704                 if ((i === 0) && (Ext.isIE6 || Ext.isIE7)) {
705                     width += 1;
706                 }
707             }
708             cols.push({
709                 dataIndex: header.dataIndex,
710                 align: header.align,
711                 width: width,
712                 id: header.id,
713                 cls: header.tdCls,
714                 columnId: header.getItemId()
715             });
716         }
717         return cols;
718     },
719
720     /**
721      * Returns the number of <b>grid columns</b> descended from this HeaderContainer.
722      * Group Columns are HeaderContainers. All grid columns are returned, including hidden ones.
723      */
724     getColumnCount: function() {
725         return this.getGridColumns().length;
726     },
727
728     /**
729      * Gets the full width of all columns that are visible.
730      */
731     getFullWidth: function(flushCache) {
732         var fullWidth = 0,
733             headers     = this.getVisibleGridColumns(flushCache),
734             headersLn   = headers.length,
735             i         = 0;
736
737         for (; i < headersLn; i++) {
738             if (!isNaN(headers[i].width)) {
739                 // use headers getDesiredWidth if its there
740                 if (headers[i].getDesiredWidth) {
741                     fullWidth += headers[i].getDesiredWidth();
742                 // if injected a diff cmp use getWidth
743                 } else {
744                     fullWidth += headers[i].getWidth();
745                 }
746             }
747         }
748         return fullWidth;
749     },
750
751     // invoked internally by a header when not using triStateSorting
752     clearOtherSortStates: function(activeHeader) {
753         var headers   = this.getGridColumns(),
754             headersLn = headers.length,
755             i         = 0,
756             oldSortState;
757
758         for (; i < headersLn; i++) {
759             if (headers[i] !== activeHeader) {
760                 oldSortState = headers[i].sortState;
761                 // unset the sortstate and dont recurse
762                 headers[i].setSortState(null, true);
763                 //if (!silent && oldSortState !== null) {
764                 //    this.fireEvent('sortchange', this, headers[i], null);
765                 //}
766             }
767         }
768     },
769
770     /**
771      * Returns an array of the <b>visible</b> columns in the grid. This goes down to the lowest column header
772      * level, and does not return <i>grouped</i> headers which contain sub headers.
773      * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache.
774      * @returns {Array}
775      */
776     getVisibleGridColumns: function(refreshCache) {
777         return Ext.ComponentQuery.query(':not([hidden])', this.getGridColumns(refreshCache));
778     },
779
780     /**
781      * Returns an array of all columns which map to Store fields. This goes down to the lowest column header
782      * level, and does not return <i>grouped</i> headers which contain sub headers.
783      * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache.
784      * @returns {Array}
785      */
786     getGridColumns: function(refreshCache) {
787         var me = this,
788             result = refreshCache ? null : me.gridDataColumns;
789
790         // Not already got the column cache, so collect the base columns
791         if (!result) {
792             me.gridDataColumns = result = [];
793             me.cascade(function(c) {
794                 if ((c !== me) && !c.isGroupHeader) {
795                     result.push(c);
796                 }
797             });
798         }
799
800         return result;
801     },
802
803     /**
804      * @private
805      * For use by column headers in determining whether there are any hideable columns when deciding whether or not
806      * the header menu should be disabled.
807      */
808     getHideableColumns: function(refreshCache) {
809         var me = this,
810             result = refreshCache ? null : me.hideableColumns;
811
812         if (!result) {
813             result = me.hideableColumns = me.query('[hideable]');
814         }
815         return result;
816     },
817
818     /**
819      * Get the index of a leaf level header regardless of what the nesting
820      * structure is.
821      */
822     getHeaderIndex: function(header) {
823         var columns = this.getGridColumns();
824         return Ext.Array.indexOf(columns, header);
825     },
826
827     /**
828      * Get a leaf level header by index regardless of what the nesting
829      * structure is.
830      */
831     getHeaderAtIndex: function(index) {
832         var columns = this.getGridColumns();
833         return columns[index];
834     },
835
836     /**
837      * Maps the record data to base it on the header id's.
838      * This correlates to the markup/template generated by
839      * TableChunker.
840      */
841     prepareData: function(data, rowIdx, record, view, panel) {
842         var obj       = {},
843             headers   = this.gridDataColumns || this.getGridColumns(),
844             headersLn = headers.length,
845             colIdx    = 0,
846             header,
847             headerId,
848             renderer,
849             value,
850             metaData,
851             store = panel.store;
852
853         for (; colIdx < headersLn; colIdx++) {
854             metaData = {
855                 tdCls: '',
856                 style: ''
857             };
858             header = headers[colIdx];
859             headerId = header.id;
860             renderer = header.renderer;
861             value = data[header.dataIndex];
862
863             // When specifying a renderer as a string, it always resolves
864             // to Ext.util.Format
865             if (typeof renderer === "string") {
866                 header.renderer = renderer = Ext.util.Format[renderer];
867             }
868
869             if (typeof renderer === "function") {
870                 value = renderer.call(
871                     header.scope || this.ownerCt,
872                     value,
873                     // metadata per cell passing an obj by reference so that
874                     // it can be manipulated inside the renderer
875                     metaData,
876                     record,
877                     rowIdx,
878                     colIdx,
879                     store,
880                     view
881                 );
882             }
883
884             // <debug>
885             if (metaData.css) {
886                 // This warning attribute is used by the compat layer
887                 obj.cssWarning = true;
888                 metaData.tdCls = metaData.css;
889                 delete metaData.css;
890             }
891             // </debug>
892
893             obj[headerId+'-modified'] = record.isModified(header.dataIndex) ? Ext.baseCSSPrefix + 'grid-dirty-cell' : '';
894             obj[headerId+'-tdCls'] = metaData.tdCls;
895             obj[headerId+'-tdAttr'] = metaData.tdAttr;
896             obj[headerId+'-style'] = metaData.style;
897             if (value === undefined || value === null || value === '') {
898                 value = '&#160;';
899             }
900             obj[headerId] = value;
901         }
902         return obj;
903     },
904
905     expandToFit: function(header) {
906         if (this.view) {
907             this.view.expandToFit(header);
908         }
909     }
910 });
911