UNPKG

decks

Version:

JavaScript UI library for viewing collections of items.

467 lines (385 loc) 13.8 kB
var _ = require("lodash"); var hasEmitter = require("./events").hasEmitter; var binder = require("./events").binder; var DecksEvent = require("./events").DecksEvent; var Item = require("./item"); var validate = require("./utils/validate"); /** * Represents a collection of Items * * @class * @mixes binder * @param {(Item[]|Object[])} items items with which to initialize collection */ function ItemCollection(items, options) { if (!(this instanceof ItemCollection)) { return new ItemCollection(items); } items = items || []; options = _.merge({}, this.defaultOptions, options); this.setEmitter(options.emitter || {}); this.items = {}; this.addItems(items); this.emit(DecksEvent("item:collection:ready", this)); } _.extend(ItemCollection.prototype, binder, hasEmitter, /** @lends ItemCollection.prototype */ { /** * Default options for ItemCollection instances */ defaultOptions: { }, /** * Item events to which to bind for each Item added to the collection. * * @return {undefined} */ getItemEvents: function() { return { "*": "onAnyItemEvent" }; }, /** * Binds to all {@link Item} events. * * It's not necessary to call this directly, unless you called this.unbind at some point. * * @return {undefined} */ bind: function bind() { _.each(this.getItems(), function(item) { this.bindEvents(item, this.getItemEvents()); }, this); }, /** * Unbinds from all {@link Item} events. * * @return {undefined} */ unbind: function bind() { _.each(this.getItems(), function(item) { this.unbindEvents(item, this.getItemEvents()); }, this); }, /** * Destroys the {@link ItemCollection} * * @return {undefined} */ destroy: function() { this.unbind(); _.each(this.getItems(), function(item) { item.destroy(); }, this); }, /** * Gets a single item from the collection * * @param {!String} id - Item id to find * @return {Item} - Item or null if not found */ getItem: function getItem(id) { if (_.isNumber(id)) { id = "" + id; } validate(id, "id", { isString: true }); return this.items[id]; }, /** * Gets all {@link Item}s from the collection, or a subset of {@link Item}s based on an optional filter function. * * @param {?Function} filter - the filter predicate function (takes an item and should return whether Item passes the filter) * @return {Item[]} - Array of {@link Item}s (not an {@link ItemCollection} instance) */ getItems: function getItems(filter) { var items = _.values(this.items); if (filter) { items = _.filter(items, filter); } return items; }, /** * Adds an {@link Item} to the {@link ItemCollection}. * * An event is emitted just before adding the {@link Item} and just after. * * After adding the {@link Item} the {@link ItemCollection} is re-indexed, based on the current * filter function, sortBy function, and reversed flag. * * @param {!(Item|Object)} item - item instance or options to add * @param {?Object} [options={}] - additional options * @param {?boolean} [options.silent=false] - if true, the {@link ItemCollection} will not emit events * @param {?boolean} [options.noIndex=false] - if true, the {@link ItemCollection} will not be re-indexed after adding the {@link Item} * @return {Item} - the {@link Item} instance that was added */ addItem: function addItem(item, options) { validate(item, "item", { isRequired: true }); options = options || {}; if (!(item instanceof Item)) { item = new Item(item); } if (this.items[item.id]) { throw new Error("ItemCollection#addItem: collection already contains an item with id " + item.id); } if (!options.silent) { this.emit(DecksEvent("item:collection:item:adding", this, item)); } this.items[item.id] = item; this.bindEvents(item, this.getItemEvents()); if (!options.noIndex) { this.index({ isAddItem: true }, { silent: options.silent }); } if (!options.silent) { this.emit(DecksEvent("item:collection:item:added", this, item)); } return item; }, /** * Adds an array of items to the collection. * * Events and indexing will be suppressed for each individual {@link Item} added. * * @param {!(Item[]|Object[])} items - Array of {@link Item} instances or options objects to add * @param {?Object} [options={}] - additional options * @param {?boolean} [options.silent=false] - if true, no events will be emitted * @param {?boolean} [options.noIndex=false] - if true, the {@link ItemCollection} will not be re-indexed * @return {Item[]} - Array of {@link Item} instances that were added. */ addItems: function addItems(items, options) { validate(items, "items", { isArray: true }); options = options || {}; if (_.isEmpty(items)) { return; } if (!options.silent) { this.emit(DecksEvent("item:collection:items:adding", this)); } items = _.map(items, function(item) { return this.addItem(item, { silent: true, noIndex: true }); }, this); if (!options.noIndex) { this.index({ isAddItems: true }, { silent: options.silent }); } if (!options.silent) { this.emit(DecksEvent("item:collection:items:added", this, items)); } return items; }, /** * Removes an item from the collection * * @param {Item} item item to remove * @param {?Object} options additional options * @return {undefined} */ removeItem: function removeItem(item, options) { validate(item, "item", { isInstanceOf: Item }); options = options || {}; if (!_.has(this.items, item.id)) { return; } if (!options.silent) { this.emit(DecksEvent("item:collection:item:removing", this, item)); } this.unbindEvents(item, this.getItemEvents()); delete this.items[item.id]; if (!options.noIndex) { this.index({ isRemoveItem: true }, { silent: options.silent }); } if (!options.silent) { this.emit(DecksEvent("item:collection:item:removed", this, item)); } return item; }, /** * Clears the collection by removing all the {@link Item}s one-by-one. * * Events and indexing will be suppressed for each individual {@link Item} removed. * * @param {?Object} options additional options * @return {undefined} */ clear: function clear(options) { options = options || {}; if (_.isEmpty(this.items)) { return; } if (!options.silent) { this.emit(DecksEvent("item:collection:clearing", this)); } var items = _.map(this.items, function(item) { return this.removeItem(item, { silent: true, noIndex: true }); }, this); // There's not really a need to re-index the collection, but some things rely // on the indexing events being emitted if (!options.noIndex) { this.index({ isClear: true }, { silent: options.silent }); } if (!options.silent) { this.emit(DecksEvent("item:collection:cleared", this, items)); } return items; }, /** * Sets or clears the current filter function. * * The filter function is used to filter {@link Item}s out of the {@link ItemCollection} based * on a predicate. The {@link Item}s are marked as filtered-out by setting the {@link Item} index * to -1 (they are not actually removed from the collection, just flagged in this way). * * @param {?Function} [filter=null] - the filter function (or null to clear the current filter) * @param {?Object} [options={}] - additional options * @param {?boolean} [options.silent=false] - if true, events will not be emitted * @param {?boolean} [options.noIndex=false] - if true, the collection will not be re-indexed with the new filter * @return {undefined} */ setFilter: function setFilter(filter, options) { if (!_.isFunction(filter)) { filter = null; } options = options || {}; if (this.filter === filter) { return; } if (!options.silent) { this.emit(DecksEvent("item:collection:filter:setting", this, { oldFilter: this.filter, newFilter: filter })); } this.filter = filter; if (!options.noIndex) { this.index({ isSetFilter: true }, { silent: options.silent }); } if (!options.silent) { this.emit(DecksEvent("item:collection:filter:set", this, this.filter)); } }, /** * Sets or clears the current sort by function. * * The sort by function is used to sort the {@link Item}s in the collection. * The sortBy function should return a sortable value for each {@link Item}. * The sort is applied by updating the {@link Item} index values. * * @param {?Function} [sortBy=null] - the sort by function (or null to clear the current sort) * @param {?Object} [options={}] - additional options * @param {?boolean} [options.silent=false] - if true, events will not be emitted * @param {?boolean} [options.noIndex=false] - if true, the collection will not be re-indexed with the new sort * @return {undefined} */ setSortBy: function setSortBy(sortBy, options) { if (!_.isFunction(sortBy)) { sortBy = null; } options = options || {}; if (this.sortBy === sortBy) { return; } if (!options.silent) { this.emit(DecksEvent("item:collection:sort:by:setting", this, { oldSortBy: this.sortBy, newSortBy: sortBy })); } this.sortBy = sortBy; if (!options.noIndex) { this.index({ isSetSortBy: true }, { silent: options.silent }); } if (!options.silent) { this.emit(DecksEvent("item:collection:sort:by:set", this, this.sortBy)); } }, /** * Reverses the sort on the collection. * * The reversing is applied by re-indexing all the {@link Item}s in the {@link ItemCollection}. * * @param {?boolean} [isReversed=false] - whether the collection should be reversed. * @param {?Object} [options={}] - additional options * @param {?boolean} [options.silent=false] - if true, events will not be emitted * @param {?boolean} [options.noIndex=false] - if true, the collection will not be re-indexed with the new reverse value. * @return {undefined} */ setReversed: function setReversed(isReversed, options) { isReversed = !!isReversed; options = options || {}; if (this.isReversed === isReversed) { return; } if (!options.silent) { this.emit(DecksEvent("item:collection:reversed:setting", this, { oldIsReversed: this.isReversed, newIsReversed: isReversed })); } this.isReversed = isReversed; if (!options.noIndex) { this.index({ isSetReversed: true }, { silent: options.silent }); } if (!options.silent) { this.emit(DecksEvent("item:collection:reversed:set", this, this.isReversed)); } }, /** * Indexes the {@link ItemCollection} based on the current filter function, sortBy function, and * reversed flag. Each {@link Item} in the collection will have it's index value set based on * the filter/sortBy/reversed representation of the {@link ItemCollection} items. * * The sorting and filtering is applied to the {@link Item}s by updating the {@link Item} index values. * {@link Item}s are not physically moved or removed from the {@link ItemCollection}. * * If {@link Item}s are filtered out via the filter function, the {@link Item} index will be set to * -1. The {@link Layout#getRenders} method can check if item.index is -1 to decide if the {@link Item} * should be displayed. * * @param {!Object} reason - an object which indicates why the collection is being indexed * @param {?Object} options - additional options * @param {?boolean} [options.silent=false] - if true, no events will be emitted * @return {undefined} */ index: function index(reason, options) { validate(reason, "reason", { isRequired: true }); options = options || {}; // Create a list of filtered, sorted, and reversed items, then find the index of each item // in the reference list. If the item is not in the ref list, it will get an index of -1, // which means it should not be drawn (or should be hidden). var referenceItems = _(this.getItems()); if (this.filter) { referenceItems = referenceItems.filter(this.filter); } if (this.sortBy) { referenceItems = referenceItems.sortBy(this.sortBy); } if (this.isReversed) { referenceItems = referenceItems.reverse(); } referenceItems = referenceItems.value(); var items = this.getItems(); var totalCount = items.length; var referenceCount = referenceItems.length; if (!options.silent) { this.emit(DecksEvent("item:collection:indexing", this, { reason: reason, totalCount: totalCount, referenceCount: referenceCount })); } var changedCount = _.reduce(items, function indexItem(count, item) { var index = _.indexOf(referenceItems, item); if (item.setIndex(index, { silent: options.silent })) { count++; } return count; }, 0); if (!options.silent) { this.emit(DecksEvent("item:collection:indexed", this, { reason: reason, totalCount: totalCount, referenceCount: referenceCount, changedCount: changedCount })); } }, /** * Called for {@link Item} events. Forwards the {@link Item} events via the {@ItemCollection} * emitter. * * @param e * @return {undefined} */ onAnyItemEvent: function onAnyItemEvent(e) { this.emit(e); } }); module.exports = ItemCollection;