/** * @private * A collection containing the result of applying grouping to the records in the store. */Ext.define('Ext.util.GroupCollection', { extend: 'Ext.util.Collection', requires: [ 'Ext.util.Group', // Since Collection uses sub-collections of various derived types we step up to // list all the requirements of Collection. The idea being that instead of a // "requires" of Ext.util.Collection (which cannot pull everything) you instead // do a "requires" of Ext.util.GroupCollection and it will. 'Ext.util.SorterCollection', 'Ext.util.FilterCollection', 'Ext.util.GrouperCollection' ], isGroupCollection: true, config: { grouper: null, groupConfig: null, itemRoot: null }, observerPriority: -100, emptyGroupRetainTime: 300000, // Private timer to hang on to emptied groups. Milliseconds. rootProperty: '_data', // this is required by the sorter constructor: function(config) { this.emptyGroups = {}; this.callParent([config]); this.on('remove', 'onGroupRemove', this); }, /** * Returns the `Ext.util.Group` associated with the given record. * * @param {Object} item The item for which the group is desired. * @return {Ext.util.Group} * @since 6.5.0 */ getItemGroup: function(item) { var grouper = this.lastMonitoredGrouper, key, group; if (!grouper && this.items.length) { grouper = this.items[0].getGrouper(); } if (grouper) { key = grouper.getGroupString(item); group = this.get(key); } return group; }, /** * Find the group that matches the specified path or `false` if not found. * * @param {String} path Path to the group * @return {Ext.util.Group} */ getByPath: function(path) { var paths = path ? String(path).split(Ext.util.Group.prototype.pathSeparator) : [], len = paths.length, items = this, group = false, i; for (i = 0; i < len; i++) { if (!items || items.length === 0) { break; } group = items.get(paths[i]); if (group) { items = group.getGroups(); } } return group || false; }, /** * Find all groups that contain the specified item * * @param {Object} item * @return {Ext.util.Group[]} */ getGroupsByItem: function(item) { var me = this, groups = [], length = me.items.length, i, group, children; if (item) { for (i = 0; i < length; i++) { group = me.items[i]; if (group.indexOf(item) >= 0) { groups.push(group); children = group.getGroups(); if (children) { /* eslint-disable-next-line max-len */ return Ext.Array.insert(groups, groups.length, children.getGroupsByItem(item)); } } } } return groups; }, //------------------------------------------------------------------------- // Calls from the source Collection: onCollectionAdd: function(source, details) { if (!this.isConfiguring) { this.addItemsToGroups(source, details.items, details.at); } }, onCollectionBeforeItemChange: function(source, details) { this.changeDetails = details; }, onCollectionBeginUpdate: function() { this.beginUpdate(); }, onCollectionEndUpdate: function() { this.endUpdate(); }, onCollectionItemChange: function(source, details) { // Check if the change to the item caused the item to move. If it did, the group // ordering will be handled by virtue of being removed/added to the collection. // If not, check whether we're in the correct group and fix up if not. if (!details.indexChanged) { this.syncItemGrouping(source, details); } this.changeDetails = null; }, onCollectionRefresh: function(source) { if (source.generation) { // eslint-disable-next-line vars-on-top var me = this, itemGroupKeys = me.itemGroupKeys = {}, groupData, entries, groupKey, i, len, entry, j; me.groupersChanged = true; groupData = me.createEntries(source, source.items); entries = groupData.entries; // The magic of Collection will automatically update the group with its new // members. for (i = 0, len = entries.length; i < len; ++i) { entry = entries[i]; // Will add or replace entry.group.splice(0, 1e99, entry.items); // Add item key -> group mapping for every entry for (j = 0; j < entry.items.length; j++) { itemGroupKeys[source.getKey(entry.items[j])] = entry.group; } } // Remove groups to which we have not added items. entries = null; for (groupKey in me.map) { if (!(groupKey in groupData.groups)) { (entries || (entries = [])).push(me.map[groupKey]); } } if (entries) { me.remove(entries); } // autoSort is disabled when adding new groups because // it relies on there being at least one record in the group me.sortItems(); me.groupersChanged = false; } }, onCollectionRemove: function(source, details) { var me = this, groupers = source.getGroupers(), changeDetails = me.changeDetails, itemGroupKeys = me.itemGroupKeys || (me.itemGroupKeys = {}), entries, entry, group, i, n, j, removeGroups, item; if (!groupers || !groupers.length) { return; } if (source.getCount()) { if (changeDetails) { // The item has changed, so the group key may be different, need // to look it up item = changeDetails.item || changeDetails.items[0]; entries = me.createEntries(source, [item], false).entries; entries[0].group = itemGroupKeys['oldKey' in details ? details.oldKey : source.getKey(item)]; } else { entries = me.createEntries(source, details.items, false).entries; } for (i = 0, n = entries.length; i < n; ++i) { group = (entry = entries[i]).group; if (group) { group.remove(entry.items); } // Delete any item key -> group mapping for (j = 0; j < entry.items.length; j++) { delete itemGroupKeys[source.getKey(entry.items[j])]; } if (group && !group.length) { (removeGroups || (removeGroups = [])).push(group); } } } // Straight cleardown else { me.itemGroupKeys = {}; removeGroups = me.items; for (i = 0, n = removeGroups.length; i < n; ++i) { removeGroups[i].clear(); } } if (removeGroups) { me.remove(removeGroups); } }, // If the SorterCollection instance is not changing, the Group will react to // changes inside the SorterCollection, but if the instance changes we need // to sync the Group to the new SorterCollection. onCollectionSort: function(source) { // sorting the collection effectively sorts the items in each group... var me = this, sorters = source.getSorters(false), items, length, i, group; if (sorters) { items = me.items; length = me.length; for (i = 0; i < length; ++i) { group = items[i]; if (group.getSorters() === sorters) { group.sortItems(); } else { group.setSorters(sorters); } } } }, onCollectionUpdateKey: function(source, details) { if (!details.indexChanged) { details.oldIndex = source.indexOf(details.item); this.syncItemGrouping(source, details); } }, onCollectionGroupersChanged: function(source) { var me = this, groupers = source.getGroupers(), grouper; if (groupers.length > 0) { grouper = groupers.items[0]; me.changeSorterFn(grouper); if (me.lastMonitoredGrouper) { me.lastMonitoredGrouper.removeObserver(me); } me.lastMonitoredGrouper = grouper; grouper.addObserver(me); } else { me.removeAll(); } }, onGrouperDirectionChange: function(grouper) { // if the grouper changes its direction then we need to sort again our groups this.changeSorterFn(grouper); this.onEndUpdateSorters(this.getSorters()); }, onEndUpdateSorters: function(sorters) { var me = this, was = me.sorted, is = (me.grouped && me.getAutoGroup()) || (me.getAutoSort() && sorters && sorters.length > 0); if (was || is) { // ensure flag property is a boolean. // sorters && (sorters.length > 0) may evaluate to null me.sorted = !!is; me.onSortChange(sorters); } }, //------------------------------------------------------------------------- // Private changeSorterFn: function(grouper) { var me = this, sorters = me.getSorters(), // this group collection needs to be sorted sorter = { root: me.getRootProperty() }; sorter.direction = grouper.getDirection(); if (grouper) { sorter.id = grouper.getProperty(); if (grouper.initialConfig.sorterFn) { sorter.sorterFn = grouper.initialConfig.sorterFn; } else { sorter.property = grouper.getSortProperty() || grouper.getProperty(); } } if (sorter.property || sorter.sorterFn) { if (sorters.length === 0) { sorters.add(sorter); } else { sorters.items[0].setConfig(sorter); } } else { sorters.clear(); } }, addItemsToGroups: function(source, items, at, oldIndex) { var me = this, itemGroupKeys = me.itemGroupKeys || (me.itemGroupKeys = {}), entries = me.createEntries(source, items).entries, index = -1, sourceStartIndex, entry, i, len, j, group, firstIndex, item; for (i = 0, len = entries.length; i < len; ++i) { entry = entries[i]; group = entry.group; // A single item moved - from onCollectionItemChange if (oldIndex || oldIndex === 0) { item = items[0]; if (group.getCount() > 0 && source.getSorters().getCount() === 0) { // We have items in the group & it's not sorted, so find the // correct position in the group to insert. firstIndex = source.indexOf(group.items[0]); if (oldIndex < firstIndex) { index = 0; } else { index = oldIndex - firstIndex; } } if (index === -1) { group.add(item); } else { group.insert(index, item); } } else { if (me.length > 1 && at) { sourceStartIndex = source.indexOf(entries[0].group.getAt(0)); at = Math.max(at - sourceStartIndex, 0); } entry.group.insert(at != null ? at : group.items.length, entry.items); // Add item key -> group mapping for (j = 0; j < entry.items.length; j++) { itemGroupKeys[source.getKey(entry.items[j])] = entry.group; } } } // autoSort is disabled when adding new groups because // it relies on there being at least one record in the group me.sortItems(); }, createEntries: function(source, items, createGroups) { // Separate the items out into arrays by group var me = this, groups = {}, entries = [], groupers = source.getGroupers().getRange(), grouper, entry, group, groupKey, i, item, len; if (groupers.length) { grouper = groupers.shift(); groupers = groupers.length ? Ext.clone(groupers) : null; for (i = 0, len = items.length; i < len; ++i) { groupKey = grouper.getGroupString(item = items[i]); if (!(entry = groups[groupKey])) { group = me.getGroup(source, item, grouper, groupers, createGroups); entries.push(groups[groupKey] = entry = { group: group, items: [] }); } // Collect items to add/remove for each group // which has items in the array entry.items.push(item); } } return { groups: groups, entries: entries }; }, syncItemGrouping: function(source, details) { var me = this, itemGroupKeys = me.itemGroupKeys || (me.itemGroupKeys = {}), item = details.item, groupers = source.getGroupers().getRange(), grouper, oldKey, itemKey, oldGroup, group; if (!groupers.length) { return; } grouper = groupers.shift(); groupers = groupers.length ? Ext.clone(groupers) : null; itemKey = source.getKey(item); oldKey = 'oldKey' in details ? details.oldKey : itemKey; // The group the item was in before the change took place. oldGroup = itemGroupKeys[oldKey]; // Look up/create the group into which the item now must be added. group = me.getGroup(source, item, grouper, groupers); details.group = group; details.oldGroup = oldGroup; details.groupChanged = (group !== oldGroup); // The change did not cause a change in group if (group === oldGroup) { // Inform group about change oldGroup.itemChanged(item, details.modified, details.oldKey, details); } else { // Remove from its old group if there was one. if (oldGroup) { // Ensure Group knows about any unknown key changes, or item will not be removed. oldGroup.updateKey(item, oldKey, itemKey); oldGroup.remove(item); // Queue newly empy group for destruction. if (!oldGroup.length) { me.remove(oldGroup); } } // Add to new group me.addItemsToGroups(source, [item], null, details.oldIndex); } // Keep item key -> group mapping up to date delete itemGroupKeys[oldKey]; itemGroupKeys[itemKey] = group; }, getGroup: function(source, item, grouper, groupers, createGroups) { var me = this, key = grouper.getGroupString(item), prop = grouper.getSortProperty(), root = grouper.getRoot(), group = me.get(key), autoSort = me.getAutoSort(), label; if (group) { group.setSorters(source.getSorters()); if (me.groupersChanged) { // if the groupers changed then we need to update the groupers on the existing group label = group.getLabel(); group.setLabel(null); group.setGroupers(groupers); group.setGrouper(grouper); group.setParent(source.isGroup ? source : me); group.setLabel(label); } } else if (createGroups !== false) { group = me.emptyGroups[key]; if (group && group.destroyed) { delete me.emptyGroups[key]; group = null; } if (group) { group.setLabel(null); } else { group = Ext.create(Ext.apply({ xclass: 'Ext.util.Group', groupConfig: me.getGroupConfig() }, me.getGroupConfig())); } me.setAutoSort(false); group.setConfig({ groupKey: key, grouper: grouper, groupers: groupers, label: key, rootProperty: me.getItemRoot(), sorters: source.getSorters(), autoSort: autoSort, autoGroup: me.getAutoGroup(), parent: source.isGroup ? source : me }); group.ejectTime = null; me.add(group); me.setAutoSort(autoSort); if (prop) { group.setCustomData(prop, (root ? item[root] : item)[prop]); } } return group; }, getKey: function(item) { return item.getGroupKey(); }, createSortFn: function() { return this.getSorters().getSortFn(); }, // override the collection fn getGrouper: function() { return this.lastMonitoredGrouper; }, // override the collection fn updateGrouper: Ext.emptyFn, updateAutoGroup: function(autoGroup) { var len = this.length, i; // the group collection is not grouped but sorting could be // disabled when autoGroup is false in the source Collection this.setAutoSort(autoGroup); for (i = 0; i < len; i++) { this.items[i].setAutoGroup(autoGroup); } // Important to call this so it can clear the .sorted flag // as needed this.onEndUpdateSorters(this._sorters); }, destroy: function() { var me = this, grouper = me.lastMonitoredGrouper; if (grouper) { grouper.removeObserver(me); } me.$groupable = null; // Ensure group objects get destroyed, they may have // added listeners to the main collection sorters. me.destroyGroups(me.items); Ext.undefer(me.checkRemoveQueueTimer); me.callParent(); }, privates: { destroyGroups: function(groups) { var len = groups.length, i; for (i = len - 1; i >= 0; i--) { groups[i].destroy(); } }, onGroupRemove: function(collection, info) { var me = this, groups = info.items, emptyGroups = me.emptyGroups, len, group, i; groups = Ext.Array.from(groups); for (i = 0, len = groups.length; i < len; i++) { group = groups[i]; group.eject(); emptyGroups[group.getGroupKey()] = group; } // Removed empty groups are reclaimable by getGroup for // emptyGroupRetainTime milliseconds me.checkRemoveQueue(); }, checkRemoveQueue: function() { var me = this, emptyGroups = me.emptyGroups, groupKey, group, reschedule; for (groupKey in emptyGroups) { group = emptyGroups[groupKey]; if (!group || group.destroyed) { delete emptyGroups[groupKey]; } // If the group's retain time has expired, destroy it. else if (!group.length && Ext.now() - group.ejectTime > me.emptyGroupRetainTime) { Ext.destroy(group); delete emptyGroups[groupKey]; } else { reschedule = true; } } // Still some to remove in the future. Check back in emptyGroupRetainTime if (reschedule) { Ext.undefer(me.checkRemoveQueueTimer); me.checkRemoveQueueTimer = Ext.defer(me.checkRemoveQueue, me.emptyGroupRetainTime, me); } } }});