/**
* Multi level grouping feature for the Grid panel.
*
* The following functions are added to the grid panel instance:
*
* - setSummaryPosition
* - setGroupSummaryPosition
* - expandAll
* - collapseAll
*/
Ext.define('Ext.grid.feature.AdvancedGrouping', {
extend: 'Ext.grid.feature.Feature',
alias: 'feature.advancedgrouping',
requires: [
'Ext.grid.feature.AdvancedGroupStore',
'Ext.grid.column.Groups'
],
eventPrefix: 'group',
eventCls: Ext.baseCSSPrefix + 'grid-advanced-group-row',
eventSelector: '.' + Ext.baseCSSPrefix + 'grid-advanced-group-row',
groupSelector: '.' + Ext.baseCSSPrefix + 'grid-advanced-group-hd',
groupSummaryCls: Ext.baseCSSPrefix + 'grid-advanced-group-summary',
groupSummarySelector: '.' + Ext.baseCSSPrefix + 'grid-advanced-group-summary',
groupHeaderExpandedCls: Ext.baseCSSPrefix + 'grid-advanced-group-header-expanded',
groupHeaderCollapsedCls: Ext.baseCSSPrefix + 'grid-advanced-group-header-collapsed',
groupTitleSelector: '.' + Ext.baseCSSPrefix + 'grid-advanced-group-title',
/**
* @cfg {String} [expandAllText="Expand all"]
* Text displayed in the grid header menu.
* @locale
*/
expandAllText: 'Expand all',
/**
* @cfg {String} [collapseAllText="Collapse all"]
* Text displayed in the grid header menu.
* @locale
*/
collapseAllText: 'Collapse all',
/**
* @cfg {String} [groupsText="Groups"]
* Text displayed in the grid header menu.
* @locale
*/
groupsText: 'Groups',
/**
* @cfg {String} [groupByText="Group by this field"]
* Text displayed in the grid header menu.
* @locale
*/
groupByText: 'Group by this field',
/**
* @cfg {String} [addToGroupingText="Add to grouping"]
* Text displayed in the grid header menu.
* @locale
*/
addToGroupingText: 'Add to grouping',
/**
* @cfg {String} [removeFromGroupingText="Remove from grouping"]
* Text displayed in the grid header menu.
* @locale
*/
removeFromGroupingText: 'Remove from grouping',
/**
* @cfg {Boolean} [startGroupedHeadersHidden=false]
* True to hide the headers that are currently grouped when the grid
* is rendered for the first time.
*/
startGroupedHeadersHidden: true,
/**
* @cfg {Boolean} [startCollapsed=false]
* True to start all groups collapsed when the grid is rendered for the first time.
*/
startCollapsed: true,
/**
* @cfg {Boolean} [enableGroupingMenu=true]
* True to enable the grouping control in the header menu.
*/
enableGroupingMenu: true,
/**
* @cfg {String} [groupSummaryPosition='hidden']
* Set the position of the summary row for each group:
*
* * `'hidden'`: Hide the group summary row
* * `'top'`: If the group is expanded or collapsed the summary is
* shown on the group header
* * `'bottom'`: When the group is expanded the summary row is shown
* as a group footer, after all records/groups are shown
*/
groupSummaryPosition: 'hidden',
/**
* @cfg {String} [summaryPosition='hidden']
* Set the position of the summary row for the entire grid:
*
* * `'hidden'`: Hide the summary row
* * `'top'`: Show the summary row as the first row in the grid
* * `'bottom'`: Show the summary row as the last row in the grid
*/
summaryPosition: 'hidden',
/**
* Width of the grouping column
* @cfg {Number} groupsColumnWidth
*/
groupsColumnWidth: 200,
/**
* @cfg {String/Array/Ext.Template} groupHeaderTpl
* A string Template snippet, an array of strings (optionally followed by
* an object containing Template methods) to be used to construct a
* Template, or a Template instance.
*
* - Example 1 (Template snippet):
*
* groupHeaderTpl: 'Group: {name} ({group.items.length})'
*
* - Example 2 (Array):
*
* groupHeaderTpl: [
* 'Group: ',
* '<div>{name:this.formatName}</div>',
* {
* formatName: function(name) {
* return Ext.String.trim(name);
* }
* }
* ]
*
* - Example 3 (Template Instance):
*
* groupHeaderTpl: Ext.create('Ext.XTemplate',
* 'Group: ',
* '<div>{name:this.formatName}</div>',
* {
* formatName: function(name) {
* return Ext.String.trim(name);
* }
* }
* )
*
* @cfg {String} groupHeaderTpl.groupField The field name being grouped by.
* @cfg {String} groupHeaderTpl.columnName The column header associated with
* the field being grouped by *if there is a column for the field*,
* falls back to the groupField name.
* @cfg {String} groupHeaderTpl.name The name of the group.
* @cfg {Ext.util.Group} groupHeaderTpl.group The group object.
*/
groupHeaderTpl: '{name}',
/**
* @cfg {String/Array/Ext.Template} groupSummaryTpl
* A string Template snippet, an array of strings (optionally followed by
* an object containing Template methods) to be used to construct
* a Template, or a Template instance.
*
* - Example 1 (Template snippet):
*
* groupSummaryTpl: 'Group: {name}'
*
* - Example 2 (Array):
*
* groupSummaryTpl: [
* 'Group: ',
* '<div>{name:this.formatName}</div>',
* {
* formatName: function(name) {
* return Ext.String.trim(name);
* }
* }
* ]
*
* - Example 3 (Template Instance):
*
* groupSummaryTpl: Ext.create('Ext.XTemplate',
* 'Group: ',
* '<div>{name:this.formatName}</div>',
* {
* formatName: function(name) {
* return Ext.String.trim(name);
* }
* }
* )
*
* @cfg {String} groupSummaryTpl.groupField The field name being grouped by.
* @cfg {String} groupSummaryTpl.columnName The column header associated
* with the field being grouped by *if there is a column for the field*,
* falls back to the groupField name.
* @cfg {String} groupSummaryTpl.name The name of the group.
* @cfg {Ext.util.Group} groupSummaryTpl.group The group object.
*/
groupSummaryTpl: 'Summary ({name})',
/**
* @cfg {String/Array/Ext.Template} summaryTpl
* A string Template snippet, an array of strings (optionally followed by
* an object containing Template methods) to be used to construct
* a Template, or a Template instance.
*
* - Example (Template snippet):
*
* groupSummaryTpl: 'Summary: {store.data.length}'
*
* @cfg {Ext.data.Store} summaryTpl.store The store object.
*/
summaryTpl: 'Summary ({store.data.length})',
outerTpl: [
'{%',
// Set up the grouping unless we are disabled
'var me = this.groupingFeature;',
'if (!(me.disabled)) {',
'me.setup(values);',
'}',
// Process the item
'this.nextTpl.applyOut(values, out, parent);',
'%}',
{
priority: 200
}],
// we need this template to fix the recordIndex; it should have a
// priority bigger than the outerRowTpl from Ext.view.Table
rowTpl: [
'{%',
'var me = this.groupingFeature;',
'if (!(me.disabled)) {',
'me.setupRowData(values);',
'}',
// 'values.view.renderColumnSizer(values, out);',
'this.nextTpl.applyOut(values, out, parent);',
'if (!(me.disabled)) {',
'me.resetRenderers();',
'}',
'%}',
{
priority: 10000
}
],
init: function(grid) {
var me = this,
view = me.view,
store = view.getStore(),
ownerGrid = view.ownerGrid,
lockPartner;
/**
* Fires before the grouping changes on the grid store
*
* @event beforegroupschange
* @param {Ext.grid.Panel} grid The grid panel instance
* @param {Ext.util.Grouper[]} groupers The new groupers
* @param {Ext.EventObject} e Event object
*/
/**
* Fires after the grouping changes on the grid store
*
* @event aftergroupschange
* @param {Ext.grid.Panel} grid The grid panel instance
* @param {Ext.util.Grouper[]} groupers The new groupers
* @param {Ext.EventObject} e Event object
*/
/**
* Fires when a group is expanded
*
* @event groupexpand
* @param {Ext.grid.Panel} grid The grid panel instance
* @param {Object} params An object with multiple keys to identify the group
* @param {Ext.EventObject} e Event object
*/
/**
* Fires when a group is collapsed
*
* @event groupcollapse
* @param {Ext.grid.Panel} grid The grid panel instance
* @param {Object} params An object with multiple keys to identify the group
* @param {Ext.EventObject} e Event object
*/
/**
* Fires when a group header cell is clicked
*
* @event groupclick
* @param {Ext.grid.Panel} grid The grid panel instance
* @param {Object} params An object with multiple keys to identify the group
* @param {Ext.EventObject} e Event object
*/
/**
* Fires when a group header cell is right clicked
*
* @event groupcontextmenu
* @param {Ext.grid.Panel} grid The grid panel instance
* @param {Object} params An object with multiple keys to identify the group
* @param {Ext.EventObject} e Event object
*/
/**
* Fires when a group summary cell is clicked
*
* @event groupsummaryclick
* @param {Ext.grid.Panel} grid The grid panel instance
* @param {Object} params An object with multiple keys to identify the group
* @param {Ext.EventObject} e Event object
*/
/**
* Fires when a group summary cell is right clicked
*
* @event groupsummarycontextmenu
* @param {Ext.grid.Panel} grid The grid panel instance
* @param {Object} params An object with multiple keys to identify the group
* @param {Ext.EventObject} e Event object
*/
/**
* Fires when a summary cell is clicked
*
* @event summaryclick
* @param {Ext.grid.Panel} grid The grid panel instance
* @param {Object} params An object with multiple keys to identify the group
* @param {Ext.EventObject} e Event object
*/
/**
* Fires when a summary cell is right clicked
*
* @event summarycontextmenu
* @param {Ext.grid.Panel} grid The grid panel instance
* @param {Object} params An object with multiple keys to identify the group
* @param {Ext.EventObject} e Event object
*/
me.callParent([grid]);
// we do not support buffered stores yet
if (store && store.isBufferedStore) {
// <debug>
Ext.log('Buffered stores are not supported yet by multi level grouping feature');
// </debug>
return;
}
// Add a table level processor
view.addTpl(Ext.XTemplate.getTpl(me, 'outerTpl')).groupingFeature = me;
// Add a row level processor
view.addRowTpl(Ext.XTemplate.getTpl(me, 'rowTpl')).groupingFeature = me;
view.preserveScrollOnRefresh = true;
view.doGrouping = store.isGrouped();
if (view.bufferedRenderer) {
// eslint-disable-next-line max-len
view.bufferedRenderer.variableRowHeight = view.hasVariableRowHeight() || view.doGrouping;
}
lockPartner = me.lockingPartner;
if (lockPartner && lockPartner.dataSource) {
me.dataSource = view.dataSource = lockPartner.dataSource;
}
else {
me.dataSource = view.dataSource = new Ext.grid.feature.AdvancedGroupStore({
summaryPosition: me.summaryPosition,
groupSummaryPosition: me.groupSummaryPosition,
startCollapsed: me.startCollapsed,
gridLocked: grid.isLocked,
view: me.view,
source: store
});
ownerGrid.expandAll = Ext.bind(me.expandAll, me);
ownerGrid.collapseAll = Ext.bind(me.collapseAll, me);
ownerGrid.setGroupSummaryPosition = Ext.bind(me.setGroupSummaryPosition, me);
ownerGrid.setSummaryPosition = Ext.bind(me.setSummaryPosition, me);
}
me.initEventsListeners();
if (me.enableGroupingMenu) {
me.injectGroupingMenu();
}
},
destroy: function() {
var me = this,
ownerGrid = me.view.ownerGrid;
ownerGrid.setGroupSummaryPosition = ownerGrid.setSummaryPosition = null;
ownerGrid.expandAll = ownerGrid.collapseAll = null;
me.destroyEventsListeners();
Ext.destroy(me.dataSource);
me.callParent();
},
enable: function() {
var me = this,
view = me.view,
store = view.getStore();
view.doGrouping = false;
if (view.lockingPartner) {
view.lockingPartner.doGrouping = false;
}
me.callParent();
if (me.lastGroupers) {
store.group(me.lastGroupers);
me.lastGroupers = null;
}
},
disable: function() {
var view = this.view,
store = view.getStore(),
lastGroupers = store.getGroupers();
view.doGrouping = false;
if (view.lockingPartner) {
view.lockingPartner.doGrouping = false;
}
this.callParent();
if (lastGroupers) {
this.lastGroupers = lastGroupers.getRange();
store.clearGrouping();
}
},
/**
* Change the group summary position
* @param {String} value Check {@link #groupSummaryPosition}
*/
setGroupSummaryPosition: function(value) {
var me = this,
lockingPartner = me.lockingPartner;
me.groupSummaryPosition = value;
if (lockingPartner) {
lockingPartner.groupSummaryPosition = value;
}
me.dataSource.setGroupSummaryPosition(value);
me.dataSource.refreshData();
},
/**
* Change the summary position
* @param {String} value Check {@link #summaryPosition}
*/
setSummaryPosition: function(value) {
var me = this,
lockingPartner = me.lockingPartner;
me.summaryPosition = value;
if (lockingPartner) {
lockingPartner.summaryPosition = value;
}
me.dataSource.setSummaryPosition(value);
me.dataSource.refreshData();
},
collapse: function(path, options) {
this.doCollapseExpand(false, path, options);
},
expand: function(path, options) {
this.doCollapseExpand(true, path, options);
},
expandAll: function() {
var me = this;
Ext.suspendLayouts();
me.dataSource.setStartCollapsed(false);
me.dataSource.refreshData(true);
Ext.resumeLayouts(true);
},
collapseAll: function() {
var me = this;
Ext.suspendLayouts();
me.dataSource.setStartCollapsed(true);
me.dataSource.refreshData(true);
Ext.resumeLayouts(true);
},
doCollapseExpand: function(expanded, path, options, fireArg) {
var me = this,
lockingPartner = me.lockingPartner,
ownerGrid = me.view.ownerGrid,
record;
me.isExpandingCollapsing = true;
record = me.dataSource.doExpandCollapseByPath(path, expanded);
if (options === true) {
options = {
focus: true
};
}
// Sync the group state and focus the row if requested.
me.afterCollapseExpand(expanded, record, options);
// Sync the lockingPartner's group state.
if (lockingPartner) {
// Clear focus flag (without mutating a passed in object).
// If we were told to focus, we must focus, not the other side.
if (options && options.focus) {
options = Ext.Object.chain(options);
options.focus = false;
}
lockingPartner.afterCollapseExpand(expanded, record, options);
}
if (!fireArg) {
fireArg = Ext.apply({
record: record,
column: me.getGroupingColumn(),
row: me.view.getRowByRecord(record)
}, me.dataSource.getRenderData(record));
}
ownerGrid.fireEvent(expanded ? 'groupexpand' : 'groupcollapse', ownerGrid, fireArg);
me.isExpandingCollapsing = false;
},
afterCollapseExpand: function(expanded, record, options) {
if (record && options) {
this.grid.ensureVisible(record, options);
}
},
vetoEvent: function(record, row, rowIndex, e) {
var shouldVeto = false,
key = e.getKey();
// Do not veto mouseover/mouseout and keycode != ENTER
if (!e.getTarget(this.groupSummarySelector) && e.getTarget(this.eventSelector)) {
shouldVeto = key
? key === e.ENTER
// eslint-disable-next-line max-len
: (e.type !== 'mouseover' && e.type !== 'mouseout' && e.type !== 'mouseenter' && e.type !== 'mouseleave');
}
if (shouldVeto) {
return false;
}
},
setup: function(values) {
var me = this,
view = values.view,
store = view.store,
model = store.model.getSummaryModel(),
columns = view.headerCt.getGridColumns(),
length = columns.length,
column, i;
// first we check if the store is grouped or not
me.doGrouping = !me.disabled && view.store.isGrouped();
if (me.doGrouping) {
me.dataSource.isRTL = me.isRTL();
}
for (i = 0; i < length; i++) {
column = columns[i];
// if there is a summaryType configured on the column then use
// that instead of the one from the model
if (column.summaryType && column.dataIndex && model) {
model.setSummaryField(column.dataIndex, column.summaryType);
}
}
},
setupRowData: function(rowValues) {
var me = this,
record = rowValues.record,
renderData, field, group, header, grouper;
// the recordIndex needs to be fixed because it is used by the selection models
rowValues.recordIndex = me.dataSource.indexOf(record);
renderData = me.dataSource.getRenderData(record);
if (renderData) {
if (renderData.isSummary) {
renderData.store = me.view.getStore();
}
else {
group = renderData.group;
grouper = group.getGrouper();
field = grouper.getProperty();
header = me.getGroupedHeader(grouper);
Ext.apply(renderData, {
groupField: field,
columnName: header ? header.text : field,
name: group.getLabel()
});
renderData.column = header;
record.ownerGroup = renderData.name;
}
me.setupRowValues(rowValues, renderData);
me.setRenderers(renderData);
}
},
setupRowValues: function(rowValues, renderData) {
var me = this,
group = renderData.group;
rowValues.rowClasses.push(me.eventCls);
if (renderData.isGroupSummary) {
rowValues.rowClasses.push(me.groupSummaryCls);
}
if (renderData.isGroup && group) {
// eslint-disable-next-line max-len
rowValues.rowClasses.push(group.isCollapsed ? me.groupHeaderCollapsedCls : me.groupHeaderExpandedCls);
}
},
isRTL: function() {
var grid = this.grid;
if (Ext.isFunction(grid.isLocalRtl)) {
return grid.isLocalRtl();
}
return false;
},
setRenderers: function(renderData) {
var me = this,
startIdx = me.getGroupingColumnPosition(),
columns = me.view.headerCt.getGridColumns(),
length = columns.length,
position = me.groupSummaryPosition,
column, group, i;
if (me.renderersAreSet > 0) {
// avoid setting renderers again if they were not reset before
return;
}
if (renderData.isSummary) {
for (i = 0; i < startIdx - 1; i++) {
column = columns[i];
column.backupRenderer = column.renderer;
// eslint-disable-next-line max-len
column.renderer = (column.summaryType || column.summaryRenderer) ? column.summaryRenderer : Ext.renderEmpty;
}
}
for (i = startIdx; i < length; i++) {
column = columns[i];
column.backupRenderer = column.renderer;
if (renderData.isGroupSummary || renderData.isSummary) {
column.renderer = column.summaryRenderer;
}
else if (renderData.isGroup) {
group = renderData.group;
column.renderer = (position === 'bottom' && !group.isCollapsed) ||
(position === 'hidden')
? this.renderEmpty
: column.summaryRenderer;
}
}
me.renderersAreSet = (me.renderersAreSet || 0) + 1;
},
resetRenderers: function() {
var me = this,
columns = me.view.headerCt.getGridColumns(),
length = columns.length,
column, i;
if (me.renderersAreSet > 0) {
me.renderersAreSet--;
}
if (!me.renderersAreSet) {
for (i = 0; i < length; i++) {
column = columns[i];
if (column.backupRenderer != null) {
column.renderer = column.backupRenderer;
column.backupRenderer = null;
}
}
}
},
getHeaderNode: function(groupName) {
var el = this.view.getEl(),
nodes, i, len, node;
if (el) {
nodes = el.query(this.groupTitleSelector);
for (i = 0, len = nodes.length; i < len; ++i) {
node = nodes[i];
if (node.getAttribute('data-groupName') === groupName) {
return node;
}
}
}
},
/**
* Returns `true` if the named group is expanded.
* @param {String} groupName The group name.
* @return {Boolean} `true` if the group defined by that value is expanded.
*/
isExpanded: function(groupName) {
var groups = this.view.getStore().getGroups(),
group = groups.getByPath(groupName);
return group && !group.isCollapsed;
},
getGroupedHeader: function(grouper) {
var me = this,
headers = me.headers,
headerCt = me.view.headerCt,
partner = me.lockingPartner,
selector, header, groupField;
if (!headers) {
me.headers = headers = {};
}
if (grouper) {
groupField = grouper.getId();
header = headers[groupField];
if (!header) {
selector = '[grouperId=' + groupField + ']';
header = headerCt.down(selector);
// The header may exist in the locking partner, so check there as well
if (!header && partner) {
headers[groupField] = header = partner.view.headerCt.down(selector);
}
}
}
return header || null;
},
getGroupingColumnConfig: function(store) {
var me = this,
isGrouped = store ? store.isGrouped() : me.view.getStore().isGrouped();
me.lastColumnWidth = me.groupsColumnWidth;
return {
xtype: 'groupscolumn',
groupHeaderTpl: me.groupHeaderTpl,
groupSummaryTpl: me.groupSummaryTpl,
summaryTpl: me.summaryTpl,
editRenderer: me.renderEmpty,
width: isGrouped ? me.lastColumnWidth : 1
};
},
renderEmpty: function() {
return '\u00a0';
},
// add the grouping column to the locked side if we are locked
// otherwise add it to the normal view
getGroupingColumn: function() {
var me = this,
result = me.groupingColumn,
view = me.view,
ownerGrid = view.ownerGrid;
if (!result || result.destroyed) {
// Always put the grouping column in the locked side if there is one.
if (!ownerGrid.lockable || view.isLockedView) {
result = me.groupingColumn = view.headerCt.down('groupscolumn') ||
view.headerCt.add(me.getGroupingColumnPosition(), me.getGroupingColumnConfig());
}
}
return result;
},
getGroupingColumnPosition: function() {
var columns = this.view.headerCt.items.items,
length = columns.length,
pos = 0,
i, column;
for (i = 0; i < length; i++) {
column = columns[i];
// we need to insert the grouping column after the selection model columns
if (!column.hideable && !column.draggable) {
pos++;
}
}
return pos;
},
onBeforeReconfigure: function(grid, store, columns, oldStore, oldColumns) {
var me = this,
view = me.view,
dataSource = me.dataSource,
ownerGrid = view.ownerGrid,
columnsChanged = false,
column;
if (columns && (!ownerGrid.lockable || view.isLockedView)) {
column = me.getGroupingColumnConfig(store && store !== oldStore ? store : null);
column.locked = ownerGrid.lockable;
Ext.Array.insert(columns, 0, [column]);
columnsChanged = true;
}
if (store && store !== oldStore) {
// bufferedStore = store.isBufferedStore;
Ext.destroy(me.storeListeners);
me.setupStoreListeners(store);
me.doGrouping = store.isGrouped();
dataSource.setSource(store);
if (!columnsChanged) {
// we might need to show the groups column if the new store is grouped
me.onGroupsChange(store, store.getGroupers(false));
}
}
},
onAfterViewRendered: function(view) {
var me = this,
store = view.getStore(),
groupers = store.getGroupers(),
length = groupers.length,
i, header, grouper;
// create the dedicated groups column
me.getGroupingColumn();
view.unbindStoreListeners(store);
// should we hide columns that are grouped?
if (me.startGroupedHeadersHidden) {
for (i = 0; i < length; i++) {
grouper = groupers.getAt(i);
header = me.getGroupedHeader(grouper);
if (header) {
header.hide();
}
}
}
},
injectGroupingMenu: function() {
var me = this,
headerCt = me.view.headerCt;
headerCt.showMenuBy = Ext.Function.createInterceptor(headerCt.showMenuBy, me.showMenuBy);
headerCt.getMenuItems = me.getMenuItems();
},
showMenuBy: function(clickEvent, t, header) {
var me = this,
menu = me.getMenu(),
grid = me.view.ownerGrid,
store = me.view.getStore(),
groupers = store.getGroupers(),
headerNotGroupable = !header.groupable || !header.dataIndex,
groupMenuMeth = headerNotGroupable ? 'disable' : 'enable',
isGrouped = store.isGrouped(),
grouper = groupers.getByProperty(header.dataIndex);
menu.down('#groupByMenuItem')[groupMenuMeth]();
menu.down('#groupsMenuItem').setVisible(isGrouped);
menu.down('#addGroupMenuItem')[headerNotGroupable || grouper ? 'disable' : 'enable']();
menu.down('#removeGroupMenuItem')[headerNotGroupable || !grouper ? 'disable' : 'enable']();
grid.fireEvent('showheadermenuitems', grid, {
grid: grid,
column: header,
menu: menu
});
},
getMenuItems: function() {
var me = this,
grid = me.view.ownerGrid,
getMenuItems = me.view.headerCt.getMenuItems;
// runs in the scope of headerCt
return function() {
// We cannot use the method from HeaderContainer's prototype here
// because other plugins or features may already have injected an implementation
var o = getMenuItems.call(this);
o.push('-', {
iconCls: Ext.baseCSSPrefix + 'groups-icon',
itemId: 'groupsMenuItem',
text: me.groupsText,
menu: [{
itemId: 'expandAll',
text: me.expandAllText,
handler: me.expandAll,
scope: me
}, {
itemId: 'collapseAll',
text: me.collapseAllText,
handler: me.collapseAll,
scope: me
}]
}, {
iconCls: Ext.baseCSSPrefix + 'group-by-icon',
itemId: 'groupByMenuItem',
text: me.groupByText,
handler: me.onGroupByMenuItemClick,
scope: me
}, {
iconCls: Ext.baseCSSPrefix + 'add-group-icon',
itemId: 'addGroupMenuItem',
text: me.addToGroupingText,
handler: me.onAddGroupMenuItemClick,
scope: me
}, {
iconCls: Ext.baseCSSPrefix + 'remove-group-icon',
itemId: 'removeGroupMenuItem',
text: me.removeFromGroupingText,
handler: me.onRemoveGroupMenuItemClick,
scope: me
});
grid.fireEvent('collectheadermenuitems', grid, {
grid: grid,
headerContainer: this,
items: o
});
return o;
};
},
/**
* Group by the header the user has clicked on.
* @private
*/
onGroupByMenuItemClick: function(menuItem, e) {
var me = this,
hdr = menuItem.parentMenu.activeHeader,
store = me.view.getStore(),
groupers = store.getGroupers(),
length = groupers.length,
i, header;
if (me.disabled) {
me.enable();
}
Ext.suspendLayouts();
for (i = 0; i < length; i++) {
header = me.getGroupedHeader(groupers.items[i]);
if (header) {
header.show();
}
}
hdr.hide();
groupers.replaceAll(me.createGrouperFromHeader(hdr));
Ext.resumeLayouts(true);
},
/**
* Group by the header the user has clicked on.
* @private
*/
onAddGroupMenuItemClick: function(menuItem, e) {
var me = this,
hdr = menuItem.parentMenu.activeHeader,
groupers = me.view.getStore().getGroupers();
if (me.disabled) {
me.enable();
}
Ext.suspendLayouts();
hdr.hide();
groupers.add(me.createGrouperFromHeader(hdr));
Ext.resumeLayouts(true);
},
/**
* Create a grouper configuration object out of a grid header
* @private
* @param header
* @return {Object}
*/
createGrouperFromHeader: function(header) {
return header.getGrouper() || {
property: header.dataIndex,
direction: header.sortState || 'ASC',
formatter: header.groupFormatter
};
},
/**
* Remove the grouper
* @private
*/
onRemoveGroupMenuItemClick: function(menuItem, e) {
var me = this,
hdr = menuItem.parentMenu.activeHeader,
groupers = me.view.getStore().getGroupers(),
grouper;
if (me.disabled) {
me.enable();
}
grouper = groupers.getByProperty(hdr.dataIndex);
if (grouper) {
groupers.remove(grouper);
}
},
onCellEvent: function(view, row, e) {
var me = this,
record = view.getRecord(row),
groupHd = e.getTarget(me.groupSelector),
groupSum = e.getTarget(me.groupSummarySelector),
cell = e.getTarget(view.getCellSelector()),
ownerGrid = view.ownerGrid,
prefix = 'group',
data = me.dataSource.getRenderData(record),
fireArg = Ext.applyIf({
grid: ownerGrid,
view: view,
record: record,
column: view.getHeaderByCell(cell),
cell: cell,
row: row,
feature: me,
e: e
}, data);
if (!(record && data)) {
return;
}
// check if the mouse event occured on a group header
if (groupHd) {
if (e.type === 'click') {
me.doCollapseExpand(data.group.isCollapsed, data.group.getPath(), {
focus: true,
column: me.getGroupingColumn()
}, fireArg);
}
}
// check if the mouse event occured on a group summary cell
if (groupSum) {
prefix = data.isGroupSummary ? 'groupsummary' : 'summary';
}
ownerGrid.fireEvent(prefix + e.type, ownerGrid, fireArg);
return false;
},
onKeyEvent: function(view, rowElement, e) {
var me = this,
position = e.position,
groupHd = e.getTarget(me.groupSelector),
column = me.getGroupingColumn(),
record, data, fireArg, cell;
if (position) {
cell = position.getCell();
groupHd = cell.down(me.groupSelector);
}
if (e.getKey() === e.ENTER && rowElement && groupHd) {
record = view.getRecord(rowElement);
data = me.dataSource.getRenderData(record);
if (record && record.isGroup && data) {
fireArg = Ext.applyIf({
record: record,
column: column,
cell: cell,
row: rowElement
}, data);
me.doCollapseExpand(data.group.isCollapsed, data.group.getPath(), {
focus: true,
column: column
}, fireArg);
}
}
},
onBeforeGroupsChange: function(store, groupers) {
var view = this.view,
grid = view.ownerGrid;
if (!grid.lockable || view.isLockedView) {
grid.fireEvent('beforegroupschange', grid, groupers);
}
},
onGroupChange: function(store, grouper) {
this.onGroupsChange(store, grouper ? [grouper] : null);
},
onGroupsChange: function(store, groupers) {
var me = this,
groupingColumn = me.getGroupingColumn(),
view = me.view,
grid = view.ownerGrid,
isGrouped = groupers && groupers.length,
width;
if (groupingColumn) {
if (groupingColumn.rendered) {
// we can't hide the grouping column because it may mess up the locked view
if (isGrouped) {
groupingColumn.setWidth(me.lastColumnWidth);
}
else {
width = groupingColumn.getWidth();
if (width > 1) {
me.lastColumnWidth = width;
groupingColumn.setWidth(1);
}
}
}
else if (isGrouped) {
groupingColumn.width = me.lastColumnWidth;
}
}
if (!grid.lockable || view.isLockedView) {
me.dataSource.fireRefresh();
grid.fireEvent('aftergroupschange', grid, groupers);
}
},
privates: {
getViewListeners: function() {
var me = this,
viewListeners = {
afterrender: me.onAfterViewRendered,
scope: me,
destroyable: true
};
// after view is rendered we need to add our grouping column
// after view is rendered start monitoring for group mouse/keyboard events
viewListeners[me.eventPrefix + 'click'] = me.onCellEvent;
viewListeners[me.eventPrefix + 'dblclick'] = me.onCellEvent;
viewListeners[me.eventPrefix + 'contextmenu'] = me.onCellEvent;
viewListeners[me.eventPrefix + 'keyup'] = me.onKeyEvent;
return viewListeners;
},
getOwnerGridListeners: function() {
return {
beforereconfigure: this.onBeforeReconfigure,
destroyable: true,
scope: this
};
},
getStoreListeners: function() {
return {
beforegroupschange: this.onBeforeGroupsChange,
groupchange: this.onGroupChange,
groupschange: this.onGroupsChange,
scope: this,
destroyable: true
};
},
initEventsListeners: function() {
var me = this,
view = me.view,
grid = view.ownerGrid,
lockPartner = me.lockingPartner;
me.viewListeners = view.on(me.getViewListeners());
// if grid is reconfigured we need to add our grouping column and monitor the new store
if (!lockPartner || (lockPartner && !lockPartner.gridListeners)) {
me.ownerGridListeners = grid.on(me.getOwnerGridListeners());
}
// when new columns are added we need to change the menu
// store needs to be monitored for the group event to refresh the view
me.setupStoreListeners(view.getStore());
},
destroyEventsListeners: function() {
Ext.destroyMembers(this, 'viewListeners', 'storeListeners', 'ownerGridListeners');
},
setupStoreListeners: function(store) {
Ext.destroy(this.storeListeners);
this.storeListeners = store.on(this.getStoreListeners());
}
}
});