/* (c) Diego Cardozo - license: https://github.com/diegocard/dry.js/blob/master/LICENSE */
/* (c) Diego Cardozo - license: https://github.com/diegocard/dry.js/blob/master/LICENSE */
dry = {
jQuery-like function
$: function(element) {
return new dry.Dom(element);
},
Apps container
apps: {},
Returns an app or create it if it doesn’t exist (Singleton)
app: function(name, options) {
return this.apps[name] || (this.apps[name] = new dry.App(name, options));
},
Navigate to a certain url
navigate: function(url) {
window.location.href = url || '';
},
Execute a route without navigating
run: function(route) {
var app;
for (app in dry.apps) {
dry.apps[app].router.run(route);
}
},
};
dry.settings = {
When a route is left empty, the router will look for this controller
DEFAULT_CONTROLLER_NAME: "default",
When a route doesn’t target any specific method, it will look for this one
DEFAULT_CONTROLLER_METHOD: "default",
Time in milliseconds after which apending AJAX request is considered unresponsive and is aborted. Useful to deal with bad connectivity (e.g. on a mobile network). A 0 value disables AJAX timeouts.
AJAX_TIMEOUT: 10000
};
Check if the given parameter is an array
dry.isArray = function(arr) {
return Object.prototype.toString.call(arr) === "[object Array]";
};
Check if the given parameter is an object
dry.isObject = function(obj) {
return obj === Object(obj) && !dry.isFunction(obj);
};
Check if the given parameter is strictly an object
dry.isStrictlyObject = function(obj) {
return dry.isObject(obj) && !dry.isArray(obj);
};
Check if the given parameter is boolean
dry.isBoolean = function(bool) {
return bool === true || bool === false;
};
Check if the given parameter is a string
dry.isString = function(str) {
return Object.prototype.toString.call(str) === "[object String]";
};
Check if the given parameter is a function
dry.isFunction = function(fun) {
return Object.prototype.toString.call(fun) === "[object Function]";
};
Check if the given parameter is undefined
dry.isUndefined = function(obj) {
return typeof obj === "undefined";
};
Check if the given parameter is numeric
dry.isNumeric = function(num) {
return !isNaN(parseFloat(num)) && isFinite(num);
};
Returns an array with the property names for the given object
dry.keys = function (obj) {
var keys = [],
key;
for (key in obj) {
if (obj.hasOwnProperty(key)) {
keys.push(key);
}
}
return keys;
};
Concise and efficient forEach implementation
dry.each = function (obj, func, context) {
var i, len, keys;
if (dry.isStrictlyObject(obj)) {
keys = dry.keys(obj);
for (i=0, len=keys.length; i<len; i++) {
func.call(context, obj[keys[i]], keys[i], obj);
}
} else {
for (i=0, len=obj.length; i<len; i++) {
func.call(context, obj[i], i, obj);
}
}
};
JSONP requests
dry.jsonp = function(url, callback) {
var callbackName = 'dry_jsonp_callback_' + Math.round(100000 * Math.random());
window[callbackName] = function(data) {
delete window[callbackName];
document.body.removeChild(script);
callback(data);
};
var script = document.createElement('script');
script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'callback=' + callbackName;
document.body.appendChild(script);
};
Ajax methods
dry.ajax = function (options) {
var method = options.type || 'GET',
data = options.data || {},
headers = options.headers || {},
url = options.url,
timeout = options.timeout || dry.settings.AJAX_TIMEOUT,
p = dry.deferred(),
xhr,
newXhr = function() {
var xhr;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else if (window.ActiveXObject) {
try {
xhr = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
}
return xhr;
},
onTimeout = function() {
xhr.abort();
p.reject(xhr);
},
payload, h, tid;
try {
xhr = newXhr();
} catch (e) {
p.reject("XHR not supported on this browser");
return p.promise;
}
payload = dry.param(data);
if (method === 'GET' && payload) {
url += '?' + payload;
payload = null;
}
xhr.open(method, url);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
dry.each(headers, function (h) {
xhr.setRequestHeader(h, headers[h]);
});
if (timeout) {
tid = setTimeout(onTimeout, timeout);
}
xhr.onreadystatechange = function() {
var err, res;
if (timeout) {
clearTimeout(tid);
}
if (xhr.readyState === 4) {
err = (!xhr.status ||
(xhr.status < 200 || xhr.status >= 300) &&
xhr.status !== 304);
if (err) {
p.reject(xhr);
} else {
try {
/* Try to convert the output to JSON */
res = JSON.parse(this.responseText);
} catch(e) {
/* If it fails, return the output as-is */
res = this.responseText;
}
p.resolve(res);
}
}
};
xhr.send(payload);
return p.promise
.success(options.success)
.error(options.error);
};
Utility ajax method shortcuts for POST, PUT and DELETE requests
dry.each(['GET', 'POST', 'PUT', 'DELETE'], function(method){
dry[method.toLowerCase()] = function(url, data, success, error) {
return dry.ajax({
type: method,
url: url,
data: data,
success: success,
error: error
});
};
});
Send a GET request to retrieve JSON from a given URL
dry.getJSON = function(url, success, error) {
return dry.ajax({
type: 'GET',
url: url,
success: success,
error: error
});
};
Serialize an array of form elements or a set of key/values into a query string
dry.param = function(obj) {
var r20 = /%20/g,
rbracket = /\[\]$/,
rCRLF = /\r?\n/g,
rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
rsubmittable = /^(?:input|select|textarea|keygen)/i,
arr = [],
prefix,
add = function(key, value) {
/* If value is a function, invoke it and return its value */
value = dry.isFunction(value) ? value() : (value == null ? "" : value);
arr[arr.length] = encodeURIComponent(key) + "=" + encodeURIComponent(value);
},
buildParams = function (prefix, obj, add) {
var name;
if (dry.isArray(obj)) {
/* Serialize array item */
dry.each(obj, function(value, index) {
if (rbracket.test(prefix)) {
/* Treat each array item as a scalar */
add(prefix, value);
} else {
/* Item is non-scalar (array or object), encode its numeric index */
buildParams(prefix + "[" + (typeof value === "object" ? index : "") + "]", value, add);
}
});
} else if (dry.isObject(obj)) {
/* Serialize object item */
for (name in obj) {
buildParams(prefix + "[" + name + "]", obj[name], add);
}
} else {
/* Serialize scalar item */
add(prefix, obj);
}
};
/* If an array was passed in, assume that it is an array of form elements */
if (dry.isArray(obj) || (!dry.isStrictlyObject(obj))) {
/* Serialize the form elements */
dry.each(obj, function() {
add(this.name, this.value);
});
} else {
/* Encode params recursively */
for (prefix in obj) {
buildParams(prefix, obj[prefix], add);
}
}
/* Return the resulting serialization */
return arr.join("&").replace(r20, "+");
};
dry.Dom = function(name) {
this.element = document.querySelector(name);
};
jQuery-like html function
dry.Dom.prototype.html = function(content) {
if (content) {
this.element.innerHTML = content;
return this;
} else {
return this.element.innerHTML;
}
};
jQuery-like event handlers
dry.Dom.prototype.on = function(eventName, eventHandler) {
this.element.addEventListener(eventName, eventHandler);
};
dry.Dom.prototype.off = function(eventName, eventHandler) {
this.element.removeEventListener(eventName, eventHandler);
};
dry.Dom.prototype.trigger = function(eventName, data) {
var event;
if (window.CustomEvent) {
event = new CustomEvent(eventName, {
detail: data
});
} else {
event = document.createEvent('CustomEvent');
event.initCustomEvent(eventName, true, true, data);
}
this.element.dispatchEvent(event);
};
dry.App = function(name, options) {
this.name = name;
this.options = options || {};
this.routes = this.options.routes || {};
this.router = new dry.Router(this.name);
this.filters = this.options.filters || {};
this.controllers = this.options.controllers || {};
this.views = this.options.views || {};
this.modelDefinitions = this.options.models || {};
};
dry.App.prototype.controller = function(name, methods) {
var self = this,
controller;
if (dry.isUndefined(methods)) {
controller = this.controllers[name];
} else {
controller = new dry.Controller(name, methods);
this.controllers[name] = controller;
/* Register the controller's routes */
dry.each(methods, function(method, methodName){
self.router.addRoute(controller, methodName);
});
}
return controller;
};
dry.App.prototype.redirect = function(controller, method, params) {
this.controllers[controller].invokeMethod(method, params);
};
dry.App.prototype.filter = function(name, condition, action) {
if (dry.isUndefined(condition) && dry.isUndefined(action)) {
return this.views[name];
} else {
this.filters[name] = new dry.Filter(name, condition, action);
}
};
dry.App.prototype.view = function(name, templateData) {
if (dry.isUndefined(templateData)) {
return this.views[name];
} else {
this.views[name] = new dry.View(name, templateData);
}
};
dry.App.prototype.init = function() {
/* Initialize the app's router */
this.router.init();
};
dry.App.prototype.model = function(name, params) {
if (!params) {
return new dry.Model(name, this.modelDefinitions[name]);
} else {
/* Store the model definition if needed */
this.modelDefinitions[name] = params;
}
};
dry.Router = function(appName, routes) {
this.rules = {};
this.routes = [];
};
Register a route, composed by controller and method
dry.Router.prototype.addRoute = function (controller, methodName) {
var firstPart = (controller.name === dry.settings.DEFAULT_CONTROLLER_NAME ? '' : controller.name),
secondPart = (methodName === dry.settings.DEFAULT_CONTROLLER_NAME ? '' : methodName),
route = secondPart ? firstPart + '/' + secondPart : firstPart,
Callback to be executed on route navigation
routeCallback = function(controller, methodName) {
return function(route) {
/* Call the appropriate controller method */
controller.invokeMethod(methodName, route.params);
};
};
this.routes.push(route);
this.register(route, routeCallback(controller, methodName));
};
dry.Router.prototype.register = function(route, handler) {
var pieces = route.split('/'),
rules = this.rules;
dry.each(pieces, function (piece) {
var name = piece.length && piece.charAt(0) == ':' ? ':' : piece;
if (!rules[name]) {
rules = (rules[name] = {});
if (name == ':') {
rules['@name'] = piece.slice(1);
}
} else {
rules = rules[name];
}
});
rules['@'] = handler;
};
dry.Router.prototype.run = function(url) {
if (url && url.length) {
url = url.replace('/?', '?');
url.charAt(0) == '/' && (url = url.slice(1));
url.length && url.slice(-1) == '/' && (url = url.slice(0, -1));
}
var rules = this.rules,
querySplit = url.split('?', 2),
pieces = querySplit[0].split('/', 50),
params = {};
(function parseUrl() {
dry.each(pieces, function (piece) {
var rule = rules[piece.toLowerCase()];
if (!rule && (rule = rules[':'])) {
params[rule['@name']] = piece;
}
rules = rule;
});
})();
(function parseQuery(q) {
var query = q.split('&', 50);
dry.each(query, function (component) {
var nameValue = component.split('=', 2);
nameValue.length == 2 && (params[nameValue[0]] = nameValue[1]);
});
})(querySplit.length == 2 ? querySplit[1] : '');
if (rules && rules['@']) {
rules['@']({
url: url,
params: params
});
return true;
}
return false;
};
dry.Router.prototype.init = function() {
/* Hash-based routing */
var self = this,
processHash = function() {
var hash = location.hash || '#';
self.run(hash.substr(1));
};
window.addEventListener('hashchange', processHash);
processHash();
};
dry.Model = function(name, params) {
params = params || {};
this.name = name;
this.attributes = params.attributes || {};
this.validations = [];
var self = this;
/* Generate model methods for each given endpoint */
dry.each(params, function(value, property){
var split, httpMethod, url;
if (dry.isString(value)){
split = value.split(' ');
httpMethod = split[0];
url = split[1];
self[property] = self.endpointMethod(httpMethod, url);
}
});
};
Generate a model method which will perform an ajax request for a given endpoint
dry.Model.prototype.endpointMethod = function(httpMethod, url) {
return function(params, success, error) {
var urlAfterReplacement = url,
attributes = this.attributes,
attribute, param;
/* Replace attributes in URL */
for (attribute in attributes) {
urlAfterReplacement = urlAfterReplacement.replace('{' + attribute + '}', attributes[attribute]);
}
/* Replace params in URL */
for (param in params) {
urlAfterReplacement = urlAfterReplacement.replace('{' + param + '}', params[param]);
}
return dry.ajax({
url: urlAfterReplacement,
type: httpMethod,
data: params,
success: success,
error: error
});
};
};
Set an attribute/s for the given model
dry.Model.prototype.set = function(name, value) {
var attrs = dry.isStrictlyObject(name) ? name : {name: value},
self = this;
dry.each(attrs, function (val, attr) {
self.attributes[attr] = val;
});
return this;
};
Get an attribute of the given model TODO: test, doc
dry.Model.prototype.get = function(name) {
return this.attributes[name];
};
Register a validation function for an attribute TODO: test, doc
dry.Model.prototype.validate = function(attr, func) {
this.validations[attr].push(func);
return this;
};
Check if the entire model or one of its attributes is valid TODO: test, doc
dry.Model.prototype.isValid = function(attr) {
var attributesToCheck = [attr] || dry.keys(this.attributes),
i = 0, /* Attribute iterator */
j = 0, /* Attribute validation iterator */
attributeCount = attributesToCheck.length,
isValid = true,
validationCount,
currentAttrValidations;
while (isValid && i<attributeCount) {
j = 0;
currentAttrValidations = this.validations[attributesToCheck[i]];
validationCount = currentAttrValidations.length;
while (isValid && j<validationCount) {
isValid = currentAttrValidations[j].apply(this);
j++;
}
i++;
}
/* If an invalid attribute is found, return which one */
return isValid || i;
};
dry.Filter = function(name, condition, action) {
this.name = name;
this.condition = condition;
this.action = action;
};
Evaluate the condition. If it is true, then run the action.
dry.Filter.prototype.runFilter = function() {
var result = this.condition();
if (result && this.action) {
this.action();
}
return result;
};
dry.Controller = function(name, methods) {
this.name = name;
this.methods = methods || {};
};
dry.Controller.prototype.invokeMethod = function(methodName, params) {
var self = this,
method = this.methods[methodName],
result, defaultView, defaultViewName;
if (method) {
/* Invoke the method with the given parameters */
result = method.call(this, params);
} else {
/* Default behavior: check for a default template and render it.
* The default template should be called controllerName/methodName or
* simply controllerName if methodName is the default one.
*/
defaultViewName = this.name + '/' + methodName;
result = new dry.View(defaultViewName, {templateData: params, controller: this});
}
if (result) {
/* Treat all returned values as promises */
dry.Promise.promisify(result).then(function (res) {
/* If a view was returned, render it */
if (res.constructor === dry.View) {
/* If the controller's method execution resulted in the creation of a view,
* store this information in the view itself.
*/
res.controller = res.controller || self;
res.render();
}
});
}
};
dry.Controller.prototype.redirect = function(methodName, params) {
this.invokeMethod(methodName, params);
};
dry.Template = function(name, tmpl) {
this.name = name;
if (dry.isFunction(tmpl)) {
Allow for pre-compiled funcions
this.compile = tmpl;
} else {
this.templateId = tmpl || ('script[data-dry="' + name + '"]');
}
};
dry.Template.prototype.cache = [];
dry.Template.prototype.compile = function compile(model) {
/* Figure out if we're getting a template, or if we need to
* load the template - and be sure to cache the result.
*/
var str = this.cache[this.name] || dry.$(this.templateId).element.innerHTML,
fn = new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments);};" +
/* Introduce the data as local variables using with(){} */
"with(obj){p.push('" +
/* Convert the template into pure JavaScript */
str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'") + "');}return p.join('');");
/* Provide some basic currying to the user */
return fn(model.attributes);
};
dry.View = function(name, options) {
options = options || {};
this.name = name;
this.el = options.el || (':not(script)[data-dry*="' + name + '"]');
this.model = options.model || new dry.Model(name);
this.template = new dry.Template(name, options.template);
this.controller = options.controller;
this.events = options.events || {};
};
dry.View.prototype.render = function() {
var viewElement = dry.$(this.el),
compiledTemplate = this.template.compile(this.model),
events = this.events,
self = this;
viewElement.html(compiledTemplate);
dry.each(events, function(action, key){
self.addEvent(key, action);
});
};
dry.View.prototype.addEvent = function(eventKey, eventAction) {
var eventKeySplit, eventElement, eventTrigger;
eventKeySplit = eventKey.split(' ');
eventElement = eventKeySplit[1];
eventTrigger = eventKeySplit[0];
if (dry.isString(eventAction)) {
TODO: Finish, test
eventAction = function(params) {
this.controller.invokeMethod(eventAction, params);
};
}
dry.$(eventElement).on(eventTrigger, eventAction);
};
/* (c) Jonathan Gotti - licence: https://github.com/malko/d.js/LICENCE.txt @version 0.7.2*/
!function(a){"use strict";function b(a){l(function(){throw a})}function c(b){return this.then(b,a)}function d(b){return this.then(a,b)}function e(b,c){return this.then(function(a){return m(b)?b.apply(null,n(a)?a:[a]):v.onlyFuncs?a:b},c||a)}function f(a){function b(){a()}return this.then(b,b),this}function g(a){return this.then(function(b){return m(a)?a.apply(null,n(b)?b.splice(0,0,void 0)&&b:[void 0,b]):v.onlyFuncs?b:a},function(b){return a(b)})}function h(c){return this.then(a,c?function(a){throw c(a),a}:b)}function i(a,b){var c=q(a);if(1===c.length&&n(c[0])){if(!c[0].length)return v.fulfilled([]);c=c[0]}var d=[],e=v(),f=c.length;if(f)for(var g=function(a){c[a]=v.promisify(c[a]),c[a].then(function(g){d[a]=b?c[a]:g,--f||e.resolve(d)},function(g){b?(d[a]=c[a],--f||e.resolve(d)):e.reject(g)})},h=0,i=f;i>h;h++)g(h);else e.resolve(d);return e.promise}function j(a,b){return a.then(m(b)?b:function(){return b})}function k(a){var b=q(a);1===b.length&&n(b[0])&&(b=b[0]);for(var c=v(),d=0,e=b.length,f=v.resolved();e>d;d++)f=j(f,b[d]);return c.resolve(f),c.promise}var l,m=function(a){return"function"==typeof a},n=function(a){return Array.isArray?Array.isArray(a):a instanceof Array},o=function(a){return!(!a||!(typeof a).match(/function|object/))},p=function(b){return b===!1||b===a||null===b},q=function(a,b){return[].slice.call(a,b)},r="undefined",s=typeof TypeError===r?Error:TypeError;if(typeof process!==r&&process.nextTick)l=process.nextTick;else if(typeof MessageChannel!==r){var t=new MessageChannel,u=[];t.port1.onmessage=function(){u.length&&u.shift()()},l=function(a){u.push(a),t.port2.postMessage(0)}}else l=function(a){setTimeout(a,0)};var v=function(b){function i(){if(0!==r){var a,b=t,c=0,d=b.length,e=~r?0:1;for(t=[];d>c;c++)(a=b[c][e])&&a(n)}}function j(a){function b(a){return function(b){return c?void 0:(c=!0,a(b))}}var c=!1;if(r)return this;try{var d=o(a)&&a.then;if(m(d)){if(a===u)throw new s("Promise can't resolve itself");return d.call(a,b(j),b(k)),this}}catch(e){return b(k)(e),this}return q(function(){n=a,r=1,i()}),this}function k(a){return r||q(function(){try{throw a}catch(b){n=b}r=-1,i()}),this}var n,q=(a!==b?b:v.alwaysAsync)?l:function(a){a()},r=0,t=[],u={then:function(a,b){var c=v();return t.push([function(b){try{p(a)?c.resolve(b):c.resolve(m(a)?a(b):v.onlyFuncs?b:a)}catch(d){c.reject(d)}},function(a){if((p(b)||!m(b)&&v.onlyFuncs)&&c.reject(a),b)try{c.resolve(m(b)?b(a):b)}catch(d){c.reject(d)}}]),0!==r&&q(i),c.promise},success:c,error:d,otherwise:d,apply:e,spread:e,ensure:f,nodify:g,rethrow:h,isPending:function(){return 0===r},getStatus:function(){return r}};return u.toSource=u.toString=u.valueOf=function(){return n===a?this:n},{promise:u,resolve:j,fulfill:j,reject:k}};if(v.deferred=v.defer=v,v.nextTick=l,v.alwaysAsync=!0,v.onlyFuncs=!0,v.resolved=v.fulfilled=function(a){return v(!0).resolve(a).promise},v.rejected=function(a){return v(!0).reject(a).promise},v.wait=function(a){var b=v();return setTimeout(b.resolve,a||0),b.promise},v.delay=function(a,b){var c=v();return setTimeout(function(){try{c.resolve(m(a)?a.apply(null):a)}catch(b){c.reject(b)}},b||0),c.promise},v.promisify=function(a){return a&&m(a.then)?a:v.resolved(a)},v.all=function(){return i(arguments,!1)},v.resolveAll=function(){return i(arguments,!0)},v.sequence=function(){return k(arguments)},v.nodeCapsule=function(a,b){return b||(b=a,a=void 0),function(){var c=v(),d=q(arguments);d.push(function(a,b){a?c.reject(a):c.resolve(arguments.length>2?q(arguments,1):b)});try{b.apply(a,d)}catch(e){c.reject(e)}return c.promise}},"function"==typeof define&&define.amd)define("D.js",[],function(){return v});else if(typeof window!==r){var w=window.D;v.noConflict=function(){return window.D=w,v},window.D=v}else typeof module!==r&&module.exports&&(module.exports=v)}();
Constructor
dry.Promise = D;
Deferred method (from promises)
dry.deferred = function() {
return D();
};