Upgrade to ExtJS 4.0.2 - Released 06/09/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     // Invalidate column cache on add
200     // We cannot refresh the View on every add because this method is called
201     // when the HeaderDropZone moves Headers around, that will also refresh the view
202     onAdd: function(c) {
203         var me = this;
204         if (!c.headerId) {
205             c.headerId = 'h' + (++me.headerCounter);
206         }
207         me.callParent(arguments);
208         me.purgeCache();
209     },
210
211     // Invalidate column cache on remove
212     // We cannot refresh the View on every remove because this method is called
213     // when the HeaderDropZone moves Headers around, that will also refresh the view
214     onRemove: function(c) {
215         var me = this;
216         me.callParent(arguments);
217         me.purgeCache();
218     },
219
220     afterRender: function() {
221         this.callParent();
222         var store   = this.up('[store]').store,
223             sorters = store.sorters,
224             first   = sorters.first(),
225             hd;
226
227         if (first) {
228             hd = this.down('gridcolumn[dataIndex=' + first.property  +']');
229             if (hd) {
230                 hd.setSortState(first.direction, false, true);
231             }
232         }
233     },
234
235     afterLayout: function() {
236         if (!this.isHeader) {
237             var me = this,
238                 topHeaders = me.query('>gridcolumn:not([hidden])'),
239                 viewEl,
240                 firstHeaderEl,
241                 lastHeaderEl;
242
243             me.callParent(arguments);
244
245             if (topHeaders.length) {
246                 firstHeaderEl = topHeaders[0].el;
247                 if (firstHeaderEl !== me.pastFirstHeaderEl) {
248                     if (me.pastFirstHeaderEl) {
249                         me.pastFirstHeaderEl.removeCls(me.firstHeaderCls);
250                     }
251                     firstHeaderEl.addCls(me.firstHeaderCls);
252                     me.pastFirstHeaderEl = firstHeaderEl;
253                 }
254
255                 lastHeaderEl = topHeaders[topHeaders.length - 1].el;
256                 if (lastHeaderEl !== me.pastLastHeaderEl) {
257                     if (me.pastLastHeaderEl) {
258                         me.pastLastHeaderEl.removeCls(me.lastHeaderCls);
259                     }
260                     lastHeaderEl.addCls(me.lastHeaderCls);
261                     me.pastLastHeaderEl = lastHeaderEl
262                 }
263             }
264         }
265
266     },
267
268     onHeaderShow: function(header) {
269         // Pass up to the GridSection
270         var me = this,
271             gridSection = me.ownerCt,
272             menu = me.getMenu(),
273             topItems, topItemsVisible,
274             colCheckItem,
275             itemToEnable,
276             len, i;
277
278         if (menu) {
279
280             colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
281             if (colCheckItem) {
282                 colCheckItem.setChecked(true, true);
283             }
284
285             // There's more than one header visible, and we've disabled some checked items... re-enable them
286             topItems = menu.query('#columnItem>menucheckitem[checked]');
287             topItemsVisible = topItems.length;
288             if ((me.getVisibleGridColumns().length > 1) && me.disabledMenuItems && me.disabledMenuItems.length) {
289                 if (topItemsVisible == 1) {
290                     Ext.Array.remove(me.disabledMenuItems, topItems[0]);
291                 }
292                 for (i = 0, len = me.disabledMenuItems.length; i < len; i++) {
293                     itemToEnable = me.disabledMenuItems[i];
294                     if (!itemToEnable.isDestroyed) {
295                         itemToEnable[itemToEnable.menu ? 'enableCheckChange' : 'enable']();
296                     }
297                 }
298                 if (topItemsVisible == 1) {
299                     me.disabledMenuItems = topItems;
300                 } else {
301                     me.disabledMenuItems = [];
302                 }
303             }
304         }
305
306         // Only update the grid UI when we are notified about base level Header shows;
307         // Group header shows just cause a layout of the HeaderContainer
308         if (!header.isGroupHeader) {
309             if (me.view) {
310                 me.view.onHeaderShow(me, header, true);
311             }
312             if (gridSection) {
313                 gridSection.onHeaderShow(me, header);
314             }
315         }
316         me.fireEvent('columnshow', me, header);
317
318         // The header's own hide suppresses cascading layouts, so lay the headers out now
319         me.doLayout();
320     },
321
322     onHeaderHide: function(header, suppressLayout) {
323         // Pass up to the GridSection
324         var me = this,
325             gridSection = me.ownerCt,
326             menu = me.getMenu(),
327             colCheckItem;
328
329         if (menu) {
330
331             // If the header was hidden programmatically, sync the Menu state
332             colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
333             if (colCheckItem) {
334                 colCheckItem.setChecked(false, true);
335             }
336             me.setDisabledItems();
337         }
338
339         // Only update the UI when we are notified about base level Header hides;
340         if (!header.isGroupHeader) {
341             if (me.view) {
342                 me.view.onHeaderHide(me, header, true);
343             }
344             if (gridSection) {
345                 gridSection.onHeaderHide(me, header);
346             }
347
348             // The header's own hide suppresses cascading layouts, so lay the headers out now
349             if (!suppressLayout) {
350                 me.doLayout();
351             }
352         }
353         me.fireEvent('columnhide', me, header);
354     },
355
356     setDisabledItems: function(){
357         var me = this,
358             menu = me.getMenu(),
359             i = 0,
360             len,
361             itemsToDisable,
362             itemToDisable;
363
364         // Find what to disable. If only one top level item remaining checked, we have to disable stuff.
365         itemsToDisable = menu.query('#columnItem>menucheckitem[checked]');
366         if ((itemsToDisable.length === 1)) {
367             if (!me.disabledMenuItems) {
368                 me.disabledMenuItems = [];
369             }
370
371             // If down to only one column visible, also disable any descendant checkitems
372             if ((me.getVisibleGridColumns().length === 1) && itemsToDisable[0].menu) {
373                 itemsToDisable = itemsToDisable.concat(itemsToDisable[0].menu.query('menucheckitem[checked]'));
374             }
375
376             len = itemsToDisable.length;
377             // Disable any further unchecking at any level.
378             for (i = 0; i < len; i++) {
379                 itemToDisable = itemsToDisable[i];
380                 if (!Ext.Array.contains(me.disabledMenuItems, itemToDisable)) {
381
382                     // If we only want to disable check change: it might be a disabled item, so enable it prior to
383                     // setting its correct disablement level.
384                     itemToDisable.disabled = false;
385                     itemToDisable[itemToDisable.menu ? 'disableCheckChange' : 'disable']();
386                     me.disabledMenuItems.push(itemToDisable);
387                 }
388             }
389         }
390     },
391
392     /**
393      * Temporarily lock the headerCt. This makes it so that clicking on headers
394      * don't trigger actions like sorting or opening of the header menu. This is
395      * done because extraneous events may be fired on the headers after interacting
396      * with a drag drop operation.
397      * @private
398      */
399     tempLock: function() {
400         this.ddLock = true;
401         Ext.Function.defer(function() {
402             this.ddLock = false;
403         }, 200, this);
404     },
405
406     onHeaderResize: function(header, w, suppressFocus) {
407         this.tempLock();
408         if (this.view && this.view.rendered) {
409             this.view.onHeaderResize(header, w, suppressFocus);
410         }
411         this.fireEvent('columnresize', this, header, w);
412     },
413
414     onHeaderClick: function(header, e, t) {
415         this.fireEvent("headerclick", this, header, e, t);
416     },
417
418     onHeaderTriggerClick: function(header, e, t) {
419         // generate and cache menu, provide ability to cancel/etc
420         if (this.fireEvent("headertriggerclick", this, header, e, t) !== false) {
421             this.showMenuBy(t, header);
422         }
423     },
424
425     showMenuBy: function(t, header) {
426         var menu = this.getMenu(),
427             ascItem  = menu.down('#ascItem'),
428             descItem = menu.down('#descItem'),
429             sortableMth;
430
431         menu.activeHeader = menu.ownerCt = header;
432         menu.setFloatParent(header);
433         // TODO: remove coupling to Header's titleContainer el
434         header.titleContainer.addCls(this.headerOpenCls);
435
436         // enable or disable asc & desc menu items based on header being sortable
437         sortableMth = header.sortable ? 'enable' : 'disable';
438         if (ascItem) {
439             ascItem[sortableMth]();
440         }
441         if (descItem) {
442             descItem[sortableMth]();
443         }
444         menu.showBy(t);
445     },
446
447     // remove the trigger open class when the menu is hidden
448     onMenuDeactivate: function() {
449         var menu = this.getMenu();
450         // TODO: remove coupling to Header's titleContainer el
451         menu.activeHeader.titleContainer.removeCls(this.headerOpenCls);
452     },
453
454     moveHeader: function(fromIdx, toIdx) {
455
456         // An automatically expiring lock
457         this.tempLock();
458         this.onHeaderMoved(this.move(fromIdx, toIdx), fromIdx, toIdx);
459     },
460
461     purgeCache: function() {
462         var me = this;
463         // Delete column cache - column order has changed.
464         delete me.gridDataColumns;
465
466         // Menu changes when columns are moved. It will be recreated.
467         if (me.menu) {
468             me.menu.destroy();
469             delete me.menu;
470         }
471     },
472
473     onHeaderMoved: function(header, fromIdx, toIdx) {
474         var me = this,
475             gridSection = me.ownerCt;
476
477         if (gridSection) {
478             gridSection.onHeaderMove(me, header, fromIdx, toIdx);
479         }
480         me.fireEvent("columnmove", me, header, fromIdx, toIdx);
481     },
482
483     /**
484      * Gets the menu (and will create it if it doesn't already exist)
485      * @private
486      */
487     getMenu: function() {
488         var me = this;
489
490         if (!me.menu) {
491             me.menu = Ext.create('Ext.menu.Menu', {
492                 hideOnParentHide: false,  // Persists when owning ColumnHeader is hidden
493                 items: me.getMenuItems(),
494                 listeners: {
495                     deactivate: me.onMenuDeactivate,
496                     scope: me
497                 }
498             });
499             me.setDisabledItems();
500             me.fireEvent('menucreate', me, me.menu);
501         }
502         return me.menu;
503     },
504
505     /**
506      * Returns an array of menu items to be placed into the shared menu
507      * across all headers in this header container.
508      * @returns {Array} menuItems
509      */
510     getMenuItems: function() {
511         var me = this,
512             menuItems = [],
513             hideableColumns = me.enableColumnHide ? me.getColumnMenu(me) : null;
514
515         if (me.sortable) {
516             menuItems = [{
517                 itemId: 'ascItem',
518                 text: me.sortAscText,
519                 cls: 'xg-hmenu-sort-asc',
520                 handler: me.onSortAscClick,
521                 scope: me
522             },{
523                 itemId: 'descItem',
524                 text: me.sortDescText,
525                 cls: 'xg-hmenu-sort-desc',
526                 handler: me.onSortDescClick,
527                 scope: me
528             }];
529         };
530         if (hideableColumns && hideableColumns.length) {
531             menuItems.push('-', {
532                 itemId: 'columnItem',
533                 text: me.columnsText,
534                 cls: Ext.baseCSSPrefix + 'cols-icon',
535                 menu: hideableColumns
536             });
537         }
538         return menuItems;
539     },
540
541     // sort asc when clicking on item in menu
542     onSortAscClick: function() {
543         var menu = this.getMenu(),
544             activeHeader = menu.activeHeader;
545
546         activeHeader.setSortState('ASC');
547     },
548
549     // sort desc when clicking on item in menu
550     onSortDescClick: function() {
551         var menu = this.getMenu(),
552             activeHeader = menu.activeHeader;
553
554         activeHeader.setSortState('DESC');
555     },
556
557     /**
558      * Returns an array of menu CheckItems corresponding to all immediate children of the passed Container which have been configured as hideable.
559      */
560     getColumnMenu: function(headerContainer) {
561         var menuItems = [],
562             i = 0,
563             item,
564             items = headerContainer.query('>gridcolumn[hideable]'),
565             itemsLn = items.length,
566             menuItem;
567
568         for (; i < itemsLn; i++) {
569             item = items[i];
570             menuItem = Ext.create('Ext.menu.CheckItem', {
571                 text: item.text,
572                 checked: !item.hidden,
573                 hideOnClick: false,
574                 headerId: item.id,
575                 menu: item.isGroupHeader ? this.getColumnMenu(item) : undefined,
576                 checkHandler: this.onColumnCheckChange,
577                 scope: this
578             });
579             if (itemsLn === 1) {
580                 menuItem.disabled = true;
581             }
582             menuItems.push(menuItem);
583
584             // If the header is ever destroyed - for instance by dragging out the last remaining sub header,
585             // then the associated menu item must also be destroyed.
586             item.on({
587                 destroy: Ext.Function.bind(menuItem.destroy, menuItem)
588             });
589         }
590         return menuItems;
591     },
592
593     onColumnCheckChange: function(checkItem, checked) {
594         var header = Ext.getCmp(checkItem.headerId);
595         header[checked ? 'show' : 'hide']();
596     },
597
598     /**
599      * Get the columns used for generating a template via TableChunker.
600      * Returns an array of all columns and their
601      *  - dataIndex
602      *  - align
603      *  - width
604      *  - id
605      *  - columnId - used to create an identifying CSS class
606      *  - cls The tdCls configuration from the Column object
607      *  @private
608      */
609     getColumnsForTpl: function(flushCache) {
610         var cols    = [],
611             headers   = this.getGridColumns(flushCache),
612             headersLn = headers.length,
613             i = 0,
614             header,
615             width;
616
617         for (; i < headersLn; i++) {
618             header = headers[i];
619
620             if (header.hidden) {
621                 width = 0;
622             } else {
623                 width = header.getDesiredWidth();
624                 // IE6 and IE7 bug.
625                 // Setting the width of the first TD does not work - ends up with a 1 pixel discrepancy.
626                 // We need to increment the passed with in this case.
627                 if ((i == 0) && (Ext.isIE6 || Ext.isIE7)) {
628                     width += 1;
629                 }
630             }
631             cols.push({
632                 dataIndex: header.dataIndex,
633                 align: header.align,
634                 width: width,
635                 id: header.id,
636                 cls: header.tdCls,
637                 columnId: header.getItemId()
638             });
639         }
640         return cols;
641     },
642
643     /**
644      * Returns the number of <b>grid columns</b> descended from this HeaderContainer.
645      * Group Columns are HeaderContainers. All grid columns are returned, including hidden ones.
646      */
647     getColumnCount: function() {
648         return this.getGridColumns().length;
649     },
650
651     /**
652      * Gets the full width of all columns that are visible.
653      */
654     getFullWidth: function(flushCache) {
655         var fullWidth = 0,
656             headers     = this.getVisibleGridColumns(flushCache),
657             headersLn   = headers.length,
658             i         = 0;
659
660         for (; i < headersLn; i++) {
661             if (!isNaN(headers[i].width)) {
662                 // use headers getDesiredWidth if its there
663                 if (headers[i].getDesiredWidth) {
664                     fullWidth += headers[i].getDesiredWidth();
665                 // if injected a diff cmp use getWidth
666                 } else {
667                     fullWidth += headers[i].getWidth();
668                 }
669             }
670         }
671         return fullWidth;
672     },
673
674     // invoked internally by a header when not using triStateSorting
675     clearOtherSortStates: function(activeHeader) {
676         var headers   = this.getGridColumns(),
677             headersLn = headers.length,
678             i         = 0,
679             oldSortState;
680
681         for (; i < headersLn; i++) {
682             if (headers[i] !== activeHeader) {
683                 oldSortState = headers[i].sortState;
684                 // unset the sortstate and dont recurse
685                 headers[i].setSortState(null, true);
686                 //if (!silent && oldSortState !== null) {
687                 //    this.fireEvent('sortchange', this, headers[i], null);
688                 //}
689             }
690         }
691     },
692
693     /**
694      * Returns an array of the <b>visible</b> columns in the grid. This goes down to the lowest column header
695      * level, and does not return <i>grouped</i> headers which contain sub headers.
696      * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache.
697      * @returns {Array}
698      */
699     getVisibleGridColumns: function(refreshCache) {
700         return Ext.ComponentQuery.query(':not([hidden])', this.getGridColumns(refreshCache));
701     },
702
703     /**
704      * Returns an array of all columns which map to Store fields. This goes down to the lowest column header
705      * level, and does not return <i>grouped</i> headers which contain sub headers.
706      * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache.
707      * @returns {Array}
708      */
709     getGridColumns: function(refreshCache) {
710         var me = this,
711             result = refreshCache ? null : me.gridDataColumns;
712
713         // Not already got the column cache, so collect the base columns
714         if (!result) {
715             me.gridDataColumns = result = [];
716             me.cascade(function(c) {
717                 if ((c !== me) && !c.isGroupHeader) {
718                     result.push(c);
719                 }
720             });
721         }
722
723         return result;
724     },
725
726     /**
727      * Get the index of a leaf level header regardless of what the nesting
728      * structure is.
729      */
730     getHeaderIndex: function(header) {
731         var columns = this.getGridColumns();
732         return Ext.Array.indexOf(columns, header);
733     },
734
735     /**
736      * Get a leaf level header by index regardless of what the nesting
737      * structure is.
738      */
739     getHeaderAtIndex: function(index) {
740         var columns = this.getGridColumns();
741         return columns[index];
742     },
743
744     /**
745      * Maps the record data to base it on the header id's.
746      * This correlates to the markup/template generated by
747      * TableChunker.
748      */
749     prepareData: function(data, rowIdx, record, view, panel) {
750         var obj       = {},
751             headers   = this.gridDataColumns || this.getGridColumns(),
752             headersLn = headers.length,
753             colIdx    = 0,
754             header,
755             headerId,
756             renderer,
757             value,
758             metaData,
759             store = panel.store;
760
761         for (; colIdx < headersLn; colIdx++) {
762             metaData = {
763                 tdCls: '',
764                 style: ''
765             };
766             header = headers[colIdx];
767             headerId = header.id;
768             renderer = header.renderer;
769             value = data[header.dataIndex];
770
771             // When specifying a renderer as a string, it always resolves
772             // to Ext.util.Format
773             if (typeof renderer === "string") {
774                 header.renderer = renderer = Ext.util.Format[renderer];
775             }
776
777             if (typeof renderer === "function") {
778                 value = renderer.call(
779                     header.scope || this.ownerCt,
780                     value,
781                     // metadata per cell passing an obj by reference so that
782                     // it can be manipulated inside the renderer
783                     metaData,
784                     record,
785                     rowIdx,
786                     colIdx,
787                     store,
788                     view
789                 );
790             }
791
792             // <debug>
793             if (metaData.css) {
794                 // This warning attribute is used by the compat layer
795                 obj.cssWarning = true;
796                 metaData.tdCls = metaData.css;
797                 delete metaData.css;
798             }
799             // </debug>
800
801             obj[headerId+'-modified'] = record.isModified(header.dataIndex) ? Ext.baseCSSPrefix + 'grid-dirty-cell' : '';
802             obj[headerId+'-tdCls'] = metaData.tdCls;
803             obj[headerId+'-tdAttr'] = metaData.tdAttr;
804             obj[headerId+'-style'] = metaData.style;
805             if (value === undefined || value === null || value === '') {
806                 value = '&#160;';
807             }
808             obj[headerId] = value;
809         }
810         return obj;
811     },
812
813     expandToFit: function(header) {
814         if (this.view) {
815             this.view.expandToFit(header);
816         }
817     }
818 });
819