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