Roo/util/MixedCollection.js
[roojs1] / Roo / util / MixedCollection.js
1 /*
2  * Based on:
3  * Ext JS Library 1.1.1
4  * Copyright(c) 2006-2007, Ext JS, LLC.
5  *
6  * Originally Released Under LGPL - original licence link has changed is not relivant.
7  *
8  * Fork - LGPL
9  * <script type="text/javascript">
10  */
11
12  
13 /**
14  * @class Roo.util.MixedCollection
15  * @extends Roo.util.Observable
16  * A Collection class that maintains both numeric indexes and keys and exposes events.
17  * @constructor
18  * @param {Boolean} allowFunctions True if the addAll function should add function references to the
19  * collection (defaults to false)
20  * @param {Function} keyFn A function that can accept an item of the type(s) stored in this MixedCollection
21  * and return the key value for that item.  This is used when available to look up the key on items that
22  * were passed without an explicit key parameter to a MixedCollection method.  Passing this parameter is
23  * equivalent to providing an implementation for the {@link #getKey} method.
24  */
25 Roo.util.MixedCollection = function(allowFunctions, keyFn){
26     this.items = [];
27     this.map = {};
28     this.keys = [];
29     this.length = 0;
30     this.addEvents({
31         /**
32          * @event clear
33          * Fires when the collection is cleared.
34          */
35         "clear" : true,
36         /**
37          * @event add
38          * Fires when an item is added to the collection.
39          * @param {Number} index The index at which the item was added.
40          * @param {Object} o The item added.
41          * @param {String} key The key associated with the added item.
42          */
43         "add" : true,
44         /**
45          * @event replace
46          * Fires when an item is replaced in the collection.
47          * @param {String} key he key associated with the new added.
48          * @param {Object} old The item being replaced.
49          * @param {Object} new The new item.
50          */
51         "replace" : true,
52         /**
53          * @event remove
54          * Fires when an item is removed from the collection.
55          * @param {Object} o The item being removed.
56          * @param {String} key (optional) The key associated with the removed item.
57          */
58         "remove" : true,
59         "sort" : true
60     });
61     this.allowFunctions = allowFunctions === true;
62     if(keyFn){
63         this.getKey = keyFn;
64     }
65     Roo.util.MixedCollection.superclass.constructor.call(this);
66 };
67
68 Roo.extend(Roo.util.MixedCollection, Roo.util.Observable, {
69     allowFunctions : false,
70     
71 /**
72  * Adds an item to the collection.
73  * @param {String} key The key to associate with the item
74  * @param {Object} o The item to add.
75  * @return {Object} The item added.
76  */
77     add : function(key, o){
78         
79         if(arguments.length == 1){
80             o = arguments[0];
81             key = this.getKey(o);
82         }
83         
84         Roo.log([key, o]);
85         
86         if(typeof key == "undefined" || key === null){
87             this.length++;
88             this.items.push(o);
89             this.keys.push(null);
90         }else{
91             Roo.log(this);
92             var old = this.map[key * 1];
93             Roo.log(this.map);
94             Roo.log('old.........................');
95             Roo.log(old);
96             if(old){
97                 return this.replace(key, o);
98             }
99             this.length++;
100             this.items.push(o);
101             this.map[key] = o;
102             this.keys.push(key);
103         }
104         this.fireEvent("add", this.length-1, o, key);
105         return o;
106     },
107        
108 /**
109   * MixedCollection has a generic way to fetch keys if you implement getKey.
110 <pre><code>
111 // normal way
112 var mc = new Roo.util.MixedCollection();
113 mc.add(someEl.dom.id, someEl);
114 mc.add(otherEl.dom.id, otherEl);
115 //and so on
116
117 // using getKey
118 var mc = new Roo.util.MixedCollection();
119 mc.getKey = function(el){
120    return el.dom.id;
121 };
122 mc.add(someEl);
123 mc.add(otherEl);
124
125 // or via the constructor
126 var mc = new Roo.util.MixedCollection(false, function(el){
127    return el.dom.id;
128 });
129 mc.add(someEl);
130 mc.add(otherEl);
131 </code></pre>
132  * @param o {Object} The item for which to find the key.
133  * @return {Object} The key for the passed item.
134  */
135     getKey : function(o){
136          return o.id; 
137     },
138    
139 /**
140  * Replaces an item in the collection.
141  * @param {String} key The key associated with the item to replace, or the item to replace.
142  * @param o {Object} o (optional) If the first parameter passed was a key, the item to associate with that key.
143  * @return {Object}  The new item.
144  */
145     replace : function(key, o){
146         if(arguments.length == 1){
147             o = arguments[0];
148             key = this.getKey(o);
149         }
150         Roo.log('replace.........................');
151             Roo.log([o, key]);
152         var old = this.item(key);
153         if(typeof key == "undefined" || key === null || typeof old == "undefined"){
154              return this.add(key, o);
155         }
156         var index = this.indexOfKey(key);
157         Roo.log(index);
158         
159         this.items[index] = o;
160         this.map[key] = o;
161         this.fireEvent("replace", key, old, o);
162         return o;
163     },
164    
165 /**
166  * Adds all elements of an Array or an Object to the collection.
167  * @param {Object/Array} objs An Object containing properties which will be added to the collection, or
168  * an Array of values, each of which are added to the collection.
169  */
170     addAll : function(objs){
171         if(arguments.length > 1 || objs instanceof Array){
172             var args = arguments.length > 1 ? arguments : objs;
173             for(var i = 0, len = args.length; i < len; i++){
174                 this.add(args[i]);
175             }
176         }else{
177             for(var key in objs){
178                 if(this.allowFunctions || typeof objs[key] != "function"){
179                     this.add(key, objs[key]);
180                 }
181             }
182         }
183     },
184    
185 /**
186  * Executes the specified function once for every item in the collection, passing each
187  * item as the first and only parameter. returning false from the function will stop the iteration.
188  * @param {Function} fn The function to execute for each item.
189  * @param {Object} scope (optional) The scope in which to execute the function.
190  */
191     each : function(fn, scope){
192         var items = [].concat(this.items); // each safe for removal
193         for(var i = 0, len = items.length; i < len; i++){
194             if(fn.call(scope || items[i], items[i], i, len) === false){
195                 break;
196             }
197         }
198     },
199    
200 /**
201  * Executes the specified function once for every key in the collection, passing each
202  * key, and its associated item as the first two parameters.
203  * @param {Function} fn The function to execute for each item.
204  * @param {Object} scope (optional) The scope in which to execute the function.
205  */
206     eachKey : function(fn, scope){
207         for(var i = 0, len = this.keys.length; i < len; i++){
208             fn.call(scope || window, this.keys[i], this.items[i], i, len);
209         }
210     },
211    
212 /**
213  * Returns the first item in the collection which elicits a true return value from the
214  * passed selection function.
215  * @param {Function} fn The selection function to execute for each item.
216  * @param {Object} scope (optional) The scope in which to execute the function.
217  * @return {Object} The first item in the collection which returned true from the selection function.
218  */
219     find : function(fn, scope){
220         for(var i = 0, len = this.items.length; i < len; i++){
221             if(fn.call(scope || window, this.items[i], this.keys[i])){
222                 return this.items[i];
223             }
224         }
225         return null;
226     },
227    
228 /**
229  * Inserts an item at the specified index in the collection.
230  * @param {Number} index The index to insert the item at.
231  * @param {String} key The key to associate with the new item, or the item itself.
232  * @param {Object} o  (optional) If the second parameter was a key, the new item.
233  * @return {Object} The item inserted.
234  */
235     insert : function(index, key, o){
236         if(arguments.length == 2){
237             o = arguments[1];
238             key = this.getKey(o);
239         }
240         if(index >= this.length){
241             return this.add(key, o);
242         }
243         this.length++;
244         this.items.splice(index, 0, o);
245         if(typeof key != "undefined" && key != null){
246             this.map[key] = o;
247         }
248         this.keys.splice(index, 0, key);
249         this.fireEvent("add", index, o, key);
250         return o;
251     },
252    
253 /**
254  * Removed an item from the collection.
255  * @param {Object} o The item to remove.
256  * @return {Object} The item removed.
257  */
258     remove : function(o){
259         return this.removeAt(this.indexOf(o));
260     },
261    
262 /**
263  * Remove an item from a specified index in the collection.
264  * @param {Number} index The index within the collection of the item to remove.
265  */
266     removeAt : function(index){
267         if(index < this.length && index >= 0){
268             this.length--;
269             var o = this.items[index];
270             this.items.splice(index, 1);
271             var key = this.keys[index];
272             if(typeof key != "undefined"){
273                 delete this.map[key];
274             }
275             this.keys.splice(index, 1);
276             this.fireEvent("remove", o, key);
277         }
278     },
279    
280 /**
281  * Removed an item associated with the passed key fom the collection.
282  * @param {String} key The key of the item to remove.
283  */
284     removeKey : function(key){
285         return this.removeAt(this.indexOfKey(key));
286     },
287    
288 /**
289  * Returns the number of items in the collection.
290  * @return {Number} the number of items in the collection.
291  */
292     getCount : function(){
293         return this.length; 
294     },
295    
296 /**
297  * Returns index within the collection of the passed Object.
298  * @param {Object} o The item to find the index of.
299  * @return {Number} index of the item.
300  */
301     indexOf : function(o){
302         if(!this.items.indexOf){
303             for(var i = 0, len = this.items.length; i < len; i++){
304                 if(this.items[i] == o) return i;
305             }
306             return -1;
307         }else{
308             return this.items.indexOf(o);
309         }
310     },
311    
312 /**
313  * Returns index within the collection of the passed key.
314  * @param {String} key The key to find the index of.
315  * @return {Number} index of the key.
316  */
317     indexOfKey : function(key){
318         if(!this.keys.indexOf){
319             for(var i = 0, len = this.keys.length; i < len; i++){
320                 if(this.keys[i] == key) return i;
321             }
322             return -1;
323         }else{
324             return this.keys.indexOf(key);
325         }
326     },
327    
328 /**
329  * Returns the item associated with the passed key OR index. Key has priority over index.
330  * @param {String/Number} key The key or index of the item.
331  * @return {Object} The item associated with the passed key.
332  */
333     item : function(key){
334         var item = typeof this.map[key] != "undefined" ? this.map[key] : this.items[key];
335         return typeof item != 'function' || this.allowFunctions ? item : null; // for prototype!
336     },
337     
338 /**
339  * Returns the item at the specified index.
340  * @param {Number} index The index of the item.
341  * @return {Object}
342  */
343     itemAt : function(index){
344         return this.items[index];
345     },
346     
347 /**
348  * Returns the item associated with the passed key.
349  * @param {String/Number} key The key of the item.
350  * @return {Object} The item associated with the passed key.
351  */
352     key : function(key){
353         return this.map[key];
354     },
355    
356 /**
357  * Returns true if the collection contains the passed Object as an item.
358  * @param {Object} o  The Object to look for in the collection.
359  * @return {Boolean} True if the collection contains the Object as an item.
360  */
361     contains : function(o){
362         return this.indexOf(o) != -1;
363     },
364    
365 /**
366  * Returns true if the collection contains the passed Object as a key.
367  * @param {String} key The key to look for in the collection.
368  * @return {Boolean} True if the collection contains the Object as a key.
369  */
370     containsKey : function(key){
371         return typeof this.map[key] != "undefined";
372     },
373    
374 /**
375  * Removes all items from the collection.
376  */
377     clear : function(){
378         this.length = 0;
379         this.items = [];
380         this.keys = [];
381         this.map = {};
382         this.fireEvent("clear");
383     },
384    
385 /**
386  * Returns the first item in the collection.
387  * @return {Object} the first item in the collection..
388  */
389     first : function(){
390         return this.items[0]; 
391     },
392    
393 /**
394  * Returns the last item in the collection.
395  * @return {Object} the last item in the collection..
396  */
397     last : function(){
398         return this.items[this.length-1];   
399     },
400     
401     _sort : function(property, dir, fn){
402         var dsc = String(dir).toUpperCase() == "DESC" ? -1 : 1;
403         fn = fn || function(a, b){
404             return a-b;
405         };
406         var c = [], k = this.keys, items = this.items;
407         for(var i = 0, len = items.length; i < len; i++){
408             c[c.length] = {key: k[i], value: items[i], index: i};
409         }
410         c.sort(function(a, b){
411             var v = fn(a[property], b[property]) * dsc;
412             if(v == 0){
413                 v = (a.index < b.index ? -1 : 1);
414             }
415             return v;
416         });
417         for(var i = 0, len = c.length; i < len; i++){
418             items[i] = c[i].value;
419             k[i] = c[i].key;
420         }
421         this.fireEvent("sort", this);
422     },
423     
424     /**
425      * Sorts this collection with the passed comparison function
426      * @param {String} direction (optional) "ASC" or "DESC"
427      * @param {Function} fn (optional) comparison function
428      */
429     sort : function(dir, fn){
430         this._sort("value", dir, fn);
431     },
432     
433     /**
434      * Sorts this collection by keys
435      * @param {String} direction (optional) "ASC" or "DESC"
436      * @param {Function} fn (optional) a comparison function (defaults to case insensitive string)
437      */
438     keySort : function(dir, fn){
439         this._sort("key", dir, fn || function(a, b){
440             return String(a).toUpperCase()-String(b).toUpperCase();
441         });
442     },
443     
444     /**
445      * Returns a range of items in this collection
446      * @param {Number} startIndex (optional) defaults to 0
447      * @param {Number} endIndex (optional) default to the last item
448      * @return {Array} An array of items
449      */
450     getRange : function(start, end){
451         var items = this.items;
452         if(items.length < 1){
453             return [];
454         }
455         start = start || 0;
456         end = Math.min(typeof end == "undefined" ? this.length-1 : end, this.length-1);
457         var r = [];
458         if(start <= end){
459             for(var i = start; i <= end; i++) {
460                     r[r.length] = items[i];
461             }
462         }else{
463             for(var i = start; i >= end; i--) {
464                     r[r.length] = items[i];
465             }
466         }
467         return r;
468     },
469         
470     /**
471      * Filter the <i>objects</i> in this collection by a specific property. 
472      * Returns a new collection that has been filtered.
473      * @param {String} property A property on your objects
474      * @param {String/RegExp} value Either string that the property values 
475      * should start with or a RegExp to test against the property
476      * @return {MixedCollection} The new filtered collection
477      */
478     filter : function(property, value){
479         if(!value.exec){ // not a regex
480             value = String(value);
481             if(value.length == 0){
482                 return this.clone();
483             }
484             value = new RegExp("^" + Roo.escapeRe(value), "i");
485         }
486         return this.filterBy(function(o){
487             return o && value.test(o[property]);
488         });
489         },
490     
491     /**
492      * Filter by a function. * Returns a new collection that has been filtered.
493      * The passed function will be called with each 
494      * object in the collection. If the function returns true, the value is included 
495      * otherwise it is filtered.
496      * @param {Function} fn The function to be called, it will receive the args o (the object), k (the key)
497      * @param {Object} scope (optional) The scope of the function (defaults to this) 
498      * @return {MixedCollection} The new filtered collection
499      */
500     filterBy : function(fn, scope){
501         var r = new Roo.util.MixedCollection();
502         r.getKey = this.getKey;
503         var k = this.keys, it = this.items;
504         for(var i = 0, len = it.length; i < len; i++){
505             if(fn.call(scope||this, it[i], k[i])){
506                                 r.add(k[i], it[i]);
507                         }
508         }
509         return r;
510     },
511     
512     /**
513      * Creates a duplicate of this collection
514      * @return {MixedCollection}
515      */
516     clone : function(){
517         var r = new Roo.util.MixedCollection();
518         var k = this.keys, it = this.items;
519         for(var i = 0, len = it.length; i < len; i++){
520             r.add(k[i], it[i]);
521         }
522         r.getKey = this.getKey;
523         return r;
524     }
525 });
526 /**
527  * Returns the item associated with the passed key or index.
528  * @method
529  * @param {String/Number} key The key or index of the item.
530  * @return {Object} The item associated with the passed key.
531  */
532 Roo.util.MixedCollection.prototype.get = Roo.util.MixedCollection.prototype.item;