/ext-4.0.7/src/util/AbstractMixedCollection.js
JavaScript | 761 lines | 373 code | 78 blank | 310 comment | 84 complexity | eb0411aa2eecaf3b5624d38c9ac9e2f6 MD5 | raw file
1/*
2
3This file is part of Ext JS 4
4
5Copyright (c) 2011 Sencha Inc
6
7Contact: http://www.sencha.com/contact
8
9GNU General Public License Usage
10This 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
12If 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 */
19Ext.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
123var mc = new Ext.util.MixedCollection();
124mc.add(someEl.dom.id, someEl);
125mc.add(otherEl.dom.id, otherEl);
126//and so on
127
128// using getKey
129var mc = new Ext.util.MixedCollection();
130mc.getKey = function(el){
131 return el.dom.id;
132};
133mc.add(someEl);
134mc.add(otherEl);
135
136// or via the constructor
137var mc = new Ext.util.MixedCollection(false, function(el){
138 return el.dom.id;
139});
140mc.add(someEl);
141mc.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
588var people = new Ext.util.MixedCollection();
589people.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
597var 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