/** * This class is used to contain grid columns at the top-level of a grid as well as a * base class for `Ext.grid.column.Column`. * @since 6.5.0 */Ext.define('Ext.grid.HeaderContainer', { extend: 'Ext.Container', xtype: 'headercontainer', /** * @property {Boolean} * `true` in this class to identify an object this type, or subclass thereof. */ isHeaderContainer: true, config: { /** * @cfg {Ext.grid.column.Column[]} [columns] * The sub columns within this column/header container. */ columns: null, /** * A default {@link #ui ui} to use for {@link Ext.grid.column.Column columns} in * this header. */ defaultColumnUI: null, /** * @cfg {Boolean} sortable * Set this to `false` to disable sorting via tap on all column headers * @private */ sortable: true, // Private /** * @cfg {Boolean} verticalOverflow * Updated by the grid to inform the header container whether it must account for a * vertical scrollbar. * @private */ verticalOverflow: null, /** * @cfg {Boolean} reserveScrollbar * Passed in from the owning grid's own configuration * @private */ reserveScrollbar: null, grid: null }, docked: 'top', defaultType: 'column', layout: { type: 'hbox', align: 'stretch' }, inheritUi: true, scrollable: { x: false, y: false }, weighted: true, autoSize: null, constructor: function(config) { var me = this, isRoot = !me.isGridColumn; me.isRootHeader = isRoot; // Called things need early access to the property if (isRoot) { config.grid._headerContainer = me; } me.columns = []; me.callParent([config]); // Must not prevent the updater from running if (isRoot) { config.grid._headerContainer = null; } }, initialize: function() { var me = this; me.callParent(); // This is the top level HeaderContainer if (me.isRootHeader) { me.setInstanceCls(Ext.baseCSSPrefix + 'headercontainer'); me.on({ tap: 'onHeaderTap', triggertap: 'onHeaderTriggerTap', columnresize: 'onColumnResize', show: 'onColumnShow', hide: 'onColumnHide', sort: 'onColumnSort', scope: me, delegate: '[isLeafHeader]' }); me.on({ tap: 'onGroupTap', triggertap: 'onGroupTriggerTap', show: 'onGroupShow', hide: 'onGroupHide', add: 'onColumnAdd', move: 'onColumnMove', remove: 'onColumnRemove', scope: me, delegate: '[isHeaderGroup]' }); me.on({ add: 'onColumnAdd', move: 'onColumnMove', remove: 'onColumnRemove', scope: me }); } }, // Find the topmost HeaderContainer getRootHeaderCt: function() { var grid = this.getGrid(); return grid && grid.getHeaderContainer(); }, getColumnForField: function(fieldName) { var columns = this.columns, n = columns.length, c, i; for (i = 0; i < n; ++i) { c = columns[i].getColumnForField(fieldName); if (c) { return c; } } return null; }, /** * Returns all the leaf columns, regardless of visibility * @param selector * @returns {Array} */ getColumns: function(selector) { var result = this.columns; if (selector) { if (typeof selector === 'string') { result = Ext.ComponentQuery.query(selector, result); } else if (Ext.isFunction(selector)) { return result.filter(selector); } } return result; }, /** * Returns all visible leaf columns. * @returns {Array} */ getVisibleColumns: function() { var me = this, result = me.visibleColumns; if (!result) { result = me.visibleColumns = me.columns.filter(me.visibleLeafFilter); } return result; }, /** * When passed a column index, or a column, 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. * * If called from a group header, returns the visible index of a leaf level header * relative to the group header with the same stipulations as outlined above. * * @param {Number/Ext.grid.column.Column} index Position at which to find the closest * visible column, or a column for which to find the closest visible sibling. */ getClosestVisibleHeader: function(index) { var result = typeof index === 'number' ? this.getVisibleColumns()[index] : index; if (result && result.hidden) { result = result.next(':visible') || result.prev(':visible'); } return result; }, indexOfLeaf: function(column) { return this.getVisibleColumns().indexOf(column); }, factoryItem: function(item) { var grid = this.getGrid(); if (item.isComponent) { if (item.isGridColumn) { item.setGrid(grid); } } else { item = Ext.apply({ grid: grid }, item); } return this.callParent([item]); }, updateColumns: function(columns) { var me = this; // Only gather the columns array if we are the root header if (me.isRootHeader) { me.columns = []; me.visibleColumns = null; me.add(columns); } }, beginColumnUpdate: function() { var me = this; if (!me.isRootHeader) { return; } me.hasBulkUpdate = me.hasBulkUpdate || 0; me.hasBulkUpdate++; if (me.hasBulkUpdate === 1) { me.bulkAdd = []; // This is called on column add/remove, so disable it // while updatingColumns me.updateMenuDisabledState = Ext.emptyFn; } }, endColumnUpdate: function() { var me = this, length, i, columns, item; if (!me.isRootHeader || !me.hasBulkUpdate) { return; } me.hasBulkUpdate--; if (me.hasBulkUpdate === 0) { columns = me.bulkAdd; length = columns && columns.length; if (length) { me.visibleColumns = null; me.columns = me.getLeaves(); for (i = 0; i < length; i++) { item = columns[i]; item.columnIndex = me.columns.indexOf(item.column); } // we need to sort the columns by their position otherwise the cells will end up in // wrong places Ext.Array.sort(columns, me.sortByColumnIndex); for (i = 0; i < length; i++) { item = columns[i]; me.fireEvent('columnadd', me, item.column, item.columnIndex); } } // refresh the grid innerWidth in one shot me.getGrid().refreshInnerWidth(); me.bulkAdd = null; // Now reassess column menuitem disabled states in one shot. delete me.updateMenuDisabledState; me.updateMenuDisabledState(); // Call func to re-set width on column update end if (this.isRootHeader) { this.onColumnComputedWidthChange(); } } }, sortByColumnIndex: function(a, b) { return a.columnIndex - b.columnIndex; }, add: function(items) { var ret, rootHeaders = this.getRootHeaderCt(); if (rootHeaders) { rootHeaders.beginColumnUpdate(); } ret = this.callParent([items]); if (rootHeaders) { rootHeaders.endColumnUpdate(); } return ret; }, insert: function(index, item) { var ret, rootHeaders = this.getRootHeaderCt(); if (rootHeaders) { rootHeaders.beginColumnUpdate(); } ret = this.callParent([index, item]); if (rootHeaders) { rootHeaders.endColumnUpdate(); } return ret; }, remove: function(which, destroy) { var ret, rootHeaders = this.getRootHeaderCt(); if (rootHeaders) { rootHeaders.beginColumnUpdate(); } ret = this.callParent([which, destroy]); if (rootHeaders) { rootHeaders.endColumnUpdate(); } return ret; }, onColumnAdd: function(container, column) { var me = this, grid = me.getGrid(), groupColumns, ln, i, ui; if (column.isHeaderGroup) { groupColumns = column.getItems().items; for (i = 0, ln = groupColumns.length; i < ln; i++) { me.onColumnAdd(column, groupColumns[i]); } } else { ui = column.getUi(); if (ui == null) { column.setUi(me.getDefaultColumnUI()); } column.setGrid(grid); me.bulkAdd.push({ column: column }); } me.updateMenuDisabledState(); }, onColumnMove: function(parent, column, toIdx, fromIdx) { var me = this, columns = me.columns, group = null, cols; // leaf column set will have to be recalculated. // Must ask for the absolute column index AFTER this. me.visibleColumns = null; if (column.isHeaderGroup) { cols = column.getItems().items; group = column; } else { cols = [column]; } fromIdx = columns.indexOf(cols[0]); me.columns = me.getLeaves(); me.fireEvent('columnmove', me, cols, group, fromIdx); }, onColumnRemove: function(parent, column) { var me = this, columns, i, ln; // leaf column set will have to be recalculated. me.visibleColumns = null; if (column.isHeaderGroup) { if (!column.destroying) { columns = column.getItems().items; ln = columns.length; for (i = 0; i < ln; i++) { me.onColumnRemove(column, columns[i]); } } } else { Ext.Array.remove(me.columns, column); me.fireEvent('columnremove', me, column); } me.updateMenuDisabledState(); }, onHeaderTap: function(column, e) { var selModel = this.getGrid().getSelectable(), ret = this.fireEvent('columntap', this, column, e); if (ret !== false) { if (selModel.onHeaderTap) { selModel.onHeaderTap(this, column, e); } } }, onGroupTriggerTap: function(column) { column.showMenu(); }, onHeaderTriggerTap: function(column) { column.showMenu(); }, onColumnShow: function(column) { var me = this; // leaf column set will have to be recalculated. me.visibleColumns = null; me.fireEvent('columnshow', me, column); me.updateMenuDisabledState(); // Call func to re-set width on column show end if (this.isRootHeader) { this.onColumnComputedWidthChange(); } }, onColumnHide: function(column) { var me = this; // leaf column set will have to be recalculated. me.visibleColumns = null; me.fireEvent('columnhide', me, column); me.updateMenuDisabledState(); // Call func to re-set width on column Hide end if (this.isRootHeader) { this.onColumnComputedWidthChange(); } }, onGroupShow: function(group) { var columns = group.getInnerItems(), ln = columns.length, i, column; // leaf column set will have to be recalculated. this.visibleColumns = null; for (i = 0; i < ln; i++) { column = columns[i]; if (!column.isHidden()) { this.fireEvent('columnshow', this, column); } } this.updateMenuDisabledState(); }, onGroupHide: function(group) { var columns = group.getInnerItems(), ln = columns.length, i, column; // leaf column set will have to be recalculated. this.visibleColumns = null; for (i = 0; i < ln; i++) { column = columns[i]; this.fireEvent('columnhide', this, column); } this.updateMenuDisabledState(); }, onGroupTap: function(column, e) { return this.fireEvent('headergrouptap', this, column, e); }, onColumnResize: function(column, width, oldWidth) { this.fireEvent('columnresize', this, column, width, oldWidth); }, onColumnSort: function(column, direction, newDirection) { if (direction !== null) { this.fireEvent('columnsort', this, column, direction, newDirection); } }, scrollTo: function(x) { this.getScrollable().scrollTo(x); }, updateGrid: function(grid) { if (this.isRootHeader) { this.parent = grid; } }, doDestroy: function() { var me = this, task = me.spacerTask; if (task) { task.cancel(); me.spacerTask = null; } me.setGrid(null); me.callParent(); }, afterRender: function() { this.callParent(); if (this.isRootHeader) { this.onColumnComputedWidthChange(); } }, privates: { columnsResizing: null, // TODO: Account for RTL and then non-RTL switching scrollbars on Safari updateVerticalOverflow: function() { this.syncReserveSpace(); }, // TODO: Account for RTL and then non-RTL switching scrollbars on Safari updateReserveScrollbar: function() { this.syncReserveSpace(); }, /** * Adjusts the checkChangeEnabled state of all column hide/show items based upon * whether it's safe to hide the column. * @private */ updateMenuDisabledState: function() { if (this.rendered) { // eslint-disable-next-line vars-on-top var me = this.isRootHeader ? this : this.getRootHeaderCt(), columns = [], menuOfferingColumns = [], len, i, column, columnIsHideable, checkItem; // Collect columns, and menu offering columns so that we can assess // column hideability on a global level without having to ask each // column to assess its own hideability. // Cannot use CQ because we need to use getConfig with peek flag to // check whether there's a menu without instantiating it. me.visitPreOrder('gridcolumn:not([hidden])', function(col) { columns.push(col); // The :not([hidden]) selector only eliminated immediately hidden columns // If a parent is hidden we still need to check !isHidden(true) if (!col.isHidden(true) && !col.getMenuDisabled() && col.getConfig('menu', true)) { menuOfferingColumns.push(col); } }); len = columns.length; for (i = 0; i < len; ++i) { column = columns[i]; checkItem = column.getHideShowMenuItem(false); // Either call setDisabled or setCheckChangeDisabled if (checkItem) { columnIsHideable = menuOfferingColumns.length > 1 || menuOfferingColumns[0] !== column; checkItem['set' + (checkItem.getMenu() ? 'CheckChange' : '') + 'Disabled']( !columnIsHideable); } } } }, getLeaves: function() { return this.query('[isLeafHeader]'); }, onColumnComputedWidthChange: function(column, computedWidth) { // We are called directly from child columns when their computed width // changes. // // We force all flexed columns to republish their computed width, and // then loop through the rows updating all cells which need to change // width in one pass. var me = this, totalColumnWidth = 0, changedColumns = me.columnsResizing, columns, len, i, c, width; if (me.destroying) { return; } if (changedColumns) { changedColumns.push(column); return; } // Ensure that when those other flexed/relative sized columns publish // their new width, we do not recurse into here. me.columnsResizing = changedColumns = []; columns = me.getColumns(); len = columns.length; // Collect all columns which are changing. // Ensure they update their computed width. // Fire all columnresize events in column order. for (i = 0; i < len; i++) { c = columns[i]; // This is the one that caused the resize; we know its details. if (c === column) { changedColumns.push(c); width = computedWidth; } // Gather all visible column sizes, forcing them to re-evaluate their // computedWidth. If they change. that will recurse into here // and fire their columnresize event, but we will not begin // another column width update (due to me.columnsResizing). else { width = c.isHidden(true) ? 0 : c.measureWidth(); // changedColumns.push(c) will happen if width changes } // Accumulate column width after column width has been synced. totalColumnWidth += width; } totalColumnWidth = Math.floor(totalColumnWidth); me.getGrid().onColumnComputedWidthChange(changedColumns, totalColumnWidth); me.columnsResizing = null; }, setRendered: function(rendered) { // Either way, rendering, or derendering, column set must // be refreshed at next request of columns. this.visibleColumns = null; this.callParent([rendered]); }, /** * @private * Synchronize column UI visible sort state with Store's sorters. */ setSortState: function() { var grid = this.getGrid(), store = grid.getStore(), columns = grid.getColumns(), isGrouped = store.isGrouped(), len = columns && columns.length, sorters = store.getSorters(), grouper = store.getGrouper(), i, header, isGroupedHeader, 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, but only if it has actually been instantiated and saved // by the updater. sorter = header.sorter; // Is this column being used to group this grid isGroupedHeader = store.getGroupField() === header.getDataIndex(); if (sorter) { // FIRST: If the grid is grouped and this is not the column being used to group // it there is no sorting to be done here. You can only sort by the column // that is grouping the grid. // SECOND: If the grid is grouped and this is the column being used to group it // we need to use the grouper as the sorter to update the UI correctly. // THIRD: 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 (isGrouped && !isGroupedHeader) { sorter = null; } else if (isGrouped && isGroupedHeader) { sorter = grouper; } else if (!(sorters.contains(sorter) || grouper === sorter)) { sorter = null; } } // Important: A null sorter will *clear* the UI sort indicator. header.setSortState(sorter); } }, syncReserveSpace: function() { var reserve = this.getVerticalOverflow() || this.getReserveScrollbar(), scrollbarWidth = 0, grid, scroller; if (reserve) { grid = this.getGrid(); if (grid) { scroller = grid.getScrollable(); if (scroller) { scrollbarWidth = scroller.getScrollbarSize().width + 'px'; } } } // use padding, not margin so that the background-color of the header container // shows in the reserved space. this.el.setStyle('padding-right', scrollbarWidth); }, visibleLeafFilter: function(c) { return c.isLeafHeader && !c.isHidden(); } }});