Source: manager.js

/*!
 * manager
 * https://github.com/Voliware/Util
 * Licensed under the MIT license.
 */

if(!isDefined(EventSystem))
	throw new ReferenceError("Manager requires EventSystem");

/**
 * Generic object manager.
 * Adds/updates/deletes objects in a collection.
 * Can accept an array or object of
 * data and determine which objects
 * are new, old, or no longer exist
 * @extends EventSystem
 */
class Manager extends EventSystem {

	/**
	 * Constructor
	 * @param {object} [options]
	 * @param {string} [options.identifier='id'] - the property name that identifies
	 * each object, which is a property of each object, managed by this manager
	 * @param {boolean} [options.useObjectNames=false] - whether to use the names
	 * of objects passed to manage() as their object identifiers
	 * @returns {Manager}
	 */
	constructor(options) {
		super();
		var defaults = {
			// to manage objects, they must have
			// a unique id before they get to Manager.
			// this is their id property name
			identifier: 'id',
			// OR instead of using an identifier,
			// use the name of the object.
			// this only works when passing
			// objects of objects to manage()
			useObjectNames : false,
			// max number of objs to manage
			max : 0
		};
		this.settings = Object.assign(defaults, options);

		// all child classes will use
		// a more friendly name for objects
		// so any mutations must be done
		// to the object itself, not this ref
		this.objects = {};
		this.count = 0;

		// cached and processed data passed to manage()
		this._cachedData = {};
		this._processedData = {};
		// last serialized collection
		this.serializedObjects = [];
		// a flag that is set to true when the add/edit/delete
		// functions are called, to indicate that the
		// previously serialized data is now old
		this.requiresNewSerialize = false;

		return this;
	}

	/**
	 * Check if an object exists in the collection
	 * @param {...(number|object|string)} arguments - the object or the id of the object
	 * @returns {boolean}
	 * @private
	 */
	_exists(){
		var arg = arguments[0];
		if(isString(arg) || isNumber(arg))
			return isDefined(this.objects[arg]);
		else
			return isDefined(this.objects[arg[this.settings.identifier]]);
	}

	/**
	 * Get an object in the managed collection
	 * by id (string/number) or by an object's
	 * property as set in this.settings.identifier
	 * @param {...(number|object|string)} arguments - the object or the id of the object
	 * @returns {*|null}
	 * @private
	 */
	_get() {
		var arg = arguments[0];
		var obj = null;
		var identifier = this.settings.identifier;

		// an object id was passed
		if(isString(arg) || isNumber(arg)){
			String(arg);
			if(this.objects[arg])
				obj = this.objects[arg];
		}
		// an object was passed
		else if (this.objects[arg[identifier]]) {
			obj = this.objects[arg[identifier]];
		}

		return obj;
	}

	/**
	 * Adds an object to the collection.
	 * Replaces any existing object with the same identifier.
	 * @param {object} obj - the object to add
	 * @param {string} [id] - the id of the object
	 * @returns {*}
	 * @private
	 */
	_add(obj, id) {
		var self = this;
		var identifier = this.settings.identifier;

		// if an id is passed, add it to
		// the object as the identifier property
		if(isDefined(id)) {
			String(id);
			obj[identifier] = id;
			this.objects[id] = obj;
			postAdd();
		}
		// if no id is passed, check that it has
		// an identifier property already
		else if(!isNullOrUndefined(obj[identifier])) {
			this.objects[obj[identifier]] = obj;
			postAdd();
		}
		// otherwise, it cannot be managed
		else {
			console.warn('Manager._add: cannot add an object with no identifier');
		}
		
		return obj;

		/**
		 * After a successful add, trigger
		 * the event and increase the counter
		 */
		function postAdd(){
			self.requiresNewSerialize = true;
			self.trigger('add', obj);
			self.count++;
			obj._count = self.count;
		}
	}	

	/**
	 * Replaces or adds an object
	 * @param {object} obj - the object to update
	 * @param {string} [id] - the id of the object
	 * @returns {*}
	 * @private
	 */
	_update(obj, id) {
		var self = this;
		var identifier = this.settings.identifier;

		if(isDefined(id)) {
			String(id);
			this.objects[id] = obj;
			postUpdate();
		}
		else if(!isNullOrUndefined(obj[identifier])){
			this.objects[obj[identifier]] = obj;
			postUpdate();
		}
		else {
			console.warn('Manager._update: cannot update an object with no identifier');
		}
				
		return obj;

		/**
		 * After a successful update, trigger
		 * the event and reset the serialize flag
		 */
		function postUpdate(){
			self.requiresNewSerialize = true;
			self.trigger('update', obj);
			self.count++;
			obj._count = self.count;
		}
	}

	/**
	 * Deletes an object from the collection
	 * @param {...(number|object|string)} arguments - the object or the id of the object
	 * @returns {Manager}
	 * @private
	 */
	_delete() {
		var arg = arguments[0];
		var obj = null;
		var identifier = this.settings.identifier;

		obj = this._get(...arguments);

		if(obj) {
			var id = obj[identifier];
			this.trigger('delete', id);
			delete this.objects[id];
			this.requiresNewSerialize = true;
			if (this.count > 0)
				this.count--;
		}
		else{
			console.error('Manager._delete: cannot delete an object with no identifier');
		}

		return this;
	}

	/**
	 * Deletes all objects from the collection
	 * @returns {Manager}
	 * @private
	 */
	_empty(){
		// ..in likely case there are references
		for(var i in this.objects){
			delete this.objects[i];
		}
		return this;
	}

	/**
	 * Cache data
	 * @param {*} data
	 * @returns {Manager}
	 * @private
	 */
	_cacheData(data){
		this._cachedData = deepCopy(data);
		return this;
	}

	/**
	 * Process all incoming data to manage
	 * @param {object} data
	 * @returns {Manager}
	 * @private
	 */
	_processData(data){
		this._processedData = deepCopy(data);
		return this;
	}

	/**
	 * Get the ids of all objects
	 * @returns {string[]}
	 */
	getIds() {
		var ids = [];
		for(var i in this.objects){
			var id = this.getId(this.objects[i]);
			ids.push(id);
		}
		return ids;
	}

	/**
	 * Get the id of an object
	 * @param {object} obj
	 * @returns {string|undefined}
	 */
	getId(obj){
		return obj[this.settings.identifier].toString();
	}

	/**
	 * Given a collection of objects in an array, 
	 * or in an object, add and update them 
	 * in the manager's own collection.
	 * Then delete any objects still in the manager's
	 * collection that are not in the data
	 * @param {object|object[]} data
	 * @returns {Manager}
	 */
	manage(data) {
		this._cacheData(data);
		this._processData(data);

		if(!isObject(this._processedData) && this.settings.useObjectNames)
			throw new Error("Manager.manage: to use option useObjectNames, object passed to manage() must be an object.");

		var self = this;
		var id = this.settings.identifier;
		// maintain an array of ids found in data
		// then xreference this to see which objects
		// no longer exist (data is the master here)
		var dataIds = [];

		// add or update objects
		for(var i in this._processedData){
			var e = this._processedData[i];

			if(this.settings.useObjectNames)
				e[id] = i;

			// ids must be defined within objects
			// there is no other way to know if an
			// object is new or old
			if(!isDefined(e[id]))
				return console.error("Manager.manage: cannot manage objects with no ids");

			// objectIds will always be strings
			var objId = e[id].toString();
			var obj = self.objects[objId];
			if (isDefined(obj))
				self._update(e);
			else
				self._add(e);
			dataIds.push(objId);
		}

		// diff the array of object ids
		// with the array of data ids
		var objectIds = this.getIds();
		var diff = Array.diff(objectIds, dataIds);

		// delete any objects that are
		// no longer found in data
		for (i = 0; i < diff.length; i++) {
			this._delete(diff[i]);
		}

		return this;
	}

	/**
	 * Public method to see if an object exists
	 * @returns {boolean}
	 */
	exists() {
		return this._exists(...arguments);
	}

	/**
	 * Add many objects to the collection
	 * @param {...object} arguments - one or more objects
	 * @returns {Manager}
	 */
	addObjects(){
		var data = arguments.length > 1 ? [].slice.call(arguments).sort() : arguments[0];
		for(var i in data){
			var e = data[i];
			this.addObject(e);
		}
		return this;
	}

	/**
	 * Public method to add an object
	 * @returns {*}
	 */
	addObject() {
		return this._add(...arguments);
	}

	/**
	 * Public method to get an object
	 * @returns {*}
	 */
	getObject(){
		return this._get(...arguments);
	}

	/**
	 * Public method to update an object
	 * @returns {*}
	 */
	updateObject() {
		return this._update(...arguments);
	}

	/**
	 * Public method to delete an object
	 * @returns {*}
	 */
	deleteObject() {
		return this._delete(...arguments);
	}

	/**
	 * Public method to delete all objects
	 * @returns {*}
	 */
	deleteObjects() {
		return this._empty();
	}

	/**
	 * Serialize all objects that have a serializer method
	 * @param {number} [index=0] - index to start at
	 * @param {number} [max=0] - max amount to return
	 * @returns {object[]}
	 */
	serializer(index = 0, max = 0){
		if(this.requiresNewSerialize){
			var self = this;
			this.serializedObjects = [];
			$.each(this.objects, function(i, e){
				if(e.serializer)
					self.serializedObjects.push(e.serializer());
			});
			this.requiresNewSerialize = false;
		}

		max = max > 0 ? max : this.serializedObjects.length;
		return this.serializedObjects.slice(index, max);
	}
}