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 {Number} unused
54 * @param {Number} unused
55 * @param {Ext.EventObject} e
59 * @event groupdblclick
60 * @param {Ext.view.Table} view
61 * @param {HTMLElement} node
62 * @param {Number} unused
63 * @param {Number} unused
64 * @param {Ext.EventObject} e
68 * @event groupcontextmenu
69 * @param {Ext.view.Table} view
70 * @param {HTMLElement} node
71 * @param {Number} unused
72 * @param {Number} unused
73 * @param {Ext.EventObject} e
77 * @event groupcollapse
78 * @param {Ext.view.Table} view
79 * @param {HTMLElement} node
80 * @param {Number} unused
81 * @param {Number} unused
82 * @param {Ext.EventObject} e
87 * @param {Ext.view.Table} view
88 * @param {HTMLElement} node
89 * @param {Number} unused
90 * @param {Number} unused
91 * @param {Ext.EventObject} e
95 * @cfg {String} groupHeaderTpl
96 * Template snippet, this cannot be an actual template. {name} will be replaced with the current group.
97 * Defaults to 'Group: {name}'
99 groupHeaderTpl: 'Group: {name}',
102 * @cfg {Number} depthToIndent
103 * Number of pixels to indent per grouping level
107 collapsedCls: Ext.baseCSSPrefix + 'grid-group-collapsed',
108 hdCollapsedCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsed',
111 * @cfg {String} groupByText Text displayed in the grid header menu for grouping by header
112 * (defaults to 'Group By This Field').
114 groupByText : 'Group By This Field',
116 * @cfg {String} showGroupsText Text displayed in the grid header for enabling/disabling grouping
117 * (defaults to 'Show in Groups').
119 showGroupsText : 'Show in Groups',
122 * @cfg {Boolean} hideGroupedHeader<tt>true</tt> to hide the header that is currently grouped (defaults to <tt>false</tt>)
124 hideGroupedHeader : false,
127 * @cfg {Boolean} startCollapsed <tt>true</tt> to start all groups collapsed (defaults to <tt>false</tt>)
129 startCollapsed : false,
132 * @cfg {Boolean} enableGroupingMenu <tt>true</tt> to enable the grouping control in the header menu (defaults to <tt>true</tt>)
134 enableGroupingMenu : true,
137 * @cfg {Boolean} enableNoGroups <tt>true</tt> to allow the user to turn off grouping (defaults to <tt>true</tt>)
139 enableNoGroups : true,
147 if (me.lastGroupIndex) {
148 store.group(me.lastGroupIndex);
151 groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
152 groupToggleMenuItem.setChecked(true, true);
156 disable: function() {
163 lastGroup = store.groupers.first();
165 me.lastGroupIndex = lastGroup.property;
166 store.groupers.clear();
170 groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
171 groupToggleMenuItem.setChecked(true, true);
172 groupToggleMenuItem.setChecked(false, true);
176 getFeatureTpl: function(values, parent, x, xcount) {
180 '<tpl if="typeof rows !== \'undefined\'">',
182 '<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>',
183 // this is the rowbody
184 '<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>',
189 getFragmentTpl: function() {
191 indentByDepth: this.indentByDepth,
192 depthToIndent: this.depthToIndent
196 indentByDepth: function(values) {
197 var depth = values.depth || 0;
198 return 'style="padding-left:'+ depth * this.depthToIndent + 'px;"';
201 // Containers holding these components are responsible for
202 // destroying them, we are just deleting references.
203 destroy: function() {
207 delete me.prunedHeader;
210 // perhaps rename to afterViewRender
211 attachEvents: function() {
214 header, headerId, menu, menuItem;
218 groupclick: me.onGroupClick,
219 rowfocus: me.onRowFocus
221 view.store.on('groupchange', me.onGroupChange, me);
223 me.pruneGroupedHeader();
225 if (me.enableGroupingMenu) {
226 me.injectGroupingMenu();
229 if (me.hideGroupedHeader) {
230 header = view.headerCt.down('gridcolumn[dataIndex=' + me.getGroupField() + ']');
231 headerId = header.id;
232 menu = view.headerCt.getMenu();
233 menuItem = menu.down('menuitem[headerId='+ headerId +']');
235 menuItem.setChecked(false);
240 injectGroupingMenu: function() {
243 headerCt = view.headerCt;
244 headerCt.showMenuBy = me.showMenuBy;
245 headerCt.getMenuItems = me.getMenuItems();
248 showMenuBy: function(t, header) {
249 var menu = this.getMenu(),
250 groupMenuItem = menu.down('#groupMenuItem'),
251 groupableMth = header.groupable === false ? 'disable' : 'enable';
253 groupMenuItem[groupableMth]();
254 Ext.grid.header.Container.prototype.showMenuBy.apply(this, arguments);
257 getMenuItems: function() {
259 groupByText = me.groupByText,
260 disabled = me.disabled,
261 showGroupsText = me.showGroupsText,
262 enableNoGroups = me.enableNoGroups,
263 groupMenuItemClick = Ext.Function.bind(me.onGroupMenuItemClick, me),
264 groupToggleMenuItemClick = Ext.Function.bind(me.onGroupToggleMenuItemClick, me)
266 // runs in the scope of headerCt
268 var o = Ext.grid.header.Container.prototype.getMenuItems.call(this);
270 itemId: 'groupMenuItem',
272 handler: groupMenuItemClick
274 if (enableNoGroups) {
276 itemId: 'groupToggleMenuItem',
277 text: showGroupsText,
279 checkHandler: groupToggleMenuItemClick
288 * Group by the header the user has clicked on.
291 onGroupMenuItemClick: function(menuItem, e) {
292 var menu = menuItem.parentMenu,
293 hdr = menu.activeHeader,
296 delete this.lastGroupIndex;
298 view.store.group(hdr.dataIndex);
299 this.pruneGroupedHeader();
304 * Turn on and off grouping via the menu
307 onGroupToggleMenuItemClick: function(menuItem, checked) {
308 this[checked ? 'enable' : 'disable']();
312 * Prunes the grouped header from the header container
315 pruneGroupedHeader: function() {
319 groupField = me.getGroupField(),
320 headerCt = view.headerCt,
321 header = headerCt.down('header[dataIndex=' + groupField + ']');
324 if (me.prunedHeader) {
325 me.prunedHeader.show();
327 me.prunedHeader = header;
332 getGroupField: function(){
333 var group = this.view.store.groupers.first();
335 return group.property;
341 * When a row gains focus, expand the groups above it
344 onRowFocus: function(rowIdx) {
345 var node = this.view.getNode(rowIdx),
346 groupBd = Ext.fly(node).up('.' + this.collapsedCls);
349 // for multiple level groups, should expand every groupBd
351 this.expand(groupBd);
356 * Expand a group by the groupBody
357 * @param {Ext.core.Element} groupBd
360 expand: function(groupBd) {
363 grid = view.up('gridpanel'),
364 groupBdDom = Ext.getDom(groupBd);
366 me.collapsedState[groupBdDom.id] = false;
368 groupBd.removeCls(me.collapsedCls);
369 groupBd.prev().removeCls(me.hdCollapsedCls);
371 grid.determineScrollbars();
372 grid.invalidateScroller();
373 view.fireEvent('groupexpand');
377 * Collapse a group by the groupBody
378 * @param {Ext.core.Element} groupBd
381 collapse: function(groupBd) {
384 grid = view.up('gridpanel'),
385 groupBdDom = Ext.getDom(groupBd);
387 me.collapsedState[groupBdDom.id] = true;
389 groupBd.addCls(me.collapsedCls);
390 groupBd.prev().addCls(me.hdCollapsedCls);
392 grid.determineScrollbars();
393 grid.invalidateScroller();
394 view.fireEvent('groupcollapse');
397 onGroupChange: function(){
402 * Toggle between expanded/collapsed state when clicking on
406 onGroupClick: function(view, group, idx, foo, e) {
408 toggleCls = me.toggleCls,
409 groupBd = Ext.fly(group.nextSibling, '_grouping');
411 if (groupBd.hasCls(me.collapsedCls)) {
414 me.collapse(groupBd);
418 // Injects isRow and closeRow into the metaRowTpl.
419 getMetaRowTplFragments: function() {
422 closeRow: this.closeRow
426 // injected into rowtpl and wrapped around metaRowTpl
427 // becomes part of the standard tpl
429 return '<tpl if="typeof rows === \'undefined\'">';
432 // injected into rowtpl and wrapped around metaRowTpl
433 // becomes part of the standard tpl
434 closeRow: function() {
438 // isRow and closeRow are injected via getMetaRowTplFragments
439 mutateMetaRowTpl: function(metaRowTpl) {
440 metaRowTpl.unshift('{[this.isRow()]}');
441 metaRowTpl.push('{[this.closeRow()]}');
444 // injects an additional style attribute via tdAttrKey with the proper
446 getAdditionalData: function(data, idx, record, orig) {
447 var view = this.view,
449 col = hCt.items.getAt(0),
451 tdAttrKey = col.id + '-tdAttr';
453 // maintain the current tdAttr that a user may ahve set.
454 o[tdAttrKey] = this.indentByDepth(data) + " " + (orig[tdAttrKey] ? orig[tdAttrKey] : '');
455 o.collapsed = 'true';
459 // return matching preppedRecords
460 getGroupRows: function(group, records, preppedRecords, fullWidth) {
462 children = group.children,
463 rows = group.rows = [],
465 group.viewId = view.id;
467 Ext.Array.each(records, function(record, idx) {
468 if (Ext.Array.indexOf(children, record) != -1) {
469 rows.push(Ext.apply(preppedRecords[idx], {
474 delete group.children;
475 group.fullWidth = fullWidth;
476 if (me.collapsedState[view.id + '-gp-' + group.name]) {
477 group.collapsedCls = me.collapsedCls;
478 group.hdCollapsedCls = me.hdCollapsedCls;
484 // return the data in a grouped format.
485 collectData: function(records, preppedRecords, startIndex, fullWidth, o) {
487 store = me.view.store,
490 if (!me.disabled && store.isGrouped()) {
491 groups = store.getGroups();
492 Ext.Array.each(groups, function(group, idx){
493 me.getGroupRows(group, records, preppedRecords, fullWidth);
503 // adds the groupName to the groupclick, groupdblclick, groupcontextmenu
504 // events that are fired on the view. Chose not to return the actual
505 // group itself because of its expense and because developers can simply
506 // grab the group via store.getGroups(groupName)
507 getFireEventArgs: function(type, view, featureTarget) {
508 var returnArray = [type, view, featureTarget],
509 groupBd = Ext.fly(featureTarget.nextSibling, '_grouping'),
510 groupBdId = Ext.getDom(groupBd).id,
511 prefix = view.id + '-gp-',
512 groupName = groupBdId.substr(prefix.length);
514 returnArray.push(groupName);