Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / src / util / AbstractMixedCollection.js
1 /**
2  * @class Ext.util.AbstractMixedCollection
3  */
4 Ext.define('Ext.util.AbstractMixedCollection', {
5     requires: ['Ext.util.Filter'],
6     
7     mixins: {
8         observable: 'Ext.util.Observable'
9     },
10
11     constructor: function(allowFunctions, keyFn) {
12         var me = this;
13
14         me.items = [];
15         me.map = {};
16         me.keys = [];
17         me.length = 0;
18
19         me.addEvents(
20             /**
21              * @event clear
22              * Fires when the collection is cleared.
23              */
24             'clear',
25
26             /**
27              * @event add
28              * Fires when an item is added to the collection.
29              * @param {Number} index The index at which the item was added.
30              * @param {Object} o The item added.
31              * @param {String} key The key associated with the added item.
32              */
33             'add',
34
35             /**
36              * @event replace
37              * Fires when an item is replaced in the collection.
38              * @param {String} key he key associated with the new added.
39              * @param {Object} old The item being replaced.
40              * @param {Object} new The new item.
41              */
42             'replace',
43
44             /**
45              * @event remove
46              * Fires when an item is removed from the collection.
47              * @param {Object} o The item being removed.
48              * @param {String} key (optional) The key associated with the removed item.
49              */
50             'remove'
51         );
52
53         me.allowFunctions = allowFunctions === true;
54
55         if (keyFn) {
56             me.getKey = keyFn;
57         }
58
59         me.mixins.observable.constructor.call(me);
60     },
61     
62     /**
63      * @cfg {Boolean} allowFunctions Specify <tt>true</tt> if the {@link #addAll}
64      * function should add function references to the collection. Defaults to
65      * <tt>false</tt>.
66      */
67     allowFunctions : false,
68
69     /**
70      * Adds an item to the collection. Fires the {@link #add} event when complete.
71      * @param {String} key <p>The key to associate with the item, or the new item.</p>
72      * <p>If a {@link #getKey} implementation was specified for this MixedCollection,
73      * or if the key of the stored items is in a property called <tt><b>id</b></tt>,
74      * the MixedCollection will be able to <i>derive</i> the key for the new item.
75      * In this case just pass the new item in this parameter.</p>
76      * @param {Object} o The item to add.
77      * @return {Object} The item added.
78      */
79     add : function(key, obj){
80         var me = this,
81             myObj = obj,
82             myKey = key,
83             old;
84
85         if (arguments.length == 1) {
86             myObj = myKey;
87             myKey = me.getKey(myObj);
88         }
89         if (typeof myKey != 'undefined' && myKey !== null) {
90             old = me.map[myKey];
91             if (typeof old != 'undefined') {
92                 return me.replace(myKey, myObj);
93             }
94             me.map[myKey] = myObj;
95         }
96         me.length++;
97         me.items.push(myObj);
98         me.keys.push(myKey);
99         me.fireEvent('add', me.length - 1, myObj, myKey);
100         return myObj;
101     },
102
103     /**
104       * MixedCollection has a generic way to fetch keys if you implement getKey.  The default implementation
105       * simply returns <b><code>item.id</code></b> but you can provide your own implementation
106       * to return a different value as in the following examples:<pre><code>
107 // normal way
108 var mc = new Ext.util.MixedCollection();
109 mc.add(someEl.dom.id, someEl);
110 mc.add(otherEl.dom.id, otherEl);
111 //and so on
112
113 // using getKey
114 var mc = new Ext.util.MixedCollection();
115 mc.getKey = function(el){
116    return el.dom.id;
117 };
118 mc.add(someEl);
119 mc.add(otherEl);
120
121 // or via the constructor
122 var mc = new Ext.util.MixedCollection(false, function(el){
123    return el.dom.id;
124 });
125 mc.add(someEl);
126 mc.add(otherEl);
127      * </code></pre>
128      * @param {Object} item The item for which to find the key.
129      * @return {Object} The key for the passed item.
130      */
131     getKey : function(o){
132          return o.id;
133     },
134
135     /**
136      * Replaces an item in the collection. Fires the {@link #replace} event when complete.
137      * @param {String} key <p>The key associated with the item to replace, or the replacement item.</p>
138      * <p>If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key
139      * of your stored items is in a property called <tt><b>id</b></tt>, then the MixedCollection
140      * will be able to <i>derive</i> the key of the replacement item. If you want to replace an item
141      * with one having the same key value, then just pass the replacement item in this parameter.</p>
142      * @param o {Object} o (optional) If the first parameter passed was a key, the item to associate
143      * with that key.
144      * @return {Object}  The new item.
145      */
146     replace : function(key, o){
147         var me = this,
148             old,
149             index;
150
151         if (arguments.length == 1) {
152             o = arguments[0];
153             key = me.getKey(o);
154         }
155         old = me.map[key];
156         if (typeof key == 'undefined' || key === null || typeof old == 'undefined') {
157              return me.add(key, o);
158         }
159         index = me.indexOfKey(key);
160         me.items[index] = o;
161         me.map[key] = o;
162         me.fireEvent('replace', key, old, o);
163         return o;
164     },
165
166     /**
167      * Adds all elements of an Array or an Object to the collection.
168      * @param {Object/Array} objs An Object containing properties which will be added
169      * to the collection, or an Array of values, each of which are added to the collection.
170      * Functions references will be added to the collection if <code>{@link #allowFunctions}</code>
171      * has been set to <tt>true</tt>.
172      */
173     addAll : function(objs){
174         var me = this,
175             i = 0,
176             args,
177             len,
178             key;
179
180         if (arguments.length > 1 || Ext.isArray(objs)) {
181             args = arguments.length > 1 ? arguments : objs;
182             for (len = args.length; i < len; i++) {
183                 me.add(args[i]);
184             }
185         } else {
186             for (key in objs) {
187                 if (objs.hasOwnProperty(key)) {
188                     if (me.allowFunctions || typeof objs[key] != 'function') {
189                         me.add(key, objs[key]);
190                     }
191                 }
192             }
193         }
194     },
195
196     /**
197      * Executes the specified function once for every item in the collection, passing the following arguments:
198      * <div class="mdetail-params"><ul>
199      * <li><b>item</b> : Mixed<p class="sub-desc">The collection item</p></li>
200      * <li><b>index</b> : Number<p class="sub-desc">The item's index</p></li>
201      * <li><b>length</b> : Number<p class="sub-desc">The total number of items in the collection</p></li>
202      * </ul></div>
203      * The function should return a boolean value. Returning false from the function will stop the iteration.
204      * @param {Function} fn The function to execute for each item.
205      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current item in the iteration.
206      */
207     each : function(fn, scope){
208         var items = [].concat(this.items), // each safe for removal
209             i = 0,
210             len = items.length,
211             item;
212
213         for (; i < len; i++) {
214             item = items[i];
215             if (fn.call(scope || item, item, i, len) === false) {
216                 break;
217             }
218         }
219     },
220
221     /**
222      * Executes the specified function once for every key in the collection, passing each
223      * key, and its associated item as the first two parameters.
224      * @param {Function} fn The function to execute for each item.
225      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.
226      */
227     eachKey : function(fn, scope){
228         var keys = this.keys,
229             items = this.items,
230             i = 0,
231             len = keys.length;
232
233         for (; i < len; i++) {
234             fn.call(scope || window, keys[i], items[i], i, len);
235         }
236     },
237
238     /**
239      * Returns the first item in the collection which elicits a true return value from the
240      * passed selection function.
241      * @param {Function} fn The selection function to execute for each item.
242      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.
243      * @return {Object} The first item in the collection which returned true from the selection function.
244      */
245     findBy : function(fn, scope) {
246         var keys = this.keys,
247             items = this.items,
248             i = 0,
249             len = items.length;
250
251         for (; i < len; i++) {
252             if (fn.call(scope || window, items[i], keys[i])) {
253                 return items[i];
254             }
255         }
256         return null;
257     },
258
259     //<deprecated since="0.99">
260     find : function() {
261         if (Ext.isDefined(Ext.global.console)) {
262             Ext.global.console.warn('Ext.util.MixedCollection: find has been deprecated. Use findBy instead.');
263         }
264         return this.findBy.apply(this, arguments);
265     },
266     //</deprecated>
267
268     /**
269      * Inserts an item at the specified index in the collection. Fires the {@link #add} event when complete.
270      * @param {Number} index The index to insert the item at.
271      * @param {String} key The key to associate with the new item, or the item itself.
272      * @param {Object} o (optional) If the second parameter was a key, the new item.
273      * @return {Object} The item inserted.
274      */
275     insert : function(index, key, obj){
276         var me = this,
277             myKey = key,
278             myObj = obj;
279
280         if (arguments.length == 2) {
281             myObj = myKey;
282             myKey = me.getKey(myObj);
283         }
284         if (me.containsKey(myKey)) {
285             me.suspendEvents();
286             me.removeAtKey(myKey);
287             me.resumeEvents();
288         }
289         if (index >= me.length) {
290             return me.add(myKey, myObj);
291         }
292         me.length++;
293         me.items.splice(index, 0, myObj);
294         if (typeof myKey != 'undefined' && myKey !== null) {
295             me.map[myKey] = myObj;
296         }
297         me.keys.splice(index, 0, myKey);
298         me.fireEvent('add', index, myObj, myKey);
299         return myObj;
300     },
301
302     /**
303      * Remove an item from the collection.
304      * @param {Object} o The item to remove.
305      * @return {Object} The item removed or false if no item was removed.
306      */
307     remove : function(o){
308         return this.removeAt(this.indexOf(o));
309     },
310
311     /**
312      * Remove all items in the passed array from the collection.
313      * @param {Array} items An array of items to be removed.
314      * @return {Ext.util.MixedCollection} this object
315      */
316     removeAll : function(items){
317         Ext.each(items || [], function(item) {
318             this.remove(item);
319         }, this);
320
321         return this;
322     },
323
324     /**
325      * Remove an item from a specified index in the collection. Fires the {@link #remove} event when complete.
326      * @param {Number} index The index within the collection of the item to remove.
327      * @return {Object} The item removed or false if no item was removed.
328      */
329     removeAt : function(index){
330         var me = this,
331             o,
332             key;
333
334         if (index < me.length && index >= 0) {
335             me.length--;
336             o = me.items[index];
337             me.items.splice(index, 1);
338             key = me.keys[index];
339             if (typeof key != 'undefined') {
340                 delete me.map[key];
341             }
342             me.keys.splice(index, 1);
343             me.fireEvent('remove', o, key);
344             return o;
345         }
346         return false;
347     },
348
349     /**
350      * Removed an item associated with the passed key fom the collection.
351      * @param {String} key The key of the item to remove.
352      * @return {Object} The item removed or false if no item was removed.
353      */
354     removeAtKey : function(key){
355         return this.removeAt(this.indexOfKey(key));
356     },
357
358     /**
359      * Returns the number of items in the collection.
360      * @return {Number} the number of items in the collection.
361      */
362     getCount : function(){
363         return this.length;
364     },
365
366     /**
367      * Returns index within the collection of the passed Object.
368      * @param {Object} o The item to find the index of.
369      * @return {Number} index of the item. Returns -1 if not found.
370      */
371     indexOf : function(o){
372         return Ext.Array.indexOf(this.items, o);
373     },
374
375     /**
376      * Returns index within the collection of the passed key.
377      * @param {String} key The key to find the index of.
378      * @return {Number} index of the key.
379      */
380     indexOfKey : function(key){
381         return Ext.Array.indexOf(this.keys, key);
382     },
383
384     /**
385      * Returns the item associated with the passed key OR index.
386      * Key has priority over index.  This is the equivalent
387      * of calling {@link #key} first, then if nothing matched calling {@link #getAt}.
388      * @param {String/Number} key The key or index of the item.
389      * @return {Object} If the item is found, returns the item.  If the item was not found, returns <tt>undefined</tt>.
390      * If an item was found, but is a Class, returns <tt>null</tt>.
391      */
392     get : function(key) {
393         var me = this,
394             mk = me.map[key],
395             item = mk !== undefined ? mk : (typeof key == 'number') ? me.items[key] : undefined;
396         return typeof item != 'function' || me.allowFunctions ? item : null; // for prototype!
397     },
398
399     /**
400      * Returns the item at the specified index.
401      * @param {Number} index The index of the item.
402      * @return {Object} The item at the specified index.
403      */
404     getAt : function(index) {
405         return this.items[index];
406     },
407
408     /**
409      * Returns the item associated with the passed key.
410      * @param {String/Number} key The key of the item.
411      * @return {Object} The item associated with the passed key.
412      */
413     getByKey : function(key) {
414         return this.map[key];
415     },
416
417     /**
418      * Returns true if the collection contains the passed Object as an item.
419      * @param {Object} o  The Object to look for in the collection.
420      * @return {Boolean} True if the collection contains the Object as an item.
421      */
422     contains : function(o){
423         return Ext.Array.contains(this.items, o);
424     },
425
426     /**
427      * Returns true if the collection contains the passed Object as a key.
428      * @param {String} key The key to look for in the collection.
429      * @return {Boolean} True if the collection contains the Object as a key.
430      */
431     containsKey : function(key){
432         return typeof this.map[key] != 'undefined';
433     },
434
435     /**
436      * Removes all items from the collection.  Fires the {@link #clear} event when complete.
437      */
438     clear : function(){
439         var me = this;
440
441         me.length = 0;
442         me.items = [];
443         me.keys = [];
444         me.map = {};
445         me.fireEvent('clear');
446     },
447
448     /**
449      * Returns the first item in the collection.
450      * @return {Object} the first item in the collection..
451      */
452     first : function() {
453         return this.items[0];
454     },
455
456     /**
457      * Returns the last item in the collection.
458      * @return {Object} the last item in the collection..
459      */
460     last : function() {
461         return this.items[this.length - 1];
462     },
463
464     /**
465      * Collects all of the values of the given property and returns their sum
466      * @param {String} property The property to sum by
467      * @param {String} root Optional 'root' property to extract the first argument from. This is used mainly when
468      * summing fields in records, where the fields are all stored inside the 'data' object
469      * @param {Number} start (optional) The record index to start at (defaults to <tt>0</tt>)
470      * @param {Number} end (optional) The record index to end at (defaults to <tt>-1</tt>)
471      * @return {Number} The total
472      */
473     sum: function(property, root, start, end) {
474         var values = this.extractValues(property, root),
475             length = values.length,
476             sum    = 0,
477             i;
478
479         start = start || 0;
480         end   = (end || end === 0) ? end : length - 1;
481
482         for (i = start; i <= end; i++) {
483             sum += values[i];
484         }
485
486         return sum;
487     },
488
489     /**
490      * Collects unique values of a particular property in this MixedCollection
491      * @param {String} property The property to collect on
492      * @param {String} root Optional 'root' property to extract the first argument from. This is used mainly when
493      * summing fields in records, where the fields are all stored inside the 'data' object
494      * @param {Boolean} allowBlank (optional) Pass true to allow null, undefined or empty string values
495      * @return {Array} The unique values
496      */
497     collect: function(property, root, allowNull) {
498         var values = this.extractValues(property, root),
499             length = values.length,
500             hits   = {},
501             unique = [],
502             value, strValue, i;
503
504         for (i = 0; i < length; i++) {
505             value = values[i];
506             strValue = String(value);
507
508             if ((allowNull || !Ext.isEmpty(value)) && !hits[strValue]) {
509                 hits[strValue] = true;
510                 unique.push(value);
511             }
512         }
513
514         return unique;
515     },
516
517     /**
518      * @private
519      * Extracts all of the given property values from the items in the MC. Mainly used as a supporting method for
520      * functions like sum and collect.
521      * @param {String} property The property to extract
522      * @param {String} root Optional 'root' property to extract the first argument from. This is used mainly when
523      * extracting field data from Model instances, where the fields are stored inside the 'data' object
524      * @return {Array} The extracted values
525      */
526     extractValues: function(property, root) {
527         var values = this.items;
528
529         if (root) {
530             values = Ext.Array.pluck(values, root);
531         }
532
533         return Ext.Array.pluck(values, property);
534     },
535
536     /**
537      * Returns a range of items in this collection
538      * @param {Number} startIndex (optional) The starting index. Defaults to 0.
539      * @param {Number} endIndex (optional) The ending index. Defaults to the last item.
540      * @return {Array} An array of items
541      */
542     getRange : function(start, end){
543         var me = this,
544             items = me.items,
545             range = [],
546             i;
547
548         if (items.length < 1) {
549             return range;
550         }
551
552         start = start || 0;
553         end = Math.min(typeof end == 'undefined' ? me.length - 1 : end, me.length - 1);
554         if (start <= end) {
555             for (i = start; i <= end; i++) {
556                 range[range.length] = items[i];
557             }
558         } else {
559             for (i = start; i >= end; i--) {
560                 range[range.length] = items[i];
561             }
562         }
563         return range;
564     },
565
566     /**
567      * <p>Filters the objects in this collection by a set of {@link Ext.util.Filter Filter}s, or by a single
568      * property/value pair with optional parameters for substring matching and case sensitivity. See
569      * {@link Ext.util.Filter Filter} for an example of using Filter objects (preferred). Alternatively,
570      * MixedCollection can be easily filtered by property like this:</p>
571 <pre><code>
572 //create a simple store with a few people defined
573 var people = new Ext.util.MixedCollection();
574 people.addAll([
575     {id: 1, age: 25, name: 'Ed'},
576     {id: 2, age: 24, name: 'Tommy'},
577     {id: 3, age: 24, name: 'Arne'},
578     {id: 4, age: 26, name: 'Aaron'}
579 ]);
580
581 //a new MixedCollection containing only the items where age == 24
582 var middleAged = people.filter('age', 24);
583 </code></pre>
584      *
585      *
586      * @param {Array/String} property A property on your objects, or an array of {@link Ext.util.Filter Filter} objects
587      * @param {String/RegExp} value Either string that the property values
588      * should start with or a RegExp to test against the property
589      * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning
590      * @param {Boolean} caseSensitive (optional) True for case sensitive comparison (defaults to False).
591      * @return {MixedCollection} The new filtered collection
592      */
593     filter : function(property, value, anyMatch, caseSensitive) {
594         var filters = [],
595             filterFn;
596
597         //support for the simple case of filtering by property/value
598         if (Ext.isString(property)) {
599             filters.push(Ext.create('Ext.util.Filter', {
600                 property     : property,
601                 value        : value,
602                 anyMatch     : anyMatch,
603                 caseSensitive: caseSensitive
604             }));
605         } else if (Ext.isArray(property) || property instanceof Ext.util.Filter) {
606             filters = filters.concat(property);
607         }
608
609         //at this point we have an array of zero or more Ext.util.Filter objects to filter with,
610         //so here we construct a function that combines these filters by ANDing them together
611         filterFn = function(record) {
612             var isMatch = true,
613                 length = filters.length,
614                 i;
615
616             for (i = 0; i < length; i++) {
617                 var filter = filters[i],
618                     fn     = filter.filterFn,
619                     scope  = filter.scope;
620
621                 isMatch = isMatch && fn.call(scope, record);
622             }
623
624             return isMatch;
625         };
626
627         return this.filterBy(filterFn);
628     },
629
630     /**
631      * Filter by a function. Returns a <i>new</i> collection that has been filtered.
632      * The passed function will be called with each object in the collection.
633      * If the function returns true, the value is included otherwise it is filtered.
634      * @param {Function} fn The function to be called, it will receive the args o (the object), k (the key)
635      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this MixedCollection.
636      * @return {MixedCollection} The new filtered collection
637      */
638     filterBy : function(fn, scope) {
639         var me = this,
640             newMC  = new this.self(),
641             keys   = me.keys,
642             items  = me.items,
643             length = items.length,
644             i;
645
646         newMC.getKey = me.getKey;
647
648         for (i = 0; i < length; i++) {
649             if (fn.call(scope || me, items[i], keys[i])) {
650                 newMC.add(keys[i], items[i]);
651             }
652         }
653
654         return newMC;
655     },
656
657     /**
658      * Finds the index of the first matching object in this collection by a specific property/value.
659      * @param {String} property The name of a property on your objects.
660      * @param {String/RegExp} value A string that the property values
661      * should start with or a RegExp to test against the property.
662      * @param {Number} start (optional) The index to start searching at (defaults to 0).
663      * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning.
664      * @param {Boolean} caseSensitive (optional) True for case sensitive comparison.
665      * @return {Number} The matched index or -1
666      */
667     findIndex : function(property, value, start, anyMatch, caseSensitive){
668         if(Ext.isEmpty(value, false)){
669             return -1;
670         }
671         value = this.createValueMatcher(value, anyMatch, caseSensitive);
672         return this.findIndexBy(function(o){
673             return o && value.test(o[property]);
674         }, null, start);
675     },
676
677     /**
678      * Find the index of the first matching object in this collection by a function.
679      * If the function returns <i>true</i> it is considered a match.
680      * @param {Function} fn The function to be called, it will receive the args o (the object), k (the key).
681      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this MixedCollection.
682      * @param {Number} start (optional) The index to start searching at (defaults to 0).
683      * @return {Number} The matched index or -1
684      */
685     findIndexBy : function(fn, scope, start){
686         var me = this,
687             keys = me.keys,
688             items = me.items,
689             i = start || 0,
690             len = items.length;
691
692         for (; i < len; i++) {
693             if (fn.call(scope || me, items[i], keys[i])) {
694                 return i;
695             }
696         }
697         return -1;
698     },
699
700     /**
701      * Returns a regular expression based on the given value and matching options. This is used internally for finding and filtering,
702      * and by Ext.data.Store#filter
703      * @private
704      * @param {String} value The value to create the regex for. This is escaped using Ext.escapeRe
705      * @param {Boolean} anyMatch True to allow any match - no regex start/end line anchors will be added. Defaults to false
706      * @param {Boolean} caseSensitive True to make the regex case sensitive (adds 'i' switch to regex). Defaults to false.
707      * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. Ignored if anyMatch is true.
708      */
709     createValueMatcher : function(value, anyMatch, caseSensitive, exactMatch) {
710         if (!value.exec) { // not a regex
711             var er = Ext.String.escapeRegex;
712             value = String(value);
713
714             if (anyMatch === true) {
715                 value = er(value);
716             } else {
717                 value = '^' + er(value);
718                 if (exactMatch === true) {
719                     value += '$';
720                 }
721             }
722             value = new RegExp(value, caseSensitive ? '' : 'i');
723         }
724         return value;
725     },
726
727     /**
728      * Creates a shallow copy of this collection
729      * @return {MixedCollection}
730      */
731     clone : function() {
732         var me = this,
733             copy = new this.self(),
734             keys = me.keys,
735             items = me.items,
736             i = 0,
737             len = items.length;
738
739         for(; i < len; i++){
740             copy.add(keys[i], items[i]);
741         }
742         copy.getKey = me.getKey;
743         return copy;
744     }
745 });