/** * @private */ Ext.define('Ext.util.AbstractMixedCollection', { requires: ['Ext.util.Filter'], mixins: { observable: 'Ext.mixin.Observable' }, /** * @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. */ /** * Creates new MixedCollection. * @param {Boolean} [allowFunctions=false] Specify `true` if the {@link #addAll} * function should add function references to the collection. * @param {Function} [keyFn] A function that can accept an item of the type(s) stored in this MixedCollection * and return the key value for that item. This is used when available to look up the key on items that * were passed without an explicit key parameter to a MixedCollection method. Passing this parameter is * equivalent to providing an implementation for the {@link #getKey} method. */ constructor: function(allowFunctions, keyFn) { var me = this; me.items = []; me.map = {}; me.keys = []; me.length = 0; 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. */ allowFunctions : false, /** * Adds an item to the collection. Fires the {@link #event-add} event when complete. * @param {String} 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} obj 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.length++; me.items.push(myObj); me.keys.push(myKey); 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 {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); } index = me.indexOfKey(key); me.items[index] = o; me.map[key] = o; 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. * * @param {Function} fn The function to execute for each item. * @param {Mixed} fn.item The collection item. * @param {Number} fn.index The item's index. * @param {Number} fn.length The total number of items in the collection. * @param {Boolean} fn.return Returning `false` will stop the iteration. * @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 {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 {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. */ 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; }, /** * 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} [obj] 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.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); 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){ 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){ Ext.each(items || [], function(item) { this.remove(item); }, this); 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/Boolean} 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); me.fireEvent('remove', o, key); return o; } return false; }, /** * Removed an item associated with the passed key from the collection. * @param {String} key The key of the item to remove. * @return {Object/Boolean} 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} The 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 Ext.Array.contains(this.items, o); }, /** * 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.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] 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 {Number} [start=0] (optional) The record index to start at. * @param {Number} [end=-1] (optional) 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} [allowNull] 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 MixedCollection. 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} [start=0] The starting index. * @param {Number} [end=-1] The ending index. * @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 (optional) `true` to match any part of the string, not just the beginning * @param {Boolean} [caseSensitive=false] (optional) `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(Ext.create('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; for (i = 0; i < length; i++) { var filter = filters[i], fn = filter.getFilterFn(), scope = filter.getScope(); 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, it will receive the args `o` (the object), `k` (the key) * @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] (optional) The index to start searching at. * @param {Boolean} anyMatch (optional) `true` to match any part of the string, not just the beginning. * @param {Boolean} caseSensitive (optional) `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, it will receive the args `o` (the object), `k` (the key). * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this MixedCollection. * @param {Number} [start=0] (optional) 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=false] `true` to allow any match - no regex start/end line anchors will be added. * @param {Boolean} [caseSensitive=false] `true` to make the regex case sensitive (adds 'i' switch to regex). * @param {Boolean} [exactMatch=false] `true` to force exact match (^ and $ characters added to the regex). 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; } });