Upgrade to ExtJS 4.0.7 - Released 10/19/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         var me = this;
60         
61         me.collapsedState = {};
62         me.callParent(arguments);
63     },
64     
65     /**
66      * @event groupclick
67      * @param {Ext.view.Table} view
68      * @param {HTMLElement} node
69      * @param {String} group The name of the group
70      * @param {Ext.EventObject} e
71      */
72
73     /**
74      * @event groupdblclick
75      * @param {Ext.view.Table} view
76      * @param {HTMLElement} node
77      * @param {String} group The name of the group
78      * @param {Ext.EventObject} e
79      */
80
81     /**
82      * @event groupcontextmenu
83      * @param {Ext.view.Table} view
84      * @param {HTMLElement} node
85      * @param {String} group The name of the group
86      * @param {Ext.EventObject} e
87      */
88
89     /**
90      * @event groupcollapse
91      * @param {Ext.view.Table} view
92      * @param {HTMLElement} node
93      * @param {String} group The name of the group
94      * @param {Ext.EventObject} e
95      */
96
97     /**
98      * @event groupexpand
99      * @param {Ext.view.Table} view
100      * @param {HTMLElement} node
101      * @param {String} group The name of the group
102      * @param {Ext.EventObject} e
103      */
104
105     /**
106      * @cfg {String} groupHeaderTpl
107      * Template snippet, this cannot be an actual template. {name} will be replaced with the current group.
108      * Defaults to 'Group: {name}'
109      */
110     groupHeaderTpl: 'Group: {name}',
111
112     /**
113      * @cfg {Number} depthToIndent
114      * Number of pixels to indent per grouping level
115      */
116     depthToIndent: 17,
117
118     collapsedCls: Ext.baseCSSPrefix + 'grid-group-collapsed',
119     hdCollapsedCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsed',
120
121     /**
122      * @cfg {String} groupByText Text displayed in the grid header menu for grouping by header.
123      */
124     groupByText : 'Group By This Field',
125     /**
126      * @cfg {String} showGroupsText Text displayed in the grid header for enabling/disabling grouping.
127      */
128     showGroupsText : 'Show in Groups',
129
130     /**
131      * @cfg {Boolean} hideGroupedHeader<tt>true</tt> to hide the header that is currently grouped.
132      */
133     hideGroupedHeader : false,
134
135     /**
136      * @cfg {Boolean} startCollapsed <tt>true</tt> to start all groups collapsed
137      */
138     startCollapsed : false,
139
140     /**
141      * @cfg {Boolean} enableGroupingMenu <tt>true</tt> to enable the grouping control in the header menu
142      */
143     enableGroupingMenu : true,
144
145     /**
146      * @cfg {Boolean} enableNoGroups <tt>true</tt> to allow the user to turn off grouping
147      */
148     enableNoGroups : true,
149     
150     enable: function() {
151         var me    = this,
152             view  = me.view,
153             store = view.store,
154             groupToggleMenuItem;
155             
156         me.lastGroupField = me.getGroupField();
157
158         if (me.lastGroupIndex) {
159             store.group(me.lastGroupIndex);
160         }
161         me.callParent();
162         groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
163         groupToggleMenuItem.setChecked(true, true);
164         me.refreshIf();
165     },
166
167     disable: function() {
168         var me    = this,
169             view  = me.view,
170             store = view.store,
171             remote = store.remoteGroup,
172             groupToggleMenuItem,
173             lastGroup;
174             
175         lastGroup = store.groupers.first();
176         if (lastGroup) {
177             me.lastGroupIndex = lastGroup.property;
178             me.block();
179             store.clearGrouping();
180             me.unblock();
181         }
182         
183         me.callParent();
184         groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
185         groupToggleMenuItem.setChecked(true, true);
186         groupToggleMenuItem.setChecked(false, true);
187         if (!remote) {
188             view.refresh();
189         }
190     },
191     
192     refreshIf: function() {
193         if (this.blockRefresh !== true) {
194             this.view.refresh();
195         }    
196     },
197
198     getFeatureTpl: function(values, parent, x, xcount) {
199         var me = this;
200         
201         return [
202             '<tpl if="typeof rows !== \'undefined\'">',
203                 // group row tpl
204                 '<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>',
205                 // this is the rowbody
206                 '<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>',
207             '</tpl>'
208         ].join('');
209     },
210
211     getFragmentTpl: function() {
212         return {
213             indentByDepth: this.indentByDepth,
214             depthToIndent: this.depthToIndent
215         };
216     },
217
218     indentByDepth: function(values) {
219         var depth = values.depth || 0;
220         return 'style="padding-left:'+ depth * this.depthToIndent + 'px;"';
221     },
222
223     // Containers holding these components are responsible for
224     // destroying them, we are just deleting references.
225     destroy: function() {
226         var me = this;
227         
228         delete me.view;
229         delete me.prunedHeader;
230     },
231
232     // perhaps rename to afterViewRender
233     attachEvents: function() {
234         var me = this,
235             view = me.view;
236
237         view.on({
238             scope: me,
239             groupclick: me.onGroupClick,
240             rowfocus: me.onRowFocus
241         });
242         view.store.on('groupchange', me.onGroupChange, me);
243
244         me.pruneGroupedHeader();
245
246         if (me.enableGroupingMenu) {
247             me.injectGroupingMenu();
248         }
249         me.lastGroupField = me.getGroupField();
250         me.block();
251         me.onGroupChange();
252         me.unblock();
253     },
254     
255     injectGroupingMenu: function() {
256         var me       = this,
257             view     = me.view,
258             headerCt = view.headerCt;
259         headerCt.showMenuBy = me.showMenuBy;
260         headerCt.getMenuItems = me.getMenuItems();
261     },
262     
263     showMenuBy: function(t, header) {
264         var menu = this.getMenu(),
265             groupMenuItem  = menu.down('#groupMenuItem'),
266             groupableMth = header.groupable === false ?  'disable' : 'enable';
267             
268         groupMenuItem[groupableMth]();
269         Ext.grid.header.Container.prototype.showMenuBy.apply(this, arguments);
270     },
271     
272     getMenuItems: function() {
273         var me                 = this,
274             groupByText        = me.groupByText,
275             disabled           = me.disabled,
276             showGroupsText     = me.showGroupsText,
277             enableNoGroups     = me.enableNoGroups,
278             groupMenuItemClick = Ext.Function.bind(me.onGroupMenuItemClick, me),
279             groupToggleMenuItemClick = Ext.Function.bind(me.onGroupToggleMenuItemClick, me);
280         
281         // runs in the scope of headerCt
282         return function() {
283             var o = Ext.grid.header.Container.prototype.getMenuItems.call(this);
284             o.push('-', {
285                 iconCls: Ext.baseCSSPrefix + 'group-by-icon',
286                 itemId: 'groupMenuItem',
287                 text: groupByText,
288                 handler: groupMenuItemClick
289             });
290             if (enableNoGroups) {
291                 o.push({
292                     itemId: 'groupToggleMenuItem',
293                     text: showGroupsText,
294                     checked: !disabled,
295                     checkHandler: groupToggleMenuItemClick
296                 });
297             }
298             return o;
299         };
300     },
301
302
303     /**
304      * Group by the header the user has clicked on.
305      * @private
306      */
307     onGroupMenuItemClick: function(menuItem, e) {
308         var me = this,
309             menu = menuItem.parentMenu,
310             hdr  = menu.activeHeader,
311             view = me.view,
312             store = view.store,
313             remote = store.remoteGroup;
314
315         delete me.lastGroupIndex;
316         me.block();
317         me.enable();
318         store.group(hdr.dataIndex);
319         me.pruneGroupedHeader();
320         me.unblock();
321         if (!remote) {
322             view.refresh();
323         }  
324     },
325     
326     block: function(){
327         this.blockRefresh = this.view.blockRefresh = true;
328     },
329     
330     unblock: function(){
331         this.blockRefresh = this.view.blockRefresh = false;
332     },
333
334     /**
335      * Turn on and off grouping via the menu
336      * @private
337      */
338     onGroupToggleMenuItemClick: function(menuItem, checked) {
339         this[checked ? 'enable' : 'disable']();
340     },
341
342     /**
343      * Prunes the grouped header from the header container
344      * @private
345      */
346     pruneGroupedHeader: function() {
347         var me         = this,
348             view       = me.view,
349             store      = view.store,
350             groupField = me.getGroupField(),
351             headerCt   = view.headerCt,
352             header     = headerCt.down('header[dataIndex=' + groupField + ']');
353
354         if (header) {
355             if (me.prunedHeader) {
356                 me.prunedHeader.show();
357             }
358             me.prunedHeader = header;
359             header.hide();
360         }
361     },
362
363     getGroupField: function(){
364         var group = this.view.store.groupers.first();
365         if (group) {
366             return group.property;    
367         }
368         return ''; 
369     },
370
371     /**
372      * When a row gains focus, expand the groups above it
373      * @private
374      */
375     onRowFocus: function(rowIdx) {
376         var node    = this.view.getNode(rowIdx),
377             groupBd = Ext.fly(node).up('.' + this.collapsedCls);
378
379         if (groupBd) {
380             // for multiple level groups, should expand every groupBd
381             // above
382             this.expand(groupBd);
383         }
384     },
385
386     /**
387      * Expand a group by the groupBody
388      * @param {Ext.Element} groupBd
389      * @private
390      */
391     expand: function(groupBd) {
392         var me = this,
393             view = me.view,
394             grid = view.up('gridpanel'),
395             groupBdDom = Ext.getDom(groupBd);
396             
397         me.collapsedState[groupBdDom.id] = false;
398
399         groupBd.removeCls(me.collapsedCls);
400         groupBd.prev().removeCls(me.hdCollapsedCls);
401
402         grid.determineScrollbars();
403         grid.invalidateScroller();
404         view.fireEvent('groupexpand');
405     },
406
407     /**
408      * Collapse a group by the groupBody
409      * @param {Ext.Element} groupBd
410      * @private
411      */
412     collapse: function(groupBd) {
413         var me = this,
414             view = me.view,
415             grid = view.up('gridpanel'),
416             groupBdDom = Ext.getDom(groupBd);
417             
418         me.collapsedState[groupBdDom.id] = true;
419
420         groupBd.addCls(me.collapsedCls);
421         groupBd.prev().addCls(me.hdCollapsedCls);
422
423         grid.determineScrollbars();
424         grid.invalidateScroller();
425         view.fireEvent('groupcollapse');
426     },
427     
428     onGroupChange: function(){
429         var me = this,
430             field = me.getGroupField(),
431             menuItem;
432             
433         if (me.hideGroupedHeader) {
434             if (me.lastGroupField) {
435                 menuItem = me.getMenuItem(me.lastGroupField);
436                 if (menuItem) {
437                     menuItem.setChecked(true);
438                 }
439             }
440             if (field) {
441                 menuItem = me.getMenuItem(field);
442                 if (menuItem) {
443                     menuItem.setChecked(false);
444                 }
445             }
446         }
447         if (me.blockRefresh !== true) {
448             me.view.refresh();
449         }
450         me.lastGroupField = field;
451     },
452     
453     /**
454      * Gets the related menu item for a dataIndex
455      * @private
456      * @return {Ext.grid.header.Container} The header
457      */
458     getMenuItem: function(dataIndex){
459         var view = this.view,
460             header = view.headerCt.down('gridcolumn[dataIndex=' + dataIndex + ']'),
461             menu = view.headerCt.getMenu();
462             
463         return menu.down('menuitem[headerId='+ header.id +']');
464     },
465
466     /**
467      * Toggle between expanded/collapsed state when clicking on
468      * the group.
469      * @private
470      */
471     onGroupClick: function(view, group, idx, foo, e) {
472         var me = this,
473             toggleCls = me.toggleCls,
474             groupBd = Ext.fly(group.nextSibling, '_grouping');
475
476         if (groupBd.hasCls(me.collapsedCls)) {
477             me.expand(groupBd);
478         } else {
479             me.collapse(groupBd);
480         }
481     },
482
483     // Injects isRow and closeRow into the metaRowTpl.
484     getMetaRowTplFragments: function() {
485         return {
486             isRow: this.isRow,
487             closeRow: this.closeRow
488         };
489     },
490
491     // injected into rowtpl and wrapped around metaRowTpl
492     // becomes part of the standard tpl
493     isRow: function() {
494         return '<tpl if="typeof rows === \'undefined\'">';
495     },
496
497     // injected into rowtpl and wrapped around metaRowTpl
498     // becomes part of the standard tpl
499     closeRow: function() {
500         return '</tpl>';
501     },
502
503     // isRow and closeRow are injected via getMetaRowTplFragments
504     mutateMetaRowTpl: function(metaRowTpl) {
505         metaRowTpl.unshift('{[this.isRow()]}');
506         metaRowTpl.push('{[this.closeRow()]}');
507     },
508
509     // injects an additional style attribute via tdAttrKey with the proper
510     // amount of padding
511     getAdditionalData: function(data, idx, record, orig) {
512         var view = this.view,
513             hCt  = view.headerCt,
514             col  = hCt.items.getAt(0),
515             o = {},
516             tdAttrKey = col.id + '-tdAttr';
517
518         // maintain the current tdAttr that a user may ahve set.
519         o[tdAttrKey] = this.indentByDepth(data) + " " + (orig[tdAttrKey] ? orig[tdAttrKey] : '');
520         o.collapsed = 'true';
521         return o;
522     },
523
524     // return matching preppedRecords
525     getGroupRows: function(group, records, preppedRecords, fullWidth) {
526         var me = this,
527             children = group.children,
528             rows = group.rows = [],
529             view = me.view;
530         group.viewId = view.id;
531
532         Ext.Array.each(records, function(record, idx) {
533             if (Ext.Array.indexOf(children, record) != -1) {
534                 rows.push(Ext.apply(preppedRecords[idx], {
535                     depth: 1
536                 }));
537             }
538         });
539         delete group.children;
540         group.fullWidth = fullWidth;
541         if (me.collapsedState[view.id + '-gp-' + group.name]) {
542             group.collapsedCls = me.collapsedCls;
543             group.hdCollapsedCls = me.hdCollapsedCls;
544         }
545
546         return group;
547     },
548
549     // return the data in a grouped format.
550     collectData: function(records, preppedRecords, startIndex, fullWidth, o) {
551         var me    = this,
552             store = me.view.store,
553             groups;
554             
555         if (!me.disabled && store.isGrouped()) {
556             groups = store.getGroups();
557             Ext.Array.each(groups, function(group, idx){
558                 me.getGroupRows(group, records, preppedRecords, fullWidth);
559             }, me);
560             return {
561                 rows: groups,
562                 fullWidth: fullWidth
563             };
564         }
565         return o;
566     },
567     
568     // adds the groupName to the groupclick, groupdblclick, groupcontextmenu
569     // events that are fired on the view. Chose not to return the actual
570     // group itself because of its expense and because developers can simply
571     // grab the group via store.getGroups(groupName)
572     getFireEventArgs: function(type, view, featureTarget, e) {
573         var returnArray = [type, view, featureTarget],
574             groupBd     = Ext.fly(featureTarget.nextSibling, '_grouping'),
575             groupBdId   = Ext.getDom(groupBd).id,
576             prefix      = view.id + '-gp-',
577             groupName   = groupBdId.substr(prefix.length);
578         
579         returnArray.push(groupName, e);
580         
581         return returnArray;
582     }
583 });
584