UNPKG

data-binding

Version:

Data attribute binding and interpolation

1,055 lines (866 loc) 22.1 kB
;(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"); }})();