PageRenderTime 44ms CodeModel.GetById 24ms app.highlight 15ms RepoModel.GetById 0ms app.codeStats 0ms

/ext-4.0.7/src/util/AbstractMixedCollection.js

https://bitbucket.org/srogerf/javascript
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