extjs-gpl
Version:
GPL licensed version of Sencha Ext JS
1,330 lines (1,223 loc) • 137 kB
JavaScript
/**
* 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,
/**
* @cfg {Boolean} [autoSort=true] `true` to maintain sorted order when items
* are added regardless of requested insertion point, or when an item mutation
* results in a new sort position.
*
* This does not affect a filtered Collection's reaction to mutations of the source
* Collection. If sorters are present when the source Collection is mutated, this Collection's
* sort order will always be maintained.
* @private
*/
autoSort: true,
/**
* @cfg {Boolean} [autoGroup=true] `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,
/**
* @cfg {Boolean} trackGroups
* `true` to track individual groups in a Ext.util.GroupCollection
* @private
*/
trackGroups: true
},
/**
* @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,
/**
* @private
* `true` to destroy the sorter collection on destroy.
*/
manageSorters: true,
/**
* @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) {
// Set to false here so updateSorters doesn't trigger
// the template methods
me.grouped = me.sorted = false;
me.setSorters(null);
if (me.manageSorters) {
sorters.destroy();
}
}
if (groups) {
groups.destroy();
me._groups = null;
}
me.setSource(null);
me.observers = me.items = me.map = null;
me.callParent();
},
/**
* 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.splice(me.length, 0, items);
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,
ret, items;
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