Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / util / AbstractMixedCollection.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
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.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.util.AbstractMixedCollection
17  * @private
18  */
19 Ext.define('Ext.util.AbstractMixedCollection', {
20     requires: ['Ext.util.Filter'],
21
22     mixins: {
23         observable: 'Ext.util.Observable'
24     },
25
26     constructor: function(allowFunctions, keyFn) {
27         var me = this;
28
29         me.items = [];
30         me.map = {};
31         me.keys = [];
32         me.length = 0;
33
34         me.addEvents(
35             /**
36              * @event clear
37              * Fires when the collection is cleared.
38              */
39             'clear',
40
41             /**
42              * @event add
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.
47              */
48             'add',
49
50             /**
51              * @event replace
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.
56              */
57             'replace',
58
59             /**
60              * @event remove
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.
64              */
65             'remove'
66         );
67
68         me.allowFunctions = allowFunctions === true;
69
70         if (keyFn) {
71             me.getKey = keyFn;
72         }
73
74         me.mixins.observable.constructor.call(me);
75     },
76
77     /**
78      * @cfg {Boolean} allowFunctions Specify <tt>true</tt> if the {@link #addAll}
79      * function should add function references to the collection. Defaults to
80      * <tt>false</tt>.
81      */
82     allowFunctions : false,
83
84     /**
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.
93      */
94     add : function(key, obj){
95         var me = this,
96             myObj = obj,
97             myKey = key,
98             old;
99
100         if (arguments.length == 1) {
101             myObj = myKey;
102             myKey = me.getKey(myObj);
103         }
104         if (typeof myKey != 'undefined' && myKey !== null) {
105             old = me.map[myKey];
106             if (typeof old != 'undefined') {
107                 return me.replace(myKey, myObj);
108             }
109             me.map[myKey] = myObj;
110         }
111         me.length++;
112         me.items.push(myObj);
113         me.keys.push(myKey);
114         me.fireEvent('add', me.length - 1, myObj, myKey);
115         return myObj;
116     },
117
118     /**
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>
122 // normal way
123 var mc = new Ext.util.MixedCollection();
124 mc.add(someEl.dom.id, someEl);
125 mc.add(otherEl.dom.id, otherEl);
126 //and so on
127
128 // using getKey
129 var mc = new Ext.util.MixedCollection();
130 mc.getKey = function(el){
131    return el.dom.id;
132 };
133 mc.add(someEl);
134 mc.add(otherEl);
135
136 // or via the constructor
137 var mc = new Ext.util.MixedCollection(false, function(el){
138    return el.dom.id;
139 });
140 mc.add(someEl);
141 mc.add(otherEl);
142      * </code></pre>
143      * @param {Object} item The item for which to find the key.
144      * @return {Object} The key for the passed item.
145      */
146     getKey : function(o){
147          return o.id;
148     },
149
150     /**
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
158      * with that key.
159      * @return {Object}  The new item.
160      */
161     replace : function(key, o){
162         var me = this,
163             old,
164             index;
165
166         if (arguments.length == 1) {
167             o = arguments[0];
168             key = me.getKey(o);
169         }
170         old = me.map[key];
171         if (typeof key == 'undefined' || key === null || typeof old == 'undefined') {
172              return me.add(key, o);
173         }
174         index = me.indexOfKey(key);
175         me.items[index] = o;
176         me.map[key] = o;
177         me.fireEvent('replace', key, old, o);
178         return o;
179     },
180
181     /**
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>.
187      */
188     addAll : function(objs){
189         var me = this,
190             i = 0,
191             args,
192             len,
193             key;
194
195         if (arguments.length > 1 || Ext.isArray(objs)) {
196             args = arguments.length > 1 ? arguments : objs;
197             for (len = args.length; i < len; i++) {
198                 me.add(args[i]);
199             }
200         } else {
201             for (key in objs) {
202                 if (objs.hasOwnProperty(key)) {
203                     if (me.allowFunctions || typeof objs[key] != 'function') {
204                         me.add(key, objs[key]);
205                     }
206                 }
207             }
208         }
209     },
210
211     /**
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>
217      * </ul></div>
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.
221      */
222     each : function(fn, scope){
223         var items = [].concat(this.items), // each safe for removal
224             i = 0,
225             len = items.length,
226             item;
227
228         for (; i < len; i++) {
229             item = items[i];
230             if (fn.call(scope || item, item, i, len) === false) {
231                 break;
232             }
233         }
234     },
235
236     /**
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.
241      */
242     eachKey : function(fn, scope){
243         var keys = this.keys,
244             items = this.items,
245             i = 0,
246             len = keys.length;
247
248         for (; i < len; i++) {
249             fn.call(scope || window, keys[i], items[i], i, len);
250         }
251     },
252
253     /**
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
259      */
260     findBy : function(fn, scope) {
261         var keys = this.keys,
262             items = this.items,
263             i = 0,
264             len = items.length;
265
266         for (; i < len; i++) {
267             if (fn.call(scope || window, items[i], keys[i])) {
268                 return items[i];
269             }
270         }
271         return null;
272     },
273
274     //<deprecated since="0.99">
275     find : function() {
276         if (Ext.isDefined(Ext.global.console)) {
277             Ext.global.console.warn('Ext.util.MixedCollection: find has been deprecated. Use findBy instead.');
278         }
279         return this.findBy.apply(this, arguments);
280     },
281     //</deprecated>
282
283     /**
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.
289      */
290     insert : function(index, key, obj){
291         var me = this,
292             myKey = key,
293             myObj = obj;
294
295         if (arguments.length == 2) {
296             myObj = myKey;
297             myKey = me.getKey(myObj);
298         }
299         if (me.containsKey(myKey)) {
300             me.suspendEvents();
301             me.removeAtKey(myKey);
302             me.resumeEvents();
303         }
304         if (index >= me.length) {
305             return me.add(myKey, myObj);
306         }
307         me.length++;
308         Ext.Array.splice(me.items, index, 0, myObj);
309         if (typeof myKey != 'undefined' && myKey !== null) {
310             me.map[myKey] = myObj;
311         }
312         Ext.Array.splice(me.keys, index, 0, myKey);
313         me.fireEvent('add', index, myObj, myKey);
314         return myObj;
315     },
316
317     /**
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.
321      */
322     remove : function(o){
323         return this.removeAt(this.indexOf(o));
324     },
325
326     /**
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
330      */
331     removeAll : function(items){
332         Ext.each(items || [], function(item) {
333             this.remove(item);
334         }, this);
335
336         return this;
337     },
338
339     /**
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.
343      */
344     removeAt : function(index){
345         var me = this,
346             o,
347             key;
348
349         if (index < me.length && index >= 0) {
350             me.length--;
351             o = me.items[index];
352             Ext.Array.erase(me.items, index, 1);
353             key = me.keys[index];
354             if (typeof key != 'undefined') {
355                 delete me.map[key];
356             }
357             Ext.Array.erase(me.keys, index, 1);
358             me.fireEvent('remove', o, key);
359             return o;
360         }
361         return false;
362     },
363
364     /**
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.
368      */
369     removeAtKey : function(key){
370         return this.removeAt(this.indexOfKey(key));
371     },
372
373     /**
374      * Returns the number of items in the collection.
375      * @return {Number} the number of items in the collection.
376      */
377     getCount : function(){
378         return this.length;
379     },
380
381     /**
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.
385      */
386     indexOf : function(o){
387         return Ext.Array.indexOf(this.items, o);
388     },
389
390     /**
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.
394      */
395     indexOfKey : function(key){
396         return Ext.Array.indexOf(this.keys, key);
397     },
398
399     /**
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>.
406      */
407     get : function(key) {
408         var me = this,
409             mk = me.map[key],
410             item = mk !== undefined ? mk : (typeof key == 'number') ? me.items[key] : undefined;
411         return typeof item != 'function' || me.allowFunctions ? item : null; // for prototype!
412     },
413
414     /**
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.
418      */
419     getAt : function(index) {
420         return this.items[index];
421     },
422
423     /**
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.
427      */
428     getByKey : function(key) {
429         return this.map[key];
430     },
431
432     /**
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.
436      */
437     contains : function(o){
438         return Ext.Array.contains(this.items, o);
439     },
440
441     /**
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.
445      */
446     containsKey : function(key){
447         return typeof this.map[key] != 'undefined';
448     },
449
450     /**
451      * Removes all items from the collection.  Fires the {@link #clear} event when complete.
452      */
453     clear : function(){
454         var me = this;
455
456         me.length = 0;
457         me.items = [];
458         me.keys = [];
459         me.map = {};
460         me.fireEvent('clear');
461     },
462
463     /**
464      * Returns the first item in the collection.
465      * @return {Object} the first item in the collection..
466      */
467     first : function() {
468         return this.items[0];
469     },
470
471     /**
472      * Returns the last item in the collection.
473      * @return {Object} the last item in the collection..
474      */
475     last : function() {
476         return this.items[this.length - 1];
477     },
478
479     /**
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
487      */
488     sum: function(property, root, start, end) {
489         var values = this.extractValues(property, root),
490             length = values.length,
491             sum    = 0,
492             i;
493
494         start = start || 0;
495         end   = (end || end === 0) ? end : length - 1;
496
497         for (i = start; i <= end; i++) {
498             sum += values[i];
499         }
500
501         return sum;
502     },
503
504     /**
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
511      */
512     collect: function(property, root, allowNull) {
513         var values = this.extractValues(property, root),
514             length = values.length,
515             hits   = {},
516             unique = [],
517             value, strValue, i;
518
519         for (i = 0; i < length; i++) {
520             value = values[i];
521             strValue = String(value);
522
523             if ((allowNull || !Ext.isEmpty(value)) && !hits[strValue]) {
524                 hits[strValue] = true;
525                 unique.push(value);
526             }
527         }
528
529         return unique;
530     },
531
532     /**
533      * @private
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
540      */
541     extractValues: function(property, root) {
542         var values = this.items;
543
544         if (root) {
545             values = Ext.Array.pluck(values, root);
546         }
547
548         return Ext.Array.pluck(values, property);
549     },
550
551     /**
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
556      */
557     getRange : function(start, end){
558         var me = this,
559             items = me.items,
560             range = [],
561             i;
562
563         if (items.length < 1) {
564             return range;
565         }
566
567         start = start || 0;
568         end = Math.min(typeof end == 'undefined' ? me.length - 1 : end, me.length - 1);
569         if (start <= end) {
570             for (i = start; i <= end; i++) {
571                 range[range.length] = items[i];
572             }
573         } else {
574             for (i = start; i >= end; i--) {
575                 range[range.length] = items[i];
576             }
577         }
578         return range;
579     },
580
581     /**
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>
586 <pre><code>
587 //create a simple store with a few people defined
588 var people = new Ext.util.MixedCollection();
589 people.addAll([
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'}
594 ]);
595
596 //a new MixedCollection containing only the items where age == 24
597 var middleAged = people.filter('age', 24);
598 </code></pre>
599      *
600      *
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
607      */
608     filter : function(property, value, anyMatch, caseSensitive) {
609         var filters = [],
610             filterFn;
611
612         //support for the simple case of filtering by property/value
613         if (Ext.isString(property)) {
614             filters.push(Ext.create('Ext.util.Filter', {
615                 property     : property,
616                 value        : value,
617                 anyMatch     : anyMatch,
618                 caseSensitive: caseSensitive
619             }));
620         } else if (Ext.isArray(property) || property instanceof Ext.util.Filter) {
621             filters = filters.concat(property);
622         }
623
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) {
627             var isMatch = true,
628                 length = filters.length,
629                 i;
630
631             for (i = 0; i < length; i++) {
632                 var filter = filters[i],
633                     fn     = filter.filterFn,
634                     scope  = filter.scope;
635
636                 isMatch = isMatch && fn.call(scope, record);
637             }
638
639             return isMatch;
640         };
641
642         return this.filterBy(filterFn);
643     },
644
645     /**
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
652      */
653     filterBy : function(fn, scope) {
654         var me = this,
655             newMC  = new this.self(),
656             keys   = me.keys,
657             items  = me.items,
658             length = items.length,
659             i;
660
661         newMC.getKey = me.getKey;
662
663         for (i = 0; i < length; i++) {
664             if (fn.call(scope || me, items[i], keys[i])) {
665                 newMC.add(keys[i], items[i]);
666             }
667         }
668
669         return newMC;
670     },
671
672     /**
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
681      */
682     findIndex : function(property, value, start, anyMatch, caseSensitive){
683         if(Ext.isEmpty(value, false)){
684             return -1;
685         }
686         value = this.createValueMatcher(value, anyMatch, caseSensitive);
687         return this.findIndexBy(function(o){
688             return o && value.test(o[property]);
689         }, null, start);
690     },
691
692     /**
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
699      */
700     findIndexBy : function(fn, scope, start){
701         var me = this,
702             keys = me.keys,
703             items = me.items,
704             i = start || 0,
705             len = items.length;
706
707         for (; i < len; i++) {
708             if (fn.call(scope || me, items[i], keys[i])) {
709                 return i;
710             }
711         }
712         return -1;
713     },
714
715     /**
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
718      * @private
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.
723      */
724     createValueMatcher : function(value, anyMatch, caseSensitive, exactMatch) {
725         if (!value.exec) { // not a regex
726             var er = Ext.String.escapeRegex;
727             value = String(value);
728
729             if (anyMatch === true) {
730                 value = er(value);
731             } else {
732                 value = '^' + er(value);
733                 if (exactMatch === true) {
734                     value += '$';
735                 }
736             }
737             value = new RegExp(value, caseSensitive ? '' : 'i');
738         }
739         return value;
740     },
741
742     /**
743      * Creates a shallow copy of this collection
744      * @return {Ext.util.MixedCollection}
745      */
746     clone : function() {
747         var me = this,
748             copy = new this.self(),
749             keys = me.keys,
750             items = me.items,
751             i = 0,
752             len = items.length;
753
754         for(; i < len; i++){
755             copy.add(keys[i], items[i]);
756         }
757         copy.getKey = me.getKey;
758         return copy;
759     }
760 });
761