2 * @class Ext.grid.feature.Grouping
3 * @extends Ext.grid.feature.Feature
5 * This feature allows to display the grid rows aggregated into groups as specified by the {@link Ext.data.Store#groupers}
6 * specified on the Store. The group will show the title for the group name and then the appropriate records for the group
7 * underneath. The groups can also be expanded and collapsed.
10 * This feature adds several extra events that will be fired on the grid to interact with the groups:
12 * - {@link #groupclick}
13 * - {@link #groupdblclick}
14 * - {@link #groupcontextmenu}
15 * - {@link #groupexpand}
16 * - {@link #groupcollapse}
18 * ## Menu Augmentation
19 * This feature adds extra options to the grid column menu to provide the user with functionality to modify the grouping.
20 * This can be disabled by setting the {@link #enableGroupingMenu} option. The option to disallow grouping from being turned off
21 * by thew user is {@link #enableNoGroups}.
23 * ## Controlling Group Text
24 * The {@link #groupHeaderTpl} is used to control the rendered title for each group. It can modified to customized
25 * the default display.
29 * var groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
30 * groupHeaderTpl: 'Group: {name} ({rows.length})', //print the number of items in the group
31 * startCollapsed: true // start all groups collapsed
35 * @author Nicolas Ferrero
37 Ext.define('Ext.grid.feature.Grouping', {
38 extend: 'Ext.grid.feature.Feature',
39 alias: 'feature.grouping',
42 eventSelector: '.' + Ext.baseCSSPrefix + 'grid-group-hd',
44 constructor: function() {
45 this.collapsedState = {};
46 this.callParent(arguments);
51 * @param {Ext.view.Table} view
52 * @param {HTMLElement} node
53 * @param {String} group The name of the group
54 * @param {Ext.EventObject} e
58 * @event groupdblclick
59 * @param {Ext.view.Table} view
60 * @param {HTMLElement} node
61 * @param {String} group The name of the group
62 * @param {Ext.EventObject} e
66 * @event groupcontextmenu
67 * @param {Ext.view.Table} view
68 * @param {HTMLElement} node
69 * @param {String} group The name of the group
70 * @param {Ext.EventObject} e
74 * @event groupcollapse
75 * @param {Ext.view.Table} view
76 * @param {HTMLElement} node
77 * @param {String} group The name of the group
78 * @param {Ext.EventObject} e
83 * @param {Ext.view.Table} view
84 * @param {HTMLElement} node
85 * @param {String} group The name of the group
86 * @param {Ext.EventObject} e
90 * @cfg {String} groupHeaderTpl
91 * Template snippet, this cannot be an actual template. {name} will be replaced with the current group.
92 * Defaults to 'Group: {name}'
94 groupHeaderTpl: 'Group: {name}',
97 * @cfg {Number} depthToIndent
98 * Number of pixels to indent per grouping level
102 collapsedCls: Ext.baseCSSPrefix + 'grid-group-collapsed',
103 hdCollapsedCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsed',
106 * @cfg {String} groupByText Text displayed in the grid header menu for grouping by header
107 * (defaults to 'Group By This Field').
109 groupByText : 'Group By This Field',
111 * @cfg {String} showGroupsText Text displayed in the grid header for enabling/disabling grouping
112 * (defaults to 'Show in Groups').
114 showGroupsText : 'Show in Groups',
117 * @cfg {Boolean} hideGroupedHeader<tt>true</tt> to hide the header that is currently grouped (defaults to <tt>false</tt>)
119 hideGroupedHeader : false,
122 * @cfg {Boolean} startCollapsed <tt>true</tt> to start all groups collapsed (defaults to <tt>false</tt>)
124 startCollapsed : false,
127 * @cfg {Boolean} enableGroupingMenu <tt>true</tt> to enable the grouping control in the header menu (defaults to <tt>true</tt>)
129 enableGroupingMenu : true,
132 * @cfg {Boolean} enableNoGroups <tt>true</tt> to allow the user to turn off grouping (defaults to <tt>true</tt>)
134 enableNoGroups : true,
142 if (me.lastGroupIndex) {
143 store.group(me.lastGroupIndex);
146 groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
147 groupToggleMenuItem.setChecked(true, true);
151 disable: function() {
158 lastGroup = store.groupers.first();
160 me.lastGroupIndex = lastGroup.property;
161 store.groupers.clear();
165 groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
166 groupToggleMenuItem.setChecked(true, true);
167 groupToggleMenuItem.setChecked(false, true);
171 getFeatureTpl: function(values, parent, x, xcount) {
175 '<tpl if="typeof rows !== \'undefined\'">',
177 '<tr class="' + Ext.baseCSSPrefix + 'grid-group-hd ' + (me.startCollapsed ? me.hdCollapsedCls : '') + ' {hdCollapsedCls}"><td class="' + Ext.baseCSSPrefix + 'grid-cell" colspan="' + parent.columns.length + '" {[this.indentByDepth(values)]}><div class="' + Ext.baseCSSPrefix + 'grid-cell-inner"><div class="' + Ext.baseCSSPrefix + 'grid-group-title">{collapsed}' + me.groupHeaderTpl + '</div></div></td></tr>',
178 // this is the rowbody
179 '<tr id="{viewId}-gp-{name}" class="' + Ext.baseCSSPrefix + 'grid-group-body ' + (me.startCollapsed ? me.collapsedCls : '') + ' {collapsedCls}"><td colspan="' + parent.columns.length + '">{[this.recurse(values)]}</td></tr>',
184 getFragmentTpl: function() {
186 indentByDepth: this.indentByDepth,
187 depthToIndent: this.depthToIndent
191 indentByDepth: function(values) {
192 var depth = values.depth || 0;
193 return 'style="padding-left:'+ depth * this.depthToIndent + 'px;"';
196 // Containers holding these components are responsible for
197 // destroying them, we are just deleting references.
198 destroy: function() {
202 delete me.prunedHeader;
205 // perhaps rename to afterViewRender
206 attachEvents: function() {
209 header, headerId, menu, menuItem;
213 groupclick: me.onGroupClick,
214 rowfocus: me.onRowFocus
216 view.store.on('groupchange', me.onGroupChange, me);
218 me.pruneGroupedHeader();
220 if (me.enableGroupingMenu) {
221 me.injectGroupingMenu();
224 if (me.hideGroupedHeader) {
225 header = view.headerCt.down('gridcolumn[dataIndex=' + me.getGroupField() + ']');
226 headerId = header.id;
227 menu = view.headerCt.getMenu();
228 menuItem = menu.down('menuitem[headerId='+ headerId +']');
230 menuItem.setChecked(false);
235 injectGroupingMenu: function() {
238 headerCt = view.headerCt;
239 headerCt.showMenuBy = me.showMenuBy;
240 headerCt.getMenuItems = me.getMenuItems();
243 showMenuBy: function(t, header) {
244 var menu = this.getMenu(),
245 groupMenuItem = menu.down('#groupMenuItem'),
246 groupableMth = header.groupable === false ? 'disable' : 'enable';
248 groupMenuItem[groupableMth]();
249 Ext.grid.header.Container.prototype.showMenuBy.apply(this, arguments);
252 getMenuItems: function() {
254 groupByText = me.groupByText,
255 disabled = me.disabled,
256 showGroupsText = me.showGroupsText,
257 enableNoGroups = me.enableNoGroups,
258 groupMenuItemClick = Ext.Function.bind(me.onGroupMenuItemClick, me),
259 groupToggleMenuItemClick = Ext.Function.bind(me.onGroupToggleMenuItemClick, me);
261 // runs in the scope of headerCt
263 var o = Ext.grid.header.Container.prototype.getMenuItems.call(this);
265 itemId: 'groupMenuItem',
267 handler: groupMenuItemClick
269 if (enableNoGroups) {
271 itemId: 'groupToggleMenuItem',
272 text: showGroupsText,
274 checkHandler: groupToggleMenuItemClick
283 * Group by the header the user has clicked on.
286 onGroupMenuItemClick: function(menuItem, e) {
287 var menu = menuItem.parentMenu,
288 hdr = menu.activeHeader,
291 delete this.lastGroupIndex;
293 view.store.group(hdr.dataIndex);
294 this.pruneGroupedHeader();
299 * Turn on and off grouping via the menu
302 onGroupToggleMenuItemClick: function(menuItem, checked) {
303 this[checked ? 'enable' : 'disable']();
307 * Prunes the grouped header from the header container
310 pruneGroupedHeader: function() {
314 groupField = me.getGroupField(),
315 headerCt = view.headerCt,
316 header = headerCt.down('header[dataIndex=' + groupField + ']');
319 if (me.prunedHeader) {
320 me.prunedHeader.show();
322 me.prunedHeader = header;
327 getGroupField: function(){
328 var group = this.view.store.groupers.first();
330 return group.property;
336 * When a row gains focus, expand the groups above it
339 onRowFocus: function(rowIdx) {
340 var node = this.view.getNode(rowIdx),
341 groupBd = Ext.fly(node).up('.' + this.collapsedCls);
344 // for multiple level groups, should expand every groupBd
346 this.expand(groupBd);
351 * Expand a group by the groupBody
352 * @param {Ext.core.Element} groupBd
355 expand: function(groupBd) {
358 grid = view.up('gridpanel'),
359 groupBdDom = Ext.getDom(groupBd);
361 me.collapsedState[groupBdDom.id] = false;
363 groupBd.removeCls(me.collapsedCls);
364 groupBd.prev().removeCls(me.hdCollapsedCls);
366 grid.determineScrollbars();
367 grid.invalidateScroller();
368 view.fireEvent('groupexpand');
372 * Collapse a group by the groupBody
373 * @param {Ext.core.Element} groupBd
376 collapse: function(groupBd) {
379 grid = view.up('gridpanel'),
380 groupBdDom = Ext.getDom(groupBd);
382 me.collapsedState[groupBdDom.id] = true;
384 groupBd.addCls(me.collapsedCls);
385 groupBd.prev().addCls(me.hdCollapsedCls);
387 grid.determineScrollbars();
388 grid.invalidateScroller();
389 view.fireEvent('groupcollapse');
392 onGroupChange: function(){
397 * Toggle between expanded/collapsed state when clicking on
401 onGroupClick: function(view, group, idx, foo, e) {
403 toggleCls = me.toggleCls,
404 groupBd = Ext.fly(group.nextSibling, '_grouping');
406 if (groupBd.hasCls(me.collapsedCls)) {
409 me.collapse(groupBd);
413 // Injects isRow and closeRow into the metaRowTpl.
414 getMetaRowTplFragments: function() {
417 closeRow: this.closeRow
421 // injected into rowtpl and wrapped around metaRowTpl
422 // becomes part of the standard tpl
424 return '<tpl if="typeof rows === \'undefined\'">';
427 // injected into rowtpl and wrapped around metaRowTpl
428 // becomes part of the standard tpl
429 closeRow: function() {
433 // isRow and closeRow are injected via getMetaRowTplFragments
434 mutateMetaRowTpl: function(metaRowTpl) {
435 metaRowTpl.unshift('{[this.isRow()]}');
436 metaRowTpl.push('{[this.closeRow()]}');
439 // injects an additional style attribute via tdAttrKey with the proper
441 getAdditionalData: function(data, idx, record, orig) {
442 var view = this.view,
444 col = hCt.items.getAt(0),
446 tdAttrKey = col.id + '-tdAttr';
448 // maintain the current tdAttr that a user may ahve set.
449 o[tdAttrKey] = this.indentByDepth(data) + " " + (orig[tdAttrKey] ? orig[tdAttrKey] : '');
450 o.collapsed = 'true';
454 // return matching preppedRecords
455 getGroupRows: function(group, records, preppedRecords, fullWidth) {
457 children = group.children,
458 rows = group.rows = [],
460 group.viewId = view.id;
462 Ext.Array.each(records, function(record, idx) {
463 if (Ext.Array.indexOf(children, record) != -1) {
464 rows.push(Ext.apply(preppedRecords[idx], {
469 delete group.children;
470 group.fullWidth = fullWidth;
471 if (me.collapsedState[view.id + '-gp-' + group.name]) {
472 group.collapsedCls = me.collapsedCls;
473 group.hdCollapsedCls = me.hdCollapsedCls;
479 // return the data in a grouped format.
480 collectData: function(records, preppedRecords, startIndex, fullWidth, o) {
482 store = me.view.store,
485 if (!me.disabled && store.isGrouped()) {
486 groups = store.getGroups();
487 Ext.Array.each(groups, function(group, idx){
488 me.getGroupRows(group, records, preppedRecords, fullWidth);
498 // adds the groupName to the groupclick, groupdblclick, groupcontextmenu
499 // events that are fired on the view. Chose not to return the actual
500 // group itself because of its expense and because developers can simply
501 // grab the group via store.getGroups(groupName)
502 getFireEventArgs: function(type, view, featureTarget, e) {
503 var returnArray = [type, view, featureTarget],
504 groupBd = Ext.fly(featureTarget.nextSibling, '_grouping'),
505 groupBdId = Ext.getDom(groupBd).id,
506 prefix = view.id + '-gp-',
507 groupName = groupBdId.substr(prefix.length);
509 returnArray.push(groupName, e);