3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This 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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.util.AbstractMixedCollection
18 Ext.define('Ext.util.AbstractMixedCollection', {
19 requires: ['Ext.util.Filter'],
22 observable: 'Ext.util.Observable'
25 constructor: function(allowFunctions, keyFn) {
36 * Fires when the collection is cleared.
42 * Fires when an item is added to the collection.
43 * @param {Number} index The index at which the item was added.
44 * @param {Object} o The item added.
45 * @param {String} key The key associated with the added item.
51 * Fires when an item is replaced in the collection.
52 * @param {String} key he key associated with the new added.
53 * @param {Object} old The item being replaced.
54 * @param {Object} new The new item.
60 * Fires when an item is removed from the collection.
61 * @param {Object} o The item being removed.
62 * @param {String} key (optional) The key associated with the removed item.
67 me.allowFunctions = allowFunctions === true;
73 me.mixins.observable.constructor.call(me);
77 * @cfg {Boolean} allowFunctions Specify <tt>true</tt> if the {@link #addAll}
78 * function should add function references to the collection. Defaults to
81 allowFunctions : false,
84 * Adds an item to the collection. Fires the {@link #add} event when complete.
85 * @param {String} key <p>The key to associate with the item, or the new item.</p>
86 * <p>If a {@link #getKey} implementation was specified for this MixedCollection,
87 * or if the key of the stored items is in a property called <tt><b>id</b></tt>,
88 * the MixedCollection will be able to <i>derive</i> the key for the new item.
89 * In this case just pass the new item in this parameter.</p>
90 * @param {Object} o The item to add.
91 * @return {Object} The item added.
93 add : function(key, obj){
99 if (arguments.length == 1) {
101 myKey = me.getKey(myObj);
103 if (typeof myKey != 'undefined' && myKey !== null) {
105 if (typeof old != 'undefined') {
106 return me.replace(myKey, myObj);
108 me.map[myKey] = myObj;
111 me.items.push(myObj);
113 me.fireEvent('add', me.length - 1, myObj, myKey);
118 * MixedCollection has a generic way to fetch keys if you implement getKey. The default implementation
119 * simply returns <b><code>item.id</code></b> but you can provide your own implementation
120 * to return a different value as in the following examples:<pre><code>
122 var mc = new Ext.util.MixedCollection();
123 mc.add(someEl.dom.id, someEl);
124 mc.add(otherEl.dom.id, otherEl);
128 var mc = new Ext.util.MixedCollection();
129 mc.getKey = function(el){
135 // or via the constructor
136 var mc = new Ext.util.MixedCollection(false, function(el){
142 * @param {Object} item The item for which to find the key.
143 * @return {Object} The key for the passed item.
145 getKey : function(o){
150 * Replaces an item in the collection. Fires the {@link #replace} event when complete.
151 * @param {String} key <p>The key associated with the item to replace, or the replacement item.</p>
152 * <p>If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key
153 * of your stored items is in a property called <tt><b>id</b></tt>, then the MixedCollection
154 * will be able to <i>derive</i> the key of the replacement item. If you want to replace an item
155 * with one having the same key value, then just pass the replacement item in this parameter.</p>
156 * @param o {Object} o (optional) If the first parameter passed was a key, the item to associate
158 * @return {Object} The new item.
160 replace : function(key, o){
165 if (arguments.length == 1) {
170 if (typeof key == 'undefined' || key === null || typeof old == 'undefined') {
171 return me.add(key, o);
173 index = me.indexOfKey(key);
176 me.fireEvent('replace', key, old, o);
181 * Adds all elements of an Array or an Object to the collection.
182 * @param {Object/Array} objs An Object containing properties which will be added
183 * to the collection, or an Array of values, each of which are added to the collection.
184 * Functions references will be added to the collection if <code>{@link #allowFunctions}</code>
185 * has been set to <tt>true</tt>.
187 addAll : function(objs){
194 if (arguments.length > 1 || Ext.isArray(objs)) {
195 args = arguments.length > 1 ? arguments : objs;
196 for (len = args.length; i < len; i++) {
201 if (objs.hasOwnProperty(key)) {
202 if (me.allowFunctions || typeof objs[key] != 'function') {
203 me.add(key, objs[key]);
211 * Executes the specified function once for every item in the collection, passing the following arguments:
212 * <div class="mdetail-params"><ul>
213 * <li><b>item</b> : Mixed<p class="sub-desc">The collection item</p></li>
214 * <li><b>index</b> : Number<p class="sub-desc">The item's index</p></li>
215 * <li><b>length</b> : Number<p class="sub-desc">The total number of items in the collection</p></li>
217 * The function should return a boolean value. Returning false from the function will stop the iteration.
218 * @param {Function} fn The function to execute for each item.
219 * @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 each : function(fn, scope){
222 var items = [].concat(this.items), // each safe for removal
227 for (; i < len; i++) {
229 if (fn.call(scope || item, item, i, len) === false) {
236 * Executes the specified function once for every key in the collection, passing each
237 * key, and its associated item as the first two parameters.
238 * @param {Function} fn The function to execute for each item.
239 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.
241 eachKey : function(fn, scope){
242 var keys = this.keys,
247 for (; i < len; i++) {
248 fn.call(scope || window, keys[i], items[i], i, len);
253 * Returns the first item in the collection which elicits a true return value from the
254 * passed selection function.
255 * @param {Function} fn The selection function to execute for each item.
256 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.
257 * @return {Object} The first item in the collection which returned true from the selection function.
259 findBy : function(fn, scope) {
260 var keys = this.keys,
265 for (; i < len; i++) {
266 if (fn.call(scope || window, items[i], keys[i])) {
273 //<deprecated since="0.99">
275 if (Ext.isDefined(Ext.global.console)) {
276 Ext.global.console.warn('Ext.util.MixedCollection: find has been deprecated. Use findBy instead.');
278 return this.findBy.apply(this, arguments);
283 * Inserts an item at the specified index in the collection. Fires the {@link #add} event when complete.
284 * @param {Number} index The index to insert the item at.
285 * @param {String} key The key to associate with the new item, or the item itself.
286 * @param {Object} o (optional) If the second parameter was a key, the new item.
287 * @return {Object} The item inserted.
289 insert : function(index, key, obj){
294 if (arguments.length == 2) {
296 myKey = me.getKey(myObj);
298 if (me.containsKey(myKey)) {
300 me.removeAtKey(myKey);
303 if (index >= me.length) {
304 return me.add(myKey, myObj);
307 Ext.Array.splice(me.items, index, 0, myObj);
308 if (typeof myKey != 'undefined' && myKey !== null) {
309 me.map[myKey] = myObj;
311 Ext.Array.splice(me.keys, index, 0, myKey);
312 me.fireEvent('add', index, myObj, myKey);
317 * Remove an item from the collection.
318 * @param {Object} o The item to remove.
319 * @return {Object} The item removed or false if no item was removed.
321 remove : function(o){
322 return this.removeAt(this.indexOf(o));
326 * Remove all items in the passed array from the collection.
327 * @param {Array} items An array of items to be removed.
328 * @return {Ext.util.MixedCollection} this object
330 removeAll : function(items){
331 Ext.each(items || [], function(item) {
339 * Remove an item from a specified index in the collection. Fires the {@link #remove} event when complete.
340 * @param {Number} index The index within the collection of the item to remove.
341 * @return {Object} The item removed or false if no item was removed.
343 removeAt : function(index){
348 if (index < me.length && index >= 0) {
351 Ext.Array.erase(me.items, index, 1);
352 key = me.keys[index];
353 if (typeof key != 'undefined') {
356 Ext.Array.erase(me.keys, index, 1);
357 me.fireEvent('remove', o, key);
364 * Removed an item associated with the passed key fom the collection.
365 * @param {String} key The key of the item to remove.
366 * @return {Object} The item removed or false if no item was removed.
368 removeAtKey : function(key){
369 return this.removeAt(this.indexOfKey(key));
373 * Returns the number of items in the collection.
374 * @return {Number} the number of items in the collection.
376 getCount : function(){
381 * Returns index within the collection of the passed Object.
382 * @param {Object} o The item to find the index of.
383 * @return {Number} index of the item. Returns -1 if not found.
385 indexOf : function(o){
386 return Ext.Array.indexOf(this.items, o);
390 * Returns index within the collection of the passed key.
391 * @param {String} key The key to find the index of.
392 * @return {Number} index of the key.
394 indexOfKey : function(key){
395 return Ext.Array.indexOf(this.keys, key);
399 * Returns the item associated with the passed key OR index.
400 * Key has priority over index. This is the equivalent
401 * of calling {@link #key} first, then if nothing matched calling {@link #getAt}.
402 * @param {String/Number} key The key or index of the item.
403 * @return {Object} If the item is found, returns the item. If the item was not found, returns <tt>undefined</tt>.
404 * If an item was found, but is a Class, returns <tt>null</tt>.
406 get : function(key) {
409 item = mk !== undefined ? mk : (typeof key == 'number') ? me.items[key] : undefined;
410 return typeof item != 'function' || me.allowFunctions ? item : null; // for prototype!
414 * Returns the item at the specified index.
415 * @param {Number} index The index of the item.
416 * @return {Object} The item at the specified index.
418 getAt : function(index) {
419 return this.items[index];
423 * Returns the item associated with the passed key.
424 * @param {String/Number} key The key of the item.
425 * @return {Object} The item associated with the passed key.
427 getByKey : function(key) {
428 return this.map[key];
432 * Returns true if the collection contains the passed Object as an item.
433 * @param {Object} o The Object to look for in the collection.
434 * @return {Boolean} True if the collection contains the Object as an item.
436 contains : function(o){
437 return Ext.Array.contains(this.items, o);
441 * Returns true if the collection contains the passed Object as a key.
442 * @param {String} key The key to look for in the collection.
443 * @return {Boolean} True if the collection contains the Object as a key.
445 containsKey : function(key){
446 return typeof this.map[key] != 'undefined';
450 * Removes all items from the collection. Fires the {@link #clear} event when complete.
459 me.fireEvent('clear');
463 * Returns the first item in the collection.
464 * @return {Object} the first item in the collection..
467 return this.items[0];
471 * Returns the last item in the collection.
472 * @return {Object} the last item in the collection..
475 return this.items[this.length - 1];
479 * Collects all of the values of the given property and returns their sum
480 * @param {String} property The property to sum by
481 * @param {String} root Optional 'root' property to extract the first argument from. This is used mainly when
482 * summing fields in records, where the fields are all stored inside the 'data' object
483 * @param {Number} start (optional) The record index to start at (defaults to <tt>0</tt>)
484 * @param {Number} end (optional) The record index to end at (defaults to <tt>-1</tt>)
485 * @return {Number} The total
487 sum: function(property, root, start, end) {
488 var values = this.extractValues(property, root),
489 length = values.length,
494 end = (end || end === 0) ? end : length - 1;
496 for (i = start; i <= end; i++) {
504 * Collects unique values of a particular property in this MixedCollection
505 * @param {String} property The property to collect on
506 * @param {String} root Optional 'root' property to extract the first argument from. This is used mainly when
507 * summing fields in records, where the fields are all stored inside the 'data' object
508 * @param {Boolean} allowBlank (optional) Pass true to allow null, undefined or empty string values
509 * @return {Array} The unique values
511 collect: function(property, root, allowNull) {
512 var values = this.extractValues(property, root),
513 length = values.length,
518 for (i = 0; i < length; i++) {
520 strValue = String(value);
522 if ((allowNull || !Ext.isEmpty(value)) && !hits[strValue]) {
523 hits[strValue] = true;
533 * Extracts all of the given property values from the items in the MC. Mainly used as a supporting method for
534 * functions like sum and collect.
535 * @param {String} property The property to extract
536 * @param {String} root Optional 'root' property to extract the first argument from. This is used mainly when
537 * extracting field data from Model instances, where the fields are stored inside the 'data' object
538 * @return {Array} The extracted values
540 extractValues: function(property, root) {
541 var values = this.items;
544 values = Ext.Array.pluck(values, root);
547 return Ext.Array.pluck(values, property);
551 * Returns a range of items in this collection
552 * @param {Number} startIndex (optional) The starting index. Defaults to 0.
553 * @param {Number} endIndex (optional) The ending index. Defaults to the last item.
554 * @return {Array} An array of items
556 getRange : function(start, end){
562 if (items.length < 1) {
567 end = Math.min(typeof end == 'undefined' ? me.length - 1 : end, me.length - 1);
569 for (i = start; i <= end; i++) {
570 range[range.length] = items[i];
573 for (i = start; i >= end; i--) {
574 range[range.length] = items[i];
581 * <p>Filters the objects in this collection by a set of {@link Ext.util.Filter Filter}s, or by a single
582 * property/value pair with optional parameters for substring matching and case sensitivity. See
583 * {@link Ext.util.Filter Filter} for an example of using Filter objects (preferred). Alternatively,
584 * MixedCollection can be easily filtered by property like this:</p>
586 //create a simple store with a few people defined
587 var people = new Ext.util.MixedCollection();
589 {id: 1, age: 25, name: 'Ed'},
590 {id: 2, age: 24, name: 'Tommy'},
591 {id: 3, age: 24, name: 'Arne'},
592 {id: 4, age: 26, name: 'Aaron'}
595 //a new MixedCollection containing only the items where age == 24
596 var middleAged = people.filter('age', 24);
600 * @param {Array/String} property A property on your objects, or an array of {@link Ext.util.Filter Filter} objects
601 * @param {String/RegExp} value Either string that the property values
602 * should start with or a RegExp to test against the property
603 * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning
604 * @param {Boolean} caseSensitive (optional) True for case sensitive comparison (defaults to False).
605 * @return {MixedCollection} The new filtered collection
607 filter : function(property, value, anyMatch, caseSensitive) {
611 //support for the simple case of filtering by property/value
612 if (Ext.isString(property)) {
613 filters.push(Ext.create('Ext.util.Filter', {
617 caseSensitive: caseSensitive
619 } else if (Ext.isArray(property) || property instanceof Ext.util.Filter) {
620 filters = filters.concat(property);
623 //at this point we have an array of zero or more Ext.util.Filter objects to filter with,
624 //so here we construct a function that combines these filters by ANDing them together
625 filterFn = function(record) {
627 length = filters.length,
630 for (i = 0; i < length; i++) {
631 var filter = filters[i],
632 fn = filter.filterFn,
633 scope = filter.scope;
635 isMatch = isMatch && fn.call(scope, record);
641 return this.filterBy(filterFn);
645 * Filter by a function. Returns a <i>new</i> collection that has been filtered.
646 * The passed function will be called with each object in the collection.
647 * If the function returns true, the value is included otherwise it is filtered.
648 * @param {Function} fn The function to be called, it will receive the args o (the object), k (the key)
649 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this MixedCollection.
650 * @return {MixedCollection} The new filtered collection
652 filterBy : function(fn, scope) {
654 newMC = new this.self(),
657 length = items.length,
660 newMC.getKey = me.getKey;
662 for (i = 0; i < length; i++) {
663 if (fn.call(scope || me, items[i], keys[i])) {
664 newMC.add(keys[i], items[i]);
672 * Finds the index of the first matching object in this collection by a specific property/value.
673 * @param {String} property The name of a property on your objects.
674 * @param {String/RegExp} value A string that the property values
675 * should start with or a RegExp to test against the property.
676 * @param {Number} start (optional) The index to start searching at (defaults to 0).
677 * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning.
678 * @param {Boolean} caseSensitive (optional) True for case sensitive comparison.
679 * @return {Number} The matched index or -1
681 findIndex : function(property, value, start, anyMatch, caseSensitive){
682 if(Ext.isEmpty(value, false)){
685 value = this.createValueMatcher(value, anyMatch, caseSensitive);
686 return this.findIndexBy(function(o){
687 return o && value.test(o[property]);
692 * Find the index of the first matching object in this collection by a function.
693 * If the function returns <i>true</i> it is considered a match.
694 * @param {Function} fn The function to be called, it will receive the args o (the object), k (the key).
695 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this MixedCollection.
696 * @param {Number} start (optional) The index to start searching at (defaults to 0).
697 * @return {Number} The matched index or -1
699 findIndexBy : function(fn, scope, start){
706 for (; i < len; i++) {
707 if (fn.call(scope || me, items[i], keys[i])) {
715 * Returns a regular expression based on the given value and matching options. This is used internally for finding and filtering,
716 * and by Ext.data.Store#filter
718 * @param {String} value The value to create the regex for. This is escaped using Ext.escapeRe
719 * @param {Boolean} anyMatch True to allow any match - no regex start/end line anchors will be added. Defaults to false
720 * @param {Boolean} caseSensitive True to make the regex case sensitive (adds 'i' switch to regex). Defaults to false.
721 * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. Ignored if anyMatch is true.
723 createValueMatcher : function(value, anyMatch, caseSensitive, exactMatch) {
724 if (!value.exec) { // not a regex
725 var er = Ext.String.escapeRegex;
726 value = String(value);
728 if (anyMatch === true) {
731 value = '^' + er(value);
732 if (exactMatch === true) {
736 value = new RegExp(value, caseSensitive ? '' : 'i');
742 * Creates a shallow copy of this collection
743 * @return {MixedCollection}
747 copy = new this.self(),
754 copy.add(keys[i], items[i]);
756 copy.getKey = me.getKey;