/** * Container which holds headers and is docked at the top or bottom of a TablePanel. * The HeaderContainer drives resizing/moving/hiding of columns within the TableView. * As headers are hidden, moved or resized the headercontainer is responsible for * triggering changes within the view. */ Ext.define('Ext.grid.header.Container', { extend: 'Ext.container.Container', requires: [ 'Ext.grid.ColumnLayout', 'Ext.grid.plugin.HeaderResizer', 'Ext.grid.plugin.HeaderReorderer' ], uses: [ 'Ext.grid.column.Column', 'Ext.menu.Menu', 'Ext.menu.CheckItem', 'Ext.menu.Separator' ], border: true, alias: 'widget.headercontainer', baseCls: Ext.baseCSSPrefix + 'grid-header-ct', dock: 'top', /** * @cfg {Number} weight * HeaderContainer overrides the default weight of 0 for all docked items to 100. * This is so that it has more priority over things like toolbars. */ weight: 100, defaultType: 'gridcolumn', detachOnRemove: false, /** * @cfg {Number} defaultWidth * Width of the header if no width or flex is specified. */ defaultWidth: 100, /** * @cfg {Boolean} [sealed=false] * Specify as `true` to constrain column dragging so that a column cannot be dragged into or out of this column. * * **Note that this config is only valid for column headers which contain child column headers, eg:** * { * sealed: true * text: 'ExtJS', * columns: [{ * text: '3.0.4', * dataIndex: 'ext304' * }, { * text: '4.1.0', * dataIndex: 'ext410' * } * } * */ // sortAscText: 'Sort Ascending', // // sortDescText: 'Sort Descending', // // sortClearText: 'Clear Sort', // // columnsText: 'Columns', // headerOpenCls: Ext.baseCSSPrefix + 'column-header-open', // private; will probably be removed by 4.0 triStateSort: false, ddLock: false, dragging: false, /** * @property {Boolean} isGroupHeader * True if this HeaderContainer is in fact a group header which contains sub headers. */ /** * @cfg {Boolean} sortable * Provides the default sortable state for all Headers within this HeaderContainer. * Also turns on or off the menus in the HeaderContainer. Note that the menu is * shared across every header and therefore turning it off will remove the menu * items for every header. */ sortable: true, initComponent: function() { var me = this; me.headerCounter = 0; me.plugins = me.plugins || []; // TODO: Pass in configurations to turn on/off dynamic // resizing and disable resizing all together // Only set up a Resizer and Reorderer for the topmost HeaderContainer. // Nested Group Headers are themselves HeaderContainers if (!me.isHeader) { if (me.enableColumnResize) { me.resizer = new Ext.grid.plugin.HeaderResizer(); me.plugins.push(me.resizer); } if (me.enableColumnMove) { me.reorderer = new Ext.grid.plugin.HeaderReorderer(); me.plugins.push(me.reorderer); } } // Base headers do not need a box layout if (me.isHeader && !me.items) { me.layout = me.layout || 'auto'; } // HeaderContainer and Group header needs a gridcolumn layout. else { me.layout = Ext.apply({ type: 'gridcolumn', align: 'stretchmax' }, me.initialConfig.layout); } me.defaults = me.defaults || {}; Ext.applyIf(me.defaults, { triStateSort: me.triStateSort, sortable: me.sortable }); me.menuTask = new Ext.util.DelayedTask(me.updateMenuDisabledState, me); me.callParent(); me.addEvents( /** * @event columnresize * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition * @param {Number} width */ 'columnresize', /** * @event headerclick * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition * @param {Ext.EventObject} e * @param {HTMLElement} t */ 'headerclick', /** * @event headertriggerclick * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition * @param {Ext.EventObject} e * @param {HTMLElement} t */ 'headertriggerclick', /** * @event columnmove * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition * @param {Number} fromIdx * @param {Number} toIdx */ 'columnmove', /** * @event columnhide * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition */ 'columnhide', /** * @event columnshow * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition */ 'columnshow', /** * @event sortchange * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition * @param {String} direction */ 'sortchange', /** * @event menucreate * Fired immediately after the column header menu is created. * @param {Ext.grid.header.Container} ct This instance * @param {Ext.menu.Menu} menu The Menu that was created */ 'menucreate' ); }, onDestroy: function() { var me = this; me.menuTask.cancel(); Ext.destroy(me.resizer, me.reorderer); me.callParent(); }, applyColumnsState: function(columns) { if (!columns || !columns.length) { return; } var me = this, items = me.items.items, count = items.length, i = 0, length = columns.length, c, col, columnState, index; for (c = 0; c < length; c++) { columnState = columns[c]; for (index = count; index--; ) { col = items[index]; if (col.getStateId && col.getStateId() == columnState.id) { // If a column in the new grid matches up with a saved state... // Ensure that the column is restored to the state order. // i is incremented upon every column match, so all persistent // columns are ordered before any new columns. if (i !== index) { me.moveHeader(index, i); } if (col.applyColumnState) { col.applyColumnState(columnState); } ++i; break; } } } }, getColumnsState: function () { var me = this, columns = [], state; me.items.each(function (col) { state = col.getColumnState && col.getColumnState(); if (state) { columns.push(state); } }); return columns; }, // Invalidate column cache on add // We cannot refresh the View on every add because this method is called // when the HeaderDropZone moves Headers around, that will also refresh the view onAdd: function(c) { var me = this, headerCt = me.isHeader ? me.getOwnerHeaderCt() : me; if (!c.headerId) { c.headerId = c.initialConfig.id || Ext.id(null, 'header-'); } if (!c.stateId) { // This was the headerId generated in 4.0, so to preserve saved state, we now // assign a default stateId in that same manner. The stateId's of a column are // not global at the stateProvider, but are local to the grid state data. The // headerId should still follow our standard naming convention. c.stateId = c.initialConfig.id || ('h' + (++me.headerCounter)); } // if (Ext.global.console && Ext.global.console.warn) { if (!me._usedIDs) { me._usedIDs = {}; } if (me._usedIDs[c.headerId]) { Ext.global.console.warn(this.$className, 'attempted to reuse an existing id', c.headerId); } me._usedIDs[c.headerId] = true; } // me.callParent(arguments); // Upon add of any column we need to purge the *HeaderContainer's* cache of leaf view columns. if (headerCt) { headerCt.purgeCache(); } }, // Invalidate column cache on remove // We cannot refresh the View on every remove because this method is called // when the HeaderDropZone moves Headers around, that will also refresh the view onRemove: function(c) { var me = this, headerCt = me.isHeader ? me.getOwnerHeaderCt() : me; me.callParent(arguments); // if (!me._usedIDs) { me._usedIDs = {}; } delete me._usedIDs[c.headerId]; // // Upon removal of any column we need to purge the *HeaderContainer's* cache of leaf view columns. if (headerCt) { me.purgeCache(); } }, // @private applyDefaults: function(config) { var ret; /* * Ensure header.Container defaults don't get applied to a RowNumberer * if an xtype is supplied. This isn't an ideal solution however it's * much more likely that a RowNumberer with no options will be created, * wanting to use the defaults specified on the class as opposed to * those setup on the Container. */ if (config && !config.isComponent && config.xtype == 'rownumberer') { ret = config; } else { ret = this.callParent(arguments); // Apply default width unless it's a group header (in which case it must be left to shrinkwrap), or it's flexed if (!config.isGroupHeader && !('width' in ret) && !ret.flex) { ret.width = this.defaultWidth; } } return ret; }, afterRender: function() { this.callParent(); this.setSortState(); }, setSortState: function(){ var store = this.up('[store]').store, // grab the first sorter, since there may also be groupers // in this collection first = store.getFirstSorter(), hd; if (first) { hd = this.down('gridcolumn[dataIndex=' + first.property +']'); if (hd) { hd.setSortState(first.direction, false, true); } } else { this.clearOtherSortStates(null); } }, getHeaderMenu: function(){ var menu = this.getMenu(), item; if (menu) { item = menu.child('#columnItem'); if (item) { return item.menu; } } return null; }, onHeaderVisibilityChange: function(header, visible){ var me = this, menu = me.getHeaderMenu(), item; if (menu) { // If the header was hidden programmatically, sync the Menu state item = me.getMenuItemForHeader(menu, header); if (item) { item.setChecked(visible, true); } // delay this since the headers may fire a number of times if we're hiding/showing groups me.menuTask.delay(50); } }, /** * @private * Gets all "leaf" menu nodes and returns the checked count for those leaves. * Only includes columns that are hideable via the menu */ getLeafMenuItems: function() { var me = this, columns = me.getGridColumns(), items = [], i = 0, count = 0, len = columns.length, menu = me.getMenu(), item; for (; i < len; ++i) { item = columns[i]; if (item.hideable) { item = me.getMenuItemForHeader(menu, item); if (item) { items.push(item); if (item.checked) { ++count; } } } else if (!item.hidden && !item.menuDisabled) { ++count; } } return { items: items, checkedCount: count }; }, updateMenuDisabledState: function(){ var me = this, result = me.getLeafMenuItems(), total = result.checkedCount, items = result.items, len = items.length, i = 0, rootItem = me.getMenu().child('#columnItem'); if (total <= 1) { // only one column visible, prevent hiding of the remaining item me.disableMenuItems(rootItem, Ext.ComponentQuery.query('[checked=true]', items)[0]); } else { // at least 2 visible, set the state appropriately for (; i < len; ++i) { me.setMenuItemState(total, rootItem, items[i]); } } }, disableMenuItems: function(rootItem, item){ while (item && item != rootItem) { item.disableCheckChange(); item = item.parentMenu.ownerItem; } }, setMenuItemState: function(total, rootItem, item){ var parentMenu, checkedChildren; while (item && item != rootItem) { parentMenu = item.parentMenu; checkedChildren = item.parentMenu.query('[checked=true]:not([menu])').length; item.enableCheckChange(); item = parentMenu.ownerItem; if (checkedChildren === total) { // contains all the checked children, jump out the item and all parents break; } } // while we're not at the top, disable from the current item up this.disableMenuItems(rootItem, item); }, getMenuItemForHeader: function(menu, header){ return header ? menu.down('menucheckitem[headerId=' + header.id + ']') : null; }, onHeaderShow: function(header) { // Pass up to the GridSection var me = this, gridSection = me.ownerCt; me.onHeaderVisibilityChange(header, true); // Only update the grid UI when we are notified about base level Header shows; // Group header shows just cause a layout of the HeaderContainer if (!header.isGroupHeader) { if (gridSection) { gridSection.onHeaderShow(me, header); } } me.fireEvent('columnshow', me, header); }, onHeaderHide: function(header) { // Pass up to the GridSection var me = this, gridSection = me.ownerCt; me.onHeaderVisibilityChange(header, false); // Only update the UI when we are notified about base level Header hides; if (!header.isGroupHeader) { if (gridSection) { gridSection.onHeaderHide(me, header); } } me.fireEvent('columnhide', me, header); }, /** * Temporarily lock the headerCt. This makes it so that clicking on headers * don't trigger actions like sorting or opening of the header menu. This is * done because extraneous events may be fired on the headers after interacting * with a drag drop operation. * @private */ tempLock: function() { this.ddLock = true; Ext.Function.defer(function() { this.ddLock = false; }, 200, this); }, onHeaderResize: function(header, w, suppressFocus) { var me = this, view = me.view, gridSection = me.ownerCt; // Do not react to header sizing during initial Panel layout when there is no view content to size. if (view && view.table.dom) { me.tempLock(); if (gridSection) { gridSection.onHeaderResize(me, header, w); } } me.fireEvent('columnresize', this, header, w); }, onHeaderClick: function(header, e, t) { header.fireEvent('headerclick', this, header, e, t); this.fireEvent("headerclick", this, header, e, t); }, onHeaderTriggerClick: function(header, e, t) { // generate and cache menu, provide ability to cancel/etc var me = this; if (header.fireEvent('headertriggerclick', me, header, e, t) !== false && me.fireEvent("headertriggerclick", me, header, e, t) !== false) { me.showMenuBy(t, header); } }, showMenuBy: function(t, header) { var menu = this.getMenu(), ascItem = menu.down('#ascItem'), descItem = menu.down('#descItem'), sortableMth; menu.activeHeader = menu.ownerCt = header; menu.setFloatParent(header); // TODO: remove coupling to Header's titleContainer el header.titleEl.addCls(this.headerOpenCls); // enable or disable asc & desc menu items based on header being sortable sortableMth = header.sortable ? 'enable' : 'disable'; if (ascItem) { ascItem[sortableMth](); } if (descItem) { descItem[sortableMth](); } menu.showBy(t); }, // remove the trigger open class when the menu is hidden onMenuDeactivate: function() { var menu = this.getMenu(); // TODO: remove coupling to Header's titleContainer el menu.activeHeader.titleEl.removeCls(this.headerOpenCls); }, moveHeader: function(fromIdx, toIdx) { // An automatically expiring lock this.tempLock(); this.onHeaderMoved(this.move(fromIdx, toIdx), 1, fromIdx, toIdx); }, purgeCache: function() { var me = this; // Delete column cache - column order has changed. delete me.gridDataColumns; delete me.hideableColumns; // Menu changes when columns are moved. It will be recreated. if (me.menu) { // Must hide before destroy so that trigger el is deactivated me.menu.hide(); me.menu.destroy(); delete me.menu; } }, onHeaderMoved: function(header, colsToMove, fromIdx, toIdx) { var me = this, gridSection = me.ownerCt; if (gridSection && gridSection.onHeaderMove) { gridSection.onHeaderMove(me, header, colsToMove, fromIdx, toIdx); } me.fireEvent("columnmove", me, header, fromIdx, toIdx); }, /** * Gets the menu (and will create it if it doesn't already exist) * @private */ getMenu: function() { var me = this; if (!me.menu) { me.menu = new Ext.menu.Menu({ hideOnParentHide: false, // Persists when owning ColumnHeader is hidden items: me.getMenuItems(), listeners: { deactivate: me.onMenuDeactivate, scope: me } }); me.updateMenuDisabledState(); me.fireEvent('menucreate', me, me.menu); } return me.menu; }, /** * Returns an array of menu items to be placed into the shared menu * across all headers in this header container. * @returns {Array} menuItems */ getMenuItems: function() { var me = this, menuItems = [], hideableColumns = me.enableColumnHide ? me.getColumnMenu(me) : null; if (me.sortable) { menuItems = [{ itemId: 'ascItem', text: me.sortAscText, cls: Ext.baseCSSPrefix + 'hmenu-sort-asc', handler: me.onSortAscClick, scope: me },{ itemId: 'descItem', text: me.sortDescText, cls: Ext.baseCSSPrefix + 'hmenu-sort-desc', handler: me.onSortDescClick, scope: me }]; } if (hideableColumns && hideableColumns.length) { menuItems.push('-', { itemId: 'columnItem', text: me.columnsText, cls: Ext.baseCSSPrefix + 'cols-icon', menu: hideableColumns }); } return menuItems; }, // sort asc when clicking on item in menu onSortAscClick: function() { var menu = this.getMenu(), activeHeader = menu.activeHeader; activeHeader.setSortState('ASC'); }, // sort desc when clicking on item in menu onSortDescClick: function() { var menu = this.getMenu(), activeHeader = menu.activeHeader; activeHeader.setSortState('DESC'); }, /** * Returns an array of menu CheckItems corresponding to all immediate children * of the passed Container which have been configured as hideable. */ getColumnMenu: function(headerContainer) { var menuItems = [], i = 0, item, items = headerContainer.query('>gridcolumn[hideable]'), itemsLn = items.length, menuItem; for (; i < itemsLn; i++) { item = items[i]; menuItem = new Ext.menu.CheckItem({ text: item.menuText || item.text, checked: !item.hidden, hideOnClick: false, headerId: item.id, menu: item.isGroupHeader ? this.getColumnMenu(item) : undefined, checkHandler: this.onColumnCheckChange, scope: this }); menuItems.push(menuItem); // If the header is ever destroyed - for instance by dragging out the last remaining sub header, // then the associated menu item must also be destroyed. item.on({ destroy: Ext.Function.bind(menuItem.destroy, menuItem) }); } return menuItems; }, onColumnCheckChange: function(checkItem, checked) { var header = Ext.getCmp(checkItem.headerId); header[checked ? 'show' : 'hide'](); }, /** * Get the columns used for generating a template via TableChunker. * Returns an array of all columns and their * * - dataIndex * - align * - width * - id * - columnId - used to create an identifying CSS class * - cls The tdCls configuration from the Column object * * @private */ getColumnsForTpl: function(flushCache) { var cols = [], headers = this.getGridColumns(flushCache), headersLn = headers.length, i = 0, header, width; for (; i < headersLn; i++) { header = headers[i]; if (header.hidden || header.up('headercontainer[hidden=true]')) { width = 0; } else { width = header.getDesiredWidth(); } cols.push({ dataIndex: header.dataIndex, align: header.align, width: width, id: header.id, cls: header.tdCls, columnId: header.getItemId() }); } return cols; }, /** * Returns the number of grid columns descended from this HeaderContainer. * Group Columns are HeaderContainers. All grid columns are returned, including hidden ones. */ getColumnCount: function() { return this.getGridColumns().length; }, /** * Gets the full width of all columns that are visible. */ getFullWidth: function(flushCache) { var fullWidth = 0, headers = this.getVisibleGridColumns(flushCache), headersLn = headers.length, i = 0, header; for (; i < headersLn; i++) { header = headers[i]; // use headers getDesiredWidth if its there if (header.getDesiredWidth) { fullWidth += header.getDesiredWidth() || 0; // if injected a diff cmp use getWidth } else { fullWidth += header.getWidth(); } } return fullWidth; }, // invoked internally by a header when not using triStateSorting clearOtherSortStates: function(activeHeader) { var headers = this.getGridColumns(), headersLn = headers.length, i = 0; for (; i < headersLn; i++) { if (headers[i] !== activeHeader) { // unset the sortstate and dont recurse headers[i].setSortState(null, true); } } }, /** * Returns an array of the **visible** columns in the grid. This goes down to the lowest column header * level, and does not return **grouped** headers which contain sub headers. * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache. * @returns {Array} */ getVisibleGridColumns: function(refreshCache) { return Ext.ComponentQuery.query(':not([hidden])', this.getGridColumns(refreshCache)); }, /** * Returns an array of all columns which map to Store fields. This goes down to the lowest column header * level, and does not return **grouped** headers which contain sub headers. * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache. * @returns {Array} */ getGridColumns: function(refreshCache) { var me = this, result = refreshCache ? null : me.gridDataColumns; // Not already got the column cache, so collect the base columns if (!result) { me.gridDataColumns = result = []; me.cascade(function(c) { if ((c !== me) && !c.isGroupHeader) { result.push(c); } }); } return result; }, /** * @private * For use by column headers in determining whether there are any hideable columns when deciding whether or not * the header menu should be disabled. */ getHideableColumns: function(refreshCache) { var me = this, result = refreshCache ? null : me.hideableColumns; if (!result) { result = me.hideableColumns = me.query('[hideable]'); } return result; }, /** * Returns the index of a leaf level header regardless of what the nesting * structure is. * * If a group header is passed, the index of the first leaf level heder within it is returned. * * @param {Ext.grid.column.Column} header The header to find the index of * @return {Number} The index of the specified column header */ getHeaderIndex: function(header) { // If we are being asked the index of a group header, find the first leaf header node, and return the index of that if (header.isGroupHeader) { header = header.down(':not([isgroupHeader])'); } return Ext.Array.indexOf(this.getGridColumns(), header); }, /** * Get a leaf level header by index regardless of what the nesting * structure is. * @param {Number} The column index for which to retrieve the column. */ getHeaderAtIndex: function(index) { var columns = this.getGridColumns(); return columns.length ? columns[index] : null; }, /** * When passed a column index, returns the closet *visible* column to that. If the column at the passed index is visible, * that is returned. If it is hidden, either the next visible, or the previous visible column is returned. * @param {Number} index Position at which to find the closest visible column. */ getVisibleHeaderClosestToIndex: function(index) { var result = this.getHeaderAtIndex(index); if (result && result.hidden) { result = result.next(':not([hidden])') || result.prev(':not([hidden])'); } return result; }, /** * Maps the record data to base it on the header id's. * This correlates to the markup/template generated by * TableChunker. */ prepareData: function(data, rowIdx, record, view, panel) { var me = this, obj = {}, headers = me.gridDataColumns || me.getGridColumns(), headersLn = headers.length, colIdx = 0, header, headerId, renderer, value, metaData, store = panel.store; for (; colIdx < headersLn; colIdx++) { metaData = { tdCls: '', style: '' }; header = headers[colIdx]; headerId = header.id; renderer = header.renderer; value = data[header.dataIndex]; if (typeof renderer == "function") { value = renderer.call( header.scope || me.ownerCt, value, // metadata per cell passing an obj by reference so that // it can be manipulated inside the renderer metaData, record, rowIdx, colIdx, store, view ); } // if (metaData.css) { // This warning attribute is used by the compat layer obj.cssWarning = true; metaData.tdCls = metaData.css; delete metaData.css; } // if (me.markDirty) { obj[headerId + '-modified'] = record.isModified(header.dataIndex) ? Ext.baseCSSPrefix + 'grid-dirty-cell' : ''; } obj[headerId+'-tdCls'] = metaData.tdCls; obj[headerId+'-tdAttr'] = metaData.tdAttr; obj[headerId+'-style'] = metaData.style; if (typeof value === 'undefined' || value === null || value === '') { value = header.emptyCellText; } obj[headerId] = value; } return obj; }, expandToFit: function(header) { var view = this.view; if (view) { view.expandToFit(header); } } });