data-binding
Version:
Data attribute binding and interpolation
1,055 lines (866 loc) • 22.1 kB
JavaScript
;(function(){
/**
* Require the given path.
*
* @param {String} path
* @return {Object} exports
* @api public
*/
function require(path, parent, orig) {
var resolved = require.resolve(path);
// lookup failed
if (null == resolved) {
orig = orig || path;
parent = parent || 'root';
var err = new Error('Failed to require "' + orig + '" from "' + parent + '"');
err.path = orig;
err.parent = parent;
err.require = true;
throw err;
}
var module = require.modules[resolved];
// perform real require()
// by invoking the module's
// registered function
if (!module._resolving && !module.exports) {
var mod = {};
mod.exports = {};
mod.client = mod.component = true;
module._resolving = true;
module.call(this, mod.exports, require.relative(resolved), mod);
delete module._resolving;
module.exports = mod.exports;
}
return module.exports;
}
/**
* Registered modules.
*/
require.modules = {};
/**
* Registered aliases.
*/
require.aliases = {};
/**
* Resolve `path`.
*
* Lookup:
*
* - PATH/index.js
* - PATH.js
* - PATH
*
* @param {String} path
* @return {String} path or null
* @api private
*/
require.resolve = function(path) {
if (path.charAt(0) === '/') path = path.slice(1);
var paths = [
path,
path + '.js',
path + '.json',
path + '/index.js',
path + '/index.json'
];
for (var i = 0; i < paths.length; i++) {
var path = paths[i];
if (require.modules.hasOwnProperty(path)) return path;
if (require.aliases.hasOwnProperty(path)) return require.aliases[path];
}
};
/**
* Normalize `path` relative to the current path.
*
* @param {String} curr
* @param {String} path
* @return {String}
* @api private
*/
require.normalize = function(curr, path) {
var segs = [];
if ('.' != path.charAt(0)) return path;
curr = curr.split('/');
path = path.split('/');
for (var i = 0; i < path.length; ++i) {
if ('..' == path[i]) {
curr.pop();
} else if ('.' != path[i] && '' != path[i]) {
segs.push(path[i]);
}
}
return curr.concat(segs).join('/');
};
/**
* Register module at `path` with callback `definition`.
*
* @param {String} path
* @param {Function} definition
* @api private
*/
require.register = function(path, definition) {
require.modules[path] = definition;
};
/**
* Alias a module definition.
*
* @param {String} from
* @param {String} to
* @api private
*/
require.alias = function(from, to) {
if (!require.modules.hasOwnProperty(from)) {
throw new Error('Failed to alias "' + from + '", it does not exist');
}
require.aliases[to] = from;
};
/**
* Return a require function relative to the `parent` path.
*
* @param {String} parent
* @return {Function}
* @api private
*/
require.relative = function(parent) {
var p = require.normalize(parent, '..');
/**
* lastIndexOf helper.
*/
function lastIndexOf(arr, obj) {
var i = arr.length;
while (i--) {
if (arr[i] === obj) return i;
}
return -1;
}
/**
* The relative require() itself.
*/
function localRequire(path) {
var resolved = localRequire.resolve(path);
return require(resolved, parent, path);
}
/**
* Resolve relative to the parent.
*/
localRequire.resolve = function(path) {
var c = path.charAt(0);
if ('/' == c) return path.slice(1);
if ('.' == c) return require.normalize(p, path);
// resolve deps by returning
// the dep in the nearest "deps"
// directory
var segs = parent.split('/');
var i = lastIndexOf(segs, 'deps') + 1;
if (!i) i = 0;
path = segs.slice(0, i + 1).join('/') + '/deps/' + path;
return path;
};
/**
* Check if module is defined at `path`.
*/
localRequire.exists = function(path) {
return require.modules.hasOwnProperty(localRequire.resolve(path));
};
return localRequire;
};
require.register("bredele-trim/index.js", function(exports, require, module){
/**
* Expose 'trim'
* @param {String} str
* @api public
*/
module.exports = function(str){
if(str.trim) return str.trim();
return str.replace(/^\s*|\s*$/g, '');
};
});
require.register("bredele-supplant/index.js", function(exports, require, module){
var indexOf = require('indexof'),
trim = require('trim'),
props = require('./lib/props');
var cache = {};
function scope(statement){
var result = props(statement, 'model.');
return new Function('model', 'return ' + result);
};
/**
* Variable substitution on the string.
*
* @param {String} str
* @param {Object} model
* @return {String} interpolation's result
*/
module.exports = function(text, model){
//TODO: cache the function the entire text or just the expression?
return text.replace(/\{([^}]+)\}/g, function(_, expr) {
if(/[.'[+(]/.test(expr)) {
var fn = cache[expr] = cache[expr] || scope(expr);
return fn(model) || '';
}
return model[trim(expr)] || '';
});
};
module.exports.attrs = function(text) {
var exprs = [];
text.replace(/\{([^}]+)\}/g, function(_, expr){
var val = trim(expr);
if(!~indexOf(exprs, val)) exprs.push(val);
});
return exprs;
};
});
require.register("bredele-supplant/lib/props.js", function(exports, require, module){
var indexOf = require('indexof');
/**
* Global Names
*/
var globals = /\b(Array|Date|Object|Math|JSON)\b/g;
/**
* Return immediate identifiers parsed from `str`.
*
* @param {String} str
* @param {String|Function} map function or prefix
* @return {Array}
* @api public
*/
module.exports = function(str, fn){
var p = unique(props(str));
if (fn && 'string' == typeof fn) fn = prefixed(fn);
if (fn) return map(str, p, fn);
return p;
};
/**
* Return immediate identifiers in `str`.
*
* @param {String} str
* @return {Array}
* @api private
*/
function props(str) {
return str
.replace(/\.\w+|\w+ *\(|"[^"]*"|'[^']*'|\/([^/]+)\//g, '')
.replace(globals, '')
.match(/[a-zA-Z_]\w*/g)
|| [];
}
/**
* Return `str` with `props` mapped with `fn`.
*
* @param {String} str
* @param {Array} props
* @param {Function} fn
* @return {String}
* @api private
*/
function map(str, props, fn) {
var re = /\.\w+|\w+ *\(|"[^"]*"|'[^']*'|\/([^/]+)\/|[a-zA-Z_]\w*/g;
return str.replace(re, function(_){
if ('(' == _[_.length - 1]) return fn(_);
if (!~indexOf(props, _)) return _;
return fn(_);
});
}
/**
* Return unique array.
*
* @param {Array} arr
* @return {Array}
* @api private
*/
function unique(arr) {
var ret = [];
for (var i = 0; i < arr.length; i++) {
if (~indexOf(ret, arr[i])) continue;
ret.push(arr[i]);
}
return ret;
}
/**
* Map with prefix `str`.
*/
function prefixed(str) {
return function(_){
return str + _;
};
}
});
require.register("bredele-plugin-parser/index.js", function(exports, require, module){
/**
* Plugin constructor.
* @api public
*/
module.exports = function(str) {
str = str.replace(/ /g,'');
var phrases = str ? str.split(';') : ['main'];
//var phrases = str.replace(/ /g,'').split(';') || ['main'];
var results = [];
for(var i = 0, l = phrases.length; i < l; i++) {
var expr = phrases[i].split(':');
var params = [];
var name = expr[0];
if(expr[1]) {
params = expr[1].split(',');
} else {
name = 'main';
}
results.push({
method: expr[0],
params: params
});
}
return results;
};
});
require.register("component-emitter/index.js", function(exports, require, module){
/**
* Expose `Emitter`.
*/
module.exports = Emitter;
/**
* Initialize a new `Emitter`.
*
* @api public
*/
function Emitter(obj) {
if (obj) return mixin(obj);
};
/**
* Mixin the emitter properties.
*
* @param {Object} obj
* @return {Object}
* @api private
*/
function mixin(obj) {
for (var key in Emitter.prototype) {
obj[key] = Emitter.prototype[key];
}
return obj;
}
/**
* Listen on the given `event` with `fn`.
*
* @param {String} event
* @param {Function} fn
* @return {Emitter}
* @api public
*/
Emitter.prototype.on =
Emitter.prototype.addEventListener = function(event, fn){
this._callbacks = this._callbacks || {};
(this._callbacks[event] = this._callbacks[event] || [])
.push(fn);
return this;
};
/**
* Adds an `event` listener that will be invoked a single
* time then automatically removed.
*
* @param {String} event
* @param {Function} fn
* @return {Emitter}
* @api public
*/
Emitter.prototype.once = function(event, fn){
var self = this;
this._callbacks = this._callbacks || {};
function on() {
self.off(event, on);
fn.apply(this, arguments);
}
on.fn = fn;
this.on(event, on);
return this;
};
/**
* Remove the given callback for `event` or all
* registered callbacks.
*
* @param {String} event
* @param {Function} fn
* @return {Emitter}
* @api public
*/
Emitter.prototype.off =
Emitter.prototype.removeListener =
Emitter.prototype.removeAllListeners =
Emitter.prototype.removeEventListener = function(event, fn){
this._callbacks = this._callbacks || {};
// all
if (0 == arguments.length) {
this._callbacks = {};
return this;
}
// specific event
var callbacks = this._callbacks[event];
if (!callbacks) return this;
// remove all handlers
if (1 == arguments.length) {
delete this._callbacks[event];
return this;
}
// remove specific handler
var cb;
for (var i = 0; i < callbacks.length; i++) {
cb = callbacks[i];
if (cb === fn || cb.fn === fn) {
callbacks.splice(i, 1);
break;
}
}
return this;
};
/**
* Emit `event` with the given args.
*
* @param {String} event
* @param {Mixed} ...
* @return {Emitter}
*/
Emitter.prototype.emit = function(event){
this._callbacks = this._callbacks || {};
var args = [].slice.call(arguments, 1)
, callbacks = this._callbacks[event];
if (callbacks) {
callbacks = callbacks.slice(0);
for (var i = 0, len = callbacks.length; i < len; ++i) {
callbacks[i].apply(this, args);
}
}
return this;
};
/**
* Return array of callbacks for `event`.
*
* @param {String} event
* @return {Array}
* @api public
*/
Emitter.prototype.listeners = function(event){
this._callbacks = this._callbacks || {};
return this._callbacks[event] || [];
};
/**
* Check if this emitter has `event` handlers.
*
* @param {String} event
* @return {Boolean}
* @api public
*/
Emitter.prototype.hasListeners = function(event){
return !! this.listeners(event).length;
};
});
require.register("bredele-each/index.js", function(exports, require, module){
/**
* Expose 'each'
*/
module.exports = function(obj, fn, scope){
if( obj instanceof Array) {
array(obj, fn, scope);
} else if(typeof obj === 'object') {
object(obj, fn, scope);
}
};
/**
* Object iteration.
* @param {Object} obj
* @param {Function} fn
* @param {Object} scope
* @api private
*/
function object(obj, fn, scope) {
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
fn.call(scope, i, obj[i]);
}
}
}
/**
* Array iteration.
* @param {Array} obj
* @param {Function} fn
* @param {Object} scope
* @api private
*/
function array(obj, fn, scope){
for(var i = 0, l = obj.length; i < l; i++){
fn.call(scope, i, obj[i]);
}
}
});
require.register("bredele-clone/index.js", function(exports, require, module){
/**
* Expose 'clone'
* @param {Object} obj
* @api public
*/
module.exports = function(obj) {
if(obj instanceof Array) {
return obj.slice(0);
}
return clone(obj);
};
/**
* Clone object.
* @param {Object} obj
* @api private
*/
function clone(obj){
if(typeof obj === 'object') {
var copy = {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = clone(obj[key]);
}
}
return copy;
}
return obj;
}
});
require.register("bredele-store/index.js", function(exports, require, module){
var Emitter = require('emitter'),
clone = require('clone'),
each = require('each'),
storage = window.localStorage;
/**
* Expose 'Store'
*/
module.exports = Store;
/**
* Store constructor
* @api public
*/
function Store(data) {
if(data instanceof Store) return data;
this.data = data || {};
this.formatters = {};
}
Emitter(Store.prototype);
/**
* Set store attribute.
* @param {String} name
* @param {Everything} value
* @api public
*/
Store.prototype.set = function(name, value, plugin) { //add object options
var prev = this.data[name];
if(prev !== value) {
this.data[name] = value;
this.emit('change', name, value, prev);
this.emit('change ' + name, value, prev);
}
};
/**
* Get store attribute.
* @param {String} name
* @return {Everything}
* @api public
*/
Store.prototype.get = function(name) {
var formatter = this.formatters[name];
var value = this.data[name];
if(formatter) {
value = formatter[0].call(formatter[1], value);
}
return value;
};
/**
* Get store attribute.
* @param {String} name
* @return {Everything}
* @api private
*/
Store.prototype.has = function(name) {
//NOTE: I don't know if it should be public
return this.data.hasOwnProperty(name);
};
/**
* Delete store attribute.
* @param {String} name
* @return {Everything}
* @api public
*/
Store.prototype.del = function(name) {
//TODO:refactor this is ugly
if(this.has(name)){
if(this.data instanceof Array){
this.data.splice(name, 1);
} else {
delete this.data[name]; //NOTE: do we need to return something?
}
this.emit('deleted', name, name);
this.emit('deleted ' + name, name);
}
};
/**
* Set format middleware.
* Call formatter everytime a getter is called.
* A formatter should always return a value.
* @param {String} name
* @param {Function} callback
* @param {Object} scope
* @return this
* @api public
*/
Store.prototype.format = function(name, callback, scope) {
this.formatters[name] = [callback,scope];
return this;
};
/**
* Compute store attributes
* @param {String} name
* @return {Function} callback
* @api public
*/
Store.prototype.compute = function(name, callback) {
//NOTE: I want something clean instaead passing the computed
//attribute in the function
var str = callback.toString();
var attrs = str.match(/this.[a-zA-Z0-9]*/g);
this.set(name, callback.call(this.data)); //TODO: refactor (may be use replace)
for(var l = attrs.length; l--;){
this.on('change ' + attrs[l].slice(5), function(){
this.set(name, callback.call(this.data));
});
}
};
/**
* Reset store
* @param {Object} data
* @api public
*/
Store.prototype.reset = function(data) {
var copy = clone(this.data),
length = data.length;
this.data = data;
//remove undefined attributes
//TODO: we don't need to go through each items for array (only difference)
each(copy, function(key, val){
if(typeof data[key] === 'undefined'){
this.emit('deleted', key, length);
this.emit('deleted ' + key, length);
}
}, this);
//set new attributes
each(data, function(key, val){
//TODO:refactor with this.set
var prev = copy[key];
if(prev !== val) {
this.emit('change', key, val, prev);
this.emit('change ' + key, val, prev);
}
}, this);
};
/**
* Loop through store data.
* @param {Function} cb
* @param {[type]} scope
* @api public
*/
Store.prototype.loop = function(cb, scope) {
each(this.data, cb, scope || this);
};
/**
* Synchronize with local storage.
*
* @param {String} name
* @param {Boolean} bool save in localstore
* @api public
*/
Store.prototype.local = function(name, bool) {
//TODO: should we do a clear for .local()?
if(!bool) {
storage.setItem(name, this.toJSON());
} else {
this.reset(JSON.parse(storage.getItem(name)));
}
//TODO: should we return this?
};
/**
* Use middlewares to extend store.
* A middleware is a function with the store
* as first argument.
*
* @param {Function} fn
* @return {this}
* @api public
*/
Store.prototype.use = function(fn) {
fn(this);
return this;
};
/**
* Stringify model
* @return {String} json
* @api public
*/
Store.prototype.toJSON = function() {
return JSON.stringify(this.data);
};
//TODO: localstorage middleware like
});
require.register("component-indexof/index.js", function(exports, require, module){
module.exports = function(arr, obj){
if (arr.indexOf) return arr.indexOf(obj);
for (var i = 0; i < arr.length; ++i) {
if (arr[i] === obj) return i;
}
return -1;
};
});
require.register("binding/index.js", function(exports, require, module){
var subs = require('./lib/attr'),
Store = require('store'),
parser = require('plugin-parser');
/**
* Expose 'data binding'
*/
module.exports = Binding;
/**
* Intitialize a binding.
* @param {Object} model
*/
function Binding(model){
if(!(this instanceof Binding)) return new Binding(model);
//TODO: remove store of dependencies
this.model = new Store(model);
this.plugins = {};
}
/**
* Bind object as function.
* @api private
*/
function binder(obj) {
return function(el, expr) {
var formats = parser(expr);
for(var i = 0, l = formats.length; i < l; i++) {
var format = formats[i];
format.params.splice(0, 0, el);
obj[format.method].apply(obj, format.params);
}
};
}
/**
* Add binding by name
* @param {String} name
* @param {Object} plugin
* @api public
*/
Binding.prototype.add = function(name, plugin) {
if(typeof plugin === 'object') plugin = binder(plugin);
this.plugins[name] = plugin;
return this;
};
/**
* Attribute binding.
* @param {HTMLElement} node
* @api private
*/
Binding.prototype.bindAttrs = function(node) {
var attrs = node.attributes;
//reverse loop doesn't work on IE...
for(var i = 0, l = attrs.length; i < l; i++) {
var attr = attrs[i],
plugin = this.plugins[attr.nodeName];
if(plugin) {
plugin.call(this.model, node, attr.nodeValue);
} else {
subs(attr, this.model);
}
}
};
/**
* Apply bindings on a single node
* @param {DomElement} node
* @api private
*/
Binding.prototype.bind = function(node) {
var type = node.nodeType;
//dom element
if (type === 1) return this.bindAttrs(node);
// text node
if (type === 3) subs(node, this.model);
};
/**
* Apply bindings on nested DOM element.
* @param {DomElement} node
* @api public
*/
Binding.prototype.apply = function(node) {
var nodes = node.childNodes;
this.bind(node);
//use each?
for (var i = 0, l = nodes.length; i < l; i++) {
this.apply(nodes[i]);
}
};
});
require.register("binding/lib/attr.js", function(exports, require, module){
var supplant = require('supplant'), //don't use supplant for attributes (remove attrs)
indexOf = require('indexof'),
props = require('supplant/lib/props'); //TODO: make component props or supplant middleware
/**
* Node text substitution constructor.
* @param {HTMLElement} node type 3
* @param {Store} store
*/
module.exports = function(node, store) {
var text = node.nodeValue;
//TODO: it seems faster if index in index.js??
//is it enought to say that's an interpolation?
if(!~ indexOf(text, '{')) return;
var exprs = getProps(text),
handle = function() {
node.nodeValue = supplant(text, store.data);
};
for(var l = exprs.length; l--;) {
//when destroy binding, we should do off store
store.on('change ' + exprs[l], handle);
}
handle();
};
function getProps(text) {
var exprs = [];
//is while and test faster?
text.replace(/\{([^}]+)\}/g, function(_, expr){
if(!~indexOf(exprs, expr)) exprs = exprs.concat(props(expr));
});
return exprs;
}
});
require.alias("bredele-supplant/index.js", "binding/deps/supplant/index.js");
require.alias("bredele-supplant/lib/props.js", "binding/deps/supplant/lib/props.js");
require.alias("bredele-supplant/index.js", "binding/deps/supplant/index.js");
require.alias("bredele-supplant/index.js", "supplant/index.js");
require.alias("component-indexof/index.js", "bredele-supplant/deps/indexof/index.js");
require.alias("bredele-trim/index.js", "bredele-supplant/deps/trim/index.js");
require.alias("bredele-trim/index.js", "bredele-supplant/deps/trim/index.js");
require.alias("bredele-trim/index.js", "bredele-trim/index.js");
require.alias("bredele-supplant/index.js", "bredele-supplant/index.js");
require.alias("bredele-plugin-parser/index.js", "binding/deps/plugin-parser/index.js");
require.alias("bredele-plugin-parser/index.js", "binding/deps/plugin-parser/index.js");
require.alias("bredele-plugin-parser/index.js", "plugin-parser/index.js");
require.alias("bredele-plugin-parser/index.js", "bredele-plugin-parser/index.js");
require.alias("bredele-store/index.js", "binding/deps/store/index.js");
require.alias("bredele-store/index.js", "binding/deps/store/index.js");
require.alias("bredele-store/index.js", "store/index.js");
require.alias("component-emitter/index.js", "bredele-store/deps/emitter/index.js");
require.alias("bredele-each/index.js", "bredele-store/deps/each/index.js");
require.alias("bredele-each/index.js", "bredele-store/deps/each/index.js");
require.alias("bredele-each/index.js", "bredele-each/index.js");
require.alias("bredele-clone/index.js", "bredele-store/deps/clone/index.js");
require.alias("bredele-clone/index.js", "bredele-store/deps/clone/index.js");
require.alias("bredele-clone/index.js", "bredele-clone/index.js");
require.alias("bredele-store/index.js", "bredele-store/index.js");
require.alias("component-indexof/index.js", "binding/deps/indexof/index.js");
require.alias("component-indexof/index.js", "indexof/index.js");
require.alias("binding/index.js", "binding/index.js");if (typeof exports == "object") {
module.exports = require("binding");
} else if (typeof define == "function" && define.amd) {
define(function(){ return require("binding"); });
} else {
this["binding"] = require("binding");
}})();