Upgrade to ExtJS 3.2.0 - Released 03/30/2010
[extjs.git] / src / data / GroupingStore.js
1 /*!
2  * Ext JS Library 3.2.0
3  * Copyright(c) 2006-2010 Ext JS, Inc.
4  * licensing@extjs.com
5  * http://www.extjs.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     groupDir : 'ASC',
89
90     /**
91      * Clears any existing grouping and refreshes the data using the default sort.
92      */
93     clearGrouping : function(){
94         this.groupField = false;
95
96         if(this.remoteGroup){
97             if(this.baseParams){
98                 delete this.baseParams.groupBy;
99                 delete this.baseParams.groupDir;
100             }
101             var lo = this.lastOptions;
102             if(lo && lo.params){
103                 delete lo.params.groupBy;
104                 delete lo.params.groupDir;
105             }
106
107             this.reload();
108         }else{
109             this.sort();
110             this.fireEvent('datachanged', this);
111         }
112     },
113
114     /**
115      * Groups the data by the specified field.
116      * @param {String} field The field name by which to sort the store's data
117      * @param {Boolean} forceRegroup (optional) True to force the group to be refreshed even if the field passed
118      * in is the same as the current grouping field, false to skip grouping on the same field (defaults to false)
119      */
120     groupBy : function(field, forceRegroup, direction) {
121         direction = direction ? (String(direction).toUpperCase() == 'DESC' ? 'DESC' : 'ASC') : this.groupDir;
122
123         if (this.groupField == field && this.groupDir == direction && !forceRegroup) {
124             return; // already grouped by this field
125         }
126
127         //check the contents of the first sorter. If the field matches the CURRENT groupField (before it is set to the new one),
128         //remove the sorter as it is actually the grouper. The new grouper is added back in by this.sort
129         sorters = this.multiSortInfo.sorters;
130         if (sorters.length > 0 && sorters[0].field == this.groupField) {
131             sorters.shift();
132         }
133
134         this.groupField = field;
135         this.groupDir = direction;
136         this.applyGroupField();
137
138         var fireGroupEvent = function() {
139             this.fireEvent('groupchange', this, this.getGroupState());
140         };
141
142         if (this.groupOnSort) {
143             this.sort(field, direction);
144             fireGroupEvent.call(this);
145             return;
146         }
147
148         if (this.remoteGroup) {
149             this.on('load', fireGroupEvent, this, {single: true});
150             this.reload();
151         } else {
152             this.sort(sorters);
153             fireGroupEvent.call(this);
154         }
155     },
156
157     //GroupingStore always uses multisorting so we intercept calls to sort here to make sure that our grouping sorter object
158     //is always injected first.
159     sort : function(fieldName, dir) {
160         if (this.remoteSort) {
161             return Ext.data.GroupingStore.superclass.sort.call(this, fieldName, dir);
162         }
163
164         var sorters = [];
165
166         //cater for any existing valid arguments to this.sort, massage them into an array of sorter objects
167         if (Ext.isArray(arguments[0])) {
168             sorters = arguments[0];
169         } else if (fieldName == undefined) {
170             //we preserve the existing sortInfo here because this.sort is called after
171             //clearGrouping and there may be existing sorting
172             sorters = [this.sortInfo];
173         } else {
174             //TODO: this is lifted straight from Ext.data.Store's singleSort function. It should instead be
175             //refactored into a common method if possible
176             var field = this.fields.get(fieldName);
177             if (!field) return false;
178
179             var name       = field.name,
180                 sortInfo   = this.sortInfo || null,
181                 sortToggle = this.sortToggle ? this.sortToggle[name] : null;
182
183             if (!dir) {
184                 if (sortInfo && sortInfo.field == name) { // toggle sort dir
185                     dir = (this.sortToggle[name] || 'ASC').toggle('ASC', 'DESC');
186                 } else {
187                     dir = field.sortDir;
188                 }
189             }
190
191             this.sortToggle[name] = dir;
192             this.sortInfo = {field: name, direction: dir};
193
194             sorters = [this.sortInfo];
195         }
196
197         //add the grouping sorter object as the first multisort sorter
198         if (this.groupField) {
199             sorters.unshift({direction: this.groupDir, field: this.groupField});
200         }
201
202         return this.multiSort.call(this, sorters, dir);
203     },
204
205     /**
206      * @private
207      * Saves the current grouping field and direction to this.baseParams and this.lastOptions.params
208      * if we're using remote grouping. Does not actually perform any grouping - just stores values
209      */
210     applyGroupField: function(){
211         if (this.remoteGroup) {
212             if(!this.baseParams){
213                 this.baseParams = {};
214             }
215
216             Ext.apply(this.baseParams, {
217                 groupBy : this.groupField,
218                 groupDir: this.groupDir
219             });
220
221             var lo = this.lastOptions;
222             if (lo && lo.params) {
223                 lo.params.groupDir = this.groupDir;
224
225                 //this is deleted because of a bug reported at http://www.extjs.com/forum/showthread.php?t=82907
226                 delete lo.params.groupBy;
227             }
228         }
229     },
230
231     /**
232      * @private
233      * TODO: This function is apparently never invoked anywhere in the framework. It has no documentation
234      * and should be considered for deletion
235      */
236     applyGrouping : function(alwaysFireChange){
237         if(this.groupField !== false){
238             this.groupBy(this.groupField, true, this.groupDir);
239             return true;
240         }else{
241             if(alwaysFireChange === true){
242                 this.fireEvent('datachanged', this);
243             }
244             return false;
245         }
246     },
247
248     /**
249      * @private
250      * Returns the grouping field that should be used. If groupOnSort is used this will be sortInfo's field,
251      * otherwise it will be this.groupField
252      * @return {String} The group field
253      */
254     getGroupState : function(){
255         return this.groupOnSort && this.groupField !== false ?
256                (this.sortInfo ? this.sortInfo.field : undefined) : this.groupField;
257     }
258 });
259 Ext.reg('groupingstore', Ext.data.GroupingStore);