1 /**
  2  * Emily.js - http://flams.github.com/emily/
  3  * Copyright(c) 2012-2014 Olivier Scherrer <pode.fr@gmail.com>
  4  * MIT Licensed
  5  */
  6 "use strict";
  7 
  8 var Observable = require("./Observable"),
  9     Tools = require("./Tools");
 10 
 11 /**
 12  * @class
 13  * Store creates an observable structure based on a key/values object
 14  * or on an array
 15  * @param {Array/Object} the data to initialize the store with
 16  * @returns
 17  */
 18 module.exports = function StoreConstructor($data) {
 19 
 20     /**
 21      * Where the data is stored
 22      * @private
 23      */
 24     var _data = Tools.clone($data) || {},
 25 
 26     /**
 27      * The observable for publishing changes on the store iself
 28      * @private
 29      */
 30     _storeObservable = new Observable(),
 31 
 32     /**
 33      * The observable for publishing changes on a value
 34      * @private
 35      */
 36     _valueObservable = new Observable(),
 37 
 38     /**
 39      * Saves the handles for the subscriptions of the computed properties
 40      * @private
 41      */
 42     _computed = [],
 43 
 44     /**
 45      * Gets the difference between two objects and notifies them
 46      * @private
 47      * @param {Object} previousData
 48      */
 49     _notifyDiffs = function _notifyDiffs(previousData) {
 50         var diffs = Tools.objectsDiffs(previousData, _data);
 51         ["updated",
 52          "deleted",
 53          "added"].forEach(function (value) {
 54              diffs[value].forEach(function (dataIndex) {
 55                     _storeObservable.notify(value, dataIndex, _data[dataIndex]);
 56                     _valueObservable.notify(dataIndex, _data[dataIndex], value);
 57              });
 58         });
 59     };
 60 
 61     /**
 62      * Get the number of items in the store
 63      * @returns {Number} the number of items in the store
 64      */
 65     this.getNbItems = function() {
 66         return _data instanceof Array ? _data.length : Tools.count(_data);
 67     };
 68 
 69     /**
 70      * Count is an alias for getNbItems
 71      * @returns {Number} the number of items in the store
 72      */
 73     this.count = this.getNbItems;
 74 
 75     /**
 76      * Get a value from its index
 77      * @param {String} name the name of the index
 78      * @returns the value
 79      */
 80     this.get = function get(name) {
 81         return _data[name];
 82     };
 83 
 84     /**
 85      * Checks if the store has a given value
 86      * @param {String} name the name of the index
 87      * @returns {Boolean} true if the value exists
 88      */
 89     this.has = function has(name) {
 90         return _data.hasOwnProperty(name);
 91     };
 92 
 93     /**
 94      * Set a new value and overrides an existing one
 95      * @param {String} name the name of the index
 96      * @param value the value to assign
 97      * @returns true if value is set
 98      */
 99     this.set = function set(name, value) {
100         var hasPrevious,
101             previousValue,
102             action;
103 
104         if (typeof name != "undefined") {
105             hasPrevious = this.has(name);
106             previousValue = this.get(name);
107             _data[name] = value;
108             action = hasPrevious ? "updated" : "added";
109             _storeObservable.notify(action, name, _data[name], previousValue);
110             _valueObservable.notify(name, _data[name], action, previousValue);
111             return true;
112         } else {
113             return false;
114         }
115     };
116 
117     /**
118      * Update the property of an item.
119      * @param {String} name the name of the index
120      * @param {String} property the property to modify.
121      * @param value the value to assign
122      * @returns false if the Store has no name index
123      */
124     this.update = function update(name, property, value) {
125         var item;
126         if (this.has(name)) {
127             item = this.get(name);
128             Tools.setNestedProperty(item, property, value);
129             _storeObservable.notify("updated", property, value);
130             _valueObservable.notify(name, item, "updated");
131             return true;
132         } else {
133             return false;
134         }
135     };
136 
137     /**
138      * Delete value from its index
139      * @param {String} name the name of the index from which to delete the value
140      * @returns true if successfully deleted.
141      */
142     this.del = function del(name) {
143         if (this.has(name)) {
144             if (!this.alter("splice", name, 1)) {
145                 delete _data[name];
146                 _storeObservable.notify("deleted", name);
147                 _valueObservable.notify(name, _data[name], "deleted");
148             }
149             return true;
150         } else {
151             return false;
152         }
153     };
154 
155     /**
156      * Delete multiple indexes. Prefer this one over multiple del calls.
157      * @param {Array}
158      * @returns false if param is not an array.
159      */
160     this.delAll = function delAll(indexes) {
161         if (indexes instanceof Array) {
162             // Indexes must be removed from the greatest to the lowest
163             // To avoid trying to remove indexes that don't exist.
164             // i.e: given [0, 1, 2], remove 1, then 2, 2 doesn't exist anymore
165             indexes.sort(Tools.compareNumbers)
166                 .reverse()
167                 .forEach(this.del, this);
168             return true;
169         } else {
170             return false;
171         }
172     };
173 
174     /**
175      * Alter the data by calling one of it's method
176      * When the modifications are done, it notifies on changes.
177      * If the function called doesn't alter the data, consider using proxy instead
178      * which is much, much faster.
179      * @param {String} func the name of the method
180      * @params {*} any number of params to be given to the func
181      * @returns the result of the method call
182      */
183     this.alter = function alter(func) {
184         var apply,
185             previousData;
186 
187         if (_data[func]) {
188             previousData = Tools.clone(_data);
189             apply = this.proxy.apply(this, arguments);
190             _notifyDiffs(previousData);
191             _storeObservable.notify("altered", _data, previousData);
192             return apply;
193         } else {
194             return false;
195         }
196     };
197 
198     /**
199      * Proxy is similar to alter but doesn't trigger events.
200      * It's preferable to call proxy for functions that don't
201      * update the interal data source, like slice or filter.
202      * @param {String} func the name of the method
203      * @params {*} any number of params to be given to the func
204      * @returns the result of the method call
205      */
206     this.proxy = function proxy(func) {
207         if (_data[func]) {
208             return _data[func].apply(_data, Array.prototype.slice.call(arguments, 1));
209         } else {
210             return false;
211         }
212     };
213 
214     /**
215      * Watch the store's modifications
216      * @param {String} added/updated/deleted
217      * @param {Function} func the function to execute
218      * @param {Object} scope the scope in which to execute the function
219      * @returns {Handle} the subscribe's handler to use to stop watching
220      */
221     this.watch = function watch(name, func, scope) {
222         return _storeObservable.watch(name, func, scope);
223     };
224 
225     /**
226      * Unwatch the store modifications
227      * @param {Handle} handle the handler returned by the watch function
228      * @returns
229      */
230     this.unwatch = function unwatch(handle) {
231         return _storeObservable.unwatch(handle);
232     };
233 
234     /**
235      * Get the observable used for watching store's modifications
236      * Should be used only for debugging
237      * @returns {Observable} the Observable
238      */
239     this.getStoreObservable = function getStoreObservable() {
240         return _storeObservable;
241     };
242 
243     /**
244      * Watch a value's modifications
245      * @param {String} name the name of the value to watch for
246      * @param {Function} func the function to execute
247      * @param {Object} scope the scope in which to execute the function
248      * @returns handler to pass to unwatchValue
249      */
250     this.watchValue = function watchValue(name, func, scope) {
251         return _valueObservable.watch(name, func, scope);
252     };
253 
254     /**
255      * Unwatch the value's modifications
256      * @param {Handler} handler the handler returned by the watchValue function
257      * @private
258      * @returns true if unwatched
259      */
260     this.unwatchValue = function unwatchValue(handler) {
261         return _valueObservable.unwatch(handler);
262     };
263 
264     /**
265      * Get the observable used for watching value's modifications
266      * Should be used only for debugging
267      * @private
268      * @returns {Observable} the Observable
269      */
270     this.getValueObservable = function getValueObservable() {
271         return _valueObservable;
272     };
273 
274     /**
275      * Loop through the data
276      * @param {Function} func the function to execute on each data
277      * @param {Object} scope the scope in wich to run the callback
278      */
279     this.loop = function loop(func, scope) {
280         Tools.loop(_data, func, scope);
281     };
282 
283     /**
284      * Reset all data and get notifications on changes
285      * @param {Arra/Object} data the new data
286      * @returns {Boolean}
287      */
288     this.reset = function reset(data) {
289         if (data instanceof Object) {
290             var previousData = Tools.clone(_data);
291             _data = Tools.clone(data) || {};
292             _notifyDiffs(previousData);
293             _storeObservable.notify("resetted", _data, previousData);
294             return true;
295         } else {
296             return false;
297         }
298 
299     };
300 
301     /**
302      * Compute a new property from other properties.
303      * The computed property will look exactly similar to any none
304      * computed property, it can be watched upon.
305      * @param {String} name the name of the computed property
306      * @param {Array} computeFrom a list of properties to compute from
307      * @param {Function} callback the callback to compute the property
308      * @param {Object} scope the scope in which to execute the callback
309      * @returns {Boolean} false if wrong params given to the function
310      */
311     this.compute = function compute(name, computeFrom, callback, scope) {
312         var args = [];
313 
314         if (typeof name == "string" &&
315             typeof computeFrom == "object" &&
316             typeof callback == "function" &&
317             !this.isCompute(name)) {
318 
319             _computed[name] = [];
320 
321             Tools.loop(computeFrom, function (property) {
322                 _computed[name].push(this.watchValue(property, function () {
323                     this.set(name, callback.call(scope));
324                 }, this));
325             }, this);
326 
327             this.set(name, callback.call(scope));
328             return true;
329         } else {
330             return false;
331         }
332     };
333 
334     /**
335      * Remove a computed property
336      * @param {String} name the name of the computed to remove
337      * @returns {Boolean} true if the property is removed
338      */
339     this.removeCompute = function removeCompute(name) {
340         if (this.isCompute(name)) {
341             Tools.loop(_computed[name], function (handle) {
342                 this.unwatchValue(handle);
343             }, this);
344             this.del(name);
345             return true;
346         } else {
347             return false;
348         }
349     };
350 
351     /**
352      * Tells if a property is a computed property
353      * @param {String} name the name of the property to test
354      * @returns {Boolean} true if it's a computed property
355      */
356     this.isCompute = function isCompute(name) {
357         return !!_computed[name];
358     };
359 
360     /**
361      * Returns a JSON version of the data
362      * Use dump if you want all the data as a plain js object
363      * @returns {String} the JSON
364      */
365     this.toJSON = function toJSON() {
366         return JSON.stringify(_data);
367     };
368 
369     /**
370      * Returns the store's data
371      * @returns {Object} the data
372      */
373     this.dump = function dump() {
374         return _data;
375     };
376 };