/**
* @class Ext.grid.HeaderContainer
* @extends Ext.container.Container
*
* Container which holds healders and is docked at the top or bottom of a grid
* section. The HeaderContainer drives resizing/moving/hiding of columns within
* the gridview. As headers are hidden, moved or resized the headercontainer is
* responsible for triggering changes within the view.
*
* @xtype headercontainer
*/
Ext.define('Ext.grid.HeaderContainer', {
extend: 'Ext.container.Container',
requires: [
'Ext.grid.ColumnLayout',
'Ext.grid.Header',
'Ext.menu.Menu',
'Ext.menu.CheckItem',
'Ext.menu.Separator',
'Ext.grid.HeaderResizer',
'Ext.grid.HeaderReorderer'
],
alias: 'widget.headercontainer',
cls: Ext.baseCSSPrefix + 'grid-header-ct',
dock: 'top',
height: 23,
defaultType: 'gridheader',
/**
* @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',
lastCellCls: Ext.baseCSSPrefix + 'grid-cell-last',
firstCellCls: Ext.baseCSSPrefix + 'grid-cell-first',
// private; will probably be removed by 4.0
triStateSort: false,
locked: false,
initComponent: function() {
this.plugins = this.plugins || [];
// TODO: Pass in configurations to turn on/off dynamic
// resizing and disable resizing all together
var resizer = new Ext.grid.HeaderResizer(),
reorderer = new Ext.grid.HeaderReorderer();
// Order is important here in IE, as the Reorderer and Resizer fight
// for drag and drop events
if (Ext.isIE) {
this.plugins.push(reorderer, resizer);
} else {
this.plugins.push(resizer, reorderer);
}
this.layout = {
type: 'gridcolumn',
align: 'stretchmax'
};
this.defaults = this.defaults || {};
Ext.applyIf(this.defaults, {
width: this.defaultWidth,
triStateSort: this.triStateSort
});
Ext.grid.HeaderContainer.superclass.initComponent.call(this);
this.addEvents(
/**
* @event headerresize
* @param {Ext.HeaderContainer} ct
* @param {Ext.Header} header
* @param {Number} width
*/
'headerresize',
/**
* @event headerclick
* @param {Ext.HeaderContainer} ct
* @param {Ext.Header} header
* @param {Ext.EventObject} e
* @param {HTMLElement} t
*/
'headerclick',
/**
* @event headerclick
* @param {Ext.HeaderContainer} ct
* @param {Ext.Header} header
* @param {Ext.EventObject} e
* @param {HTMLElement} t
*/
'headertriggerclick',
/**
* @event headermove
* @param {Ext.HeaderContainer} ct
* @param {Ext.Header} header
* @param {Number} fromIdx
* @param {Number} toIdx
*/
'headermove'
);
},
afterRender: function() {
Ext.grid.HeaderContainer.superclass.afterRender.apply(this, arguments);
var store = this.up('gridpanel').store,
sorters = store.sorters,
first = sorters.first(),
hd;
if (first) {
hd = this.down('gridheader[dataIndex=' + first.property +']');
hd.setSortState(first.direction);
}
},
afterLayout: function() {
Ext.grid.HeaderContainer.superclass.afterLayout.apply(this, arguments);
var headers = this.query('gridheader:not(gridheader[hidden])'),
viewEl;
headers[0].el.radioCls(this.firstHeaderCls);
headers[headers.length - 1].el.radioCls(this.lastHeaderCls);
// Maintain First and Last cell cls
if (this.view) {
viewEl = this.view.el;
viewEl.select('.'+this.firstCellCls).removeCls(this.firstCellCls);
viewEl.select('.'+this.lastCellCls).removeCls(this.lastCellCls);
viewEl.select(headers[0].getCellSelector()).addCls(this.firstCellCls);
viewEl.select(headers[headers.length - 1].getCellSelector()).addCls(this.lastCellCls);
}
},
onHeaderShow: function(header) {
// Pass up to the GridSection
var gridSection = this.ownerCt,
// explicitly reference .menu and NOT getMenu()
// to avoid unnecessary creation
menu = this.menu,
idx,
checkItems,
colCheckItem;
if (menu) {
colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
if (colCheckItem) {
colCheckItem.setChecked(true, true);
}
if (this.disabledHeaderItem) {
checkItems = menu.query('#columnItem menucheckitem[checked]');
if (checkItems.length > 1) {
this.disabledHeaderItem.enable();
delete this.disabledHeaderItem;
}
}
}
if (this.view) {
this.view.onHeaderShow(this, header, idx);
}
if (gridSection) {
idx = this.items.indexOf(header);
gridSection.onHeaderShow(this, header, idx);
}
},
onHeaderHide: function(header) {
// Pass up to the GridSection
var gridSection = this.ownerCt,
// explicitly reference .menu and NOT getMenu()
// to avoid unnecessary creation
menu = this.menu,
idx,
colCheckItem,
checkItems;
if (menu) {
colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
if (colCheckItem) {
colCheckItem.setChecked(false, true);
}
checkItems = menu.query('#columnItem menucheckitem[checked]');
if (checkItems.length === 1) {
checkItems[0].disable();
this.disabledHeaderItem = checkItems[0];
}
}
if (this.view) {
this.view.onHeaderHide(this, header, idx);
}
if (gridSection) {
idx = this.items.indexOf(header);
this.ownerCt.onHeaderHide(this, header, idx);
}
},
/**
* 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.locked = true;
Ext.Function.defer(function() {
this.locked = false;
}, 200, this);
},
onHeaderResize: function(header, w) {
this.tempLock();
if (this.view) {
this.view.onHeaderResize(header, w);
}
this.fireEvent('headerresize', 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
// TODO: allow individual header to add additional menu items
// provide way to invalidate cache.
this.showMenuBy(t, header);
this.fireEvent("headertriggerclick", this, header, e, t);
},
showMenuBy: function(t, header) {
var menu = this.getMenu(),
sortableMth;
menu.activeHeader = header;
header.addCls(this.headerOpenCls);
// enable or disable asc & desc menu items based on header being sortable
sortableMth = header.sortable ? 'enable' : 'disable';
menu.down('#ascItem')[sortableMth]();
menu.down('#descItem')[sortableMth]();
menu.showBy(t);
},
// remove the trigger open class when the menu is hidden
onMenuHide: function() {
var menu = this.getMenu();
menu.activeHeader.removeCls(this.headerOpenCls);
},
moveHeader: function(fromIdx, toIdx) {
this.tempLock();
var gridSection = this.ownerCt,
header = this.move(fromIdx, toIdx);
if (gridSection) {
gridSection.onHeaderMove(this, header, fromIdx, toIdx);
}
this.fireEvent("headermove", this, header, fromIdx, toIdx);
},
/**
* Gets the menu (and will create it if it doesn't already exist)
* @private
*/
getMenu: function() {
if (!this.menu) {
this.menu = new Ext.menu.Menu({
items: this.getMenuItems()
});
this.menu.on('hide', this.onMenuHide, this);
}
return this.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() {
return [{
itemId: 'ascItem',
text: this.sortAscText,
cls: 'xg-hmenu-sort-asc',
handler: this.onSortAscClick,
scope: this
},{
itemId: 'descItem',
text: this.sortDescText,
cls: 'xg-hmenu-sort-desc',
handler: this.onSortDescClick,
scope: this
},'-',{
itemId: 'columnItem',
text: this.columnsText,
cls: 'x-cols-icon',
menu: this.getColumnsMenu()
}];
},
// 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 all headers which have been configured as hideable to be
* placed in the Columns menu.
*/
getColumnsMenu: function() {
var menuItems = [],
i = 0,
item,
items = this.query('gridheader[hideable]'),
itemsLn = items.length;
for (; i < itemsLn; i++) {
item = items[i];
menuItems.push({
text: item.text,
checked: !item.hidden,
hideOnClick: false,
headerId: item.id,
checkHandler: this.onColumnCheckChange,
scope: this
});
}
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
* @private
*/
getColumnsForTpl: function() {
var cols = [],
items = this.query('gridheader'),
itemsLn = items.length,
i = 0,
item;
for (; i < itemsLn; i++) {
item = items[i];
cols.push({
dataIndex: item.dataIndex,
align: item.align,
width: item.hidden ? 0 : item.getDesiredWidth(),
id: item.id
});
}
return cols;
},
/**
* Returns the number of grid headers in this headercontainer.
*/
getCount: function() {
return this.query('gridheader').length;
},
/**
* Returns the number of grid headers that are currently visible in this
* headercontainer.
*/
getVisibleCount: function() {
return this.query('gridheader:not(gridheader[hidden])').length;
},
/**
* Gets the full width of all columns that are visible.
*/
getFullWidth: function() {
var fullWidth = 0,
items = this.items.items,
itemsLn = items.length,
i = 0;
for (; i < itemsLn; i++) {
if (!isNaN(items[i].width) && !items[i].hidden) {
// use headers getDesiredWidth if its there
if (items[i].getDesiredWidth) {
fullWidth += items[i].getDesiredWidth();
// if injected a diff cmp use getWidth
} else {
fullWidth += items[i].getWidth();
}
}
}
return fullWidth;
},
// invoked internally by a header when not using triStateSorting
clearOtherSortStates: function(activeHeader) {
var items = this.items.items,
itemsLn = items.length,
i = 0;
for (; i < itemsLn; i++) {
if (items[i] !== activeHeader) {
// unset the sortstate and dont recurse
items[i].setSortState(null, true);
}
}
},
/**
* 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) {
var obj = {},
items = this.items.items,
itemsLn = items.length,
colIdx = 0,
item, value,
metaData,
store = this.up('gridpanel').store;
for (; colIdx < itemsLn; colIdx++) {
metaData = {
tdCls: '',
style: ''
};
item = items[colIdx];
value = data[item.dataIndex];
// When specifying a renderer as a string, it always resolves
// to Ext.util.Format
if (Ext.isString(item.renderer)) {
item.renderer = Ext.util.Format[item.renderer];
}
if (Ext.isFunction(item.renderer)) {
value = item.renderer.call(
item.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
);
}
//
if (metaData.css) {
console.warn("Header renderer: metadata css has been replaced by tdCls.");
metaData.tdCls = metaData.css;
delete metaData.css;
}
//
obj[item.id+'-modified'] = record.isModified(item.dataIndex) ? Ext.baseCSSPrefix + 'grid-dirty-cell' : Ext.baseCSSPrefix + 'grid-clean-cell';
obj[item.id+'-tdCls'] = metaData.tdCls;
obj[item.id+'-tdAttr'] = metaData.tdAttr;
obj[item.id+'-style'] = metaData.style;
obj[item.id] = value;
}
return obj;
}
});