/** * Headercontainer is a docked container (_`top` or `bottom` only_) that holds the * headers ({@link Ext.grid.column.Column grid columns}) of a * {@link Ext.grid.Panel grid} or {@link Ext.tree.Panel tree}. The headercontainer * handles resizing, moving, and hiding columns. As columns are hidden, moved or * resized, the headercontainer triggers changes within the grid or tree's * {@link Ext.view.Table view}. You will not generally need to instantiate this class * directly. * * You may use the * {@link Ext.panel.Table#method-getHeaderContainer getHeaderContainer()} * accessor method to access the tree or grid's headercontainer. * * Grids and trees also have an alias to the two more useful headercontainer methods: * * - **{@link Ext.panel.Table#method-getColumns getColumns}** - aliases * {@link Ext.grid.header.Container#getGridColumns} * - **{@link Ext.panel.Table#method-getVisibleColumns getVisibleColumns}** - aliases * {@link Ext.grid.header.Container#getVisibleGridColumns} */Ext.define('Ext.grid.header.Container', { extend: 'Ext.container.Container', requires: [ 'Ext.grid.ColumnLayout', 'Ext.grid.plugin.HeaderResizer', 'Ext.grid.plugin.HeaderReorderer', 'Ext.util.KeyNav' ], uses: [ 'Ext.grid.column.Column', 'Ext.grid.ColumnManager', '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', /** * @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' * } * } * */ /** * @cfg {String} sortAscText * The text to display in the sort menu to sort items in ascending order. * @locale */ sortAscText: 'Sort Ascending', /** * @cfg {String} sortDescText * The text to display in the sort menu to sort items in descending order. * @locale */ sortDescText: 'Sort Descending', /** * @cfg {String} sortClearText * The text to display in the sort menu to clear the sort order. * @locale */ sortClearText: 'Clear Sort', /** * @cfg {String} columnsText * The text for the columns submenu item. * @locale */ columnsText: 'Columns', headerOpenCls: Ext.baseCSSPrefix + 'column-header-open', menuSortAscCls: Ext.baseCSSPrefix + 'hmenu-sort-asc', menuSortDescCls: Ext.baseCSSPrefix + 'hmenu-sort-desc', menuColsIcon: Ext.baseCSSPrefix + 'cols-icon', blockEvents: false, dragging: false, // May be set to false by a SptreadSheetSelectionModel sortOnClick: true, // Disable FocusableContainer behavior by default, since we only want it // to be enabled for the root header container (we'll set the flag in initComponent) focusableContainer: false, childHideCount: 0, /** * @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, /** * @cfg {Boolean} [enableColumnHide=true] * False to disable column hiding within this grid. */ enableColumnHide: true, /** * @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 */ /** * @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.event.Event} e * @param {HTMLElement} t */ /** * @event headercontextmenu * @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.event.Event} e * @param {HTMLElement} t */ /** * @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.event.Event} e * @param {HTMLElement} t */ /** * @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 */ /** * @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 */ /** * @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 */ /** * @event columnschanged * Fired after the columns change in any way, when a column has been hidden or shown, or when a column * is added to or removed from this header container. * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. */ /** * @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 */ /** * @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 */ /** * @event headermenucreate * Fired immediately after the column header menu is created. * @param {Ext.panel.Table} grid This grid instance * @param {Ext.menu.Menu} menu The Menu that was created * @param {Ext.grid.header.Container} headerCt This header container * @member Ext.panel.Table */ initComponent: function() { var me = this; me.plugins = me.plugins || []; me.defaults = me.defaults || {}; // TODO: Pass in configurations to turn on/off dynamic // resizing and disable resizing all together // Only set up a Resizer and Reorderer for the root HeaderContainer. // Nested Group Headers are themselves HeaderContainers if (!me.isColumn) { me.isRootHeader = true; 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); } } // If this is a leaf column header, and is NOT functioning as a container, // use Container layout with a no-op calculate method. if (me.isColumn && !me.isGroupHeader) { if (!me.items || me.items.length === 0) { me.isContainer = me.isFocusableContainer = false; // Allow overriding via instance config if (!me.hasOwnProperty('focusable')) { me.focusable = true; } me.layout = { type: 'container', calculate: Ext.emptyFn }; } } // HeaderContainer and Group header needs a gridcolumn layout. else { me.layout = Ext.apply({ type: 'gridcolumn', align: 'stretch' }, me.initialConfig.layout); // All HeaderContainers need to know this so that leaf Columns can adjust for cell border width if using content box model me.defaults.columnLines = me.columnLines; // Initialize the root header. if (me.isRootHeader) { // The root header is a FocusableContainer if it's not carrying hidden headers. if (!me.hiddenHeaders) { me.focusableContainer = true; me.ariaRole = 'rowgroup'; } // Create column managers for the root header. me.columnManager = new Ext.grid.ColumnManager(false, me); me.visibleColumnManager = new Ext.grid.ColumnManager(true, me); // In the grid config, if grid.columns is a header container instance and not a columns // config, then it currently has no knowledge of a containing grid. Create the column // manager now and bind it to the grid later in Ext.panel.Table:initComponent(). // // In most cases, though, grid.columns will be a config, so the grid is already known // and the column manager can be bound to it. if (me.grid) { me.grid.columnManager = me.columnManager; me.grid.visibleColumnManager = me.visibleColumnManager; } } else { // Is a group header, also create column managers. me.visibleColumnManager = new Ext.grid.ColumnManager(true, me); me.columnManager = new Ext.grid.ColumnManager(false, me); } } me.menuTask = new Ext.util.DelayedTask(me.updateMenuDisabledState, me); me.callParent(); }, isNested: function () { return !!this.getRootHeaderCt().down('[isNestedParent]'); }, isNestedGroupHeader: function () { // The owner only has one item that isn't hidden and it's me; hide the owner. var header = this, items = header.getRefOwner().query('>:not([hidden])'); return (items.length === 1 && items[0] === header); }, /** * Returns the column's sealed status. * * @return {Boolean} `true` if this column is sealed, `false` otherwise. * * @since 6.5.0 */ isSealed: function() { return !!(this.sealed || this.getInherited().sealed); }, maybeShowNestedGroupHeader: function () { // Group headers are special in that they are auto-hidden when their subheaders are all // hidden and auto-shown when the first subheader is reshown. They are the only headers // that should now be auto-shown or -hidden. // // It follows that since group headers are dictated by some automation depending upon the // state of their child items that all group headers should be shown if anyone in the // hierarchy is shown since these special group headers only contain one child, which is // the next group header in the stack. // This only should apply to the following grouped header scenario: // // +-----------------------------------+ // | Group 1 | // |-----------------------------------| // | Group 2 | // other |-----------------------------------| other // headers | Group 3 | headers // |-----------------------------------| // | Field3 | Field4 | Field5 | Field6 | // |===================================| // | view | // +-----------------------------------+ // var items = this.items, item; if (items && items.length === 1 && (item = items.getAt(0)) && item.hidden) { item.show(); } }, setNestedParent: function (target) { // Here we need to prevent the removal of ancestor group headers from occuring if a flag is set. This // is needed when there are stacked group headers and only the deepest nested group header has leaf items // in its collection. In this specific scenario, the group headers above it only have 1 item, which is its // child nested group header. // // If we don't set this flag, then all of the grouped headers will be recursively removed all the way up to // the root container b/c Ext.grid.header.Container#onRemove will remove all containers that don't contain // any items. // // Note that if an ownerCt only has one item, then we know that this item is the group header that we're // currently dragging. // // Also, note that we mark the owner as the target header because everything up to that should be removed. // // We have to reset any previous headers that may have been target.ownerCts! target.isNestedParent = false; target.ownerCt.isNestedParent = !!(this.ownerCt.items.length === 1 && target.ownerCt.items.length === 1); }, initEvents: function() { var me = this, onHeaderCtEvent, listeners; me.callParent(); // If this is top level, listen for events to delegate to descendant headers. if (!me.isColumn && !me.isGroupHeader) { onHeaderCtEvent = me.onHeaderCtEvent; listeners = { click: onHeaderCtEvent, dblclick: onHeaderCtEvent, contextmenu: onHeaderCtEvent, mousedown: me.onHeaderCtMouseDown, mouseover: me.onHeaderCtMouseOver, mouseout: me.onHeaderCtMouseOut, scope: me }; if (Ext.supports.Touch) { listeners.longpress = me.onHeaderCtLongPress; } me.mon(me.el, listeners); } }, onHeaderCtEvent: function(e, t) { var me = this, headerEl = me.getHeaderElByEvent(e), header, targetEl, activeHeader; if (me.longPressFired) { // if we just showed the menu as a result of a longpress, do not process // the click event and sort the column. me.longPressFired = false; return; } if (headerEl && !me.blockEvents) { header = Ext.getCmp(headerEl.id); if (header) { targetEl = header[header.clickTargetName]; // If there's no possibility that the mouseEvent was on child header items, // or it was definitely in our titleEl, then process it if ((!header.isGroupHeader && !header.isContainer) || e.within(targetEl)) { if (e.type === 'click' || e.type === 'tap') { // The header decides which header to activate on click // on Touch, anywhere in the splitter zone activates // the left header. activeHeader = header.onTitleElClick(e, targetEl, me.sortOnClick); if (activeHeader) { // If activated by touch, there is no trigger el to align with, so align to the header element. me.onHeaderTriggerClick(activeHeader, e, e.pointerType === 'touch' ? activeHeader.el : activeHeader.triggerEl); } else { me.onHeaderClick(header, e, t); } } else if (e.type === 'contextmenu') { me.onHeaderContextMenu(header, e, t); } else if (e.type === 'dblclick') { header.onTitleElDblClick(e, targetEl.dom); } } } } }, blockNextEvent: function() { var me = this; me.blockEvents = true; if (!me.unblockTimer) { me.unblockTimer = Ext.asap(me.unblockEvents, me); } }, unblockEvents: function() { this.blockEvents = this.unblockTimer = false; }, onHeaderCtMouseDown: function(e, target) { var targetCmp = Ext.Component.from(target), cols, i, len, scrollable, col; if (!e.defaultPrevented && targetCmp !== this) { // The DDManager (Header Containers are draggable) prevents mousedown default // So we must explicitly focus the header if (targetCmp.isGroupHeader) { cols = targetCmp.getVisibleGridColumns(); scrollable = this.getScrollable(); for (i = 0, len = cols.length; i < len; ++i) { col = cols[i]; if (scrollable.doIsInView(col.el, true).x) { targetCmp = col; break; } } } targetCmp.focus(); } }, onHeaderCtMouseOver: function(e, t) { var headerEl, header, targetEl; // Only proces the mouse entering this HeaderContainer. // From header to header, and exiting this HeaderContainer we track using mouseout events. if (!e.within(this.el, true)) { headerEl = e.getTarget('.' + Ext.grid.column.Column.prototype.baseCls); header = headerEl && Ext.getCmp(headerEl.id); if (header) { targetEl = header[header.clickTargetName]; if (e.within(targetEl)) { header.onTitleMouseOver(e, targetEl.dom); } } } }, onHeaderCtMouseOut: function(e, t) { var headerSelector = '.' + Ext.grid.column.Column.prototype.baseCls, outHeaderEl = e.getTarget(headerSelector), inHeaderEl = e.getRelatedTarget(headerSelector), header, targetEl; // It's a mouseenter/leave, not an internal element change within a Header if (outHeaderEl !== inHeaderEl) { if (outHeaderEl) { header = Ext.getCmp(outHeaderEl.id); if (header) { targetEl = header[header.clickTargetName]; header.onTitleMouseOut(e, targetEl.dom); } } if (inHeaderEl) { header = Ext.getCmp(inHeaderEl.id); if (header) { targetEl = header[header.clickTargetName]; header.onTitleMouseOver(e, targetEl.dom); } } } }, onHeaderCtLongPress: function(e) { var me = this, headerEl = me.getHeaderElByEvent(e), header; // Might be outside the headers. if (headerEl) { header = Ext.getCmp(headerEl.id); if (header && !header.menuDisabled) { me.longPressFired = true; me.showMenuBy(e, headerEl, header); } } }, getHeaderElByEvent: function(e) { return e.getTarget('.' + Ext.grid.column.Column.prototype.baseCls); }, isLayoutRoot: function(){ // Since we're docked, the width is always calculated // If we're hidden, the height is explicitly 0, which // means we'll be considered a layout root. However, we // still need the view to layout to update the underlying // table to match the size. if (this.hiddenHeaders) { return false; } return this.callParent(); }, // Find the topmost HeaderContainer getRootHeaderCt: function() { return this.isRootHeader ? this : this.up('[isRootHeader]'); }, doDestroy: function() { var me = this; if (me.menu) { me.menu.un('hide', me.onMenuHide, me); } Ext.unasap(me.unblockTimer); me.menuTask.cancel(); Ext.destroy(me.visibleColumnManager, me.columnManager, me.menu); me.callParent(); }, removeAll: function(autoDestroy) { var me = this; // fire a single columnschanged event after all removes have been made me.suspendEvent('columnschanged'); me.callParent([autoDestroy]); me.resumeEvent('columnschanged'); me.fireEvent('columnschanged', me); }, applyColumnsState: function(columnsState, storeState) { if (!columnsState) { return; } var me = this, items = me.items.items, count = items.length, i = 0, moved = false, newOrder = [], newCols = [], length, col, columnState, index; me.purgeCache(); for (i = 0; i < count; i++) { col = items[i]; columnState = columnsState[col.getStateId()]; // There's a column state for this column. // Add it to the newOrder array at the specified index if (columnState) { index = columnState.index; newOrder[index] = col; if (i !== index) { moved = true; } if (col.applyColumnState) { col.applyColumnState(columnState, storeState); } } // A new column. // It must be inserted at this index after state restoration, else { newCols.push({ index: i, column: col }); } } // If any saved columns were missing, close the gaps where they were newOrder = Ext.Array.clean(newOrder); // New column encountered. // Insert them into the newOrder at their configured position length = newCols.length; if (length) { for (i = 0; i < length; i++) { columnState = newCols[i]; index = columnState.index; if (index < newOrder.length) { moved = true; Ext.Array.splice(newOrder, index, 0, columnState.column); } else { newOrder.push(columnState.column); } } } if (moved) { // This flag will prevent the groupheader from being removed by its owner when it (temporarily) has no child items. me.applyingState = true; me.removeAll(false); delete me.applyingState; me.add(newOrder); } }, 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, rootHeader; //<debug> var stateId = c.getStateId(); if (stateId != null) { if (!me._usedIDs) { me._usedIDs = {}; } if (me._usedIDs[stateId] && me._usedIDs[stateId] !== c) { Ext.log.warn(this.$className + ' attempted to reuse an existing id: ' + stateId); } me._usedIDs[stateId] = c; } //</debug> me.callParent(arguments); rootHeader = me.getRootHeaderCt(); me.onHeadersChanged(c, rootHeader && rootHeader.isDDMoveInGrid); }, move: function(fromIdx, toIdx) { var me = this, items = me.items, headerToMove; if (fromIdx.isComponent) { headerToMove = fromIdx; fromIdx = items.indexOf(headerToMove); } else { headerToMove = items.getAt(fromIdx); } // Take real grid column index of column being moved headerToMove.visibleFromIdx = me.getRootHeaderCt().visibleColumnManager.indexOf(headerToMove); me.callParent(arguments); }, onMove: function(headerToMove, fromIdx, toIdx) { var me = this, gridHeaderCt = me.getRootHeaderCt(), gridVisibleColumnManager = gridHeaderCt.visibleColumnManager, numColsToMove = 1, visibleToIdx; // Purges cache so that indexOf returns new position of header me.onHeadersChanged(headerToMove, true); visibleToIdx = gridVisibleColumnManager.indexOf(headerToMove); if (visibleToIdx >= headerToMove.visibleFromIdx) { visibleToIdx++; } me.callParent(arguments); // If what is being moved is a group header, then pass the correct column count if (headerToMove.isGroupHeader) { numColsToMove = headerToMove.visibleColumnManager.getColumns().length; } gridHeaderCt.onHeaderMoved(headerToMove, numColsToMove, headerToMove.visibleFromIdx, visibleToIdx); }, // @private maybeContinueRemove: function () { var me = this; // Note that if the column is a group header and is the current target of a drag, we don't want to remove it // if it since it could be one of any number of (empty) nested group headers. // See #isNested. // // There are also other scenarios in which the remove should not occur. For instance, when applying column // state to a groupheader, the subheaders are all removed before being re-added in their stateful order, // and the groupheader should not be removed in the meantime. // See EXTJS-17577. return (me.isGroupHeader && !me.applyingState) && !me.isNestedParent && me.ownerCt && !me.items.getCount(); }, // 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, isDestroying) { var me = this, ownerCt = me.ownerCt; me.callParent([c, isDestroying]); //<debug> if (!me._usedIDs) { me._usedIDs = {}; } delete me._usedIDs[c.headerId]; //</debug> if (!me.destroying) { // isDDMoveInGrid flag set by Ext.grid.header.DropZone when moving into another container *within the same grid*. // This stops header change processing from being executed twice, once on remove and then on the subsequent add. if (!me.getRootHeaderCt().isDDMoveInGrid) { me.onHeadersChanged(c, false); } if (me.maybeContinueRemove()) { // Detach the header from the DOM here. Since we're removing and destroying the container, // the inner DOM may get overwritten, since Container::deatchOnRemove gets processed after // onRemove. if (c.rendered) { c.detachFromBody(); } // If we don't have any items left and we're a group, remove ourselves. // This will cascade up if necessary. DO NOT destroy ourselves here, // we have to defer that until all moves are done and events are fired. me.destroyAfterRemoving = true; Ext.suspendLayouts(); ownerCt.remove(me, false); Ext.resumeLayouts(true); } } }, // Private // Called to clear all caches of columns whenever columns are added, removed to just moved. // We need to be informed if it's just a move operation so that we don't call the heavier // grid.onHeadersChanged which refreshes the view. // The onMove handler ensures that grid.inHeaderMove is called which just swaps cells. onHeadersChanged: function(c, isMove) { var gridPanel, gridHeaderCt = this.getRootHeaderCt(); // Each HeaderContainer up the chain must have its cache purged so that its getGridColumns method will return correct results. this.purgeHeaderCtCache(this); if (gridHeaderCt) { gridHeaderCt.onColumnsChanged(); gridPanel = gridHeaderCt.ownerCt; // The grid needs to be informed even if the added/removed column is a group header // If it an add or remove operation causing this header change call, then inform the grid which refreshes. // Moving calls the onHeaderMoved method of the grid which just swaps cells. if (gridPanel && !isMove) { gridPanel.onHeadersChanged(gridHeaderCt, c); } } }, // Private onHeaderMoved: function(header, colsToMove, fromIdx, toIdx) { var me = this, gridSection = me.ownerCt; if (me.rendered) { if (gridSection && gridSection.onHeaderMove) { gridSection.onHeaderMove(me, header, colsToMove, fromIdx, toIdx); } me.fireEvent('columnmove', me, header, fromIdx, toIdx); } }, // Private // Only called on the grid's headerCt. // Called whenever a column is added or removed or moved at any level below. // Ensures that the gridColumns caches are cleared. onColumnsChanged: function() { var me = this, menu = me.menu, columnItemSeparator, columnItem; if (me.rendered) { me.fireEvent('columnschanged', me); // Column item (and its associated menu) menu has to be destroyed (if it exits) when columns are changed. // It will be recreated just before the main container menu is next shown. if (menu) { columnItemSeparator = menu.child('#columnItemSeparator'); columnItem = menu.child('#columnItem'); // Destroy the column visibility items // They will be recreated before the next show if (columnItemSeparator) { columnItemSeparator.destroy(); } if (columnItem) { columnItem.destroy(); } } } }, /** * @private */ lookupComponent: function(comp) { var result = 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. // Test whether width is undefined so that width: null can be used to have the header shrinkwrap its text. if (!result.isGroupHeader && result.width === undefined && !result.flex) { result.width = this.defaultWidth; } return result; }, /** * @private * Synchronize column UI visible sort state with Store's sorters. */ setSortState: function() { var store = this.up('[store]').store, columns = this.visibleColumnManager.getColumns(), len = columns.length, i, header, sorter; for (i = 0; i < len; i++) { header = columns[i]; // Access the column's custom sorter in preference to one keyed on the data index. sorter = header.getSorter(); if (sorter) { // If the column was configured with a sorter, we must check that the sorter // is part of the store's sorter collection to update the UI to the correct state. // The store may not actually BE sorted by that sorter. if (!store.getSorters().contains(sorter)) { sorter = null; } } else { sorter = store.getSorters().get(header.getSortParam()); } // Important: A null sorter for this column will *clear* the UI sort indicator. header.setSortState(sorter); } }, 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; // Invalidate column collections upon column hide/show me.purgeHeaderCtCache(header.ownerCt); 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 if (menu.isVisible()) { me.menuTask.delay(50); } } }, updateMenuDisabledState: function(menu) { var me = this, columns = me.query('gridcolumn:not([hidden])'), i, len = columns.length, item, checkItem, method; // If called from menu creation, it will be passed to avoid infinite recursion if (!menu) { menu = me.getMenu(); } for (i = 0; i < len; ++i) { item = columns[i]; checkItem = me.getMenuItemForHeader(menu, item); if (checkItem) { method = item.isHideable() ? 'enable' : 'disable'; if (checkItem.menu) { method += 'CheckChange'; } checkItem[method](); } } }, getMenuItemForHeader: function(menu, header) { return header ? menu.down('menucheckitem[headerId=' + header.id + ']') : null; }, onHeaderShow: function (header) { var me = this, ownerCt = me.ownerCt, lastHiddenHeader = header.lastHiddenHeader; if (!ownerCt) { return; } if (me.forceFit) { delete me.flex; } // If lastHiddenHeader exists we know that header is a groupHeader and if all its subheaders // are hidden then we need to show the last one that was hidden. if (lastHiddenHeader && !header.query('[hidden=false]').length) { lastHiddenHeader.show(); header.lastHiddenHeader = null; } me.onHeaderVisibilityChange(header, true); ownerCt.onHeaderShow(me, header); me.fireEvent('columnshow', me, header); me.fireEvent('columnschanged', this); }, onHeaderHide: function (header) { var me = this, ownerCt = me.ownerCt; if (!ownerCt) { return; } me.onHeaderVisibilityChange(header, false); ownerCt.onHeaderHide(me, header); me.fireEvent('columnhide', me, header); me.fireEvent('columnschanged', this); }, onHeaderResize: function(header, w) { var me = this, gridSection = me.ownerCt; if (gridSection) { gridSection.onHeaderResize(me, header, w); } me.fireEvent('columnresize', me, header, w); }, onHeaderClick: function(header, e, t) { var me = this, selModel = header.getView().getSelectionModel(), ret; header.fireEvent('headerclick', me, header, e, t); ret = me.fireEvent('headerclick', me, header, e, t); if (ret !== false) { if (selModel.onHeaderClick) { selModel.onHeaderClick(me, header, e); } } return ret; }, onHeaderContextMenu: function(header, e, t) { header.fireEvent('headercontextmenu', this, header, e, t); this.fireEvent('headercontextmenu', this, header, e, t); }, onHeaderTriggerClick: function(header, e, t) { var me = this; if (header.fireEvent('headertriggerclick', me, header, e, t) !== false && me.fireEvent('headertriggerclick', me, header, e, t) !== false) { // If menu is already active... if (header.activeMenu) { // Click/tap toggles the menu visibility. if (e.pointerType) { header.activeMenu.hide(); } else { header.activeMenu.focus(); } } else { me.showMenuBy(e, t, header); } } }, /** * @private * * Shows the column menu under the target element passed. This method is used when the trigger element on the column * header is clicked on and rarely should be used otherwise. * * @param {Ext.event.Event} [clickEvent] The event which triggered the current handler. If omitted * or a key event, the menu autofocuses its first item. * @param {HTMLElement/Ext.dom.Element} t The target to show the menu by * @param {Ext.grid.header.Container} header The header container that the trigger was clicked on. */ showMenuBy: function(clickEvent, t, header) { var menu = this.getMenu(), ascItem = menu.down('#ascItem'), descItem = menu.down('#descItem'), sortableMth, isTouch = clickEvent && clickEvent.pointerType === 'touch'; // Use ownerCmp as the upward link. Menus *must have no ownerCt* - they are global floaters. // Upward navigation is done using the up() method. menu.activeHeader = menu.ownerCmp = header; header.setMenuActive(menu); // 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](); } // Pointer-invoked menus do not auto focus, key invoked ones do. menu.autoFocus = !clickEvent || clickEvent.keyCode; // For longpress t is the header, for click/hover t is the trigger menu.showBy(t, 'tl-bl?'); // Menu show was vetoed by event handler - clear context if (!menu.isVisible()) { this.onMenuHide(menu); } }, hideMenu: function() { if (this.menu) { this.menu.hide(); } }, // remove the trigger open class when the menu is hidden onMenuHide: function(menu) { menu.activeHeader.setMenuActive(false); }, purgeHeaderCtCache: function (headerCt) { while (headerCt) { headerCt.purgeCache(); if (headerCt.isRootHeader) { return; } headerCt = headerCt.ownerCt; } }, purgeCache: function() { var me = this, visibleColumnManager = me.visibleColumnManager, columnManager = me.columnManager; // Delete column cache - column order has changed. me.gridVisibleColumns = me.gridDataColumns = me.hideableColumns = null; // ColumnManager. Only the top if (visibleColumnManager) { visibleColumnManager.invalidate(); columnManager.invalidate(); } }, /** * Gets the menu (and will create it if it doesn't already exist) * @private */ getMenu: function() { var me = this, grid = me.view && me.view.ownerGrid; if (!me.menu) { me.menu = new Ext.menu.Menu({ hideOnParentHide: false, // Persists when owning ColumnHeader is hidden items: me.getMenuItems(), listeners: { beforeshow: me.beforeMenuShow, hide: me.onMenuHide, scope: me } }); me.fireEvent('menucreate', me, me.menu); if (grid) { grid.fireEvent('headermenucreate', grid, me.menu, me); } } return me.menu; }, // Render our menus to the first enclosing scrolling element so that they scroll with the grid beforeMenuShow: function(menu) { var me = this, columnItem = menu.child('#columnItem'), hideableColumns, insertPoint; // If a change of column structure caused destruction of the column menu item // or the main menu was created without the column menu item because it began with no hideable headers // Then create it and its menu now. if (!columnItem) { hideableColumns = me.enableColumnHide ? me.getColumnMenu(me) : null; // Insert after the "Sort Ascending", "Sort Descending" menu items if they are present. insertPoint = me.sortable ? 2 : 0; if (hideableColumns && hideableColumns.length) { menu.insert(insertPoint, [{ itemId: 'columnItemSeparator', xtype: 'menuseparator' }, { itemId: 'columnItem', text: me.columnsText, iconCls: me.menuColsIcon, menu: { items: hideableColumns }, hideOnClick: false }]); } } me.updateMenuDisabledState(me.menu); // TODO: rendering the menu to the nearest overlfowing ancestor has been disabled // since DomQuery is no longer available by default in 5.0 // if (!menu.rendered) { // menu.render(this.el.up('{overflow=auto}') || document.body); // } }, /** * Returns an array of menu items to be placed into the shared menu * across all headers in this header container. * @return {Array} menuItems */ getMenuItems: function() { var me = this, menuItems = [], hideableColumns = me.enableColumnHide ? me.getColumnMenu(me) : null; if (me.sortable) { menuItems = [{ itemId: 'ascItem', text: me.sortAscText, iconCls: me.menuSortAscCls, handler: me.onSortAscClick, scope: me },{ itemId: 'descItem', text: me.sortDescText, iconCls: me.menuSortDescCls, handler: me.onSortDescClick, scope: me }]; } if (hideableColumns && hideableColumns.length) { if (me.sortable) { menuItems.push({ itemId: 'columnItemSeparator', xtype: 'menuseparator' }); } menuItems.push({ itemId: 'columnItem', text: me.columnsText, iconCls: me.menuColsIcon, menu: hideableColumns, hideOnClick: false }); } return menuItems; }, // sort asc when clicking on item in menu onSortAscClick: function() { var menu = this.getMenu(), activeHeader = menu.activeHeader; activeHeader.sort('ASC'); }, // sort desc when clicking on item in menu onSortDescClick: function() { var menu = this.getMenu(), activeHeader = menu.activeHeader; activeHeader.sort('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); } // Prevent creating a submenu if we have no items return menuItems.length ? menuItems : null; }, onColumnCheckChange: function(checkItem, checked) { var header = Ext.getCmp(checkItem.headerId); if (header.rendered) { header[checked ? 'show' : 'hide'](); } else { header.hidden = !checked; } }, /** * Returns the number of <b>grid columns</b> 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 for setting width of tables. */ getTableWidth: function() { var fullWidth = 0, headers = this.getVisibleGridColumns(), headersLn = headers.length, i; for (i = 0; i < headersLn; i++) { fullWidth += headers[i].getCellWidth() || 0; } return fullWidth; }, /** * 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. * * See also {@link Ext.grid.header.Container#getGridColumns} * @return {Ext.grid.column.Column[]} columns An array of visible columns. Returns * an empty array if no visible columns are found. */ getVisibleGridColumns: function() { var me = this, allColumns, rootHeader, result, len, i, column; if (me.gridVisibleColumns) { return me.gridVisibleColumns; } allColumns = me.getGridColumns(); rootHeader = me.getRootHeaderCt(); result = []; len = allColumns.length; // Use an inline check instead of ComponentQuery filtering for better performance for // repeated grid row rendering - as in buffered rendering. for (i = 0; i < len; i++) { column = allColumns[i]; if (!column.hidden && !column.isColumnHidden(rootHeader)) { result[result.length] = column; } } me.gridVisibleColumns = result; return result; }, isColumnHidden: function(rootHeader) { var owner = this.getRefOwner(); while (owner && owner !== rootHeader) { if (owner.hidden) { return true; } owner = owner.getRefOwner(); } return false; }, /** * @method getGridColumns * Returns an array of all columns which exist in the grid's View, visible or not. * This goes down to the leaf column header level, and does not return **grouped** * headers which contain sub headers. * * It includes hidden headers even though they are not rendered. This is for * collection of menu items for the column hide/show menu. * * Headers which have a hidden ancestor have a `hiddenAncestor: true` property * injected so that descendants are known to be hidden without interrogating that * header's ownerCt axis for a hidden ancestor. * * See also {@link Ext.grid.header.Container#getVisibleGridColumns} * @return {Ext.grid.column.Column[]} columns An array of columns. Returns an * empty array if no columns are found. */ getGridColumns: function(/* private - used in recursion*/inResult, hiddenAncestor) { if (!inResult && this.gridDataColumns) { return this.gridDataColumns; } var me = this, result = inResult || [], items, i, len, item, lastVisibleColumn; hiddenAncestor = hiddenAncestor || me.hidden; if (me.items) { items = me.items.items; // An ActionColumn (Columns extend HeaderContainer) may have an items *array* being the action items that it renders. if (items) { for (i = 0, len = items.length; i < len; i++) { item = items[i]; if (item.isGroupHeader) { // Group headers will need a visibleIndex for if/when they're removed from their owner. // See Ext.layout.container.Container#moveItemBefore. item.visibleIndex = result.length; item.getGridColumns(result, hiddenAncestor); } else { item.hiddenAncestor = hiddenAncestor; result.push(item); } } } } if (!inResult) { me.gridDataColumns = result; } // If top level, correct first and last visible column flags if (!inResult && len) { // Set firstVisible and lastVisible flags for (i = 0, len = result.length; i < len; i++) { item = result[i]; // The column index within all (visible AND hidden) leaf level columns. // Used as the cellIndex in TableView's cell renderer call item.fullColumnIndex = i; item.isFirstVisible = item.isLastVisible = false; if (!(item.hidden || item.hiddenAncestor)) { if (!lastVisibleColumn) { item.isFirstVisible = true; } lastVisibleColumn = item; } } // If we haven't hidden all columns, tag the last visible one encountered if (lastVisibleColumn) { lastVisibleColumn.isLastVisible = true; } } 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() { var me = this, result = 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 header 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) { // Binding the columnManager to a column makes it backwards-compatible with versions // that only bind the columnManager to a root header. if (!this.columnManager) { this.columnManager = this.getRootHeaderCt().columnManager; } return this.columnManager.getHeaderIndex(header); }, /** * Get a leaf level header by index regardless of what the nesting * structure is. * * @param {Number} index The column index for which to retrieve the column. */ getHeaderAtIndex: function (index) { // Binding the columnManager to a column makes it backwards-compatible with versions // that only bind the columnManager to a root header. if (!this.columnManager) { this.columnManager = this.getRootHeaderCt().columnManager; } return this.columnManager.getHeaderAtIndex(index); }, /** * 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) { // Binding the columnManager to a column makes it backwards-compatible with versions // that only bind the columnManager to a root header. if (!this.visibleColumnManager) { this.visibleColumnManager = this.getRootHeaderCt().visibleColumnManager; } return this.visibleColumnManager.getVisibleHeaderClosestToIndex(index); }, applyForceFit: function (header) { var me = this, view = me.view, minWidth = Ext.grid.plugin.HeaderResizer.prototype.minColWidth, // Used when a column's max contents are larger than the available view width. useMinWidthForFlex = false, defaultWidth = Ext.grid.header.Container.prototype.defaultWidth, availFlex = me.el.dom.clientWidth - (view.el.dom.scrollHeight > view.el.dom.clientHeight ? Ext.getScrollbarSize().width : 0), totalFlex = 0, items = me.getVisibleGridColumns(), hidden = header.hidden, len, i, item, maxAvailFlexOneColumn, myWidth; function getTotalFlex() { for (i = 0, len = items.length; i < len; i++) { item = items[i]; // Skip the current header. if (item === header) { continue; } item.flex = item.flex || item.width || item.getWidth(); totalFlex += item.flex; item.width = null; } } function applyWidth() { // The currently-sized column (whether resized or reshown) will already // have a width, so all other columns will need to be flexed. var isCurrentHeader; for (i = 0, len = items.length; i < len; i++) { item = items[i]; isCurrentHeader = (item === header); if (useMinWidthForFlex && !isCurrentHeader) { // The selected column is extremely large so set all the others as flex minWidth. item.flex = minWidth; item.width = null; } else if (!isCurrentHeader) { // Note that any widths MUST be converted to flex. Imagine that all but one columns // are hidden. The widths of each column very easily could be greater than the total // available width (think about the how visible header widths increase as sibling // columns are hidden), so they cannot be reliably used to size the header, and the only // safe approach is to convert any all widths to flex (except for the current header). myWidth = item.flex || defaultWidth; item.flex = Math.max(Math.ceil((myWidth / totalFlex) * availFlex), minWidth); item.width = null; } item.setWidth(item.width || item.flex); } } Ext.suspendLayouts(); // Determine the max amount of flex that a single column can have. maxAvailFlexOneColumn = (availFlex - ((items.length + 1) * minWidth)); // First, remove the header's flex as it should always receive a set width // since it is the header being operated on. header.flex = null; if (hidden) { myWidth = header.width || header.savedWidth || Math.floor(maxAvailFlexOneColumn/(items.length + 1)); header.savedWidth = null; } else { myWidth = view.getMaxContentWidth(header); } // We need to know if the max content width of the selected column would blow out the // grid. If so, all the other visible columns will be flexed to minWidth. if (myWidth > maxAvailFlexOneColumn) { header.width = maxAvailFlexOneColumn; useMinWidthForFlex = true; } else { header.width = myWidth; // Substract the current header's width from the available flex + some padding // to ensure that the last column doesn't get nudged out of the view. availFlex -= myWidth + defaultWidth; getTotalFlex(); } applyWidth(); Ext.resumeLayouts(true); }, autoSizeColumn: function (header) { var view = this.view; if (view) { view.autoSizeColumn(header); if (this.forceFit) { this.applyForceFit(header); } } }, getRefItems: function(deep) { // Override to include the header menu in the component tree var result = this.callParent([deep]); if (this.menu) { result.push(this.menu); } return result; }, initInheritedState: function(inheritedState, inheritedStateInner) { if (this.sealed) { inheritedState.sealed = true; } this.callParent([inheritedState, inheritedStateInner]); }, privates: { beginChildHide: function() { ++this.childHideCount; }, endChildHide: function() { --this.childHideCount; }, getFocusables: function() { return this.isRootHeader ? this.getVisibleGridColumns() : this.items.items; }, initFocusableContainerKeyNav: function(el) { var me = this; if (!me.focusableKeyNav) { me.focusableKeyNav = new Ext.util.KeyNav({ target: el, scope: me, down: me.showHeaderMenu, left: me.onFocusableContainerLeftKey, right: me.onFocusableContainerRightKey, home: me.onHomeKey, end: me.onEndKey, space: me.onHeaderActivate, enter: me.onHeaderActivate }); } }, onHomeKey: function(e) { return this.focusChild(null, true, e); }, onEndKey: function(e) { return this.focusChild(null, false, e); }, showHeaderMenu: function(e) { var column = this.getFocusableFromEvent(e); // DownArrow event must be from a column, not a Component within the column (eg filter fields) if (column && column.isColumn && column.triggerEl) { this.onHeaderTriggerClick(column, e, column.triggerEl); } }, onHeaderActivate: function(e) { var column = this.getFocusableFromEvent(e), view, lastFocused; // Remember that not every descendant of a headerCt is a column! It could be a child component of a column. if (column && column.isColumn) { view = column.getView(); // Sort the column is configured that way. // sortOnClick may be set to false by SpreadsheelSelectionModel to allow click to select a column. if (column.sortable && this.sortOnClick) { lastFocused = view.getNavigationModel().getLastFocused(); column.toggleSortState(); // After keyboard sort, bring last focused record into view if (lastFocused) { view.ownerCt.ensureVisible(lastFocused.record); } } else if (e.getKey() === e.SPACE) { column.onTitleElClick(e, e.target, this.sortOnClick); } // onHeaderClick is a necessary part of accessibility processing, sortable or not. return this.onHeaderClick(column, e, column.el); } }, onOwnerGridReconfigure: function(storeChanged, columnsChanged) { var me = this; if (!me.rendered || me.destroying || me.destroyed) { return; } // Adding or removing columns during reconfiguration could result // in changed FocusableContainer state. if (storeChanged || columnsChanged) { if (Ext.Component.layoutSuspendCount) { me.$initFocusableContainerAfterLayout = true; } else { me.initFocusableContainer(); } } } }});