mies
Version:
Ultra-simple modular layout and template system, with automatic data binding and smart routing, for jQuery
1,472 lines (1,261 loc) • 39.2 kB
JavaScript
(function() {
;
//////////////////////////////////////////////////////////////////////////////////////////
// //
// Begin Template engine. You can easily swap it. //
// //
//////////////////////////////////////////////////////////////////////////////////////////
// doT.js
// 2011, Laura Doktorova, https://github.com/olado/doT
// Licensed under the MIT license.
var doT = {
version: '1.0.0',
templateSettings: {
evaluate: /\{\{([\s\S]+?\}?)\}\}/g,
interpolate: /\{\{=([\s\S]+?)\}\}/g,
encode: /\{\{!([\s\S]+?)\}\}/g,
use: /\{\{#([\s\S]+?)\}\}/g,
useParams: /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,
define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,
defineParams:/^\s*([\w$]+):([\s\S]+)/,
conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,
iterate: /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g,
varname: 'binding',
strip: true,
append: true,
selfcontained: false
},
template: undefined, //fn, compile template
compile: undefined //fn, for express
};
// No need for globals
//
//if (typeof module !== 'undefined' && module.exports) {
// module.exports = doT;
//} else if (typeof define === 'function' && define.amd) {
// define(function(){return doT;});
//} else {
// (function(){ return this || (0,eval)('this'); }()).doT = doT;
//}
function encodeHTMLSource() {
var encodeHTMLRules = { "&": "&", "<": "<", ">": ">", '"': '"', "'": ''', "/": '/' },
matchHTML = /&(?!#?\w+;)|<|>|"|'|\//g;
return function() {
return this ? this.replace(matchHTML, function(m) {return encodeHTMLRules[m] || m;}) : this;
};
}
String.prototype.encodeHTML = encodeHTMLSource();
var startend = {
append: { start: "'+(", end: ")+'", endencode: "||'').toString().encodeHTML()+'" },
split: { start: "';out+=(", end: ");out+='", endencode: "||'').toString().encodeHTML();out+='"}
}, skip = /$^/;
function resolveDefs(c, block, def) {
return ((typeof block === 'string') ? block : block.toString())
.replace(c.define || skip, function(m, code, assign, value) {
if (code.indexOf('def.') === 0) {
code = code.substring(4);
}
if (!(code in def)) {
if (assign === ':') {
if (c.defineParams) value.replace(c.defineParams, function(m, param, v) {
def[code] = {arg: param, text: v};
});
if (!(code in def)) def[code]= value;
} else {
new Function("def", "def['"+code+"']=" + value)(def);
}
}
return '';
})
.replace(c.use || skip, function(m, code) {
if (c.useParams) code = code.replace(c.useParams, function(m, s, d, param) {
if (def[d] && def[d].arg && param) {
var rw = (d+":"+param).replace(/'|\\/g, '_');
def.__exp = def.__exp || {};
def.__exp[rw] = def[d].text.replace(new RegExp("(^|[^\\w$])" + def[d].arg + "([^\\w$])", "g"), "$1" + param + "$2");
return s + "def.__exp['"+rw+"']";
}
});
var v = new Function("def", "return " + code)(def);
return v ? resolveDefs(c, v, def) : v;
});
}
function unescape(code) {
return code.replace(/\\('|\\)/g, "$1").replace(/[\r\t\n]/g, ' ');
}
doT.template = function(tmpl, c, def) {
c = c || doT.templateSettings;
var cse = c.append ? startend.append : startend.split, needhtmlencode, sid = 0, indv,
str = (c.use || c.define) ? resolveDefs(c, tmpl, def || {}) : tmpl;
str = ("var out='" + (c.strip ? str.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g,' ')
.replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,''): str)
.replace(/'|\\/g, '\\$&')
.replace(c.interpolate || skip, function(m, code) {
return cse.start + unescape(code) + cse.end;
})
.replace(c.encode || skip, function(m, code) {
needhtmlencode = true;
return cse.start + unescape(code) + cse.endencode;
})
.replace(c.conditional || skip, function(m, elsecase, code) {
return elsecase ?
(code ? "';}else if(" + unescape(code) + "){out+='" : "';}else{out+='") :
(code ? "';if(" + unescape(code) + "){out+='" : "';}out+='");
})
.replace(c.iterate || skip, function(m, iterate, vname, iname) {
if (!iterate) return "';} } out+='";
sid+=1; indv=iname || "i"+sid; iterate=unescape(iterate);
return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+"<l"+sid+"){"
+vname+"=arr"+sid+"["+indv+"+=1];out+='";
})
.replace(c.evaluate || skip, function(m, code) {
return "';" + unescape(code) + "out+='";
})
+ "';return out;")
.replace(/\n/g, '\\n').replace(/\t/g, '\\t').replace(/\r/g, '\\r')
.replace(/(\s|;|\}|^|\{)out\+='';/g, '$1').replace(/\+''/g, '')
.replace(/(\s|;|\}|^|\{)out\+=''\+/g,'$1out+=');
if (needhtmlencode && c.selfcontained) {
str = "String.prototype.encodeHTML=(" + encodeHTMLSource.toString() + "());" + str;
}
try {
return new Function(c.varname, str);
} catch (e) {
if (typeof console !== 'undefined') console.log("Could not create a template function: " + str);
throw e;
}
};
doT.compile = function(tmpl, def) {
return doT.template(tmpl, null, def);
};
//////////////////////////////////////////////////////////////////////////////////////////
// //
// Override jQuery #data methods and init templates //
// //
//////////////////////////////////////////////////////////////////////////////////////////
// Override jQuery#data,#removeData methods, watching for changes (two arguments, remove),
// updating template if changes occur. No
//
// Normal #data behavior persists, with no modification, first.
//
// Errors should be noisy if you don't properly bind a template, etc.
//
var _data = $.fn.data;
var _removeData = $.fn.removeData;
var update = function(args, _this, force) {
var $target = $(_this);
if(force || args.length === 2 || $.isPlainObject(args[0])) {
var boundTemplateId = $target.attr("data-template");
if(boundTemplateId) {
$target.html(doT.template($("#" + boundTemplateId).text())($target.data()));
}
}
};
$.fn.data = function(key, value) {
var methRes = _data.apply(this, arguments);
update(arguments, this);
return methRes;
};
$.fn.removeData = function() {
var methRes = _removeData.apply(this, arguments);
update(arguments, this, 1);
return methRes;
};
// Ask the template to re-render for this target. No changes are made to the data.
//
$.fn.replayData = function() {
update(null, this, 1);
return this;
};
//////////////////////////////////////////////////////////////////////////////////////////
// //
// Create mies api //
// //
//////////////////////////////////////////////////////////////////////////////////////////
var AP_SLICE = Array.prototype.slice;
var OP_TO_STRING = Object.prototype.toString;
var HASH_WATCHERS = [];
var CURRENT_HASH = null;
var WATCHING_HASH = false;
var ROUTES = [];
var STORE = {};
var READY = [];
var CALLS = {};
var SUBSCRIPTIONS = {};
var LAST_SUBSCRIBE = null;
var MURMUR_SEED = parseInt(Math.random() * 1000000000);
// @see #join
//
var SESSION_ID;
var IS_SOCK;
var REMOTE_URL = "";
// Adjustment for trim methods.
//
// See http://forum.jquery.com/topic/faster-jquery-trim.
// See: http://code.google.com/p/chromium/issues/detail?id=5206
// Below is a fix for browsers which do not recognize as a whitespace character.
//
// @see #trim
// @see #trimLeft
// @see #trimRight
//
var TRIM_LEFT = /^\s+/;
var TRIM_RIGHT = /\s+$/;
if(!/\s/.test("\xA0")) {
TRIM_LEFT = /^[\s\xA0]+/;
TRIM_RIGHT = /[\s\xA0]+$/;
}
// Whether #trim is a native String method.
//
var NATIVE_TRIM = !!("".trim);
var PING_CHECK;
var LAST_CALL_ID;
// @see #nextId
//
var COUNTER = 1969;
// The events which can create a UI action (which will be routed).
//
// @see #mies#bindUI
//
var BOUND_UI_EVENTS = "abort change click dblclick error mouseup mousedown mouseout mouseover mouseenter mouseleave keydown keyup keypress focus blur focusin focusout load unload submit reset resize select scroll";
// To enable a UI element to fire actions you would so something like:
//
// <div data-action="click/run/this/route/">ROUTE!</div>
//
var ACTION_SELECTOR = "[data-action]";
// These are exposed via #mies#setOption
//
var OPTIONS = {
maxRetries : 3,
callTimeout : 5000
};
// ##ITERATOR
//
// Returns accumulator as modified by passed selective function.
// This is used by #arrayMethod in cases where there is not a native implementation
// for a given array method (#map, #filter, etc). It's a fallback, in other words,
// and hopefully will go vestigial over time.
//
// Also used by #iterate, being a general iterator over either objects or arrays.
// NOTE: It is usually more efficient to write your own loop.
//
// You may break the iteration by returning Boolean `true` from your selective function.
//
// @param {Function} fn The selective function.
// @param {Object} [targ] The object to work against. If not sent
// the default becomes Subject.
// @param {Mixed} [acc] An accumulator, which is set to result of selective
// function on each interation through target.
// @param {Object} [ctxt] A context to run the iterator in.
// @see #arrayMethod
// @see #iterate
//
var ITERATOR = function(targ, fn, acc, ctxt) {
ctxt = ctxt || targ;
acc = acc || [];
var x = 0;
var len;
var n;
if(mies.is(Array, targ)) {
len = targ.length;
while(x < len) {
acc = fn.call(ctxt, targ[x], x, targ, acc);
if(acc === true) {
break;
}
x++;
}
} else {
for(n in targ) {
if(targ.hasOwnProperty(n)) {
acc = fn.call(ctxt, targ[n], n, targ, acc);
if(acc === true) {
break;
}
}
}
}
return acc;
};
// ##FIND
//
// Find nodes in an object.
//
// @see #find
//
var FIND = function(key, val, path, obj, acc, curKey) {
// Keep @path a string
//
path = !!path ? path : "";
acc = acc || {
first : null,
last : null,
node : null,
nodes : [],
paths : [],
key : key,
value : val
};
var node = obj;
var p;
// Accumulate info on any hits against this node.
//
if(typeof val === "function" ? val(curKey, val, key, node) : node[key] === val) {
if(!acc.first) {
acc.first = path;
}
acc.last = path;
acc.node = node;
acc.nodes.push(node);
acc.paths.push(path);
}
// Recurse if children.
//
if(typeof node === "object") {
for(p in node) {
if(node[p]) {
FIND(key, val, path + (path ? "." : "") + p, node[p], acc, p);
}
}
}
return acc;
};
var mies = {
// ##_callObj
//
// An internal method, a constructor, which creates a call object suitable
// for existence on the #CALLS stack.
//
_callObj : function(opts) {
opts = opts || {};
this.result = opts.result || {};
this.time = new Date().getTime();
this.tries = 0;
this._tries = 0;
this.retry = opts.args ? function() {
++this._tries;
if(this._tries < this.tries) {
mies.publish.apply(mies, this.args);
return true;
}
return false;
} : $.noop;
this.args = opts.args || [];
this.route = opts.route || "";
this.passed = opts.passed || "";
},
// ##set
//
// Set a value at key.
//
// If you would like to have the value you've just set returned, use #setget.
// Otherwise, `this` (Mies) is returned.
//
set : function(key, value, obj) {
(obj || STORE)[key] = value;
return this;
},
// ##setnx
//
// Set only if the value of key is undefined.
//
setnx : function(key, value, obj) {
obj = obj || STORE;
if(typeof obj[key] === void 0) {
this.set(key, value, obj);
}
return this;
},
// ##getset
//
// Set a value at key AND return the value set.
//
getset : function(key, value, obj) {
this.set(key, value, obj);
return this.get(key, obj);
},
// ##get
//
// Get value at key.
//
get : function(key, obj) {
return (obj || STORE)[key];
},
// ##find
//
// Returns dot-delimited paths to nodes in an object, as strings.
//
// @param {String} key The key to check.
// @param {Mixed} val The sought value of key.
// @param {String} [path] A base path to start from. Useful to avoid searching the
// entire tree if we know value is in a given branch.
// @param {Object} [t] An object to search in. Defaults to STORE.
//
find : function(key, val, path, t) {
return FIND(key, val, path, t || STORE);
},
// ##each
//
each : function(targ, fn, acc, scope) {
return ITERATOR(targ, function(elem, idx, targ) {
fn.call(scope, elem, idx, targ);
}, acc);
},
// ##map
//
map : function(targ, fn, acc, scope) {
return ITERATOR(targ, function(elem, idx, targ, acc) {
acc[idx] = fn.call(scope, elem, idx, targ);
return acc;
}, acc);
},
// ##filter
//
filter : function(targ, fn, acc, scope) {
return ITERATOR(targ, function(elem, idx, targ, acc) {
fn.call(scope, elem, idx, targ) && acc.push(elem);
return acc;
}, acc);
},
// ##all
//
all : function(targ, fn, acc, scope) {
var hit = true;
ITERATOR(targ, function(elem, idx, targ, acc) {
if(!fn.call(scope, elem, idx, targ)) {
hit = false;
return true;
}
});
return hit;
},
// ##any
//
any : function(targ, fn, acc, scope) {
var hit = false;
ITERATOR(targ, function(elem, idx, targ, acc) {
if(fn.call(scope, elem, idx, targ)) {
hit = true;
return true;
}
});
return hit;
},
// ##pluck
//
pluck : function(targ, targAtt) {
return ITERATOR(targ, function(elem, idx, targ, acc) {
elem.hasOwnProperty(targAtt) && acc.push(elem[targAtt]);
return acc;
}, []);
},
// ##leftTrim
//
// Removes whitespace from beginning of a string.
//
// @param {String} t The string to trim.
//
leftTrim : function(t) {
return t.replace(TRIM_LEFT, "");
},
// ##rightTrim
//
// Removes whitespace from end of a string.
//
// @param {String} t The string to trim.
//
rightTrim : function(t) {
return t.replace(TRIM_RIGHT, "");
},
// ##trim
//
// Removes whitespace from beginning and end of a string.
//
// @param {String} [t] The string to trim.
//
trim : function(t) {
return NATIVE_TRIM
? t.trim()
: t.replace(TRIM_LEFT, "").replace(TRIM_RIGHT, "");
},
// ##nextId
//
// Increments and returns the counter.
//
nextId : function(pref) {
COUNTER += 1;
return pref ? pref + COUNTER : COUNTER;
},
// ##is
//
// @param {Mixed} type An object type.
// @param {Mixed} val The value to check.
// @type {Boolean}
//
// Checks whether `val` is of requested `type`.
//
is : function(type, val) {
// Here we're allowing for a check of undefined:
// mies.is(undefined, [some undefined var]) // true
//
// Otherwise, we throw an error (rare case
//
if(type === void 0) {
return val === type;
}
if(val === void 0) {
return false;
}
var p;
switch(type) {
case Array:
return OP_TO_STRING.call(val) === '[object Array]';
break;
case Object:
return OP_TO_STRING.call(val) === '[object Object]';
break;
case "numeric":
return !isNaN(parseFloat(val)) && isFinite(val);
break;
case "emptyObject":
for(p in val) {
return false;
}
return true;
break;
default:
return val.constructor === type;
break;
}
},
// ##murmurhash
//
// JS Implementation of MurmurHash3 (r136) (as of May 20, 2011)
//
// @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
// @see http://github.com/garycourt/murmurhash-js
// @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
// @see http://sites.google.com/site/murmurhash/
//
// @param {string} key ASCII only
// @param {number} seed Positive integer only
// @return {number} 32-bit positive integer hash
//
murmurhash : function(key, seed) {
var remainder, bytes, h1, h1b, c1, c1b, c2, c2b, k1, i;
remainder = key.length & 3; // key.length % 4
bytes = key.length - remainder;
h1 = seed;
c1 = 0xcc9e2d51;
c2 = 0x1b873593;
i = 0;
while (i < bytes) {
k1 =
((key.charCodeAt(i) & 0xff)) |
((key.charCodeAt(++i) & 0xff) << 8) |
((key.charCodeAt(++i) & 0xff) << 16) |
((key.charCodeAt(++i) & 0xff) << 24);
++i;
k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff;
h1 ^= k1;
h1 = (h1 << 13) | (h1 >>> 19);
h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff;
h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16));
}
k1 = 0;
switch (remainder) {
case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
case 1: k1 ^= (key.charCodeAt(i) & 0xff);
k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
h1 ^= k1;
}
h1 ^= key.length;
h1 ^= h1 >>> 16;
h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
h1 ^= h1 >>> 13;
h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff;
h1 ^= h1 >>> 16;
return h1 >>> 0;
},
// ##setOption
//
// Set a Mies option.
//
setOption : function(k, v) {
if(arguments.length === 2 && OPTIONS[k]) {
OPTIONS[k] = v;
}
},
// ##extend
//
// Adds a method to Mies. Simply does some checking to ensure validity.
//
// @param {Mixed} name The name of the method. You may send multiple
// meth/func pairs by passing a map as #name.
// @param {Function} [func] If sending a {String} #name, the method.
//
extend : function(name, func) {
if(mies.is(Object, name)) {
var p;
for(p in name) {
mies.extend(n, name[p]);
}
} else if(!mies.hasOwnProperty(name) && typeof func === "function") {
mies[name] = func;
}
return this;
},
//////////////////////////////////////////////////////////////////////////////////////
// //
// URL Hash Methods //
// //
//////////////////////////////////////////////////////////////////////////////////////
// ##watchHash
//
// Enables the binding of handlers to hash change events.
//
// @param {Function} [handler] Shortcut, equivalent to:
// watchHash().addHashHandler(handler)
//
// @see #unwatchHash
// @see #addHashHandler
// @see #removeHashHandler
// @see #updateHash
//
watchHash : function(handler) {
this.addHashHandler(handler);
this.updateHash(this.getHash());
var runHandlers = function(hash) {
mies.each(HASH_WATCHERS, function(w) {
var h = hash.substring(1, Infinity);
h && w(h);
});
}
if("onhashchange" in window) {
window.onhashchange = function() {
runHandlers(mies.getHash());
};
// #onhashchange waits for subsequent change. When hash watching
// starts we want to execute any url fragment in the current location (as
// would happen with fallback method below).
//
runHandlers(this.getHash());
}
else {
window.setInterval(function() {
var ch = mies.getHash();
if(ch !== CURRENT_HASH) {
mies.updateHash(ch)
runHandlers(CURRENT_HASH);
}
}, 200);
}
WATCHING_HASH = handler;
return this;
},
unwatchHash : function() {
WATCHING_HASH = false;
return this;
},
addHashHandler : function(fn) {
if(typeof fn === "function") {
this.removeHashHandler(fn);
HASH_WATCHERS.push(fn);
}
return this;
},
removeHashHandler : function(fn) {
HASH_WATCHERS = mies.filter(HASH_WATCHERS, function(f) {
return f !== fn;
});
return this;
},
updateHash : function(hash) {
CURRENT_HASH = window.location.hash = encodeURIComponent(hash);
return this;
},
getHash : function() {
return decodeURIComponent(window.location.hash);
},
//////////////////////////////////////////////////////////////////////////////////////
// //
// pub/sub/route methods //
// //
//////////////////////////////////////////////////////////////////////////////////////
// ##publish
//
// Will Publish to a route, causing a broadcast from the server which can
// be subscribed to (on same route).
//
// @param {Mixed} Either a route, or an Array of routes.
// @param {Object} [postdata] This is always a POST.
// @param {Mixed} [passed] Data to be passed along to any handlers.
// @param {Boolean} [bType] Whether to do a mass broadcast.
// @param {Boolean} [idem] Whether
//
// @see #massPublish
//
publish : function(route, postdata, passed, bType, idem) {
postdata = postdata || {};
// Using arguments if idem, arguments + random if not.
//
LAST_CALL_ID = this.murmurhash(JSON.stringify(arguments) + (idem ? "" : Math.random()), MURMUR_SEED);
var callObj = CALLS[LAST_CALL_ID];
if(callObj && idem) {
mies.route.call(callObj, callObj.route, "broadcast", callObj.result, callObj.passed);
return this;
}
CALLS[LAST_CALL_ID] = new mies._callObj({
args : AP_SLICE.call(arguments),
route : route,
passed : passed,
idem : idem
});
var cid = bType ? route : LAST_CALL_ID;
bType = bType || 1;
// Of note:
//
// The -callid header is echoed by the server on an event, sent
// as the value of #callId when returned. On mass publish
// we are sending the route (as the specific details of the individual client
// call objects have no relevance across n clients).
//
// @see #join
//
var opts = {
type : "POST",
url : REMOTE_URL + route,
data : postdata,
dataType : "json",
headers : {
"x-mies-sessid" : SESSION_ID,
"x-mies-callid" : cid,
"x-mies-broadcast" : bType
},
timeout : OPTIONS.callTimeout
}
if(REMOTE_URL) {
opts.crossDomain = true;
opts.dataType = "jsonp";
// JSONP uses a script insert, not an xhr request. As such, headers cannot
// be sent. Send them as a query.
//
opts.url += "?x-mies-sessid=" + SESSION_ID + "&x-mies-callid=" + cid + "&x-mies-broadcast=" + bType;
}
jQuery.ajax(opts);
return mies;
},
// ##massPublish
//
// A shortcut bridge to #publish, passing correct broadcast type.
// Recommended for code clarity.
//
// @see #publish
//
massPublish : function(route, postdata, passed, idem) {
return mies.publish(route, postdata, passed, 2, idem);
},
// ##nearPublish
//
// A shortcut bridge to #publish, passing correct broadcast type.
// Recommended for code clarity.
//
// @see #publish
//
nearPublish : function(route, postdata, passed, idem) {
return mies.publish(route, postdata, passed, 3, idem);
},
publishCache : function(route, postdata, passed, bType) {
return mies.publish(route, postdata, passed, bType, 1);
},
massPublishCache : function(route, postdata, passed) {
return mies.publish(route, postdata, passed, 2, 1);
},
nearPublishCache : function(route, postdata, passed) {
return mies.publish(route, postdata, passed, 3, 1);
},
// ##subscribe
//
// Register a route for this interface which can now be published to.
//
// @param {String} route
//
subscribe : function(route, times) {
var p = this.parseRoute(route);
// Note that no checking is done for duplicate route subscription. This
// may or may not be what you want. To avoid duplicate subscriptions,
// use #subscribenx
//
if(typeof p === "object" && p.compiled) {
LAST_SUBSCRIBE = ROUTES[ROUTES.push({
regex : p.compiled,
times : times
}) -1];
}
return mies;
},
// ##subscribenx
//
// Only subscribe if this route has no other subscribers.
//
subscribenx : function(route, handler, times) {
var p = this.parseRoute(route);
var i = ROUTES.length;
// An identical route has already been registered. Exit.
//
while(i--) {
if(ROUTES[i].regex === p.compiled) {
return this;
}
}
return subscribe(route, handler, times);
},
// ##unsubscribe
//
unsubscribe : function(route, handler) {
var len = ROUTES.length;
while(len--) {
if(ROUTES[len].handler === handler) {
delete ROUTES[len];
}
}
return this;
},
// ##retry
//
retry : function(r) {
CALLS[LAST_CALL_ID].tries = Math.min(OPTIONS.maxRetries, 1*r);
return this;
},
// ##route
//
// Routes an action. There are two types of action: [A]UI Action (click, etc), and
// [B]S.S.E. (Server-Sent-Event). Called in one of two contexts (value of `this`):
//
// [A] : The event target (equiv. to $(event.currentTarget)).
// [B] : The Call Object (CALLS[id] -- see #publish)
//
// @param {String} r The route.
// @param {String} action The action which caused this routing. In the case of a
// client action, this is an event, like "click"
// or "mousedown". In the case of a server-sent-event it
// will always be "broadcast".
// @param {Object} result The action result. In the case of a client action, this
// is a jQuery event object. In the case of a S.S.E. this
// will be the response data.
// @param {Mixed} [passed] Any data passed by the original call.
//
// @see #bindUI
// @see #join
// @see #publish
//
route : function(r, action, result, passed) {
var i = ROUTES.length;
var m;
var rob;
var args;
// When routing "by hand" there may be no #result sent.
//
result = result || {};
while(i--) {
rob = ROUTES[i];
// If the regex matches (not null) will receive an array, whose first argument
// is the full route, and subsequent args will be any :part matches on
// the route. The first arg is shifted out, below, leaving only :part matches,
// which come to form the first arguments sent to route event handlers.
//
// @example route: "/foo/bar/:baz" < "foo/bar/something"
// m = ["foo/bar/something","something"]
//
if(m = r.match(rob.regex)) {
// This is the full route, first arg of successful match.
//
r = m.shift();
// Either a client action (action) or a socket action (broadcast).
// All subscribe handlers receive three arguments:
//
// 1. The action. This is only relevant on client actions, where
// it will be something like "click" or "mouseup". Socket push is
// always of action type "broadcast".
// 2. Passed object. Client actions may pass along values (sent
// when #publish is called. Broadcasts never have passed arguments,
// so this will always be an empty object.
// 3. The route. Both types receive this.
//
// #always is (ahem) always called, regardless of whether action or
// broadcast, receiving the same arguments.
//
// #error is only called if the #result#error is not undefined.
//
// All methods except for #error are called in the scope of the #result.
// #error is called within the #callObject scope, which is the scope
// this method (#route) is called within. The call object, usefully for
// #error, has a #retry method. See #publish.
//
// @see #publish
//
args = m.concat(action, passed, r);
if(result.hasOwnProperty("error")) {
// Note how we shift the actual error message onto the front of args
//
rob.error && rob.error.apply(this, [result.error].concat(args));
} else if(action === "broadcast") {
rob.broadcast && rob.broadcast.apply(result, args);
} else {
// General .action binding
//
rob.action && rob.action.apply(result, args);
// Specific (.click, .mousedown) binding.
//
rob[action] && rob[action].apply(result, args);
}
rob.always && rob.always.apply(result, args);
// For routes with a limit on call # remove if we've reached it.
//
if(rob.times) {
rob.times--;
if(rob.times < 1) {
ROUTES.splice(i, 1);
}
}
}
}
return mies;
},
// ##routeBroadcast
//
// Route messages received on socket. Expects a callId, and a result.
// Main responsibility is to dispatch to #route if all is well, or to retry
// if that has been requested.
//
// @param {String} id The call id.
// @param {Object} result The server response.
//
routeBroadcast : function(id, result) {
var callObj = CALLS[id];
if(!callObj) {
return mies;
}
callObj.result = result;
var cleanup = function() {
mies.route.call(callObj, callObj.route, "broadcast", result, callObj.passed);
if(!callObj.idem) {
delete CALLS[id];
}
}
// If in an error state and a retry was requested, run #retry.
// Otherwise route, then destroy the call object.
// Note that when the retries have finished (#retry returns false) we
// ultimately route the last result.
//
if(callObj.tries > 0 && result.error) {
if(!callObj.retry()) {
cleanup();
}
} else {
cleanup();
}
return mies;
},
// ##parseRoute
//
// Accepts a route (eg. /users/:userid/:type) and returns an object containing:
//
// #serialized : A regular expression matching the route, as a string.
// #compiled : A regular expression matching the route.
//
// Also accepts RegExp objects as a route.
//
// @param {Mixed} route The route. Either a string to be converted to a regex,
// or a regex.
//
parseRoute : function(route) {
var ret = {};
if(route.constructor === RegExp) {
ret.serialized = new String(route);
ret.compiled = route;
return ret;
}
// Leading and trailing slashes are optional. We remove these from the
// route and bracket all route regexes with `/?`.
//
if(route.charAt(route.length -1) === "/") {
route = route.substring(0, route.length -1);
};
if(route.charAt(0) === "/") {
route = route.substring(1, Infinity);
};
// Replace
// 1. All splats with a all-inclusive match (capture all that remains in the route).
// 2. All :key tokens with a group which captures all characters until first slash.
//
// Note as well that the "intra-slash" matcher ([^/]*) will match any non-slash
// character between 0(zero) and N times -- which means that a route like
// /foo/:bar/:baz will be matched given foo/// or foo/23// or foo/23/
// (but not foo/23).
//
ret.serialized = new String('^/?' + route + '/?$').replace(/\*/g, function() {
return "(.*)";
}).replace(/:([\w]+)/g, function(token, key, idx, orig) {
return "([^/]*)";
})
ret.compiled = new RegExp(ret.serialized);
return ret;
},
// ##addRouteEvent
//
// Add a named method bindable within a #subscribe block.
//
// @example mies.addRouteEvent("foo")
// mies.subscribe("/some/route")
// mies.foo(function() {
// // This can now be fired with mies.route("/some/route", "foo", {data: "here"})
// })
//
// This is the method used to bind #action, #broadcast, and the other core subscribe methods.
//
// @param {String} ev A string name for this event.
//
addRouteEvent : function(ev) {
mies[ev] = function(fn) {
if(LAST_SUBSCRIBE) {
LAST_SUBSCRIBE[ev] = fn;
}
return mies;
}
return mies;
},
//////////////////////////////////////////////////////////////////////////////////////
// //
// UI Binding //
// //
//////////////////////////////////////////////////////////////////////////////////////
// ##bindUI
//
// Route all events originating on elements with a `data-action` attribute.
//
// NOTE: you may set any number of space-separated routes.
//
// @example If I want a <div> to publish to a route when it is clicked:
// <div data-action="click/some/route/here">clickme</div>, where
// `click` indicates the action to bind, and `some/route/here`
// being the actual route published to.
//
// Also: <div data-action="click/foo mouseover/bar mouseout/baz">
//
// @see #route
//
bindUI : function() {
$(document.body).on(BOUND_UI_EVENTS, ACTION_SELECTOR, function(event) {
var $target = jQuery(event.currentTarget);
var actionRoute = $target.attr("data-action").split(" ");
var pass = {};
var readfrom = $target.attr("readfrom");
var form;
var parent;
mies.each(actionRoute, function(ar) {
var rData = ar.match(/([!#\w]+)\/([\/\w]+)([\/]?.*)/);
var type = event.type;
// If malformed, exit
//
if(!rData || !rData[1] || !rData[2]) {
return mies;
}
var action = rData[1];
var route = rData[2];
var terminals = rData[3];
var hashedRoute = "#" + route;
// Special cases
//
// If the user action is preceeded by a bang(!) then this is a direct publish
// routing. Note that the action type (such as `click`) must still match.
//
// If preceeded by a hash(#) we update the hash (which is watched),
// allowing back button, bookmarking, etc.
//
// If terminated by a period(.) prevent default.
// If terminated by a caret(^) stop propagation.
// Use both to do both.
//
// click/route.
// click/route^
// click/route.^
// click/route^.
//
if(action.charAt(0) === "!" && action.substring(1, Infinity) === type) {
return mies.publish(route);
}
if(action !== type) {
return mies;
}
if(!!terminals) {
(terminals.indexOf("^") > -1) && event.stopPropagation();
(terminals.indexOf(".") > -1) && event.preventDefault();
}
// When we have a new route request with the ! directive (update hash), and the
// current hash differs, update the hash.
//
if(CURRENT_HASH && window.location.hash !== hashedRoute) {
//mies.updateHash(hashedRoute);
}
// If the action is anything other than a `mousemove`, fetch and pass some
// useful target and event data, such as a bound form.
// (`mousemove` is unlikely to be the active interaction for a form change,
// and as such the expense of seeking unnecessary form references on
// invocations with potential microsecond periodicity is too great. Note
// handlers are called within the scope of the $target, so the handler
// is free to replicate the given seek).
//
if(type !== "mousemove") {
if(readfrom && (form = jQuery("#" + readfrom)).length) {}
else if(!(form = (parent = $target.parent()).find("form")).length) {
form = parent.parent().find("form");
}
if(form.length) {
form.find('input[type="text"]').each(function() {
var $t = jQuery(this);
$t.val(mies.trim($t.val()));
});
pass.$form = form;
pass.formData = form.length ? form.serialize() : null;
}
}
mies.route.call($target, route, type, event, pass);
});
});
return mies;
},
unbindUI : function() {
jQuery(document.body).off(BOUND_UI_EVENTS, ACTION_SELECTION);
return mies;
},
//////////////////////////////////////////////////////////////////////////////////////
// //
// Communication layer setup //
// //
//////////////////////////////////////////////////////////////////////////////////////
// ##join
//
join : function(groupId, sessId, transport, callback, remoteUrl) {
SESSION_ID = sessId || SESSION_ID;
if(typeof transport === "function") {
remoteUrl = callback;
callback = transport;
transport = null;
}
REMOTE_URL = remoteUrl || "";
IS_SOCK = transport === "socket";
$.getScript(REMOTE_URL + [
"/js/eventsource.js",
"/socket.io/socket.io.js"
][IS_SOCK ? 1 : 0], function(scr) {
var onMessage = function(data) {
var id = data.id;
if(CALLS[id]) {
return mies.routeBroadcast(id, data);
}
};
if(IS_SOCK) {
var socket = io.connect(REMOTE_URL + '/?sessId=' + SESSION_ID + '&groupId=' + groupId);
// After connection, socket emits a handshake message, which may
// contain additional info for the client. When this is received the
// #join chain is complete.
//
socket.on('handshake', function(userdata) {
callback && callback.call(userdata || {}, SESSION_ID, groupId);
});
socket.on('connect', function() {
socket.on('message', onMessage);
});
return mies;
}
var source = new EventSource(REMOTE_URL + '/system/receive/' + groupId + '/' + SESSION_ID);
// Should only fire once
//
source.addEventListener('open', function() {
// All eventsource broadcasts will be to this channel. #lastEventId will be
// an id (as per CALLS), or a route. #data is always sent as a JSON string.
//
source.addEventListener('message', function(msg) {
onMessage({
data : JSON.parse(msg.data).data,
id : msg.lastEventId
});
}, false);
callback && callback(SESSION_ID, groupId);
}, false);
});
return mies;
},
joinRemote : function(url, groupId, sessId, transport, callback) {
return mies.join(groupId, sessId, transport, callback, url);
},
// ##loadModule
//
// @param {Object} params A map of properties, in form:
// #name{String} The module name
// [#auth]{String} "true" or "false"
// [#$target]{jQueryObj} Any module html -> $target.html(...)
// [#callback]{Function} On loaded is passed [$element]
//
loadModule : function(params) {
var name = params.name;
var auth = params.auth || "";
var cb = params.callback;
var $targ = params.$target;
// Load once, avoiding future loads. To reload, $this.addClass("module").
// Add identifying selector, mainly for css.
//
$targ && $targ
.removeClass("module")
.addClass("module-" + name);
jQuery.getJSON("/module/" + name + "/" + auth, function(data) {
data.css && jQuery("<style type=\"text/css\">" + data.css + "</style>").appendTo(document.head);
data.html && $targ && $targ.html(data.html);
data.js && jQuery.globalEval(data.js);
mies.route("/module-" + name, "load", $targ);
cb && cb($targ);
});
},
loadModules : function(cb) {
var $mods = jQuery(".module[name]");
var len = $mods.length;
if(!len) {
return cb();
}
$mods.each(function() {
var $this = jQuery(this);
mies.loadModule({
name : $this.attr("name"),
auth : $this.attr("data-auth") || "",
$target : $this,
callback : function() {
if(--len < 1) {
mies.route("/modules", "load", $mods);
cb && cb($this);
}
}
});
});
return mies;
}
};
// Add the #subscribe block handlers (all ui events [click, mousedown, etc] + internals)
//
mies.each(BOUND_UI_EVENTS.split(" ").concat("action","broadcast","error","always","login"), function(e) {
mies.addRouteEvent(e);
})
// When the document is ready, bind
//
jQuery(function() {
mies
.set("timezoneOffset", new Date().getTimezoneOffset() /60)
.loadModules(mies.bindUI);
});
(typeof exports === 'object' ? exports : window)["mies"] = mies;
})();