]>
Commit | Line | Data |
---|---|---|
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 | |
11 | Ext.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 |