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