3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.util.AbstractMixedCollection
19 Ext.define('Ext.util.AbstractMixedCollection', {
20 requires: ['Ext.util.Filter'],
23 observable: 'Ext.util.Observable'
26 constructor: function(allowFunctions, keyFn) {
37 * Fires when the collection is cleared.
43 * Fires when an item is added to the collection.
44 * @param {Number} index The index at which the item was added.
45 * @param {Object} o The item added.
46 * @param {String} key The key associated with the added item.
52 * Fires when an item is replaced in the collection.
53 * @param {String} key he key associated with the new added.
54 * @param {Object} old The item being replaced.
55 * @param {Object} new The new item.
61 * Fires when an item is removed from the collection.
62 * @param {Object} o The item being removed.
63 * @param {String} key (optional) The key associated with the removed item.
68 me.allowFunctions = allowFunctions === true;
74 me.mixins.observable.constructor.call(me);
78 * @cfg {Boolean} allowFunctions Specify <tt>true</tt> if the {@link #addAll}
79 * function should add function references to the collection. Defaults to
82 allowFunctions : false,
85 * Adds an item to the collection. Fires the {@link #add} event when complete.
86 * @param {String} key <p>The key to associate with the item, or the new item.</p>
87 * <p>If a {@link #getKey} implementation was specified for this MixedCollection,
88 * or if the key of the stored items is in a property called <tt><b>id</b></tt>,
89 * the MixedCollection will be able to <i>derive</i> the key for the new item.
90 * In this case just pass the new item in this parameter.</p>
91 * @param {Object} o The item to add.
92 * @return {Object} The item added.
94 add : function(key, obj){
100 if (arguments.length == 1) {
102 myKey = me.getKey(myObj);
104 if (typeof myKey != 'undefined' && myKey !== null) {
106 if (typeof old != 'undefined') {
107 return me.replace(myKey, myObj);
109 me.map[myKey] = myObj;
112 me.items.push(myObj);
114 me.fireEvent('add', me.length - 1, myObj, myKey);
119 * MixedCollection has a generic way to fetch keys if you implement getKey. The default implementation
120 * simply returns <b><code>item.id</code></b> but you can provide your own implementation
121 * to return a different value as in the following examples:<pre><code>
123 var mc = new Ext.util.MixedCollection();
124 mc.add(someEl.dom.id, someEl);
125 mc.add(otherEl.dom.id, otherEl);
129 var mc = new Ext.util.MixedCollection();
130 mc.getKey = function(el){
136 // or via the constructor
137 var mc = new Ext.util.MixedCollection(false, function(el){
143 * @param {Object} item The item for which to find the key.
144 * @return {Object} The key for the passed item.
146 getKey : function(o){
151 * Replaces an item in the collection. Fires the {@link #replace} event when complete.
152 * @param {String} key <p>The key associated with the item to replace, or the replacement item.</p>
153 * <p>If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key
154 * of your stored items is in a property called <tt><b>id</b></tt>, then the MixedCollection
155 * will be able to <i>derive</i> the key of the replacement item. If you want to replace an item
156 * with one having the same key value, then just pass the replacement item in this parameter.</p>
157 * @param o {Object} o (optional) If the first parameter passed was a key, the item to associate
159 * @return {Object} The new item.
161 replace : function(key, o){
166 if (arguments.length == 1) {
171 if (typeof key == 'undefined' || key === null || typeof old == 'undefined') {
172 return me.add(key, o);
174 index = me.indexOfKey(key);
177 me.fireEvent('replace', key, old, o);
182 * Adds all elements of an Array or an Object to the collection.
183 * @param {Object/Array} objs An Object containing properties which will be added
184 * to the collection, or an Array of values, each of which are added to the collection.
185 * Functions references will be added to the collection if <code>{@link #allowFunctions}</code>
186 * has been set to <tt>true</tt>.
188 addAll : function(objs){
195 if (arguments.length > 1 || Ext.isArray(objs)) {
196 args = arguments.length > 1 ? arguments : objs;
197 for (len = args.length; i < len; i++) {
202 if (objs.hasOwnProperty(key)) {
203 if (me.allowFunctions || typeof objs[key] != 'function') {
204 me.add(key, objs[key]);
212 * Executes the specified function once for every item in the collection, passing the following arguments:
213 * <div class="mdetail-params"><ul>
214 * <li><b>item</b> : Mixed<p class="sub-desc">The collection item</p></li>
215 * <li><b>index</b> : Number<p class="sub-desc">The item's index</p></li>
216 * <li><b>length</b> : Number<p class="sub-desc">The total number of items in the collection</p></li>
218 * The function should return a boolean value. Returning false from the function will stop the iteration.
219 * @param {Function} fn The function to execute for each item.
220 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current item in the iteration.
222 each : function(fn, scope){
223 var items = [].concat(this.items), // each safe for removal
228 for (; i < len; i++) {
230 if (fn.call(scope || item, item, i, len) === false) {
237 * Executes the specified function once for every key in the collection, passing each
238 * key, and its associated item as the first two parameters.
239 * @param {Function} fn The function to execute for each item.
240 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.
242 eachKey : function(fn, scope){
243 var keys = this.keys,
248 for (; i < len; i++) {
249 fn.call(scope || window, keys[i], items[i], i, len);
254 * Returns the first item in the collection which elicits a true return value from the
255 * passed selection function.
256 * @param {Function} fn The selection function to execute for each item.
257 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.
258 * @return {Object} The first item in the collection which returned true from the selection function, or null if none was found
260 findBy : function(fn, scope) {
261 var keys = this.keys,
266 for (; i < len; i++) {
267 if (fn.call(scope || window, items[i], keys[i])) {
274 //<deprecated since="0.99">
276 if (Ext.isDefined(Ext.global.console)) {
277 Ext.global.console.warn('Ext.util.MixedCollection: find has been deprecated. Use findBy instead.');
279 return this.findBy.apply(this, arguments);
284 * Inserts an item at the specified index in the collection. Fires the {@link #add} event when complete.
285 * @param {Number} index The index to insert the item at.
286 * @param {String} key The key to associate with the new item, or the item itself.
287 * @param {Object} o (optional) If the second parameter was a key, the new item.
288 * @return {Object} The item inserted.
290 insert : function(index, key, obj){
295 if (arguments.length == 2) {
297 myKey = me.getKey(myObj);
299 if (me.containsKey(myKey)) {
301 me.removeAtKey(myKey);
304 if (index >= me.length) {
305 return me.add(myKey, myObj);
308 Ext.Array.splice(me.items, index, 0, myObj);
309 if (typeof myKey != 'undefined' && myKey !== null) {
310 me.map[myKey] = myObj;
312 Ext.Array.splice(me.keys, index, 0, myKey);
313 me.fireEvent('add', index, myObj, myKey);
318 * Remove an item from the collection.
319 * @param {Object} o The item to remove.
320 * @return {Object} The item removed or false if no item was removed.
322 remove : function(o){
323 return this.removeAt(this.indexOf(o));
327 * Remove all items in the passed array from the collection.
328 * @param {Array} items An array of items to be removed.
329 * @return {Ext.util.MixedCollection} this object
331 removeAll : function(items){
332 Ext.each(items || [], function(item) {
340 * Remove an item from a specified index in the collection. Fires the {@link #remove} event when complete.
341 * @param {Number} index The index within the collection of the item to remove.
342 * @return {Object} The item removed or false if no item was removed.
344 removeAt : function(index){
349 if (index < me.length && index >= 0) {
352 Ext.Array.erase(me.items, index, 1);
353 key = me.keys[index];
354 if (typeof key != 'undefined') {
357 Ext.Array.erase(me.keys, index, 1);
358 me.fireEvent('remove', o, key);
365 * Removed an item associated with the passed key fom the collection.
366 * @param {String} key The key of the item to remove.
367 * @return {Object} The item removed or false if no item was removed.
369 removeAtKey : function(key){
370 return this.removeAt(this.indexOfKey(key));
374 * Returns the number of items in the collection.
375 * @return {Number} the number of items in the collection.
377 getCount : function(){
382 * Returns index within the collection of the passed Object.
383 * @param {Object} o The item to find the index of.
384 * @return {Number} index of the item. Returns -1 if not found.
386 indexOf : function(o){
387 return Ext.Array.indexOf(this.items, o);
391 * Returns index within the collection of the passed key.
392 * @param {String} key The key to find the index of.
393 * @return {Number} index of the key.
395 indexOfKey : function(key){
396 return Ext.Array.indexOf(this.keys, key);
400 * Returns the item associated with the passed key OR index.
401 * Key has priority over index. This is the equivalent
402 * of calling {@link #getByKey} first, then if nothing matched calling {@link #getAt}.
403 * @param {String/Number} key The key or index of the item.
404 * @return {Object} If the item is found, returns the item. If the item was not found, returns <tt>undefined</tt>.
405 * If an item was found, but is a Class, returns <tt>null</tt>.
407 get : function(key) {
410 item = mk !== undefined ? mk : (typeof key == 'number') ? me.items[key] : undefined;
411 return typeof item != 'function' || me.allowFunctions ? item : null; // for prototype!
415 * Returns the item at the specified index.
416 * @param {Number} index The index of the item.
417 * @return {Object} The item at the specified index.
419 getAt : function(index) {
420 return this.items[index];
424 * Returns the item associated with the passed key.
425 * @param {String/Number} key The key of the item.
426 * @return {Object} The item associated with the passed key.
428 getByKey : function(key) {
429 return this.map[key];
433 * Returns true if the collection contains the passed Object as an item.
434 * @param {Object} o The Object to look for in the collection.
435 * @return {Boolean} True if the collection contains the Object as an item.
437 contains : function(o){
438 return Ext.Array.contains(this.items, o);
442 * Returns true if the collection contains the passed Object as a key.
443 * @param {String} key The key to look for in the collection.
444 * @return {Boolean} True if the collection contains the Object as a key.
446 containsKey : function(key){
447 return typeof this.map[key] != 'undefined';
451 * Removes all items from the collection. Fires the {@link #clear} event when complete.
460 me.fireEvent('clear');
464 * Returns the first item in the collection.
465 * @return {Object} the first item in the collection..
468 return this.items[0];
472 * Returns the last item in the collection.
473 * @return {Object} the last item in the collection..
476 return this.items[this.length - 1];
480 * Collects all of the values of the given property and returns their sum
481 * @param {String} property The property to sum by
482 * @param {String} [root] 'root' property to extract the first argument from. This is used mainly when
483 * summing fields in records, where the fields are all stored inside the 'data' object
484 * @param {Number} [start=0] The record index to start at
485 * @param {Number} [end=-1] The record index to end at
486 * @return {Number} The total
488 sum: function(property, root, start, end) {
489 var values = this.extractValues(property, root),
490 length = values.length,
495 end = (end || end === 0) ? end : length - 1;
497 for (i = start; i <= end; i++) {
505 * Collects unique values of a particular property in this MixedCollection
506 * @param {String} property The property to collect on
507 * @param {String} root (optional) 'root' property to extract the first argument from. This is used mainly when
508 * summing fields in records, where the fields are all stored inside the 'data' object
509 * @param {Boolean} allowBlank (optional) Pass true to allow null, undefined or empty string values
510 * @return {Array} The unique values
512 collect: function(property, root, allowNull) {
513 var values = this.extractValues(property, root),
514 length = values.length,
519 for (i = 0; i < length; i++) {
521 strValue = String(value);
523 if ((allowNull || !Ext.isEmpty(value)) && !hits[strValue]) {
524 hits[strValue] = true;
534 * Extracts all of the given property values from the items in the MC. Mainly used as a supporting method for
535 * functions like sum and collect.
536 * @param {String} property The property to extract
537 * @param {String} root (optional) 'root' property to extract the first argument from. This is used mainly when
538 * extracting field data from Model instances, where the fields are stored inside the 'data' object
539 * @return {Array} The extracted values
541 extractValues: function(property, root) {
542 var values = this.items;
545 values = Ext.Array.pluck(values, root);
548 return Ext.Array.pluck(values, property);
552 * Returns a range of items in this collection
553 * @param {Number} startIndex (optional) The starting index. Defaults to 0.
554 * @param {Number} endIndex (optional) The ending index. Defaults to the last item.
555 * @return {Array} An array of items
557 getRange : function(start, end){
563 if (items.length < 1) {
568 end = Math.min(typeof end == 'undefined' ? me.length - 1 : end, me.length - 1);
570 for (i = start; i <= end; i++) {
571 range[range.length] = items[i];
574 for (i = start; i >= end; i--) {
575 range[range.length] = items[i];
582 * <p>Filters the objects in this collection by a set of {@link Ext.util.Filter Filter}s, or by a single
583 * property/value pair with optional parameters for substring matching and case sensitivity. See
584 * {@link Ext.util.Filter Filter} for an example of using Filter objects (preferred). Alternatively,
585 * MixedCollection can be easily filtered by property like this:</p>
587 //create a simple store with a few people defined
588 var people = new Ext.util.MixedCollection();
590 {id: 1, age: 25, name: 'Ed'},
591 {id: 2, age: 24, name: 'Tommy'},
592 {id: 3, age: 24, name: 'Arne'},
593 {id: 4, age: 26, name: 'Aaron'}
596 //a new MixedCollection containing only the items where age == 24
597 var middleAged = people.filter('age', 24);
601 * @param {Ext.util.Filter[]/String} property A property on your objects, or an array of {@link Ext.util.Filter Filter} objects
602 * @param {String/RegExp} value Either string that the property values
603 * should start with or a RegExp to test against the property
604 * @param {Boolean} [anyMatch=false] True to match any part of the string, not just the beginning
605 * @param {Boolean} [caseSensitive=false] True for case sensitive comparison.
606 * @return {Ext.util.MixedCollection} The new filtered collection
608 filter : function(property, value, anyMatch, caseSensitive) {
612 //support for the simple case of filtering by property/value
613 if (Ext.isString(property)) {
614 filters.push(Ext.create('Ext.util.Filter', {
618 caseSensitive: caseSensitive
620 } else if (Ext.isArray(property) || property instanceof Ext.util.Filter) {
621 filters = filters.concat(property);
624 //at this point we have an array of zero or more Ext.util.Filter objects to filter with,
625 //so here we construct a function that combines these filters by ANDing them together
626 filterFn = function(record) {
628 length = filters.length,
631 for (i = 0; i < length; i++) {
632 var filter = filters[i],
633 fn = filter.filterFn,
634 scope = filter.scope;
636 isMatch = isMatch && fn.call(scope, record);
642 return this.filterBy(filterFn);
646 * Filter by a function. Returns a <i>new</i> collection that has been filtered.
647 * The passed function will be called with each object in the collection.
648 * If the function returns true, the value is included otherwise it is filtered.
649 * @param {Function} fn The function to be called, it will receive the args o (the object), k (the key)
650 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this MixedCollection.
651 * @return {Ext.util.MixedCollection} The new filtered collection
653 filterBy : function(fn, scope) {
655 newMC = new this.self(),
658 length = items.length,
661 newMC.getKey = me.getKey;
663 for (i = 0; i < length; i++) {
664 if (fn.call(scope || me, items[i], keys[i])) {
665 newMC.add(keys[i], items[i]);
673 * Finds the index of the first matching object in this collection by a specific property/value.
674 * @param {String} property The name of a property on your objects.
675 * @param {String/RegExp} value A string that the property values
676 * should start with or a RegExp to test against the property.
677 * @param {Number} [start=0] The index to start searching at.
678 * @param {Boolean} [anyMatch=false] True to match any part of the string, not just the beginning.
679 * @param {Boolean} [caseSensitive=false] True for case sensitive comparison.
680 * @return {Number} The matched index or -1
682 findIndex : function(property, value, start, anyMatch, caseSensitive){
683 if(Ext.isEmpty(value, false)){
686 value = this.createValueMatcher(value, anyMatch, caseSensitive);
687 return this.findIndexBy(function(o){
688 return o && value.test(o[property]);
693 * Find the index of the first matching object in this collection by a function.
694 * If the function returns <i>true</i> it is considered a match.
695 * @param {Function} fn The function to be called, it will receive the args o (the object), k (the key).
696 * @param {Object} [scope] The scope (<code>this</code> reference) in which the function is executed. Defaults to this MixedCollection.
697 * @param {Number} [start=0] The index to start searching at.
698 * @return {Number} The matched index or -1
700 findIndexBy : function(fn, scope, start){
707 for (; i < len; i++) {
708 if (fn.call(scope || me, items[i], keys[i])) {
716 * Returns a regular expression based on the given value and matching options. This is used internally for finding and filtering,
717 * and by Ext.data.Store#filter
719 * @param {String} value The value to create the regex for. This is escaped using Ext.escapeRe
720 * @param {Boolean} anyMatch True to allow any match - no regex start/end line anchors will be added. Defaults to false
721 * @param {Boolean} caseSensitive True to make the regex case sensitive (adds 'i' switch to regex). Defaults to false.
722 * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. Ignored if anyMatch is true.
724 createValueMatcher : function(value, anyMatch, caseSensitive, exactMatch) {
725 if (!value.exec) { // not a regex
726 var er = Ext.String.escapeRegex;
727 value = String(value);
729 if (anyMatch === true) {
732 value = '^' + er(value);
733 if (exactMatch === true) {
737 value = new RegExp(value, caseSensitive ? '' : 'i');
743 * Creates a shallow copy of this collection
744 * @return {Ext.util.MixedCollection}
748 copy = new this.self(),
755 copy.add(keys[i], items[i]);
757 copy.getKey = me.getKey;