/** * Class collection of items. The elements may be of any type, the elements can be accessed by a key or index. * @member Terrasoft.core.collections */ Ext.define("Terrasoft.core.collections.Collection", { extend: "Terrasoft.core.collections.BaseCollection", alternateClassName: "Terrasoft.Collection", mixins: { sortable: "Terrasoft.Sortable", filterable: "Terrasoft.Filterable" }, /** * Composite collection object * @private * @type {Ext.util.MixedCollection} */ collection: null, /** * Observable events. * @private * @type {String[]} */ observableEvents: [ /** * @event add * Fires after adding an element to the collection, when called {@link #add}. * @param {Object} item Inserted item. * @param {Number} index Index. * @param {String} key Key. */ "add", /** * @event remove * Fires after removing an item from the collection, when called {@link #remove}. * @param {Object} item Removed item. * @param {String} key Key. */ "remove", /** * @event dataLoaded * Fires after the collection is filled with data, when called {@link #loadAll}. * @param {Terrasoft.Collection} collection * @param {Array/Object/Terrasoft.Collection} items Added items */ "dataLoaded", /** * @event clear * Fires after the collection is cleaned, when called {@link #clear}. */ "clear", /** * @event move * Fires on move. * @param {Number} fromIndex Moving element index. * @param {Number} toIndex Destination index. * @param {String} fromKey Moving element key. */ "move", /** * @event replace * Fires when item is replaced by another item. * @param {Object} removedItem Removed item. * @param {String} removedItemKey Removed item key. * @param {Object} insertedItem Inserted item. * @param {String} insertedItemKey Inserted item key. * @param {Number} index Item index. */ "replace", /** * @event changed * Fires when any item was added or removed, when called {@link #add}, {@link #remove} or {@link #loadAll}. * @param {Object} item Collection item. * @param {String} key Item key. */ "changed"], /** * Creates a shallow copy of this collection * @return {Terrasoft.Collection} Copy of collection. */ clone: function () { var result = new this.self(); result.collection = this.collection.clone(); return result; }, /** * Extracts the key from an object. * @param {Object} o The object to get the key from. * @return {String} The key to use. */ getKey: null, /** * Sorts the collection by the comparator function, sorts the same instance of the collection * @param {Function} sortFn A comparator function that compares two elements. If the first element is greater, * the function should return 1 if the first element is less than -1, and if the elements are equal then 0. * @param {Mixed} sortFn.item1 First item. * @param {Mixed} sortFn.item2 Second item. * @param {Object} scope The scope of sortFn function. */ sortByFn: function (sortFn, scope) { this.collection.sortBy(sortFn.bind(scope || window)); }, /** * Filter the collection by the filter function, return a new collection instance with the filtered elements. * The source collection does not change. * @param {Function} filterFn A filter function that checks the element and if the element satisfies the conditions * returns true otherwise false. * @param {Mixed} filterFn.item Item. * @param {String} filterFn.key Item key. * @param {Object} scope (optional) The scope of filterFn function. * @return {Terrasoft.Collection} Returns a new instance of the filtered collection. */ filterByFn: function (filterFn, scope) { var result = new this.self(); result.collection = this.collection.filterBy(filterFn, scope); return result; }, /** * Returns the first item which passes a truth test. * @param {Function} fn Test function. * @param {Object} scope The scope of Fn function. */ findByFn: function (fn, scope) { var items = this.getItems(); var result = _.find(items, fn, scope); return result; }, /** * Returns a new collection with each element being the result of the Fn function. * @param {Function} fn Converter function. * @param {Object} scope The scope of Fn function. * @return {Terrasoft.Collection} */ map: function (fn, scope) { var items = this.getItems(); var result = new this.self(); Terrasoft.each(items, function (item, key) { var newItem = fn.call(scope, item, key); result.add(key, newItem); }, this); return result; }, /** * Returns array with each element being the result of the Fn function. * @param {Function} fn Converter function. * @param {Object} scope The scope of Fn function. * @return {Array} */ mapArray: function (fn, scope) { var items = this.getItems(); var result = []; Terrasoft.each(items, function (item, key) { var newItem = fn.call(scope, item, key); result.push(newItem); }, this); return result; }, /** * Checks index. * @private * @param {Number} index Index. * @throws {Terrasoft.ItemNotFoundException} If collection does not have items with index thorws exception. */ checkIndex: function (index) { if (index < 0 || index >= this.getCount()) { var message = Ext.String.format("{0} [{1}] {2}", Terrasoft.Resources.Collection.ItemWithIndex, index, Terrasoft.Resources.Collection.DoesNotExists); throw new Terrasoft.ItemNotFoundException({ message: message }); } }, /** * Handles replacement event of the current item. * @private * @param {Object} config Replace information. * @param {Mixed} config.removedItem Removed item. * @param {String} config.removedItemKey Removed item key. * @param {Mixed} config.insertedItem Inserted item. * @param {String} config.insertedItemKey Inserted item key. * @param {Number} config.index Item index. */ onReplaceItem: function (config) { if (this.fireEvent("replace", config) === false) { return; } this.onCollectionRemove(config.removedItem, config.removedItemKey); this.onCollectionAdd(config.index, config.insertedItem, config.insertedItemKey); }, /** * Creates an instance of the collection * @param {Object} config Configuration object * @return {Terrasoft.Collection} Returns the created collection instance */ constructor: function () { this.callParent(arguments); this.addEvents.apply(this, this.observableEvents); var collection = this.collection = Ext.create("Ext.util.MixedCollection"); collection.on("add", this.onCollectionAdd, this); collection.on("remove", this.onCollectionRemove, this); collection.on("clear", this.onCollectionClear, this); if (Ext.isFunction(this.getKey)) { this.collection.getKey = this.getKey; } }, /** * The event handler for adding an item to the collection * @protected * @param {Number} index * @param {Mixed} item Added item * @param {String} key */ onCollectionAdd: function (index, item, key) { this.fireEvent("add", item, index, key); this.fireEvent("changed"); }, /** * The event handler for removing an item from the collection * @protected * @param {Mixed} item Deleted item * @param {String} key */ onCollectionRemove: function (item, key) { this.fireEvent("remove", item, key); this.fireEvent("changed"); }, /** * Cleanup event handler * @protected */ onCollectionClear: function () { this.fireEvent("clear"); this.fireEvent("changed"); }, /** * Move event handler. * @protected * @param {Number} fromIndex Moving element index. * @param {Number} toIndex Destination index. * @param {String} fromKey Moving element name. */ onCollectionMove: function (fromIndex, toIndex, fromKey) { this.fireEvent("move", fromIndex, toIndex, fromKey); }, /** * Checks whether the collection is empty * @override * @return {Boolean} Returns true if there is no element in the collection * false otherwise */ isEmpty: function () { return this.getCount() === 0; }, /** * Returns number of items in the collection. * @override * @return {Number} Number of items in the collection. */ getCount: function () { return this.collection.getCount(); }, /** * Returns collection keys. * @return {Array} Returns collection keys array. */ getKeys: function () { return this.collection.keys; }, /** * Returns collection elements. * @return {Array} Returns collection elements array. */ getItems: function () { return this.collection.items; }, /** * Returns element by key. * @override * @throws {Terrasoft.ItemNotFoundException} * Throws an exception{@link Terrasoft.ItemNotFoundException} if element with such key not found. * For a safe receiving the item, use method {@link Terrasoft.Collection#find} or check with method {@link #contains} * @param {String} key Key * @return {} Collection element. */ get: function (key) { if (!this.contains(key)) { throw new Terrasoft.ItemNotFoundException({ message: Terrasoft.Resources.Collection.ItemWithKey + " " + key + " " + Terrasoft.Resources.Collection.DoesNotExists }); } return this.collection.getByKey(key); }, /** * Returns the element by index. * @override * @throws {Terrasoft.ItemNotFoundException} * Throws an exception {@link Terrasoft.ItemNotFoundException} if an element is not found for such an index. * To securely retrieve an item, use the {@link #find} method or check with * the {@link #contains} method * @return {} Collection item. */ getByIndex: function (index) { var item = this.findByIndex(index); if (!item) { var resources = Terrasoft.Resources.Collection; var message = resources.ItemWithIndex + " [" + index + "] " + resources.DoesNotExists; var error = new Terrasoft.ItemNotFoundException({ message: message }); if (index === this.getCount()) { this.warning(error.toString()); } else { throw error; } } return item; }, /** * Returns an element by index or null if the item is not found. * @param {Number} index Collection index. * @return {Mixed|null} */ findByIndex: function (index) { if (index < 0 || index >= this.getCount()) { return null; } return this.collection.getAt(index); }, /** * Returns an item by key * @override * @param {String} key * @return {} Item */ find: function (key) { return this.collection.getByKey(key); }, /** * Returns an item by nested property value. * See also {@link Terrasoft.utils.object#findValueByPath} * @param {String} path Item properties name path delimited with dot. * @param {Object} value Value. * @return {Object} Item. */ findByPath: function (path, value) { return this.findByFn(function (item) { var itemValue = Terrasoft.findValueByPath(item, path); return _.isEqual(itemValue, value); }, this); }, /** * Returns an items by nested property value. * @param {String} path Item properties name path delimited with dot. * @param {Object} value Value. * @return {Terrasoft.Collection} */ filterByPath: function (path, value) { return this.filterByFn(function (item) { var itemValue = Terrasoft.findValueByPath(item, path); return _.isEqual(itemValue, value); }, this); }, /** * Returns an item by attribute value. * @param {String} attr Item attribute name. * @param {Object} value Value. * @return {Object} Item. */ findByAttr: function (attr, value) { return this.filterByFn(function (item) { return _.isEqual(item.get(attr), value); }, this).first(); }, /** * Adds an item to the collection * @override * @param {String} key Key * @param {Object} item Item * @param {Number} [index] index for insertion, if not specified, it is ignored * @return {Object} Returns the added item */ add: function (key, item, index) { this.callParent(arguments); var collection = this.collection; var addedItem = Ext.isNumber(index) ? collection.insert(index, key, item) : collection.add(key, item); return addedItem; }, /** * Adds item to the collection if it not exists by the specified key. * @param {String} key Item key. * @param {Mixed} item Item. * @param {Number} [index] Optional, collection order index. * @return {} if item exists returns null, otherwise returns added item */ addIfNotExists: function (key, item, index) { if (this.contains(key)) { return null; } return this.add(key, item, index); }, /** * Inserts the obj element at the specified index into the "index" collection * @param {Number} index * @param {String} key * @param {Mixed} obj Inserted element */ insert: function (index, key, obj) { return this.collection.insert(index, key, obj); }, /** * Returns the index of an item * @override * @param {Mixed} obj Item * @return {Number} Index */ indexOf: function (obj) { return this.collection.indexOf(obj); }, /** * Removes an item from the collection * @override * @param {Mixed} item * @return {} Returns the deleted item or false if nothing is deleted */ remove: function (item) { return this.removeByIndex(this.collection.indexOf(item)); }, /** * Deletes an item by key * @override * @param {String} key * @return {} Returns the deleted item or false if nothing is deleted */ removeByKey: function (key) { return this.removeByIndex(this.collection.indexOfKey(key)); }, /** * Deletes an item by index * @override * @param {Number} index * @return {} Returns the deleted item or false if nothing is deleted */ removeByIndex: function (index) { return this.collection.removeAt(index); }, /** * Checks if the specified key is in the collection * @override * @param {String} key * @return {Boolean} Returns true if such key exists, otherwise false */ contains: function (key) { return this.collection.containsKey(key); }, /** * Goes through the collection keys and executes Fn function for each key. Fn function should return true. * Returning False value from the function will stop executing the iteration. * @override * @param {Function} fn Function to execute. * @param {Mixed} fn.item Collection element. * @param {Number} fn.index Element index. * @param {Number} fn.len Number of elements in collection * @param {Object} scope (optional) Fn function execution context. */ each: function (fn, scope) { this.collection.each(fn, scope); }, /** * Goes async through the collection items and executes Fn function for each item. * @param {Function} fn Function to execute. Call in scope context. * @param {Function} callback Callback function, invoke after Fn function should be executed over each * collection item. * @param {Object} scope Callback function call context. */ eachAsync: function (fn, callback, scope) { var items = this.getItems(); Terrasoft.eachAsync(items, fn, callback, scope); }, /** * Goes through the collection keys and executes Fn function for each key. Fn function should return true. * Returning False value from the function will stop executing the iteration. * @override * @param {Function} fn Function to execute. * @param {String} fn.key Element key. * @param {Mixed} fn.item Element. * @param {Number} fn.index Element index. * @param {Number} fn.len Number of elements in collection. * @param {Object} scope (optional) Fn function execution context. */ eachKey: function (fn, scope) { this.collection.eachKey(fn, scope); }, /** * Clears collection. * @override */ clear: function () { this.collection.clear(); }, /** * Initiates filling data collection. When calling event is generated {@link #event-dataLoaded}. * @throws {Terrasoft.UnsupportedTypeException} If items is array. * @param {Object/Terrasoft.Collection} items Elements that will be initialized collection. */ loadAll: function (items, options) { if (Ext.isArray(items)) { throw new Terrasoft.UnsupportedTypeException({ message: Terrasoft.Resources.Collection.ArrayUnsupportedInputType }); } var collection = this.collection; this.suspendEvents(false); try { if (items instanceof Terrasoft.Collection) { items.eachKey(function (key, item) { if (options && options.mode === "top") { collection.insert(0, key, item); } else { collection.add(key, item); } }, this); } else { var newCollection = Ext.create("Terrasoft.Collection"); Terrasoft.each(items, function (item, key) { newCollection.add(key, item); }, this); this.resumeEvents(); this.loadAll(newCollection, options); return; } } finally { this.resumeEvents(); } this.fireEvent("dataLoaded", this, items, options); this.fireEvent("changed"); }, /** * Moves elements in collection. * @param {Number} fromIndex Moved element index. * @param {Number} toIndex Destination index. */ move: function (fromIndex, toIndex) { this.checkIndex(fromIndex); this.checkIndex(toIndex); var collection = this.collection; var removedItem = collection.getAt(fromIndex); var keys = this.getKeys(); var removedItemKey = keys[fromIndex]; collection.suspendEvents(); collection.removeAt(fromIndex); collection.insert(toIndex, removedItemKey, removedItem); collection.resumeEvents(); this.onCollectionMove(fromIndex, toIndex, removedItemKey); }, /** * Replaces item if it exists with another one. * @param {Mixed} itemToReplace Item that will be removed from collection. * @param {Mixed} newItem Item that will be inserted instead. * @param {String} [newItemKey] Key, if not defined the key will not be changed. */ replace: function (itemToReplace, newItem, newItemKey) { var collection = this.collection; var index = collection.indexOf(itemToReplace); if (index === -1) { return; } var keys = this.getKeys(); var removedItemKey = keys[index]; var newKey = newItemKey || removedItemKey; collection.suspendEvents(); collection.removeAt(index); collection.insert(index, newKey, newItem); collection.resumeEvents(); var itemReplaceData = { removedItem: itemToReplace, removedItemKey: removedItemKey, insertedItem: newItem, insertedItemKey: newKey, index: index }; this.onReplaceItem(itemReplaceData); }, /** * Returns first element from search result. If no such element found or no search function passed - returns null. * @param {Function} filterFn Search function. * @param {Object} scope Filter function scope. * @returns {} Collection element. */ firstOrDefault: function (filterFn, scope) { return filterFn ? this.filterByFn(filterFn, scope).first() : this.first(); }, /** * Produces the set difference of two collections by using the defaultEqualityFn to compare items * if no equalityFn passed. * @param collection {Terrasoft.Collection} Collection whose elements that also occur in the first collection * will cause those elements to be removed from the returned sequence. * @param equalityFn {Function} Equality function. * @param equalityFn.value1 {Object} First object to compare. * @param equalityFn.value2 {Object} Second object to compare. * @param equalityFn.key1 {Object} First key to compare. * @param equalityFn.key2 {Object} Second key to compare. * @param scope {Object} Equality function scope. * @returns {Terrasoft.Collection} A sequence that contains the set difference of the elements of two sequences. */ except: function (collection, equalityFn, scope) { equalityFn = equalityFn || this.defaultEqualityFn; var result = Ext.create("Terrasoft.Collection"); var keys = this.getKeys(); var items = this.getItems(); items.forEach(function (value, index) { var currentKey = keys[index]; var currentItem = items[index]; var containsInExceptedCollection = collection.any(function (value, key) { return equalityFn.call(scope, currentItem, value, currentKey, key); }, this); if (!containsInExceptedCollection) { result.add(currentKey, currentItem); } }, this); return result; }, /** * Default items equality function. * @private * @param value1 {Object} First object to compare. * @param value2 {Object} Second object to compare. * @param key1 {Object} First key to compare. * @param key2 {Object} Second key to compare. * @returns {boolean} */ defaultEqualityFn: function (value1, value2, key1, key2) { return key1 === key2 || value1 === value2; }, /** * Determines whether any element of a collection satisfies a condition. * @param predicate * @param scope * @returns {boolean} */ any: function (predicate, scope) { var keys = this.getKeys(); var items = this.getItems(); var result = items.some(function (value, index) { var key = keys[index]; return predicate.call(scope, value, key); }, this); return result; }, /** * Projects each element of a collection into a new form. * @param selectFn {Function} Selection function. * @param selectFn.item {Object} Item value. * @param scope {Object} Selection fn scope. * @returns {Terrasoft.Collection} Collection selected.data */ select: function (selectFn, scope) { return this.selectKeyValue(function (value, key) { return { key: key, value: selectFn.call(scope, value) }; }, scope); }, /** * Projects each key value pair of a collection into a new form. * @param selectFn {Function} Selection function. * @param selectFn.value {Object} Item value. * @param selectFn.key {Object} Item key. * @param scope {Object} Selection fn scope. * @returns {Terrasoft.Collection} Collection selected. */ selectKeyValue: function (selectFn, scope) { var result = Ext.create("Terrasoft.Collection"); var keys = this.getKeys(); var items = this.getItems(); items.forEach(function (value, index) { var key = keys[index]; var newItem = selectFn.call(scope, value, key); result.add(newItem.key, newItem.value); }, this); return result; }, /** * Returns a specified number of contiguous elements from the start of a collection. * @param takeCount {Number} Number of items to take. * @returns {Terrasoft.Collection} Collection taken. */ take: function (takeCount) { return this.getRange(0, takeCount); }, /** * Returns a specified number of elements from the start index of a collection. * @param startIndex {Number} Index at which range starts. * @param count {Number} The number of elements in the range. * @returns {Terrasoft.Collection} Collection with given range. */ getRange: function (startIndex, count) { var result = Ext.create("Terrasoft.Collection"); var collectionCount = this.getCount(); var endIndex = startIndex + count; if (endIndex > collectionCount) { endIndex = collectionCount; } var keys = this.getKeys(); var items = this.getItems(); for (var i = startIndex; i < endIndex; i++) { result.add(keys[i], items[i]); } return result; }, /** * Returns first element. * @return {} Collection element. */ first: function () { return this.collection.first(); }, /** * Returns last element. * @return {} Collection element. */ last: function () { return this.collection.last(); }, /** * Reload collection data. * @param {Object/Terrasoft.Collection} items Elements that will be initialized collection. */ reloadAll: function (items) { this.clear(); this.loadAll(items); } });