Upgrade to ExtJS 3.3.1 - Released 11/30/2010
[extjs.git] / src / data / GroupingStore.js
1 /*!
2  * Ext JS Library 3.3.1
3  * Copyright(c) 2006-2010 Sencha Inc.
4  * licensing@sencha.com
5  * http://www.sencha.com/license
6  */
7 /**
8  * @class Ext.data.GroupingStore
9  * @extends Ext.data.Store
10  * A specialized store implementation that provides for grouping records by one of the available fields. This
11  * is usually used in conjunction with an {@link Ext.grid.GroupingView} to provide the data model for
12  * a grouped GridPanel.
13  *
14  * Internally, GroupingStore is simply a normal Store with multi sorting enabled from the start. The grouping field
15  * and direction are always injected as the first sorter pair. GroupingView picks up on the configured groupField and
16  * builds grid rows appropriately.
17  *
18  * @constructor
19  * Creates a new GroupingStore.
20  * @param {Object} config A config object containing the objects needed for the Store to access data,
21  * and read the data into Records.
22  * @xtype groupingstore
23  */
24 Ext.data.GroupingStore = Ext.extend(Ext.data.Store, {
25
26     //inherit docs
27     constructor: function(config) {
28         config = config || {};
29
30         //We do some preprocessing here to massage the grouping + sorting options into a single
31         //multi sort array. If grouping and sorting options are both presented to the constructor,
32         //the sorters array consists of the grouping sorter object followed by the sorting sorter object
33         //see Ext.data.Store's sorting functions for details about how multi sorting works
34         this.hasMultiSort  = true;
35         this.multiSortInfo = this.multiSortInfo || {sorters: []};
36
37         var sorters    = this.multiSortInfo.sorters,
38             groupField = config.groupField || this.groupField,
39             sortInfo   = config.sortInfo || this.sortInfo,
40             groupDir   = config.groupDir || this.groupDir;
41
42         //add the grouping sorter object first
43         if(groupField){
44             sorters.push({
45                 field    : groupField,
46                 direction: groupDir
47             });
48         }
49
50         //add the sorting sorter object if it is present
51         if (sortInfo) {
52             sorters.push(sortInfo);
53         }
54
55         Ext.data.GroupingStore.superclass.constructor.call(this, config);
56
57         this.addEvents(
58           /**
59            * @event groupchange
60            * Fired whenever a call to store.groupBy successfully changes the grouping on the store
61            * @param {Ext.data.GroupingStore} store The grouping store
62            * @param {String} groupField The field that the store is now grouped by
63            */
64           'groupchange'
65         );
66
67         this.applyGroupField();
68     },
69
70     /**
71      * @cfg {String} groupField
72      * The field name by which to sort the store's data (defaults to '').
73      */
74     /**
75      * @cfg {Boolean} remoteGroup
76      * True if the grouping should apply on the server side, false if it is local only (defaults to false).  If the
77      * grouping is local, it can be applied immediately to the data.  If it is remote, then it will simply act as a
78      * helper, automatically sending the grouping field name as the 'groupBy' param with each XHR call.
79      */
80     remoteGroup : false,
81     /**
82      * @cfg {Boolean} groupOnSort
83      * True to sort the data on the grouping field when a grouping operation occurs, false to sort based on the
84      * existing sort info (defaults to false).
85      */
86     groupOnSort:false,
87
88     /**
89      * @cfg {String} groupDir
90      * The direction to sort the groups. Defaults to <tt>'ASC'</tt>.
91      */
92     groupDir : 'ASC',
93
94     /**
95      * Clears any existing grouping and refreshes the data using the default sort.
96      */
97     clearGrouping : function(){
98         this.groupField = false;
99
100         if(this.remoteGroup){
101             if(this.baseParams){
102                 delete this.baseParams.groupBy;
103                 delete this.baseParams.groupDir;
104             }
105             var lo = this.lastOptions;
106             if(lo && lo.params){
107                 delete lo.params.groupBy;
108                 delete lo.params.groupDir;
109             }
110
111             this.reload();
112         }else{
113             this.sort();
114             this.fireEvent('datachanged', this);
115         }
116     },
117
118     /**
119      * Groups the data by the specified field.
120      * @param {String} field The field name by which to sort the store's data
121      * @param {Boolean} forceRegroup (optional) True to force the group to be refreshed even if the field passed
122      * in is the same as the current grouping field, false to skip grouping on the same field (defaults to false)
123      */
124     groupBy : function(field, forceRegroup, direction) {
125         direction = direction ? (String(direction).toUpperCase() == 'DESC' ? 'DESC' : 'ASC') : this.groupDir;
126
127         if (this.groupField == field && this.groupDir == direction && !forceRegroup) {
128             return; // already grouped by this field
129         }
130
131         //check the contents of the first sorter. If the field matches the CURRENT groupField (before it is set to the new one),
132         //remove the sorter as it is actually the grouper. The new grouper is added back in by this.sort
133         var sorters = this.multiSortInfo.sorters;
134         if (sorters.length > 0 && sorters[0].field == this.groupField) {
135             sorters.shift();
136         }
137
138         this.groupField = field;
139         this.groupDir = direction;
140         this.applyGroupField();
141
142         var fireGroupEvent = function() {
143             this.fireEvent('groupchange', this, this.getGroupState());
144         };
145
146         if (this.groupOnSort) {
147             this.sort(field, direction);
148             fireGroupEvent.call(this);
149             return;
150         }
151
152         if (this.remoteGroup) {
153             this.on('load', fireGroupEvent, this, {single: true});
154             this.reload();
155         } else {
156             this.sort(sorters);
157             fireGroupEvent.call(this);
158         }
159     },
160
161     //GroupingStore always uses multisorting so we intercept calls to sort here to make sure that our grouping sorter object
162     //is always injected first.
163     sort : function(fieldName, dir) {
164         if (this.remoteSort) {
165             return Ext.data.GroupingStore.superclass.sort.call(this, fieldName, dir);
166         }
167
168         var sorters = [];
169
170         //cater for any existing valid arguments to this.sort, massage them into an array of sorter objects
171         if (Ext.isArray(arguments[0])) {
172             sorters = arguments[0];
173         } else if (fieldName == undefined) {
174             //we preserve the existing sortInfo here because this.sort is called after
175             //clearGrouping and there may be existing sorting
176             sorters = this.sortInfo ? [this.sortInfo] : [];
177         } else {
178             //TODO: this is lifted straight from Ext.data.Store's singleSort function. It should instead be
179             //refactored into a common method if possible
180             var field = this.fields.get(fieldName);
181             if (!field) return false;
182
183             var name       = field.name,
184                 sortInfo   = this.sortInfo || null,
185                 sortToggle = this.sortToggle ? this.sortToggle[name] : null;
186
187             if (!dir) {
188                 if (sortInfo && sortInfo.field == name) { // toggle sort dir
189                     dir = (this.sortToggle[name] || 'ASC').toggle('ASC', 'DESC');
190                 } else {
191                     dir = field.sortDir;
192                 }
193             }
194
195             this.sortToggle[name] = dir;
196             this.sortInfo = {field: name, direction: dir};
197
198             sorters = [this.sortInfo];
199         }
200
201         //add the grouping sorter object as the first multisort sorter
202         if (this.groupField) {
203             sorters.unshift({direction: this.groupDir, field: this.groupField});
204         }
205
206         return this.multiSort.call(this, sorters, dir);
207     },
208
209     /**
210      * @private
211      * Saves the current grouping field and direction to this.baseParams and this.lastOptions.params
212      * if we're using remote grouping. Does not actually perform any grouping - just stores values
213      */
214     applyGroupField: function(){
215         if (this.remoteGroup) {
216             if(!this.baseParams){
217                 this.baseParams = {};
218             }
219
220             Ext.apply(this.baseParams, {
221                 groupBy : this.groupField,
222                 groupDir: this.groupDir
223             });
224
225             var lo = this.lastOptions;
226             if (lo && lo.params) {
227                 lo.params.groupDir = this.groupDir;
228
229                 //this is deleted because of a bug reported at http://www.extjs.com/forum/showthread.php?t=82907
230                 delete lo.params.groupBy;
231             }
232         }
233     },
234
235     /**
236      * @private
237      * TODO: This function is apparently never invoked anywhere in the framework. It has no documentation
238      * and should be considered for deletion
239      */
240     applyGrouping : function(alwaysFireChange){
241         if(this.groupField !== false){
242             this.groupBy(this.groupField, true, this.groupDir);
243             return true;
244         }else{
245             if(alwaysFireChange === true){
246                 this.fireEvent('datachanged', this);
247             }
248             return false;
249         }
250     },
251
252     /**
253      * @private
254      * Returns the grouping field that should be used. If groupOnSort is used this will be sortInfo's field,
255      * otherwise it will be this.groupField
256      * @return {String} The group field
257      */
258     getGroupState : function(){
259         return this.groupOnSort && this.groupField !== false ?
260                (this.sortInfo ? this.sortInfo.field : undefined) : this.groupField;
261     }
262 });
263 Ext.reg('groupingstore', Ext.data.GroupingStore);