Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / grid / feature / Grouping.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.grid.feature.Grouping
17  * @extends Ext.grid.feature.Feature
18  * 
19  * This feature allows to display the grid rows aggregated into groups as specified by the {@link Ext.data.Store#groupers}
20  * specified on the Store. The group will show the title for the group name and then the appropriate records for the group
21  * underneath. The groups can also be expanded and collapsed.
22  * 
23  * ## Extra Events
24  * This feature adds several extra events that will be fired on the grid to interact with the groups:
25  *
26  *  - {@link #groupclick}
27  *  - {@link #groupdblclick}
28  *  - {@link #groupcontextmenu}
29  *  - {@link #groupexpand}
30  *  - {@link #groupcollapse}
31  * 
32  * ## Menu Augmentation
33  * This feature adds extra options to the grid column menu to provide the user with functionality to modify the grouping.
34  * This can be disabled by setting the {@link #enableGroupingMenu} option. The option to disallow grouping from being turned off
35  * by thew user is {@link #enableNoGroups}.
36  * 
37  * ## Controlling Group Text
38  * The {@link #groupHeaderTpl} is used to control the rendered title for each group. It can modified to customized
39  * the default display.
40  * 
41  * ## Example Usage
42  * 
43  *     var groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
44  *         groupHeaderTpl: 'Group: {name} ({rows.length})', //print the number of items in the group
45  *         startCollapsed: true // start all groups collapsed
46  *     });
47  * 
48  * @ftype grouping
49  * @author Nicolas Ferrero
50  */
51 Ext.define('Ext.grid.feature.Grouping', {
52     extend: 'Ext.grid.feature.Feature',
53     alias: 'feature.grouping',
54
55     eventPrefix: 'group',
56     eventSelector: '.' + Ext.baseCSSPrefix + 'grid-group-hd',
57
58     constructor: function() {
59         this.collapsedState = {};
60         this.callParent(arguments);
61     },
62     
63     /**
64      * @event groupclick
65      * @param {Ext.view.Table} view
66      * @param {HTMLElement} node
67      * @param {String} group The name of the group
68      * @param {Ext.EventObject} e
69      */
70
71     /**
72      * @event groupdblclick
73      * @param {Ext.view.Table} view
74      * @param {HTMLElement} node
75      * @param {String} group The name of the group
76      * @param {Ext.EventObject} e
77      */
78
79     /**
80      * @event groupcontextmenu
81      * @param {Ext.view.Table} view
82      * @param {HTMLElement} node
83      * @param {String} group The name of the group
84      * @param {Ext.EventObject} e
85      */
86
87     /**
88      * @event groupcollapse
89      * @param {Ext.view.Table} view
90      * @param {HTMLElement} node
91      * @param {String} group The name of the group
92      * @param {Ext.EventObject} e
93      */
94
95     /**
96      * @event groupexpand
97      * @param {Ext.view.Table} view
98      * @param {HTMLElement} node
99      * @param {String} group The name of the group
100      * @param {Ext.EventObject} e
101      */
102
103     /**
104      * @cfg {String} groupHeaderTpl
105      * Template snippet, this cannot be an actual template. {name} will be replaced with the current group.
106      * Defaults to 'Group: {name}'
107      */
108     groupHeaderTpl: 'Group: {name}',
109
110     /**
111      * @cfg {Number} depthToIndent
112      * Number of pixels to indent per grouping level
113      */
114     depthToIndent: 17,
115
116     collapsedCls: Ext.baseCSSPrefix + 'grid-group-collapsed',
117     hdCollapsedCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsed',
118
119     /**
120      * @cfg {String} groupByText Text displayed in the grid header menu for grouping by header
121      * (defaults to 'Group By This Field').
122      */
123     groupByText : 'Group By This Field',
124     /**
125      * @cfg {String} showGroupsText Text displayed in the grid header for enabling/disabling grouping
126      * (defaults to 'Show in Groups').
127      */
128     showGroupsText : 'Show in Groups',
129
130     /**
131      * @cfg {Boolean} hideGroupedHeader<tt>true</tt> to hide the header that is currently grouped (defaults to <tt>false</tt>)
132      */
133     hideGroupedHeader : false,
134
135     /**
136      * @cfg {Boolean} startCollapsed <tt>true</tt> to start all groups collapsed (defaults to <tt>false</tt>)
137      */
138     startCollapsed : false,
139
140     /**
141      * @cfg {Boolean} enableGroupingMenu <tt>true</tt> to enable the grouping control in the header menu (defaults to <tt>true</tt>)
142      */
143     enableGroupingMenu : true,
144
145     /**
146      * @cfg {Boolean} enableNoGroups <tt>true</tt> to allow the user to turn off grouping (defaults to <tt>true</tt>)
147      */
148     enableNoGroups : true,
149     
150     enable: function() {
151         var me    = this,
152             view  = me.view,
153             store = view.store,
154             groupToggleMenuItem;
155             
156         if (me.lastGroupIndex) {
157             store.group(me.lastGroupIndex);
158         }
159         me.callParent();
160         groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
161         groupToggleMenuItem.setChecked(true, true);
162         view.refresh();
163     },
164
165     disable: function() {
166         var me    = this,
167             view  = me.view,
168             store = view.store,
169             groupToggleMenuItem,
170             lastGroup;
171             
172         lastGroup = store.groupers.first();
173         if (lastGroup) {
174             me.lastGroupIndex = lastGroup.property;
175             store.groupers.clear();
176         }
177         
178         me.callParent();
179         groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
180         groupToggleMenuItem.setChecked(true, true);
181         groupToggleMenuItem.setChecked(false, true);
182         view.refresh();
183     },
184
185     getFeatureTpl: function(values, parent, x, xcount) {
186         var me = this;
187         
188         return [
189             '<tpl if="typeof rows !== \'undefined\'">',
190                 // group row tpl
191                 '<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>',
192                 // this is the rowbody
193                 '<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>',
194             '</tpl>'
195         ].join('');
196     },
197
198     getFragmentTpl: function() {
199         return {
200             indentByDepth: this.indentByDepth,
201             depthToIndent: this.depthToIndent
202         };
203     },
204
205     indentByDepth: function(values) {
206         var depth = values.depth || 0;
207         return 'style="padding-left:'+ depth * this.depthToIndent + 'px;"';
208     },
209
210     // Containers holding these components are responsible for
211     // destroying them, we are just deleting references.
212     destroy: function() {
213         var me = this;
214         
215         delete me.view;
216         delete me.prunedHeader;
217     },
218
219     // perhaps rename to afterViewRender
220     attachEvents: function() {
221         var me = this,
222             view = me.view,
223             header, headerId, menu, menuItem;
224
225         view.on({
226             scope: me,
227             groupclick: me.onGroupClick,
228             rowfocus: me.onRowFocus
229         });
230         view.store.on('groupchange', me.onGroupChange, me);
231
232         me.pruneGroupedHeader();
233
234         if (me.enableGroupingMenu) {
235             me.injectGroupingMenu();
236         }
237
238         if (me.hideGroupedHeader) {
239             header = view.headerCt.down('gridcolumn[dataIndex=' + me.getGroupField() + ']');
240             headerId = header.id;
241             menu = view.headerCt.getMenu();
242             menuItem = menu.down('menuitem[headerId='+ headerId +']');
243             if (menuItem) {
244                 menuItem.setChecked(false);
245             }
246         }
247     },
248     
249     injectGroupingMenu: function() {
250         var me       = this,
251             view     = me.view,
252             headerCt = view.headerCt;
253         headerCt.showMenuBy = me.showMenuBy;
254         headerCt.getMenuItems = me.getMenuItems();
255     },
256     
257     showMenuBy: function(t, header) {
258         var menu = this.getMenu(),
259             groupMenuItem  = menu.down('#groupMenuItem'),
260             groupableMth = header.groupable === false ?  'disable' : 'enable';
261             
262         groupMenuItem[groupableMth]();
263         Ext.grid.header.Container.prototype.showMenuBy.apply(this, arguments);
264     },
265     
266     getMenuItems: function() {
267         var me                 = this,
268             groupByText        = me.groupByText,
269             disabled           = me.disabled,
270             showGroupsText     = me.showGroupsText,
271             enableNoGroups     = me.enableNoGroups,
272             groupMenuItemClick = Ext.Function.bind(me.onGroupMenuItemClick, me),
273             groupToggleMenuItemClick = Ext.Function.bind(me.onGroupToggleMenuItemClick, me);
274         
275         // runs in the scope of headerCt
276         return function() {
277             var o = Ext.grid.header.Container.prototype.getMenuItems.call(this);
278             o.push('-', {
279                 itemId: 'groupMenuItem',
280                 text: groupByText,
281                 handler: groupMenuItemClick
282             });
283             if (enableNoGroups) {
284                 o.push({
285                     itemId: 'groupToggleMenuItem',
286                     text: showGroupsText,
287                     checked: !disabled,
288                     checkHandler: groupToggleMenuItemClick
289                 });
290             }
291             return o;
292         };
293     },
294
295
296     /**
297      * Group by the header the user has clicked on.
298      * @private
299      */
300     onGroupMenuItemClick: function(menuItem, e) {
301         var menu = menuItem.parentMenu,
302             hdr  = menu.activeHeader,
303             view = this.view;
304
305         delete this.lastGroupIndex;
306         this.enable();
307         view.store.group(hdr.dataIndex);
308         this.pruneGroupedHeader();
309         
310     },
311
312     /**
313      * Turn on and off grouping via the menu
314      * @private
315      */
316     onGroupToggleMenuItemClick: function(menuItem, checked) {
317         this[checked ? 'enable' : 'disable']();
318     },
319
320     /**
321      * Prunes the grouped header from the header container
322      * @private
323      */
324     pruneGroupedHeader: function() {
325         var me         = this,
326             view       = me.view,
327             store      = view.store,
328             groupField = me.getGroupField(),
329             headerCt   = view.headerCt,
330             header     = headerCt.down('header[dataIndex=' + groupField + ']');
331
332         if (header) {
333             if (me.prunedHeader) {
334                 me.prunedHeader.show();
335             }
336             me.prunedHeader = header;
337             header.hide();
338         }
339     },
340
341     getGroupField: function(){
342         var group = this.view.store.groupers.first();
343         if (group) {
344             return group.property;    
345         }
346         return ''; 
347     },
348
349     /**
350      * When a row gains focus, expand the groups above it
351      * @private
352      */
353     onRowFocus: function(rowIdx) {
354         var node    = this.view.getNode(rowIdx),
355             groupBd = Ext.fly(node).up('.' + this.collapsedCls);
356
357         if (groupBd) {
358             // for multiple level groups, should expand every groupBd
359             // above
360             this.expand(groupBd);
361         }
362     },
363
364     /**
365      * Expand a group by the groupBody
366      * @param {Ext.core.Element} groupBd
367      * @private
368      */
369     expand: function(groupBd) {
370         var me = this,
371             view = me.view,
372             grid = view.up('gridpanel'),
373             groupBdDom = Ext.getDom(groupBd);
374             
375         me.collapsedState[groupBdDom.id] = false;
376
377         groupBd.removeCls(me.collapsedCls);
378         groupBd.prev().removeCls(me.hdCollapsedCls);
379
380         grid.determineScrollbars();
381         grid.invalidateScroller();
382         view.fireEvent('groupexpand');
383     },
384
385     /**
386      * Collapse a group by the groupBody
387      * @param {Ext.core.Element} groupBd
388      * @private
389      */
390     collapse: function(groupBd) {
391         var me = this,
392             view = me.view,
393             grid = view.up('gridpanel'),
394             groupBdDom = Ext.getDom(groupBd);
395             
396         me.collapsedState[groupBdDom.id] = true;
397
398         groupBd.addCls(me.collapsedCls);
399         groupBd.prev().addCls(me.hdCollapsedCls);
400
401         grid.determineScrollbars();
402         grid.invalidateScroller();
403         view.fireEvent('groupcollapse');
404     },
405     
406     onGroupChange: function(){
407         this.view.refresh();
408     },
409
410     /**
411      * Toggle between expanded/collapsed state when clicking on
412      * the group.
413      * @private
414      */
415     onGroupClick: function(view, group, idx, foo, e) {
416         var me = this,
417             toggleCls = me.toggleCls,
418             groupBd = Ext.fly(group.nextSibling, '_grouping');
419
420         if (groupBd.hasCls(me.collapsedCls)) {
421             me.expand(groupBd);
422         } else {
423             me.collapse(groupBd);
424         }
425     },
426
427     // Injects isRow and closeRow into the metaRowTpl.
428     getMetaRowTplFragments: function() {
429         return {
430             isRow: this.isRow,
431             closeRow: this.closeRow
432         };
433     },
434
435     // injected into rowtpl and wrapped around metaRowTpl
436     // becomes part of the standard tpl
437     isRow: function() {
438         return '<tpl if="typeof rows === \'undefined\'">';
439     },
440
441     // injected into rowtpl and wrapped around metaRowTpl
442     // becomes part of the standard tpl
443     closeRow: function() {
444         return '</tpl>';
445     },
446
447     // isRow and closeRow are injected via getMetaRowTplFragments
448     mutateMetaRowTpl: function(metaRowTpl) {
449         metaRowTpl.unshift('{[this.isRow()]}');
450         metaRowTpl.push('{[this.closeRow()]}');
451     },
452
453     // injects an additional style attribute via tdAttrKey with the proper
454     // amount of padding
455     getAdditionalData: function(data, idx, record, orig) {
456         var view = this.view,
457             hCt  = view.headerCt,
458             col  = hCt.items.getAt(0),
459             o = {},
460             tdAttrKey = col.id + '-tdAttr';
461
462         // maintain the current tdAttr that a user may ahve set.
463         o[tdAttrKey] = this.indentByDepth(data) + " " + (orig[tdAttrKey] ? orig[tdAttrKey] : '');
464         o.collapsed = 'true';
465         return o;
466     },
467
468     // return matching preppedRecords
469     getGroupRows: function(group, records, preppedRecords, fullWidth) {
470         var me = this,
471             children = group.children,
472             rows = group.rows = [],
473             view = me.view;
474         group.viewId = view.id;
475
476         Ext.Array.each(records, function(record, idx) {
477             if (Ext.Array.indexOf(children, record) != -1) {
478                 rows.push(Ext.apply(preppedRecords[idx], {
479                     depth: 1
480                 }));
481             }
482         });
483         delete group.children;
484         group.fullWidth = fullWidth;
485         if (me.collapsedState[view.id + '-gp-' + group.name]) {
486             group.collapsedCls = me.collapsedCls;
487             group.hdCollapsedCls = me.hdCollapsedCls;
488         }
489
490         return group;
491     },
492
493     // return the data in a grouped format.
494     collectData: function(records, preppedRecords, startIndex, fullWidth, o) {
495         var me    = this,
496             store = me.view.store,
497             groups;
498             
499         if (!me.disabled && store.isGrouped()) {
500             groups = store.getGroups();
501             Ext.Array.each(groups, function(group, idx){
502                 me.getGroupRows(group, records, preppedRecords, fullWidth);
503             }, me);
504             return {
505                 rows: groups,
506                 fullWidth: fullWidth
507             };
508         }
509         return o;
510     },
511     
512     // adds the groupName to the groupclick, groupdblclick, groupcontextmenu
513     // events that are fired on the view. Chose not to return the actual
514     // group itself because of its expense and because developers can simply
515     // grab the group via store.getGroups(groupName)
516     getFireEventArgs: function(type, view, featureTarget, e) {
517         var returnArray = [type, view, featureTarget],
518             groupBd     = Ext.fly(featureTarget.nextSibling, '_grouping'),
519             groupBdId   = Ext.getDom(groupBd).id,
520             prefix      = view.id + '-gp-',
521             groupName   = groupBdId.substr(prefix.length);
522         
523         returnArray.push(groupName, e);
524         
525         return returnArray;
526     }
527 });
528