/** * @class Ext.util.AbstractMixedCollection * @private */ Ext.define('Ext.util.AbstractMixedCollection', { requires: ['Ext.util.Filter'], mixins: { observable: 'Ext.util.Observable' }, /** * @property {Boolean} isMixedCollection * `true` in this class to identify an object as an instantiated MixedCollection, or subclass thereof. */ isMixedCollection: true, /** * @private Mutation counter which is incremented upon add and remove. */ generation: 0, constructor: function(allowFunctions, keyFn) { var me = this; me.items = []; me.map = {}; me.keys = []; me.length = 0; /** * @event clear * Fires when the collection is cleared. */ /** * @event add * Fires when an item is added to the collection. * @param {Number} index The index at which the item was added. * @param {Object} o The item added. * @param {String} key The key associated with the added item. */ /** * @event replace * Fires when an item is replaced in the collection. * @param {String} key he key associated with the new added. * @param {Object} old The item being replaced. * @param {Object} new The new item. */ /** * @event remove * Fires when an item is removed from the collection. * @param {Object} o The item being removed. * @param {String} key (optional) The key associated with the removed item. */ me.allowFunctions = allowFunctions === true; if (keyFn) { me.getKey = keyFn; } me.mixins.observable.constructor.call(me); }, /** * @cfg {Boolean} allowFunctions Specify true if the {@link #addAll} * function should add function references to the collection. Defaults to * false. */ allowFunctions : false, /** * Adds an item to the collection. Fires the {@link #event-add} event when complete. * * @param {String/Object} key The key to associate with the item, or the new item. * * If a {@link #getKey} implementation was specified for this MixedCollection, * or if the key of the stored items is in a property called `id`, * the MixedCollection will be able to *derive* the key for the new item. * In this case just pass the new item in this parameter. * * @param {Object} [o] The item to add. * * @return {Object} The item added. */ add : function(key, obj){ var me = this, myObj = obj, myKey = key, old; if (arguments.length == 1) { myObj = myKey; myKey = me.getKey(myObj); } if (typeof myKey != 'undefined' && myKey !== null) { old = me.map[myKey]; if (typeof old != 'undefined') { return me.replace(myKey, myObj); } me.map[myKey] = myObj; } me.generation++; me.length++; me.items.push(myObj); me.keys.push(myKey); if (me.hasListeners.add) { me.fireEvent('add', me.length - 1, myObj, myKey); } return myObj; }, /** * MixedCollection has a generic way to fetch keys if you implement getKey. The default implementation * simply returns item.id but you can provide your own implementation * to return a different value as in the following examples:

// normal way
var mc = new Ext.util.MixedCollection();
mc.add(someEl.dom.id, someEl);
mc.add(otherEl.dom.id, otherEl);
//and so on

// using getKey
var mc = new Ext.util.MixedCollection();
mc.getKey = function(el){
   return el.dom.id;
};
mc.add(someEl);
mc.add(otherEl);

// or via the constructor
var mc = new Ext.util.MixedCollection(false, function(el){
   return el.dom.id;
});
mc.add(someEl);
mc.add(otherEl);
     * 
* @param {Object} item The item for which to find the key. * @return {Object} The key for the passed item. */ getKey : function(o){ return o.id; }, /** * Replaces an item in the collection. Fires the {@link #event-replace} event when complete. * @param {String} key

The key associated with the item to replace, or the replacement item.

*

If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key * of your stored items is in a property called id, then the MixedCollection * will be able to derive the key of the replacement item. If you want to replace an item * with one having the same key value, then just pass the replacement item in this parameter.

* @param o {Object} o (optional) If the first parameter passed was a key, the item to associate * with that key. * @return {Object} The new item. */ replace : function(key, o){ var me = this, old, index; if (arguments.length == 1) { o = arguments[0]; key = me.getKey(o); } old = me.map[key]; if (typeof key == 'undefined' || key === null || typeof old == 'undefined') { return me.add(key, o); } me.generation++; index = me.indexOfKey(key); me.items[index] = o; me.map[key] = o; if (me.hasListeners.replace) { me.fireEvent('replace', key, old, o); } return o; }, /** * Adds all elements of an Array or an Object to the collection. * @param {Object/Array} objs An Object containing properties which will be added * to the collection, or an Array of values, each of which are added to the collection. * Functions references will be added to the collection if {@link #allowFunctions} * has been set to true. */ addAll : function(objs){ var me = this, i = 0, args, len, key; if (arguments.length > 1 || Ext.isArray(objs)) { args = arguments.length > 1 ? arguments : objs; for (len = args.length; i < len; i++) { me.add(args[i]); } } else { for (key in objs) { if (objs.hasOwnProperty(key)) { if (me.allowFunctions || typeof objs[key] != 'function') { me.add(key, objs[key]); } } } } }, /** * Executes the specified function once for every item in the collection. * The function should return a boolean value. * Returning false from the function will stop the iteration. * * @param {Function} fn The function to execute for each item. * @param {Mixed} fn.item The collection item. * @param {Number} fn.index The index of item. * @param {Number} fn.len Total length of collection. * @param {Object} scope (optional) The scope (this reference) * in which the function is executed. Defaults to the current item in the iteration. */ each : function(fn, scope){ var items = [].concat(this.items), // each safe for removal i = 0, len = items.length, item; for (; i < len; i++) { item = items[i]; if (fn.call(scope || item, item, i, len) === false) { break; } } }, /** * Executes the specified function once for every key in the collection, passing each * key, and its associated item as the first two parameters. * @param {Function} fn The function to execute for each item. * @param {String} fn.key The key of collection item. * @param {Mixed} fn.item The collection item. * @param {Number} fn.index The index of item. * @param {Number} fn.len Total length of collection. * @param {Object} scope (optional) The scope (this reference) in which the * function is executed. Defaults to the browser window. */ eachKey : function(fn, scope){ var keys = this.keys, items = this.items, i = 0, len = keys.length; for (; i < len; i++) { fn.call(scope || window, keys[i], items[i], i, len); } }, /** * 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 {Mixed} fn.item The collection item. * @param {String} fn.key The key of collection item. * @param {Object} scope (optional) The scope (this reference) in which the * function is executed. Defaults to the browser window. * @return {Object} The first item in the collection which returned true from the selection * function, or null if none was found. */ findBy : function(fn, scope) { var keys = this.keys, items = this.items, i = 0, len = items.length; for (; i < len; i++) { if (fn.call(scope || window, items[i], keys[i])) { return items[i]; } } return null; }, // find : function() { if (Ext.isDefined(Ext.global.console)) { Ext.global.console.warn('Ext.util.MixedCollection: find has been deprecated. Use findBy instead.'); } return this.findBy.apply(this, arguments); }, // /** * Inserts an item at the specified index in the collection. Fires the {@link #event-add} event when complete. * @param {Number} index The index to insert the item at. * @param {String} key The key to associate with the new item, or the item itself. * @param {Object} o (optional) If the second parameter was a key, the new item. * @return {Object} The item inserted. */ insert : function(index, key, obj){ var me = this, myKey = key, myObj = obj; if (arguments.length == 2) { myObj = myKey; myKey = me.getKey(myObj); } if (me.containsKey(myKey)) { me.suspendEvents(); me.removeAtKey(myKey); me.resumeEvents(); } if (index >= me.length) { return me.add(myKey, myObj); } me.generation++; me.length++; Ext.Array.splice(me.items, index, 0, myObj); if (typeof myKey != 'undefined' && myKey !== null) { me.map[myKey] = myObj; } Ext.Array.splice(me.keys, index, 0, myKey); if (me.hasListeners.add) { me.fireEvent('add', index, myObj, myKey); } return myObj; }, /** * Remove an item from the collection. * @param {Object} o The item to remove. * @return {Object} The item removed or false if no item was removed. */ remove : function(o) { this.generation++; return this.removeAt(this.indexOf(o)); }, /** * Remove all items in the passed array from the collection. * @param {Array} items An array of items to be removed. * @return {Ext.util.MixedCollection} this object */ removeAll : function(items) { items = [].concat(items); var i, iLen = items.length; for (i = 0; i < iLen; i++) { this.remove(items[i]); } return this; }, /** * Remove an item from a specified index in the collection. Fires the {@link #event-remove} event when complete. * @param {Number} index The index within the collection of the item to remove. * @return {Object} The item removed or false if no item was removed. */ removeAt : function(index) { var me = this, o, key; if (index < me.length && index >= 0) { me.length--; o = me.items[index]; Ext.Array.erase(me.items, index, 1); key = me.keys[index]; if (typeof key != 'undefined') { delete me.map[key]; } Ext.Array.erase(me.keys, index, 1); if (me.hasListeners.remove) { me.fireEvent('remove', o, key); } me.generation++; return o; } return false; }, /** * Removed an item associated with the passed key fom the collection. * @param {String} key The key of the item to remove. * @return {Object} The item removed or false if no item was removed. */ removeAtKey : function(key){ return this.removeAt(this.indexOfKey(key)); }, /** * Returns the number of items in the collection. * @return {Number} the number of items in the collection. */ getCount : function(){ return this.length; }, /** * Returns index within the collection of the passed Object. * @param {Object} o The item to find the index of. * @return {Number} index of the item. Returns -1 if not found. */ indexOf : function(o){ return Ext.Array.indexOf(this.items, o); }, /** * Returns index within the collection of the passed key. * @param {String} key The key to find the index of. * @return {Number} index of the key. */ indexOfKey : function(key){ return Ext.Array.indexOf(this.keys, key); }, /** * Returns the item associated with the passed key OR index. * Key has priority over index. This is the equivalent * of calling {@link #getByKey} first, then if nothing matched calling {@link #getAt}. * @param {String/Number} key The key or index of the item. * @return {Object} If the item is found, returns the item. If the item was not found, returns undefined. * If an item was found, but is a Class, returns null. */ get : function(key) { var me = this, mk = me.map[key], item = mk !== undefined ? mk : (typeof key == 'number') ? me.items[key] : undefined; return typeof item != 'function' || me.allowFunctions ? item : null; // for prototype! }, /** * Returns the item at the specified index. * @param {Number} index The index of the item. * @return {Object} The item at the specified index. */ 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. */ getByKey : function(key) { return this.map[key]; }, /** * Returns true if the collection contains the passed Object as an item. * @param {Object} o The Object to look for in the collection. * @return {Boolean} True if the collection contains the Object as an item. */ contains : function(o){ return typeof this.map[this.getKey(o)] != 'undefined'; }, /** * 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. */ containsKey : function(key){ return typeof this.map[key] != 'undefined'; }, /** * Removes all items from the collection. Fires the {@link #event-clear} event when complete. */ clear : function(){ var me = this; me.length = 0; me.items = []; me.keys = []; me.map = {}; me.generation++; if (me.hasListeners.clear) { me.fireEvent('clear'); } }, /** * Returns the first item in the collection. * @return {Object} the first item in the collection.. */ first : function() { return this.items[0]; }, /** * Returns the last item in the collection. * @return {Object} the last item in the collection.. */ last : function() { return this.items[this.length - 1]; }, /** * Collects all of the values of the given property and returns their sum * @param {String} property The property to sum by * @param {String} [root] '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 {Number} [start=0] The record index to start at * @param {Number} [end=-1] The record index to end at * @return {Number} The total */ sum: function(property, root, start, end) { var values = this.extractValues(property, root), length = values.length, sum = 0, i; start = start || 0; end = (end || end === 0) ? end : length - 1; for (i = start; i <= end; i++) { sum += values[i]; } return sum; }, /** * Collects unique values of a particular property in this MixedCollection * @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} allowBlank (optional) Pass true to allow null, undefined or empty string values * @return {Array} The unique values */ collect: function(property, root, allowNull) { var values = this.extractValues(property, root), length = values.length, hits = {}, unique = [], value, strValue, i; for (i = 0; i < length; i++) { value = values[i]; strValue = String(value); if ((allowNull || !Ext.isEmpty(value)) && !hits[strValue]) { hits[strValue] = true; unique.push(value); } } return unique; }, /** * @private * Extracts all of the given property values from the items in the MC. Mainly used as a supporting method for * functions like sum and collect. * @param {String} property The property to extract * @param {String} root (optional) 'root' property to extract the first argument from. This is used mainly when * extracting field data from Model instances, where the fields are stored inside the 'data' object * @return {Array} The extracted values */ extractValues: function(property, root) { var values = this.items; if (root) { values = Ext.Array.pluck(values, root); } return Ext.Array.pluck(values, property); }, /** * Returns a range of items in this collection * @param {Number} startIndex (optional) The starting index. Defaults to 0. * @param {Number} endIndex (optional) The ending index. Defaults to the last item. * @return {Array} An array of items */ getRange : function(start, end){ var me = this, items = me.items, range = [], i; if (items.length < 1) { return range; } start = start || 0; end = Math.min(typeof end == 'undefined' ? me.length - 1 : end, me.length - 1); if (start <= end) { for (i = start; i <= end; i++) { range[range.length] = items[i]; } } else { for (i = start; i >= end; i--) { range[range.length] = items[i]; } } return range; }, /** *

Filters the objects in this collection by a set of {@link Ext.util.Filter Filter}s, or by a single * property/value pair with optional parameters for substring matching and case sensitivity. See * {@link Ext.util.Filter Filter} for an example of using Filter objects (preferred). Alternatively, * MixedCollection can be easily filtered by property like this:


//create a simple store with a few people defined
var people = new Ext.util.MixedCollection();
people.addAll([
    {id: 1, age: 25, name: 'Ed'},
    {id: 2, age: 24, name: 'Tommy'},
    {id: 3, age: 24, name: 'Arne'},
    {id: 4, age: 26, name: 'Aaron'}
]);

//a new MixedCollection containing only the items where age == 24
var middleAged = people.filter('age', 24);
* * * @param {Ext.util.Filter[]/String} property A property on your objects, or an array of {@link Ext.util.Filter Filter} objects * @param {String/RegExp} value Either string that the property values * should start with or a RegExp to test against the property * @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. * @return {Ext.util.MixedCollection} The new filtered collection */ filter : function(property, value, anyMatch, caseSensitive) { var filters = [], filterFn; //support for the simple case of filtering by property/value if (Ext.isString(property)) { filters.push(new Ext.util.Filter({ property : property, value : value, anyMatch : anyMatch, caseSensitive: caseSensitive })); } else if (Ext.isArray(property) || property instanceof Ext.util.Filter) { filters = filters.concat(property); } //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 filterFn = function(record) { var isMatch = true, length = filters.length, i, filter, fn, scope; for (i = 0; i < length; i++) { filter = filters[i]; fn = filter.filterFn; scope = filter.scope; isMatch = isMatch && fn.call(scope, record); } return isMatch; }; return this.filterBy(filterFn); }, /** * Filter by a function. Returns a new 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 (this reference) in * which the function is executed. Defaults to this MixedCollection. * @return {Ext.util.MixedCollection} The new filtered collection */ filterBy : function(fn, scope) { var me = this, newMC = new this.self(), keys = me.keys, items = me.items, length = items.length, i; newMC.getKey = me.getKey; for (i = 0; i < length; i++) { if (fn.call(scope || me, items[i], keys[i])) { newMC.add(keys[i], items[i]); } } return newMC; }, /** * 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} [anyMatch=false] True to match any part of the string, not just the beginning. * @param {Boolean} [caseSensitive=false] True for case sensitive comparison. * @return {Number} The matched index or -1 */ findIndex : function(property, value, start, anyMatch, caseSensitive){ if(Ext.isEmpty(value, false)){ return -1; } value = this.createValueMatcher(value, anyMatch, caseSensitive); return this.findIndexBy(function(o){ return o && value.test(o[property]); }, null, start); }, /** * 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 {Mixed} fn.item The collection item. * @param {String} fn.key The key of collection item. * @param {Object} [scope] The scope (this reference) in which the function is executed. Defaults to this MixedCollection. * @param {Number} [start=0] The index to start searching at. * @return {Number} The matched index or -1 */ findIndexBy : function(fn, scope, start){ var me = this, keys = me.keys, items = me.items, i = start || 0, len = items.length; for (; i < len; i++) { if (fn.call(scope || me, items[i], keys[i])) { return i; } } return -1; }, /** * Returns a regular expression based on the given value and matching options. This is used internally for finding and filtering, * and by Ext.data.Store#filter * @private * @param {String} value The value to create the regex for. This is escaped using Ext.escapeRe * @param {Boolean} anyMatch True to allow any match - no regex start/end line anchors will be added. Defaults to false * @param {Boolean} caseSensitive True to make the regex case sensitive (adds 'i' switch to regex). Defaults to false. * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. Ignored if anyMatch is true. */ createValueMatcher : function(value, anyMatch, caseSensitive, exactMatch) { if (!value.exec) { // not a regex var er = Ext.String.escapeRegex; value = String(value); if (anyMatch === true) { value = er(value); } else { value = '^' + er(value); if (exactMatch === true) { value += '$'; } } value = new RegExp(value, caseSensitive ? '' : 'i'); } return value; }, /** * Creates a shallow copy of this collection * @return {Ext.util.MixedCollection} */ clone : function() { var me = this, copy = new this.self(), keys = me.keys, items = me.items, i = 0, len = items.length; for(; i < len; i++){ copy.add(keys[i], items[i]); } copy.getKey = me.getKey; return copy; } });