Source: delite/ArrayToStoreAdapter.js

/** @module delite/ArrayToStoreAdapter */
define([
	"dcl/dcl",
	"decor/Evented",
	"decor/ObservableArray",
	"decor/Observable",
	"requirejs-dplugins/Promise!"
], function (dcl, Evented, ObservableArray, Observable, Promise) {

	/**
	 * An adapter to use an array in the source of delite/Store.js.
	 * Created to keep a commun interface with the use of dstore/Store instead of an array.
	 *
	 * The arguments to pass to the constructor are:
	 *
	 * - source: Array - the array represented by the adapter.
	 * - query: the query filter to apply to the source.
	 * - processQueryResult: function to apply to the source
	 *
	 * @class module:delite/ArrayToStoreAdapter
	 */
	return dcl(Evented, /** @lends module:delite/ArrayToStoreAdapter# */ {
		constructor: function (args) {
			this.source = args.source;
			// affect the callbacks of the observe functions
			this._itemHandles = [];
			this._observeCallbackArray = this.__observeCallbackArray.bind(this);
			this._observeCallbackItems = this.__observeCallbackItems.bind(this);
			for (var i = 0; i < this.source.length; i++) {
				// affect the callback to the observe function if the item is observable
				this._itemHandles[i] = Observable.observe(this.source[i], this._observeCallbackItems);
			}
			// affect the callback to the observe function if the array is observable
			this._arrayHandle = ObservableArray.observe(this.source, this._observeCallbackArray);
			this._isQueried = this._compileQuery(args.query);
			this.data = args.processQueryResult(this.source.filter(this._isQueried));
			if (this._arrayHandle || this._itemHandles) {
				this.track = true;
			}
		},

		/**
		 * Indicates if the source is observable.
		 * @member {boolean}
		 * @default false
		 * @readonly
		 */
		track: false,

		/////////////////////////////////////////////////////////////////
		// Functions dedicated to the Observability of the source
		/////////////////////////////////////////////////////////////////

		/**
		 * Function to add an item in the data and pass the good event to the function `itemAdded()` of `delite/Store`.
		 * @param evt
		 * @returns {{index: *, target: (*|evtUpdated.obj|evtRemoved.obj|evtAdded.obj|host.obj|obj)}}
		 * @private
		 */
		_addItemToCollection: function (evt) {
			var i = evt.index - 1;
			while (!this._isQueried(this.source[i])) {
				i--;
			}
			var idx = this.data.indexOf(this.source[i]);
			this.data.splice(idx + 1, 0, evt.obj);
			return {index: idx + 1, target: evt.obj};
		},

		/**
		 * Function to remove an item from the data and pass the good event to the function `itemRemoved()`
		 * of `delite/Store`.
		 * @param evt
		 * @returns {{previousIndex: (*|number|Number)}}
		 * @private
		 */
		_removeItemFromCollection: function (evt) {
			var idx = this.data.indexOf(evt.obj);
			this.data.splice(idx, 1);
			return {previousIndex: idx};
		},

		/**
		 * Function to test if the update was finally a remove or an add to the data.
		 * @param evt
		 * @param idx
		 * @returns {boolean}
		 * @private
		 */
		_isAnUpdate: function (evt, idx) {
			return this._isQueried(evt.obj) && idx >= 0;
		},

		/**
		 * Function that emit an event "add" if the update was finally an "add" and an event "remove" if it was a
		 * remove.
		 * @param evt
		 * @param idx
		 * @private
		 */
		_redirectEvt: function (evt, idx) {
			if (this._isQueried(evt.obj) && idx < 0) {
				var evtAdded = this._addItemToCollection(evt);
				this.emit("add", evtAdded);
			} else if (!this._isQueried(evt.obj) && idx >= 0) {
				var evtRemoved = this._removeItemFromCollection(evt);
				this.emit("delete", evtRemoved);
			}
		},

		/**
		 * Function to pass the good event to the function `itemUpdated()` of `delite/Store`.
		 * @param evt
		 * @param idx
		 * @returns {{index: *, previousIndex: *, target: (*|evtUpdated.obj|evtRemoved.obj|evtAdded.obj|host.obj|obj)}}
		 * @private
		 */
		_updateItemInCollection: function (evt, idx) {
			return {index: idx, previousIndex: idx, target: evt.obj};
		},

		/**
		 * Called when a modification is done on the array.
		 * @param {Array} changeRecords - sent by the Observe function.
		 */
		__observeCallbackArray: function (changeRecords) {
			if (!this._beingDiscarded) {
				for (var i = 0; i < changeRecords.length; i++) {
					if (changeRecords[i].type === "splice") {
						var j, evt;
						for (j = 0; j < changeRecords[i].removed.length; j++) {
							this._itemHandles[changeRecords[i].index].remove();
							this._itemHandles.splice(changeRecords[i].index, 1);
							var evtRemoved = {previousIndex: changeRecords[i].index,
								obj: changeRecords[i].removed[j]};
							if (this._isQueried(evtRemoved.obj)) {
								evt = this._removeItemFromCollection(evtRemoved);
								this.emit("delete", evt);
							}
						}
						for (j = 0; j < changeRecords[i].addedCount; j++) {
							var evtAdded = {
								index: changeRecords[i].index + j,
								obj: this.source[changeRecords[i].index + j]
							};
							if (this.renderItems !== null && this.renderItems !== undefined) {
								evtAdded.index = evtAdded.index <= this.renderItems.length ?
									evtAdded.index : this.renderItems.length;
							}
							// affect the callback to the observe function if the item is observable
							this._itemHandles.splice(changeRecords[i].index + j, 0,
								Observable.observe(
									this.source[changeRecords[i].index + j], this._observeCallbackItems)
							);
							if (this._isQueried(evtAdded.obj)) {
								evt = this._addItemToCollection(evtAdded);
								this.emit("add", evt);
							}
						}
					}
				}
			}
		},

		/**
		 * Called when a modification is done on the items.
		 * @param {Array} changeRecords - sent by the Observe function.
		 */
		__observeCallbackItems: function (changeRecords) {
			if (!this._beingDiscarded) {
				var objects = [];
				for (var i = 0; i < changeRecords.length; i++) {
					var object = changeRecords[i].object;
					if (objects.indexOf(object) < 0) {
						objects.push(object);
						if (changeRecords[i].type === "add" || changeRecords[i].type === "update" ||
							changeRecords[i].type === "delete" || changeRecords[i].type === "splice") {
							var evtUpdated = {
								index: this.source.indexOf(object),
								previousIndex: this.source.indexOf(object),
								obj: object,
								oldValue: changeRecords[i].oldValue,
								name: changeRecords[i].name
							};
							var idx = this.data.indexOf(evtUpdated.obj);
							if (!this._isAnUpdate(evtUpdated, idx)) {
								this._redirectEvt(evtUpdated, idx);
							} else {
								var evt = this._updateItemInCollection(evtUpdated, idx);
								this.emit("update", evt);
							}
						}
					}
				}
			}
		},

		/**
		 * Generate a function that will test if an item respects the query conditions.
		 * @param query
		 * @returns {Function}
		 * @private
		 */
		_compileQuery: function (query) {
			if (Object.getOwnPropertyNames(query).length !== 0) {
				if (typeof query === "function") {
					return query;
				}
				if (!query.type) {
					return function (item) {
						for (var property in query) {
							if (item[property] !== query[property]) { return false; }
						}
						return true;
					};
				}
				return this._compileFilterQuery(query);
			} else {
				return function () { return true; };
			}
		},

		/**
		 * Generate a function that will test if an item respects the query conditions, based on a query
		 * object generated from `dstore/Filter`.
		 * @param query
		 * @returns {Function}
		 * @private
		 */
		/* jshint maxcomplexity: 12 */
		_compileFilterQuery: function (query) {
			var prop = query.args[0];
			var value = query.args[1];
			switch (query.type) {
			case "eq":
				return function (item) {return item[prop] === value; };
			case "ne":
				return function (item) {return item[prop] !== value; };
			case "lt":
				return function (item) {return item[prop] < value; };
			case "lte":
				return function (item) {return item[prop] <= value; };
			case "gt":
				return function (item) {return item[prop] > value; };
			case "gte":
				return function (item) {return item[prop] >= value; };
			case "in":
				return function (item) {return value.indexOf(item[prop]) !== -1; };
			case "match":
				return function (item) {return value.test(item[prop]); };
			case "contains":
				return function (item) {return this._arrayContains(item[prop], value); }.bind(this);
			/**
			 * In the case of "and" and "or" the prop in in fact the first member of the two queries and value the
			 * second member.
			 */
			case "and":
				var f1 = this._compileFilterQuery(prop);
				var f2 = this._compileFilterQuery(value);
				return function (item) {return f1(item) && f2(item); };
			case "or":
				f1 = this._compileFilterQuery(prop);
				f2 = this._compileFilterQuery(value);
				return function (item) {return f1(item) || f2(item); };
			default:
				throw new Error("Unknown filter operation '" + query.type + "'");
			}
		},
		/* jshint maxcomplexity: 10*/

		/**
		 * Test if an array contains all the values in parameter values.
		 * @param array
		 * @param values
		 * @returns {boolean}
		 * @private
		 */
		_arrayContains: function (array, values) {
			for (var j = 0; j < values.length; j++) {
				if (array.indexOf(values[j]) === -1) {
					return false;
				}
			}
			return true;
		},

		/**
		 * Synchronously emit add/update/delete events for all recent changes.
		 */
		deliver: function () {
			if (this._arrayHandle) {
				this._arrayHandle.deliver();
			}
			if (this._itemHandles.length !== 0) {
				this._observeCallbackItems && Observable.deliverChangeRecords(this._observeCallbackItems);
			}
		},

		/**
		 * Discard recent change records.
		 */
		discardChanges: function () {
			if (this._arrayHandle && this._itemHandles) {
				this._beingDiscarded = true;
				this._arrayHandle.deliver();
				this._observeCallbackItems && Observable.deliverChangeRecords(this._observeCallbackItems);
				this._beingDiscarded = false;
				return this;
			}
		},

		/**
		 * Stop observing the array and its items.
		 */
		untrack: function () {
			if (this._arrayHandle) {
				this._arrayHandle.remove();
			}
			if (this._itemHandles) {
				for (var i = 0; i < this._itemHandles.length; i++) {
					this._itemHandles[i].remove();
				}
			}
		},


		/////////////////////////////////////////////////////////////////////////
		// Functions dedicated to reproduce the behaviour of dstore functions
		/////////////////////////////////////////////////////////////////////////

		/**
		 * Perform the fetch operation on the collection.
		 */
		fetch: function () {
			return Promise.resolve(this.data);
		},

		/**
		 * Perform the fetchRange operation on the collection.
		 * @param {Object} args - contains the start index and the end index of the fetch.
		 */
		fetchRange: function (args) {
			var res = this.data.slice(args.start, args.end);
			if (res.length < (args.end - args.start)) {
				var promise;
				var evt = {start: args.start, end: args.end, resLength: res.length, setPromise: function (pro) {
					promise = pro;
				}};
				this.emit("_new-query-asked", evt);
				return promise;
			} else {
				return Promise.resolve(res);
			}
		},

		/**
		 * Set the identity of an object.
		 */
		setIdentity: function (item, id) {
			item.id = id;
		},

		/**
		 * Retrieve an object in the data by its identity.
		 */
		get: function (id) {
			for (var i = 0; i < this.data.length; i++) {
				var res = this.data[i];
				if (res.id === id) {
					return Promise.resolve(res);
				}
			}
		},

		/**
		 * Returns the identity of an item.
		 * @param {Object} item - The item.
		 * @returns {*}
		 */
		getIdentity: function (item) {
			return item.id !== undefined ? item.id : this.data.indexOf(item);
		}
	});
});