appseeds.js | |
---|---|
Elegant JavaScript components for modern web applications. Made by @danburzo. Fork & contribute at github.com/danburzo/appseeds. The guiding principles were: speed, terseness and progressive disclosure. API / Annotated source codeWhat you're seeing below is the annotated source code slash makeshift API reference for AppSeeds. It was automatically generated with docco. Here goes: | |
Encapsulate the library to protect global scope. |
/*global exports, define, require, console*/
(function(){
var root = this;
|
Export AppSeeds for CommonJS and the browser. |
var AppSeeds, Seeds;
if (typeof exports !== 'undefined') {
AppSeeds = Seeds = exports;
} else {
AppSeeds = Seeds = root.AppSeeds = root.Seeds = {};
}
|
Current version of the application, using semantic versioning. |
Seeds.version = '0.7.0';
|
Polyfills, mostly for IE, around missing Array methods. |
if(!Array.isArray) {
Array.isArray = function (vArg) { return Object.prototype.toString.call(vArg) === "[object Array]"; };
}
|
Seeds.PubSubAlias: Seeds.PS Simple Publish/Subscribe implementation for the Mediator pattern.
It supports namespaced events like: |
Seeds.PubSub = Seeds.PS = {
|
Create a PubSub instance.
|
create: function(options) {
var ps = Seeds.o.beget(this._instanceMethods);
Seeds.o.mixin(ps, options, {
_pubsubEvents: {},
_pubsubHappened: {}
});
return ps;
},
|
Seeds.PubSub API |
_instanceMethods: {
|
Publish an event.
|
pub: function(eventString) {
var eventComponents = this._parseEventNamespace(eventString);
var eventArray, i, ilen, j, jlen, args, subscriber, ret, event;
for (i = 0, ilen = eventComponents.length; i < ilen; i++) {
eventArray = this._pubsubEvents[event = eventComponents[i]] || [];
args = Array.prototype.slice.call(arguments, 1);
for (j = 0, jlen = eventArray.length; j < jlen; j++) {
subscriber = eventArray[j];
ret = subscriber[0].apply(subscriber[1] || this, args);
if (subscriber[2] && ret !== false) {
this.unsub(event, subscriber[0]);
}
}
this._pubsubHappened[event] = args;
}
return this;
},
|
Parses the namespaced event string to identify its components. |
_parseEventNamespace: function(event) {
var events = [], str = '', ch, i;
for (i = 0; i < event.length; i++) {
if ((ch = event.charAt(i)) === ':') {
events.push(str);
}
str += ch;
}
events.push(str);
return events;
},
|
Subscribe a function to one or more events.
Careful! When subscribing a method to multiple events, if the events don't get published with compatible parameters, you can end up with weird behavior. To avoid this, it's best to keep a common interface for all events in the list. |
sub: function(eventStr, method, thisArg, flags) {
var events = eventStr.split(/\s+/), event, eventArray, i, len, oldArgs;
flags = flags || { once: false, recoup: false };
for (i = 0, len = events.length; i < len; i++) {
eventArray = this._pubsubEvents[event = events[i]];
if (eventArray) {
eventArray.push([method, thisArg, flags.once]);
} else {
this._pubsubEvents[event] = [[method, thisArg, flags.once]];
}
if (flags.recoup) {
oldArgs = this._pubsubHappened[event];
if (oldArgs) {
method.apply(thisArg || this, oldArgs);
}
}
}
return this;
},
|
Unsubscribe a function from one or more events.
If no method is provided, all attached methods will be unsubscribed from the event(s). |
unsub: function(eventStr, method) {
var events = eventStr.split(/\s+/), event, eventArray, newEventArray, i, j;
for (i = 0; i < events.length; i++) {
eventArray = this._pubsubEvents[event = events[i]];
newEventArray = [];
for (j = 0; method && j < eventArray.length; j++) {
if (eventArray[j][0] !== method) {
newEventArray.push(eventArray[j]);
}
}
this._pubsubEvents[event] = newEventArray;
}
return this;
},
|
Subscribe to an event once. The function will be unsubscribed upon successful exectution.
To mark the function execution as unsuccessful
(and thus keep it subscribed), make it return
|
once: function(eventStr, method, thisArg) {
return this.sub(eventStr, method, thisArg, { once: true });
},
|
Subscribe to an event, and execute immediately if that event was ever published before. If executed immediately, the subscriber will get as parameters the last values sent with the event.
|
recoup: function(eventStr, method, thisArg) {
return this.sub(eventStr, method, thisArg, { recoup: true });
},
|
Schedule an event to publish, using Seeds.Lambda.
While pub() publishes an event immediately, schedule() returns a scheduled task
which you need to trigger by using |
schedule: function() {
if (!Seeds.Lambda) {
return null;
}
return Seeds.Lambda.create.apply(
Seeds.Lambda,
[this.pub, this].concat(Array.prototype.slice.call(arguments))
);
}
},
|
PubSub Constants |
PUBLIC_API: ['pub', 'sub', 'unsub', 'once', 'recoup']
};
|
Seeds.StateManagerAlias: Seeds.SM StateManager implements a hierarchical state chart closely aligned with David Harel's definition from hi seminal paper Statecharts: A Visual Formalism For Complex Systems. Using a state chart to organize your application helps your modules react to state-specific events in a clear, maintainable way. |
Seeds.StateManager = Seeds.SM = {
|
Creates a new instance of State Manager. Used as follows:
|
create: function(options) {
var stateManager = Seeds.o.beget(this._instanceMethods);
|
Mix in a Seeds.PubSub instance to use in dispatching events. |
var pubsub = Seeds.o.facade(Seeds.PS.create(), Seeds.PS.PUBLIC_API, stateManager);
Seeds.o.mixin(stateManager, pubsub, {
_status: Seeds.StateManager.STATUS_READY,
root: 'root',
_states: {},
_actions: {},
_queue: {}
});
stateManager.state(stateManager.root, {
parent: null,
context: {},
defaultSubstate: null
});
stateManager.context = stateManager.state(stateManager.root).context;
stateManager.current = stateManager.root;
options = options || {};
if (typeof options === 'string' || Array.isArray(options)) {
stateManager.add(options);
}
return stateManager;
},
|
State Manager constantsThe state manager can accept a new transition. |
STATUS_READY: 0x01,
|
The state manager is currently in a transition, and therefore is locked. |
STATUS_TRANSITIONING: 0x02,
|
The state manager was paused as part of an asynchronous transition. |
STATUS_ASYNC: 0x03,
|
The flag to return in enter/exit to enter asynchronous mode and pause the state manager. |
ASYNC: false,
|
Seeds.StateManager API |
_instanceMethods: {
|
Name of the root state. |
root: 'root',
|
Current state. This should not be manually overwritten! |
current: null,
|
Hash for the collection of states, with state name as key and an object as value to keep information about the state:
|
_states: {},
|
The context of the current state, contains the actions and other 'private' methods defined with when() for that state. |
context: null,
|
The current status of the state manager. |
_status: null,
|
Where we left off when going into ASYNC mode. We need this in order to resume. |
_queue: null,
|
Get/set information about a state.
Returns the state object. |
state: function(stateName, val) {
if (val !== undefined) {
this._states[stateName] = val;
}
return this._states[stateName];
},
|
Gets the substates for a state.
Returns an array containing the names of the child states. |
children: function(stateName) {
var substates = [], i;
for (i in this._states) {
if (this._states.hasOwnProperty(i) && this._states[i].parent === stateName) {
substates.push(i);
}
}
return substates;
},
|
Navigates from the current state up to the root state, returning the list of state names that make up the branch. |
_toRoot: function(stateName) {
var route = [];
while(stateName) {
route.push(stateName);
stateName = this.state(stateName).parent;
}
return route;
},
|
Find the LCA (Lowest Common Ancestor) between two states. |
_lca: function(startState, endState) {
var exits = this._toRoot(startState), entries = this._toRoot(endState), lca, idx, i, j;
for (i = 0; i < exits.length; i++) {
idx = -1;
for (j = 0; j < entries.length; j++) {
if (entries[j] === exits[i]) {
idx = j;
break;
}
}
if (idx !== -1) {
lca = exits[i];
exits = exits.slice(0, i);
entries = entries.slice(0, idx).reverse();
break;
}
}
return { exits: exits, entries: entries, lca: lca };
},
|
Parse a state string. See StateManager.add() for expected formats. |
_getStatePairs: function(str) {
var tmp = str.split('->');
var parentState, childStates;
switch(tmp.length) {
case 0:
childStates = [];
break;
case 1:
parentState = this.root;
childStates = tmp[0].split(/\s+/);
break;
case 2:
parentState = tmp[0].replace(/^\s\s*/, '').replace(/\s\s*$/, ''); // trim
childStates = tmp[1].split(/\s+/);
break;
default:
this.pub('error', 'String ' + str + ' is an invalid state pair and has been dropped.');
childStates = [];
break;
}
var pairs = [], childState, defaultSubstateRegex = /^!/, i;
for (i = 0; i < childStates.length; i++) {
if (childStates[i]) {
pairs.push([
parentState,
childStates[i].replace(defaultSubstateRegex, ''),
defaultSubstateRegex.test(childStates[i])
]);
}
}
return pairs;
},
|
Transition to a new state in the manager. Attempting to transition to an inexistent state does nothing and publishes a warning. Likewise, attempting to transition to the same state as the current one will do nothing.
The additional parameters will be sent as arguments to the stay method of the destination state. |
go: function(stateName) {
var state = this.state(stateName),
args = Array.prototype.slice.call(arguments, 1) || [];
if (state === undefined) {
this.pub('error', 'State ' + stateName + ' not defined');
return;
}
if (this.current !== stateName && this._status === Seeds.SM.STATUS_READY) {
var states = this._lca(this.current, stateName);
args.unshift(states);
this._walk.apply(this, args);
}
return this;
},
_walk: function(states) {
var i, action, args = Array.prototype.slice.call(arguments, 1);
this._status = Seeds.SM.STATUS_TRANSITIONING;
/* exit to common ancestor */
for (i = 0; i < states.exits.length; i++) {
this.current = states.exits[i];
this.context = this.state(states.exits[i]).context;
if (typeof this.context.exit === 'function') {
if (this.context.exit.call(this) === Seeds.SM.ASYNC) {
this._status = Seeds.SM.STATUS_ASYNC;
this._queue = { exits: states.exits.slice(i+1), entries: states.entries, lca: states.lca, args: args };
return this;
}
}
this.pub('exit', this.current);
}
/* set common ancestor as current state */
this.current = states.lca;
this.context = this.state(this.current).context;
/* enter to desired state */
for (i = 0; i < states.entries.length; i++) {
this.current = states.entries[i];
this.context = this.state(states.entries[i]).context;
if (typeof this.context.enter === 'function') {
if (this.context.enter.call(this) === Seeds.SM.ASYNC) {
this._status = Seeds.SM.STATUS_ASYNC;
this._queue = { exits: [], entries: states.entries.slice(i+1), lca: states.entries[i], args: args };
return this;
}
}
this.pub('enter', this.current);
}
this._status = Seeds.SM.STATUS_READY;
var defaultSubstate = this.state(this.current).defaultSubstate;
if (defaultSubstate) {
/* go to default substate */
args.unshift(defaultSubstate);
this.go.apply(this, args);
} else {
/* execute 'stay' */
if (typeof this.context.stay === 'function') {
this.context.stay.apply(this, args);
}
args.unshift('stay', this.current);
this.pub.apply(this, args);
}
},
resume: function() {
var args = this._queue.args;
delete this._queue.args;
if (this._status === Seeds.SM.STATUS_ASYNC) {
args.unshift(this._queue);
this._walk.apply(this, args);
} else {
this.pub('error', 'State manager is not paused.');
}
},
|
Add states to the manager. All state names need to be unique. Attempting to add a state with an existing name will show a warning and the state will not be added. Likewise, attempting to add a state to an inexisting parent will show a warning and the state will not be added. Signatures:
Expected format for the state strings:
To specify a default substate, use
TODO would be cool to add |
add: function(stateConnection) {
var i, parentState, childState, isDefaultSubstate;
if (arguments.length > 1) {
for (i = 0; i < arguments.length; i++) {
this.add(arguments[i]);
}
} else {
if (Array.isArray(stateConnection)) {
for (i = 0; i < stateConnection.length; i++) {
this.add(stateConnection[i]);
}
} else if (typeof stateConnection === 'string') {
var pairs = this._getStatePairs(stateConnection);
for (i = 0; i < pairs.length; i++) {
parentState = pairs[i][0];
childState = pairs[i][1];
isDefaultSubstate = pairs[i][2];
if (!this.state(parentState)) {
this.pub('error', 'State ' + parentState +
' is not included in the tree. State not added.');
return;
}
if (this.state(childState)) {
this.pub('error', 'State ' + childState +
' is already defined. New state not added.');
}
this.state(childState, {
context: {},
defaultSubstate: null,
parent: parentState
});
if (isDefaultSubstate) {
if (this.state(parentState).defaultSubstate) {
this.pub('error', 'State ' + parentState +
' already has a default substate ' + this.state(parentState).defaultSubstate +
'. It will be overwritten with ' + childState);
}
this.state(parentState).defaultSubstate = childState;
}
}
} else if (typeof stateConnection === 'object') {
for (i in stateConnection) {
if (stateConnection.hasOwnProperty(i)) {
this.add(i + " -> " + stateConnection[i]);
}
}
}
}
return this;
},
|
Perform an action within the state manager. The manager will look through the entire state chain, starting from the current state and up to the root, for matching actions. You can break this chain by returning false in one of the actions, to prevent it from bubbling to the ancestor states. You can send any number of additional parameters to the action:
|
act: function(actionName) {
if (this._status !== Seeds.SM.ASYNC) {
|
Don't forget to regenerate the context to current state after the recursive call ends. |
var originalContext = this.context;
this._act(this.current, arguments);
this.context = originalContext;
} else {
this.pub('error', 'State manager is paused, can\'t perform action ' + actionName);
}
return this;
},
|
Get a reference to a state manager action.
Returns a reference to the appropriate invocation of act(). |
action: function(actionName) {
var that = this;
if (!this._actions[actionName]) {
this._actions[actionName] = function() {
return that.act.apply(that, [actionName].concat(Array.prototype.slice.call(arguments)));
};
}
return this._actions[actionName];
},
|
Act recursively up the state tree until an action returns |
_act: function(state, args) {
this.context = this.state(state).context;
var action = this.context[args[0]];
if (typeof action === 'function') {
var ret = action.apply(this, Array.prototype.slice.call(args, 1));
this.pub('act', args[0], state);
if (ret === false) {
return;
}
}
var parentState = this.state(state).parent;
if (parentState) {
this._act(parentState, args);
}
},
|
Attach a set of actions for one or more states. Subsequent declarations of the same action for a state will overwrite the previous ones. Signatures:
For example:
Actions defined here will receive all the parameters sent with StateManager.act().
|
when: function(stateString, actions, action) {
var i;
if (typeof stateString === 'object') {
for (i in stateString) {
this.when(i, stateString[i]);
}
} else {
var states = stateString.split(/\s+/);
for (i = 0; i < states.length; i++) {
var stateName = states[i];
if (stateName) {
var state = this.state(stateName);
if (!state) {
this.pub('error', 'State ' + stateName + ' doesn\'t exist. Actions not added.');
return;
}
/* interpret single function as `stay` method */
if (typeof actions === 'function') {
actions = { stay: actions };
} else if (typeof actions === 'string') {
/*
interpret add('stateName', 'actionName', function(){ ... })
as add('stateName', { actionName: function() {...}})
*/
var tmp = actions;
actions = {};
actions[tmp] = action;
}
if (!state.context) {
state.context = {};
}
for (i in actions) {
if (actions.hasOwnProperty(i)) {
state.context[i] = actions[i];
}
}
}
}
}
return this;
}
}
};
|
Seeds.LambdaLambda supercharges your functions, providing a clear API for delaying, repeating, and limiting their execution. |
Seeds.Lambda = {
|
Create a task.
Returns a StateManager.Lambda task. Can also be invoked as Seeds.f(), for convenience. |
create: function(callback, thisArg) {
var f = function() {
return f.reset.apply(f, arguments);
};
Seeds.o.mixin(f, this._instanceMethods, {
callback: callback,
args: Array.prototype.slice.call(arguments, 2),
thisArg: thisArg || this,
limit: null,
_lastCalled: null,
_timerId: null,
period: null,
type: null
});
return f;
},
|
Seeds.Lambda API |
_instanceMethods: {
|
Execute the task immediately. If the task is throttled (see Lambda.throttle()), it will not be executed with higher frequency than the one imposed by the throttle limit. If you provide parameters to this call, they will override the ones declared on create(). |
run: function() {
var now = (new Date()).getTime();
if (!this.limit || !this._lastCalled || (now - this._lastCalled > this.limit)) {
this.callback.apply(this.thisArg, arguments.length ? arguments : this.args);
this._lastCalled = now;
}
return this;
},
|
Stop the scheduled task. |
stop: function() {
if (this._timerId) {
if (this.type === 'delay') {
window.clearTimeout(this._timerId);
} else if (this.type === 'interval') {
window.clearInterval(this._timerId);
}
}
return this;
},
|
Reset the timer of the schedule task, effectively postponing its execution. |
reset: function() {
this.stop();
var that = this, f = function() { that.run(); };
if (this.type === 'delay') {
this._timerId = window.setTimeout(f, this.period);
} else if (this.type === 'interval') {
this._timerId = window.setInterval(f, this.period);
} else {
this.run.apply(this, arguments);
}
return this;
},
|
Delay task execution with a number of milliseconds.
|
delay: function(timeout) {
this.type = 'delay';
this.period = timeout;
return this;
},
|
Like delay, but also starts the timer. |
delayed: function(timeout) {
return this.delay(timeout).reset();
},
|
Repeat the execution of the task at a fixed interval.
|
repeat: function(interval) {
this.type = 'interval';
this.period = interval;
return this;
},
|
Like repeat, but also starts the timer. |
repeated: function(interval) {
return this.repeat(interval).reset();
},
|
Limit the execution frequency of a task.
|
throttle: function(limit) {
this.limit = limit;
return this;
}
}
};
|
Utility methods |
|
Seeds.oSeeds.o implements useful methods for object manipulation. |
Seeds.o = {
|
guid generates random GUIDs (Global Unique IDs) for things. |
guid: function() {
var S4 = function() { return (((1+Math.random())*0x10000)|0).toString(16).substring(1); };
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
},
|
mixin takes an arbitrary number of objects as arguments and mixes them into the first object. |
mixin: function(obj) {
var i, j;
for (i = 1; i < arguments.length; i++) {
var ext = arguments[i];
if (ext) {
for (j in ext) {
if (ext.hasOwnProperty(j)) {
obj[j] = ext[j];
}
}
}
}
return obj;
},
|
beget implements prototypal inheritance, as suggested in this article by Douglas Crockford. |
beget: function(o) {
var C = function() {};
C.prototype = o;
return new C();
},
|
facade returns a facade for a source object containing the desired subset of methods.
|
facade: function(sourceObj, api, destObj) {
var facade = {}, i;
if (Array.isArray(api)) {
for (i = 0; i < api.length; i++) {
facade[api[i]] = Seeds.o.bind(sourceObj, api[i], destObj || facade);
}
} else if (typeof api === 'object') {
for (i in api) {
if (api.hasOwnProperty(i)) {
facade[api[i]] = Seeds.o.bind(sourceObj, i, destObj || facade);
}
}
}
return facade;
},
|
bind implements functionality similar to Function.bind in that it attaches a method to a different context.
In addition to the usual bind behavior, bind detects method chaining and keeps it intact with the new context. |
bind: function(sourceObj, methodName, destObj) {
return function() {
var ret = sourceObj[methodName].apply(sourceObj, arguments);
/* detect method chaining */
return ret === sourceObj ? destObj : ret;
};
}
};
|
Seeds.fUse this as an alias for Seeds.Lambda.create. |
Seeds.f = function() {
return Seeds.Lambda.create.apply(Seeds.Lambda, arguments);
};
}(this));
|