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