X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/util/AbstractMixedCollection.js diff --git a/src/util/AbstractMixedCollection.js b/src/util/AbstractMixedCollection.js new file mode 100644 index 00000000..ba3c83ff --- /dev/null +++ b/src/util/AbstractMixedCollection.js @@ -0,0 +1,745 @@ +/** + * @class Ext.util.AbstractMixedCollection + */ +Ext.define('Ext.util.AbstractMixedCollection', { + requires: ['Ext.util.Filter'], + + mixins: { + observable: 'Ext.util.Observable' + }, + + constructor: function(allowFunctions, keyFn) { + var me = this; + + me.items = []; + me.map = {}; + me.keys = []; + me.length = 0; + + me.addEvents( + /** + * @event clear + * Fires when the collection is cleared. + */ + 'clear', + + /** + * @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. + */ + 'add', + + /** + * @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. + */ + 'replace', + + /** + * @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. + */ + 'remove' + ); + + 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 #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} 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.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 #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); + } + 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, passing the following arguments: + *
+ * 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 {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; + }, + + // + 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 #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.length++; + me.items.splice(index, 0, myObj); + if (typeof myKey != 'undefined' && myKey !== null) { + me.map[myKey] = myObj; + } + me.keys.splice(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 #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]; + me.items.splice(index, 1); + key = me.keys[index]; + if (typeof key != 'undefined') { + delete me.map[key]; + } + me.keys.splice(index, 1); + me.fireEvent('remove', o, key); + 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 #key} 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 #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 (optional) The record index to start at (defaults to 0) + * @param {Number} end (optional) The record index to end at (defaults to -1) + * @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 {Array/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 (optional) True for case sensitive comparison (defaults to False). + * @return {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.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, 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 {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 (optional) The index to start searching at (defaults to 0). + * @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 (optional) The index to start searching at (defaults to 0). + * @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 {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; + } +});