UNPKG

3m5-coco

Version:

a simple MVC Framework

731 lines (630 loc) 18.9 kB
"use strict"; var Coco = Coco || {}, _ = require("underscore"); Coco.Event = require("../event/Coco.Event.js"), Coco.EventDispatcher = require("../event/Coco.EventDispatcher.js"), Coco.ModelEvent = require("../event/Coco.ModelEvent.js"), Coco.Utils = require("../lib/Coco.Utils.js"), Coco.Model = require("./Coco.Model.js"), Coco.Math = require("../lib/Coco.Math.js"); /** * Class: Coco.Collection * * extends: <Coco.Event> * * Description: * This class holds an array of model instances and provides some helping functions. * * @author Johannes Klauss <johannes.klauss@3m5.de> */ module.exports = dejavu.Class.declare({ $name: 'Collection', $extends: Coco.EventDispatcher, /** * The internal collection id. */ __id: null, /** * A private class identifier, copied from `this.$name` */ __$name: "Collection", /** * Variable: _models * {protected} Array of Coco.Model objects. */ _models: [], /** * Variable: __handles * {private} Map of EventListener-handles for models from _models-Array */ __handles: null, /** * Variable: _modelClass * The class of the models. Calling <Coco.Collection.createOne> and <Coco.Collection.add> will always add models * with the class referred here. * {protected} */ _modelClass: null, /** * Function: Constructor * * Parameter: * @param {Array} $models - The models to add initial. */ initialize: function initialize($models) { if (this._modelClass == null || !this._modelClass.prototype || this._modelClass.prototype.__$name !== 'Model' && this._modelClass.$parent && this._modelClass.$parent.prototype.__$name !== 'Model') { throw new Error("Cannot create Collection '" + this.$name + "' with '_modelClass' being null or not extending from Coco.Model."); } this.__$name = this.$name; this.__id = Coco.Utils.uniqueId("c"); this.__handles = new Map(); this._onInitialize($models); if ($models != null) { this.add($models); } }, /** * Function: _onInitialize * Function is called after class is initialized, but BEFORE models are added. * * Parameter: * @param {Array} $models - The models to add initial. You can change those by altering the this parameter. */ _onInitialize: function _onInitialize($models) {}, /** * Function: add * Adds an array of models to the collection. This can be either an instance of Coco.Model or attributes or an Array * containing one of both. * * Parameter: * @param {Coco.Model|Object|Array} attributes - Array of models to add. * * Event: * Triggers <Coco.Event.ADD> event with each model that has been added. */ add: function add(attributes) { var _this = this; if (attributes == null) { return; } if (!(attributes instanceof Array)) { attributes = [attributes]; } var model = null; for (var i = 0; i < attributes.length; i++) { if (attributes[i] == null) { continue; } // If attribute is a model store it, otherwise create a new model and set it's attributes. model = !(attributes[i] instanceof this._modelClass) ? new this._modelClass(attributes[i]) : attributes[i]; var handle = model.addEventListener(Coco.Event.DESTROY, function (event) { _this.__onModelDestroy(event); }, true); this._models.push(model); this._addModelHandle(handle, model); this._dispatchEvent(new Coco.ModelEvent(Coco.Event.ADD, model)); } }, /** * Function: _addModelHandle * * stores all handles for one model, to delete eventlistener, after rmoving model from collection * * Parameter: * @param {Coco.Model} model - model to add handle for * * @param {Symbol} handle - handle to add * * @protected */ _addModelHandle: function _addModelHandle(handle, model) { var mh = this.__handles.get(model.getId()); if (mh == null) { mh = []; } mh.push(handle); this.__handles.set(model.getId(), mh); }, /** * Function: _removeModelHandles * * removes all handles for given model * * Parameter: * @param {Coco.Model} model - model to remove EventListener from * @protected */ _removeModelHandles: function _removeModelHandles(model) { var mh = this.__handles.get(model.getId()); if (mh != null && mh.length > 0) { for (var i = 0; i < mh.length; i++) { model.removeEventListener(mh[i]); } } this.__handles.delete(model.getId()); }, /** * Function: insertAt * Add a model a the specified index to the collection * * Parameter: * @param {integer} index - The index position. * * @param {Coco.Model} model - The <Coco.Model> instance to add. * * Event: * Triggers <Coco.Event.ADD> event */ insertAt: function insertAt(index, model) { if (model instanceof this._modelClass) { if (index >= 0 && index <= this._models.length) { this._models.splice(index, 0, model); this._dispatchEvent(new Coco.ModelEvent(Coco.Event.ADD, model)); } else { throw new Error("index out of bound error"); } } }, /** * Function: createOne * Creates a new model based on given attributes. * * Parameter: * @param {Object} $attributes - {optional} The attributes the new model should have. * * Return: * @return {Coco.Model} - The created model. * * Event: * Triggers <Coco.Event.ADD> event. */ createOne: function createOne($attributes) { var _this2 = this; if ($attributes instanceof this._modelClass) { return null; } var model = new this._modelClass($attributes); var handle = model.addEventListener(Coco.Event.DESTROY, function (event) { _this2.__onModelDestroy(event); }, true); this._models.push(model); this._addModelHandle(handle, model); this._dispatchEvent(new Coco.ModelEvent(Coco.Event.ADD, model)); return model; }, /** * Function: has * checks for existing model in collection. * * Parameter: * @param {Coco.Model} model - An <Coco.Model> instance * * Return: * @returns {boolean} - True if model is in Collection */ has: function has(model) { for (var i = 0; i < this._models.length; i++) { if (model.isEqual(this._models[i])) { return true; } } return false; }, /** * Function: reset * Removes all models from the collection. * * Event: * Triggers <Coco.Event.REMOVE> event for each model that gets removed. * * Triggers <Coco.Event.RESET> event. * * Return: * @return {Coco.Collection} - The <Coco.Collection> instance. */ reset: function reset() { for (var i = 0; i < this._models.length; i++) { //remove all model handles this._removeModelHandles(this._models[i]); this._dispatchEvent(new Coco.ModelEvent(Coco.Event.REMOVE, this._models[i])); } this._models = []; this._dispatchEvent(new Coco.ModelEvent(Coco.Event.RESET, this)); return this; }, /** * Function: remove * Removes one model. * * Parameter: * @param {Coco.Model} model - The instance of <Coco.Model> to remove. * * @param {boolean} $silent - If set to true the model won't trigger the <Coco.Event.REMOVE> event. * * Event: * Triggers <Coco.Event.REMOVE> event if $silent is not set to true. */ remove: function remove(model, $silent) { if (model == null) { console.warn(this.$name + ": can't delete null object!"); return; } for (var i = 0; i < this._models.length; i++) { if (model.isEqual(this._models[i])) { this.removeAt(i, $silent); break; } } }, /** * Function: removeAt * Removes model at specific index position. * * Parameter: * @param {integer} index - The index position. * * @param {boolean} $silent - {optional} If set to true the model won't trigger the `remove` event. * * Event: * Triggers <Coco.Event.REMOVE> event if $silent is not set to true. */ removeAt: function removeAt(index, $silent) { if (this._models.length > index) { var m = this._models.splice(index, 1); this._removeModelHandles(m[0]); if ($silent !== true) { this._dispatchEvent(new Coco.ModelEvent(Coco.Event.REMOVE, m[0])); } } }, /** * Function: getAt * Gets model at specific index. * * Parameter: * @param {integer} index - The index position. * * Return: * @return {Coco.Model} - The remove <Coco.Model> instance. */ getAt: function getAt(index) { if (index >= this._models.length || index < 0) { return null; } return this._models[index]; }, /** * Function: indexOf * Gets the index of a model. * * Parameter: * @param {Coco.Model} model - An <Coco.Model> instance * * Return: * @return {Number} - The index of the <Coco.Model> instance. */ indexOf: function indexOf(model) { for (var i = 0; i < this._models.length; i++) { if (model.isEqual(this._models[i])) { return i; } } return -1; }, /** * Function: getAll * Gets all models as <Coco.Model> instances. * * Return: * @return {Array} - Array of <Coco.Model> instances. */ getAll: function getAll() { return this._models; }, /** * Function: getAllAttributes * Gets all models. This returns an array of all attributes of all models, not the Coco.Model instances. * every model has its own entry * * Return: * @returns {Array} - Array of attributes of <Coco.Model> instances. */ getAllAttributes: function getAllAttributes() { var models = []; $.each(this._models, function (i, e) { // Add all attributes of current model models.push(e.getAttributes()); }); return models; }, /** * Function: each * Iterates over all objects in the collection and executes a given callback function for every model. * * If the callback returns false, the each function breaks. * * Parameter: * @param {Function} callback - The method to execute for each model. Parameters are Coco.Model instance and index. */ each: function each(callback) { if (this._models == null) { return false; } for (var i = 0; i < this._models.length; i++) { if (callback(this._models[i], i) === false) { break; } } }, /** * Function: push * Adds a model at the end of the collection. * * Parameter: * @param {Coco.Model} model - The <Coco.Model> instance to add. * * Event: * Triggers <Coco.Event.ADD> event */ push: function push(model) { var _this3 = this; if (model instanceof this._modelClass) { var handle = model.addEventListener(Coco.Event.DESTROY, function (event) { _this3.__onModelDestroy(event); }, true); this._models.push(model); this._addModelHandle(handle, model); this._dispatchEvent(new Coco.ModelEvent(Coco.Event.ADD, model)); } }, /** * Function: pop * Removes and returns the last model from the collection. * * Return * @return {Coco.Model} - The removed <Coco.Model> instance. * * Event: * Triggers <Coco.Event.REMOVE> event */ pop: function pop() { var model = this._models.pop(); this._dispatchEvent(new Coco.ModelEvent(Coco.Event.REMOVE, model)); return model; }, /** * Function: unshift * Adds a model at the beginning of the collection. * * Parameter: * @param {Coco.Model} model - The <Coco.Model> instance to add. * * Event: * Triggers <Coco.Event.ADD> event */ unshift: function unshift(model) { if (model instanceof this._modelClass) { //TODO ???? //this._removeModelHandles(model); this._dispatchEvent(new Coco.ModelEvent(Coco.Event.ADD, model)); return this._models.unshift(model); } }, /** * Function: shift * Removes and returns the first model from the collection. * * Return: * @return {Coco.Model} - The removed <Coco.Model> instance. * * Event: * Triggers <Coco.Event.REMOVE> event */ shift: function shift() { var model = this._models.shift(); this._dispatchEvent(new Coco.ModelEvent(Coco.Event.REMOVE, model)); return model; }, /** * Function: findBy * Finds models by given query object. The object contains attributes and their values and findBy will match this * against the collections models. * * Parameter: * @param {object} query - The object of attributes and values to look for in the collection. * * Return: * @return {Array} - Array of matched <Coco.Model> instances. */ findBy: function findBy(query) { var models = []; var valid = false; $.each(this._models, function (i, e) { valid = true; $.each(query, function (key, value) { if (!e.has(key) || e.get(key) != value) { valid = false; return false; } }); if (valid) { models.push(e); } }); return models; }, /** * Function: findOneBy * Acts like findBy but returns the first matched model. * * Parameter: * @param {object} query - The object of attributes and values to look for in the collection. * * Return: * @return {Coco.Model|null} - First matched <Coco.Model> instance of null. */ findOneBy: function findOneBy(query) { var model = null; var valid = false; $.each(this._models, function (i, e) { valid = true; $.each(query, function (key, value) { if (!e.has(key) || e.get(key) != value) { valid = false; return false; } }); if (valid) { model = e; return false; } }); return model; }, /** * Function: removeBy * removes all matched models from current collection * * Parameter: * @param {object} query - The object of attributes and values to look for in the collection. * * Return: * @return {Array} - all removed models */ removeBy: function removeBy(query, $silent) { var _this4 = this; var models = []; var modelIndex = []; var valid; $.each(this._models, function (i, e) { valid = true; $.each(query, function (key, value) { if (!e.has(key) || e.get(key) != value) { valid = false; return false; } }); if (valid) { models.push(e); modelIndex.push(i); } }); _.each(modelIndex, function (index) { _this4.removeAt(index, $silent); }); return models; }, /** * Function: sortByProperty * sorts all models in collection by property * * Parameter: * @param {String }propertyName - name of property to sort on * * @param {boolean} $descending - (optional) sort direction: descending (default) == true */ sortByProperty: function sortByProperty(propertyName, $descending) { if ($descending == null) { $descending = true; } var val; this._models.sort(function (a, b) { if (a === b) { return 0; } if (a == null) { //b is not null return $descending ? 1 : -1; } if (b == null) { //a is not null return $descending ? -1 : 1; } if (a.get(propertyName) === b.get(propertyName)) { return 0; } if (a.get(propertyName) == null) { return $descending ? 1 : -1; } if (b.get(propertyName) == null) { return $descending ? -1 : 1; } if (Coco.Math.isNumber(a.get(propertyName))) { val = parseFloat(a.get(propertyName)) < parseFloat(b.get(propertyName)) ? 1 : parseFloat(a.get(propertyName)) === parseFloat(b.get(propertyName)) ? 0 : -1; return $descending ? val : -1 * val; } //sort alphabetically - UPPER letters are sorted BEFOR lower letters!!! --> convert all letters to lower case if (typeof a.get(propertyName) == "string") { val = a.get(propertyName).toLowerCase() < b.get(propertyName).toLowerCase() ? 1 : a.get(propertyName).toLowerCase() === b.get(propertyName).toLowerCase() ? 0 : -1; return $descending ? val : -1 * val; } //other val = a.get(propertyName) < b.get(propertyName) ? 1 : a.get(propertyName) === b.get(propertyName) ? 0 : -1; return $descending ? val : -1 * val; }); }, /** * Function: size * Returns the size of the collection. * * Return: * @return {integer} - The size of the collection. */ size: function size() { return this._models.length; }, /** * Function: where * * Description: * Looks through each value in the list, filters models by given object properties * * @return {array} - returning an array of all the values that contain all of the key-value pairs listed in properties. */ where: function where(propertyObject) { return _.where(this.getAllAttributes(), propertyObject); }, /** * Function: __onModelDestroy * * EventListener for Coco.Event.DESTROY - Event * * Removes a model from the collection when it's destroyed. * * Parameter: * @param {Coco.ModelEvent} model - model to remove * @private */ __onModelDestroy: function __onModelDestroy(event) { this.remove(event.model, true); }, /** * Function: getId * Returns the internal id. Useful for comparison between different objects. If two object have the same id, * they are identical. * * Return: * @return {string} - The internal collection id. */ getId: function getId() { return this.__id; }, /** * Function: isEqual * Checks if two collections are the same * * Parameter: * @param {Coco.Collection} collection - The <Coco.Collection> instance to compare * * Return: * @return {boolean} - True if both collections are the same instance, otherwise false. */ isEqual: function isEqual(collection) { return this.__id === collection.getId(); }, /** * Function: destroy * Destroy the collection. Destroying the collection will remove and destroy * all attached models. */ destroy: function destroy() { this._dispatchEvent(new Coco.ModelEvent(Coco.Event.DESTROY, this)); this.each(function (model) { // Destroy all models model.destroy(); }); this.removeAllEventListener(); } });