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 applyDefaults: function(config){
201 * Ensure header.Container defaults don't get applied to a RowNumberer
202 * if an xtype is supplied. This isn't an ideal solution however it's
203 * much more likely that a RowNumberer with no options will be created,
204 * wanting to use the defaults specified on the class as opposed to
205 * those setup on the Container.
207 if (config && !config.isComponent && config.xtype == 'rownumberer') {
210 return this.callParent([config]);
213 applyColumnsState: function(columns) {
214 if (!columns || !columns.length) {
223 Ext.each(columns, function (columnState) {
224 col = me.down('gridcolumn[headerId=' + columnState.id + ']');
226 index = me.items.indexOf(col);
228 me.moveHeader(index, i);
231 if (col.applyColumnState) {
232 col.applyColumnState(columnState);
239 getColumnsState: function () {
244 me.items.each(function (col) {
245 state = col.getColumnState && col.getColumnState();
254 // Invalidate column cache on add
255 // We cannot refresh the View on every add because this method is called
256 // when the HeaderDropZone moves Headers around, that will also refresh the view
260 c.headerId = c.initialConfig.id || ('h' + (++me.headerCounter));
263 if (Ext.global.console && Ext.global.console.warn) {
264 if (!me._usedIDs) me._usedIDs = {};
265 if (me._usedIDs[c.headerId]) {
266 Ext.global.console.warn(this.$className, 'attempted to reuse an existing id', c.headerId);
268 me._usedIDs[c.headerId] = true;
271 me.callParent(arguments);
275 // Invalidate column cache on remove
276 // We cannot refresh the View on every remove because this method is called
277 // when the HeaderDropZone moves Headers around, that will also refresh the view
278 onRemove: function(c) {
280 me.callParent(arguments);
284 afterRender: function() {
286 var store = this.up('[store]').store,
287 sorters = store.sorters,
288 first = sorters.first(),
292 hd = this.down('gridcolumn[dataIndex=' + first.property +']');
294 hd.setSortState(first.direction, false, true);
299 afterLayout: function() {
300 if (!this.isHeader) {
302 topHeaders = me.query('>gridcolumn:not([hidden])'),
307 me.callParent(arguments);
309 if (topHeaders.length) {
310 firstHeaderEl = topHeaders[0].el;
311 if (firstHeaderEl !== me.pastFirstHeaderEl) {
312 if (me.pastFirstHeaderEl) {
313 me.pastFirstHeaderEl.removeCls(me.firstHeaderCls);
315 firstHeaderEl.addCls(me.firstHeaderCls);
316 me.pastFirstHeaderEl = firstHeaderEl;
319 lastHeaderEl = topHeaders[topHeaders.length - 1].el;
320 if (lastHeaderEl !== me.pastLastHeaderEl) {
321 if (me.pastLastHeaderEl) {
322 me.pastLastHeaderEl.removeCls(me.lastHeaderCls);
324 lastHeaderEl.addCls(me.lastHeaderCls);
325 me.pastLastHeaderEl = lastHeaderEl;
332 onHeaderShow: function(header, preventLayout) {
333 // Pass up to the GridSection
335 gridSection = me.ownerCt,
337 topItems, topItemsVisible,
344 colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
346 colCheckItem.setChecked(true, true);
349 // There's more than one header visible, and we've disabled some checked items... re-enable them
350 topItems = menu.query('#columnItem>menucheckitem[checked]');
351 topItemsVisible = topItems.length;
352 if ((me.getVisibleGridColumns().length > 1) && me.disabledMenuItems && me.disabledMenuItems.length) {
353 if (topItemsVisible == 1) {
354 Ext.Array.remove(me.disabledMenuItems, topItems[0]);
356 for (i = 0, len = me.disabledMenuItems.length; i < len; i++) {
357 itemToEnable = me.disabledMenuItems[i];
358 if (!itemToEnable.isDestroyed) {
359 itemToEnable[itemToEnable.menu ? 'enableCheckChange' : 'enable']();
362 if (topItemsVisible == 1) {
363 me.disabledMenuItems = topItems;
365 me.disabledMenuItems = [];
370 // Only update the grid UI when we are notified about base level Header shows;
371 // Group header shows just cause a layout of the HeaderContainer
372 if (!header.isGroupHeader) {
374 me.view.onHeaderShow(me, header, true);
377 gridSection.onHeaderShow(me, header);
380 me.fireEvent('columnshow', me, header);
382 // The header's own hide suppresses cascading layouts, so lay the headers out now
383 if (preventLayout !== true) {
388 doComponentLayout: function(){
390 if (me.view && me.view.saveScrollState) {
391 me.view.saveScrollState();
393 me.callParent(arguments);
394 if (me.view && me.view.restoreScrollState) {
395 me.view.restoreScrollState();
399 onHeaderHide: function(header, suppressLayout) {
400 // Pass up to the GridSection
402 gridSection = me.ownerCt,
408 // If the header was hidden programmatically, sync the Menu state
409 colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
411 colCheckItem.setChecked(false, true);
413 me.setDisabledItems();
416 // Only update the UI when we are notified about base level Header hides;
417 if (!header.isGroupHeader) {
419 me.view.onHeaderHide(me, header, true);
422 gridSection.onHeaderHide(me, header);
425 // The header's own hide suppresses cascading layouts, so lay the headers out now
426 if (!suppressLayout) {
430 me.fireEvent('columnhide', me, header);
433 setDisabledItems: function(){
441 // Find what to disable. If only one top level item remaining checked, we have to disable stuff.
442 itemsToDisable = menu.query('#columnItem>menucheckitem[checked]');
443 if ((itemsToDisable.length === 1)) {
444 if (!me.disabledMenuItems) {
445 me.disabledMenuItems = [];
448 // If down to only one column visible, also disable any descendant checkitems
449 if ((me.getVisibleGridColumns().length === 1) && itemsToDisable[0].menu) {
450 itemsToDisable = itemsToDisable.concat(itemsToDisable[0].menu.query('menucheckitem[checked]'));
453 len = itemsToDisable.length;
454 // Disable any further unchecking at any level.
455 for (i = 0; i < len; i++) {
456 itemToDisable = itemsToDisable[i];
457 if (!Ext.Array.contains(me.disabledMenuItems, itemToDisable)) {
459 // If we only want to disable check change: it might be a disabled item, so enable it prior to
460 // setting its correct disablement level.
461 itemToDisable.disabled = false;
462 itemToDisable[itemToDisable.menu ? 'disableCheckChange' : 'disable']();
463 me.disabledMenuItems.push(itemToDisable);
470 * Temporarily lock the headerCt. This makes it so that clicking on headers
471 * don't trigger actions like sorting or opening of the header menu. This is
472 * done because extraneous events may be fired on the headers after interacting
473 * with a drag drop operation.
476 tempLock: function() {
478 Ext.Function.defer(function() {
483 onHeaderResize: function(header, w, suppressFocus) {
485 if (this.view && this.view.rendered) {
486 this.view.onHeaderResize(header, w, suppressFocus);
490 onHeaderClick: function(header, e, t) {
491 this.fireEvent("headerclick", this, header, e, t);
494 onHeaderTriggerClick: function(header, e, t) {
495 // generate and cache menu, provide ability to cancel/etc
496 if (this.fireEvent("headertriggerclick", this, header, e, t) !== false) {
497 this.showMenuBy(t, header);
501 showMenuBy: function(t, header) {
502 var menu = this.getMenu(),
503 ascItem = menu.down('#ascItem'),
504 descItem = menu.down('#descItem'),
507 menu.activeHeader = menu.ownerCt = header;
508 menu.setFloatParent(header);
509 // TODO: remove coupling to Header's titleContainer el
510 header.titleContainer.addCls(this.headerOpenCls);
512 // enable or disable asc & desc menu items based on header being sortable
513 sortableMth = header.sortable ? 'enable' : 'disable';
515 ascItem[sortableMth]();
518 descItem[sortableMth]();
523 // remove the trigger open class when the menu is hidden
524 onMenuDeactivate: function() {
525 var menu = this.getMenu();
526 // TODO: remove coupling to Header's titleContainer el
527 menu.activeHeader.titleContainer.removeCls(this.headerOpenCls);
530 moveHeader: function(fromIdx, toIdx) {
532 // An automatically expiring lock
534 this.onHeaderMoved(this.move(fromIdx, toIdx), fromIdx, toIdx);
537 purgeCache: function() {
539 // Delete column cache - column order has changed.
540 delete me.gridDataColumns;
541 delete me.hideableColumns;
543 // Menu changes when columns are moved. It will be recreated.
550 onHeaderMoved: function(header, fromIdx, toIdx) {
552 gridSection = me.ownerCt;
554 if (gridSection && gridSection.onHeaderMove) {
555 gridSection.onHeaderMove(me, header, fromIdx, toIdx);
557 me.fireEvent("columnmove", me, header, fromIdx, toIdx);
561 * Gets the menu (and will create it if it doesn't already exist)
564 getMenu: function() {
568 me.menu = Ext.create('Ext.menu.Menu', {
569 hideOnParentHide: false, // Persists when owning ColumnHeader is hidden
570 items: me.getMenuItems(),
572 deactivate: me.onMenuDeactivate,
576 me.setDisabledItems();
577 me.fireEvent('menucreate', me, me.menu);
583 * Returns an array of menu items to be placed into the shared menu
584 * across all headers in this header container.
585 * @returns {Array} menuItems
587 getMenuItems: function() {
590 hideableColumns = me.enableColumnHide ? me.getColumnMenu(me) : null;
595 text: me.sortAscText,
596 cls: Ext.baseCSSPrefix + 'hmenu-sort-asc',
597 handler: me.onSortAscClick,
601 text: me.sortDescText,
602 cls: Ext.baseCSSPrefix + 'hmenu-sort-desc',
603 handler: me.onSortDescClick,
607 if (hideableColumns && hideableColumns.length) {
608 menuItems.push('-', {
609 itemId: 'columnItem',
610 text: me.columnsText,
611 cls: Ext.baseCSSPrefix + 'cols-icon',
612 menu: hideableColumns
618 // sort asc when clicking on item in menu
619 onSortAscClick: function() {
620 var menu = this.getMenu(),
621 activeHeader = menu.activeHeader;
623 activeHeader.setSortState('ASC');
626 // sort desc when clicking on item in menu
627 onSortDescClick: function() {
628 var menu = this.getMenu(),
629 activeHeader = menu.activeHeader;
631 activeHeader.setSortState('DESC');
635 * Returns an array of menu CheckItems corresponding to all immediate children of the passed Container which have been configured as hideable.
637 getColumnMenu: function(headerContainer) {
641 items = headerContainer.query('>gridcolumn[hideable]'),
642 itemsLn = items.length,
645 for (; i < itemsLn; i++) {
647 menuItem = Ext.create('Ext.menu.CheckItem', {
649 checked: !item.hidden,
652 menu: item.isGroupHeader ? this.getColumnMenu(item) : undefined,
653 checkHandler: this.onColumnCheckChange,
657 menuItem.disabled = true;
659 menuItems.push(menuItem);
661 // If the header is ever destroyed - for instance by dragging out the last remaining sub header,
662 // then the associated menu item must also be destroyed.
664 destroy: Ext.Function.bind(menuItem.destroy, menuItem)
670 onColumnCheckChange: function(checkItem, checked) {
671 var header = Ext.getCmp(checkItem.headerId);
672 header[checked ? 'show' : 'hide']();
676 * Get the columns used for generating a template via TableChunker.
677 * Returns an array of all columns and their
682 * - columnId - used to create an identifying CSS class
683 * - cls The tdCls configuration from the Column object
686 getColumnsForTpl: function(flushCache) {
688 headers = this.getGridColumns(flushCache),
689 headersLn = headers.length,
694 for (; i < headersLn; i++) {
697 if (header.hidden || header.up('headercontainer[hidden=true]')) {
700 width = header.getDesiredWidth();
702 // Setting the width of the first TD does not work - ends up with a 1 pixel discrepancy.
703 // We need to increment the passed with in this case.
704 if ((i === 0) && (Ext.isIE6 || Ext.isIE7)) {
709 dataIndex: header.dataIndex,
714 columnId: header.getItemId()
721 * Returns the number of <b>grid columns</b> descended from this HeaderContainer.
722 * Group Columns are HeaderContainers. All grid columns are returned, including hidden ones.
724 getColumnCount: function() {
725 return this.getGridColumns().length;
729 * Gets the full width of all columns that are visible.
731 getFullWidth: function(flushCache) {
733 headers = this.getVisibleGridColumns(flushCache),
734 headersLn = headers.length,
737 for (; i < headersLn; i++) {
738 if (!isNaN(headers[i].width)) {
739 // use headers getDesiredWidth if its there
740 if (headers[i].getDesiredWidth) {
741 fullWidth += headers[i].getDesiredWidth();
742 // if injected a diff cmp use getWidth
744 fullWidth += headers[i].getWidth();
751 // invoked internally by a header when not using triStateSorting
752 clearOtherSortStates: function(activeHeader) {
753 var headers = this.getGridColumns(),
754 headersLn = headers.length,
758 for (; i < headersLn; i++) {
759 if (headers[i] !== activeHeader) {
760 oldSortState = headers[i].sortState;
761 // unset the sortstate and dont recurse
762 headers[i].setSortState(null, true);
763 //if (!silent && oldSortState !== null) {
764 // this.fireEvent('sortchange', this, headers[i], null);
771 * Returns an array of the <b>visible</b> columns in the grid. This goes down to the lowest column header
772 * level, and does not return <i>grouped</i> headers which contain sub headers.
773 * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache.
776 getVisibleGridColumns: function(refreshCache) {
777 return Ext.ComponentQuery.query(':not([hidden])', this.getGridColumns(refreshCache));
781 * Returns an array of all columns which map to Store fields. This goes down to the lowest column header
782 * level, and does not return <i>grouped</i> headers which contain sub headers.
783 * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache.
786 getGridColumns: function(refreshCache) {
788 result = refreshCache ? null : me.gridDataColumns;
790 // Not already got the column cache, so collect the base columns
792 me.gridDataColumns = result = [];
793 me.cascade(function(c) {
794 if ((c !== me) && !c.isGroupHeader) {
805 * For use by column headers in determining whether there are any hideable columns when deciding whether or not
806 * the header menu should be disabled.
808 getHideableColumns: function(refreshCache) {
810 result = refreshCache ? null : me.hideableColumns;
813 result = me.hideableColumns = me.query('[hideable]');
819 * Get the index of a leaf level header regardless of what the nesting
822 getHeaderIndex: function(header) {
823 var columns = this.getGridColumns();
824 return Ext.Array.indexOf(columns, header);
828 * Get a leaf level header by index regardless of what the nesting
831 getHeaderAtIndex: function(index) {
832 var columns = this.getGridColumns();
833 return columns[index];
837 * Maps the record data to base it on the header id's.
838 * This correlates to the markup/template generated by
841 prepareData: function(data, rowIdx, record, view, panel) {
843 headers = this.gridDataColumns || this.getGridColumns(),
844 headersLn = headers.length,
853 for (; colIdx < headersLn; colIdx++) {
858 header = headers[colIdx];
859 headerId = header.id;
860 renderer = header.renderer;
861 value = data[header.dataIndex];
863 // When specifying a renderer as a string, it always resolves
864 // to Ext.util.Format
865 if (typeof renderer === "string") {
866 header.renderer = renderer = Ext.util.Format[renderer];
869 if (typeof renderer === "function") {
870 value = renderer.call(
871 header.scope || this.ownerCt,
873 // metadata per cell passing an obj by reference so that
874 // it can be manipulated inside the renderer
886 // This warning attribute is used by the compat layer
887 obj.cssWarning = true;
888 metaData.tdCls = metaData.css;
893 obj[headerId+'-modified'] = record.isModified(header.dataIndex) ? Ext.baseCSSPrefix + 'grid-dirty-cell' : '';
894 obj[headerId+'-tdCls'] = metaData.tdCls;
895 obj[headerId+'-tdAttr'] = metaData.tdAttr;
896 obj[headerId+'-style'] = metaData.style;
897 if (value === undefined || value === null || value === '') {
900 obj[headerId] = value;
905 expandToFit: function(header) {
907 this.view.expandToFit(header);