Форк Rambox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

3668 lines
132 KiB

/**
* This class manages uniquely keyed objects such as {@link Ext.data.Model records} or
* {@link Ext.Component components}.
*
* ## Keys
*
* Unlike `Ext.util.MixedCollection` this class can only manage objects whose key can be
* extracted from the instance. That is, this class does not support "external" keys. This
* makes this class more efficient because it does not need to track keys in parallel with
* items. It also means key-to-item lookup will be optimal and never need to perform a
* linear search.
*
* ### Extra Keys
*
* In some cases items may need to be looked up by multiple property values. To enable this
* there is the `extraKeys` config.
*
* For example, to quickly look up items by their "name" property:
*
* var collection = new Ext.util.Collection({
* extraKeys: {
* byName: 'name' // based on "name" property of each item
* }
* });
*
* ## Ranges
*
* When methods accept index arguments to indicate a range of items, these are either an
* index and a number of items or a "begin" and "end" index.
*
* In the case of "begin" and "end", the "end" is the first item outside the range. This
* definition makes it simple to expression empty ranges because "length = end - begin".
*
* ### Negative Indices
*
* When an item index is provided, negative values are treated as offsets from the end of
* the collection. In other words the follow are equivalent:
*
* +---+---+---+---+---+---+
* | | | | | | |
* +---+---+---+---+---+---+
* 0 1 2 3 4 5
* -6 -5 -4 -3 -2 -1
*
* ## Legacy Classes
*
* The legacy classes `Ext.util.MixedCollection' and `Ext.util.AbstractMixedCollection`
* may be needed if external keys are required, but for all other situations this class
* should be used instead.
*/
Ext.define('Ext.util.Collection', {
mixins: [
'Ext.mixin.Observable'
],
requires: [
'Ext.util.CollectionKey',
'Ext.util.Filter',
'Ext.util.Sorter',
'Ext.util.Grouper'
],
uses: [
'Ext.util.SorterCollection',
'Ext.util.FilterCollection',
'Ext.util.GroupCollection'
],
/**
* @property {Boolean} isCollection
* `true` in this class to identify an object as an instantiated Collection, or subclass
* thereof.
* @readonly
*/
isCollection: true,
config: {
autoFilter: true,
autoSort: true,
/**
* @cfg {Boolean} autoGroup `true` to sort by the grouper
* @private
*/
autoGroup: true,
/**
* @cfg {Function} decoder
* A function that can convert newly added items to a proper type before being
* added to this collection.
*/
decoder: null,
/**
* @cfg {Object} extraKeys
* One or more `Ext.util.CollectionKey' configuration objects or key properties.
* Each property of the given object is the name of the `CollectionKey` instance
* that is stored on this collection. The value of each property configures the
* `CollectionKey` instance.
*
* var collection = new Ext.util.Collection({
* extraKeys: {
* byName: 'name' // based on "name" property of each item
* }
* });
*
* Or equivalently:
*
* var collection = new Ext.util.Collection({
* extraKeys: {
* byName: {
* property: 'name'
* }
* }
* });
*
* To provide a custom key extraction function instead:
*
* var collection = new Ext.util.Collection({
* extraKeys: {
* byName: {
* keyFn: function (item) {
* return item.name;
* }
* }
* }
* });
*
* Or to call a key getter method from each item:
*
* var collection = new Ext.util.Collection({
* extraKeys: {
* byName: {
* keyFn: 'getName'
* }
* }
* });
*
* To use the above:
*
* var item = collection.byName.get('somename');
*
* **NOTE** Either a `property` or `keyFn` must be be specified to define each
* key.
* @since 5.0.0
*/
extraKeys: null,
/**
* @cfg {Array/Ext.util.FilterCollection} filters
* The collection of {@link Ext.util.Filter Filters} for this collection. At the
* time a collection is created `filters` can be specified as a unit. After that
* time the normal `setFilters` method can also be given a set of replacement
* filters for the collection.
*
* Individual filters can be specified as an `Ext.util.Filter` instance, a config
* object for `Ext.util.Filter` or simply a function that will be wrapped in a
* instance with its {@Ext.util.Filter#filterFn filterFn} set.
*
* For fine grain control of the filters collection, call `getFilters` to return
* the `Ext.util.Collection` instance that holds this collection's filters.
*
* var collection = new Ext.util.Collection();
* var filters = collection.getFilters(); // an Ext.util.FilterCollection
*
* function legalAge (item) {
* return item.age >= 21;;
* }
*
* filters.add(legalAge);
*
* //...
*
* filters.remove(legalAge);
*
* Any changes to the `filters` collection will cause this collection to adjust
* its items accordingly (if `autoFilter` is `true`).
* @since 5.0.0
*/
filters: null,
/**
* @cfg {Object} grouper
* A configuration object for this collection's {@link Ext.util.Grouper grouper}.
*
* For example, to group items by the first letter of the last name:
*
* var collection = new Ext.util.Collection({
* grouper: {
* groupFn: function (item) {
* return item.lastName.substring(0, 1);
* }
* }
* });
*/
grouper: null,
/**
* @cfg {Ext.util.GroupCollection} groups
* The collection of to hold each group container. This collection is created and
* removed dynamically based on `grouper`. Application code should only need to
* call `getGroups` to retrieve the collection and not `setGroups`.
*/
groups: null,
/**
* @cfg {String} rootProperty
* The root property to use for aggregation, filtering and sorting. By default
* this is `null` but when containing things like {@link Ext.data.Model records}
* this config would likely be set to "data" so that property names are applied
* to the fields of each record.
*/
rootProperty: null,
/**
* @cfg {Array/Ext.util.SorterCollection} sorters
* Array of {@link Ext.util.Sorter sorters} for this collection. At the time a
* collection is created the `sorters` can be specified as a unit. After that time
* the normal `setSorters` method can be also be given a set of replacement
* sorters.
*
* Individual sorters can be specified as an `Ext.util.Sorter` instance, a config
* object for `Ext.util.Sorter` or simply the name of a property by which to sort.
*
* An alternative way to extend the sorters is to call the `sort` method and pass
* a property or sorter config to add to the sorters.
*
* For fine grain control of the sorters collection, call `getSorters` to return
* the `Ext.util.Collection` instance that holds this collection's sorters.
*
* var collection = new Ext.util.Collection();
* var sorters = collection.getSorters(); // an Ext.util.SorterCollection
*
* sorters.add('name');
*
* //...
*
* sorters.remove('name');
*
* Any changes to the `sorters` collection will cause this collection to adjust
* its items accordingly (if `autoSort` is `true`).
*
* @since 5.0.0
*/
sorters: null,
/**
* @cfg {Number} [multiSortLimit=3]
* The maximum number of sorters which may be applied to this Sortable when using
* the "multi" insertion position when adding sorters.
*
* New sorters added using the "multi" insertion position are inserted at the top
* of the sorters list becoming the new primary sort key.
*
* If the sorters collection has grown to longer then **`multiSortLimit`**, then
* the it is trimmed.
*/
multiSortLimit: 3,
/**
* cfg {String} defaultSortDirection
* The default sort direction to use if one is not specified.
*/
defaultSortDirection: 'ASC',
/**
* @cfg {Ext.util.Collection} source
* The base `Collection`. This collection contains the items to which filters
* are applied to populate this collection. In this configuration, only the
* root `source` collection can have items truly added or removed.
* @since 5.0.0
*/
source: null
},
/**
* @property {Number} generation
* Mutation counter which is incremented when the collection changes.
* @readonly
* @since 5.0.0
*/
generation: 0,
/**
* @property {Object} indices
* An object used as map to get the index of an item.
* @private
* @since 5.0.0
*/
indices: null,
/**
* @property {Number} indexRebuilds
* The number of times the `indices` have been rebuilt. This is for diagnostic use.
* @private
* @readonly
* @since 5.0.0
*/
indexRebuilds: 0,
/**
* @property {Number} updating
* A counter that is increased by `beginUpdate` and decreased by `endUpdate`. When
* this transitions from 0 to 1 the `{@link #event-beginupdate beginupdate}` event is
* fired. When it transitions back from 1 to 0 the `{@link #event-endupdate endupdate}`
* event is fired.
* @readonly
* @since 5.0.0
*/
updating: 0,
/**
* @property {Boolean} grouped
* A read-only flag indicating if this object is grouped.
* @readonly
*/
grouped: false,
/**
* @property {Boolean} sorted
* A read-only flag indicating if this object is sorted. This flag may not be correct
* during an update of the sorter collection but will be correct before `onSortChange`
* is called. This flag is `true` if `grouped` is `true` because the collection is at
* least sorted by the `grouper`.
* @readonly
*/
sorted: false,
/**
* @property {Boolean} filtered
* A read-only flag indicating if this object is filtered.
* @readonly
*/
filtered: false,
// private priority that is used for endupdate listeners on the filters and sorters.
// set to a very high priority so that our processing of these events takes place prior
// to user code - data must already be filtered/sorted when the user's handler runs
$endUpdatePriority: 1001,
/**
* @event add
* Fires after items have been added to the collection.
*
* All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
* `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
* events so it is best to do only the minimal amount of work in response to these
* events and move the more expensive side-effects to an `endupdate` listener.
*
* @param {Ext.util.Collection} collection The collection being modified.
*
* @param {Object} details An object describing the addition.
*
* @param {Number} details.at The index in the collection where the add occurred.
*
* @param {Object} details.atItem The item after which the new items were inserted or
* `null` if at the beginning of the collection.
*
* @param {Object[]} details.items The items that are now added to the collection.
*
* @param {Array} [details.keys] If available this array holds the keys (extracted by
* `getKey`) for each item in the `items` array.
*
* @param {Object} [details.next] If more `{@link #event-add add}` events are in queue
* to be delivered this is a reference to the `details` instance for the next
* `{@link #event-add add}` event. This will only be the case when the collection is
* sorted as the new items often need to be inserted at multiple locations to maintain
* the sort. In this case, all of the new items have already been added not just those
* described by the first `{@link #event-add add}` event.
*
* @param {Object} [details.replaced] If this addition has a corresponding set of
* `{@link #event-remove remove}` events this reference holds the `details` object for
* the first `remove` event. That `details` object may have a `next` property if there
* are multiple associated `remove` events.
*
* @since 5.0.0
*/
/**
* @event beginupdate
* Fired before changes are made to the collection. This event fires when the
* `beginUpdate` method is called and the counter it manages transitions from 0 to 1.
*
* All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
* `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
* events so it is best to do only the minimal amount of work in response to these
* events and move the more expensive side-effects to an `endupdate` listener.
*
* @param {Ext.util.Collection} collection The collection being modified.
*
* @since 5.0.0
*/
/**
* @event endupdate
* Fired after changes are made to the collection. This event fires when the `endUpdate`
* method is called and the counter it manages transitions from 1 to 0.
*
* All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
* `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
* events so it is best to do only the minimal amount of work in response to these
* events and move the more expensive side-effects to an `endupdate` listener.
*
* @param {Ext.util.Collection} collection The collection being modified.
*
* @since 5.0.0
*/
/**
* @event beforeitemchange
* This event fires before an item change is reflected in the collection. This event
* is always followed by an `itemchange` event and, depending on the change, possibly
* an `add`, `remove` and/or `updatekey` event.
*
* @param {Ext.util.Collection} collection The collection being modified.
*
* @param {Object} details An object describing the change.
*
* @param {Object} details.item The item that has changed.
*
* @param {String} details.key The key of the item that has changed.
*
* @param {Boolean} details.filterChanged This is `true` if the filter status of the
* `item` has changed. That is, the item was previously filtered out and is no longer
* or the opposite.
*
* @param {Boolean} details.keyChanged This is `true` if the item has changed keys. If
* so, check `oldKey` for the old key. An `updatekey` event will follow.
*
* @param {Boolean} details.indexChanged This is `true` if the item needs to move to
* a new index in the collection due to sorting. The index can be seen in `index`.
* The old index is in `oldIndex`.
*
* @param {String[]} [details.modified] If known this property holds the array of names
* of the modified properties of the item.
*
* @param {Boolean} [details.filtered] This value is `true` if the item will be filtered
* out of the collection.
*
* @param {Number} [details.index] The new index in the collection for the item if
* the item is being moved (see `indexChanged`). If the item is being removed due to
* filtering, this will be -1.
*
* @param {Number} [details.oldIndex] The old index in the collection for the item if
* the item is being moved (see `indexChanged`). If the item was being removed due to
* filtering, this will be -1.
*
* @param {Object} [details.oldKey] The old key for the `item` if the item's key has
* changed (see `keyChanged`).
*
* @param {Boolean} [details.wasFiltered] This value is `true` if the item was filtered
* out of the collection.
*
* @since 5.0.0
*/
/**
* @event itemchange
* This event fires after an item change is reflected in the collection. This event
* always follows a `beforeitemchange` event and its corresponding `add`, `remove`
* and/or `updatekey` events.
*
* @param {Ext.util.Collection} collection The collection being modified.
*
* @param {Object} details An object describing the change.
*
* @param {Object} details.item The item that has changed.
*
* @param {String} details.key The key of the item that has changed.
*
* @param {Boolean} details.filterChanged This is `true` if the filter status of the
* `item` has changed. That is, the item was previously filtered out and is no longer
* or the opposite.
*
* @param {Object} details.keyChanged This is `true` if the item has changed keys. If
* so, check `oldKey` for the old key. An `updatekey` event will have been sent.
*
* @param {Boolean} details.indexChanged This is `true` if the item was moved to a
* new index in the collection due to sorting. The index can be seen in `index`.
* The old index is in `oldIndex`.
*
* @param {String[]} [details.modified] If known this property holds the array of names
* of the modified properties of the item.
*
* @param {Boolean} [details.filtered] This value is `true` if the item is filtered
* out of the collection.
*
* @param {Number} [details.index] The new index in the collection for the item if
* the item has been moved (see `indexChanged`). If the item is removed due to
* filtering, this will be -1.
*
* @param {Number} [details.oldIndex] The old index in the collection for the item if
* the item has been moved (see `indexChanged`). If the item was being removed due to
* filtering, this will be -1.
*
* @param {Object} [details.oldKey] The old key for the `item` if the item's key has
* changed (see `keyChanged`).
*
* @param {Boolean} [details.wasFiltered] This value is `true` if the item was filtered
* out of the collection.
*
* @since 5.0.0
*/
/**
* @event refresh
* This event fires when the collection has changed entirely. This event is fired in
* cases where the collection's filter is updated or the items are sorted. While the
* items previously in the collection may remain the same, the order at a minimum has
* changed in ways that cannot be simply translated to other events.
*
* @param {Ext.util.Collection} collection The collection being modified.
*/
/**
* @event remove
* Fires after items have been removed from the collection. Some properties of this
* object may not be present if calculating them is deemed too expensive. These are
* marked as "optional".
*
* All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
* `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
* events so it is best to do only the minimal amount of work in response to these
* events and move the more expensive side-effects to an `endupdate` listener.
*
* @param {Ext.util.Collection} collection The collection being modified.
*
* @param {Object} details An object describing the removal.
*
* @param {Number} details.at The index in the collection where the removal occurred.
*
* @param {Object[]} details.items The items that are now removed from the collection.
*
* @param {Array} [details.keys] If available this array holds the keys (extracted by
* `getKey`) for each item in the `items` array.
*
* @param {Object} [details.map] If available this is a map keyed by the key of each
* item in the `items` array. This will often contain all of the items being removed
* and not just the items in the range described by this event. The value held in this
* map is the item.
*
* @param {Object} [details.next] If more `{@link #event-remove remove}` events are in
* queue to be delivered this is a reference to the `details` instance for the next
* remove event.
*
* @param {Object} [details.replacement] If this removal has a corresponding
* `{@link #event-add add}` taking place this reference holds the `details` object for
* that `add` event. If the collection is sorted, the new items are pre-sorted but the
* `at` property for the `replacement` will **not** be correct. The new items will be
* added in one or more chunks at their proper index.
*
* @since 5.0.0
*/
/**
* @event sort
* This event fires after the contents of the collection have been sorted.
*
* @param {Ext.util.Collection} collection The collection being sorted.
*/
/**
* @event beforesort
* @private
* This event fires before the contents of the collection have been sorted.
*
* @param {Ext.util.Collection} collection The collection being sorted.
* @param {Ext.util.Sorter[]} sorters Array of sorters applied to the Collection.
*/
/**
* @event updatekey
* Fires after the key for an item has changed.
*
* @param {Ext.util.Collection} collection The collection being modified.
*
* @param {Object} details An object describing the update.
*
* @param {Object} details.item The item whose key has changed.
*
* @param {Object} details.newKey The new key for the `item`.
*
* @param {Object} details.oldKey The old key for the `item`.
*
* @since 5.0.0
*/
constructor: function (config) {
var me = this;
/**
* @property {Object[]} items
* An array containing the items.
* @private
* @since 5.0.0
*/
me.items = [];
/**
* @property {Object} map
* An object used as a map to find items based on their key.
* @private
* @since 5.0.0
*/
me.map = {};
/**
* @property {Number} length
* The count of items in the collection.
* @readonly
* @since 5.0.0
*/
me.length = 0;
/**
* @cfg {Function} [keyFn]
* A function to retrieve the key of an item in the collection. If provided,
* this replaces the default `getKey` method. The default `getKey` method handles
* items that have either an "id" or "_id" property or failing that a `getId`
* method to call.
* @since 5.0.0
*/
if (config && config.keyFn) {
me.getKey = config.keyFn;
}
me.mixins.observable.constructor.call(me, config);
},
/**
* Destroys this collection. This is only necessary if this collection uses a `source`
* collection as that relationship will keep a reference from the `source` to this
* collection and potentially leak memory.
* @since 5.0.0
*/
destroy: function () {
var me = this,
filters = me._filters,
sorters = me._sorters,
groups = me._groups;
if (filters) {
filters.destroy();
me._filters = null;
}
if (sorters) {
sorters.destroy();
me._sorters = null;
}
if (groups) {
groups.destroy();
me._groups = null;
}
me.setSource(null);
me.observers = me.items = me.map = null;
},
/**
* Adds an item to the collection. If the item already exists or an item with the
* same key exists, the old item will be removed and the new item will be added to
* the end.
*
* This method also accepts an array of items or simply multiple items as individual
* arguments. The following 3 code sequences have the same end result:
*
* // Call add() once per item (not optimal - best avoided):
* collection.add(itemA);
* collection.add(itemB);
* collection.add(itemC);
* collection.add(itemD);
*
* // Call add() with each item as an argument:
* collection.add(itemA, itemB, itemC, itemD);
*
* // Call add() with the items as an array:
* collection.add([ itemA, itemB, itemC, itemD ]);
*
* The first form should be avoided where possible because the collection and all
* parties "watching" it will be updated 4 times.
*
* @param {Object/Object[]} item The item or items to add.
* @return {Object/Object[]} The item or items added.
* @since 5.0.0
*/
add: function (item) {
var me = this,
items = me.decodeItems(arguments, 0),
ret = items;
if (items.length) {
me.requestedIndex = me.length;
me.splice(me.length, 0, items);
delete me.requestedIndex;
ret = (items.length === 1) ? items[0] : items;
}
return ret;
},
/**
* Adds an item to the collection while removing any existing items. Similar to {@link #method-add}.
* @param {Object/Object[]} item The item or items to add.
* @return {Object/Object[]} The item or items added.
* @since 5.0.0
*/
replaceAll: function() {
var me = this,
len = me.length,
ret, items;
if (len === 0) {
return me.add.apply(me, arguments);
}
items = me.decodeItems(arguments, 0);
ret = items;
if (items.length) {
me.splice(0, me.length, items);
ret = (items.length === 1) ? items[0] : items;
} else {
me.removeAll();
}
return ret;
},
/**
* Returns the result of the specified aggregation operation against all items in this
* collection.
*
* This method is not typically called directly because there are convenience methods
* for each of the supported `operation` values. These are:
*
* * **average** - Returns the average value.
* * **bounds** - Returns an array of `[min, max]`.
* * **max** - Returns the maximum value or `undefined` if empty.
* * **min** - Returns the minimum value or `undefined` if empty.
* * **sum** - Returns the sum of all values.
*
* For example:
*
* result = collection.aggregate('age', 'sum');
*
* result = collection.aggregate('age', 'sum', 2, 10); // the 8 items at index 2
*
* To provide a custom operation function:
*
* function averageAgeOfMinors (items, values) {
* var sum = 0,
* count = 0;
*
* for (var i = 0; i < values.length; ++i) {
* if (values[i] < 18) {
* sum += values[i];
* ++count;
* }
* }
*
* return count ? sum / count : 0;
* }
*
* result = collection.aggregate('age', averageAgeOfMinors);
*
* @param {String} property The name of the property to aggregate from each item.
* @param {String/Function} operation The operation to perform.
* @param {Array} operation.items The items on which the `operation` function is to
* operate.
* @param {Array} operation.values The values on which the `operation` function is to
* operate.
* @param {Number} [begin] The index of the first item in `items` to include in the
* aggregation.
* @param {Number} [end] The index at which to stop aggregating `items`. The item at
* this index will *not* be included in the aggregation.
* @param {Object} [scope] The `this` pointer to use if `operation` is a function.
* Defaults to this collection.
* @return {Object}
*/
aggregate: function (property, operation, begin, end, scope) {
var me = this,
args = Ext.Array.slice(arguments);
args.unshift(me.items);
return me.aggregateItems.apply(me, args);
},
/**
* See {@link #aggregate}. The functionality is the same, however the aggregates are
* provided per group. Assumes this collection has an active {@link #grouper}.
*
* @param {String} property The name of the property to aggregate from each item.
* @param {String/Function} operation The operation to perform.
* @param {Array} operation.items The items on which the `operation` function is to
* operate.
* @param {Array} operation.values The values on which the `operation` function is to
* operate.
* @param {Object} [scope] The `this` pointer to use if `operation` is a function.
* Defaults to this collection.
* @return {Object}
*/
aggregateByGroup: function(property, operation, scope) {
var groups = this.getGroups();
return this.aggregateGroups(groups, property, operation, scope);
},
/**
* Returns the result of the specified aggregation operation against the given items.
* For details see `aggregate`.
*
* @param {Array} items The items to aggregate.
* @param {String} property The name of the property to aggregate from each item.
* @param {String/Function} operation The operation to perform.
* @param {Array} operation.items The items on which the `operation` function is to
* operate.
* @param {Array} operation.values The values on which the `operation` function is to
* operate.
* @param {Number} [begin] The index of the first item in `items` to include in the
* aggregation.
* @param {Number} [end] The index at which to stop aggregating `items`. The item at
* this index will *not* be included in the aggregation.
* @param {Object} [scope] The `this` pointer to use if `operation` is a function.
* Defaults to this collection.
*
* @private
* @return {Object}
*/
aggregateItems: function (items, property, operation, begin, end, scope) {
var me = this,
range = Ext.Number.clipIndices(items.length, [ begin, end ]),
// Only extract items into new array if a subset is required
subsetRequested = (begin !== 0 && end !== items.length),
i, j,
rangeLen,
root, value,
values, valueItems;
begin = range[0];
end = range[1];
if (!Ext.isFunction(operation)) {
operation = me._aggregators[operation];
return operation.call(me, items, begin, end, property, me.getRootProperty());
}
root = me.getRootProperty();
// Preallocate values array with known set size.
// valueItems can be just the items array is a subset has not been requested
values = new Array(rangeLen);
valueItems = subsetRequested ? new Array(rangeLen) : items;
// Collect the extracted property values and the items for passing to the operation.
for (i = begin, j = 0; i < end; ++i, j++) {
if (subsetRequested) {
valueItems[j] = value = items[i];
}
values[j] = (root ? value[root] : value)[property];
}
return operation.call(scope || me, items, values, 0);
},
/**
* Aggregates a set of groups.
* @param {Ext.util.GroupCollection} groups The groups
* @param {String} property The name of the property to aggregate from each item.
* @param {String/Function} operation The operation to perform.
* @param {Array} operation.values The values on which the `operation` function is to
* operate.
* @param {Array} operation.items The items on which the `operation` function is to
* operate.
* @param {Number} operation.index The index in `items` at which the `operation`
* function is to start. The `values.length` indicates the number of items involved.
* @param {Object} [scope] The `this` pointer to use if `operation` is a function.
* Defaults to this collection.
*
* @return {Object}
* @private
*/
aggregateGroups: function(groups, property, operation, scope) {
var items = groups.items,
len = items.length,
callDirect = !Ext.isFunction(operation),
out = {},
i, group, result;
for (i = 0; i < len; ++i) {
group = items[i];
if (!callDirect) {
result = this.aggregateItems(group.items, property, operation, null, null, scope);
} else {
result = group[operation](property);
}
out[group.getGroupKey()] = result;
}
return out;
},
/**
* This method is called to indicate the start of multiple changes to the collection.
* Application code should seldom need to call this method as it is called internally
* when needed. If multiple collection changes are needed, consider wrapping them in
* an `update` call rather than calling `beginUpdate` directly.
*
* Internally this method increments a counter that is decremented by `endUpdate`. It
* is important, therefore, that if you call `beginUpdate` directly you match that
* call with a call to `endUpdate` or you will prevent the collection from updating
* properly.
*
* For example:
*
* var collection = new Ext.util.Collection();
*
* collection.beginUpdate();
*
* collection.add(item);
* // ...
*
* collection.insert(index, otherItem);
* //...
*
* collection.endUpdate();
*
* @since 5.0.0
*/
beginUpdate: function () {
if (!this.updating++) { // jshint ignore:line
this.notify('beginupdate');
}
},
/**
* Removes all items from the collection. This is similar to `removeAll` except that
* `removeAll` fire events to inform listeners. This means that this method should be
* called only when you are sure there are no listeners.
* @since 5.0.0
*/
clear: function () {
var me = this,
generation = me.generation,
ret = generation ? me.items : [],
extraKeys,
indexName;
if (generation) {
me.items = [];
me.length = 0;
me.map = {};
me.indices = {};
me.generation++;
// Clear any extraKey indices associated with this Collection
extraKeys = me.getExtraKeys();
if (extraKeys) {
for (indexName in extraKeys) {
extraKeys[indexName].clear();
}
}
}
return ret;
},
/**
* Creates a shallow copy of this collection
* @return {Ext.util.Collection}
* @since 5.0.0
*/
clone: function () {
var me = this,
copy = new me.self(me.initialConfig);
copy.add(me.items);
return copy;
},
/**
* Collects unique values of a particular property in this Collection.
* @param {String} property The property to collect on
* @param {String} root (optional) 'root' property to extract the first argument from. This is used mainly when
* summing fields in records, where the fields are all stored inside the 'data' object
* @param {Boolean} [allowNull] Pass `true` to include `null`, `undefined` or empty
* string values.
* @return {Array} The unique values
* @since 5.0.0
*/
collect: function (property, root, allowNull) {
var items = this.items,
length = items.length,
map = {},
ret = [],
i, strValue, value;
for (i = 0; i < length; ++i) {
value = items[i];
value = (root ? value[root] : value)[property];
strValue = String(value);
if ((allowNull || !Ext.isEmpty(value)) && !map[strValue]) {
map[strValue] = 1;
ret.push(value);
}
}
return ret;
},
/**
* Returns true if the collection contains the passed Object as an item.
* @param {Object} item The Object to look for in the collection.
* @return {Boolean} `true` if the collection contains the Object as an item.
* @since 5.0.0
*/
contains: function (item) {
var ret = false,
key;
if (item != null) {
key = this.getKey(item);
ret = this.map[key] === item;
}
return ret;
},
/**
* Returns true if the collection contains the passed Object as a key.
* @param {String} key The key to look for in the collection.
* @return {Boolean} True if the collection contains the Object as a key.
* @since 5.0.0
*/
containsKey: function (key) {
return key in this.map;
},
/**
* Creates a new collection that is a filtered subset of this collection. The filter
* passed can be a function, a simple property name and value, an `Ext.util.Filter`
* instance, an array of `Ext.util.Filter` instances.
*
* If the passed filter is a function the second argument is its "scope" (or "this"
* pointer). The function should return `true` given each item in the collection if
* that item should be included in the filtered collection.
*
* var people = new Ext.util.Collection();
*
* people.add([
* { id: 1, age: 25, name: 'Ed' },
* { id: 2, age: 24, name: 'Tommy' },
* { id: 3, age: 24, name: 'Arne' },
* { id: 4, age: 26, name: 'Aaron' }
* ]);
*
* // Create a collection of people who are older than 24:
* var oldPeople = people.createFiltered(function (item) {
* return item.age > 24;
* });
*
* If the passed filter is a `Ext.util.Filter` instance or array of `Ext.util.Filter`
* instances the filter(s) are used to produce the filtered collection and there are
* no further arguments.
*
* If the passed filter is a string it is understood as the name of the property by
* which to filter. The second argument is the "value" used to compare each item's
* property value. This comparison can be further tuned with the `anyMatch` and
* `caseSensitive` (optional) arguments.
*
* // Create a new Collection containing only the items where age == 24
* var middleAged = people.createFiltered('age', 24);
*
* Alternatively you can apply `filters` to this Collection by calling `setFilters`
* or modifying the filter collection returned by `getFilters`.
*
* @param {Ext.util.Filter[]/String/Function} property A property on your objects, an
* array of {@link Ext.util.Filter Filter} objects or a filter function.
*
* @param {Object} value If `property` is a function, this argument is the "scope"
* (or "this" pointer) for the function. Otherwise this is either a `RegExp` to test
* property values or the value with which to compare.
*
* @param {Boolean} [anyMatch=false] True to match any part of the string, not just
* the beginning.
*
* @param {Boolean} [caseSensitive=false] True for case sensitive comparison.
*
* @param {Boolean} [exactMatch=false] `true` to force exact match (^ and $ characters added to the regex).
*
* @return {Ext.util.Collection} The new, filtered collection.
*
* @since 5.0.0
*/
createFiltered: function (property, value, anyMatch, caseSensitive, exactMatch) {
var me = this,
ret = new me.self(me.initialConfig),
root = me.getRootProperty(),
items = me.items,
length, i, filters, fn, scope;
if (Ext.isFunction(property)) {
fn = property;
scope = value;
} else {
//support for the simple case of filtering by property/value
if (Ext.isString(property)) {
filters = [
new Ext.util.Filter({
property : property,
value : value,
root : root,
anyMatch : anyMatch,
caseSensitive: caseSensitive,
exactMatch : exactMatch
})
];
} else if (property instanceof Ext.util.Filter) {
filters = [ property ];
property.setRoot(root);
} else if (Ext.isArray(property)) {
filters = property.slice(0);
for (i = 0, length = filters.length; i < length; ++i) {
filters[i].setRoot(root);
}
}
// At this point we have an array of zero or more Ext.util.Filter objects to
// filter with, so here we construct a function that combines these filters by
// ANDing them together and filter by that.
fn = Ext.util.Filter.createFilterFn(filters);
}
scope = scope || me;
for (i = 0, length = items.length; i < length; i++) {
if (fn.call(scope, items[i])) {
ret.add(items[i]);
}
}
return ret;
},
/**
* Filter by a function. Returns a <i>new</i> collection that has been filtered.
* The passed function will be called with each object in the collection.
* If the function returns true, the value is included otherwise it is filtered.
* @param {Function} fn The function to be called.
* @param {Mixed} fn.item The collection item.
* @param {String} fn.key The key of collection item.
* @param {Object} scope (optional) The scope (<code>this</code> reference) in
* which the function is executed. Defaults to this Collection.
* @return {Ext.util.Collection} The new filtered collection
* @since 5.0.0
* @deprecated
*/
filterBy: function(fn, scope) {
return this.createFiltered(fn, scope);
},
/**
* Executes the specified function once for every item in the collection. If the value
* returned by `fn` is `false` the iteration stops. In all cases, the last value that
* `fn` returns is returned by this method.
*
* @param {Function} fn The function to execute for each item.
* @param {Object} fn.item The collection item.
* @param {Number} fn.index The index of item.
* @param {Number} fn.len Total length of collection.
* @param {Object} [scope=this] The scope (`this` reference) in which the function
* is executed. Defaults to this collection.
* @since 5.0.0
*/
each: function (fn, scope) {
var items = this.items,
len = items.length,
i, ret;
if (len) {
scope = scope || this;
items = items.slice(0); // safe for re-entrant calls
for (i = 0; i < len; i++) {
ret = fn.call(scope, items[i], i, len);
if (ret === false) {
break;
}
}
}
return ret;
},
/**
* Executes the specified function once for every key in the collection, passing each
* key, and its associated item as the first two parameters. If the value returned by
* `fn` is `false` the iteration stops. In all cases, the last value that `fn` returns
* is returned by this method.
*
* @param {Function} fn The function to execute for each item.
* @param {String} fn.key The key of collection item.
* @param {Object} fn.item The collection item.
* @param {Number} fn.index The index of item.
* @param {Number} fn.len Total length of collection.
* @param {Object} [scope=this] The scope (`this` reference) in which the function
* is executed. Defaults to this collection.
* @since 5.0.0
*/
eachKey: function (fn, scope) {
var me = this,
items = me.items,
len = items.length,
i, item, key, ret;
if (len) {
scope = scope || me;
items = items.slice(0); // safe for re-entrant calls
for (i = 0; i < len; i++) {
key = me.getKey(item = items[i]);
ret = fn.call(scope, key, item, i, len);
if (ret === false) {
break;
}
}
}
return ret;
},
/**
* This method is called after modifications are complete on a collection. For details
* see `beginUpdate`.
* @since 5.0.0
*/
endUpdate: function () {
if (! --this.updating) {
this.notify('endupdate');
}
},
/**
* Finds the first matching object in this collection by a specific property/value.
*
* @param {String} property The name of a property on your objects.
* @param {String/RegExp} value A string that the property values
* should start with or a RegExp to test against the property.
* @param {Number} [start=0] The index to start searching at.
* @param {Boolean} [startsWith=true] Pass `false` to allow a match start anywhere in
* the string. By default the `value` will match only at the start of the string.
* @param {Boolean} [endsWith=true] Pass `false` to allow the match to end before the
* end of the string. By default the `value` will match only at the end of the string.
* @param {Boolean} [ignoreCase=true] Pass `false` to make the `RegExp` case
* sensitive (removes the 'i' flag).
* @return {Object} The first item in the collection which matches the criteria or
* `null` if none was found.
* @since 5.0.0
*/
find: function (property, value, start, startsWith, endsWith, ignoreCase) {
if (Ext.isEmpty(value, false)) {
return null;
}
var regex = Ext.String.createRegex(value, startsWith, endsWith, ignoreCase),
root = this.getRootProperty();
return this.findBy(function (item) {
return item && regex.test((root ? item[root] : item)[property]);
}, null, start);
},
/**
* Returns the first item in the collection which elicits a true return value from the
* passed selection function.
* @param {Function} fn The selection function to execute for each item.
* @param {Object} fn.item The collection item.
* @param {String} fn.key The key of collection item.
* @param {Object} [scope=this] The scope (`this` reference) in which the function
* is executed. Defaults to this collection.
* @param {Number} [start=0] The index at which to start searching.
* @return {Object} The first item in the collection which returned true from the selection
* function, or null if none was found.
* @since 5.0.0
*/
findBy: function (fn, scope, start) {
var me = this,
items = me.items,
len = items.length,
i, item, key;
scope = scope || me;
for (i = start || 0; i < len; i++) {
key = me.getKey(item = items[i]);
if (fn.call(scope, item, key)) {
return items[i];
}
}
return null;
},
/**
* Finds the index of the first matching object in this collection by a specific
* property/value.
*
* @param {String} property The name of a property on your objects.
* @param {String/RegExp} value A string that the property values
* should start with or a RegExp to test against the property.
* @param {Number} [start=0] The index to start searching at.
* @param {Boolean} [startsWith=true] Pass `false` to allow a match start anywhere in
* the string. By default the `value` will match only at the start of the string.
* @param {Boolean} [endsWith=true] Pass `false` to allow the match to end before the
* end of the string. By default the `value` will match only at the end of the string.
* @param {Boolean} [ignoreCase=true] Pass `false` to make the `RegExp` case
* sensitive (removes the 'i' flag).
* @return {Number} The matched index or -1 if not found.
* @since 5.0.0
*/
findIndex: function (property, value, start, startsWith, endsWith, ignoreCase) {
var item = this.find(property, value, start, startsWith, endsWith, ignoreCase);
return item ? this.indexOf(item) : -1;
},
/**
* Find the index of the first matching object in this collection by a function.
* If the function returns `true` it is considered a match.
* @param {Function} fn The function to be called.
* @param {Object} fn.item The collection item.
* @param {String} fn.key The key of collection item.
* @param {Object} [scope=this] The scope (`this` reference) in which the function
* is executed. Defaults to this collection.
* @param {Number} [start=0] The index at which to start searching.
* @return {Number} The matched index or -1
* @since 5.0.0
*/
findIndexBy: function (fn, scope, start) {
var item = this.findBy(fn, scope, start);
return item ? this.indexOf(item) : -1;
},
/**
* Returns the first item in the collection.
* @param {Boolean} [grouped] `true` to extract the first item in each group. Only applies if
* a {@link #grouper} is active in the collection.
* @return {Object} The first item in the collection. If the grouped parameter is passed,
* see {@link #aggregateByGroup} for information on the return type.
* @since 5.0.0
*/
first: function (grouped) {
var groups = grouped ? this.getGroups() : undefined;
return groups ? this.aggregateGroups(groups, null, 'first') : this.items[0];
},
/**
* Returns the last item in the collection.
* @param {Boolean} [grouped] `true` to extract the first item in each group. Only applies if
* a {@link #grouper} is active in the collection.
* @return {Object} The last item in the collection. If the grouped parameter is passed,
* see {@link #aggregateByGroup} for information on the return type.
* @since 5.0.0
*/
last: function (grouped) {
var groups = grouped ? this.getGroups() : undefined;
return groups ? this.aggregateGroups(groups, null, 'last') : this.items[this.length - 1];
},
/**
* Returns the item associated with the passed key.
* @param {String/Number} key The key of the item.
* @return {Object} The item associated with the passed key.
* @since 5.0.0
*/
get: function (key) {
return this.map[key];
},
/**
* Returns the item at the specified index.
* @param {Number} index The index of the item.
* @return {Object} The item at the specified index.
* @since 5.0.0
*/
getAt: function (index) {
return this.items[index];
},
/**
* Returns the item associated with the passed key.
* @param {String/Number} key The key of the item.
* @return {Object} The item associated with the passed key.
* @since 5.0.0
*/
getByKey: function (key) {
return this.map[key];
},
/**
* Returns the number of items in the collection.
* @return {Number} the number of items in the collection.
* @since 5.0.0
*/
getCount: function () {
return this.length;
},
/**
* A function which will be called, passing an object belonging to this collection.
* The function should return the key by which that object will be indexed. This key
* must be unique to this item as only one item with this key will be retained.
*
* The default implementation looks basically like this (give or take special case
* handling of 0):
*
* function getKey (item) {
* return item.id || item._id || item.getId();
* }
*
* You can provide your own implementation by passing the `keyFn` config.
*
* For example, to hold items that have a unique "name" property:
*
* var elementCollection = new Ext.util.Collection({
* keyFn: function (item) {
* return item.name;
* }
* });
*
* The collection can have `extraKeys` if items need to be quickly looked up by other
* (potentially non-unique) properties.
*
* @param {Object} item The item.
* @return {Object} The key for the passed item.
* @since 5.0.0
*/
getKey: function (item) {
var id = item.id;
return (id === 0 || id) ? id :
((id = item._id) === 0 || id) ? id : item.getId();
},
/**
* Returns a range of items in this collection
* @param {Number} [begin=0] The index of the first item to get.
* @param {Number} [end] The ending index. The item at this index is *not* included.
* @return {Array} An array of items
* @since 5.0.0
*/
getRange: function (begin, end) {
var items = this.items,
length = items.length,
range;
//<debug>
if (begin > end) {
Ext.Error.raise('Inverted range passed to Collection.getRange: [' + begin +
',' + end + ']');
}
//</debug>
if (!length) {
range = [];
} else {
range = Ext.Number.clipIndices(length, [begin, end]);
range = items.slice(range[0], range[1]);
}
return range;
},
/**
* @method getSource
* Returns all unfiltered items in the Collection when the Collection has been
* filtered. Returns `null` when the Collection is not filtered.
* @return {Ext.util.Collection} items All unfiltered items (or `null` when the
* Collection is not filtered)
*/
/**
* Returns an array of values for the specified (sub) property.
*
* For example, to get an array of "name" properties from a collection of records (of
* `Ext.data.Model` objects):
*
* var names = collection.getValues('name', 'data');
*
* @param {String} property The property to collect on
* @param {String} [root] 'root' property to extract the first argument from. This is
* used mainly when operating on fields in records, where the fields are all stored
* inside the 'data' object.
* @return {Array} The values.
* @param {Number} [start=0] The index of the first item to include.
* @param {Number} [end] The index at which to stop getting values. The value of this
* item is *not* included.
* @return {Object[]} The array of values.
* @since 5.0.0
*/
getValues: function (property, root, start, end) {
var items = this.items,
range = Ext.Number.clipIndices(items.length, [start, end]),
ret = [],
i, value;
for (i = range[0], end = range[1]; i < end; ++i) {
value = items[i];
value = (root ? value[root] : value)[property];
ret.push(value);
}
return ret;
},
/**
* Returns index within the collection of the passed Object.
* @param {Object} item The item to find.
* @return {Number} The index of the item or -1 if not found.
* @since 5.0.0
*/
indexOf: function (item) {
if (!item) {
return -1;
}
var key = this.getKey(item);
return this.indexOfKey(key);
},
/**
* Returns index within the collection of the passed key.
* @param {Object} key The key to find.
* @return {Number} The index of the item or -1 if not found.
* @since 5.0.0
*/
indexOfKey: function (key) {
var me = this,
indices = me.indices;
if (key in me.map) {
if (!indices) {
indices = me.getIndices();
}
return indices[key];
}
return -1;
},
/**
* Inserts one or more items to the collection. The `index` value is the position at
* which the first item will be placed. The items starting at that position will be
* shifted to make room.
*
* @param {Number} index The index at which to insert the item(s).
* @param {Object/Object[]} item The item or items to add.
* @return {Object/Object[]} The item or items added.
* @since 5.0.0
*/
insert: function (index, item) {
var me = this,
items = me.decodeItems(arguments, 1),
ret = items;
if (items.length) {
me.requestedIndex = index;
me.splice(index, 0, items);
delete me.requestedIndex;
ret = (items.length === 1) ? items[0] : items;
}
return ret;
},
/**
* This method should be called when an item in this collection has been modified. If
* the collection is sorted or filtered the result of modifying an item needs to be
* reflected in the collection. If the item's key is also being modified, it is best
* to pass the `oldKey` to this same call rather than call `updateKey` separately.
*
* @param {Object} item The item that was modified.
* @param {String[]} [modified] The names of the modified properties of the item.
* @param {String/Number} [oldKey] Passed if the item's key was also modified.
* @since 5.0.0
*/
itemChanged: function (item, modified, oldKey, /* private */ meta) {
var me = this,
keyChanged = oldKey === 0 || !!oldKey,
filtered = me.filtered && me.getAutoFilter(),
filterChanged = false,
itemMovement = 0,
items = me.items,
last = me.length - 1,
sorted = me.sorted && last > 0 && me.getAutoSort(), // one or zero items is not really sorted
// CAN be called on an empty Collection
// A TreeStore can call afterEdit on a hidden root before
// any child nodes exist in the store.
source = me.getSource(),
toAdd,
toRemove = 0,
index,
itemFiltered = false,
newIndex,
wasFiltered = false,
details, newKey, sortFn;
// We are owned, we cannot react, inform owning collection.
if (source && !source.updating) {
source.itemChanged(item, modified, oldKey, meta);
}
// Root Collection has been informed.
// Change is propagating downward from root.
else {
newKey = me.getKey(item);
if (filtered) {
index = me.indexOfKey(keyChanged ? oldKey : newKey);
wasFiltered = (index < 0);
itemFiltered = me.isItemFiltered(item);
filterChanged = (wasFiltered !== itemFiltered);
}
if (filterChanged) {
if (itemFiltered) {
toRemove = [ item ];
newIndex = -1;
} else {
toAdd = [ item ];
newIndex = me.length; // this will be ignored if sorted
}
}
// If sorted, the newIndex must be reported correctly in the beforeitemchange and itemchange events.
// Even though splice ignores the parameter and calculates the insertion point
else if (sorted && !itemFiltered) {
// If we are sorted and there are 2 or more items make sure this item is at
// the proper index.
if (!filtered) {
// If the filter has not changed we may need to move the item but if
// there is a filter we have already determined its index.
index = me.indexOfKey(keyChanged ? oldKey : newKey);
}
sortFn = me.getSortFn();
if (index && sortFn(items[index - 1], items[index]) > 0) {
// If this item is not the first and the item before it compares as
// greater-than then item needs to move left since it is less-than
// item[index - 1].
itemMovement = -1;
// We have to bound the binarySearch or else the presence of the
// out-of-order "item" would break it.
newIndex = Ext.Array.binarySearch(items, item, 0, index, sortFn);
}
else if (index < last && sortFn(items[index], items[index + 1]) > 0) {
// If this item is not the last and the item after it compares as
// less-than then item needs to move right since it is greater-than
// item[index + 1].
itemMovement = 1;
// We have to bound the binarySearch or else the presence of the
// out-of-order "item" would break it.
newIndex = Ext.Array.binarySearch(items, item, index + 1, sortFn);
}
if (itemMovement) {
toAdd = [ item ];
}
}
// One may be tempted to avoid this notification when none of our three vars
// are true, *but* the problem with that is that these three changes we care
// about are only what this collection cares about. Child collections or
// outside parties still need to know that the item has changed in some way.
// We do NOT adjust the newIndex reported here to allow for position *after* the item has been removed
// We report the "visual" position at which the item would be inserted as if it were new.
details = {
item: item,
key: newKey,
index: newIndex,
filterChanged: filterChanged,
keyChanged: keyChanged,
indexChanged: !!itemMovement,
filtered: itemFiltered,
oldIndex: index,
newIndex: newIndex,
wasFiltered: wasFiltered,
meta: meta
};
if (keyChanged) {
details.oldKey = oldKey;
}
if (modified) {
details.modified = modified;
}
me.beginUpdate();
me.notify('beforeitemchange', [details]);
if (keyChanged) {
me.updateKey(item, oldKey);
}
if (toAdd || toRemove) {
// In sorted mode (which is the only time we get here), newIndex is
// correct but *ignored* by splice since it has to assume that *insert*
// index values need to be determined internally. In other words, the
// first argument here is both the remove and insert index but in sorted
// mode the insert index is calculated by splice.
me.splice(newIndex, toRemove, toAdd);
}
// Ensure that the newIndex always refers to the item the insertion is *before*.
// Ensure that the oldIndex always refers to the item the insertion was *before*.
//
// Before change to "c" to "h": | Before change "i" to "d":
// |
// +---+---+---+---+---+---+ | +---+---+---+---+---+---+
// | a | c | e | g | i | k | | | a | c | e | g | i | k |
// +---+---+---+---+---+---+ | +---+---+---+---+---+---+
// 0 1 2 3 4 5 | 0 1 2 3 4 5
// ^ ^ | ^ ^
// | | | | |
// oldIndex newIndex | newIndex oldIndex
// |
// After change to "c" to "h": | After change "i" to "d":
// |
// +---+---+---+---+---+---+ | +---+---+---+---+---+---+
// | a | e | g | h | i | k | | | a | c | d | e | g | k |
// +---+---+---+---+---+---+ | +---+---+---+---+---+---+
// 0 1 2 3 4 5 | 0 1 2 3 4 5
// ^ ^ | ^ ^
// | | | | |
// oldIndex newIndex | newIndex oldIndex
//
if (itemMovement > 0) {
details.newIndex--;
} else if (itemMovement < 0) {
details.oldIndex++;
}
// Divergence depending on whether the record if filtered out at this level in a chaining hierarchy.
// Child collections of this collection will not care about filtereditemchange because the record is not in them.
// Stores however will still need to know because the record *is* in them, just filtered.
me.notify(itemFiltered ? 'filtereditemchange' : 'itemchange', [details]);
me.endUpdate();
}
},
/**
* Remove an item from the collection.
* @param {Object/Object[]} item The item or items to remove.
* @return {Number} The number of items removed.
* @since 5.0.0
*/
remove: function (item) {
var me = this,
items = me.decodeRemoveItems(arguments, 0),
length = me.length;
me.splice(0, items);
return length - me.length;
},
/**
* Remove all items in the collection.
* @return {Ext.util.Collection} This object.
* @since 5.0.0
*/
removeAll: function () {
var me = this,
length = me.length;
if (me.generation && length) {
me.splice(0, length);
}
return me;
},
/**
* Remove an item from a specified index in the collection.
* @param {Number} index The index within the collection of the item to remove.
* @param {Number} [count=1] The number of items to remove.
* @return {Object/Number} If `count` was 1 and the item was removed, that item is
* returned. Otherwise the number of items removed is returned.
* @since 5.0.0
*/
removeAt: function (index, count) {
var me = this,
length = me.length,
Num = Ext.Number,
range = Num.clipIndices(length, [ index, (count === undefined) ? 1 : count ],
Num.Clip.COUNT),
n = range[0],
removeCount = range[1] - n,
item = (removeCount === 1) && me.getAt(n),
removed;
me.splice(n, removeCount);
removed = me.length - length;
return (item && removed) ? item : removed;
},
/**
* Removes the item associated with the passed key from the collection.
* @param {String} key The key of the item to remove.
* @return {Object} Only returned if removing at a specified key. The item removed or
* `false` if no item was removed.
* @since 5.0.0
*/
removeByKey: function (key) {
var item = this.getByKey(key);
if (!item || !this.remove(item)) {
return false;
}
return item;
},
/*
* @private
* Replace an old entry with a new entry of the same key if the entry existed.
* @param {Object} item The item to insert.
* @return {Object} inserted The item inserted.
*/
replace: function(item) {
var index = this.indexOf(item);
if (index === -1) {
this.add(item);
} else {
this.insert(index, item);
}
},
/**
* This method is basically the same as the JavaScript Array splice method.
*
* Negative indexes are interpreted starting at the end of the collection. That is,
* a value of -1 indicates the last item, or equivalent to `length - 1`.
*
* @param {Number} index The index at which to add or remove items.
* @param {Number/Object[]} toRemove The number of items to remove or an array of the
* items to remove.
* @param {Object[]} [toAdd] The items to insert at the given `index`.
* @since 5.0.0
*/
splice: function (index, toRemove, toAdd) {
var me = this,
autoSort = me.sorted && me.getAutoSort(),
map = me.map,
items = me.items,
length = me.length,
removeItems = (toRemove instanceof Array) ? me.decodeRemoveItems(toRemove) : null,
isRemoveCount = !removeItems,
Num = Ext.Number,
range = Num.clipIndices(length, [index, isRemoveCount ? toRemove : 0],
Num.Clip.COUNT),
begin = range[0],
end = range[1],
// Determine how many items we might actually remove:
removeCount = end - begin,
newItems = me.decodeItems(arguments, 2),
newCount = newItems ? newItems.length : 0,
addItems, newItemsMap, removeMap,
insertAt = begin,
indices = me.indices || ((newCount || removeItems) ? me.getIndices() : null),
adds = null,
removes = removeCount ? [begin] : null,
newKeys = null,
source = me.getSource(),
chunk, chunkItems, chunks, i, item, itemIndex, k, key, keys, n, duplicates,
sorters;
if (source && !source.updating) {
// Modifying the content of a child collection has to be translated into a
// change of its source. Because the source has all of the items of the child
// (but likely at different indices) we can translate "index" and convert a
// "removeCount" request into a "removeItems" request.
if (isRemoveCount) {
removeItems = [];
for (i = 0; i < removeCount; ++i) {
removeItems.push(items[begin + i]);
}
}
if (begin < length) {
// Map index based on the item at that index since that item will be in
// the source collection.
i = source.indexOf(items[begin]);
} else {
// Map end of this collection to end of the source collection.
i = source.length;
}
source.splice(i, removeItems, newItems);
return me;
}
// Loop over the newItems because they could already be in the collection or may
// be replacing items in the collection that just happen to have the same key. In
// this case, those items must be removed as well. Since we need to call getKey
// on each newItem to do this we may as well keep those keys for later.
if (newCount) {
addItems = newItems;
newKeys = [];
newItemsMap = {};
// If this collection is sorted we will eventually need to sort addItems so
// do that now so we can line up the newKeys properly. We optimize for the
// case where we have no duplicates. It would be more expensive to do this
// in two passes in an attempt to take advantage of removed duplicates.
if (autoSort) {
// We'll need the sorters later as well
sorters = me.getSorters();
if (newCount > 1) {
if (!addItems.$cloned) {
newItems = addItems = addItems.slice(0);
}
me.sortData(addItems);
}
}
for (i = 0; i < newCount; ++i) {
key = me.getKey(item = newItems[i]);
if ((k = newItemsMap[key]) !== undefined) {
// Duplicates in the incoming newItems need to be discarded keeping the
// last of the duplicates. We add the index of the last duplicate of
// this key to the "duplicates" map.
(duplicates || (duplicates = {}))[k] = 1;
} else {
// This item's index is outside the remove range, so we need to remove
// some extra stuff. Only the first occurrence of a given key in the
// newItems needs this processing.
itemIndex = indices[key];
if (itemIndex < begin || end <= itemIndex) {
(removes || (removes = [])).push(itemIndex); // might be the first
}
}
newItemsMap[key] = i; // track the last index of this key in newItems
newKeys.push(key); // must correspond 1-to-1 with newItems
}
if (duplicates) {
keys = newKeys;
addItems = [];
newKeys = [];
addItems.$cloned = true;
for (i = 0; i < newCount; ++i) {
if (!duplicates[i]) {
item = newItems[i];
addItems.push(item);
newKeys.push(keys[i]);
}
}
newCount = addItems.length;
}
adds = {
//at: insertAt, // must fill this in later
//next: null, // only set by spliceMerge
//replaced: null, // must fill this in later
items: addItems,
keys: newKeys
};
}
// If we are given a set of items to remove, map them to their indices.
for (i = removeItems ? removeItems.length : 0; i-- > 0; ) {
key = me.getKey(removeItems[i]);
if ((itemIndex = indices[key]) !== undefined) {
// ignore items we don't have (probably due to filtering)
(removes || (removes = [])).push(itemIndex); // might be the first remove
}
}
if (!adds && !removes) {
return me;
}
me.beginUpdate();
// Now we that everything we need to remove has its index in the removes array.
// We start by sorting the array so we can coalesce the index values into chunks
// or ranges.
if (removes) {
chunk = null;
chunks = [];
removeMap = {};
if (removes.length > 1) {
removes.sort(Ext.Array.numericSortFn);
}
// Coalesce the index array into chunks of (index, count) pairs for efficient
// removal.
for (i = 0, n = removes.length; i < n; ++i) {
key = me.getKey(item = items[itemIndex = removes[i]]);
if (!(key in map)) {
continue;
}
// Avoids 2nd loop of removed items but also means we won't process any
// given item twice (in case of duplicates in removeItems).
delete map[key];
// Consider chunk = { at: 1, items: [ item1, item2 ] }
//
// +---+---+---+---+---+---+
// | | x | x | | | |
// +---+---+---+---+---+---+
// 0 1 2 3 4 5
//
// If we are adding an itemIndex > 3 we need a new chunk.
//
if (!chunk || itemIndex > (chunk.at + chunkItems.length)) {
chunks.push(chunk = {
at: itemIndex,
items: (chunkItems = []),
keys: (keys = []),
map: removeMap,
next: chunk,
replacement: adds
});
// Point "replaced" at the last chunk
if (adds) {
adds.replaced = chunk;
}
}
chunkItems.push(removeMap[key] = item);
keys.push(key);
if (itemIndex < insertAt) {
// If the removal is ahead of the insertion point specified, we need
// to move the insertAt backwards.
//
// Consider the following splice:
//
// collection.splice(3, 2, [ { id: 'b' } ]);
//
// +---+---+---+---+---+---+
// | a | b | c | x | y | d |
// +---+---+---+---+---+---+
// 0 1 2 3 4 5
// ^ ^ ^
// | \ /
// replace remove
//
// The intent is to replace x and y with the new item at index=3. But
// since the new item has the same key as the item at index=1, that
// item must be replaced. The resulting collection will be:
//
// +---+---+---+---+
// | a | c | b | d |
// +---+---+---+---+
// 0 1 2 3
//
--insertAt;
}
if (removeCount > 1 && itemIndex === begin) {
// To account for the given range to remove we started by putting the
// index of the first such item ("begin") in the array. When we find
// it in this loop we have to process all of the items and add them
// to the current chunk. The following trick allows us to repeat the
// loop for each item in the removeCount.
//
--removeCount; // countdown...
removes[i--] = ++begin; // backup and increment begin
}
} // for (removes)
if (adds) {
adds.at = insertAt; // we have the correct(ed) insertAt now
}
// Loop over the chunks in reverse so as to not invalidate index values on
// earlier chunks.
for (k = chunks.length; k-- > 0; ) {
chunk = chunks[k];
i = chunk.at;
n = chunk.items.length;
if (i + n < length) {
// If we are removing the tail of the collection, we can keep the
// indices for the rest of the things... otherwise we need to zap it
// and fix up later.
me.indices = indices = null;
}
me.length = length -= n;
// We can use splice directly. The IE8 bug which Ext.Array works around
// only affects *insertion*
// http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/
//Ext.Array.erase(items, i, n);
items.splice(i, n);
if (indices) {
keys = chunk.keys;
for (i = 0; i < n; ++i) {
delete indices[keys[i]];
}
}
++me.generation;
me.notify('remove', [ chunk ]);
}
} // if (removes)
if (adds) {
if (autoSort && newCount > 1 && length) {
me.spliceMerge(addItems, newKeys);
} else {
if (autoSort) {
if (newCount > 1) {
// We have multiple addItems but we are empty, so just add at 0
insertAt = 0;
me.indices = indices = null;
} else {
// If we are adding one item we can position it properly now and
// avoid a full sort.
insertAt = sorters.findInsertionIndex(adds.items[0], items, me.getSortFn());
}
}
if (insertAt === length) {
// appending
items.push.apply(items, addItems);
// The indices may have been regenerated, so we need to check if they have been
// and update them
indices = me.indices;
if (indices) {
for (i = 0; i < newCount; ++i) {
indices[newKeys[i]] = insertAt + i;
}
}
} else {
// inserting
me.indices = null;
Ext.Array.insert(items, insertAt, addItems);
}
for (i = 0; i < newCount; ++i) {
map[newKeys[i]] = addItems[i];
}
me.length += newCount;
adds.at = insertAt;
adds.atItem = insertAt === 0 ? null : items[insertAt - 1];
++me.generation;
me.notify('add', [ adds ]);
}
} // if (adds)
me.endUpdate();
return me;
},
/**
* This method calls the supplied function `fn` between `beginUpdate` and `endUpdate`
* calls.
*
* collection.update(function () {
* // Perform multiple collection updates...
*
* collection.add(item);
* // ...
*
* collection.insert(index, otherItem);
* //...
*
* collection.remove(someItem);
* });
*
* @param {Function} fn The function to call that will modify this collection.
* @param {Ext.util.Collection} fn.collection This collection.
* @param {Object} [scope=this] The `this` pointer to use when calling `fn`.
* @return {Object} Returns the value returned from `fn` (typically `undefined`).
* @since 5.0.0
*/
update: function (fn, scope) {
var me = this;
me.beginUpdate();
try {
return fn.call(scope || me, me);
}
catch (e) {
//<debug>
Ext.log.error(this.$className + ': Unhandled Exception: ', e.description || e.message);
//</debug>
throw e;
}
finally {
me.endUpdate();
}
},
/**
* Change the key for an existing item in the collection. If the old key does not
* exist this call does nothing. Even so, it is highly recommended to *avoid* calling
* this method for an `item` that is not a member of this collection.
*
* @param {Object} item The item whose key has changed. The `item` should be a member
* of this collection.
* @param {String} oldKey The old key for the `item`.
* @since 5.0.0
*/
updateKey: function (item, oldKey) {
var me = this,
map = me.map,
indices = me.indices,
source = me.getSource(),
newKey;
if (source && !source.updating) {
// If we are being told of the key change and the source has the same idea
// on keying the item, push the change down instead.
source.updateKey(item, oldKey);
}
else if ((newKey = me.getKey(item)) !== oldKey) {
// If the key has changed and "item" is the item mapped to the oldKey and
// there is no collision with an item with the newKey, we can proceed.
if (map[oldKey] === item && !(newKey in map)) {
delete map[oldKey];
// We need to mark ourselves as updating so that observing collections
// don't reflect the updateKey back to us (see above check) but this is
// not really a normal update cycle so we don't call begin/endUpdate.
me.updating++;
me.generation++;
map[newKey] = item;
if (indices) {
indices[newKey] = indices[oldKey];
delete indices[oldKey];
}
me.notify('updatekey', [{
item: item,
newKey: newKey,
oldKey: oldKey
}]);
me.updating--;
}
//<debug>
else {
// It may be that the item is (somehow) already in the map using the
// newKey or that there is no item in the map with the oldKey. These
// are not errors.
if (newKey in map && map[newKey] !== item) {
// There is a different item in the map with the newKey which is an
// error. To properly handle this, add the item instead.
Ext.Error.raise('Duplicate newKey "' + newKey +
'" for item with oldKey "' + oldKey + '"');
}
if (oldKey in map && map[oldKey] !== item) {
// There is a different item in the map with the oldKey which is also
// an error. Do not call this method for items that are not part of
// the collection.
Ext.Error.raise('Incorrect oldKey "' + oldKey +
'" for item with newKey "' + newKey + '"');
}
}
//</debug>
}
},
findInsertIndex: function(item) {
var source = this.getSource(),
sourceItems = source.items,
i = source.indexOf(item) - 1,
sourceItem, index;
while (i > -1) {
sourceItem = sourceItems[i];
index = this.indexOf(sourceItem);
if (index > -1) {
return index + 1;
}
--i;
}
// If we get here we didn't find any item in the parent before us, so insert
// at the start
return 0;
},
//-------------------------------------------------------------------------
// Calls from the source Collection:
/**
* This method is called when items are added to the `source` collection. This is
* equivalent to the `{@link #event-add add}` event but is called before the `add`
* event is fired.
* @param {Ext.util.Collection} source The source collection.
* @param {Object} details The `details` of the `{@link #event-add add}` event.
* @private
* @since 5.0.0
*/
onCollectionAdd: function (source, details) {
var me = this,
atItem = details.atItem,
items = details.items,
requestedIndex = me.requestedIndex,
filtered, index,
copy, i, item, n;
// No point determining the index if we're sorted
if (!me.sorted) {
// If we have a requestedIndex, it means the add/insert was on our collection, so try
// use that specified index to do the insertion.
if (requestedIndex !== undefined) {
index = requestedIndex;
} else if (atItem) {
index = me.indexOf(atItem);
if (index === -1) {
// We can't find the reference item in our collection, which means it's probably
// filtered out, so we need to search for an appropriate index. Pass the first item
// and work back to find
index = me.findInsertIndex(items[0]);
} else {
// We also have that item in our collection, we need to insert after it, so increment
++index;
}
} else {
// If there was no atItem, must be at the front of the collection
index = 0;
}
}
if (me.getAutoFilter() && me.filtered) {
for (i = 0, n = items.length; i < n; ++i) {
item = items[i];
if (me.isItemFiltered(item)) {
// If we have an item that is filtered out of this collection, we need
// to make a copy of the items up to this point.
if (!copy) {
copy = items.slice(0, i);
}
if (!filtered) {
filtered = [];
}
filtered.push(item);
} else if (copy) {
// If we have a copy of the items, we need to put this item in that
// copy since it is not being filtered out.
copy.push(item);
}
}
}
me.splice((index < 0) ? me.length : index, 0, copy || items);
if (filtered) {
// Private for now. We may want to let any observers know we just
// added these items but got filtered out
me.notify('filteradd', [filtered]);
}
},
/**
* This method is called when an item is modified in the `source` collection. This is
* equivalent to the `{@link #event-beforeitemchange beforeitemchange}` event but is
* called before the `beforeitemchange` event is fired.
* @param {Ext.util.Collection} source The source collection.
* @param {Object} details The `details` of the
* `{@link #event-beforeitemchange beforeitemchange}` event.
* @private
* @since 5.0.0
*/
onCollectionBeforeItemChange: function (source, details) {
// Drop the next updatekey event
this.onCollectionUpdateKey = null;
},
/**
* This method is called when the `source` collection starts updating. This is
* equivalent to the `{@link #event-beginupdate beginupdate}` event but is called
* before the `beginupdate` event is fired.
* @param {Ext.util.Collection} source The source collection.
* @private
* @since 5.0.0
*/
onCollectionBeginUpdate: function () {
this.beginUpdate();
},
/**
* This method is called when the `source` collection finishes updating. This is
* equivalent to the `{@link #event-endupdate endupdate}` event but is called before
* the `endupdate` event is fired.
* @param {Ext.util.Collection} source The source collection.
* @private
* @since 5.0.0
*/
onCollectionEndUpdate: function () {
this.endUpdate();
},
/**
* This method is called when an item is modified in the `source` collection. This is
* equivalent to the `{@link #event-itemchange itemchange}` event but is called before
* the `itemchange` event is fired.
* @param {Ext.util.Collection} source The source collection.
* @param {Object} details The `details` of the `{@link #event-itemchange itemchange}`
* event.
* @private
* @since 5.0.0
*/
onCollectionItemChange: function (source, details) {
// Restore updatekey events
delete this.onCollectionUpdateKey;
this.itemChanged(details.item, details.modified, details.oldKey, details.meta);
},
// If our source collection informs us that a filtered out item has changed, we do not care
// We contain only the filtered in items of the source collection.
onCollectionFilteredItemChange: null,
/**
* This method is called when the `source` collection refreshes. This is equivalent to
* the `{@link #event-refresh refresh}` event but is called before the `refresh` event
* is fired.
* @param {Ext.util.Collection} source The source collection.
* @private
* @since 5.0.0
*/
onCollectionRefresh: function (source) {
var me = this,
map = {},
indices = {},
i, item, items, key, length;
items = source.items;
items = me.filtered && me.getAutoFilter() ? Ext.Array.filter(items, me.getFilterFn()) : items.slice(0);
if (me.sorted) {
me.sortData(items);
}
me.items = items;
me.length = length = items.length;
me.map = map;
me.indices = indices;
for (i = 0; i < length; ++i) {
key = me.getKey(item = items[i]);
map[key] = item;
indices[key] = i;
}
me.notify('refresh');
},
/**
* This method is called when items are removed from the `source` collection. This is
* equivalent to the `{@link #event-remove remove}` event but is called before the
* `remove` event is fired.
* @param {Ext.util.Collection} source The source collection.
* @param {Object} details The `details` of the `remove` event.
* @private
* @since 5.0.0
*/
onCollectionRemove: function (source, details) {
this.splice(0, details.items);
},
/**
* @method onCollectionSort
* This method is called when the `source` collection is sorted. This is equivalent to
* the `{@link #event-sort sort}` event but is called before the `sort` event is fired.
* @param {Ext.util.Collection} source The source collection.
* @private
* @since 5.0.0
*/
// onCollectionSort: function (source) {
// we ignore sorting of the source collection because we prefer our own order.
// },
/**
* This method is called when key changes in the `source` collection. This is
* equivalent to the `updatekey` event but is called before the `updatekey` event is
* fired.
* @param {Ext.util.Collection} source The source collection.
* @param {Object} details The `details` of the `updatekey` event.
* @private
* @since 5.0.0
*/
onCollectionUpdateKey: function (source, details) {
this.updateKey(details.item, details.oldKey);
},
//-------------------------------------------------------------------------
// Private
/**
* @method average
* Averages property values from some or all of the items in this collection.
*
* @param {String} property The name of the property to average from each item.
* @param {Number} [begin] The index of the first item to include in the average.
* @param {Number} [end] The index at which to stop averaging `items`. The item at
* this index will *not* be included in the average.
* @return {Object} The result of averaging the specified property from the indicated
* items.
* @since 5.0.0
*/
/**
* @method averageByGroup
* See {@link #average}. The result is partitioned by group.
*
* @param {String} property The name of the property to average from each item.
* @return {Object} The result of {@link #average}, partitioned by group. See {@link #aggregateByGroup}.
* @since 5.0.0
*/
/**
* @method bounds
* Determines the minimum and maximum values for the specified property over some or
* all of the items in this collection.
*
* @param {String} property The name of the property from each item.
* @param {Number} [begin] The index of the first item to include in the bounds.
* @param {Number} [end] The index at which to stop in `items`. The item at this index
* will *not* be included in the bounds.
* @return {Array} An array `[min, max]` with the minimum and maximum of the specified
* property.
* @since 5.0.0
*/
/**
* @method boundsByGroup
* See {@link #bounds}. The result is partitioned by group.
*
* @param {String} property The name of the property from each item.
* @return {Object} The result of {@link #bounds}, partitioned by group. See {@link #aggregateByGroup}.
* @since 5.0.0
*/
/**
* @method count
* Determines the number of items in the collection.
*
* @return {Number} The number of items.
* @since 5.0.0
*/
/**
* @method countByGroup
* See {@link #count}. The result is partitioned by group.
*
* @return {Object} The result of {@link #count}, partitioned by group. See {@link #aggregateByGroup}.
* @since 5.0.0
*/
/**
* @method extremes
* Finds the items with the minimum and maximum for the specified property over some
* or all of the items in this collection.
*
* @param {String} property The name of the property from each item.
* @param {Number} [begin] The index of the first item to include.
* @param {Number} [end] The index at which to stop in `items`. The item at this index
* will *not* be included.
* @return {Array} An array `[minItem, maxItem]` with the items that have the minimum
* and maximum of the specified property.
* @since 5.0.0
*/
/**
* @method extremesByGroup
* See {@link #extremes}. The result is partitioned by group.
*
* @param {String} property The name of the property from each item.
* @return {Object} The result of {@link #extremes}, partitioned by group. See {@link #aggregateByGroup}.
* @since 5.0.0
*/
/**
* @method max
* Determines the maximum value for the specified property over some or all of the
* items in this collection.
*
* @param {String} property The name of the property from each item.
* @param {Number} [begin] The index of the first item to include in the maximum.
* @param {Number} [end] The index at which to stop in `items`. The item at this index
* will *not* be included in the maximum.
* @return {Object} The maximum of the specified property from the indicated items.
* @since 5.0.0
*/
/**
* @method maxByGroup
* See {@link #max}. The result is partitioned by group.
*
* @param {String} property The name of the property from each item.
* @return {Object} The result of {@link #max}, partitioned by group. See {@link #aggregateByGroup}.
* @since 5.0.0
*/
/**
* @method maxItem
* Finds the item with the maximum value for the specified property over some or all
* of the items in this collection.
*
* @param {String} property The name of the property from each item.
* @param {Number} [begin] The index of the first item to include in the maximum.
* @param {Number} [end] The index at which to stop in `items`. The item at this index
* will *not* be included in the maximum.
* @return {Object} The item with the maximum of the specified property from the
* indicated items.
* @since 5.0.0
*/
/**
* @method maxItemByGroup
* See {@link #maxItem}. The result is partitioned by group.
*
* @param {String} property The name of the property from each item.
* @return {Object} The result of {@link #maxItem}, partitioned by group. See {@link #aggregateByGroup}.
* @since 5.0.0
*/
/**
* @method min
* Determines the minimum value for the specified property over some or all of the
* items in this collection.
*
* @param {String} property The name of the property from each item.
* @param {Number} [begin] The index of the first item to include in the minimum.
* @param {Number} [end] The index at which to stop in `items`. The item at this index
* will *not* be included in the minimum.
* @return {Object} The minimum of the specified property from the indicated items.
* @since 5.0.0
*/
/**
* @method minByGroup
* See {@link #min}. The result is partitioned by group.
*
* @param {String} property The name of the property from each item.
* @return {Object} The result of {@link #min}, partitioned by group. See {@link #aggregateByGroup}.
* @since 5.0.0
*/
/**
* @method minItem
* Finds the item with the minimum value for the specified property over some or all
* of the items in this collection.
*
* @param {String} property The name of the property from each item.
* @param {Number} [begin] The index of the first item to include in the minimum.
* @param {Number} [end] The index at which to stop in `items`. The item at this index
* will *not* be included in the minimum.
* @return {Object} The item with the minimum of the specified property from the
* indicated items.
* @since 5.0.0
*/
/**
* @method minItemByGroup
* See {@link #minItem}. The result is partitioned by group.
*
* @param {String} property The name of the property from each item.
* @return {Object} The result of {@link #minItem}, partitioned by group. See {@link #aggregateByGroup}.
* @since 5.0.0
*/
/**
* @method sum
* Sums property values from some or all of the items in this collection.
*
* @param {String} property The name of the property to sum from each item.
* @param {Number} [begin] The index of the first item to include in the sum.
* @param {Number} [end] The index at which to stop summing `items`. The item at this
* index will *not* be included in the sum.
* @return {Object} The result of summing the specified property from the indicated
* items.
* @since 5.0.0
*/
/**
* @method sumByGroup
* See {@link #sum}. The result is partitioned by group.
*
* @param {String} property The name of the property to sum from each item.
* @return {Object} The result of {@link #sum}, partitioned by group. See {@link #aggregateByGroup}.
* @since 5.0.0
*/
_aggregators: {
average: function (items, begin, end, property, root) {
var n = end - begin;
return n &&
this._aggregators.sum.call(this, items, begin, end, property, root) / n;
},
bounds: function (items, begin, end, property, root) {
for (var value, max, min, i = begin; i < end; ++i) {
value = items[i];
value = (root ? value[root] : value)[property];
// First pass max and min are undefined and since nothing is less than
// or greater than undefined we always evaluate these "if" statements as
// true to pick up the first value as both max and min.
if (!(value < max)) { // jshint ignore:line
max = value;
}
if (!(value > min)) { // jshint ignore:line
min = value;
}
}
return [min, max];
},
count: function(items) {
return items.length;
},
extremes: function (items, begin, end, property, root) {
var most = null,
least = null,
i, item, max, min, value;
for (i = begin; i < end; ++i) {
item = items[i];
value = (root ? item[root] : item)[property];
// Same trick as "bounds"
if (!(value < max)) { // jshint ignore:line
max = value;
most = item;
}
if (!(value > min)) { // jshint ignore:line
min = value;
least = item;
}
}
return [least, most];
},
max: function (items, begin, end, property, root) {
var b = this._aggregators.bounds.call(this, items, begin, end, property, root);
return b[1];
},
maxItem: function (items, begin, end, property, root) {
var b = this._aggregators.extremes.call(this, items, begin, end, property, root);
return b[1];
},
min: function (items, begin, end, property, root) {
var b = this._aggregators.bounds.call(this, items, begin, end, property, root);
return b[0];
},
minItem: function (items, begin, end, property, root) {
var b = this._aggregators.extremes.call(this, items, begin, end, property, root);
return b[0];
},
sum: function (items, begin, end, property, root) {
for (var value, sum = 0, i = begin; i < end; ++i) {
value = items[i];
value = (root ? value[root] : value)[property];
sum += value;
}
return sum;
}
},
_eventToMethodMap: {
add: 'onCollectionAdd',
beforeitemchange: 'onCollectionBeforeItemChange',
beginupdate: 'onCollectionBeginUpdate',
endupdate: 'onCollectionEndUpdate',
itemchange: 'onCollectionItemChange',
filtereditemchange: 'onCollectionFilteredItemChange',
refresh: 'onCollectionRefresh',
remove: 'onCollectionRemove',
beforesort: 'beforeCollectionSort',
sort: 'onCollectionSort',
filter: 'onCollectionFilter',
filteradd: 'onCollectionFilterAdd',
updatekey: 'onCollectionUpdateKey'
},
/**
* Adds an observing object to this collection. Observers are given first view of all
* events that we may fire. For any event an observer may implement a method whose
* name starts with "onCollection" to receive the event. The `{@link #event-add add}`
* event for example would be passed to `"onCollectionAdd"`.
*
* The only restriction to observers is that they are not allowed to add or remove
* observers from inside these methods.
*
* @param {Ext.util.Collection} observer The observer instance.
* @private
* @since 5.0.0
*/
addObserver: function (observer) {
var me = this,
observers = me.observers;
if (!observers) {
me.observers = observers = [];
}
//<debug>
if (Ext.Array.contains(observers, observer)) {
Ext.Error.raise('Observer already added');
}
//</debug>
observers.push(observer);
if (observers.length > 1) {
// Allow observers to be inserted with a priority.
// For example GroupCollections must react to Collection mutation before views.
Ext.Array.sort(observers, me.prioritySortFn);
}
},
prioritySortFn: function(o1, o2) {
var a = o1.observerPriority || 0,
b = o2.observerPriority || 0;
return a - b;
},
applyExtraKeys: function (extraKeys, oldExtraKeys) {
var me = this,
ret = oldExtraKeys || {},
config,
name,
value;
for (name in extraKeys) {
value = extraKeys[name];
if (!value.isCollectionKey) {
config = {
collection: me
};
if (Ext.isString(value)) {
config.property = value;
} else {
config = Ext.apply(config, value);
}
value = new Ext.util.CollectionKey(config);
} else {
value.setCollection(me);
}
ret[name] = me[name] = value;
value.name = name;
}
return ret;
},
applyGrouper: function (grouper) {
if (grouper) {
grouper = this.getSorters().decodeSorter(grouper, 'Ext.util.Grouper');
}
return grouper;
},
/**
* Returns the items array on which to operate. This is called to handle the two
* possible forms used by various methods that accept items:
*
* collection.add(item1, item2, item3);
* collection.add([ item1, item2, item3 ]);
*
* Things get interesting when other arguments are involved:
*
* collection.insert(index, item1, item2, item3);
* collection.insert(index, [ item1, item2, item3 ]);
*
* As well as below because we have to distinguish the one item from from the array:
*
* collection.add(item);
* collection.insert(index, item);
*
* @param {Arguments} args The arguments object from the caller.
* @param {Number} index The index in `args` (the caller's arguments) of `items`.
* @return {Object[]} The array of items on which to operate.
* @private
* @since 5.0.0
*/
decodeItems: function (args, index) {
var me = this,
ret = (index === undefined) ? args : args[index],
cloned, decoder, i;
if (!ret || !ret.$cloned) {
cloned = args.length > index + 1 || !Ext.isIterable(ret);
if (cloned) {
ret = Ext.Array.slice(args, index);
if (ret.length === 1 && ret[0] === undefined) {
ret.length = 0;
}
}
decoder = me.getDecoder();
if (decoder) {
if (!cloned) {
ret = ret.slice(0);
cloned = true;
}
for (i = ret.length; i-- > 0; ) {
if ((ret[i] = decoder.call(me, ret[i])) === false) {
ret.splice(i, 1);
}
}
}
if (cloned) {
ret.$cloned = true;
}
}
return ret;
},
/**
* Returns the map of key to index for all items in this collection. This method will
* lazily populate this map on request. This map is maintained when doing so does not
* involve too much overhead. When this threshold is cross, the index map is discarded
* and must be rebuilt by calling this method.
*
* @return {Object}
* @private
* @since 5.0.0
*/
getIndices: function () {
var me = this,
indices = me.indices,
items = me.items,
n = items.length,
i, key;
if (!indices) {
me.indices = indices = {};
++me.indexRebuilds;
for (i = 0; i < n; ++i) {
key = me.getKey(items[i]);
indices[key] = i;
}
}
return indices;
},
/**
* This method wraps all fired events and gives observers first view of the change.
*
* @param {String} eventName The name of the event to fire.
* @param {Array} [args] The event arguments. This collection instance is inserted at
* the front of this array if there is any receiver for the notification.
*
* @private
* @since 5.0.0
*/
notify: function (eventName, args) {
var me = this,
observers = me.observers,
methodName = me._eventToMethodMap[eventName],
added = 0,
index, length, method, observer;
args = args || [];
if (observers && methodName) {
for (index = 0, length = observers.length; index < length; ++index) {
method = (observer = observers[index])[methodName];
if (method) {
if (!added++) { // jshint ignore:line
args.unshift(me); // put this Collection as the first argument
}
method.apply(observer, args);
}
}
}
// During construction, no need to fire an event here
if (!me.hasListeners) {
return;
}
if (me.hasListeners[eventName]) {
if (!added) {
args.unshift(me); // put this Collection as the first argument
}
me.fireEventArgs(eventName, args);
}
},
/**
* Returns the filter function.
* @return {Function} sortFn The sort function.
*/
getFilterFn: function () {
return this.getFilters().getFilterFn();
},
/**
* Returns the `Ext.util.FilterCollection`. Unless `autoCreate` is explicitly passed
* as `false` this collection will be automatically created if it does not yet exist.
* @param [autoCreate=true] Pass `false` to disable auto-creation of the collection.
* @return {Ext.util.FilterCollection} The collection of filters.
*/
getFilters: function (autoCreate) {
var ret = this._filters;
if (!ret && autoCreate !== false) {
ret = new Ext.util.FilterCollection();
this.setFilters(ret);
}
return ret;
},
/**
* This method can be used to conveniently test whether an individual item would be
* removed due to the current filter.
* @param {Object} item The item to test.
* @return {Boolean} The value `true` if the item would be "removed" from the
* collection due to filters or `false` otherwise.
*/
isItemFiltered: function (item) {
return !this.getFilters().filterFn(item);
},
/**
* Called after a change of the filter is complete.
*
* For example:
*
* onFilterChange: function (filters) {
* if (this.filtered) {
* // process filters
* } else {
* // no filters present
* }
* }
*
* @template
* @method
* @param {Ext.util.FilterCollection} filters The filters collection.
*/
onFilterChange: function (filters) {
var me = this,
source = me.getSource(),
extraKeys, newKeys, key;
if (!source) {
// In this method, we have changed the filter but since we don't start with
// any and we create the source collection as needed that means we are getting
// our first filter.
extraKeys = me.getExtraKeys()
if (extraKeys) {
newKeys = {};
for (key in extraKeys) {
newKeys[key] = extraKeys[key].clone(me);
}
}
source = new Ext.util.Collection({
keyFn: me.getKey,
extraKeys: newKeys,
rootProperty: me.getRootProperty()
});
if (me.length) {
source.add(me.items);
}
me.setSource(source);
me.autoSource = source;
} else if (source.length || me.length) {
// if both us and the source are empty then we can skip this
me.onCollectionRefresh(source);
}
me.notify('filter');
},
//-------------------------------------------------------------------------
// Private
applyFilters: function (filters, collection) {
if (filters == null || (filters && filters.isFilterCollection)) {
return filters;
}
if (filters) {
if (!collection) {
collection = this.getFilters();
}
collection.splice(0, collection.length, filters);
}
return collection;
},
updateFilters: function (newFilters, oldFilters) {
var me = this;
if (oldFilters) {
// Do not disconnect from owning Filterable because
// default options (eg _rootProperty) are read from there.
// FilterCollections are detached from the Collection when the owning Store is remoteFilter: true
// or the owning store is a TreeStore and only filters new nodes before filling a parent node.
oldFilters.un('endupdate', 'onEndUpdateFilters', me);
}
if (newFilters) {
newFilters.on({
endupdate: 'onEndUpdateFilters',
scope: me,
priority: me.$endUpdatePriority
});
newFilters.$filterable = me;
}
me.onEndUpdateFilters(newFilters);
},
onEndUpdateFilters: function (filters) {
var me = this,
was = me.filtered,
is = !!filters && (filters.length > 0); // booleanize filters
if (was || is) {
me.filtered = is;
me.onFilterChange(filters);
}
},
/**
* Returns an up to date sort function.
* @return {Function} The sort function.
*/
getSortFn: function () {
return this._sortFn || this.createSortFn();
},
/**
* Returns the `Ext.util.SorterCollection`. Unless `autoCreate` is explicitly passed
* as `false` this collection will be automatically created if it does not yet exist.
* @param [autoCreate=true] Pass `false` to disable auto-creation of the collection.
* @return {Ext.util.SorterCollection} The collection of sorters.
*/
getSorters: function (autoCreate) {
var ret = this._sorters;
if (!ret && autoCreate !== false) {
ret = new Ext.util.SorterCollection();
this.setSorters(ret);
}
return ret;
},
/**
* Called after a change of the sort is complete.
*
* For example:
*
* onSortChange: function (sorters) {
* if (this.sorted) {
* // process sorters
* } else {
* // no sorters present
* }
* }
*
* @template
* @method
* @param {Ext.util.SorterCollection} sorters The sorters collection.
*/
onSortChange: function () {
if (this.sorted) {
this.sortItems();
}
},
/**
* Updates the sorters collection and triggers sorting of this Sortable.
*
* For example:
*
* //sort by a single field
* myStore.sort('myField', 'DESC');
*
* //sorting by multiple fields
* myStore.sort([{
* property : 'age',
* direction: 'ASC'
* }, {
* property : 'name',
* direction: 'DESC'
* }]);
*
* When passing a single string argument to sort, the `direction` is maintained for
* each field and is toggled automatically. So this code:
*
* store.sort('myField');
* store.sort('myField');
*
* Is equivalent to the following:
*
* store.sort('myField', 'ASC');
* store.sort('myField', 'DESC');
*
* @param {String/Function/Ext.util.Sorter[]} [property] Either the name of a property
* (such as a field of a `Ext.data.Model` in a `Store`), an array of configurations
* for `Ext.util.Sorter` instances or just a comparison function.
* @param {String} [direction] The direction by which to sort the data. This parameter
* is only valid when `property` is a String, otherwise the second parameter is the
* `mode`.
* @param {String} [mode="replace"] Where to put new sorters in the collection. This
* should be one the following values:
*
* * `**replace**` : The new sorter(s) become the sole sorter set for this Sortable.
* This is the most useful call mode to programmatically sort by multiple fields.
*
* * `**prepend**` : The new sorters are inserted as the primary sorters. The sorter
* collection length must be controlled by the developer.
*
* * `**multi**` : Similar to `**prepend**` the new sorters are inserted at the front
* of the collection of sorters. Following the insertion, however, this mode trims
* the sorter collection to enforce the `multiSortLimit` config. This is useful for
* implementing intuitive "Sort by this" user interfaces.
*
* * `**append**` : The new sorters are added at the end of the collection.
* @return {Ext.util.Collection} This instance.
*/
sort: function (property, direction, mode) {
var sorters = this.getSorters();
sorters.addSort.apply(sorters, arguments);
return this;
},
/**
* This method will sort an array based on the currently configured {@link #sorters}.
* @param {Array} data The array you want to have sorted.
* @return {Array} The array you passed after it is sorted.
*/
sortData: function (data) {
Ext.Array.sort(data, this.getSortFn());
return data;
},
/**
* Sorts the items of the collection using the supplied function. This should only be
* called for collections that have no `sorters` defined.
* @param {Function} sortFn The function by which to sort the items.
* @since 5.0.0
*/
sortItems: function (sortFn) {
var me = this;
if (me.sorted) {
//<debug>
if (sortFn) {
Ext.Error.raise('Collections with sorters cannot resorted');
}
//</debug>
sortFn = me.getSortFn();
}
me.indices = null;
me.notify('beforesort', [me.getSorters(false)]);
if (me.length) {
Ext.Array.sort(me.items, sortFn);
}
// Even if there's no data, notify interested parties.
// Eg: Stores must react and fire their refresh and sort events.
me.notify('sort');
},
/**
* Sorts the collection by a single sorter function
* @param {Function} sorterFn The function to sort by
* @deprecated
*/
sortBy: function(sortFn) {
return this.sortItems(sortFn);
},
//-------------------------------------------------------------------------
// Private
// Can be called to find the insertion index of a passed object in this collection.
// Or can be passed an items array to search in, and may be passed a comparator
findInsertionIndex: function(item, items, comparatorFn) {
if (!items) {
items = this.items;
}
if (!comparatorFn) {
comparatorFn = this.getSortFn();
}
return Ext.Array.binarySearch(items, item, comparatorFn);
},
applySorters: function (sorters, collection) {
if (sorters == null || (sorters && sorters.isSorterCollection)) {
return sorters;
}
if (sorters) {
if (!collection) {
collection = this.getSorters();
}
collection.splice(0, collection.length, sorters);
}
return collection;
},
createSortFn: function () {
var me = this,
grouper = me.getGrouper(),
sorters = me.getSorters(false),
sorterFn = sorters ? sorters.getSortFn() : null;
if (!grouper) {
return sorterFn;
}
return function (lhs, rhs) {
var ret = grouper.sort(lhs, rhs);
if (!ret && sorterFn) {
ret = sorterFn(lhs, rhs);
}
return ret;
};
},
updateGrouper: function (grouper) {
var me = this,
groups = me.getGroups(),
sorters = me.getSorters(),
populate;
me.onSorterChange();
me.grouped = !!grouper;
if (grouper) {
if (!groups) {
groups = new Ext.util.GroupCollection({
itemRoot: me.getRootProperty()
});
groups.$groupable = me;
me.setGroups(groups);
}
groups.setGrouper(grouper);
populate = true;
} else {
if (groups) {
me.removeObserver(groups);
groups.destroy();
}
me.setGroups(null);
}
if (!sorters.updating) {
me.onEndUpdateSorters(sorters);
}
if (populate) {
groups.onCollectionRefresh(me);
}
},
updateSorters: function (newSorters, oldSorters) {
var me = this;
if (oldSorters) {
// Do not disconnect from owning Filterable because
// default options (eg _rootProperty) are read from there.
// SorterCollections are detached from the Collection when the owning Store is remoteSort: true
// or the owning store is a TreeStore and only sorts new nodes before filling a parent node.
oldSorters.un('endupdate', 'onEndUpdateSorters', me);
}
if (newSorters) {
newSorters.on({
endupdate: 'onEndUpdateSorters',
scope: me,
priority: me.$endUpdatePriority
});
newSorters.$sortable = me;
}
me.onSorterChange();
me.onEndUpdateSorters(newSorters);
},
onSorterChange: function() {
this._sortFn = null;
},
onEndUpdateSorters: function (sorters) {
var me = this,
was = me.sorted,
is = (me.grouped && me.getAutoGroup()) || (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);
}
},
/**
* Removes an observing object to this collection. See `addObserver` for details.
*
* @param {Ext.util.Collection} observer The observer instance.
* @private
* @since 5.0.0
*/
removeObserver: function (observer) {
var observers = this.observers;
if (observers) {
Ext.Array.remove(observers, observer);
}
},
/**
* This method is what you might find in the core of a merge sort. We have an items
* array that is sorted so we sort the newItems and merge the two sorted arrays. In
* the general case, newItems will be no larger than all items so sorting it will be
* faster than simply concatenating the arrays and calling sort on it.
*
* We take advantage of the nature of this process to generate add events as ranges.
*
* @param {Object[]} newItems
* @private
* @since 5.0.0
*/
spliceMerge: function (newItems, newKeys) {
var me = this,
map = me.map,
newLength = newItems.length,
oldIndex = 0,
oldItems = me.items,
oldLength = oldItems.length,
adds = [],
count = 0,
items = [],
sortFn = me.getSortFn(), // account for grouper and sorter(s)
addItems, end, i, newItem, oldItem, newIndex;
me.items = items;
//
// oldItems
// +----+----+----+----+
// | 15 | 25 | 35 | 45 |
// +----+----+----+----+
// 0 1 2 3
//
// newItems
// +----+----+----+----+----+----+
// | 10 | 11 | 20 | 21 | 50 | 51 |
// +----+----+----+----+----+----+
// 0 1 2 3 4 5
//
for (newIndex = 0; newIndex < newLength; newIndex = end) {
newItem = newItems[newIndex];
// Flush out any oldItems that are <= newItem
for ( ; oldIndex < oldLength; ++oldIndex) {
// Consider above arrays...
// at newIndex == 0 this loop sets oldItem but breaks immediately
// at newIndex == 2 this loop pushes 15 and breaks w/oldIndex=1
// at newIndex == 4 this loop pushes 25, 35 and 45 and breaks w/oldIndex=4
if (sortFn(newItem, oldItem = oldItems[oldIndex]) < 0) {
break;
}
items.push(oldItem);
}
if (oldIndex === oldLength) {
// Consider above arrays...
// at newIndex == 0 we won't come in here (oldIndex == 0)
// at newIndex == 2 we won't come in here (oldIndex == 1)
// at newIndex == 4 we come here (oldIndex == 4), push 50 & 51 and break
adds[count++] = {
at: items.length,
itemAt: items[items.length - 1],
items: (addItems = [])
};
if (count > 1) {
adds[count - 2].next = adds[count - 1];
}
for (; newIndex < newLength; ++newIndex) {
addItems.push(newItem = newItems[newIndex]);
items.push(newItem);
}
break;
}
// else oldItem is the item from oldItems that is > newItem
// Consider above arrays...
// at newIndex == 0 we will push 10
// at newIndex == 2 we will push 20
adds[count++] = {
at: items.length,
itemAt: items[items.length - 1],
items: (addItems = [ newItem ])
};
if (count > 1) {
adds[count - 2].next = adds[count - 1];
}
items.push(newItem);
for (end = newIndex + 1; end < newLength; ++end) {
// Consider above arrays...
// at newIndex == 0 this loop pushes 11 then breaks w/end == 2
// at newIndex == 2 this loop pushes 21 the breaks w/end == 4
if (sortFn(newItem = newItems[end], oldItem) >= 0) {
break;
}
items.push(newItem);
addItems.push(newItem);
}
// if oldItems had 55 as its final element, the above loop would have pushed
// all of newItems so the outer for loop would then fall out
}
for (; oldIndex < oldLength; ++oldIndex) {
// In the above example, we won't come in here, but if you imagine a 55 in
// oldItems we would have oldIndex == 4 and oldLength == 5
items.push(oldItems[oldIndex]);
}
for (i = 0; i < newLength; ++i) {
map[newKeys[i]] = newItems[i];
}
me.length = items.length;
++me.generation;
me.indices = null;
// Tell users of the adds in increasing index order.
for (i = 0; i < count; ++i) {
me.notify('add', [ adds[i] ]);
}
},
getGroups: function() {
return this.callParent() || null;
},
updateAutoGroup: function(autoGroup) {
var groups = this.getGroups();
if (groups) {
groups.setAutoGroup(autoGroup);
}
// Important to call this so it can clear the .sorted flag
// as needed
this.onEndUpdateSorters(this._sorters);
},
updateGroups: function (newGroups, oldGroups) {
if (oldGroups) {
this.removeObserver(oldGroups);
}
if (newGroups) {
this.addObserver(newGroups);
}
},
updateSource: function (newSource, oldSource) {
var auto = this.autoSource;
if (oldSource) {
oldSource.removeObserver(this);
if (oldSource === auto) {
auto.destroy();
this.autoSource = null;
}
}
if (newSource) {
newSource.addObserver(this);
if (newSource.length || this.length) {
this.onCollectionRefresh(newSource);
}
}
}
},
function () {
var prototype = this.prototype;
// Minor compat method
prototype.removeAtKey = prototype.removeByKey;
/**
* This method is an alias for `decodeItems` but is called when items are being
* removed. If a `decoder` is provided it may be necessary to also override this
* method to achieve symmetry between adding and removing items. This is the case
* for `Ext.util.FilterCollection' and `Ext.util.SorterCollection' for example.
*
* @method decodeRemoveItems
* @protected
* @since 5.0.0
*/
prototype.decodeRemoveItems = prototype.decodeItems;
Ext.Object.each(prototype._aggregators, function (name) {
prototype[name] = function (property, begin, end) {
return this.aggregate(property, name, begin, end);
};
prototype[name + 'ByGroup'] = function(property) {
return this.aggregateByGroup(property, name);
};
});
});