/**
* This feature allows to display the grid rows aggregated into groups as specified by the
* {@link Ext.data.Store#grouper grouper} underneath. The groups can also be expanded and collapsed.
*
* ## Extra Events
*
* This feature adds several extra events that will be fired on the grid to interact
* with the groups:
*
* - {@link #groupclick}
* - {@link #groupdblclick}
* - {@link #groupcontextmenu}
* - {@link #groupexpand}
* - {@link #groupcollapse}
*
* ## Menu Augmentation
*
* This feature adds extra options to the grid column menu to provide the user with functionality
* to modify the grouping. This can be disabled by setting the {@link #enableGroupingMenu} option.
* The option to disallow grouping from being turned off by the user is {@link #enableNoGroups}.
*
* ## Controlling Group Text
*
* The {@link #groupHeaderTpl} is used to control the rendered title for each group. It can modified
* to customized the default display.
*
* ## Groupers
*
* By default, this feature expects that the data field that is mapped to by the
* {@link Ext.data.AbstractStore#groupField} config is a simple data type such as a
* String or a Boolean. However, if you intend to group by a data field that is a
* complex data type such as an Object or Array, it is necessary to define one or more
* {@link Ext.util.Grouper groupers} on the feature that it can then use to lookup
* internal group information when grouping by different fields.
*
* var feature = Ext.create('Ext.grid.feature.Grouping', {
* startCollapsed: true,
* groupers: [{
* property: 'asset',
* groupFn: function(val) {
* return val.data.name;
* }
* }]
* });
*
* ## Example Usage
*
* @example
* var store = Ext.create('Ext.data.Store', {
* fields: ['name', 'seniority', 'department'],
* groupField: 'department',
* data: [
* { name: 'Michael Scott', seniority: 7, department: 'Management' },
* { name: 'Dwight Schrute', seniority: 2, department: 'Sales' },
* { name: 'Jim Halpert', seniority: 3, department: 'Sales' },
* { name: 'Kevin Malone', seniority: 4, department: 'Accounting' },
* { name: 'Angela Martin', seniority: 5, department: 'Accounting' }
* ]
* });
*
* Ext.create('Ext.grid.Panel', {
* title: 'Employees',
* store: store,
* columns: [
* { text: 'Name', dataIndex: 'name', flex: 1 },
* { text: 'Seniority', dataIndex: 'seniority', flex: 1 }
* ],
* features: [{ftype:'grouping'}],
* renderTo: Ext.getBody()
* });
*
* **Note:** To use grouping with a grid that has
* {@link Ext.grid.column.Column#locked locked columns}, you need to supply the grouping feature
* as a config object - so the grid can create two instances of the grouping feature.
*
* ## Grouping Summary
*
* An aggregate summary row may be added at the bottom of each group by setting
* {@link #cfg-showSummaryRow showSummaryRow} to `true`. There are two
* aspects to the summary:
*
* ## Calculation
*
* The summary value needs to be calculated for each column in the grid. This is
* controlled by the {@link Ext.grid.column.Column#cfg-summaryType summaryType} option
* specified on the column. There are several built in summary types which can be
* specified as a string on the column configuration. These call underlying methods on
* the store:
*
* - {@link Ext.data.Store#method-count count}
* - {@link Ext.data.Store#method-sum sum}
* - {@link Ext.data.Store#method-min min}
* - {@link Ext.data.Store#method-max max}
* - {@link Ext.data.Store#method-average average}
*
* Alternatively, the summaryType can be a function definition. If this is the case,
* the function is called with two parameters:
*
* - records {Array} - an array of records
* - values {Array} - an array of field values to calculate the summary value
*
* ## Rendering
*
* Similar to a column, the summary also supports a
* {@link Ext.grid.column.Column.html#cfg-summaryRenderer summaryRenderer} function. This
* summaryRenderer is called before displaying a value. The function is optional. If
* not specified, the default calculated value is shown. The summaryRenderer is called
* with:
*
* - value {Object} - The calculated value.
* - summaryData {Object} - Contains all raw summary values for the row.
* - field {String} - The name of the field we are calculating
* - metaData {Object} - A collection of metadata about the current cell; can be used or
* modified by the renderer.
*
* ## Example Usage
*
* @example
* Ext.define('TestResult', {
* extend: 'Ext.data.Model',
* fields: ['student', 'subject', {
* name: 'mark',
* type: 'int'
* }]
* });
*
* Ext.create('Ext.grid.Panel', {
* width: 200,
* height: 240,
* renderTo: document.body,
* features: [{
* ftype: 'grouping',
* groupHeaderTpl: 'Subject: {name}',
* showSummaryRow: true
* }],
* store: {
* model: 'TestResult',
* groupField: 'subject',
* data: [{
* student: 'Student 1',
* subject: 'Math',
* mark: 84
* },{
* student: 'Student 1',
* subject: 'Science',
* mark: 72
* },{
* student: 'Student 2',
* subject: 'Math',
* mark: 96
* },{
* student: 'Student 2',
* subject: 'Science',
* mark: 68
* }]
* },
* columns: [{
* dataIndex: 'student',
* text: 'Name',
* summaryType: 'count',
* summaryRenderer: function(value){
* return Ext.String.format('{0} student{1}', value, value !== 1 ? 's' : '');
* }
* }, {
* dataIndex: 'mark',
* text: 'Mark',
* summaryType: 'average'
* }]
* });
*/
Ext.define('Ext.grid.feature.Grouping', {
extend: 'Ext.grid.feature.Feature',
alias: 'feature.grouping',
mixins: {
summary: 'Ext.grid.feature.AbstractSummary'
},
requires: ['Ext.grid.feature.GroupStore'],
eventPrefix: 'group',
eventSelector: '.' + Ext.baseCSSPrefix + 'grid-group-hd',
refreshData: {},
wrapsItem: true,
/**
* @event groupclick
* @param {Ext.view.Table} view
* @param {HTMLElement} node
* @param {String} group The name of the group
* @param {Ext.event.Event} e
*/
/**
* @event groupdblclick
* @param {Ext.view.Table} view
* @param {HTMLElement} node
* @param {String} group The name of the group
* @param {Ext.event.Event} e
*/
/**
* @event groupcontextmenu
* @param {Ext.view.Table} view
* @param {HTMLElement} node
* @param {String} group The name of the group
* @param {Ext.event.Event} e
*/
/**
* @event groupcollapse
* @param {Ext.view.Table} view
* @param {HTMLElement} node
* @param {String} group The name of the group
*/
/**
* @event groupexpand
* @param {Ext.view.Table} view
* @param {HTMLElement} node
* @param {String} group The name of the group
*/
/**
* @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}'
*
* - 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 {Mixed} groupHeaderTpl.groupValue The value of the
* {@link Ext.data.Store#groupField groupField} for the group header being rendered.
* @cfg {String} groupHeaderTpl.renderedGroupValue The rendered value of the
* {@link Ext.data.Store#groupField groupField} for the group header being rendered,
* as produced by the column renderer.
* @cfg {String} groupHeaderTpl.name An alias for renderedGroupValue
* @cfg {Ext.data.Model[]} groupHeaderTpl.rows Deprecated - use children instead.
* An array containing the child records for the group being rendered.
* *Not available if the store is a {@link Ext.data.BufferedStore BufferedStore}*
* @cfg {Ext.data.Model[]} groupHeaderTpl.children An array containing the child records
* for the group being rendered. *Not available if the store is a
* {@link Ext.data.BufferedStore BufferedStore}*
*/
groupHeaderTpl: '{columnName}: {name}',
/**
* @cfg {Number} [depthToIndent=17]
* Number of pixels to indent per grouping level
*/
depthToIndent: 17,
collapsedCls: Ext.baseCSSPrefix + 'grid-group-collapsed',
hdCollapsedCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsed',
hdNotCollapsibleCls: Ext.baseCSSPrefix + 'grid-group-hd-not-collapsible',
collapsibleCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsible',
ctCls: Ext.baseCSSPrefix + 'group-hd-container',
/**
* @cfg {String} [groupByText="Group by this field"]
* Text displayed in the grid header menu for grouping by header.
* @locale
*/
groupByText: 'Group by this field',
/**
* @cfg {String} [showGroupsText="Show in groups"]
* Text displayed in the grid header for enabling/disabling grouping.
* @locale
*/
showGroupsText: 'Show in groups',
/**
* @cfg {Boolean} [hideGroupedHeader=false]
* True to hide the header that is currently grouped.
*/
hideGroupedHeader: false,
/**
* @cfg {Boolean} [startCollapsed=false]
* True to start all groups collapsed.
*/
startCollapsed: false,
/**
* @cfg {Boolean} [enableGroupingMenu=true]
* True to enable the grouping control in the header menu.
*/
enableGroupingMenu: true,
/**
* @cfg {Boolean} [enableNoGroups=true]
* True to allow the user to turn off grouping.
*/
enableNoGroups: true,
/**
* @cfg {Boolean} [collapsible=true]
* Set to `false` to disable collapsing groups from the UI.
*
* This is set to `false` when the associated {@link Ext.data.Store store} is
* a {@link Ext.data.BufferedStore BufferedStore}.
*/
collapsible: true,
/**
* @cfg {Array} [groupers=null]
* These are grouper objects defined for the feature. If the group names are derived
* from complex data types, it is necessary to convert them as a store would.
*
* However, since only one grouper can be defined on the store at a time and
* this feature clears the current grouper when a new one is added, it is
* necessary to define a cache of groupers that the feature can lookup as needed.
*
* Expected grouper object properties are `property` and `groupFn`.
*/
groupers: null,
/**
* @cfg {String} expandTip
* The tooltip for the group expander tool when the group is collapsed.
* @locale
*/
expandTip: 'Click to expand. CTRL key collapses all others',
/**
* @cfg {String} collapseTip
* The tooltip for the group expander tool when the group is expanded.
* @locale
*/
collapseTip: 'Click to collapse. CTRL/click collapses all others',
/**
* `True` to display a summary row for each grouping.
*
* **Note:** Requires the setting of
* {@link Ext.grid.column.Column#cfg-summaryType summaryType} on each participating
* grid column. To specify the summary text you may configure the column's
* {@link Ext.grid.column.Column#cfg-summaryRenderer summaryRenderer} option. See
* the Grouping Summary heading in the Grouping class description for a working
* example.
*/
showSummaryRow: false,
/* eslint-disable indent, max-len */
outerTpl: [
'{%',
// Set up the grouping unless we are disabled, or it's just a summary record
'if (!(this.groupingFeature.disabled || values.rows.length === 1 && values.rows[0].isSummary)) {',
'this.groupingFeature.setup(values.rows, values.view.rowValues);',
'}',
// Process the item
'this.nextTpl.applyOut(values, out, parent);',
// Clean up the grouping unless we are disabled, or it's just a summary record
'if (!(this.groupingFeature.disabled || values.rows.length === 1 && values.rows[0].isSummary)) {',
'this.groupingFeature.cleanup(values.rows, values.view.rowValues);',
'}',
'%}',
{
priority: 200
}],
groupRowTpl: [
'{%',
'var me = this.groupingFeature,',
'colspan = "colspan=" + values.columns.length;',
// If grouping is disabled or it's just a summary record, do not call setupRowData, and do not wrap
'if (me.disabled || parent.rows.length === 1 && parent.rows[0].isSummary) {',
'values.needsWrap = false;',
'} else {',
// setupRowData requires the index in the data source, not the index in the real store
'me.setupRowData(values.record, values.rowIndex, values);',
'}',
'%}',
'<tpl if="needsWrap">',
'<tpl if="isFirstRow">',
// MUST output column sizing elements because the first row in this table
// contains one colspanning TD, and that overrides subsequent column width settings.
'{% values.view.renderColumnSizer(values, out); %}',
'<tr data-boundView="{view.id}" data-recordId="{record.internalId:htmlEncode}" data-recordIndex="{[values.isCollapsedGroup ? -1 : values.recordIndex]}" class="{groupHeaderCls}">',
'<td class="{[me.ctCls]}" {[colspan]}>',
'{%',
// Group title is visible if not locking, or we are the locked side, or the locked side has no columns/
// Use visibility to keep row heights synced without intervention.
'var groupTitleStyle = (!values.view.lockingPartner || (values.view.ownerCt === values.view.ownerCt.ownerLockable.lockedGrid) || (values.view.lockingPartner.headerCt.getVisibleGridColumns().length === 0)) ? "" : "visibility:hidden",',
'tooltip = "";',
// Only display a tooltip if the group is collapsible
'if (me.collapsible) {',
'tooltip = Ext.String.format(\'data-qtip="{0}"\', values.isCollapsedGroup ? me.expandTip : me.collapseTip);',
'}',
'%}',
// TODO. Make the group header tabbable with tabIndex="0" and enable grid navigation "Action Mode"
// to activate it.
'<div data-groupname="{groupName:htmlEncode}" class="', Ext.baseCSSPrefix, 'grid-group-hd {collapsibleCls}" nottabindex="0" hidefocus="on" {ariaCellInnerAttr}>',
'<div class="', Ext.baseCSSPrefix, 'grid-group-title" style="{[groupTitleStyle]}" {ariaGroupTitleAttr} {[tooltip]}>',
'{[values.groupHeaderTpl.apply(values.groupRenderInfo, parent) || " "]}',
'</div>',
'</div>',
'</td>',
'</tr>',
'</tpl>',
// Only output the first row if this is *not* a collapsed group
'<tpl if="!isCollapsedGroup">',
'{%',
'values.itemClasses.length = 0;',
'this.nextTpl.applyOut(values, out, parent);',
'%}',
'</tpl>',
'<tpl if="summaryRecord">',
'{%me.outputSummaryRecord(values.summaryRecord, values, out, parent);%}',
'</tpl>',
'<tpl else>',
'{%this.nextTpl.applyOut(values, out, parent);%}',
'</tpl>', {
priority: 200,
beginRowSync: function(rowSync) {
var groupingFeature = this.groupingFeature;
rowSync.add('header', groupingFeature.eventSelector);
rowSync.add('summary', groupingFeature.summaryRowSelector);
},
syncContent: function(destRow, sourceRow, columnsToUpdate) {
destRow = Ext.fly(destRow, 'syncDest');
sourceRow = Ext.fly(sourceRow, 'syncSrc');
// eslint-disable-next-line vars-on-top
var groupingFeature = this.groupingFeature,
destHd = destRow.down(groupingFeature.eventSelector, true),
sourceHd = sourceRow.down(groupingFeature.eventSelector, true),
destSummaryRow = destRow.down(groupingFeature.summaryRowSelector, true),
sourceSummaryRow = sourceRow.down(groupingFeature.summaryRowSelector, true);
// Sync the content of header element.
if (destHd && sourceHd) {
Ext.fly(destHd).syncContent(sourceHd);
}
// Sync just the updated columns in the summary row.
if (destSummaryRow && sourceSummaryRow) {
// If we were passed a column set, only update them
if (columnsToUpdate) {
this.groupingFeature.view.updateColumns(
destSummaryRow, sourceSummaryRow, columnsToUpdate
);
}
else {
Ext.fly(destSummaryRow).syncContent(sourceSummaryRow);
}
}
}
}
],
/* eslint-enable indent, max-len */
relayedEvents: [
'groupcollapse',
'groupexpand'
],
init: function(grid) {
var me = this,
view = me.view,
store = me.gridStore = grid.getStore(),
dataSource;
view.isGrouping = store.isGrouped();
me.mixins.summary.init.call(me);
me.callParent([grid]);
view.headerCt.on({
columnhide: me.onColumnHideShow,
columnshow: me.onColumnHideShow,
columnmove: me.onColumnMove,
scope: me
});
// 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, 'groupRowTpl')).groupingFeature = me;
view.preserveScrollOnRefresh = true;
// Sparse store - we can never collapse groups
if (store.isBufferedStore) {
me.collapsible = false;
}
// If it's a local store we can build a grouped store for use as the view's dataSource
else if (!store.isEmptyStore) {
dataSource = me.createDataSource();
}
grid = grid.ownerLockable || grid;
// Before the reconfigure, rebind our GroupStore dataSource to the new store
grid.on('beforereconfigure', me.beforeReconfigure, me);
if (!view.isLockedView) {
me.gridEventRelayers = grid.relayEvents(view, me.relayedEvents);
}
view.on({
afterrender: me.afterViewRender,
scope: me,
single: true
});
me.groupRenderInfo = {};
if (store.isEmptyStore) {
return;
}
else if (dataSource) {
// Listen to dataSource groupchange so it has a chance to do any processing
// before we react to it
dataSource.on('groupchange', me.onGroupChange, me);
}
else {
me.setupStoreListeners(store);
}
me.mixins.summary.bindStore.call(me, grid, grid.getStore());
},
getGridStore: function() {
return this.gridStore;
},
indexOf: function(record) {
if (record.isCollapsedPlaceholder) {
return this.dataSource.indexOfPlaceholder(record);
}
return this.dataSource.indexOf(record);
},
indexOfPlaceholder: function(record) {
return this.dataSource.indexOfPlaceholder(record);
},
isInCollapsedGroup: function(record) {
var me = this,
store = me.getGridStore(),
result = false,
metaGroup;
if (store.isGrouped() && (metaGroup = me.getMetaGroup(record))) {
result = !!(metaGroup && metaGroup.isCollapsed);
}
return result;
},
getCache: function() {
var me = this,
id = me.getId(),
metaGroupCache = {},
groups = this.getGridStore().getGroups(),
groupingContext;
// Our group contextual data is stored as an expand on the Ext.util.Group
if (groups) {
groups.eachKey(function(key, group) {
groupingContext = group.$groupingContext || (group.$groupingContext = {});
metaGroupCache[key] = groupingContext[id];
});
}
return metaGroupCache;
},
invalidateCache: function() {
var me = this,
id = me.getId(),
groups = me.getGridStore().getGroups(),
groupingContext;
// Our group contextual data is stored as an expand on the Ext.util.Group
if (groups) {
groups.eachKey(function(key, group) {
groupingContext = group.$groupingContext;
if (groupingContext) {
groupingContext[id] = null;
}
});
}
},
vetoEvent: function(record, row, rowIndex, e) {
var shouldVeto = false;
// Do not veto mouseover/mouseout
if (e.type !== 'mouseover' && e.type !== 'mouseout' && e.type !== 'mouseenter' &&
e.type !== 'mouseleave' && e.getTarget(this.eventSelector)) {
shouldVeto = true;
}
if (this.showSummaryRow && !shouldVeto && e.getTarget(this.summaryRowSelector)) {
shouldVeto = true;
}
if (shouldVeto) {
return false;
}
},
enable: function(fromPartner) {
var me = this,
view = me.view,
store = me.getGridStore(),
currentGroupedHeader = me.hideGroupedHeader && me.getGroupedHeader(),
groupToggleMenuItem;
view.isGrouping = true;
if (view.lockingPartner) {
view.lockingPartner.isGrouping = true;
}
me.callParent();
if (me.lastGrouper) {
store.group(me.lastGrouper);
me.lastGrouper = null;
}
// Update the UI.
if (currentGroupedHeader) {
currentGroupedHeader.hide();
}
groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
// Enable the grouping feature of locking partner.
// This will help maintain the same state of the feature in normal view and locked view.
if (me.lockingPartner && !fromPartner) {
me.lockingPartner.enable(true);
}
if (groupToggleMenuItem) {
groupToggleMenuItem.setChecked(true, true);
}
},
disable: function(fromPartner) {
var me = this,
view = me.view,
store = me.getGridStore(),
currentGroupedHeader = me.hideGroupedHeader && me.getGroupedHeader(),
lastGrouper = store.getGrouper(),
groupToggleMenuItem;
view.isGrouping = false;
if (view.lockingPartner) {
view.lockingPartner.isGrouping = false;
}
me.callParent();
if (lastGrouper) {
me.lastGrouper = lastGrouper;
store.clearGrouping();
}
// Update the UI.
if (currentGroupedHeader) {
currentGroupedHeader.show();
}
groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
// Disable the grouping feature of locking partner.
// This will help maintain the same state of the feature in normal view and locked view.
if (me.lockingPartner && !fromPartner) {
me.lockingPartner.disable(true);
}
if (groupToggleMenuItem) {
groupToggleMenuItem.setChecked(false, true);
groupToggleMenuItem.disable();
}
},
// Attach events to view
afterViewRender: function() {
var me = this,
view = me.view;
view.on({
scope: me,
groupmousedown: me.onGroupMousedown,
groupclick: me.onGroupClick
});
if (me.enableGroupingMenu) {
me.injectGroupingMenu();
}
me.pruneGroupedHeader();
me.lastGrouper = me.getGridStore().getGrouper();
// If disabled in the config, disable now so the store load won't
// send the grouping query params in the request.
if (me.disabled) {
me.disable();
}
},
injectGroupingMenu: function() {
var me = this,
headerCt = me.view.headerCt;
headerCt.showMenuBy = Ext.Function.createInterceptor(headerCt.showMenuBy, me.showMenuBy);
headerCt.getMenuItems = me.getMenuItems();
},
onColumnHideShow: function(headerOwnerCt, header) {
var me = this,
view = me.view,
headerCt = view.headerCt,
menu = headerCt.getMenu(),
activeHeader = menu.activeHeader,
groupMenuItem = menu.down('#groupMenuItem'),
groupMenuMethod,
colCount = me.grid.getVisibleColumnManager().getColumns().length,
items, len, i;
// "Group by this field" must be disabled if there's only one column left visible.
if (activeHeader && groupMenuItem) {
groupMenuMethod = (
activeHeader.groupable === false ||
!activeHeader.dataIndex ||
(me.view.headerCt.getVisibleGridColumns().length < 2))
? 'disable'
: 'enable';
groupMenuItem[groupMenuMethod]();
}
// header containing TDs have to span all columns, hidden are just zero width
// Also check the colCount on the off chance that they are all hidden
if (view.rendered && colCount) {
items = view.el.query('.' + me.ctCls);
for (i = 0, len = items.length; i < len; ++i) {
items[i].colSpan = colCount;
}
}
},
// Update first and last records in groups when column moves
// Because of the RowWrap template, this will update the groups' headers and footers
onColumnMove: function() {
var me = this,
view = me.view,
groupName, groupNames, group, firstRec, lastRec, metaGroup;
if (view.getStore().isGrouped()) {
groupNames = me.getCache();
Ext.suspendLayouts();
for (groupName in groupNames) {
group = me.getGroup(groupName);
if (group) {
firstRec = group.first();
lastRec = group.last();
metaGroup = me.getMetaGroup(group);
if (metaGroup.isCollapsed) {
firstRec = lastRec = me.dataSource.getGroupPlaceholder(groupName);
}
view.refreshNode(firstRec);
if (me.showSummaryRow && lastRec !== firstRec) {
view.refreshNode(lastRec);
}
}
}
Ext.resumeLayouts(true);
}
},
showMenuBy: function(clickEvent, t, header) {
var me = this,
menu = me.getMenu(),
groupMenuItem = menu.down('#groupMenuItem'),
groupMenuMethod = (
header.groupable === false ||
!header.dataIndex ||
(me.view.headerCt.getVisibleGridColumns().length < 2))
? 'disable'
: 'enable',
groupToggleMenuItem = menu.down('#groupToggleMenuItem'),
isGrouped = me.grid.getStore().isGrouped();
groupMenuItem[groupMenuMethod]();
if (groupToggleMenuItem) {
groupToggleMenuItem.setChecked(isGrouped, true);
groupToggleMenuItem[isGrouped ? 'enable' : 'disable']();
}
},
getMenuItems: function() {
var me = this,
groupByText = me.groupByText,
disabled = me.disabled || !me.getGroupField(),
showGroupsText = me.showGroupsText,
enableNoGroups = me.enableNoGroups,
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 + 'group-by-icon',
itemId: 'groupMenuItem',
text: groupByText,
handler: me.onGroupMenuItemClick,
scope: me
});
if (enableNoGroups) {
o.push({
itemId: 'groupToggleMenuItem',
text: showGroupsText,
checked: !disabled,
checkHandler: me.onGroupToggleMenuItemClick,
scope: me
});
}
return o;
};
},
/**
* Group by the header the user has clicked on.
* @private
*/
onGroupMenuItemClick: function(menuItem, e) {
var me = this,
menu = menuItem.parentMenu,
hdr = menu.activeHeader,
view = me.view,
store = me.getGridStore();
if (me.disabled) {
me.lastGrouper = null;
me.block();
me.enable();
me.unblock();
}
view.isGrouping = true;
// First check if there is a grouper defined for the feature. This is necessary
// when the value is a complex type.
store.group(me.getGrouper(hdr.dataIndex) || hdr.dataIndex);
me.pruneGroupedHeader();
},
block: function(fromPartner) {
var me = this;
me.blockRefresh = me.view.blockRefresh = true;
if (me.lockingPartner && !fromPartner) {
me.lockingPartner.block(true);
}
},
unblock: function(fromPartner) {
var me = this;
me.blockRefresh = me.view.blockRefresh = false;
if (me.lockingPartner && !fromPartner) {
me.lockingPartner.unblock(true);
}
},
/**
* Turn on and off grouping via the menu
* @private
*/
onGroupToggleMenuItemClick: function(menuItem, checked) {
this[checked ? 'enable' : 'disable']();
},
/**
* Prunes the grouped header from the header container
* @private
*/
pruneGroupedHeader: function() {
var me = this,
header = me.getGroupedHeader();
if (me.hideGroupedHeader) {
Ext.suspendLayouts();
if (me.prunedHeader && me.prunedHeader !== header) {
me.prunedHeader.show();
}
me.prunedHeader = header;
// For cases where there was a grouping, but the grouper
// was changed to no groups.
if (header && header.rendered) {
header.hide();
}
Ext.resumeLayouts(true);
}
},
getHeaderNode: function(groupName) {
var el = this.view.getEl(),
nodes, i, len, node;
if (el) {
// Don't htmlEncode the groupName here. The name in the attribute has already been
// "decoded" so we don't need to do it.
nodes = el.query(this.eventSelector);
for (i = 0, len = nodes.length; i < len; ++i) {
node = nodes[i];
if (node.getAttribute('data-groupName') === groupName) {
return node;
}
}
}
},
getGroup: function(name) {
var store = this.getGridStore(),
value = name,
groups, group;
if (store.isGrouped()) {
if (name.isModel) {
name = name.get(store.getGroupField());
}
// If a complex type let's try to get the string from a groupFn.
if (typeof name !== 'string') {
name = store.getGrouper().getGroupString(value);
}
// BufferedStore does not own real Groups.
if (store.isBufferedStore) {
groups = store.groups || (store.groups = {});
group = groups[name] || (groups[name] = {
getGroupKey: function() {
return name;
}
});
}
else {
group = store.getGroups().getByKey(name);
}
}
return group;
},
// Groupers may be defined on the feature itself if the datIndex is a complex type.
/**
* @private
*
*/
getGrouper: function(dataIndex) {
var groupers = this.groupers;
if (!groupers) {
return null;
}
return Ext.Array.findBy(groupers, function(grouper) {
return grouper.property === dataIndex;
});
},
getGroupField: function() {
return this.getGridStore().getGroupField();
},
getMetaGroup: function(group) {
var me = this,
id = me.getId(),
gridStore,
key, metaGroup, Model, modelData, groupPlaceholder,
aggregateRecord, groupingContext;
// Get the group from a member record or the group name
if (group.isModel || typeof group === 'string') {
group = me.getGroup(group);
}
if (group) {
key = group.getGroupKey();
groupingContext = group.$groupingContext || (group.$groupingContext = {});
metaGroup = groupingContext[id];
if (!metaGroup) {
gridStore = me.getGridStore();
Model = gridStore.getModel();
// Add a placeholder record which represents the group if collapsed.
modelData = {};
modelData[me.getGroupField()] = key;
// Create the two records a group needs to produce a UI.
// One to represent a collapsed group.
// And one to append to the end of an expanded group.
groupPlaceholder = new Model(modelData, gridStore.session, true);
groupPlaceholder.isNonData = groupPlaceholder.isCollapsedPlaceholder = true;
groupPlaceholder.groupKey = key;
aggregateRecord = new Ext.data.Model(modelData);
aggregateRecord.isNonData = aggregateRecord.isSummary = true;
aggregateRecord.groupKey = key;
metaGroup = groupingContext[id] = {
placeholder: groupPlaceholder,
isCollapsed: false,
lastGroup: null,
lastGroupGeneration: null,
lastFilterGeneration: null,
aggregateRecord: aggregateRecord
};
}
}
return metaGroup;
},
/**
* Returns `true` if the named group is expanded.
* @param {String} groupName The group name. This is the value of
* the {@link Ext.data.Store#groupField groupField}.
* @return {Boolean} `true` if the group defined by that value is expanded.
*/
isExpanded: function(groupName) {
return !this.getMetaGroup(groupName).isCollapsed;
},
/**
* Expand a group
* @param {String} groupName The group name.
* @param {Object} [options] Pass when the group should be scrolled into view.
* This contains flags for postProcessing the group's first row after
* expansion. See {@link Ext.panel.Table#ensureVisible} for details. *note:*
* a boolean may be passed to indicate whether to focus the target group after expand.
*/
expand: function(groupName, options) {
this.doCollapseExpand(false, groupName, options);
},
/**
* Expand all groups
*/
expandAll: function() {
var me = this,
metaGroupCache = me.getCache(),
lockingPartner = me.lockingPartner,
groupName;
// Clear all collapsed flags.
// metaGroupCache is shared between two lockingPartners
for (groupName in metaGroupCache) {
if (metaGroupCache.hasOwnProperty(groupName)) {
metaGroupCache[groupName].isCollapsed = false;
}
}
// We do not need to inform our lockingPartner.
// It shares the same group cache - it will have the same set of expanded groups.
Ext.suspendLayouts();
me.dataSource.onDataChanged();
Ext.resumeLayouts(true);
// Fire event for all groups post expand
for (groupName in metaGroupCache) {
if (metaGroupCache.hasOwnProperty(groupName)) {
me.afterCollapseExpand(false, groupName);
if (lockingPartner) {
lockingPartner.afterCollapseExpand(false, groupName);
}
}
}
},
/**
* Collapse a group
* @param {String} groupName The group name.
* @param {Object} options. Pass when the group should be scrolled into view.
* This contains flags for postProcessing the group's header row after
* collapsing. See {@link Ext.panel.Table#ensureVisible} for details.
*/
collapse: function(groupName, options) {
this.doCollapseExpand(true, groupName, options);
},
/**
* @private
* Returns true if all groups are collapsed
* @return {boolean}
*/
isAllCollapsed: function() {
var me = this,
metaGroupCache = me.getCache(),
groupName;
// Clear all collapsed flags.
// metaGroupCache is shared between two lockingPartners
for (groupName in metaGroupCache) {
if (metaGroupCache.hasOwnProperty(groupName)) {
if (!metaGroupCache[groupName].isCollapsed) {
return false;
}
}
}
return true;
},
/**
* @private
* Returns true if all groups are expanded
* @return {boolean}
*/
isAllExpanded: function() {
var me = this,
metaGroupCache = me.getCache(),
groupName;
// Clear all collapsed flags.
// metaGroupCache is shared between two lockingPartners
for (groupName in metaGroupCache) {
if (metaGroupCache.hasOwnProperty(groupName)) {
if (metaGroupCache[groupName].isCollapsed) {
return false;
}
}
}
return true;
},
/**
* Collapse all groups
*/
collapseAll: function() {
var me = this,
metaGroupCache = me.getCache(),
groupName,
lockingPartner = me.lockingPartner;
// Set all collapsed flags
// metaGroupCache is shared between two lockingPartners
for (groupName in metaGroupCache) {
if (metaGroupCache.hasOwnProperty(groupName)) {
metaGroupCache[groupName].isCollapsed = true;
}
}
// We do not need to inform our lockingPartner.
// It shares the same group cache - it will have the same set of collapsed groups.
Ext.suspendLayouts();
me.dataSource.onDataChanged();
Ext.resumeLayouts(true);
// Fire event for all groups post collapse
for (groupName in metaGroupCache) {
if (metaGroupCache.hasOwnProperty(groupName)) {
me.afterCollapseExpand(true, groupName);
if (lockingPartner) {
lockingPartner.afterCollapseExpand(true, groupName);
}
}
}
},
doCollapseExpand: function(collapsed, groupName, options) {
var me = this,
lockingPartner = me.lockingPartner,
group = me.getGroup(groupName);
if (options === true) {
options = {
focus: true
};
}
// metaGroupCache is shared between two lockingPartners.
if (me.getMetaGroup(group).isCollapsed !== collapsed) {
me.isExpandingOrCollapsing = true;
// The GroupStore is shared by partnered Grouping features, so this will refresh
// both sides. We only want one layout as a result though, so suspend layouts
// while refreshing.
Ext.suspendLayouts();
if (collapsed) {
me.dataSource.collapseGroup(group);
}
else {
me.dataSource.expandGroup(group);
}
Ext.resumeLayouts(true);
// Sync the group state and focus the row if requested.
me.afterCollapseExpand(collapsed, groupName, 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(collapsed, groupName, options);
}
me.isExpandingOrCollapsing = false;
}
},
afterCollapseExpand: function(collapsed, groupName, options) {
var me = this,
view = me.view,
header, record;
header = me.getHeaderNode(groupName);
view.fireEvent(collapsed ? 'groupcollapse' : 'groupexpand', view, header, groupName);
if (options) {
// NavigationModel cannot focus a collapsed group header. They are not navigable yet.
if (collapsed) {
options.focus = false;
record = me.getMetaGroup(groupName).placeholder;
}
else {
record = me.getGroup(groupName).getAt(0);
}
me.grid.ensureVisible(record, options);
}
},
onGroupChange: function(store, grouper) {
// If changed to a non-null grouper, the Store will be sorted (either remotely or locally),
// and therefore fire a refresh. If changed to a null grouper - setGrouper(null) -
// that causes no mutation to a store, so we must refresh the view to remove
// the group headers/footers.
if (!grouper) {
this.view.ownerGrid.getView().refreshView();
}
else {
this.lastGrouper = grouper;
}
this.pruneGroupedHeader();
},
/**
* Gets the related menu item for a dataIndex
* @private
* @return {Ext.grid.header.Container} The header
*/
getMenuItem: function(dataIndex) {
var view = this.view,
header = view.headerCt.down('gridcolumn[dataIndex=' + dataIndex + ']'),
menu = view.headerCt.getMenu();
return header ? menu.down('menuitem[headerId=' + header.id + ']') : null;
},
onGroupKey: function(keyCode, event) {
var me = this,
groupName = me.getGroupName(event.target);
if (groupName) {
me.onGroupClick(me.view, event.target, groupName, event);
}
},
/**
* Prevent focusing - it causes a scroll between mousedown and mouseup.
* @private
*/
onGroupMousedown: function(view, rowElement, groupName, e) {
e.preventDefault();
},
/**
* Toggle between expanded/collapsed state when clicking on
* the group.
* @private
*/
onGroupClick: function(view, rowElement, groupName, e) {
var me = this,
metaGroupCache, groupIsCollapsed, g;
if (!me.collapsible) {
return;
}
metaGroupCache = me.getCache();
groupIsCollapsed = !me.isExpanded(groupName);
// CTRL means collapse all others.
if (e.ctrlKey) {
Ext.suspendLayouts();
for (g in metaGroupCache) {
if (g === groupName) {
if (groupIsCollapsed) {
me.expand(groupName);
}
}
else if (!metaGroupCache[g].isCollapsed) {
me.doCollapseExpand(true, g, false);
}
}
Ext.resumeLayouts(true);
}
else {
me[groupIsCollapsed ? 'expand' : 'collapse'](groupName);
}
},
setupRowData: function(record, idx, rowValues) {
var me = this,
recordIndex = rowValues.recordIndex,
data = me.refreshData,
groupRenderInfo = me.groupRenderInfo,
header = data.header,
groupField = data.groupField,
store = me.getGridStore(),
dataSource = me.view.dataSource,
isBufferedStore = dataSource.isBufferedStore,
column = me.grid.columnManager.getHeaderByDataIndex(groupField),
hasRenderer = !!(column && column.renderer),
groupKey = record.groupKey,
// MetaGroup placeholder records store the groupKey not a reference.
// See EXTJS-18655.
group = record.isCollapsedPlaceholder && Ext.isDefined(groupKey)
? me.getGroup(groupKey)
: record.group,
grouper, groupName, prev, next, items;
rowValues.isCollapsedGroup = false;
rowValues.summaryRecord = rowValues.groupHeaderCls = null;
if (data.doGrouping) {
grouper = store.getGrouper();
// This is a placeholder record which represents a whole collapsed group
// It is a special case.
if (record.isCollapsedPlaceholder) {
groupName = group.getGroupKey();
items = group.items;
record = items[0];
rowValues.isFirstRow = rowValues.isLastRow = true;
rowValues.groupHeaderCls = me.hdCollapsedCls;
rowValues.isCollapsedGroup = rowValues.needsWrap = true;
rowValues.groupName = groupName;
rowValues.groupRenderInfo = groupRenderInfo;
groupRenderInfo.groupField = groupField;
groupRenderInfo.groupValue = record.get(groupField);
groupRenderInfo.name = groupRenderInfo.renderedGroupValue = hasRenderer
? column.renderer(groupRenderInfo.groupValue, {}, record)
: groupName;
groupRenderInfo.columnName = header ? header.text : groupField;
rowValues.collapsibleCls =
me.collapsible ? me.collapsibleCls : me.hdNotCollapsibleCls;
groupRenderInfo.rows = groupRenderInfo.children = items;
if (me.showSummaryRow) {
rowValues.summaryRecord = data.summaryData[groupName];
}
return;
}
groupName = grouper.getGroupString(record);
// If caused by an update event on the first or last records of a group
// fired by a GroupStore, the record's group will be attached.
if (group) {
items = group.items;
rowValues.isFirstRow = record === items[0];
rowValues.isLastRow = record === items[items.length - 1];
}
else {
// See if the current record is the last in the group
rowValues.isFirstRow = recordIndex === 0;
if (!rowValues.isFirstRow) {
prev = store.getAt(recordIndex - 1);
// If the previous row is of a different group, then we're at the first
// for a new group
if (prev) {
// Must use Model's comparison because Date objects are never equal
rowValues.isFirstRow =
!prev.isEqual(grouper.getGroupString(prev), groupName);
}
}
// See if the current record is the last in the group
// eslint-disable-next-line max-len
rowValues.isLastRow = recordIndex === (isBufferedStore ? store.getTotalCount() : store.getCount()) - 1;
if (!rowValues.isLastRow) {
next = store.getAt(recordIndex + 1);
if (next) {
// Must use Model's comparison because Date objects are never equal
rowValues.isLastRow =
!next.isEqual(grouper.getGroupString(next), groupName);
}
}
}
if (rowValues.isFirstRow) {
groupRenderInfo.groupField = groupField;
groupRenderInfo.name = groupRenderInfo.renderedGroupValue =
hasRenderer ? column.renderer(record.get(groupField), {}, record) : groupName;
groupRenderInfo.groupValue = record.get(groupField);
groupRenderInfo.columnName = header ? header.text : groupField;
rowValues.collapsibleCls =
me.collapsible ? me.collapsibleCls : me.hdNotCollapsibleCls;
rowValues.groupName = groupName;
if (!me.isExpanded(groupName)) {
rowValues.itemClasses.push(me.hdCollapsedCls);
rowValues.isCollapsedGroup = true;
}
// We only get passed a GroupStore if the store is not buffered.
if (isBufferedStore) {
groupRenderInfo.rows = groupRenderInfo.children = [];
}
else {
groupRenderInfo.rows = groupRenderInfo.children =
me.getRecordGroup(record).items;
}
rowValues.groupRenderInfo = groupRenderInfo;
}
if (rowValues.isLastRow) {
// Add the group's summary record to the last record in the group
if (me.showSummaryRow) {
rowValues.summaryRecord = data.summaryData[groupName];
rowValues.itemClasses.push(Ext.baseCSSPrefix + 'grid-group-last');
}
}
rowValues.needsWrap = (rowValues.isFirstRow || rowValues.summaryRecord);
}
},
setup: function(rows, rowValues) {
var me = this,
data = me.refreshData,
view = rowValues.view,
// Need to check if groups have been added since init(), such as in the case
// of stateful grids.
isGrouping = view.isGrouping = !me.disabled && me.getGridStore().isGrouped(),
bufferedRenderer = view.bufferedRenderer;
me.skippedRows = 0;
if (bufferedRenderer) {
bufferedRenderer.variableRowHeight = view.hasVariableRowHeight() || isGrouping;
}
data.groupField = me.getGroupField();
data.header = me.getGroupedHeader(data.groupField);
data.doGrouping = isGrouping;
rowValues.groupHeaderTpl = Ext.XTemplate.getTpl(me, 'groupHeaderTpl');
if (isGrouping && me.showSummaryRow) {
data.summaryData = me.generateSummaryData();
}
},
cleanup: function(rows, rowValues) {
var data = this.refreshData;
rowValues.groupRenderInfo = rowValues.groupHeaderTpl = rowValues.isFirstRow = null;
data.groupField = data.header = data.summaryData = null;
},
/**
* Used by the Grouping Feature when {@link #cfg!showSummaryRow} is `true`.
*
* Generates group summary data for the whole store.
* @private
* @return {Object} An object hash keyed by group name containing summary records.
*/
generateSummaryData: function() {
var me = this,
store = me.getGridStore(),
filters = store.getFilters(),
groups = store.getGroups().items,
groupField = me.getGroupField(),
lockingPartner = me.lockingPartner,
updateSummaryRow = me.updateSummaryRow,
data = {},
ownerCt = me.view.ownerCt,
columnsChanged = me.didColumnsChange(),
i, len, group, metaGroup, record, hasRemote, remoteData;
/**
* @cfg {String} remoteRoot
* The name of the property which contains the Array of summary objects.
* It allows to use server-side calculated summaries.
*
* Alternatively, the {@link Ext.data.reader.Reader#cfg!summaryRootProperty} can
* be used to specify the container for summary data from the server. This allows
* the {@link Ext.data.Model Model} to determine this when configuring its proxy.
*/
if (me.remoteRoot || store.getProxy().getReader().getSummaryRootProperty()) {
remoteData = me.mixins.summary.generateSummaryData.call(me, groupField);
hasRemote = !!remoteData;
}
for (i = 0, len = groups.length; i < len; ++i) {
group = groups[i];
metaGroup = me.getMetaGroup(group);
// Something has changed or it doesn't exist, populate it.
if (updateSummaryRow || hasRemote || store.updating || me.grid.reconfiguring ||
columnsChanged || me.didGroupChange(group, metaGroup, filters)) {
record = me.populateRecord(group, metaGroup, remoteData);
// Clear the dirty state of the group if this is the only Summary,
// or this is the right hand (normal grid's) summary.
if (!lockingPartner || (ownerCt === ownerCt.ownerLockable.normalGrid)) {
metaGroup.lastGroup = group;
metaGroup.lastGroupGeneration = group.generation;
metaGroup.lastFilterGeneration = filters.generation;
}
}
else {
record = metaGroup.aggregateRecord;
}
data[group.getGroupKey()] = record;
}
me.updateSummaryRow = false;
return data;
},
getGroupName: function(element) {
var me = this,
view = me.view,
eventSelector = me.eventSelector,
targetEl, row;
// See if element is, or is within a group header. If so, we can extract its name
targetEl = Ext.fly(element).findParent(eventSelector);
if (!targetEl) {
// Otherwise, navigate up to the row and look down to see if we can find it
row = Ext.fly(element).findParent(view.itemSelector);
if (row) {
targetEl = row.down(eventSelector, true);
}
}
if (targetEl) {
// Explicitly not html decoding here. Once the attribute value is set, when we
// retrieve it, the value is already automatically "unescaped", so doing it here
// would be double.
return targetEl.getAttribute('data-groupname');
}
},
/**
* Returns the group data object for the group to which the passed record belongs
* **if the Store is grouped**.
*
* @param {Ext.data.Model} record The record for which to return group information.
* @return {Object} A single group data block as returned from
* {@link Ext.data.Store#getGroups Store.getGroups}. Returns `undefined` if the Store
* is not grouped.
*
*/
getRecordGroup: function(record) {
var store = this.getGridStore(),
grouper = store.getGrouper();
if (grouper) {
return store.getGroups().getItemGroup(record);
}
},
getGroupedHeader: function(groupField) {
var me = this,
headerCt = me.view.headerCt,
partner = me.lockingPartner,
selector, header;
groupField = groupField || me.getGroupField();
if (groupField) {
selector = '[dataIndex=' + groupField + ']';
header = headerCt.down(selector);
// The header may exist in the locking partner, so check there as well
if (!header && partner) {
header = partner.view.headerCt.down(selector);
}
}
return header || null;
},
getFireEventArgs: function(type, view, targetEl, e) {
return [type, view, targetEl, this.getGroupName(targetEl), e];
},
destroy: function() {
var me = this,
dataSource = me.dataSource;
Ext.destroy(me.gridEventRelayers);
me.gridEventRelayers = null;
me.storeListeners = Ext.destroy(me.storeListeners);
me.view = me.prunedHeader = me.grid = me.dataSource = me.groupers = null;
if (dataSource && !dataSource.destroyed) {
dataSource.bindStore(null);
Ext.destroy(dataSource);
}
me.callParent();
},
beforeReconfigure: function(grid, store, columns, oldStore, oldColumns) {
var me = this,
view = me.view,
dataSource = me.dataSource,
bufferedRenderer = view.bufferedRenderer,
bufferedStore;
if (store && store !== oldStore) {
me.gridStore = store;
bufferedStore = store.isBufferedStore;
if (me.storeListeners) {
Ext.destroy(me.storeListeners);
}
// Grouping involves injecting a dataSource in early
if (!oldStore.isEmptyStore && bufferedStore !== oldStore.isBufferedStore) {
Ext.raise('Cannot reconfigure grouping switching between buffered ' +
'and non-buffered stores');
}
if (!dataSource) {
if (bufferedStore) {
me.collapsible = false;
me.setupStoreListeners(store);
}
else {
dataSource = me.createDataSource();
dataSource.on('groupchange', me.onGroupChange, me);
if (bufferedRenderer) {
bufferedRenderer.bindStore(dataSource);
}
}
}
if (!bufferedStore) {
view.isGrouping = !!store.getGrouper();
dataSource.bindStore(store);
}
me.pruneGroupedHeader();
me.mixins.summary.bindStore.call(me, grid, store);
}
},
createDataSource: function() {
var me = this,
view = me.view,
lockPartner = me.lockingPartner,
dataSource;
// Share the GroupStore between both sides of a locked grid
if (lockPartner && lockPartner.dataSource) {
me.dataSource = view.dataSource = dataSource = lockPartner.dataSource;
}
else {
me.dataSource = view.dataSource = dataSource =
new Ext.grid.feature.GroupStore(me, me.gridStore);
}
return dataSource;
},
populateRecord: function(group, metaGroup, remoteData) {
var me = this,
view = me.grid.ownerLockable ? me.grid.ownerLockable.view : me.view,
store = me.getGridStore(),
record = metaGroup.aggregateRecord,
// Use the full column set, regardless of locking
columns = view.headerCt.getGridColumns(),
len = columns.length,
groupName = group.getGroupKey(),
groupData, field, i, column, fieldName, summaryValue;
record.beginEdit();
if (remoteData) {
// Remote summary grouping provides the grouping totals so there's no need to
// iterate through the columns to map the column's dataIndex to the field name.
// Instead, enumerate the grouping record and set the field in the aggregate
// record for each one.
groupData = remoteData[groupName];
for (field in groupData) {
if (groupData.hasOwnProperty(field)) {
if (field !== record.idProperty) {
record.set(field, groupData[field]);
}
}
}
}
// Here we iterate through the columns with two objectives:
// 1. For local grouping, get the summary for each column and update the record.
// 2. For both local and remote grouping, set the summary data object
// which is passed to the summaryRenderer (if defined).
for (i = 0; i < len; ++i) {
column = columns[i];
// Use the column id if there's no mapping, could be a calculated field
fieldName = column.dataIndex || column.getItemId();
// We need to capture the summary value because it could get overwritten when
// setting on the model if there is a convert() method on the model.
if (!remoteData) {
summaryValue = me.getSummary(store, column.summaryType, fieldName, group);
record.set(fieldName, summaryValue);
}
else {
// For remote groupings, just get the value from the model.
summaryValue = record.get(column.dataIndex);
}
// Capture the columnId:value for the summaryRenderer in the summaryData object.
me.setSummaryData(record, column.getItemId(), summaryValue, groupName);
}
// Poke on the owner group for easy lookup in this.createRenderer().
record.ownerGroup = groupName;
record.endEdit(true);
record.commit();
return record;
},
privates: {
didGroupChange: function(group, metaGroup, filters) {
var ret = true;
if (group === metaGroup.lastGroup) {
ret = metaGroup.lastGroupGeneration !== group.generation ||
metaGroup.lastFilterGeneration !== filters.generation;
}
return ret;
},
didColumnsChange: function() {
var me = this,
result = (me.view.headerCt.items.generation !== me.lastHeaderCtGeneration);
me.lastHeaderCtGeneration = me.view.headerCt.items.generation;
return result;
},
setupStoreListeners: function(store) {
var me = this;
me.storeListeners = store.on({
groupchange: me.onGroupChange,
scope: me,
destroyable: true
});
}
}
});