/*! Storm.JS - v0.0.8 - 2014-04-21
* https://github.com/JosephClay/StormJS
* Copyright (c) 2012-2014 Joe Clay; Licensed */
(function(root, _, Signal, undefined) {
// "root" is a safe reference to the environment.
// setup so that this can be used in a node environment
//----
// Hold on to previous Storm reference (can release with noConflict)
var previousStorm = root.Storm,
// Define Storm
/** @namespace Storm */
Storm = root.Storm = {
name: 'StormJS',
ajax: root.$ || { ajax: function() { console.error(_errorMessage('Storm.ajax', 'NYI')); } },
$: root.$ || function() { console.error(_errorMessage('Storm.$', 'NYI')); }
};
//----
// Helpers ##########################################################################
/**
* Noop
* @return {undefined}
* @private
*/
var _noop = function() {};
// Small polyfill for console
var console = root.console || {};
console.log = console.log || _noop;
console.error = console.error || console.log;
/**
* Easy slice function for array duplication
* @param {Array} arr
* @return {Array} duplicate
* @private
*/
var _slice = (function(ARRAY) {
return function(arr) {
return ARRAY.slice.call(arr);
};
}([]));
/**
* Existence check
* @param {*} value
* @return {Boolean}
* @private
*/
var _exists = function(value) {
return (value !== null && value !== undefined);
};
/**
* Format a string using an object. Keys in the object
* will match {keys} in the string
* @return {String} e.g. _stringFormat('hello {where}', { where: 'world' }) === 'hello world'
* @private
*/
var _stringFormat = (function() {
var _REGEX = new RegExp('{(.+?)}', 'g');
return function(str, fill) {
return str.replace(_REGEX, function(capture, value) {
return _exists(fill[value]) ? fill[value] + '' : '';
});
};
}());
/**
* Normalize how Storm returns strings of its
* objects for debugging
* @param {String} name
* @param {Object} [props]
* @return {String}
* @private
*/
var _toString = function(name, props) {
var hasProps = _exists(props);
if (hasProps) {
var key, arr = [];
for (key in props) {
arr.push(key + ': '+ props[key]);
}
props = arr.join(', ');
}
return _stringFormat('[{storm}: {name}{spacer}{props}]', {
storm: Storm.name,
name: name,
spacer: hasProps ? ' - ' : '',
props: hasProps ? props : ''
});
};
/**
* Normalize how Storm logs an error message for debugging
* @param {String} name
* @param {String} message
* @return {String}
* @private
*/
var _errorMessage = function(name, message) {
return _stringFormat('{storm}: {name}, {message}', {
storm: Storm.name,
name: name,
message: message
});
};
//----
// Mixin ############################################################################
/**
* Mix a key-value into Storm, protecting Storm from
* having a pre-existing key overwritten. Of course,
* items can be directly assigned to Storm via Storm.foo = foo;
* but in this framework, I'm considering it a bad practice
* @param {String} name
* @param {*} prop
* @private
*/
var _mixin = function(name, prop) {
if (Storm[name] !== undefined) { return console.error(_errorMessage('mixin', 'Cannot mixin. "'+ name +'" already exists: '), name, Storm[name], prop); }
Storm[name] = prop;
};
/**
* Allows you to extend Storm with your own methods, classes and modules.
* Pass a hash of `{name: function}` definitions to have your functions added.
* @namespace Storm.mixin
* @param {String|Object} name Name of the object
* @param {Object} prop The object to mixin
*/
Storm.mixin = function(name, prop) {
// Mix single
if (_.isString(name)) {
return _mixin(name, prop);
}
// Mix multiple
var key;
for (key in name) {
_mixin(key, name[key]);
}
};
//----
var memo = Storm.memo = function(getter) {
var secret;
return function() {
return secret || (secret = getter.call());
};
};
//----
// Unique Id ########################################################################
/**
* @typedef {Number} Id
*/
/**
* Generates an ID number that is unique within the context of the current {@link Storm} instance.
* The internal counter does not persist between page loads.
* @function Storm.uniqId
* @param {String} [prefix] Defines an optional scope (i.e., namespace) for the identifiers.
* @return {Id} Unique ID number.
*/
var _uniqId = Storm.uniqId = (function() {
var _scopedIdentifiers = {};
return function(scope) {
scope = scope || '';
var inc = (_scopedIdentifiers[scope] || 0) + 1;
return (_scopedIdentifiers[scope] = inc);
};
}()),
/**
* Generates an ID number prefixed with the given string that is unique within the context of the current {@link Storm} instance.
* The internal counter does not persist between page loads.
* @function Storm.uniqIdStr
* @param {String} prefix String to prepend the generated ID number with. Also used to scope (namespace) the unique ID number.
* @return {String} Unique ID number prefixed with the given string.
* @private
*/
_uniqIdStr = function(prefix) {
return (prefix || 'id') + '' + _uniqId(prefix);
};
//----
// Extend ###########################################################################
/**
* Prototypical class extension
* @see {@link https://github.com/JosephClay/Extend.git Extend}
* @param {Function} [constructor]
* @param {Object} [extension]
* @function Storm.Extend
*/
var Extend = Storm.Extend = function(constructor, extension) {
var hasConstructor = (typeof constructor === 'function');
if (!hasConstructor) { extension = constructor; }
var self = this,
fn = function() {
var ret = self.apply(this, arguments);
if (hasConstructor) {
ret = constructor.apply(this, arguments);
}
return ret;
};
// Add properties to the object
_.extend(fn, this);
// Duplicate the prototype
var NoOp = function() {};
NoOp.prototype = this.prototype;
fn.prototype = new NoOp();
// Merge the prototypes
_.extend(fn.prototype, this.prototype, extension);
fn.prototype.constructor = constructor || fn;
return fn;
};
//----
// Events ###########################################################################
/**
* Proxy to Signal.
* @class Storm.Events
* @see {@link https://github.com/JosephClay/Signal Signal}
*/
var Events = Storm.Events = Signal.core;
/**
* Instantiate and merge a new Event system
* into Storm so that Storm can be used as
* a pub/sub
*/
_.extend(Storm, Events.construct());
//----
// Promise ##########################################################################
/**
* The name of the class
* @const
* @type {String}
* @private
*/
var _PROMISE = 'Promise',
/**
* Status values, determines
* what the promise's status is
* @readonly
* @enum {Number}
* @alias Storm.Promise.STATUS
*/
_PROMISE_STATUS = {
idle: 0,
progressed: 1,
failed: 2,
done: 3
},
/**
* Call values, used to determine
* what kind of functions to call
* @readonly
* @enum {Number}
* @alias Storm.Promise.CALL
*/
_PROMISE_CALL = {
done: 0,
fail: 1,
always: 2,
progress: 3,
pipe: 4
},
_PROMISE_CALL_NAME = _.invert(_PROMISE_CALL);
/**
* A lightweight implementation of promises.
* API based on {@link https://api.jquery.com/promise/ jQuery.promises}
* @class Storm.Promise
*/
var Promise = Storm.Promise = function() {
/**
* @type {Id}
* @private
*/
this._id = _uniqId(_PROMISE);
/**
* Registered functions organized by _PROMISE_CALL
* @type {Object}
* @private
*/
this._calls = {};
/**
* Current status
* @type {Number}
* @private
*/
this._status = _PROMISE_STATUS.idle;
};
Promise.STATUS = _PROMISE_STATUS;
Promise.CALL = _PROMISE_CALL;
Promise.prototype = /** @lends Storm.Promise# */ {
constructor: Promise,
/**
* Register a done call that is fired after a Promise is resolved
* @param {Function} func
* @return {Storm.Promise}
*/
done: function(func) { return this._pushCall.call(this, _PROMISE_CALL.done, func); },
/**
* Register a fail call that is fired after a Promise is rejected
* @param {Function} func
* @return {Storm.Promise}
*/
fail: function(func) { return this._pushCall.call(this, _PROMISE_CALL.fail, func); },
/**
* Register a call that fires after done or fail
* @param {Function} func
* @return {Storm.Promise}
*/
always: function(func) { return this._pushCall.call(this, _PROMISE_CALL.always, func); },
/**
* Register a progress call that is fired after a Promise is notified
* @param {Function} func
* @return {Storm.Promise}
*/
progress: function(func) { return this._pushCall.call(this, _PROMISE_CALL.progress, func); },
/**
* Register a pipe call that is fired before done or fail and whose return value
* is passed to the next pipe/done/fail call
* @param {Function} func
* @return {Storm.Promise}
*/
pipe: function(func) { return this._pushCall.call(this, _PROMISE_CALL.pipe, func); },
/**
* Pushes a function into a call array by type
* @param {Storm.Promise.CALL} callType
* @param {Function} func
* @return {Storm.Promise}
* @private
*/
_pushCall: function(callType, func) {
this._getCalls(callType).push(func);
return this;
},
/**
* Notify the promise - calls any functions in
* Promise.progress
* @return {Storm.Promise}
*/
notify: function() {
this._status = _PROMISE_STATUS.progressed;
var args = this._runPipe(arguments);
this._fire(_PROMISE_CALL.progress, args)._fire(_PROMISE_CALL.always, args);
return this;
},
/**
* Reject the promise - calls any functions in
* Promise.fail, then calls any functions in
* Promise.always
* @return {Storm.Promise}
*/
reject: function() {
// If we've already called failed or done, go no further
if (this._status === _PROMISE_STATUS.failed || this._status === _PROMISE_STATUS.done) { return this; }
this._status = _PROMISE_STATUS.failed;
// Never run the pipe on fail. Simply fail.
// Running the pipe after an unexpected failure may lead to
// more failures
this._fire(_PROMISE_CALL.fail, arguments)
._fire(_PROMISE_CALL.always, arguments);
this._cleanup();
return this;
},
/**
* Resolve the promise - calls any functions in
* Promise.done, then calls any functions in
* Promise.always
* @return {Storm.Promise}
*/
resolve: function() {
// If we've already called failed or done, go no further
if (this._status === _PROMISE_STATUS.failed || this._status === _PROMISE_STATUS.done) { return this; }
this._status = _PROMISE_STATUS.done;
var args = this._runPipe(arguments);
this._fire(_PROMISE_CALL.done, args)
._fire(_PROMISE_CALL.always, args);
this._cleanup();
return this;
},
/**
* Determine if the promise is in the status provided
* @param {String|Storm.Promise.STATUS} status key or STATUS value
* @return {Boolean}
*/
is: function(status) {
if (_.isNumber(status)) { return (this._status === status); }
return (this._status === Promise.STATUS[status]);
},
/**
* Returns the status of the Promise
* @return {Number} STATUS
*/
status: function() {
return this._status;
},
/**
* Fires a _PROMISE_CALL type with the provided arguments
* @param {Storm.Promise.CALL} callType
* @param {Array} args
* @return {Storm.Promise}
* @private
*/
_fire: function(callType, args) {
var calls = this._getCalls(callType),
idx = 0, length = calls.length;
for (; idx < length; idx++) {
calls[idx].apply(null, args);
}
return this;
},
/**
* Runs the pipe, catching the return value
* to pass to the next pipe. Returns the
* arguments to used by the calling method
* to proceed to call other methods (e.g. done/fail/always)
* @param {Array} args
* @return {Array} args
* @private
*/
_runPipe: function(args) {
var pipes = this._getCalls(_PROMISE_CALL.pipe),
idx = 0, length = pipes.length, val;
for (; idx < length; idx++) {
val = pipes[idx].apply(null, args);
if (val !== undefined) { args = [val]; }
}
return args;
},
/**
* Lazy generate arrays based on type to
* avoid creating disposable arrays for
* methods that aren't going to be used/called
* @param {Storm.Promise.CALL} type
* @return {Array}
* @private
*/
_getCalls: function(type) {
return this._calls[_PROMISE_CALL_NAME[type]] || (this._calls[_PROMISE_CALL_NAME[type]] = []);
},
/**
* Allows a promise to be called like a
* Function.call() or Function.apply()
*
* Very useful for passing a promise as
* a callback function to 3rd party code
* (as long as the third party code doesn't
* try to invoke the Promise directly)
*/
call: function() {
var args = _.toArray(arguments);
args.splice(0, 1); // Throw away the context
this.notify.apply(this, args);
},
apply: function(ctx, args) {
this.notify.apply(this, args);
},
/**
* Cleanup references to functions stored in
* arrays that are no longer able to be called
* @private
*/
_cleanup: function() {
this._getCalls(_PROMISE_CALL.done).length = 0;
this._getCalls(_PROMISE_CALL.fail).length = 0;
this._getCalls(_PROMISE_CALL.always).length = 0;
},
/**
* Debug string
* @return {String}
*/
toString: function() {
return _toString(_PROMISE, {
id: this._id,
status: _.invert(_PROMISE_STATUS)[this._status],
done: this._getCalls(_PROMISE_CALL.done).length,
fail: this._getCalls(_PROMISE_CALL.fail).length,
always: this._getCalls(_PROMISE_CALL.always).length,
progress: this._getCalls(_PROMISE_CALL.progress).length,
pipe: this._getCalls(_PROMISE_CALL.pipe).length
});
}
};
//----
// When #############################################################################
/**
* When to go with Promise. Used by calling `Storm.when()` and passing
* promises to listen to. Storm.when can be chained with multiple calls
* e.g. `Storm.when(p1, p2, p3).then(func).then(func).done(func).always(func);`
* @function Storm.when
* @param {...Storm.Promise} promises
* @return {Storm.Promise} A new promise that resolves when all of the given promises resolve.
*/
var When = Storm.when = (function(Promise) {
/**
* The when object. It's not exposed to the user,
* they only see a promise (with a .then() method),
* but all the magic happens here
*/
var When = function() {
/**
* Store our promise
* @type {Storm.Promise}
*/
this._p = null;
/**
* Store the promises being listened to
* @type {Array.}
*/
this._events = [];
};
When.prototype = {
constructor: When,
/**
* Called by the public Storm.when function to initialize
* the when object
* @return {Storm.Promise}
*/
init: function() {
this._events = _.isArray(arguments[0]) ? arguments[0] : _.toArray(arguments);
this._subscribe();
var promise = new Promise();
promise.then = function() { this.done.apply(this, arguments); };
this._p = promise;
return promise; // Return the promise so that it can be subscribed to
},
/**
* Subscribe to the promises passed and react
* when they fire events
* @private
*/
_subscribe: function() {
var check = _.bind(this._checkStatus, this),
fireProgress = _.bind(this._fireProgress, this),
events = this._events,
idx = events.length;
while (idx--) {
events[idx].done(check).fail(check).progress(fireProgress);
}
},
/**
* Check the status of all promises when
* any one promise fires an event
* @private
*/
_checkStatus: function() {
var events = this._events, evt,
total = events.length,
done = 0, failed = 0,
idx = total;
while (idx--) {
evt = events[idx];
// We're waiting for everything to complete
// so if there's an item with no status, stop
if (evt.status() === Promise.STATUS.idle) { return; }
if (evt.status() === Promise.STATUS.done) { done += 1; continue; }
if (evt.status() === Promise.STATUS.failed) { failed += 1; continue; }
}
this._fire(total, done, failed, arguments);
},
/**
* Based on the statuses of our promises, fire the
* appropriate events
* @param {Number} total total number of promises
* @param {Number} done promises in a done state
* @param {Number} failed promises in a failed state
* @param {Arguments} args arguments to pass
* @private
*/
_fire: function(total, done, failed, args) {
var promise = this._p; // Our promise
// If everything completed, call done (this will call always)
if (done === total) { return promise.resolve.apply(promise, args); }
// If everything failed, call fail (this will call always)
if (failed === total) { return promise.reject.apply(promise, args); }
// If everything fired, but they're not all one thing, then just call always.
// The only way to do that without exposing a public function in Promise is
// to use the private _fire event
if ((done + failed) === total) { return promise._fire(Promise.CALL.always, args); }
},
/**
* Handled separately from fire because we want to trigger
* anytime any of the promises progress regardless of sate
* @private
*/
_fireProgress: function() {
var promise = this._p;
promise.notify.apply(promise, arguments);
}
};
return function() {
var w = new When();
return w.init.apply(w, arguments);
};
}(Promise));
//----
// Tick ##########################################################################
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller
// fixes from Paul Irish and Tino Zijdel
(function() {
var lastTime = 0,
vendors = ['ms', 'moz', 'webkit', 'o'],
idx = 0, length = vendors.length;
for (; idx < length && !root.requestAnimationFrame; idx++) {
root.requestAnimationFrame = root[vendors[idx] + 'RequestAnimationFrame'];
root.cancelAnimationFrame = root[vendors[idx] + 'CancelAnimationFrame'] || root[vendors[idx] + 'CancelRequestAnimationFrame'];
}
if (!root.requestAnimationFrame) {
root.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime(),
timeToCall = Math.max(0, 16 - (currTime - lastTime)),
id = root.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
if (!root.cancelAnimationFrame) {
root.cancelAnimationFrame = function(id) { clearTimeout(id); };
}
}());
// Date.now polyfill
var _now = (function(Date) {
return Date.now || function() {
return new Date().valueOf();
};
}(Date));
/**
* A hook into a polyfilled `requestAnimationFrame`.
* Keeps a single raf that can be hooked into and
* prevent multiple implementations of raf.
*
* @namespace Storm.tick
*/
Storm.tick = (function() {
/**
* The name of the class
* @const
* @type {String}
* @private
*/
var _TICK = 'tick',
/**
* Stores the index of loop functions
* @type {Object}
* @private
*/
_hooks = {},
/**
* Our event object (for reuse)
* @type {Object}
*/
_e = {},
/**
* Stores function calls
* @type {Array.}
* @private
*/
_loop = [],
/**
* The id of this animation until another
* is called
* @type {Id}
* @private
*/
_id = null,
/**
* Whether the _loop is running
* @type {Boolean}
* @private
*/
_isRunning = true,
/**
* Runs the functions in the _loop
* @private
*/
_tick = function() {
var idx = 0,
length = _loop.length;
_e.now = _now();
while (idx < length) {
_loop[idx](_e);
idx += 1;
}
_id = root.requestAnimationFrame(_tick);
};
_tick(); // Auto-start
return /** @lends Storm.tick */ {
/**
* Add a function to `raf`.
* @param {Function|Array.} func A function or an array of functions to hook.
* @return {Id|Array.} Unique ID assigned to the hook, or an array of unique IDs if func was an array of functions.
*/
hook: function(func) {
if (_.isArray(func)) {
var ids = [],
idx = 0,
length = func.length;
for (; idx < length; idx++) {
ids[idx] = this.hook(func[idx]);
}
return ids;
}
if (!_.isFunction(func)) {
return console.error(_errorMessage(_TICK, 'Parameter must be a function'), func);
}
var id = _uniqId(_TICK);
_hooks[id] = _loop.length;
_loop.push(func);
return id;
},
/**
* Remove a function from `raf`.
* @param {Id} id Hook function ID to remove.
* @return {Storm.tick}
*/
unhook: function(id) {
_loop.splice(_hooks[id], 1);
delete _hooks[id];
return this;
},
/**
* Check if animate is running.
* @return {Boolean}
*/
isRunning: function() {
return _isRunning;
},
/**
* Start `raf` calling hooked functions.
* @return {Storm.tick}
*/
start: function() {
if (_isRunning) { return this; }
_isRunning = true;
_tick();
return this;
},
/**
* Stop `raf` from calling hooked functions.
* @return {Storm.tick}
*/
stop: function() {
if (!_isRunning) { return this; }
_isRunning = false;
root.cancelAnimationFrame(_id);
return this;
}
};
}());
//----
// Request ##########################################################################
/**
* @const
* @type {string}
* @private
*/
var _REQUEST = 'request',
/**
* Stores in-progress AjaxCalls by id
* @type {Object}
* @private
*/
_requestsRecords = {},
/**
* Private AjaxCall tracker. Only gets called from AjaxCall
* when the state of the call changes
* @private
*/
Request = {
/**
* Called when an AjaxCall is sent, notifies Storm.request
* Records the call in the records
* @param {Storm.AjaxCall} call
*/
send: function(call) {
// this call is already being tracked, stop
if (_requestsRecords[call.getId()]) { return; }
_requestsRecords[call.getId()] = call;
Storm.request.trigger('send', call);
},
/**
* Called when an AjaxCall is done, notifies Storm.request
* @param {Storm.AjaxCall} call
*/
done: function(call) {
Storm.request.trigger('done', call);
},
/**
* Called when an AjaxCall fails, notifies Storm.request
* @param {Storm.AjaxCall} call
*/
fail: function(call) {
Storm.request.trigger('fail', call);
},
/**
* Called when an AjaxCall is aborted, notifies Storm.request
* @param {Storm.AjaxCall} call
*/
abort: function(call) {
Storm.request.trigger('abort', call);
},
/**
* Called when an AjaxCall is done/aborted/failed, notifies Storm.request
* Removes the call from the records
* @param {Storm.AjaxCall} call
*/
always: function(call) {
// This call is not being tracked, stop
if (!_requestsRecords[call.getId()]) { return; }
delete _requestsRecords[call.getId()];
Storm.request.trigger('always', call);
}
};
/**
* Ajax tracking mechanism. Operates via events
* passing the AjaxCalls that trigger the events.
*
* Possible events are: `send`, `done`, `fail`, `abort`, `always`
*
* @namespace Storm.request
*/
Storm.request = Events.construct();
_.extend(Storm.request, /** @lends Storm.request# */ {
/**
* Get the requests in-progress.
* @function Storm.request.getQueue
* @return {Object}
*/
getQueue: function() {
return _requestsRecords;
},
/**
* Get the total number of requests in-progress.
* @function Storm.request.getTotal
* @return {Number}
*/
getTotal: function() {
return _.size(_requestsRecords);
},
/**
* Debug string
* @function Storm.request.toString
* @return {String}
*/
toString: function() {
return _toString(_REQUEST);
}
});
//----
// Ajax Call ########################################################################
/**
* The name of the class
* @const
* @type {String}
* @private
*/
var _AJAX_CALL = 'AjaxCall',
/**
* Available classifications for a call to reside in.
* Gives flexibility to a call to be in a classification
* that gives it meaning to the application and not the
* server
* @readonly
* @enum {Number}
* @private
*/
_CLASSIFICATION = {
nonblocking: 0,
blocking: 1
},
/**
* @param {String} type
* @private
*/
_addClassification = function(type) {
// The type has already been defined
if (type in _CLASSIFICATION) { return; }
// Using _.size ensures a unique id
// for the type passed
_CLASSIFICATION[type] = _.size(_CLASSIFICATION);
};
/**
* A wrapper for an ajax `call` configuration (referred to as a "call").
* This object can ajax, abort and be passed around the application.
*
* @class Storm.AjaxCall
* @param {Object} callObj
* @param {Object} opts
* @param {Object} callTemplate
*/
var AjaxCall = Storm.AjaxCall = function(callObj, opts, callTemplate) {
/**
* @type {Id}
* @private
*/
this._id = _uniqId(_AJAX_CALL);
/**
* The call object that will be sent to Storm.ajax
* @type {Object}
* @private
*/
this._call = this._configure(callObj, opts, callTemplate);
};
_.extend(AjaxCall, /** @lends Storm.AjaxCall */ {
/**
* @readonly
* @enum {Number}
*/
CLASSIFICATION: _CLASSIFICATION,
/**
* Add a classification type to the AjaxCall
* as a global option
* @param {String|Array.} type
*/
addClassification: function(type) {
if (_.isArray(type)) {
var idx = 0, length = type.length;
for (; idx < length; idx++) {
_addClassification(type[idx]);
}
} else {
_addClassification(type);
}
}
});
AjaxCall.prototype = /** @lends Storm.AjaxCall# */ {
constructor: AjaxCall,
/**
* Defaults.
* @type {Object}
*/
defaults: {
name: '',
type: 'GET',
content: 'application/json; charset=utf-8',
url: '',
cache: false,
classification: _CLASSIFICATION.nonblocking
},
/**
* Configure the call object so that it's ready to ajax
* @param {Object} providedCall call object
* @param {Object} opts configurations for the url
* @param {Object} callTemplate
* @returns {Storm.AjaxCall}
* @private
*/
_configure: function(providedCall, opts, callTemplate) {
var call = _.extend({}, this.defaults, callTemplate, providedCall);
call.url = _stringFormat(call.url, opts);
return call;
},
/**
* Get or set the data on the call. Passing a parameter
* will set the data where-as no parameters will return
* the data on the call
* @param {*} [data]
* @return {Storm.AjaxCall|*}
*/
data: function(data) {
if (!arguments.length) { return this._call.data; }
this._call.data = data;
return this;
},
/**
* Returns the call object
* @return {Object}
*/
getConfiguration: function() {
return this._call;
},
/**
* Get the private id of the AjaxCall
* @return {Id} id
*/
getId: function() {
return this._id;
},
/**
* Determine if the type passed is the same as this
* call's configuration
* @param {String} type GET, POST, PUT, DELETE
* @return {Boolean}
*/
isType: function(type) {
return (type.toUpperCase() === this._call.type.toUpperCase());
},
/**
* Determine if the classification passed is the same as this
* call's classification
* @type {@link Storm.AjaxCall.CLASSIFICATION}
* @param {Storm.AjaxCall.CLASSIFICATION} classification
* @return {Boolean}
*/
isClassification: function(type) {
return (type === this._call.classification);
},
/**
* Gets a property on the call configuration
* @param {String} key the key to get from the configuration
* @return {*}
*/
get: function(key) {
return this._call[key];
},
/**
* Sets a property on the call configuration
* @param {String} key the key to replace in the configuration
* @param {*} value the value to apply
* @return {Storm.AjaxCall}
*/
set: function(key, value) {
this._call[key] = value;
return this;
},
/**
* Uses {@link Storm.ajax} to ajax the stored call object
* @param {Storm.Promise} [promise]
* @return {Storm.Promise} request
*/
send: function(promise) {
promise = promise || new Storm.Promise();
// Ensure the promise has the ability to abort
promise.abort = _.bind(this.abort, this);
var self = this,
call = this._call,
params = _.extend({}, call, {
contentType: call.content,
success: function(data) {
if (promise) { promise.resolve(data); }
self.success.apply(self, arguments);
Request.done(self);
},
progress: function() {
if (promise) { promise.notify(); }
},
error: function(req, status, err) {
self.req = req;
self.status = status;
self.err = err;
if (promise) { promise.reject(req); }
// Abort
if (req.status === 0) {
Request.abort(req, status, err, self);
return;
}
// Fail
self.error.apply(self, arguments);
Request.fail(self);
},
complete: function() {
self.complete.apply(self, arguments);
Request.always(self);
}
});
// Record the call
Request.send(this);
var request = this.request = Storm.ajax.ajax(params);
return promise;
},
/**
* Fired when an `xhr` request is successful.
* Feel free to override.
* @param {Object|String|null} data
*/
success: function(data) {},
/**
* Fired when an `xhr` request completes.
* Feel free to override.
*/
error: function(req, status, err) {},
/**
* Fired when an `xhr` request completes.
* Feel free to override.
* @function
*/
complete: _noop,
/**
* Aborts the current request
* @return {Storm.AjaxCall}
*/
abort: function() {
if (this.request) {
this.request.abort();
this.request = null;
}
return this;
},
/**
* Debug string
* @return {String}
*/
toString: function() {
return _toString(_AJAX_CALL, {
id: this._id,
call: JSON.stringify(this._call)
});
}
};
//----
// Data Context #####################################################################
/**
* The name of the class
* @const
* @type {String}
* @private
*/
var _DATA_CONTEXT = 'DataContext';
/**
* Used to construct ajax calls to communicate with the server.
* Is a central location for configuration data
* to get and send data about models and collections
* @class Storm.DataContext
*/
var DataContext = Storm.DataContext = function() {
/**
* @type {Id}
* @private
*/
this._id = _uniqId(_DATA_CONTEXT);
};
_.extend(DataContext, /** @lends Storm.DataContext */ {
/**
* Default settings for the DataContext,
* where we are is usually useful
* @type {Object}
*/
settings: (function() {
var loc = root.location || {};
return {
host: loc.host,
hostname: loc.hostname,
origin: loc.origin,
pathname: loc.pathname,
port: loc.port
};
}()),
/**
* Add settings to the global `DataContext.settings`
* object. Basically a protected `_.extend`
* @param {Object} settings
* @return {Object} DataContext.settings
*/
addSettings: function(settings) {
return _.extend(DataContext.settings, settings);
},
/**
* Get a setting from the global `DataContext.settings`
* @param {String} setting
* @return {*} value
*/
getSetting: function(setting) {
return DataContext.settings[setting];
}
});
DataContext.prototype = /** @lends Storm.DataContext# */ {
constructor: DataContext,
/**
* AjaxCall, the constructor of the AjaxCall
* to use when configuring a new call object
* @type {Storm.AjaxCall}
*/
AjaxCall: AjaxCall,
/**
* @type {Object}
*/
defaults: {},
/**
* The merged defaults and passed options
* @type {Object}
*/
options: {},
/**
* Overrides AjaxCall's defaults
* @type {Object}
*/
callTemplate: {},
/**
* Get a setting from the global `DataContext.settings`
* @param {String} setting
* @return {*} value
*/
getSetting: function(setting) {
return DataContext.getSetting(setting);
},
/**
* Add more options to this data context
* @param {Object} opts
* @return {Storm.DataContext}
*/
extendOptions: function(opts) {
this.options = _.extend({}, this.defaults, opts);
return this;
},
/**
* Remove an option from this data context
* @param {String} key
* @return {Storm.DataContext}
*/
removeOption: function(key) {
delete this.options[key];
return this;
},
/**
* Creates and returns a new AjaxCall
* @param {Object} call see AjaxCall.defaults
* @param {Object} extensionData Addition configurations for the url
* @return {Storm.AjaxCall}
*/
createCall: function(call, extensionData) {
var configuration = _.extend({}, DataContext.settings, this.options, extensionData);
return new this.AjaxCall(call, configuration, this.callTemplate);
},
/**
* Debug string
* @return {String}
*/
toString: function() {
return _toString(_DATA_CONTEXT, {
id: this._id
});
}
};
//----
// Template ########################################################################
/**
* Centralized template registration for
* holding, compiling and rendering client-side
* templates
*
* The template engine only has one requirement,
* a `compile` function that returns a render function.
* The render function will be called with the data
* as the first parameter.
*
* Common libraries that use this paradigm are:
* Mustache, Handlebars, Underscore etc...
*
* @namespace Storm.template
*/
Storm.template = (function() {
/**
* The name of this class
* @const
* @type {String}
* @private
*/
var _TEMPLATE = 'template',
/**
* Template strings registered by an id string
* @type {Object}
* @private
*/
_templates = {},
/**
* Compiled templates registered by an id string
* @type {Object}
* @private
*/
_compiledTemplates = {},
/**
* The current template engine being used. Use
* underscore by default and proxy "compile" to
* "template"
* @private
*/
_engine = {
compile: _.template
};
/**
* Register a template
* @param {String} name id
* @param {String|Function|Array} tpl
* @param {Object} [opts]
* @return {Storm.template}
*/
var _register = function(name, tpl, opts) {
opts = opts || {};
// If an object, multiple items are being registered
// and tpl is actually opts
if (!_.isString(name)) {
var key, obj = name;
for (key in obj) {
_register(key, obj[key], tpl);
}
return this;
}
// Not an object, must be a string. If it's an
// id string, go get the html for the template
if (name[0] === '#') {
var element = document.getElementById(name.substring(1, name.length));
if (!element) { return console.error(_errorMessage(_TEMPLATE, 'Cannot find reference to "'+ name +'" in DOM'), name, tpl); }
tpl = element.innerHTML;
}
// If the tpl is a compiled template @type {Function},
// then register it to _compiledTemplates
if (opts.isCompiled) {
_compiledTemplates[name] = tpl;
return this;
}
_templates[name] = _coerceTemplateToString(tpl);
return this;
};
/**
* Coerce a template to a string value. If a function is
* passed, it's executed and coercion continues. If an array
* is passed, it is joined. All strings are trimmed to prevent
* any problems with the templating engine
* @param {String|Function|Array} tpl
* @return {String}
* @private
*/
var _coerceTemplateToString = function(tpl) {
if (_.isFunction(tpl)) { tpl = tpl.call(); }
if (_.isString(tpl)) { return tpl.trim(); }
if (_.isArray(tpl)) { return tpl.join('\n').trim(); }
console.error(_errorMessage(_TEMPLATE, 'Template (or the return value) was of unknown type'), tpl);
return '';
};
/**
* Get a template via a name
* @param {String} name id
* @return {Function} compiled template
* @private
*/
var _retrieve = function(name) {
// If there's a compiled template, return that one
var compTpl = _compiledTemplates[name];
if (compTpl) { return compTpl; }
if (!_engine) { console.error(_errorMessage(_TEMPLATE, 'No template engine is available'), _engine); }
return (_compiledTemplates[name] = _engine.compile(_templates[name]));
};
/**
* Remove a template from Storm.template.
* Removes both the string and compiled versions
* @param {String} name id
*/
var _remove = function(name) {
delete _templates[name];
delete _compiledTemplates[name];
};
/**
* Render a registered template
* @param {String} name id of the template
* @param {Object} [data] passed to the template engine as a parameter
* @return {String} rendered template
*/
var _render = function(name, data) {
var tpl = _retrieve(name);
return tpl(data || {});
};
return /** @lends Storm.template */ {
add: _register,
remove: _remove,
render: _render,
/**
* Sets the client-side templating engine
* for `Storm.template` to use.
* @param {Object} engine
*/
setEngine: function(engine) {
_engine = engine;
},
/**
* Return the registered template strings
* @param {String} [key] a specific template
* @return {String|Object}
*/
toJSON: function(key) {
var value = (key) ? _templates[key] : _templates;
return value;
},
/**
* Debug string
* @return {String}
*/
toString: function() {
return _toString(_TEMPLATE, {
size: _.size(_templates)
});
}
};
}());
//----
// View #############################################################################
/**
* The name of this class
* @const
* @type {String}
* @private
*/
var _VIEW = 'View';
/**
* A view at its most basic. Sets up a couple
* defaults for cloning and commonly used methods
* @class Storm.View
* @extends Storm.Events
* @param {Object} [opts]
*/
var View = Storm.View = function(opts) {
Events.call(this);
opts = opts || {};
/**
* @type {Id}
* @private
*/
this._id = _uniqId(_VIEW);
/** @type {Object} */
this.options = opts || {};
/** @type {Element} */
this.elem = opts.elem || null;
/** @type {String} */
this.template = this.template || opts.template || '';
};
_.extend(View.prototype, Events.prototype, /** @lends Storm.View# */ {
constructor: View,
/**
* Get the private id of the view
* @return {Number} id
*/
getId: function() { return this._id; },
/**
* Returns a clone of the view
* @return {Storm.View}
*/
clone: function() {
return new this.constructor(this.options);
},
/**
* Generates the HTML markup for the view.
* Must be overridden by subclasses.
* @function
* @return {String} HTML markup.
*/
render: _noop,
/**
* Returns the cached elem or caches and returns
* an elem using render
* @return {Element}
*/
getElem: function() {
return this.elem || (this.elem = this.render());
},
/**
* Debug string
* @return {String}
*/
toString: function() {
return _toString(_VIEW, {
id: this._id
});
}
});
//----
// Model ############################################################################
/**
* The name of the class
* @const
* @type {String}
* @private
*/
var _MODEL = 'Model';
/**
* The base data object for the application. Stores
* and protects a piece of data and gives an interface
* to manipulate it. Works in conjunction
* with a Collection to organize data into sets.
*
* @class Storm.Model
* @extends Storm.Events
*
* @param {Object} data Key-value pairs.
* @param {Object} [opts]
*/
var Model = Storm.Model = function(data, opts) {
Events.call(this);
/**
* @type {Id}
* @private
*/
this._id = _uniqId(_MODEL);
/**
* Hold on to the passed options both for
* reference inside the model and for cloning
* @type {Object}
*/
this.options = opts;
/**
* getters (recorded by key as a function)
* @type {Object}
*/
this._getters = {};
/**
* setters (recorded by key as a function)
* @type {Object}
*/
this._setters = {};
// Merge the data and the defaults
data = _.extend({}, this.defaults, data);
/**
* Protect the data, __ === secret... gentleman's agreement
* @type {Object}
*/
this.__data = this._duplicate(data);
/**
* Create duplicate of the original data to
* compare against for checks and allow restoration.
* Make sure these are protected as well
* @type {Object}
*/
this.__originalData = this.__previousData = this._duplicate(data);
if (this.comparator) {
/**
* If there's a Comparator, then bind it to this model
* @type {Storm.Comparator}
*/
this.comparator = _.bind(this.comparator, this);
}
};
_.extend(Model.prototype, Events.prototype, /** @lends Storm.Model# */ {
constructor: Model,
/**
* If not supporting complex data types (default), the model
* creates a reference-free version of the data to keep the
* data from being contaminated.
*
* If supporting complex data types, non-primitive values
* will be maintained in the model data, but exposes the
* possibility of contaminating the data object by outside
* sources
*
* @type {Boolean}
* @default false
*/
supportComplexDataTypes: false,
/**
* The defaults to be merged into the
* data object when constructed
* @type {Object}
*/
defaults: {},
/**
* Get the private id of the Model
* @return {Number} id
*/
getId: function() { return this._id; },
/**
* Restores the data of the model back to the
* original value
* @param {Object} opts
* @return {Storm.Model}
*/
restore: function(opts) {
// Set the current data back to the original data
this.__data = this._duplicate(this.__originalData);
// Check if silent
if (opts && !opts.isSilent) { return this; }
// Let every property know that it has
// been changed and fire a restore event
var prop;
for (prop in this.__data) {
this.trigger('change:' + prop, this.__data[prop]);
this.trigger('model:change', prop, this.__data[prop]);
}
this.trigger('model:restore');
return this;
},
/**
* Adds a getter for the property
* @param {String} prop
* @param {Function} func
* @return {Storm.Model}
*/
getter: function(prop, func) {
if (!_.isFunction(func)) { return console.error(_errorMessage(_MODEL, 'Getter must be a function.', prop, func)); }
if (this._getters[prop]) { return console.error(_errorMessage(_MODEL, 'Getter is already defined', prop, func)); }
this._getters[prop] = func;
return this;
},
/**
* Adds a setter for the property
* @param {String} prop
* @param {Function} func
* @return {Storm.Model}
*/
setter: function(prop, func) {
if (!_.isFunction(func)) { return console.error(_errorMessage(_MODEL, 'Setter must be a function', prop, func)); }
if (this._setters[prop]) { return console.error(_errorMessage(_MODEL, 'Setter is already defined', prop, func)); }
this._setters[prop] = func;
return this;
},
/**
* Get a value from the Model, passing it through the getter
* method if one exists. An array can be passed to get multiple
* values or a string to get a single value
* @param {String|Array.} prop
* @return {*|Array.<*>}
*/
get: function(prop) {
if (_.isArray(prop)) {
// Reuse the array passed, replacing
// each index with the value retrieved
// from the model.
var idx = prop.length;
while (idx--) {
prop[idx] = this._get(prop[idx]);
}
return prop;
}
return this._get(prop);
},
/**
* Private version of get. Gets single values
* @param {String} prop
* @return {*}
* @private
*/
_get: function(prop) {
// If a getter is set, call the function to get the return value
if (this._getters[prop]) {
return this._getters[prop].call(null, this.__data[prop]);
}
// Otherwise, return the value
return this.__data[prop];
},
/**
* Sets the value of a property in the Model. Values can be set
* in key-value pairs in an object, or as string/value as
* separate parameters
* @param {String|Object} prop
* @param {*} data
* @param {Object} opts
* @return {Storm.Model}
*/
set: function(prop, data, opts) {
// If prop is not a string (is an object), then set
// each key in the object to the value
if (!_.isString(prop)) {
var key;
for (key in prop) {
this._set(key, prop[key], opts);
}
return this;
}
// Otherwise, set the value
this._set(prop, data, opts);
return this;
},
/**
* Private version of set. Sets single values
* @param {String} prop
* @param {*} data
* @param {Object} [opts]
* @private
*/
_set: function(prop, data, opts) {
// If a setter is set
if (this._setters[prop]) {
data = this._setters[prop].call(null, data);
}
this._change(prop, data, opts);
},
/**
* Adds properties/values to the model data. Only works to add
* additional data to the model data, it will not modify any
* pre-existing data. An object can be passed to set multiple
* key-values or a string and value as separate parameters
* @param {String|Object} prop
* @param {*} data
* @param {Object} [opts]
* @return {Storm.Model}
*/
add: function(prop, data, opts) {
// If the prop is not a string (is an object)
// then add multiple key-values in one pass
if (!_.isString(prop)) {
var key;
for (key in prop) {
this._add(key, prop[key], opts);
}
return this;
}
this._add(prop, data, opts);
return this;
},
/**
* Removes properties from the model data. Only works to remove
* items that pre-exist in the data. No remove event will be fired
* if the property has an undefined value. An array can be passed
* to remove multiple properties or a string as a single parameter
* @param {String|Array.} prop
* @param {Object} [opts]
* @return {Storm.Model}
*/
remove: function(prop, opts) {
if (_.isArray(prop)) {
var idx = prop.length;
while (idx--) {
this._remove(prop[idx], opts);
}
return this;
}
this._remove(prop, opts);
},
/**
* Returns the previous value for the property
* @param {String} prop
* @return {*}
*/
previous: function(prop) {
return this.__previousData[prop];
},
/**
* Checks if the model data has the provided property
* @param {String} prop
* @return {Boolean}
*/
has: function(prop) {
return _exists(prop) ? (prop in this.__data) : false;
},
/**
* Checks if the model data has changed from the original data.
* If a prop is passed, then it will check if that property's value
* has changed and not the entire model data
* @param {String} [prop]
* @return {Boolean}
*/
hasChanged: function(prop) {
if (_exists(prop)) {
return (this.__originalData[prop] === this.__data[prop]) ? false : true;
}
var key;
for (key in this.__data) {
if (this.hasChanged(key)) { return true; }
}
return false;
},
/**
* Returns a clone of the model
* @return {Storm.Model}
*/
clone: function() {
return new this.constructor(this._duplicate(this.__data), this.options);
},
/**
* If not supporting complex data types (default), _duplicate
* creates a reference-free version of the data passed
* using native JSON.parse and JSON.stringify.
*
* If supporting complex data types, underscore's _.clone
* method is used, which will not create a reference-free
* version of complex data types, which may lead to pollution
* of the data, but will allow non-primitive values
*
* @param {*} data
* @return {*}
* @private
*/
_duplicate: function(data) {
// Keep null/undefined from being passed to JSON
// or needlessly cloned as both are primitives
if (!_exists(data)) { return data; }
return this.supportComplexDataTypes ? _.clone(data) : JSON.parse(JSON.stringify(data));
},
/**
* Retrieve the model data
* @return {Object}
*/
retrieve: function() {
return this.__data;
},
/**
* Duplicates the current model data and assigns it
* to previous data
* @private
*/
_backup: function() {
// Previous data should not be a reference, but a separate object
this.__previousData = this._duplicate(this.__data);
},
/**
* Adds a new property and data to the model data
* @param {String} prop
* @param {*} data
* @param {Object} [opts]
* @private
*/
_add: function(prop, data, opts) {
this._backup();
if (this.__previousData[prop] !== undefined) { return; } // This data isn't actually an addition
this.__data[prop] = this._duplicate(data);
if (this.__previousData[prop] === this.__data[prop]) { return; } // The data didn't actually change
if (opts && opts.isSilent) { return; } // Check if silent
this.trigger('add:' + prop, data);
this.trigger('model:add', prop, data);
},
/**
* Removes a property from the model data
* @param {String} prop
* @param {Object} [opts]
* @private
*/
_remove: function(prop, opts) {
this._backup();
if (this.__previousData[prop] === undefined) { return; } // The property is not present
var data = this.__data[prop];
delete this.__data[prop];
if (opts && opts.isSilent) { return; } // Check if silent
this.trigger('remove:' + prop, data);
this.trigger('model:remove', prop, data);
},
/**
* Changes a value on the model data
* @param {String} prop
* @param {*} data
* @param {Object} [opts]
* @private
*/
_change: function(prop, data, opts) {
this._backup();
this.__data[prop] = this._duplicate(data);
// Fire off change events
if (_.isEqual(this.__previousData[prop], this.__data[prop])) { return; } // The data didn't actually change
if (!this._validate()) { return (this.__data = this._duplicate(this.__previousData)); } // If invalid, revert changes
if (opts && opts.isSilent) { return; } // Check if silent
this.trigger('change:' + prop, data);
this.trigger('model:change', prop, data);
},
/**
* Compares this model to another. Used by Collection for
* sorting. Checks for `Storm.Comparator` to use natively but
* can be overwritten
* @param {Storm.Model} comparisonModel
* @return {Number} sort order (1, 0, -1)
*/
compareTo: function(comparisonModel) {
if (!this.comparator || !comparisonModel || !comparisonModel.comparator) { return 0; }
return this.comparator.getSortValue(this)
.localeCompare(this.comparator.getSortValue(comparisonModel));
},
/**
* Validates the model when a value is changed via
* .set(). Looks for a .validate() method on the model.
* @return {Boolean}
* @default true
* @private
*/
_validate: function() {
if (!this.validate) { return true; }
var isValid = !!this.validate();
if (!isValid) { this.trigger('model:invalid'); }
return isValid;
},
/**
* Returns the data for serialization
* @return {Object}
*/
toJSON: function() {
return this.__data;
},
/**
* Debug string
* @return {String}
*/
toString: function() {
return _toString(_MODEL, {
id: this._id
});
}
});
// Underscore methods that we want to implement on the Model.
_.each([
'each',
'isEqual',
'isEmpty',
'size',
'keys',
'values',
'pairs',
'invert',
'pick',
'omit'
], function(method) {
Model.prototype[method] = function() {
var args = _.toArray(arguments);
// Add this Model's data as the first argument
args.unshift(this.__data);
// the method is the underscore methods with
// an underscore context and the arguments with
// the models first
return _[method].apply(_, args);
};
});
//----
// Comparator #######################################################################
/**
* The name of the class
* @const
* @type {String}
* @private
*/
var _COMPARATOR = 'Comparator',
/**
* Stores sort types to use to compare models
* Default is alphabetical (0)
* @readonly
* @enum {Number}
* @default alphabetical
*/
_SORT = {
alphabetical: 0,
numeric: 1,
date: 2
},
/**
* Reverse look-up for _SORT
* @type {Object}
*/
_SORT_NAMES = _.invert(_SORT),
/**
* Add a sort type to the _SORT object
* @param {String} type
*/
_addSort = function(type) {
// The type has already been defined
if (type in _SORT) { return; }
// Using _.size ensures a unique id
// for the type passed
_SORT[type] = _.size(_SORT);
},
/**
* Caching of the sort value by model id
* @type {Object}
*/
_store = {};
/**
* Used by the Collection to sort Models. Having
* a separate object used by the model normalizes
* sorting and allows optimization by caching values
* @class Storm.Comparator
* @param {String} key the key on the model used for comparison
* @param {Storm.Comparator.SORT} [type]
*/
var Comparator = Storm.Comparator = function(key, type) {
/**
* @type {Id}
* @private
*/
this._id = _uniqId(_COMPARATOR);
/**
* The model key to use to sort
* @type {String}
* @private
*/
this._key = key;
/**
* The type of sorting we'll be doing
* @type {Storm.Comparator.SORT}
* @default alphabetical
* @private
*/
this._type = type || _SORT.alphabetical;
};
_.extend(Comparator, /** @lends Storm.Comparator */ {
/**
* Default string to use if no value is present to
* compare against.
* @type {String}
*/
HOIST: '___',
/**
* Sort types
* @readonly
* @enum {Number}
* @default alphabetical
*/
SORT: _SORT,
/**
* Add a sort type to the Comparator
* as a global option
* @param {String|Array.} type
*/
addSort: function(type) {
// If is an array, add multiple types
if (_.isArray(type)) {
var idx = 0, length = type.length;
for (; idx < length; idx++) {
_addSort(type[idx]);
}
} else {
_addSort(type);
}
// Refresh the sort names after addition
_SORT_NAMES = _.invert(_SORT);
}
});
Comparator.prototype = /** @lends Storm.Comparator# */ {
constructor: Comparator,
/**
* Bind to the key on the model that the comparator
* watches. If the key changes, invalidate the sort
* value so that it's recalculated
* @param {Storm.Model} model
*/
bind: function(model) {
model.on('change:' + this._key, _.bind(this.invalidateSortValue, this, model));
},
/**
* Invalidates the sort value of a model
* by deleting it from the store
* @param {Storm.Model} model
* @return {Storm.Comparator}
*/
invalidateSortValue: function(model) {
delete this.store[model.getId()];
return this;
},
/**
* Get the value to sort by
* @param {Storm.model} model
* @return {*}
*/
getSortValue: function(model) {
var id = model.getId();
if (_store[id]) { return _store[id]; }
if (!this[_SORT_NAMES[this._type]]) { return console.error(_errorMessage('Comparator', 'No method for the sort type assigned'), this._type, _SORT_NAMES[this._type]); }
var value = this[_SORT_NAMES[this._type]].call(this, model);
_store[id] = value;
return value;
},
/**
* Default alphabetical sort.
* This method gets the value from the model
* and ensures a string return value
* @param {Storm.Model} model
* @return {String} value
*/
alphabetical: function(model) {
var value = model.get(this._key);
value = _.exists(value) ? (value + '').toLocaleLowerCase() : Comparator.HOIST;
return value;
},
/**
* Default numeric sort.
* This method gets the value from the model
* and ensures a number return value
* @param {Storm.Model} model
* @return {Number} value
*/
numeric: function(model) {
var value = model.get(this._key) || 0;
value = +value;
return value;
},
/**
* Default date sort.
* This method gets the value from the model
* and ensures a date return value
* @param {Storm.Model} model
* @return {Date} value
*/
date: function(model) {
var value = model.get(this._key) || new Date();
value = _.isDate(value) ? value : new Date(value);
return value;
},
/**
* Debug string
* @return {String}
*/
toString: function() {
return _toString(_COMPARATOR, {
id: this._id,
key: this._key,
type: _SORT_NAMES[this._type]
});
}
};
//----
// Collection #######################################################################
/**
* The name of the class
* @const
* @type {String}
* @private
*/
var _COLLECTION = 'Collection';
var _getModelId = function(model) {
return (model instanceof Storm.Model) ? model.getId() : parseInt(model, 10) || -1;
};
/**
* A collection of Models
* @param {Object} [data]
* @class Storm.Collection
* @extends Storm.Events
*/
var Collection = Storm.Collection = function(data) {
Events.call(this);
data = data || {};
/**
* @type {Id}
* @private
*/
this._id = _uniqId(_COLLECTION);
/**
* Storage for the models
* @type {Array.}
* @private
*/
this._models = [];
this.add(data.models, _.extend({ isSilent: true }, data));
};
_.extend(Collection.prototype, Events.prototype, /** @lends Storm.Collection# */ {
constructor: Collection,
/** @type {Storm.Model} */
Model: Model,
/**
* Get the private id of the collection
* @return {Id} id
*/
getId: function() {
return this._id;
},
/**
* Create a new model
* @param {Object} model the model data
* @param {Object} [opts]
* @return {Storm.Model}
*/
newModel: function(model, opts) {
return new this.Model(model, opts);
},
/**
* Get all models
* @return {Array.}
*/
getModels: function() {
return this._models;
},
/**
* Get a model by key-value
* @param {String} key
* @param {*} value
* @return {Storm.Model}
*/
getBy: function(key, value) {
var models = this._models,
idx = models.length;
while (idx--) {
if (models[idx].get(key) === value) {
return models[idx];
}
}
return null;
},
/**
* Return the length of the models,
* same as `getModels().length`
* @return {Number} length
*/
length: function() {
return this._models.length;
},
/**
* Clear all of the models from the collection
* @return {Storm.Collection}
*/
clear: function() {
var models = this._models,
idx = models.length;
while (idx--) {
models[idx].trigger('destroy');
}
this._models.length = 0;
return this;
},
/**
* Overwrites the private models array
* with a new array of models
* @param {Array.} models
* @return {Array.}
*/
overwrite: function(models) {
return (this._models = models);
},
/**
* Retrieve a model at the provided index
* @param {Number} idx
* @return {Storm.Model}
*/
at: function(idx) {
return this._models[idx];
},
/**
* Get the index of a model
* @param {Storm.Model|Id} model
* @return {Number} index
*/
indexOf: function(model) {
var id = _getModelId(model),
models = this._models,
idx = models.length;
while (idx--) {
if (models[idx].getId() === id) { return idx; }
}
return -1;
},
/**
* Add models to the collection, creating new models
* if the model is not an instance of `Storm.Model`,
* sorting the models and firing events
* @param {Array.} models
* @param {Object} [opts]
* @param {Object} data additional data to pass to the new models
*/
add: function(models, opts) {
models = _.isArray(models) ? models.slice() : [models];
opts = opts || {};
var idx = 0, length = models.length,
model,
add = [],
at = opts.at;
for (; idx < length; idx++) {
model = models[idx];
if (!model) { continue; }
// If the model is not a Storm.Model, make it into one
if (!(model instanceof Storm.Model)) {
model = this.newModel(model, opts);
}
// Check if the model is valid
if (!model._validate()) {
if (!opts.isSilent) {
this.trigger('collection:invalid', model, models);
}
continue;
}
// If the model is a duplicate prevent it from being added and
// optionally merge it into the existing model.
if (this.get(model.getId())) {
if (opts.merge) {
var key,
obj = models[idx];
for (key in obj) {
model.set(key, obj[key], opts);
}
if (!opts.isSilent) {
model.trigger('collection:merge');
}
}
continue;
}
// This is a new model, push it to the add list
add.push(model);
}
// Add the new models
if (add.length) {
if (_exists(at)) {
var i = 0, len = add.length;
for (; i < len; i++) {
this._models.splice(at + i, 0, add[i]);
}
} else {
this._models = this._models.concat(add);
// Only sort if we're not adding the models
// at a specific point
this.sort(opts);
}
}
// Stop if silent
if (opts.isSilent) { return this; }
var self = this;
_.each(add, function(model, idx) {
model.trigger('collection:add', self);
});
this.trigger('add', add, opts);
return this;
},
/** Proxy for add */
set: function() { this.add.apply(this, arguments); },
/**
* As you would expect
* @param {Storm.Model} model
* @param {Object} opts will be passed to remove
* @return {Storm.Model} the removed model
*/
push: function(model, opts) {
opts = _.extend({ at: this._models.length }, opts);
this.add(model, opts);
return model;
},
/**
* As you would expect
* @param {Storm.Model} model
* @param {Object} opts will be passed to remove
* @return {Storm.Model} the removed model
*/
unshift: function(model, opts) {
opts = _.extend({ at: 0 }, opts);
this.add(model, opts);
return model;
},
/**
* Remove a model from the collection
* @param {Storm.Model|Storm.Model[]} models
* @param {Object} [opts]
* @return {Storm.Collection}
*/
remove: function(models, opts) {
models = _.isArray(models) ? models.slice() : [models];
opts = opts || {};
var idx = models.length, model;
while (idx--) {
model = models[idx];
model = (model instanceof Storm.Model) ? model : this.get(model);
if (!model) { continue; }
var index = this.indexOf(model);
if (index === -1) { continue; }
this._models.splice(index, 1);
if (!opts.isSilent) {
model.trigger('collection:remove', this);
}
}
if (opts.isSilent) { return this; }
this.trigger('remove', models, opts);
return this;
},
/**
* As you would expect
* @param {Object} opts will be passed to remove
* @return {Storm.Model} the removed model
*/
shift: function(opts) {
var model = this.at(0);
this.remove(model, opts);
return model;
},
/**
* As you would expect
* @param {Object} opts will be passed to remove
* @return {Storm.Model} the removed model
*/
pop: function(opts) {
var model = this.at(this.length - 1);
this.remove(model, opts);
return model;
},
/**
* As you would expect
* @param {Number} begin
* @param {Number} end
* @return {Array.}
*/
slice: function(begin, end) {
return this._models.slice(begin, end);
},
/**
* Force the collection to re-sort itself. You don't need to call this under
* normal circumstances, as the collection will maintain sort order as items
* are added.
* @param {Object} [opts]
* @return {Storm.Collection}
*/
sort: function(opts) {
opts = opts || {};
if (opts.skipSort || // Skipping sort
!this.length() || // Nothing to sort
!this.at(0).comparator) { // Models not sortable
return this;
}
Collection.arraySort.call(this._models, function(a, b) {
return a.compareTo(b);
});
if (!opts.silent) {
var self = this;
this.each(function(model, idx) {
model.trigger('collection:sort', self, idx);
});
this.trigger('sort', this, opts);
}
return this;
},
/**
* Get models matching the key-values passed
* @param {Object} values Hash containing key-value pairs
* @param {Boolean} [first] return first found
* @return {Storm.Model|Array.}
*/
where: function(values, first) {
if (_.isEmpty(values)) { return first ? undefined : []; }
var method = this[first ? 'find' : 'filter'];
return method.call(this, function(model) {
var key;
for (key in values) {
if (values[key] !== model.get(key)) { return false; }
}
return true;
});
},
/**
* Proxy for `.where(values, true)`
* @param {Object} values
* @return {Storm.Model}
*/
findWhere: function(values) {
return this.where(values, true);
},
/**
* Find a model by id
* @param {Id} id model _id
* @return {Storm.Model}
*/
findById: function(id) {
if (!_exists(id)) { return null; }
id = _.isNumber(id) ? id : parseInt(id, 10); // make sure id is a number
var models = this.getModels(),
idx = models.length,
model;
while (idx--) {
model = models[idx];
if (model && model.getId() === id) {
return model;
}
}
return null;
},
/**
* Get all model values of the provided key
* @param {String} key
* @return {Array.<*>}
*/
pluck: function(key) {
return _.invoke(this._models, 'get', key);
},
/**
* Gets a model by its id
* @param {Id} id
* @return {Storm.Model}
*/
getById: function(id) {
if (!_exists(id)) { return null; }
id = parseInt(id, 10); // make sure id is a number
var models = this._models,
idx = models.length;
while (idx--) {
if (models[idx].getId() === id) {
return models[idx];
}
}
return null;
},
/**
* Proxy for getById
* @alias {#getById}
*/
get: function(modelOrId) {
var id = modelOrId instanceof Storm.Model ? modelOrId.getId() : modelOrId;
return this.getById(id);
},
/**
* Drops all models from the collection
* @param {Object} [opts]
* @return {Storm.Collection}
*/
reset: function(opts) {
opts = opts || {};
this._models.length = 0;
if (!opts.isSilent) {
this.trigger('reset');
}
return this;
},
/**
* Returns a clone of the collection
* @return {Storm.Model}
*/
clone: function() {
return new this.constructor({ model: this.model, models: this._models });
},
/**
* Similar to `Storm.Model.retrieve`, returns all model data
* in the collection
* @return {Array.