2 * @class Ext.grid.header.Container
3 * @extends Ext.container.Container
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.
11 * @xtype headercontainer
13 Ext.define('Ext.grid.header.Container', {
14 extend: 'Ext.container.Container',
16 'Ext.grid.ColumnLayout',
17 'Ext.grid.column.Column',
21 'Ext.grid.plugin.HeaderResizer',
22 'Ext.grid.plugin.HeaderReorderer'
26 alias: 'widget.headercontainer',
28 baseCls: Ext.baseCSSPrefix + 'grid-header-ct',
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.
37 defaultType: 'gridcolumn',
39 * @cfg {Number} defaultWidth
40 * Width of the header if no width or flex is specified. Defaults to 100.
45 sortAscText: 'Sort Ascending',
46 sortDescText: 'Sort Descending',
47 sortClearText: 'Clear Sort',
48 columnsText: 'Columns',
50 lastHeaderCls: Ext.baseCSSPrefix + 'column-header-last',
51 firstHeaderCls: Ext.baseCSSPrefix + 'column-header-first',
52 headerOpenCls: Ext.baseCSSPrefix + 'column-header-open',
54 // private; will probably be removed by 4.0
62 * <code>true</code> if this HeaderContainer is in fact a group header which contains sub headers.
64 * @property isGroupHeader
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.
76 initComponent: function() {
80 me.plugins = me.plugins || [];
82 // TODO: Pass in configurations to turn on/off dynamic
83 // resizing and disable resizing all together
85 // Only set up a Resizer and Reorderer for the topmost HeaderContainer.
86 // Nested Group Headers are themselves HeaderContainers
88 me.resizer = Ext.create('Ext.grid.plugin.HeaderResizer');
89 me.reorderer = Ext.create('Ext.grid.plugin.HeaderReorderer');
90 if (!me.enableColumnResize) {
93 if (!me.enableColumnMove) {
94 me.reorderer.disable();
96 me.plugins.push(me.reorderer, me.resizer);
99 // Base headers do not need a box layout
100 if (me.isHeader && !me.items) {
103 // HeaderContainer and Group header needs a gridcolumn layout.
107 availableSpaceOffset: me.availableSpaceOffset,
112 me.defaults = me.defaults || {};
113 Ext.applyIf(me.defaults, {
114 width: me.defaultWidth,
115 triStateSort: me.triStateSort,
116 sortable: me.sortable
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
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
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
144 'headertriggerclick',
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
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
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
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
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
183 onDestroy: function() {
184 Ext.destroy(this.resizer, this.reorderer);
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
194 c.headerId = 'h' + (++me.headerCounter);
196 me.callParent(arguments);
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) {
205 me.callParent(arguments);
209 afterRender: function() {
211 var store = this.up('[store]').store,
212 sorters = store.sorters,
213 first = sorters.first(),
217 hd = this.down('gridcolumn[dataIndex=' + first.property +']');
219 hd.setSortState(first.direction, false, true);
224 afterLayout: function() {
225 if (!this.isHeader) {
227 topHeaders = me.query('>gridcolumn:not([hidden])'),
232 me.callParent(arguments);
234 if (topHeaders.length) {
235 firstHeaderEl = topHeaders[0].el;
236 if (firstHeaderEl !== me.pastFirstHeaderEl) {
237 if (me.pastFirstHeaderEl) {
238 me.pastFirstHeaderEl.removeCls(me.firstHeaderCls);
240 firstHeaderEl.addCls(me.firstHeaderCls);
241 me.pastFirstHeaderEl = firstHeaderEl;
244 lastHeaderEl = topHeaders[topHeaders.length - 1].el;
245 if (lastHeaderEl !== me.pastLastHeaderEl) {
246 if (me.pastLastHeaderEl) {
247 me.pastLastHeaderEl.removeCls(me.lastHeaderCls);
249 lastHeaderEl.addCls(me.lastHeaderCls);
250 me.pastLastHeaderEl = lastHeaderEl
257 onHeaderShow: function(header) {
258 // Pass up to the GridSection
260 gridSection = me.ownerCt,
262 topItems, topItemsVisible,
269 colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
271 colCheckItem.setChecked(true, true);
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]);
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']();
287 if (topItemsVisible == 1) {
288 me.disabledMenuItems = topItems;
290 me.disabledMenuItems = [];
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) {
299 me.view.onHeaderShow(me, header, true);
302 gridSection.onHeaderShow(me, header);
305 me.fireEvent('columnshow', me, header);
307 // The header's own hide suppresses cascading layouts, so lay the headers out now
311 onHeaderHide: function(header, suppressLayout) {
312 // Pass up to the GridSection
314 gridSection = me.ownerCt,
320 // If the header was hidden programmatically, sync the Menu state
321 colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
323 colCheckItem.setChecked(false, true);
325 me.setDisabledItems();
328 // Only update the UI when we are notified about base level Header hides;
329 if (!header.isGroupHeader) {
331 me.view.onHeaderHide(me, header, true);
334 gridSection.onHeaderHide(me, header);
337 // The header's own hide suppresses cascading layouts, so lay the headers out now
338 if (!suppressLayout) {
342 me.fireEvent('columnhide', me, header);
345 setDisabledItems: function(){
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 = [];
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]'));
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);
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.
384 tempLock: function() {
386 Ext.Function.defer(function() {
391 onHeaderResize: function(header, w, suppressFocus) {
393 if (this.view && this.view.rendered) {
394 this.view.onHeaderResize(header, w, suppressFocus);
396 this.fireEvent('columnresize', this, header, w);
399 onHeaderClick: function(header, e, t) {
400 this.fireEvent("headerclick", this, header, e, t);
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);
410 showMenuBy: function(t, header) {
411 var menu = this.getMenu(),
412 ascItem = menu.down('#ascItem'),
413 descItem = menu.down('#descItem'),
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);
421 // enable or disable asc & desc menu items based on header being sortable
422 sortableMth = header.sortable ? 'enable' : 'disable';
424 ascItem[sortableMth]();
427 descItem[sortableMth]();
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);
439 moveHeader: function(fromIdx, toIdx) {
441 // An automatically expiring lock
443 this.onHeaderMoved(this.move(fromIdx, toIdx), fromIdx, toIdx);
446 purgeCache: function() {
448 // Delete column cache - column order has changed.
449 delete me.gridDataColumns;
451 // Menu changes when columns are moved. It will be recreated.
458 onHeaderMoved: function(header, fromIdx, toIdx) {
460 gridSection = me.ownerCt;
463 gridSection.onHeaderMove(me, header, fromIdx, toIdx);
465 me.fireEvent("columnmove", me, header, fromIdx, toIdx);
469 * Gets the menu (and will create it if it doesn't already exist)
472 getMenu: function() {
476 me.menu = Ext.create('Ext.menu.Menu', {
477 items: me.getMenuItems(),
479 deactivate: me.onMenuDeactivate,
483 me.setDisabledItems();
484 me.fireEvent('menucreate', me, me.menu);
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
494 getMenuItems: function() {
497 itemId: 'columnItem',
498 text: me.columnsText,
499 cls: Ext.baseCSSPrefix + 'cols-icon',
500 menu: me.getColumnMenu(me)
506 text: me.sortAscText,
507 cls: 'xg-hmenu-sort-asc',
508 handler: me.onSortAscClick,
512 text: me.sortDescText,
513 cls: 'xg-hmenu-sort-desc',
514 handler: me.onSortDescClick,
521 // sort asc when clicking on item in menu
522 onSortAscClick: function() {
523 var menu = this.getMenu(),
524 activeHeader = menu.activeHeader;
526 activeHeader.setSortState('ASC');
529 // sort desc when clicking on item in menu
530 onSortDescClick: function() {
531 var menu = this.getMenu(),
532 activeHeader = menu.activeHeader;
534 activeHeader.setSortState('DESC');
538 * Returns an array of menu CheckItems corresponding to all immediate children of the passed Container which have been configured as hideable.
540 getColumnMenu: function(headerContainer) {
544 items = headerContainer.query('>gridcolumn[hideable]'),
545 itemsLn = items.length,
548 for (; i < itemsLn; i++) {
550 menuItem = Ext.create('Ext.menu.CheckItem', {
552 checked: !item.hidden,
555 menu: item.isGroupHeader ? this.getColumnMenu(item) : undefined,
556 checkHandler: this.onColumnCheckChange,
560 menuItem.disabled = true;
562 menuItems.push(menuItem);
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.
567 destroy: Ext.Function.bind(menuItem.destroy, menuItem)
573 onColumnCheckChange: function(checkItem, checked) {
574 var header = Ext.getCmp(checkItem.headerId);
575 header[checked ? 'show' : 'hide']();
579 * Get the columns used for generating a template via TableChunker.
580 * Returns an array of all columns and their
585 * - columnId - used to create an identifying CSS class
586 * - cls The tdCls configuration from the Column object
589 getColumnsForTpl: function(flushCache) {
591 headers = this.getGridColumns(flushCache),
592 headersLn = headers.length,
596 for (; i < headersLn; i++) {
599 dataIndex: header.dataIndex,
601 width: header.hidden ? 0 : header.getDesiredWidth(),
604 columnId: header.getItemId()
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.
614 getColumnCount: function() {
615 return this.getGridColumns().length;
619 * Gets the full width of all columns that are visible.
621 getFullWidth: function(flushCache) {
623 headers = this.getVisibleGridColumns(flushCache),
624 headersLn = headers.length,
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
634 fullWidth += headers[i].getWidth();
641 // invoked internally by a header when not using triStateSorting
642 clearOtherSortStates: function(activeHeader) {
643 var headers = this.getGridColumns(),
644 headersLn = headers.length,
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);
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.
666 getVisibleGridColumns: function(refreshCache) {
667 return Ext.ComponentQuery.query(':not([hidden])', this.getGridColumns(refreshCache));
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.
676 getGridColumns: function(refreshCache) {
678 result = refreshCache ? null : me.gridDataColumns;
680 // Not already got the column cache, so collect the base columns
682 me.gridDataColumns = result = [];
683 me.cascade(function(c) {
684 if ((c !== me) && !c.isGroupHeader) {
694 * Get the index of a leaf level header regardless of what the nesting
697 getHeaderIndex: function(header) {
698 var columns = this.getGridColumns();
699 return Ext.Array.indexOf(columns, header);
703 * Get a leaf level header by index regardless of what the nesting
706 getHeaderAtIndex: function(index) {
707 var columns = this.getGridColumns();
708 return columns[index];
712 * Maps the record data to base it on the header id's.
713 * This correlates to the markup/template generated by
716 prepareData: function(data, rowIdx, record, view, panel) {
718 headers = this.gridDataColumns || this.getGridColumns(),
719 headersLn = headers.length,
728 for (; colIdx < headersLn; colIdx++) {
733 header = headers[colIdx];
734 headerId = header.id;
735 renderer = header.renderer;
736 value = data[header.dataIndex];
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];
744 if (typeof renderer === "function") {
745 value = renderer.call(
746 header.scope || this.ownerCt,
748 // metadata per cell passing an obj by reference so that
749 // it can be manipulated inside the renderer
761 // This warning attribute is used by the compat layer
762 obj.cssWarning = true;
763 metaData.tdCls = metaData.css;
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 === '') {
775 obj[headerId] = value;
780 expandToFit: function(header) {
782 this.view.expandToFit(header);