3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.grid.header.Container
17 * @extends Ext.container.Container
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.
24 Ext.define('Ext.grid.header.Container', {
25 extend: 'Ext.container.Container',
27 'Ext.grid.ColumnLayout',
28 'Ext.grid.column.Column',
32 'Ext.grid.plugin.HeaderResizer',
33 'Ext.grid.plugin.HeaderReorderer'
37 alias: 'widget.headercontainer',
39 baseCls: Ext.baseCSSPrefix + 'grid-header-ct',
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.
48 defaultType: 'gridcolumn',
50 * @cfg {Number} defaultWidth
51 * Width of the header if no width or flex is specified. Defaults to 100.
56 sortAscText: 'Sort Ascending',
57 sortDescText: 'Sort Descending',
58 sortClearText: 'Clear Sort',
59 columnsText: 'Columns',
61 lastHeaderCls: Ext.baseCSSPrefix + 'column-header-last',
62 firstHeaderCls: Ext.baseCSSPrefix + 'column-header-first',
63 headerOpenCls: Ext.baseCSSPrefix + 'column-header-open',
65 // private; will probably be removed by 4.0
73 * <code>true</code> if this HeaderContainer is in fact a group header which contains sub headers.
75 * @property isGroupHeader
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.
87 initComponent: function() {
91 me.plugins = me.plugins || [];
93 // TODO: Pass in configurations to turn on/off dynamic
94 // resizing and disable resizing all together
96 // Only set up a Resizer and Reorderer for the topmost HeaderContainer.
97 // Nested Group Headers are themselves HeaderContainers
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();
104 if (!me.enableColumnMove) {
105 me.reorderer.disable();
107 me.plugins.push(me.reorderer, me.resizer);
110 // Base headers do not need a box layout
111 if (me.isHeader && !me.items) {
114 // HeaderContainer and Group header needs a gridcolumn layout.
118 availableSpaceOffset: me.availableSpaceOffset,
123 me.defaults = me.defaults || {};
124 Ext.applyIf(me.defaults, {
125 width: me.defaultWidth,
126 triStateSort: me.triStateSort,
127 sortable: me.sortable
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
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
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
155 'headertriggerclick',
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
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
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
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
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
194 onDestroy: function() {
195 Ext.destroy(this.resizer, this.reorderer);
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
205 c.headerId = 'h' + (++me.headerCounter);
207 me.callParent(arguments);
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) {
216 me.callParent(arguments);
220 afterRender: function() {
222 var store = this.up('[store]').store,
223 sorters = store.sorters,
224 first = sorters.first(),
228 hd = this.down('gridcolumn[dataIndex=' + first.property +']');
230 hd.setSortState(first.direction, false, true);
235 afterLayout: function() {
236 if (!this.isHeader) {
238 topHeaders = me.query('>gridcolumn:not([hidden])'),
243 me.callParent(arguments);
245 if (topHeaders.length) {
246 firstHeaderEl = topHeaders[0].el;
247 if (firstHeaderEl !== me.pastFirstHeaderEl) {
248 if (me.pastFirstHeaderEl) {
249 me.pastFirstHeaderEl.removeCls(me.firstHeaderCls);
251 firstHeaderEl.addCls(me.firstHeaderCls);
252 me.pastFirstHeaderEl = firstHeaderEl;
255 lastHeaderEl = topHeaders[topHeaders.length - 1].el;
256 if (lastHeaderEl !== me.pastLastHeaderEl) {
257 if (me.pastLastHeaderEl) {
258 me.pastLastHeaderEl.removeCls(me.lastHeaderCls);
260 lastHeaderEl.addCls(me.lastHeaderCls);
261 me.pastLastHeaderEl = lastHeaderEl
268 onHeaderShow: function(header) {
269 // Pass up to the GridSection
271 gridSection = me.ownerCt,
273 topItems, topItemsVisible,
280 colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
282 colCheckItem.setChecked(true, true);
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]);
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']();
298 if (topItemsVisible == 1) {
299 me.disabledMenuItems = topItems;
301 me.disabledMenuItems = [];
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) {
310 me.view.onHeaderShow(me, header, true);
313 gridSection.onHeaderShow(me, header);
316 me.fireEvent('columnshow', me, header);
318 // The header's own hide suppresses cascading layouts, so lay the headers out now
322 onHeaderHide: function(header, suppressLayout) {
323 // Pass up to the GridSection
325 gridSection = me.ownerCt,
331 // If the header was hidden programmatically, sync the Menu state
332 colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
334 colCheckItem.setChecked(false, true);
336 me.setDisabledItems();
339 // Only update the UI when we are notified about base level Header hides;
340 if (!header.isGroupHeader) {
342 me.view.onHeaderHide(me, header, true);
345 gridSection.onHeaderHide(me, header);
348 // The header's own hide suppresses cascading layouts, so lay the headers out now
349 if (!suppressLayout) {
353 me.fireEvent('columnhide', me, header);
356 setDisabledItems: function(){
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 = [];
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]'));
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)) {
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);
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.
399 tempLock: function() {
401 Ext.Function.defer(function() {
406 onHeaderResize: function(header, w, suppressFocus) {
408 if (this.view && this.view.rendered) {
409 this.view.onHeaderResize(header, w, suppressFocus);
411 this.fireEvent('columnresize', this, header, w);
414 onHeaderClick: function(header, e, t) {
415 this.fireEvent("headerclick", this, header, e, t);
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);
425 showMenuBy: function(t, header) {
426 var menu = this.getMenu(),
427 ascItem = menu.down('#ascItem'),
428 descItem = menu.down('#descItem'),
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);
436 // enable or disable asc & desc menu items based on header being sortable
437 sortableMth = header.sortable ? 'enable' : 'disable';
439 ascItem[sortableMth]();
442 descItem[sortableMth]();
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);
454 moveHeader: function(fromIdx, toIdx) {
456 // An automatically expiring lock
458 this.onHeaderMoved(this.move(fromIdx, toIdx), fromIdx, toIdx);
461 purgeCache: function() {
463 // Delete column cache - column order has changed.
464 delete me.gridDataColumns;
466 // Menu changes when columns are moved. It will be recreated.
473 onHeaderMoved: function(header, fromIdx, toIdx) {
475 gridSection = me.ownerCt;
478 gridSection.onHeaderMove(me, header, fromIdx, toIdx);
480 me.fireEvent("columnmove", me, header, fromIdx, toIdx);
484 * Gets the menu (and will create it if it doesn't already exist)
487 getMenu: function() {
491 me.menu = Ext.create('Ext.menu.Menu', {
492 hideOnParentHide: false, // Persists when owning ColumnHeader is hidden
493 items: me.getMenuItems(),
495 deactivate: me.onMenuDeactivate,
499 me.setDisabledItems();
500 me.fireEvent('menucreate', me, me.menu);
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
510 getMenuItems: function() {
513 hideableColumns = me.enableColumnHide ? me.getColumnMenu(me) : null;
518 text: me.sortAscText,
519 cls: 'xg-hmenu-sort-asc',
520 handler: me.onSortAscClick,
524 text: me.sortDescText,
525 cls: 'xg-hmenu-sort-desc',
526 handler: me.onSortDescClick,
530 if (hideableColumns && hideableColumns.length) {
531 menuItems.push('-', {
532 itemId: 'columnItem',
533 text: me.columnsText,
534 cls: Ext.baseCSSPrefix + 'cols-icon',
535 menu: hideableColumns
541 // sort asc when clicking on item in menu
542 onSortAscClick: function() {
543 var menu = this.getMenu(),
544 activeHeader = menu.activeHeader;
546 activeHeader.setSortState('ASC');
549 // sort desc when clicking on item in menu
550 onSortDescClick: function() {
551 var menu = this.getMenu(),
552 activeHeader = menu.activeHeader;
554 activeHeader.setSortState('DESC');
558 * Returns an array of menu CheckItems corresponding to all immediate children of the passed Container which have been configured as hideable.
560 getColumnMenu: function(headerContainer) {
564 items = headerContainer.query('>gridcolumn[hideable]'),
565 itemsLn = items.length,
568 for (; i < itemsLn; i++) {
570 menuItem = Ext.create('Ext.menu.CheckItem', {
572 checked: !item.hidden,
575 menu: item.isGroupHeader ? this.getColumnMenu(item) : undefined,
576 checkHandler: this.onColumnCheckChange,
580 menuItem.disabled = true;
582 menuItems.push(menuItem);
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.
587 destroy: Ext.Function.bind(menuItem.destroy, menuItem)
593 onColumnCheckChange: function(checkItem, checked) {
594 var header = Ext.getCmp(checkItem.headerId);
595 header[checked ? 'show' : 'hide']();
599 * Get the columns used for generating a template via TableChunker.
600 * Returns an array of all columns and their
605 * - columnId - used to create an identifying CSS class
606 * - cls The tdCls configuration from the Column object
609 getColumnsForTpl: function(flushCache) {
611 headers = this.getGridColumns(flushCache),
612 headersLn = headers.length,
617 for (; i < headersLn; i++) {
623 width = header.getDesiredWidth();
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)) {
632 dataIndex: header.dataIndex,
637 columnId: header.getItemId()
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.
647 getColumnCount: function() {
648 return this.getGridColumns().length;
652 * Gets the full width of all columns that are visible.
654 getFullWidth: function(flushCache) {
656 headers = this.getVisibleGridColumns(flushCache),
657 headersLn = headers.length,
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
667 fullWidth += headers[i].getWidth();
674 // invoked internally by a header when not using triStateSorting
675 clearOtherSortStates: function(activeHeader) {
676 var headers = this.getGridColumns(),
677 headersLn = headers.length,
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);
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.
699 getVisibleGridColumns: function(refreshCache) {
700 return Ext.ComponentQuery.query(':not([hidden])', this.getGridColumns(refreshCache));
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.
709 getGridColumns: function(refreshCache) {
711 result = refreshCache ? null : me.gridDataColumns;
713 // Not already got the column cache, so collect the base columns
715 me.gridDataColumns = result = [];
716 me.cascade(function(c) {
717 if ((c !== me) && !c.isGroupHeader) {
727 * Get the index of a leaf level header regardless of what the nesting
730 getHeaderIndex: function(header) {
731 var columns = this.getGridColumns();
732 return Ext.Array.indexOf(columns, header);
736 * Get a leaf level header by index regardless of what the nesting
739 getHeaderAtIndex: function(index) {
740 var columns = this.getGridColumns();
741 return columns[index];
745 * Maps the record data to base it on the header id's.
746 * This correlates to the markup/template generated by
749 prepareData: function(data, rowIdx, record, view, panel) {
751 headers = this.gridDataColumns || this.getGridColumns(),
752 headersLn = headers.length,
761 for (; colIdx < headersLn; colIdx++) {
766 header = headers[colIdx];
767 headerId = header.id;
768 renderer = header.renderer;
769 value = data[header.dataIndex];
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];
777 if (typeof renderer === "function") {
778 value = renderer.call(
779 header.scope || this.ownerCt,
781 // metadata per cell passing an obj by reference so that
782 // it can be manipulated inside the renderer
794 // This warning attribute is used by the compat layer
795 obj.cssWarning = true;
796 metaData.tdCls = metaData.css;
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 === '') {
808 obj[headerId] = value;
813 expandToFit: function(header) {
815 this.view.expandToFit(header);