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