web-apps/vendor/touch/src/util/Collection.js
Maxim Kadushkin 741b10515d webapps added
2016-03-10 21:48:53 -03:00

1036 lines
31 KiB
JavaScript

/**
* @private
*/
Ext.define('Ext.util.Collection', {
/**
* @cfg {Object[]} filters
* Array of {@link Ext.util.Filter Filters} for this collection.
*/
/**
* @cfg {Object[]} sorters
* Array of {@link Ext.util.Sorter Sorters} for this collection.
*/
config: {
autoFilter: true,
autoSort: true
},
mixins: {
sortable: 'Ext.mixin.Sortable',
filterable: 'Ext.mixin.Filterable'
},
constructor: function(keyFn, config) {
var me = this;
/**
* @property {Array} [all=[]]
* An array containing all the items (unsorted, unfiltered)
*/
me.all = [];
/**
* @property {Array} [items=[]]
* An array containing the filtered items (sorted)
*/
me.items = [];
/**
* @property {Array} [keys=[]]
* An array containing all the filtered keys (sorted)
*/
me.keys = [];
/**
* @property {Object} [indices={}]
* An object used as map to get a sorted and filtered index of an item
*/
me.indices = {};
/**
* @property {Object} [map={}]
* An object used as map to get an object based on its key
*/
me.map = {};
/**
* @property {Number} [length=0]
* The count of items in the collection filtered and sorted
*/
me.length = 0;
if (keyFn) {
me.getKey = keyFn;
}
this.initConfig(config);
},
updateAutoSort: function(autoSort, oldAutoSort) {
if (oldAutoSort === false && autoSort && this.items.length) {
this.sort();
}
},
updateAutoFilter: function(autoFilter, oldAutoFilter) {
if (oldAutoFilter === false && autoFilter && this.all.length) {
this.filter();
}
},
insertSorters: function() {
// We override the insertSorters method that exists on the Sortable mixin. This method always
// gets called whenever you add or insert a new sorter. We do this because we actually want
// to sort right after this happens.
this.mixins.sortable.insertSorters.apply(this, arguments);
if (this.getAutoSort() && this.items.length) {
this.sort();
}
return this;
},
removeSorters: function(sorters) {
// We override the removeSorters method that exists on the Sortable mixin. This method always
// gets called whenever you remove a sorter. If we are still sorted after we removed this sorter,
// then we have to resort the whole collection.
this.mixins.sortable.removeSorters.call(this, sorters);
if (this.sorted && this.getAutoSort() && this.items.length) {
this.sort();
}
return this;
},
applyFilters: function(filters) {
var collection = this.mixins.filterable.applyFilters.call(this, filters);
if (!filters && this.all.length && this.getAutoFilter()) {
this.filter();
}
return collection;
},
addFilters: function(filters) {
// We override the insertFilters method that exists on the Filterable mixin. This method always
// gets called whenever you add or insert a new filter. We do this because we actually want
// to filter right after this happens.
this.mixins.filterable.addFilters.call(this, filters);
if (this.items.length && this.getAutoFilter()) {
this.filter();
}
return this;
},
removeFilters: function(filters) {
// We override the removeFilters method that exists on the Filterable mixin. This method always
// gets called whenever you remove a filter. If we are still filtered after we removed this filter,
// then we have to re-filter the whole collection.
this.mixins.filterable.removeFilters.call(this, filters);
if (this.filtered && this.all.length && this.getAutoFilter()) {
this.filter();
}
return this;
},
/**
* This method will sort a collection based on the currently configured sorters.
* @param {Object} property
* @param {Object} value
* @param {Object} anyMatch
* @param {Object} caseSensitive
* @return {Array}
*/
filter: function(property, value, anyMatch, caseSensitive) {
// Support for the simple case of filtering by property/value
if (property) {
if (Ext.isString(property)) {
this.addFilters({
property : property,
value : value,
anyMatch : anyMatch,
caseSensitive: caseSensitive
});
return this.items;
}
else {
this.addFilters(property);
return this.items;
}
}
this.items = this.mixins.filterable.filter.call(this, this.all.slice());
this.updateAfterFilter();
if (this.sorted && this.getAutoSort()) {
this.sort();
}
},
updateAfterFilter: function() {
var items = this.items,
keys = this.keys,
indices = this.indices = {},
ln = items.length,
i, item, key;
keys.length = 0;
for (i = 0; i < ln; i++) {
item = items[i];
key = this.getKey(item);
indices[key] = i;
keys[i] = key;
}
this.length = items.length;
this.dirtyIndices = false;
},
sort: function(sorters, defaultDirection) {
var items = this.items,
keys = this.keys,
indices = this.indices,
ln = items.length,
i, item, key;
// If we pass sorters to this method we have to add them first.
// Because adding a sorter automatically sorts the items collection
// we can just return items after we have added the sorters
if (sorters) {
this.addSorters(sorters, defaultDirection);
return this.items;
}
// We save the keys temporarily on each item
for (i = 0; i < ln; i++) {
items[i]._current_key = keys[i];
}
// Now we sort our items array
this.handleSort(items);
// And finally we update our keys and indices
for (i = 0; i < ln; i++) {
item = items[i];
key = item._current_key;
keys[i] = key;
indices[key] = i;
delete item._current_key;
}
this.dirtyIndices = true;
},
handleSort: function(items) {
this.mixins.sortable.sort.call(this, items);
},
/**
* Adds an item to the collection. Fires the {@link #add} event when complete.
* @param {String} key
*
* The key to associate with the item, or the new item.
*
* If a {@link #getKey} implementation was specified for this MixedCollection, or if the key of the stored items is
* in a property called **id**, the MixedCollection will be able to _derive_ the key for the new item. In this case
* just pass the new item in this parameter.
* @param {Object} item The item to add.
* @return {Object} The item added.
*/
add: function(key, item) {
var me = this,
filtered = this.filtered,
sorted = this.sorted,
all = this.all,
items = this.items,
keys = this.keys,
indices = this.indices,
filterable = this.mixins.filterable,
currentLength = items.length,
index = currentLength;
if (arguments.length == 1) {
item = key;
key = me.getKey(item);
}
if (typeof key != 'undefined' && key !== null) {
if (typeof me.map[key] != 'undefined') {
return me.replace(key, item);
}
me.map[key] = item;
}
all.push(item);
if (filtered && this.getAutoFilter() && filterable.isFiltered.call(me, item)) {
return null;
}
me.length++;
if (sorted && this.getAutoSort()) {
index = this.findInsertionIndex(items, item);
}
if (index !== currentLength) {
this.dirtyIndices = true;
Ext.Array.splice(keys, index, 0, key);
Ext.Array.splice(items, index, 0, item);
} else {
indices[key] = currentLength;
keys.push(key);
items.push(item);
}
return item;
},
/**
* MixedCollection has a generic way to fetch keys if you implement getKey. The default implementation simply
* returns **`item.id`** but you can provide your own implementation to return a different value as in the following
* examples:
*
* // normal way
* var mc = new Ext.util.MixedCollection();
* mc.add(someEl.dom.id, someEl);
* mc.add(otherEl.dom.id, otherEl);
* //and so on
*
* // using getKey
* var mc = new Ext.util.MixedCollection();
* mc.getKey = function(el){
* return el.dom.id;
* };
* mc.add(someEl);
* mc.add(otherEl);
*
* // or via the constructor
* var mc = new Ext.util.MixedCollection(false, function(el){
* return el.dom.id;
* });
* mc.add(someEl);
* mc.add(otherEl);
* @param {Object} item The item for which to find the key.
* @return {Object} The key for the passed item.
*/
getKey: function(item) {
return item.id;
},
/**
* Replaces an item in the collection. Fires the {@link #replace} event when complete.
* @param {String} oldKey
*
* The key associated with the item to replace, or the replacement item.
*
* If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key of your stored items is
* in a property called **id**, then the MixedCollection will be able to _derive_ the key of the replacement item.
* If you want to replace an item with one having the same key value, then just pass the replacement item in this
* parameter.
* @param {Object} item {Object} item (optional) If the first parameter passed was a key, the item to associate with
* that key.
* @return {Object} The new item.
*/
replace: function(oldKey, item) {
var me = this,
sorted = me.sorted,
filtered = me.filtered,
filterable = me.mixins.filterable,
items = me.items,
keys = me.keys,
all = me.all,
map = me.map,
returnItem = null,
oldItemsLn = items.length,
oldItem, index, newKey;
if (arguments.length == 1) {
item = oldKey;
oldKey = newKey = me.getKey(item);
} else {
newKey = me.getKey(item);
}
oldItem = map[oldKey];
if (typeof oldKey == 'undefined' || oldKey === null || typeof oldItem == 'undefined') {
return me.add(newKey, item);
}
me.map[newKey] = item;
if (newKey !== oldKey) {
delete me.map[oldKey];
}
if (sorted && me.getAutoSort()) {
Ext.Array.remove(items, oldItem);
Ext.Array.remove(keys, oldKey);
Ext.Array.remove(all, oldItem);
all.push(item);
me.dirtyIndices = true;
if (filtered && me.getAutoFilter()) {
// If the item is now filtered we check if it was not filtered
// before. If that is the case then we subtract from the length
if (filterable.isFiltered.call(me, item)) {
if (oldItemsLn !== items.length) {
me.length--;
}
return null;
}
// If the item was filtered, but now it is not anymore then we
// add to the length
else if (oldItemsLn === items.length) {
me.length++;
returnItem = item;
}
}
index = this.findInsertionIndex(items, item);
Ext.Array.splice(keys, index, 0, newKey);
Ext.Array.splice(items, index, 0, item);
} else {
if (filtered) {
if (me.getAutoFilter() && filterable.isFiltered.call(me, item)) {
if (items.indexOf(oldItem) !== -1) {
Ext.Array.remove(items, oldItem);
Ext.Array.remove(keys, oldKey);
me.length--;
me.dirtyIndices = true;
}
return null;
}
else if (items.indexOf(oldItem) === -1) {
items.push(item);
keys.push(newKey);
me.indices[newKey] = me.length;
me.length++;
return item;
}
}
index = me.items.indexOf(oldItem);
keys[index] = newKey;
items[index] = item;
this.dirtyIndices = true;
}
return returnItem;
},
/**
* Adds all elements of an Array or an Object to the collection.
* @param {Object/Array} objs An Object containing properties which will be added to the collection, or an Array of
* values, each of which are added to the collection. Functions references will be added to the collection if {@link
* Ext.util.MixedCollection#allowFunctions allowFunctions} has been set to `true`.
*/
addAll: function(addItems) {
var me = this,
filtered = me.filtered,
sorted = me.sorted,
all = me.all,
items = me.items,
keys = me.keys,
map = me.map,
autoFilter = me.getAutoFilter(),
autoSort = me.getAutoSort(),
newKeys = [],
newItems = [],
filterable = me.mixins.filterable,
addedItems = [],
ln, key, i, item;
if (Ext.isObject(addItems)) {
for (key in addItems) {
if (addItems.hasOwnProperty(key)) {
newItems.push(items[key]);
newKeys.push(key);
}
}
} else {
newItems = addItems;
ln = addItems.length;
for (i = 0; i < ln; i++) {
newKeys.push(me.getKey(addItems[i]));
}
}
for (i = 0; i < ln; i++) {
key = newKeys[i];
item = newItems[i];
if (typeof key != 'undefined' && key !== null) {
if (typeof map[key] != 'undefined') {
me.replace(key, item);
continue;
}
map[key] = item;
}
all.push(item);
if (filtered && autoFilter && filterable.isFiltered.call(me, item)) {
continue;
}
me.length++;
keys.push(key);
items.push(item);
addedItems.push(item);
}
if (addedItems.length) {
me.dirtyIndices = true;
if (sorted && autoSort) {
me.sort();
}
return addedItems;
}
return null;
},
/**
* Executes the specified function once for every item in the collection.
* The function should return a Boolean value. Returning `false` from the function will stop the iteration.
* @param {Function} fn The function to execute for each item.
* @param {Mixed} fn.item The collection item.
* @param {Number} fn.index The item's index.
* @param {Number} fn.length The total number of items in the collection.
* @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to the current
* item in the iteration.
*/
each: function(fn, scope) {
var items = this.items.slice(), // each safe for removal
i = 0,
len = items.length,
item;
for (; i < len; i++) {
item = items[i];
if (fn.call(scope || item, item, i, len) === false) {
break;
}
}
},
/**
* Executes the specified function once for every key in the collection, passing each key, and its associated item
* as the first two parameters.
* @param {Function} fn The function to execute for each item.
* @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to the browser
* window.
*/
eachKey: function(fn, scope) {
var keys = this.keys,
items = this.items,
ln = keys.length, i;
for (i = 0; i < ln; i++) {
fn.call(scope || window, keys[i], items[i], i, ln);
}
},
/**
* Returns the first item in the collection which elicits a `true` return value from the passed selection function.
* @param {Function} fn The selection function to execute for each item.
* @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to the browser
* window.
* @return {Object} The first item in the collection which returned `true` from the selection function.
*/
findBy: function(fn, scope) {
var keys = this.keys,
items = this.items,
i = 0,
len = items.length;
for (; i < len; i++) {
if (fn.call(scope || window, items[i], keys[i])) {
return items[i];
}
}
return null;
},
/**
* Filter by a function. Returns a _new_ collection that has been filtered. The passed function will be called with
* each object in the collection. If the function returns `true`, the value is included otherwise it is filtered.
* @param {Function} fn The function to be called.
* @param fn.o The object.
* @param fn.k The key.
* @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to this
* MixedCollection.
* @return {Ext.util.MixedCollection} The new filtered collection
*/
filterBy: function(fn, scope) {
var me = this,
newCollection = new this.self(),
keys = me.keys,
items = me.all,
length = items.length,
i;
newCollection.getKey = me.getKey;
for (i = 0; i < length; i++) {
if (fn.call(scope || me, items[i], me.getKey(items[i]))) {
newCollection.add(keys[i], items[i]);
}
}
return newCollection;
},
/**
* Inserts an item at the specified index in the collection. Fires the {@link #add} event when complete.
* @param {Number} index The index to insert the item at.
* @param {String} key The key to associate with the new item, or the item itself.
* @param {Object} item If the second parameter was a key, the new item.
* @return {Object} The item inserted.
*/
insert: function(index, key, item) {
var me = this,
sorted = this.sorted,
map = this.map,
filtered = this.filtered;
if (arguments.length == 2) {
item = key;
key = me.getKey(item);
}
if (index >= me.length || (sorted && me.getAutoSort())) {
return me.add(key, item);
}
if (typeof key != 'undefined' && key !== null) {
if (typeof map[key] != 'undefined') {
me.replace(key, item);
return false;
}
map[key] = item;
}
this.all.push(item);
if (filtered && this.getAutoFilter() && this.mixins.filterable.isFiltered.call(me, item)) {
return null;
}
me.length++;
Ext.Array.splice(me.items, index, 0, item);
Ext.Array.splice(me.keys, index, 0, key);
me.dirtyIndices = true;
return item;
},
insertAll: function(index, insertItems) {
if (index >= this.items.length || (this.sorted && this.getAutoSort())) {
return this.addAll(insertItems);
}
var me = this,
filtered = this.filtered,
sorted = this.sorted,
all = this.all,
items = this.items,
keys = this.keys,
map = this.map,
autoFilter = this.getAutoFilter(),
autoSort = this.getAutoSort(),
newKeys = [],
newItems = [],
addedItems = [],
filterable = this.mixins.filterable,
insertedUnfilteredItem = false,
ln, key, i, item;
if (sorted && this.getAutoSort()) {
// <debug>
Ext.Logger.error('Inserting a collection of items into a sorted Collection is invalid. Please just add these items or remove the sorters.');
// </debug>
}
if (Ext.isObject(insertItems)) {
for (key in insertItems) {
if (insertItems.hasOwnProperty(key)) {
newItems.push(items[key]);
newKeys.push(key);
}
}
} else {
newItems = insertItems;
ln = insertItems.length;
for (i = 0; i < ln; i++) {
newKeys.push(me.getKey(insertItems[i]));
}
}
for (i = 0; i < ln; i++) {
key = newKeys[i];
item = newItems[i];
if (typeof key != 'undefined' && key !== null) {
if (typeof map[key] != 'undefined') {
me.replace(key, item);
continue;
}
map[key] = item;
}
all.push(item);
if (filtered && autoFilter && filterable.isFiltered.call(me, item)) {
continue;
}
me.length++;
Ext.Array.splice(items, index + i, 0, item);
Ext.Array.splice(keys, index + i, 0, key);
insertedUnfilteredItem = true;
addedItems.push(item);
}
if (insertedUnfilteredItem) {
this.dirtyIndices = true;
if (sorted && autoSort) {
this.sort();
}
return addedItems;
}
return null;
},
/**
* Remove an item from the collection.
* @param {Object} item The item to remove.
* @return {Object} The item removed or `false` if no item was removed.
*/
remove: function(item) {
var index = this.items.indexOf(item);
if (index === -1) {
Ext.Array.remove(this.all, item);
if (typeof this.getKey == 'function') {
var key = this.getKey(item);
if (key !== undefined) {
delete this.map[key];
}
}
return item;
}
return this.removeAt(this.items.indexOf(item));
},
/**
* Remove all items in the passed array from the collection.
* @param {Array} items An array of items to be removed.
* @return {Ext.util.MixedCollection} this object
*/
removeAll: function(items) {
if (items) {
var ln = items.length, i;
for (i = 0; i < ln; i++) {
this.remove(items[i]);
}
}
return this;
},
/**
* Remove an item from a specified index in the collection. Fires the {@link #remove} event when complete.
* @param {Number} index The index within the collection of the item to remove.
* @return {Object} The item removed or `false` if no item was removed.
*/
removeAt: function(index) {
var me = this,
items = me.items,
keys = me.keys,
all = me.all,
item, key;
if (index < me.length && index >= 0) {
item = items[index];
key = keys[index];
if (typeof key != 'undefined') {
delete me.map[key];
}
Ext.Array.erase(items, index, 1);
Ext.Array.erase(keys, index, 1);
Ext.Array.remove(all, item);
delete me.indices[key];
me.length--;
this.dirtyIndices = true;
return item;
}
return false;
},
/**
* Removed an item associated with the passed key from the collection.
* @param {String} key The key of the item to remove.
* @return {Object/Boolean} The item removed or `false` if no item was removed.
*/
removeAtKey: function(key) {
return this.removeAt(this.indexOfKey(key));
},
/**
* Returns the number of items in the collection.
* @return {Number} the number of items in the collection.
*/
getCount: function() {
return this.length;
},
/**
* Returns index within the collection of the passed Object.
* @param {Object} item The item to find the index of.
* @return {Number} Index of the item. Returns -1 if not found.
*/
indexOf: function(item) {
if (this.dirtyIndices) {
this.updateIndices();
}
var index = this.indices[this.getKey(item)];
return (index === undefined) ? -1 : index;
},
/**
* Returns index within the collection of the passed key.
* @param {String} key The key to find the index of.
* @return {Number} Index of the key.
*/
indexOfKey: function(key) {
if (this.dirtyIndices) {
this.updateIndices();
}
var index = this.indices[key];
return (index === undefined) ? -1 : index;
},
updateIndices: function() {
var items = this.items,
ln = items.length,
indices = this.indices = {},
i, item, key;
for (i = 0; i < ln; i++) {
item = items[i];
key = this.getKey(item);
indices[key] = i;
}
this.dirtyIndices = false;
},
/**
* Returns the item associated with the passed key OR index. Key has priority over index. This is the equivalent of
* calling {@link #getByKey} first, then if nothing matched calling {@link #getAt}.
* @param {String/Number} key The key or index of the item.
* @return {Object} If the item is found, returns the item. If the item was not found, returns `undefined`. If an item
* was found, but is a Class, returns `null`.
*/
get: function(key) {
var me = this,
fromMap = me.map[key],
item;
if (fromMap !== undefined) {
item = fromMap;
}
else if (typeof key == 'number') {
item = me.items[key];
}
return typeof item != 'function' || me.getAllowFunctions() ? item : null; // for prototype!
},
/**
* Returns the item at the specified index.
* @param {Number} index The index of the item.
* @return {Object} The item at the specified index.
*/
getAt: function(index) {
return this.items[index];
},
/**
* Returns the item associated with the passed key.
* @param {String/Number} key The key of the item.
* @return {Object} The item associated with the passed key.
*/
getByKey: function(key) {
return this.map[key];
},
/**
* Returns `true` if the collection contains the passed Object as an item.
* @param {Object} item The Object to look for in the collection.
* @return {Boolean} `true` if the collection contains the Object as an item.
*/
contains: function(item) {
var key = this.getKey(item);
if (key) {
return this.containsKey(key);
} else {
return Ext.Array.contains(this.items, item);
}
},
/**
* Returns `true` if the collection contains the passed Object as a key.
* @param {String} key The key to look for in the collection.
* @return {Boolean} `true` if the collection contains the Object as a key.
*/
containsKey: function(key) {
return typeof this.map[key] != 'undefined';
},
/**
* Removes all items from the collection. Fires the {@link #clear} event when complete.
*/
clear: function(){
var me = this;
me.length = 0;
me.items.length = 0;
me.keys.length = 0;
me.all.length = 0;
me.dirtyIndices = true;
me.indices = {};
me.map = {};
},
/**
* Returns the first item in the collection.
* @return {Object} the first item in the collection.
*/
first: function() {
return this.items[0];
},
/**
* Returns the last item in the collection.
* @return {Object} the last item in the collection.
*/
last: function() {
return this.items[this.length - 1];
},
/**
* Returns a range of items in this collection
* @param {Number} [startIndex=0] The starting index.
* @param {Number} [endIndex=-1] The ending index. Defaults to the last item.
* @return {Array} An array of items.
*/
getRange: function(start, end) {
var me = this,
items = me.items,
range = [],
i;
if (items.length < 1) {
return range;
}
start = start || 0;
end = Math.min(typeof end == 'undefined' ? me.length - 1 : end, me.length - 1);
if (start <= end) {
for (i = start; i <= end; i++) {
range[range.length] = items[i];
}
} else {
for (i = start; i >= end; i--) {
range[range.length] = items[i];
}
}
return range;
},
/**
* Find the index of the first matching object in this collection by a function. If the function returns `true` it
* is considered a match.
* @param {Function} fn The function to be called.
* @param fn.o The object.
* @param fn.k The key.
* @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to this
* MixedCollection.
* @param {Number} [start=0] The index to start searching at.
* @return {Number} The matched index, or -1 if the item was not found.
*/
findIndexBy: function(fn, scope, start) {
var me = this,
keys = me.keys,
items = me.items,
i = start || 0,
ln = items.length;
for (; i < ln; i++) {
if (fn.call(scope || me, items[i], keys[i])) {
return i;
}
}
return -1;
},
/**
* Creates a shallow copy of this collection
* @return {Ext.util.MixedCollection}
*/
clone: function() {
var me = this,
copy = new this.self(),
keys = me.keys,
items = me.items,
i = 0,
ln = items.length;
for(; i < ln; i++) {
copy.add(keys[i], items[i]);
}
copy.getKey = me.getKey;
return copy;
},
destroy: function() {
this.callSuper();
this.clear();
}
});