]> git.proxmox.com Git - extjs.git/blame - extjs/classic/classic/src/grid/feature/GroupStore.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / src / grid / feature / GroupStore.js
CommitLineData
6527f429
DM
1/**\r
2 * Private record store class which takes the place of the view's data store to provide a grouped\r
3 * view of the data when the Grouping feature is used.\r
4 *\r
5 * Relays granular mutation events from the underlying store as refresh events to the view.\r
6 *\r
7 * On mutation events from the underlying store, updates the summary rows by firing update events on the corresponding\r
8 * summary records.\r
9 * @private\r
10 */\r
11Ext.define('Ext.grid.feature.GroupStore', {\r
12 extend: 'Ext.util.Observable',\r
13\r
14 isStore: true,\r
15\r
16 // Number of records to load into a buffered grid before it has been bound to a view of known size\r
17 defaultViewSize: 100,\r
18\r
19 // Use this property moving forward for all feature stores. It will be used to ensure\r
20 // that the correct object is used to call various APIs. See EXTJSIV-10022.\r
21 isFeatureStore: true,\r
22\r
23 badGrouperKey: '[object Object]',\r
24\r
25 constructor: function(groupingFeature, store) {\r
26 var me = this;\r
27\r
28 me.callParent();\r
29 me.groupingFeature = groupingFeature;\r
30 me.bindStore(store);\r
31\r
32 // We don't want to listen to store events in a locking assembly.\r
33 if (!groupingFeature.grid.isLocked) {\r
34 me.bindViewStoreListeners();\r
35 }\r
36 },\r
37\r
38 bindStore: function(store) {\r
39 var me = this;\r
40\r
41 if (!store || me.store !== store) {\r
42 Ext.destroy(me.storeListeners);\r
43 me.store = null;\r
44 }\r
45\r
46 if (store) {\r
47 me.storeListeners = store.on({\r
48 groupchange: me.onGroupChange,\r
49 remove: me.onRemove,\r
50 add: me.onAdd,\r
51 idchanged: me.onIdChanged,\r
52 update: me.onUpdate,\r
53 refresh: me.onRefresh,\r
54 clear: me.onClear,\r
55 scope: me,\r
56 destroyable: true\r
57 });\r
58\r
59 me.store = store;\r
60 me.processStore(store);\r
61 }\r
62 },\r
63\r
64 bindViewStoreListeners: function () {\r
65 var view = this.groupingFeature.view,\r
66 listeners = view.getStoreListeners();\r
67\r
68 listeners.scope = view;\r
69\r
70 this.on(listeners);\r
71 },\r
72\r
73 processStore: function (store) {\r
74 var me = this,\r
75 groupingFeature = me.groupingFeature,\r
76 collapseAll = groupingFeature.startCollapsed,\r
77 data = me.data,\r
78 ExtArray = Ext.Array,\r
79 indexOf = ExtArray.indexOf,\r
80 splice = ExtArray.splice,\r
81 groups = store.getGroups(),\r
82 groupCount = groups ? groups.length : 0,\r
83 groupField = store.getGroupField(),\r
84 // We need to know all of the possible unique group names. The only way to know this is to check itemGroupKeys, which will keep a\r
85 // list of all potential group names. It's not enough to get the key of the existing groups since the collection may be filtered.\r
86 groupNames = groups && ExtArray.unique(Ext.Object.getValues(groups.itemGroupKeys)),\r
87 isCollapsed = false,\r
88 oldMetaGroupCache = groupingFeature.getCache(),\r
89 metaGroup, metaGroupCache, i, len, featureGrouper, group, groupName, groupPlaceholder, key, modelData, Model;\r
90\r
91 groupingFeature.invalidateCache();\r
92 // Get a new cache since we invalidated the old one.\r
93 metaGroupCache = groupingFeature.getCache();\r
94\r
95 // Persist what we can.\r
96 if (oldMetaGroupCache.map) {\r
97 metaGroupCache.map = oldMetaGroupCache.map;\r
98 }\r
99\r
100 if (data) {\r
101 data.clear();\r
102 } else {\r
103 data = me.data = new Ext.util.Collection({\r
104 rootProperty: 'data',\r
105 extraKeys: {\r
106 byInternalId: {\r
107 property: 'internalId',\r
108 rootProperty: ''\r
109 }\r
110 }\r
111 });\r
112 }\r
113\r
114 if (store.getCount()) {\r
115 // Upon first process of a loaded store, clear the "always" collapse" flag\r
116 groupingFeature.startCollapsed = false;\r
117\r
118 if (groupCount > 0) {\r
119 Model = store.getModel();\r
120\r
121 for (i = 0; i < groupCount; i++) {\r
122 group = groups.getAt(i);\r
123\r
124 // Cache group information by group name.\r
125 key = group.getGroupKey();\r
126\r
127 // If there is no store grouper and the groupField looks up a complex data type, the store will stringify it and\r
128 // the group name will be '[object Object]'. To fix this, groupers can be defined in the feature config, so we'll\r
129 // simply do a lookup here and re-group the store.\r
130 //\r
131 // Note that if a grouper wasn't defined on the feature that we'll just default to the old behavior and still try\r
132 // to group.\r
133 if (me.badGrouperKey === key && (featureGrouper = groupingFeature.getGrouper(groupField))) {\r
134 // We must reset the value b/c store.group() will call into this method again!\r
135 groupingFeature.startCollapsed = collapseAll;\r
136 store.group(featureGrouper);\r
137 return;\r
138 }\r
139\r
140 // Persist what we can.\r
141 metaGroup = metaGroupCache[key] = oldMetaGroupCache[key] || groupingFeature.getMetaGroup(key);\r
142\r
143 // Remove the group name from the list of all possible group names. This is how we'll know if any remaining groups\r
144 // in the old cache should be retained.\r
145 splice(groupNames, indexOf(groupNames, key), 1);\r
146\r
147 isCollapsed = metaGroup.isCollapsed = collapseAll || metaGroup.isCollapsed;\r
148\r
149 // If group is collapsed, then represent it by one dummy row which is never visible, but which acts\r
150 // as a start and end group trigger.\r
151 if (isCollapsed) {\r
152 modelData = {};\r
153 modelData[groupField] = key;\r
154 metaGroup.placeholder = groupPlaceholder = new Model(modelData);\r
155 groupPlaceholder.isNonData = groupPlaceholder.isCollapsedPlaceholder = true;\r
156 groupPlaceholder.group = group;\r
157 data.add(groupPlaceholder);\r
158 }\r
159 // Expanded group - add the group's child records.\r
160 else {\r
161 data.insert(me.data.length, group.items);\r
162 }\r
163 }\r
164\r
165 if (groupNames.length) {\r
166 // The remainig group names in this list may refer to potential groups that have been filtered/sorted. If the group\r
167 // name exists in the old cache, we must retain it b/c the groups could be recreated. See EXTJS-15755 for an example.\r
168 // Anything left in the old cache can be discarded.\r
169 for (i = 0, len = groupNames.length; i < len; i++) {\r
170 groupName = groupNames[i];\r
171 metaGroupCache[groupName] = oldMetaGroupCache[groupName];\r
172 }\r
173 }\r
174\r
175 oldMetaGroupCache = null;\r
176 } else {\r
177 data.add(store.getRange());\r
178 }\r
179 }\r
180 },\r
181\r
182 isCollapsed: function(name) {\r
183 return this.groupingFeature.getCache()[name].isCollapsed;\r
184 },\r
185\r
186 isLoading: function() {\r
187 return false;\r
188 },\r
189\r
190 getData: function() {\r
191 return this.data;\r
192 },\r
193\r
194 getCount: function() {\r
195 return this.data.getCount();\r
196 },\r
197\r
198 getTotalCount: function() {\r
199 return this.data.getCount();\r
200 },\r
201\r
202 // This class is only created for fully loaded, non-buffered stores\r
203 rangeCached: function(start, end) {\r
204 return end < this.getCount();\r
205 },\r
206\r
207 getRange: function(start, end, options) {\r
208 // Collection's getRange is exclusive. Do NOT mutate the value: it is passed to the callback.\r
209 var result = this.data.getRange(start, Ext.isNumber(end) ? end + 1 : end);\r
210\r
211 if (options && options.callback) {\r
212 options.callback.call(options.scope || this, result, start, end, options);\r
213 }\r
214 return result;\r
215 },\r
216\r
217 getAt: function(index) {\r
218 return this.data.getAt(index);\r
219 },\r
220\r
221 /**\r
222 * Get the Record with the specified id.\r
223 *\r
224 * This method is not affected by filtering, lookup will be performed from all records\r
225 * inside the store, filtered or not.\r
226 *\r
227 * @param {Mixed} id The id of the Record to find.\r
228 * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.\r
229 */\r
230 getById: function(id) {\r
231 return this.store.getById(id);\r
232 },\r
233\r
234 /**\r
235 * @private\r
236 * Get the Record with the specified internalId.\r
237 *\r
238 * This method is not effected by filtering, lookup will be performed from all records\r
239 * inside the store, filtered or not.\r
240 *\r
241 * @param {Mixed} internalId The id of the Record to find.\r
242 * @return {Ext.data.Model} The Record with the passed internalId. Returns null if not found.\r
243 */\r
244 getByInternalId: function (internalId) {\r
245 // Find the record in the base store.\r
246 // If it was a placeholder, then it won't be there, it will be in our data Collection.\r
247 return this.store.getByInternalId(internalId) || this.data.byInternalId.get(internalId);\r
248 },\r
249\r
250 expandGroup: function(group) {\r
251 var me = this,\r
252 groupingFeature = me.groupingFeature,\r
253 metaGroup, placeholder, startIdx, items;\r
254\r
255 if (typeof group === 'string') {\r
256 group = groupingFeature.getGroup(group);\r
257 }\r
258\r
259 if (group) {\r
260 items = group.items;\r
261 metaGroup = groupingFeature.getMetaGroup(group);\r
262 placeholder = metaGroup.placeholder;\r
263 }\r
264\r
265 if (items.length && (startIdx = me.data.indexOf(placeholder)) !== -1) {\r
266 // Any event handlers must see the new state\r
267 metaGroup.isCollapsed = false;\r
268 me.isExpandingOrCollapsing = 1;\r
269\r
270 // Remove the collapsed group placeholder record\r
271 me.data.removeAt(startIdx);\r
272\r
273 // Insert the child records in its place\r
274 me.data.insert(startIdx, group.items);\r
275\r
276 // Update views\r
277 me.fireEvent('replace', me, startIdx, [placeholder], group.items);\r
278\r
279 me.fireEvent('groupexpand', me, group);\r
280 me.isExpandingOrCollapsing = 0;\r
281 }\r
282 },\r
283\r
284 collapseGroup: function(group) {\r
285 var me = this,\r
286 groupingFeature = me.groupingFeature,\r
287 startIdx,\r
288 placeholder,\r
289 len, items;\r
290\r
291 if (typeof group === 'string') {\r
292 group = groupingFeature.getGroup(group);\r
293 }\r
294\r
295 if (group) {\r
296 items = group.items;\r
297 }\r
298\r
299 if (items && (len = items.length) && (startIdx = me.data.indexOf(items[0])) !== -1) {\r
300\r
301 // Any event handlers must see the new state\r
302 groupingFeature.getMetaGroup(group).isCollapsed = true;\r
303 me.isExpandingOrCollapsing = 2;\r
304\r
305 // Remove the group child records\r
306 me.data.removeAt(startIdx, len);\r
307\r
308 // Insert a placeholder record in their place\r
309 me.data.insert(startIdx, placeholder = me.getGroupPlaceholder(group));\r
310\r
311 // Update views\r
312 me.fireEvent('replace', me, startIdx, items, [placeholder]);\r
313\r
314 me.fireEvent('groupcollapse', me, group);\r
315 me.isExpandingOrCollapsing = 0;\r
316 }\r
317 },\r
318\r
319 getGroupPlaceholder: function(group) {\r
320 var metaGroup = this.groupingFeature.getMetaGroup(group);\r
321\r
322 if (!metaGroup.placeholder) {\r
323 var store = this.store,\r
324 Model = store.getModel(),\r
325 modelData = {},\r
326 key = group.getGroupKey(),\r
327 groupPlaceholder;\r
328\r
329 modelData[store.getGroupField()] = key;\r
330 groupPlaceholder = metaGroup.placeholder = new Model(modelData);\r
331 groupPlaceholder.isNonData = groupPlaceholder.isCollapsedPlaceholder = true;\r
332\r
333 // Let's poke the groupKey onto the record instead of storing a reference to the group\r
334 // itself. The latter can cause problems if the store is reloaded and the referenced\r
335 // group is lost.\r
336 // See EXTJS-18655\r
337 groupPlaceholder.groupKey = key;\r
338 }\r
339\r
340 return metaGroup.placeholder;\r
341 },\r
342\r
343 // Find index of record in group store.\r
344 // If it's in a collapsed group, then it's -1, not present\r
345 indexOf: function(record) {\r
346 var ret = -1;\r
347 if (!record.isCollapsedPlaceholder) {\r
348 ret = this.data.indexOf(record);\r
349 }\r
350 return ret;\r
351 },\r
352\r
353 contains: function(record) {\r
354 return this.indexOf(record) > -1;\r
355 },\r
356\r
357 indexOfPlaceholder: function(record) {\r
358 return this.data.indexOf(record);\r
359 },\r
360\r
361 /**\r
362 * Get the index within the store of the Record with the passed id.\r
363 *\r
364 * Like #indexOf, this method is effected by filtering.\r
365 *\r
366 * @param {String} id The id of the Record to find.\r
367 * @return {Number} The index of the Record. Returns -1 if not found.\r
368 */\r
369 indexOfId: function(id) {\r
370 return this.data.indexOfKey(id);\r
371 },\r
372\r
373 /**\r
374 * Get the index within the entire dataset. From 0 to the totalCount.\r
375 *\r
376 * Like #indexOf, this method is effected by filtering.\r
377 *\r
378 * @param {Ext.data.Model} record The Ext.data.Model object to find.\r
379 * @return {Number} The index of the passed Record. Returns -1 if not found.\r
380 */\r
381 indexOfTotal: function(record) {\r
382 return this.store.indexOf(record);\r
383 },\r
384\r
385 onAdd: function(store) {\r
386 var me = this;\r
387\r
388 me.processStore(me.store);\r
389 me.fireEvent('refresh', me);\r
390\r
391 // Don't allow the event to propagate or another group will be added upstream by tableview!\r
392 return false;\r
393 },\r
394\r
395 onClear: function(store, records, startIndex) {\r
396 var me = this;\r
397\r
398 me.processStore(me.store);\r
399 me.fireEvent('clear', me);\r
400 },\r
401\r
402 onIdChanged: function(store, rec, oldId, newId) {\r
403 this.data.updateKey(rec, oldId);\r
404 },\r
405\r
406 onRefresh: function() {\r
407 this.processStore(this.store);\r
408 this.fireEvent('refresh', this);\r
409 },\r
410\r
411 onRemove: function() {\r
412 var me = this;\r
413\r
414 me.processStore(me.store);\r
415 me.fireEvent('refresh', me);\r
416\r
417 // Don't allow the event to propagate or the view will not be fully updated.\r
418 return false;\r
419 },\r
420\r
421 onUpdate: function(store, record, operation, modifiedFieldNames) {\r
422 var me = this,\r
423 groupingFeature = me.groupingFeature,\r
424 group, metaGroup, firstRec, lastRec, items;\r
425\r
426 // The grouping field value has been modified.\r
427 // This could either move a record from one group to another, or introduce a new group.\r
428 // Either way, we have to refresh the grid\r
429 if (store.isGrouped()) {\r
430 // Updating a single record, attach the group to the record for Grouping.setupRowData to use.\r
431 group = record.group = groupingFeature.getGroup(record);\r
432\r
433 // Make sure that still we have a group and that the last member of it wasn't just filtered.\r
434 // See EXTJS-18083.\r
435 if (group) {\r
436 metaGroup = groupingFeature.getMetaGroup(record);\r
437\r
438 if (modifiedFieldNames && Ext.Array.contains(modifiedFieldNames, groupingFeature.getGroupField())) {\r
439 return me.onRefresh(me.store);\r
440 }\r
441\r
442 // Fire an update event on the collapsed metaGroup placeholder record\r
443 if (metaGroup.isCollapsed) {\r
444 me.fireEvent('update', me, metaGroup.placeholder);\r
445 }\r
446\r
447 // Not in a collapsed group, fire update event on the modified record\r
448 // and, if in a grouped store, on the first and last records in the group.\r
449 else {\r
450 Ext.suspendLayouts();\r
451\r
452 // Propagate the record's update event\r
453 me.fireEvent('update', me, record, operation, modifiedFieldNames);\r
454\r
455 // Fire update event on first and last record in group (only once if a single row group)\r
456 // So that custom header TPL is applied, and the summary row is updated\r
457 items = group.items;\r
458 firstRec = items[0];\r
459 lastRec = items[items.length - 1];\r
460\r
461 // Fire an update on the first and last row in the group (ensure we don't refire update on the modified record).\r
462 // This is to give interested Features the opportunity to update the first item (a wrapped group header + data row),\r
463 // and last item (a wrapped data row + group summary)\r
464 if (firstRec !== record) {\r
465 firstRec.group = group;\r
466 me.fireEvent('update', me, firstRec, 'edit', modifiedFieldNames);\r
467 delete firstRec.group;\r
468 }\r
469 if (lastRec !== record && lastRec !== firstRec && groupingFeature.showSummaryRow) {\r
470 lastRec.group = group;\r
471 me.fireEvent('update', me, lastRec, 'edit', modifiedFieldNames);\r
472 delete lastRec.group;\r
473 }\r
474 Ext.resumeLayouts(true);\r
475 }\r
476 }\r
477\r
478 delete record.group;\r
479 } else {\r
480 // Propagate the record's update event\r
481 me.fireEvent('update', me, record, operation, modifiedFieldNames);\r
482 }\r
483 },\r
484\r
485 // Relay the groupchange event\r
486 onGroupChange: function(store, grouper) {\r
487 if (!grouper) {\r
488 this.processStore(store);\r
489 }\r
490 this.fireEvent('groupchange', store, grouper);\r
491 },\r
492\r
493 destroy: function() {\r
494 var me = this;\r
495\r
496 me.bindStore(null);\r
497 Ext.destroyMembers(me, 'data', 'groupingFeature');\r
498\r
499 me.callParent();\r
500 }\r
501});\r
502\r