* @class Ext.grid.header.Container
* @extends Ext.container.Container
*
* 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',
uses: [
'Ext.grid.ColumnLayout',
'Ext.grid.column.Column',
'Ext.menu.Menu',
'Ext.menu.CheckItem',
'Ext.menu.Separator',
'Ext.grid.plugin.HeaderResizer',
'Ext.grid.plugin.HeaderReorderer'
],
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. Defaults to 100.
*/
defaultWidth: 100,
sortAscText: 'Sort Ascending',
sortDescText: 'Sort Descending',
sortClearText: 'Clear Sort',
columnsText: 'Columns',
lastHeaderCls: Ext.baseCSSPrefix + 'column-header-last',
firstHeaderCls: Ext.baseCSSPrefix + 'column-header-first',
headerOpenCls: Ext.baseCSSPrefix + 'column-header-open',
// private; will probably be removed by 4.0
triStateSort: false,
ddLock: false,
dragging: false,
* <code>true</code> if this HeaderContainer is in fact a group header which contains sub headers.
* @type Boolean
* @property isGroupHeader
*/
* @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) {
me.resizer = Ext.create('Ext.grid.plugin.HeaderResizer');
me.reorderer = Ext.create('Ext.grid.plugin.HeaderReorderer');
if (!me.enableColumnResize) {
me.resizer.disable();
}
if (!me.enableColumnMove) {
me.reorderer.disable();
}
me.plugins.push(me.reorderer, me.resizer);
}
// Base headers do not need a box layout
if (me.isHeader && !me.items) {
me.layout = 'auto';
}
// HeaderContainer and Group header needs a gridcolumn layout.
else {
me.layout = {
type: 'gridcolumn',
availableSpaceOffset: me.availableSpaceOffset,
align: 'stretchmax',
resetStretch: true
};
}
me.defaults = me.defaults || {};
Ext.applyIf(me.defaults, {
width: me.defaultWidth,
triStateSort: me.triStateSort,
sortable: me.sortable
});
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() {
Ext.destroy(this.resizer, this.reorderer);
this.callParent();
},
// 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;
if (!c.headerId) {
c.headerId = 'h' + (++me.headerCounter);
}
me.callParent(arguments);
me.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;
me.callParent(arguments);
me.purgeCache();
},
afterRender: function() {
this.callParent();
var store = this.up('[store]').store,
sorters = store.sorters,
first = sorters.first(),
hd;
if (first) {
hd = this.down('gridcolumn[dataIndex=' + first.property +']');
if (hd) {
hd.setSortState(first.direction, false, true);
}
}
},
afterLayout: function() {
if (!this.isHeader) {
var me = this,
topHeaders = me.query('>gridcolumn:not([hidden])'),
viewEl,
firstHeaderEl,
lastHeaderEl;
me.callParent(arguments);
if (topHeaders.length) {
firstHeaderEl = topHeaders[0].el;
if (firstHeaderEl !== me.pastFirstHeaderEl) {
if (me.pastFirstHeaderEl) {
me.pastFirstHeaderEl.removeCls(me.firstHeaderCls);
}
firstHeaderEl.addCls(me.firstHeaderCls);
me.pastFirstHeaderEl = firstHeaderEl;
}
lastHeaderEl = topHeaders[topHeaders.length - 1].el;
if (lastHeaderEl !== me.pastLastHeaderEl) {
if (me.pastLastHeaderEl) {
me.pastLastHeaderEl.removeCls(me.lastHeaderCls);
}
lastHeaderEl.addCls(me.lastHeaderCls);
me.pastLastHeaderEl = lastHeaderEl
}
}
}
},
onHeaderShow: function(header) {
// Pass up to the GridSection
var me = this,
gridSection = me.ownerCt,
menu = me.getMenu(),
topItems, topItemsVisible,
colCheckItem,
itemToEnable,
len, i;
if (menu) {
colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
if (colCheckItem) {
colCheckItem.setChecked(true, true);
}
// There's more than one header visible, and we've disabled some checked items... re-enable them
topItems = menu.query('#columnItem>menucheckitem[checked]');
topItemsVisible = topItems.length;
if ((me.getVisibleGridColumns().length > 1) && me.disabledMenuItems && me.disabledMenuItems.length) {
if (topItemsVisible == 1) {
Ext.Array.remove(me.disabledMenuItems, topItems[0]);
}
for (i = 0, len = me.disabledMenuItems.length; i < len; i++) {
itemToEnable = me.disabledMenuItems[i];
if (!itemToEnable.isDestroyed) {
itemToEnable[itemToEnable.menu ? 'enableCheckChange' : 'enable']();
}
}
if (topItemsVisible == 1) {
me.disabledMenuItems = topItems;
} else {
me.disabledMenuItems = [];
}
}
}
// 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 (me.view) {
me.view.onHeaderShow(me, header, true);
}
if (gridSection) {
gridSection.onHeaderShow(me, header);
}
}
me.fireEvent('columnshow', me, header);
// The header's own hide suppresses cascading layouts, so lay the headers out now
me.doLayout();
},
onHeaderHide: function(header, suppressLayout) {
// Pass up to the GridSection
var me = this,
gridSection = me.ownerCt,
menu = me.getMenu(),
colCheckItem;
if (menu) {
// If the header was hidden programmatically, sync the Menu state
colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
if (colCheckItem) {
colCheckItem.setChecked(false, true);
}
me.setDisabledItems();
}
// Only update the UI when we are notified about base level Header hides;
if (!header.isGroupHeader) {
if (me.view) {
me.view.onHeaderHide(me, header, true);
}
if (gridSection) {
gridSection.onHeaderHide(me, header);
}
// The header's own hide suppresses cascading layouts, so lay the headers out now
if (!suppressLayout) {
me.doLayout();
}
}
me.fireEvent('columnhide', me, header);
},
setDisabledItems: function(){
var me = this,
menu = me.getMenu(),
i = 0,
len,
itemsToDisable,
itemToDisable;
// Find what to disable. If only one top level item remaining checked, we have to disable stuff.
itemsToDisable = menu.query('#columnItem>menucheckitem[checked]');
if ((itemsToDisable.length === 1)) {
if (!me.disabledMenuItems) {
me.disabledMenuItems = [];
}
// If down to only one column visible, also disable any descendant checkitems
if ((me.getVisibleGridColumns().length === 1) && itemsToDisable[0].menu) {
itemsToDisable = itemsToDisable.concat(itemsToDisable[0].menu.query('menucheckitem[checked]'));
}
len = itemsToDisable.length;
// Disable any further unchecking at any level.
for (i = 0; i < len; i++) {
itemToDisable = itemsToDisable[i];
if (!Ext.Array.contains(me.disabledMenuItems, itemToDisable)) {
// If we only want to disable check change: it might be a disabled item, so enable it prior to
// setting its correct disablement level.
itemToDisable.disabled = false;
itemToDisable[itemToDisable.menu ? 'disableCheckChange' : 'disable']();
me.disabledMenuItems.push(itemToDisable);
}
}
}
},
* 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) {
this.tempLock();
if (this.view && this.view.rendered) {
this.view.onHeaderResize(header, w, suppressFocus);
}
this.fireEvent('columnresize', this, header, w);
},
onHeaderClick: function(header, e, t) {
this.fireEvent("headerclick", this, header, e, t);
},
onHeaderTriggerClick: function(header, e, t) {
// generate and cache menu, provide ability to cancel/etc
if (this.fireEvent("headertriggerclick", this, header, e, t) !== false) {
this.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.titleContainer.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.titleContainer.removeCls(this.headerOpenCls);
},
moveHeader: function(fromIdx, toIdx) {
// An automatically expiring lock
this.tempLock();
this.onHeaderMoved(this.move(fromIdx, toIdx), fromIdx, toIdx);
},
purgeCache: function() {
var me = this;
// Delete column cache - column order has changed.
delete me.gridDataColumns;
// Menu changes when columns are moved. It will be recreated.
if (me.menu) {
me.menu.destroy();
delete me.menu;
}
},
onHeaderMoved: function(header, fromIdx, toIdx) {
var me = this,
gridSection = me.ownerCt;
if (gridSection) {
gridSection.onHeaderMove(me, header, 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 = Ext.create('Ext.menu.Menu', {
hideOnParentHide: false, // Persists when owning ColumnHeader is hidden
items: me.getMenuItems(),
listeners: {
deactivate: me.onMenuDeactivate,
scope: me
}
});
me.setDisabledItems();
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: 'xg-hmenu-sort-asc',
handler: me.onSortAscClick,
scope: me
},{
itemId: 'descItem',
text: me.sortDescText,
cls: 'xg-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 = Ext.create('Ext.menu.CheckItem', {
text: item.text,
checked: !item.hidden,
hideOnClick: false,
headerId: item.id,
menu: item.isGroupHeader ? this.getColumnMenu(item) : undefined,
checkHandler: this.onColumnCheckChange,
scope: this
});
if (itemsLn === 1) {
menuItem.disabled = true;
}
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) {
width = 0;
} else {
width = header.getDesiredWidth();
// IE6 and IE7 bug.
// Setting the width of the first TD does not work - ends up with a 1 pixel discrepancy.
// We need to increment the passed with in this case.
if ((i == 0) && (Ext.isIE6 || Ext.isIE7)) {
width += 1;
}
}
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 <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.
*/
getFullWidth: function(flushCache) {
var fullWidth = 0,
headers = this.getVisibleGridColumns(flushCache),
headersLn = headers.length,
i = 0;
for (; i < headersLn; i++) {
if (!isNaN(headers[i].width)) {
// use headers getDesiredWidth if its there
if (headers[i].getDesiredWidth) {
fullWidth += headers[i].getDesiredWidth();
// if injected a diff cmp use getWidth
} else {
fullWidth += headers[i].getWidth();
}
}
}
return fullWidth;
},
// invoked internally by a header when not using triStateSorting
clearOtherSortStates: function(activeHeader) {
var headers = this.getGridColumns(),
headersLn = headers.length,
i = 0,
oldSortState;
for (; i < headersLn; i++) {
if (headers[i] !== activeHeader) {
oldSortState = headers[i].sortState;
// unset the sortstate and dont recurse
headers[i].setSortState(null, true);
//if (!silent && oldSortState !== null) {
// this.fireEvent('sortchange', this, headers[i], null);
//}
}
}
},
/**
* Returns an array of the <b>visible</b> columns in the grid. This goes down to the lowest column header
* level, and does not return <i>grouped</i> 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 <i>grouped</i> 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;
},
* Get the index of a leaf level header regardless of what the nesting
* structure is.
*/
getHeaderIndex: function(header) {
var columns = this.getGridColumns();
return Ext.Array.indexOf(columns, header);
},
* Get a leaf level header by index regardless of what the nesting
* structure is.
*/
getHeaderAtIndex: function(index) {
var columns = this.getGridColumns();
return columns[index];
},
* 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 obj = {},
headers = this.gridDataColumns || this.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];
// When specifying a renderer as a string, it always resolves
// to Ext.util.Format
if (typeof renderer === "string") {
header.renderer = renderer = Ext.util.Format[renderer];
}
if (typeof renderer === "function") {
value = renderer.call(
header.scope || this.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
);
}
// <debug>
if (metaData.css) {
// This warning attribute is used by the compat layer
obj.cssWarning = true;
metaData.tdCls = metaData.css;
delete metaData.css;
}
// </debug>
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 (value === undefined || value === null || value === '') {
value = ' ';
}
obj[headerId] = value;
}
return obj;
},
expandToFit: function(header) {
if (this.view) {
this.view.expandToFit(header);
}
}
});