Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / docs / source / Grouping.html
1 <!DOCTYPE html><html><head><title>Sencha Documentation Project</title><link rel="stylesheet" href="../reset.css" type="text/css"><link rel="stylesheet" href="../prettify.css" type="text/css"><link rel="stylesheet" href="../prettify_sa.css" type="text/css"><script type="text/javascript" src="../prettify.js"></script></head><body onload="prettyPrint()"><pre class="prettyprint"><pre><span id='Ext-grid.feature.Grouping'>/**
2 </span> * @class Ext.grid.feature.Grouping
3  * @extends Ext.grid.feature.Feature
4  * 
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.
8  * 
9  * ## Extra Events
10  * This feature adds several extra events that will be fired on the grid to interact with the groups:
11  *
12  *  - {@link #groupclick}
13  *  - {@link #groupdblclick}
14  *  - {@link #groupcontextmenu}
15  *  - {@link #groupexpand}
16  *  - {@link #groupcollapse}
17  * 
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}.
22  * 
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.
26  * 
27  * ## Example Usage
28  * 
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
32  *     });
33  * 
34  * @ftype grouping
35  * @author Nicolas Ferrero
36  */
37 Ext.define('Ext.grid.feature.Grouping', {
38     extend: 'Ext.grid.feature.Feature',
39     alias: 'feature.grouping',
40
41     eventPrefix: 'group',
42     eventSelector: '.' + Ext.baseCSSPrefix + 'grid-group-hd',
43
44     constructor: function() {
45         this.collapsedState = {};
46         this.callParent(arguments);
47     },
48     
49 <span id='Ext-grid.feature.Grouping-event-groupclick'>    /**
50 </span>     * @event groupclick
51      * @param {Ext.view.Table} view
52      * @param {HTMLElement} node
53      * @param {Number} unused
54      * @param {Number} unused
55      * @param {Ext.EventObject} e
56      */
57
58 <span id='Ext-grid.feature.Grouping-event-groupdblclick'>    /**
59 </span>     * @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
65      */
66
67 <span id='Ext-grid.feature.Grouping-event-groupcontextmenu'>    /**
68 </span>     * @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
74      */
75
76 <span id='Ext-grid.feature.Grouping-event-groupcollapse'>    /**
77 </span>     * @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
83      */
84
85 <span id='Ext-grid.feature.Grouping-event-groupexpand'>    /**
86 </span>     * @event groupexpand
87      * @param {Ext.view.Table} view
88      * @param {HTMLElement} node
89      * @param {Number} unused
90      * @param {Number} unused
91      * @param {Ext.EventObject} e
92      */
93
94 <span id='Ext-grid.feature.Grouping-cfg-groupHeaderTpl'>    /**
95 </span>     * @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}'
98      */
99     groupHeaderTpl: 'Group: {name}',
100
101 <span id='Ext-grid.feature.Grouping-cfg-depthToIndent'>    /**
102 </span>     * @cfg {Number} depthToIndent
103      * Number of pixels to indent per grouping level
104      */
105     depthToIndent: 17,
106
107     collapsedCls: Ext.baseCSSPrefix + 'grid-group-collapsed',
108     hdCollapsedCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsed',
109
110 <span id='Ext-grid.feature.Grouping-cfg-groupByText'>    /**
111 </span>     * @cfg {String} groupByText Text displayed in the grid header menu for grouping by header
112      * (defaults to 'Group By This Field').
113      */
114     groupByText : 'Group By This Field',
115 <span id='Ext-grid.feature.Grouping-cfg-showGroupsText'>    /**
116 </span>     * @cfg {String} showGroupsText Text displayed in the grid header for enabling/disabling grouping
117      * (defaults to 'Show in Groups').
118      */
119     showGroupsText : 'Show in Groups',
120
121 <span id='Ext-grid.feature.Grouping-cfg-hideGroupedHeader'>    /**
122 </span>     * @cfg {Boolean} hideGroupedHeader&lt;tt&gt;true&lt;/tt&gt; to hide the header that is currently grouped (defaults to &lt;tt&gt;false&lt;/tt&gt;)
123      */
124     hideGroupedHeader : false,
125
126 <span id='Ext-grid.feature.Grouping-cfg-startCollapsed'>    /**
127 </span>     * @cfg {Boolean} startCollapsed &lt;tt&gt;true&lt;/tt&gt; to start all groups collapsed (defaults to &lt;tt&gt;false&lt;/tt&gt;)
128      */
129     startCollapsed : false,
130
131 <span id='Ext-grid.feature.Grouping-cfg-enableGroupingMenu'>    /**
132 </span>     * @cfg {Boolean} enableGroupingMenu &lt;tt&gt;true&lt;/tt&gt; to enable the grouping control in the header menu (defaults to &lt;tt&gt;true&lt;/tt&gt;)
133      */
134     enableGroupingMenu : true,
135
136 <span id='Ext-grid.feature.Grouping-cfg-enableNoGroups'>    /**
137 </span>     * @cfg {Boolean} enableNoGroups &lt;tt&gt;true&lt;/tt&gt; to allow the user to turn off grouping (defaults to &lt;tt&gt;true&lt;/tt&gt;)
138      */
139     enableNoGroups : true,
140     
141     enable: function() {
142         var me    = this,
143             view  = me.view,
144             store = view.store,
145             groupToggleMenuItem;
146             
147         if (me.lastGroupIndex) {
148             store.group(me.lastGroupIndex);
149         }
150         me.callParent();
151         groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
152         groupToggleMenuItem.setChecked(true, true);
153         view.refresh();
154     },
155
156     disable: function() {
157         var me    = this,
158             view  = me.view,
159             store = view.store,
160             groupToggleMenuItem,
161             lastGroup;
162             
163         lastGroup = store.groupers.first();
164         if (lastGroup) {
165             me.lastGroupIndex = lastGroup.property;
166             store.groupers.clear();
167         }
168         
169         me.callParent();
170         groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
171         groupToggleMenuItem.setChecked(true, true);
172         groupToggleMenuItem.setChecked(false, true);
173         view.refresh();
174     },
175
176     getFeatureTpl: function(values, parent, x, xcount) {
177         var me = this;
178         
179         return [
180             '&lt;tpl if=&quot;typeof rows !== \'undefined\'&quot;&gt;',
181                 // group row tpl
182                 '&lt;tr class=&quot;' + Ext.baseCSSPrefix + 'grid-group-hd ' + (me.startCollapsed ? me.hdCollapsedCls : '') + ' {hdCollapsedCls}&quot;&gt;&lt;td class=&quot;' + Ext.baseCSSPrefix + 'grid-cell&quot; colspan=&quot;' + parent.columns.length + '&quot; {[this.indentByDepth(values)]}&gt;&lt;div class=&quot;' + Ext.baseCSSPrefix + 'grid-cell-inner&quot;&gt;&lt;div class=&quot;' + Ext.baseCSSPrefix + 'grid-group-title&quot;&gt;{collapsed}' + me.groupHeaderTpl + '&lt;/div&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;',
183                 // this is the rowbody
184                 '&lt;tr id=&quot;{viewId}-gp-{name}&quot; class=&quot;' + Ext.baseCSSPrefix + 'grid-group-body ' + (me.startCollapsed ? me.collapsedCls : '') + ' {collapsedCls}&quot;&gt;&lt;td colspan=&quot;' + parent.columns.length + '&quot;&gt;{[this.recurse(values)]}&lt;/td&gt;&lt;/tr&gt;',
185             '&lt;/tpl&gt;'
186         ].join('');
187     },
188
189     getFragmentTpl: function() {
190         return {
191             indentByDepth: this.indentByDepth,
192             depthToIndent: this.depthToIndent
193         };
194     },
195
196     indentByDepth: function(values) {
197         var depth = values.depth || 0;
198         return 'style=&quot;padding-left:'+ depth * this.depthToIndent + 'px;&quot;';
199     },
200
201     // Containers holding these components are responsible for
202     // destroying them, we are just deleting references.
203     destroy: function() {
204         var me = this;
205         
206         delete me.view;
207         delete me.prunedHeader;
208     },
209
210     // perhaps rename to afterViewRender
211     attachEvents: function() {
212         var me = this,
213             view = me.view,
214             header, headerId, menu, menuItem;
215
216         view.on({
217             scope: me,
218             groupclick: me.onGroupClick,
219             rowfocus: me.onRowFocus
220         });
221         view.store.on('groupchange', me.onGroupChange, me);
222
223         me.pruneGroupedHeader();
224
225         if (me.enableGroupingMenu) {
226             me.injectGroupingMenu();
227         }
228
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 +']');
234             if (menuItem) {
235                 menuItem.setChecked(false);
236             }
237         }
238     },
239     
240     injectGroupingMenu: function() {
241         var me       = this,
242             view     = me.view,
243             headerCt = view.headerCt;
244         headerCt.showMenuBy = me.showMenuBy;
245         headerCt.getMenuItems = me.getMenuItems();
246     },
247     
248     showMenuBy: function(t, header) {
249         var menu = this.getMenu(),
250             groupMenuItem  = menu.down('#groupMenuItem'),
251             groupableMth = header.groupable === false ?  'disable' : 'enable';
252             
253         groupMenuItem[groupableMth]();
254         Ext.grid.header.Container.prototype.showMenuBy.apply(this, arguments);
255     },
256     
257     getMenuItems: function() {
258         var me                 = this,
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)
265         
266         // runs in the scope of headerCt
267         return function() {
268             var o = Ext.grid.header.Container.prototype.getMenuItems.call(this);
269             o.push('-', {
270                 itemId: 'groupMenuItem',
271                 text: groupByText,
272                 handler: groupMenuItemClick
273             });
274             if (enableNoGroups) {
275                 o.push({
276                     itemId: 'groupToggleMenuItem',
277                     text: showGroupsText,
278                     checked: !disabled,
279                     checkHandler: groupToggleMenuItemClick
280                 });
281             }
282             return o;
283         };
284     },
285
286
287 <span id='Ext-grid.feature.Grouping-method-onGroupMenuItemClick'>    /**
288 </span>     * Group by the header the user has clicked on.
289      * @private
290      */
291     onGroupMenuItemClick: function(menuItem, e) {
292         var menu = menuItem.parentMenu,
293             hdr  = menu.activeHeader,
294             view = this.view;
295
296         delete this.lastGroupIndex;
297         this.enable();
298         view.store.group(hdr.dataIndex);
299         this.pruneGroupedHeader();
300         
301     },
302
303 <span id='Ext-grid.feature.Grouping-method-onGroupToggleMenuItemClick'>    /**
304 </span>     * Turn on and off grouping via the menu
305      * @private
306      */
307     onGroupToggleMenuItemClick: function(menuItem, checked) {
308         this[checked ? 'enable' : 'disable']();
309     },
310
311 <span id='Ext-grid.feature.Grouping-method-pruneGroupedHeader'>    /**
312 </span>     * Prunes the grouped header from the header container
313      * @private
314      */
315     pruneGroupedHeader: function() {
316         var me         = this,
317             view       = me.view,
318             store      = view.store,
319             groupField = me.getGroupField(),
320             headerCt   = view.headerCt,
321             header     = headerCt.down('header[dataIndex=' + groupField + ']');
322
323         if (header) {
324             if (me.prunedHeader) {
325                 me.prunedHeader.show();
326             }
327             me.prunedHeader = header;
328             header.hide();
329         }
330     },
331
332     getGroupField: function(){
333         var group = this.view.store.groupers.first();
334         if (group) {
335             return group.property;    
336         }
337         return ''; 
338     },
339
340 <span id='Ext-grid.feature.Grouping-method-onRowFocus'>    /**
341 </span>     * When a row gains focus, expand the groups above it
342      * @private
343      */
344     onRowFocus: function(rowIdx) {
345         var node    = this.view.getNode(rowIdx),
346             groupBd = Ext.fly(node).up('.' + this.collapsedCls);
347
348         if (groupBd) {
349             // for multiple level groups, should expand every groupBd
350             // above
351             this.expand(groupBd);
352         }
353     },
354
355 <span id='Ext-grid.feature.Grouping-method-expand'>    /**
356 </span>     * Expand a group by the groupBody
357      * @param {Ext.core.Element} groupBd
358      * @private
359      */
360     expand: function(groupBd) {
361         var me = this,
362             view = me.view,
363             grid = view.up('gridpanel'),
364             groupBdDom = Ext.getDom(groupBd);
365             
366         me.collapsedState[groupBdDom.id] = false;
367
368         groupBd.removeCls(me.collapsedCls);
369         groupBd.prev().removeCls(me.hdCollapsedCls);
370
371         grid.determineScrollbars();
372         grid.invalidateScroller();
373         view.fireEvent('groupexpand');
374     },
375
376 <span id='Ext-grid.feature.Grouping-method-collapse'>    /**
377 </span>     * Collapse a group by the groupBody
378      * @param {Ext.core.Element} groupBd
379      * @private
380      */
381     collapse: function(groupBd) {
382         var me = this,
383             view = me.view,
384             grid = view.up('gridpanel'),
385             groupBdDom = Ext.getDom(groupBd);
386             
387         me.collapsedState[groupBdDom.id] = true;
388
389         groupBd.addCls(me.collapsedCls);
390         groupBd.prev().addCls(me.hdCollapsedCls);
391
392         grid.determineScrollbars();
393         grid.invalidateScroller();
394         view.fireEvent('groupcollapse');
395     },
396     
397     onGroupChange: function(){
398         this.view.refresh();
399     },
400
401 <span id='Ext-grid.feature.Grouping-method-onGroupClick'>    /**
402 </span>     * Toggle between expanded/collapsed state when clicking on
403      * the group.
404      * @private
405      */
406     onGroupClick: function(view, group, idx, foo, e) {
407         var me = this,
408             toggleCls = me.toggleCls,
409             groupBd = Ext.fly(group.nextSibling, '_grouping');
410
411         if (groupBd.hasCls(me.collapsedCls)) {
412             me.expand(groupBd);
413         } else {
414             me.collapse(groupBd);
415         }
416     },
417
418     // Injects isRow and closeRow into the metaRowTpl.
419     getMetaRowTplFragments: function() {
420         return {
421             isRow: this.isRow,
422             closeRow: this.closeRow
423         };
424     },
425
426     // injected into rowtpl and wrapped around metaRowTpl
427     // becomes part of the standard tpl
428     isRow: function() {
429         return '&lt;tpl if=&quot;typeof rows === \'undefined\'&quot;&gt;';
430     },
431
432     // injected into rowtpl and wrapped around metaRowTpl
433     // becomes part of the standard tpl
434     closeRow: function() {
435         return '&lt;/tpl&gt;';
436     },
437
438     // isRow and closeRow are injected via getMetaRowTplFragments
439     mutateMetaRowTpl: function(metaRowTpl) {
440         metaRowTpl.unshift('{[this.isRow()]}');
441         metaRowTpl.push('{[this.closeRow()]}');
442     },
443
444     // injects an additional style attribute via tdAttrKey with the proper
445     // amount of padding
446     getAdditionalData: function(data, idx, record, orig) {
447         var view = this.view,
448             hCt  = view.headerCt,
449             col  = hCt.items.getAt(0),
450             o = {},
451             tdAttrKey = col.id + '-tdAttr';
452
453         // maintain the current tdAttr that a user may ahve set.
454         o[tdAttrKey] = this.indentByDepth(data) + &quot; &quot; + (orig[tdAttrKey] ? orig[tdAttrKey] : '');
455         o.collapsed = 'true';
456         return o;
457     },
458
459     // return matching preppedRecords
460     getGroupRows: function(group, records, preppedRecords, fullWidth) {
461         var me = this,
462             children = group.children,
463             rows = group.rows = [],
464             view = me.view;
465         group.viewId = view.id;
466
467         Ext.Array.each(records, function(record, idx) {
468             if (Ext.Array.indexOf(children, record) != -1) {
469                 rows.push(Ext.apply(preppedRecords[idx], {
470                     depth: 1
471                 }));
472             }
473         });
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;
479         }
480
481         return group;
482     },
483
484     // return the data in a grouped format.
485     collectData: function(records, preppedRecords, startIndex, fullWidth, o) {
486         var me    = this,
487             store = me.view.store,
488             groups;
489             
490         if (!me.disabled &amp;&amp; store.isGrouped()) {
491             groups = store.getGroups();
492             Ext.Array.each(groups, function(group, idx){
493                 me.getGroupRows(group, records, preppedRecords, fullWidth);
494             }, me);
495             return {
496                 rows: groups,
497                 fullWidth: fullWidth
498             };
499         }
500         return o;
501     },
502     
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);
513         
514         returnArray.push(groupName);
515         
516         return returnArray;
517     }
518 });
519 </pre></pre></body></html>