jskick
Version:
Short syntax html data binding + templating solution using javascript, a variation of rivetsjs and tinybind.
1,729 lines (1,458 loc) • 61.7 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.kick = factory());
}(this, (function () { 'use strict';
var OPTIONS = [//'prefix',
'templateDelimiters', 'rootInterface', 'preloadData', 'handler'];
var COMPS = 'components';
var EXTENSIONS = ['binders', 'formatters', COMPS, 'adapters'];
var PRIMITIVE = 0;
var KEYPATH = 1;
var TEXT = 0;
var BINDING = 1;
var QUOTED_STR = /^'.*'$|^".*"$/;
var WHITESPACES = ' \n\r\t'.split(''); // Parser and tokenizer for getting the type and value from a string.
function parseType(string) {
var type = PRIMITIVE;
var value = string;
if (QUOTED_STR.test(string)) {
value = string.slice(1, -1);
} else if (string === "true") {
value = true;
} else if (string === "false") {
value = false;
} else if (string === "null") {
value = null;
} else if (string === "undefined") {
value = undefined;
} else if (!isNaN(string)) {
value = Number(string);
} else {
type = KEYPATH;
}
return {
type: type,
value: value
};
} // Template parser and tokenizer for mustache-style text content bindings.
// Parses the template and returns a set of tokens, separating static portions
// of text from binding declarations.
function parseTemplate(template, delimiters) {
var tokens;
var length = template.length;
var index = 0;
var lastIndex = 0;
var open = delimiters[0],
close = delimiters[1];
while (lastIndex < length) {
index = template.indexOf(open, lastIndex);
if (index < 0) {
if (tokens) {
tokens.push({
type: TEXT,
value: template.slice(lastIndex)
});
}
break;
} else {
tokens || (tokens = []);
if (index > 0 && lastIndex < index) {
tokens.push({
type: TEXT,
value: template.slice(lastIndex, index)
});
}
lastIndex = index + open.length;
index = template.indexOf(close, lastIndex);
if (index < 0) {
var substring = template.slice(lastIndex - close.length);
var lastToken = tokens[tokens.length - 1];
if (lastToken && lastToken.type === TEXT) {
lastToken.value += substring;
} else {
tokens.push({
type: TEXT,
value: substring
});
}
break;
}
var value = template.slice(lastIndex, index).trim();
tokens.push({
type: BINDING,
value: value
});
lastIndex = index + close.length;
}
}
return tokens;
}
function parseFnExpr(expr) {
function jsNested(statement) {
var regex = new RegExp("([a-zA-Z0-9_$]+)\\((.*)\\)", "g");
var root = {
_: []
};
if (QUOTED_STR.test(statement)) {
root._.push({
k: statement,
t: "l"
});
return root;
}
var r = regex.exec(statement);
if (!r || r.length < 3) {
return root;
}
var parameters = args(r[2]);
var node = {
_: []
};
parameters.forEach(function (p) {
if (p.e) {
if (p.e.indexOf("(") == -1) {
node._.push({
k: p.e,
t: "p"
});
} else {
var wrappedNode = jsNested(p.e),
k;
for (k in wrappedNode) {
node._.push(wrappedNode[k][0]);
}
}
}
}); // Assign node to the node's identifier
root._.push({
k: r[1],
t: 'f',
_: node._
}); //root._.push(r[1]:node);
return root;
}
function args(statement) {
statement += ","; // so I don't have to handle the "last, leftover parameter"
var results = [];
var chars = statement.split("");
var level = 0; // levels of parenthesis, used to track how deep I am in ().
var index = 0; // determines which parameter am I currently on.
var temp = "",
match = '',
temp2 = '';
chars.forEach(function (char) {
switch (true) {
case char === "'":
case char === '"':
if (match.length && match === char) {
temp += match + temp2 + match;
results[index] = {
s: temp2
};
match = temp2 = ''; //level--;
//temp = '';
index++;
} else {
match = char; //level++;
} //level++;
break;
case !match.length && WHITESPACES.indexOf(char) !== -1:
break;
case !match.length && char === '(':
temp += char;
level++;
break;
case !match.length && char === ')':
temp += char;
level--;
break;
case !match.length && char === ',':
// if the comma is between a set of parenthesis, ignore.
if (level !== 0) {
temp += char;
} // if the comma is external, split the string.
else {
results[index] = {
e: temp
};
temp = '';
index++;
}
break;
default:
if (match.length) {
temp2 += char;
} else {
temp += char;
}
break;
}
});
return results;
} //return if we were able to parse functions otherwise it will be null
return jsNested(expr)._[0] || null;
} // Someone bored at http://jsfiddle.net/ryanwheale/e8aaa8ny/3/
var kick = {
// Global binders.
binders: {},
// Global formatters.
formatters: {},
// Global components.
components: {},
// Global sightglass adapters.
adapters: {},
/*
// Default attribute prefix.
_prefix: 'rv',
_fullPrefix: 'rv-',
/* get prefix () {
return this._prefix
},
set prefix (value) {
this._prefix = value
this._fullPrefix = value + '-'
}, */
parseTemplate: parseTemplate,
parseType: parseType,
// Default template delimiters.
templateDelimiters: ['{{', '}}'],
// Binder shortcuts - extend it if you want to have your own
// else if(nodeName === '?') {nodeName = 'if';}
binderMap: {
'^': '^click',
'^^': '^dblclick',
'^_': '^contextmenu',
'^@': '^change',
'^+': '^focus',
'^-': '^blur',
'@': '@value',
'@x': '@checked',
'@-x': '@unchecked',
':': ':text',
'::': ':html',
'$': ':html',
'+': ':show',
'-': ':hide',
'~': ':disabled',
'~~': ':enabled',
'-~': ':enabled'
},
// was starBinder in rivetsjs, in kickjs *(star) is used for foreach binding so changing it to varBinder as an variable binder
varBinderMap: {
'.&': ':class-&',
'-.&': '-:class-&',
'..&': ':style-&',
'-..&': '-:style-&',
':&': ':attr-&',
'-:&': '-:attr-&'
},
// Default sightglass root interface.
rootInterface: '.',
// Preload data by default.
preloadData: true,
// Default event handler.
handler: function handler(context, ev, binding) {
// changing the order of returns as well as passed arguments first then $event
// todo document this breakig change
var processedArgs = binding.parseFormatterArguments(binding.fnArgs, 0, ev, binding.view.models);
var fns = this.name.split(' ');
var fn = fns[fns.length - 1];
if (!fn || fn === '') {
//, binding.view.models, ev
this.call.apply(this, [context].concat([].concat(processedArgs)));
} else {
var _ctx$fn;
var ctx = binding.model;
(_ctx$fn = ctx[fn]).call.apply(_ctx$fn, [ctx].concat([].concat(processedArgs)));
}
},
// Sets the attribute on the element. If no binder above is matched it will fall
// back to using this binder.
fallbackBinder: function fallbackBinder(el, value) {
if (value != null) {
// is this a component?
var type = this.type;
var comp = customElements.get(el.localName);
if (type.substr(0, 1) === ':') {
type = type.substr(1);
}
if (comp && comp.properties && comp.properties[type] !== undefined) {
var mapType = comp.properties[type];
el[mapType] = value;
} else {
el.setAttribute(type, value);
}
} else {
el.removeAttribute(this.type);
}
},
// Merges an object literal into the corresponding global options.
configure: function configure(options) {
var _this = this;
if (!options) {
return;
}
Object.keys(options).forEach(function (option) {
var value = options[option];
if (EXTENSIONS.indexOf(option) > -1) {
Object.keys(value).forEach(function (key) {
_this[option][key] = value[key];
});
} else {
_this[option] = value;
}
});
},
router: {}
};
// Check if a value is an object than can be observed.
function isObject(obj) {
return typeof obj === 'object' && obj !== null;
} // Error thrower.
function error(message) {
throw new Error('[Observer] ' + message);
}
var adapters;
var interfaces;
var rootInterface; // Constructs a new keypath observer and kicks things off.
function Observer(obj, keypath, callback) {
this.keypath = keypath;
this.callback = callback;
this.objectPath = [];
this.parse();
this.obj = this.getRootObject(obj);
if (isObject(this.target = this.realize())) {
this.set(true, this.key, this.target, this.callback);
}
}
Observer.updateOptions = function (options) {
adapters = options.adapters;
interfaces = Object.keys(adapters);
rootInterface = options.rootInterface;
}; // Tokenizes the provided keypath string into interface + path tokens for the
// observer to work with.
Observer.tokenize = function (keypath, root) {
var tokens = [];
var current = {
i: root,
path: ''
};
var index, chr;
for (index = 0; index < keypath.length; index++) {
chr = keypath.charAt(index);
if (!!~interfaces.indexOf(chr)) {
tokens.push(current);
current = {
i: chr,
path: ''
};
} else {
current.path += chr;
}
}
tokens.push(current);
return tokens;
}; // Parses the keypath using the interfaces defined on the view. Sets variables
// for the tokenized keypath as well as the end key.
Observer.prototype.parse = function () {
var path, root;
if (!interfaces.length) {
error('Must define at least one adapter interface.');
}
if (!!~interfaces.indexOf(this.keypath[0])) {
root = this.keypath[0];
path = this.keypath.substr(1);
} else {
root = rootInterface;
path = this.keypath;
}
this.tokens = Observer.tokenize(path, root);
this.key = this.tokens.pop();
}; // Realizes the full keypath, attaching observers for every key and correcting
// old observers to any changed objects in the keypath.
Observer.prototype.realize = function () {
var current = this.obj;
var unreached = -1;
var prev;
var token;
for (var index = 0; index < this.tokens.length; index++) {
token = this.tokens[index];
if (isObject(current)) {
if (typeof this.objectPath[index] !== 'undefined') {
if (current !== (prev = this.objectPath[index])) {
this.set(false, token, prev, this);
this.set(true, token, current, this);
this.objectPath[index] = current;
}
} else {
this.set(true, token, current, this);
this.objectPath[index] = current;
}
current = this.get(token, current);
} else {
if (unreached === -1) {
unreached = index;
}
if (prev = this.objectPath[index]) {
this.set(false, token, prev, this);
}
}
}
if (unreached !== -1) {
this.objectPath.splice(unreached);
}
return current;
}; // Updates the keypath. This is called when any intermediary key is changed.
Observer.prototype.sync = function () {
var next, oldValue, newValue;
if ((next = this.realize()) !== this.target) {
if (isObject(this.target)) {
this.set(false, this.key, this.target, this.callback);
}
if (isObject(next)) {
this.set(true, this.key, next, this.callback);
}
oldValue = this.value();
this.target = next;
newValue = this.value();
if (newValue !== oldValue || newValue instanceof Function) this.callback.sync();
} else if (next instanceof Array) {
this.callback.sync();
}
}; // Reads the current end value of the observed keypath. Returns undefined if
// the full keypath is unreachable.
Observer.prototype.value = function () {
if (isObject(this.target)) {
return this.get(this.key, this.target);
}
}; // Sets the current end value of the observed keypath. Calling setValue when
// the full keypath is unreachable is a no-op.
Observer.prototype.setValue = function (value) {
if (isObject(this.target)) {
adapters[this.key.i].set(this.target, this.key.path, value);
}
}; // Gets the provided key on an object.
Observer.prototype.get = function (key, obj) {
return adapters[key.i].get(obj, key.path);
}; // Observes or unobserves a callback on the object using the provided key.
Observer.prototype.set = function (active, key, obj, callback) {
var action = active ? 'observe' : 'unobserve';
adapters[key.i][action](obj, key.path, callback);
}; // Unobserves the entire keypath.
Observer.prototype.unobserve = function () {
var obj;
var token;
for (var index = 0; index < this.tokens.length; index++) {
token = this.tokens[index];
if (obj = this.objectPath[index]) {
this.set(false, token, obj, this);
}
}
if (isObject(this.target)) {
this.set(false, this.key, this.target, this.callback);
}
}; // traverse the scope chain to find the scope which has the root property
// if the property is not found in chain, returns the root scope
Observer.prototype.getRootObject = function (obj) {
var rootProp, current;
if (!obj.$parent) {
return obj;
}
if (this.tokens.length) {
rootProp = this.tokens[0].path;
} else {
rootProp = this.key.path;
}
current = obj;
while (current.$parent && current[rootProp] === undefined) {
current = current.$parent;
}
return current;
};
function getInputValue(el) {
var results = [];
if (el.type === "checkbox") {
return el.checked;
} else if (el.type === "select-multiple") {
el.options.forEach(function (option) {
if (option.selected) {
results.push(option.value);
}
});
return results;
} else {
return el.value;
}
}
var FORMATTER_ARGS = /[^\s']+|'([^']|'[^\s])*'|"([^"]|"[^\s])*"/g;
var FORMATTER_SPLIT = /\s+/;
var FN_CHECK = /\(.*\)/; // A single binding between a model attribute and a DOM element.
var Binding =
/*#__PURE__*/
function () {
// All information about the binding is passed into the constructor; the
// containing view, the DOM node, the type of binding, the model object and the
// keypath at which to listen for changes.
function Binding(view, el, type, keypath, binder, arg, formatters) {
this.view = view;
this.el = el;
this.type = type;
this.keypath = keypath;
this.binder = binder;
this.arg = arg;
this.formatters = formatters;
this.formatterObservers = {};
this.model = undefined; //new property in kick for fn args but not sure if this is the right way
this.fnArgs = []; //new property in kick for subscribers to get notified if value is changing specifically for jquery plugins
this.subscribers = []; //new property in kick for locking to avoid loops of updates, specifically for jquery plugins
this.locked = false; // add object to hold listners so that they can be unbinded later
this.listeners = {};
} // Observes the object keypath
var _proto = Binding.prototype;
_proto.observe = function observe(obj, keypath) {
return new Observer(obj, keypath, this);
};
_proto.parseTarget = function parseTarget() {
if (this.keypath) {
var token = parseType(this.keypath);
if (token.type === 0) {
this.value = token.value;
} else {
if (FN_CHECK.test(this.keypath)) {
var fnExpr = parseFnExpr(this.keypath);
if (fnExpr) {
this.keypath = fnExpr.k;
this.fnArgs = fnExpr._.map(function (x) {
return x.k;
}); //let fnArgs = fnExpr._.map(x => x.k)
//this.fnArgs = this.parseFormatterArguments(fnArgs, 0)
}
}
this.observer = this.observe(this.view.models, this.keypath);
this.model = this.observer.target;
}
} else {
this.value = undefined;
}
};
_proto.parseFormatterArguments = function parseFormatterArguments(args, formatterIndex, ev, vm) {
var _this = this;
return args.map(parseType).map(function (_ref, ai) {
var type = _ref.type,
value = _ref.value;
if (type === 0) {
return value;
} else if (value === '$ev' || value === '$event') {
//return event here
return ev;
} else if (value === '$vm' || value === '$scope') {
//return model here
return vm;
} else if (value === '$el') {
//return element here
return _this.el;
} else {
if (!_this.formatterObservers[formatterIndex]) {
_this.formatterObservers[formatterIndex] = {};
}
var observer = _this.formatterObservers[formatterIndex][ai];
if (!observer) {
observer = _this.observe(_this.view.models, value);
_this.formatterObservers[formatterIndex][ai] = observer;
}
return observer.value();
}
});
} // Applies all the current formatters to the supplied value and returns the
// formatted value.
;
_proto.formattedValue = function formattedValue(value) {
var _this2 = this;
return this.formatters.reduce(function (result, declaration, index) {
var args = declaration.match(FORMATTER_ARGS);
var id = args.shift();
var formatter = _this2.view.options.formatters[id];
var processedArgs = _this2.parseFormatterArguments(args, index, null, _this2.view.models);
if (formatter && formatter.read instanceof Function) {
result = formatter.read.apply(formatter, [result].concat(processedArgs));
} else if (formatter instanceof Function) {
result = formatter.apply(void 0, [result].concat(processedArgs));
}
return result;
}, value);
} // Returns an event handler for the binding around the supplied function.
;
_proto.eventHandler = function eventHandler(fn) {
var binding = this;
var handler = binding.view.options.handler;
var lfn = fn;
var type = this.type;
var el = this.el;
if (type.substr(0, 1) === '^') {
type = type.substr(1);
}
if (el.constructor && el.constructor.properties && el.constructor.properties[type] !== undefined) {
var mapType = el.constructor.properties[type];
el[mapType] = lfn;
} else {
return function (ev) {
lfn && handler.call(lfn, this, ev, binding);
};
}
} // Sets the value for the binding. This Basically just runs the binding routine
// with the supplied value formatted.
;
_proto.set = function set(value) {
if (value instanceof Function && !this.binder.function) {
var _value;
//todo update docs, probably a breaking change
var processedArgs = this.parseFormatterArguments(this.fnArgs, 0, null, this.view.models);
value = this.formattedValue((_value = value).call.apply(_value, [this.model].concat([].concat(processedArgs)))); //value = this.formattedValue(value.call(this.model))
} else {
value = this.formattedValue(value);
}
var routineFn = this.binder.routine || this.binder;
if (routineFn instanceof Function) {
routineFn.call(this, this.el, value);
this.callSubscribers(value);
}
} // Syncs up the view binding with the model.
;
_proto.sync = function sync() {
if (this.observer) {
this.model = this.observer.target;
this.set(this.observer.value());
} else {
this.set(this.value);
}
} // Publishes the value currently set on the input element back to the model.
;
_proto.publish = function publish() {
var _this3 = this;
if (this.observer) {
var value = this.formatters.reduceRight(function (result, declaration, index) {
var args = declaration.split(FORMATTER_SPLIT);
var id = args.shift();
var formatter = _this3.view.options.formatters[id];
var processedArgs = _this3.parseFormatterArguments(args, index, null, _this3.view.models);
if (formatter && formatter.publish) {
result = formatter.publish.apply(formatter, [result].concat(processedArgs));
}
return result;
}, this.getValue(this.el));
this.observer.setValue(value);
}
} // Subscribes to the model for changes at the specified keypath. Bi-directional
// routines will also listen for changes on the element to propagate them back
// to the model.
;
_proto.bind = function bind() {
this.parseTarget();
if (this.binder.hasOwnProperty("bind")) {
this.binder.bind.call(this, this.el);
}
if (this.view.options.preloadData) {
this.sync();
}
} // Unsubscribes from the model and the element.
;
_proto.unbind = function unbind() {
var _this4 = this;
if (this.binder.unbind) {
this.binder.unbind.call(this, this.el);
}
if (this.observer) {
this.observer.unobserve();
}
Object.keys(this.formatterObservers).forEach(function (fi) {
var args = _this4.formatterObservers[fi];
Object.keys(args).forEach(function (ai) {
args[ai].unobserve();
});
});
this.formatterObservers = {};
} // Updates the binding's model from what is currently set on the view. Unbinds
// the old model first and then re-binds with the new model.
;
_proto.update = function update(models) {
if (models === void 0) {
models = {};
}
if (this.observer) {
this.model = this.observer.target;
}
if (this.binder.update) {
this.binder.update.call(this, models);
}
} // Returns elements value
;
_proto.getValue = function getValue(el) {
if (this.binder && this.binder.getValue) {
return this.binder.getValue.call(this, el);
} else {
return getInputValue(el);
}
} // Subscribe to the value changes
;
_proto.subscribe = function subscribe(listener) {
var index = this.subscribers.push(listener) - 1; // Provide handle back for removal of listener
return {
remove: function remove() {
delete this.subscribers[index];
}
};
} // Call the subscribers
;
_proto.callSubscribers = function callSubscribers(value) {
// Cycle through subscribers queue, fire!
this.subscribers.forEach(function (item) {
item(value);
});
} // lock if not locked and returns false if already locked
;
_proto.lock = function lock(locking) {
// return locked state if none provided
if (locking === undefined) {
return this.locked;
} else if (this.locked === true && locking === true) {
// already locked asking again for same lock state
return false;
}
return this.locked = locking;
} // Returns the next of kin bindings on the same element
;
_proto.kins = function kins() {
var _this5 = this;
return this.view.bindings.filter(function (x) {
return x.el === _this5.el;
});
} // Returns the binding of a given type on the same element
;
_proto.kin = function kin(type) {
var mappedType = kick.binderMap[type] || type;
return this.kins().find(function (x) {
return x.type === mappedType;
});
};
return Binding;
}();
var textBinder = {
routine: function routine(node, value) {
node.data = value != null ? value : '';
}
};
var DECLARATION_SPLIT = /((?:'[^']*')*(?:(?:[^\|']*(?:'[^']*')+[^\|']*)+|[^\|]+))|^$/g;
var binderPrefixes = '@:^.-+?!*#$~'.split('');
var parseNode = function parseNode(view, node) {
var block = false;
if (node.nodeType === 3) {
var tokens = parseTemplate(node.data, kick.templateDelimiters);
if (tokens) {
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
var text = document.createTextNode(token.value);
node.parentNode.insertBefore(text, node);
if (token.type === 1) {
view.buildBinding(text, null, token.value, textBinder, null);
}
}
node.parentNode.removeChild(node);
}
block = true;
} else if (node.nodeType === 1) {
block = view.traverse(node);
}
if (!block) {
for (var _i = 0; _i < node.childNodes.length; _i++) {
parseNode(view, node.childNodes[_i]);
}
}
};
var bindingComparator = function bindingComparator(a, b) {
var aPriority = a.binder ? a.binder.priority || 0 : 0;
var bPriority = b.binder ? b.binder.priority || 0 : 0;
return bPriority - aPriority;
};
var trimStr = function trimStr(str) {
return str.trim();
}; // A collection of bindings built from a set of parent nodes.
var View =
/*#__PURE__*/
function () {
// The DOM elements and the model objects for binding are passed into the
// constructor along with any local options that should be used throughout the
// context of the view and it's bindings.
function View(els, models, options) {
if (els.jquery || els instanceof Array) {
this.els = els;
} else {
this.els = [els];
}
this.models = models;
this.options = options;
this.build();
}
var _proto = View.prototype;
_proto.buildBinding = function buildBinding(node, type, declaration, binder, arg) {
var pipes = declaration.match(DECLARATION_SPLIT).map(trimStr);
var keypath = pipes.shift();
if (arg === '' && type === '*') {
// resolve for in expression, useful for case sensitive members e.g. myItem in items
var forRE = new RegExp(/^(.+)\s+in\s+(.[^|\s]+)(.*)$/gm);
var forExp = forRE.exec(keypath); // console.log(forExp)
if (forExp && forExp.length > 2) {
arg = forExp[1] || arg;
keypath = forExp[2] || keypath;
} // console.log(forExp[3].split('|'))
}
this.bindings.push(new Binding(this, node, type, keypath, binder, arg, pipes));
} // Parses the DOM tree and builds `Binding` instances for every matched
// binding declaration.
;
_proto.build = function build() {
this.bindings = [];
var elements = this.els,
i,
len;
for (i = 0, len = elements.length; i < len; i++) {
parseNode(this, elements[i]);
}
this.bindings.sort(bindingComparator);
};
_proto.traverse = function traverse(node) {
// let bindingPrefix = kick._fullPrefix
var block = node.nodeName === 'SCRIPT' || node.nodeName === 'STYLE';
var attributes = node.attributes;
var bindInfos = [];
var varBinders = this.options.varBinders;
var mappedBinder;
var binderType, binder, identifier, arg;
for (var i = 0, len = attributes.length; i < len; i++) {
var attribute = attributes[i]; // if (attribute.name.indexOf(bindingPrefix) === 0) {
if (this.startWithBinder(attribute.name)) {
// binderType = attribute.name.slice(bindingPrefix.length);
mappedBinder = kick.binderMap[attribute.name];
binderType = mappedBinder || attribute.name; // type = attribute.name.slice(bindingPrefix.length)
binder = this.options.binders[binderType];
arg = undefined;
if (!binder) {
for (var k = 0; k < varBinders.length; k++) {
identifier = varBinders[k];
if (binderType.slice(0, identifier.length - 1) === identifier.slice(0, -1)) {
binder = this.options.binders[identifier];
arg = binderType.slice(identifier.length - 1);
break;
}
}
}
if (!binder) {
binder = kick.fallbackBinder;
}
if (binder.block) {
this.buildBinding(node, binderType, attribute.value, binder, arg);
node.removeAttribute(attribute.name);
return true;
}
bindInfos.push({
attr: attribute,
binder: binder,
type: binderType,
arg: arg
});
}
}
for (var _i2 = 0; _i2 < bindInfos.length; _i2++) {
var bindInfo = bindInfos[_i2];
this.buildBinding(node, bindInfo.type, bindInfo.attr.value, bindInfo.binder, bindInfo.arg);
node.removeAttribute(bindInfo.attr.name);
}
if (kick.components[binderType] && !node._bound) {
this.bindings.push(new ComponentBinding(this, node, binderType));
block = true;
}
return block;
} // Binds all of the current bindings for this view.
;
_proto.bind = function bind() {
this.bindings.forEach(function (binding) {
binding.bind();
});
} // Unbinds all of the current bindings for this view.
;
_proto.unbind = function unbind() {
this.bindings.forEach(function (binding) {
binding.unbind();
});
} // Syncs up the view with the model by running the routines on all bindings.
;
_proto.sync = function sync() {
this.bindings.forEach(function (binding) {
binding.sync();
});
} // Publishes the input values from the view back to the model (reverse sync).
;
_proto.publish = function publish() {
this.bindings.forEach(function (binding) {
if (binding.binder && binding.binder.publishes) {
binding.publish();
}
});
} // Updates the view's models along with any affected bindings.
;
_proto.update = function update(models) {
var _this = this;
if (models === void 0) {
models = {};
}
Object.keys(models).forEach(function (key) {
_this.models[key] = models[key];
});
this.bindings.forEach(function (binding) {
if (binding.update) {
binding.update(models);
}
});
} // move it to utils
;
_proto.startWithBinder = function startWithBinder(name) {
var prefix = name.substr(0, 1);
return binderPrefixes.some(function (pre) {
return pre === prefix;
});
};
return View;
}();
var has$ = window.jQuery ? true : false;
var on = function on(el, arg, handler) {
has$ ? $(el).on(arg, handler) : el.addEventListener(arg, handler);
};
var off = function off(el, arg, handler) {
has$ ? $(el).off(arg, handler) : el.removeEventListener(arg, handler);
};
var getString = function getString(value) {
return value != null ? value.toString() : undefined;
};
var times = function times(n, cb) {
for (var i = 0; i < n; i++) {
cb();
}
};
function createView(binding, data, anchorEl) {
var template = binding.el.cloneNode(true);
var view = new View(template, data, binding.view.options);
view.bind();
binding.marker.parentNode.insertBefore(template, anchorEl);
return view;
}
var binders = {
// Binds an event handler on the element.
'^&': {
function: true,
priority: 1000,
unbind: function unbind(el) {
if (this.handler) {
off(el, this.arg, this.handler);
}
},
routine: function routine(el, value) {
if (this.handler) {
off(el, this.arg, this.handler);
}
this.handler = this.eventHandler(value);
on(el, this.arg, this.handler);
}
},
// for $item Appends bound instances of the element in place for item in the array.
'*&': {
block: true,
priority: 4000,
bind: function bind(el) {
if (!this.marker) {
this.marker = document.createComment(" kick: " + this.type + " ");
this.iterated = [];
el.parentNode.insertBefore(this.marker, el);
el.parentNode.removeChild(el);
} else {
this.iterated.forEach(function (view) {
view.bind();
});
}
},
unbind: function unbind(el) {
if (this.iterated) {
this.iterated.forEach(function (view) {
view.unbind();
});
}
},
routine: function routine(el, collection) {
var _this = this;
var modelName = this.arg || '$item';
collection = collection || [];
var indexProp = el.getAttribute('#index') || el.getAttribute('index-property') || '$index';
collection.forEach(function (model, index) {
var data = {
$parent: _this.view.models
};
data[indexProp] = index;
data[modelName] = model;
var view = _this.iterated[index];
if (!view) {
var previous = _this.marker;
if (_this.iterated.length) {
previous = _this.iterated[_this.iterated.length - 1].els[0];
}
view = createView(_this, data, previous.nextSibling);
_this.iterated.push(view);
} else {
if (view.models[modelName] !== model) {
// search for a view that matches the model
var matchIndex, nextView;
for (var nextIndex = index + 1; nextIndex < _this.iterated.length; nextIndex++) {
nextView = _this.iterated[nextIndex];
if (nextView.models[modelName] === model) {
matchIndex = nextIndex;
break;
}
}
if (matchIndex !== undefined) {
// model is in other position
// todo: consider avoiding the splice here by setting a flag
// profile performance before implementing such change
_this.iterated.splice(matchIndex, 1);
_this.marker.parentNode.insertBefore(nextView.els[0], view.els[0]);
nextView.models[indexProp] = index;
} else {
//new model
nextView = createView(_this, data, view.els[0]);
}
_this.iterated.splice(index, 0, nextView);
} else {
view.models[indexProp] = index;
}
}
});
if (this.iterated.length > collection.length) {
times(this.iterated.length - collection.length, function () {
var view = _this.iterated.pop();
view.unbind();
_this.marker.parentNode.removeChild(view.els[0]);
});
}
if (el.nodeName === 'OPTION') {
this.view.bindings.forEach(function (binding) {
if (binding.el === _this.marker.parentNode && binding.type === 'value') {
binding.sync();
}
});
}
},
update: function update(models) {
var _this2 = this;
var data = {}; //todo: add test and fix if necessary
Object.keys(models).forEach(function (key) {
if (key !== _this2.arg) {
data[key] = models[key];
}
});
this.iterated.forEach(function (view) {
view.update(data);
});
}
},
// Order of ..& should be before .& otherwise match will trigger shorter length first
// removes the style from the element when there is value
'-..&': function _(el, value) {
var propertyName = this.arg;
if (value) {
el.style[propertyName] = '';
}
},
// Order of ..& should be before .& otherwise match will trigger shorter length first
// Adds or removes the style from the element when there is value
'..&': function _(el, value) {
var propertyName = this.arg;
if (value) {
el.style[propertyName] = value;
} else {
el.style[propertyName] = '';
}
},
// Adds or removes the class from the element when value is false or true.
'-.&': function _(el, value) {
var elClass = " " + el.className + " ";
if (value === elClass.indexOf(" " + this.arg + " ") > -1) {
if (value) {
el.className = elClass.replace(" " + this.arg + " ", ' ').trim();
} else {
el.className = el.className + " " + this.arg;
}
}
},
// Adds or removes the class from the element when value is true or false.
'.&': function _(el, value) {
var elClass = " " + el.className + " ";
var cls = this.arg; // Added to assign value using function or hardcoding .="cls()" //+ test
if (cls === undefined || cls === null || cls === '') {
cls = value;
}
if (!value === elClass.indexOf(" " + cls + " ") > -1) {
if (value) {
el.className = el.className + " " + cls;
} else {
el.className = elClass.replace(" " + cls + " ", ' ').trim();
}
}
},
// Sets the element's text value.
':text': function text(el, value) {
el.textContent = value != null ? value : '';
},
// Sets the element's HTML content.
':html': function html(el, value) {
el.innerHTML = value != null ? value : '';
},
// Shows the element when value is true.
':show': function show(el, value) {
el.style.display = value ? '' : 'none';
},
// Hides the element when value is true (negated version of `show` binder).
':hide': function hide(el, value) {
el.style.display = value ? 'none' : '';
},
// Enables the element when value is true.
':enabled': function enabled(el, value) {
el.disabled = !value;
},
// Disables the element when value is true (negated version of `enabled` binder).
':disabled': function disabled(el, value) {
el.disabled = !!value;
},
// Checks a checkbox or radio input when the value is true. Also sets the model
// property when the input is checked or unchecked (two-way binder).
// also "@x"
'@checked': {
publishes: true,
priority: 2000,
bind: function bind(el) {
var self = this;
if (!this.callback) {
this.callback = function () {
self.publish();
};
}
el.addEventListener('change', this.callback);
},
unbind: function unbind(el) {
el.removeEventListener('change', this.callback);
},
routine: function routine(el, value) {
if (el.type === 'radio') {
el.checked = getString(el.value) === getString(value);
} else {
el.checked = !!value;
}
}
},
// Unchecks a checkbox or radio input when the value is true. Also sets the model
// property when the input is checked or unchecked (two-way binder).
// also "@-x"
'@unchecked': {
publishes: true,
priority: 2000,
bind: function bind(el) {
var self = this;
if (!this.callback) {
this.callback = function () {
self.publish();
};
}
el.addEventListener('change', this.callback);
},
unbind: function unbind(el) {
el.removeEventListener('change', this.callback);
},
routine: function routine(el, value) {
if (el.type === 'radio') {
el.checked = !(getString(el.value) === getString(value));
} else {
el.checked = !!!value;
}
}
},
// Sets the element's value. Also sets the model property when the input changes
// (two-way binder).
'@value': {
publishes: true,
priority: 3000,
bind: function bind(el) {
this.isRadio = el.tagName === 'INPUT' && el.type === 'radio';
if (!this.isRadio) {
this.event = el.getAttribute('event-name') || (el.tagName === 'SELECT' ? 'change' : 'input');
var self = this;
if (!this.callback) {
this.callback = function () {
self.publish();
};
}
el.addEventListener(this.event, this.callback);
}
},
unbind: function unbind(el) {
if (!this.isRadio) {
el.removeEventListener(this.event, this.callback);
}
},
routine: function routine(el, value) {
if (this.isRadio) {
el.setAttribute('value', value);
} else {
if (el.type === 'select-multiple') {
if (value instanceof Array) {
for (var i = 0; i < el.length; i++) {
var option = el[i];
option.selected = value.indexOf(option.value) > -1;
}
}
} else if (getString(value) !== getString(el.value)) {
el.value = value != null ? value : '';
}
}
}
},
// Inserts and binds the element and it's child nodes into the DOM when true.
'?': {
block: true,
priority: 4000,
bind: function bind(el) {
if (!this.marker) {
this.marker = document.createComment(' kick: ' + this.type + ' ' + this.keypath + ' ');
this.attached = false;
el.parentNode.insertBefore(this.marker, el);
el.parentNode.removeChild(el);
} else if (this.bound === false && this.nested) {
this.nested.bind();
}
this.bound = true;
},
unbind: function unbind() {
if (this.nested) {
this.nested.unbind();
this.bound = false;
}
},
routine: function routine(el, value) {
if (!!value !== this.attached) {
if (value) {
if (!this.nested) {
this.nested = new View(el, this.view.models, this.view.options);
this.nested.bind();
}
this.marker.parentNode.insertBefore(el, this.marker.nextSibling);
this.attached = true;
} else {
el.parentNode.removeChild(el);
this.attached = false;
}
}
},
update: function update(models) {
if (this.nested) {
this.nested.update(models);
}
}
},
// Inserts and binds the element and it's child nodes into the DOM when false.
'-?': {
block: true,
priority: 4001,
bind: function bind(el) {
if (!this.marker) {
this.marker = document.createComment(' kick: ' + this.type + ' ' + this.keypath + ' ');
this.attached = false;
el.parentNode.insertBefore(this.marker, el);
el.parentNode.removeChild(el);
} else if (this.bound === false && this.nested) {
this.nested.bind();
}
this.bound = true;
},
unbind: function unbind() {
if (this.nested) {
this.nested.unbind();
this.bound = false;
}
},
routine: function routine(el, value) {
if (!!value === this.attached) {
if (!value) {
if (!this.nested) {
this.nested = new View(el, this.view.models, this.view.options);
this.nested.bind();
}
this.marker.parentNode.insertBefore(el, this.marker.nextSibling);
this.attached = true;
} else {
el.parentNode.removeChild(el);
this.attached = false;
}
}
},
update: function update(models) {
if (this.nested) {
this.nested.update(models);
}
}
},
':&': function _(el, value) {
if (value != null) {
el.setAttribute(this.arg, value);
} else {
el.removeAttribute(this.arg);
}
},
'-:&': function _(el, value) {
if (value != null) {
el.removeAttribute(this.arg);
}
}
};
var formatters = {
watch: function watch(value) {
return value;
},
not: function not(value) {
return !value;
},
negate: function negate(value) {
return !value;
}
};
// The default `.` adapter that comes with kick.js. Allows subscribing to
// properties on plain objects, implemented in ES5 natives using
// `Object.defineProperty`.
var ARRAY_METHODS = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice'];
var adapter = {
counter: 0,
weakmap: {},
weakReference: function weakReference(obj) {
if (!obj.hasOwnProperty('__rv')) {
var id = this.counter++;
Object.defineProperty(obj, '__rv', {
value: id
});
}
if (!this.weakmap[obj.__rv]) {
this.weakmap[obj.__rv] = {
callbacks: {}
};
}
return this.weakmap[obj.__rv];
},
cleanupWeakReference: function cleanupWeakReference(ref, id) {
if (!Object.keys(ref.callbacks).length) {
if (!(ref.pointers && Object.keys(ref.pointers).length)) {
delete this.weakmap[id];
}
}
},
stubFunction: function stubFunction(obj, fn) {
var original = obj[fn];
var map = this.weakReference(obj);
var weakmap = this.weakmap;
obj[fn] = function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
var response = original.apply(obj, args);
Object.keys(map.pointers).forEach(function (r) {
var k = map.pointers[r];
if (weakmap[r]) {
if (weakmap[r].callbacks[k] instanceof Array) {
weakmap[r].callbacks[k].forEach(function (callback) {
callback.sync();
});
}
}
});
return response;
};
},
observeMutations: function observeMutations(obj, ref, keypath) {
var _this = this;
if (obj instanceof Array) {
var map = this.weakReference(obj);
if (!map.pointers) {
map.pointers = {};
ARRAY_METHODS.forEach(function (fn) {
_this.stubFunction(obj, fn);
});
}
if (!map.pointers[ref]) {
map.pointers[ref] = [];
}
if (map.pointers[ref].indexOf(keypath) === -1) {
map.pointers[ref].push(keypath);
}
}
},
unobserveMutations: function unobserveMutations(obj, ref, keypath) {
if (obj instanceof Array && obj.__rv != null) {
var map = this.weakmap[obj.__rv];
if (map) {
var pointers = map.pointers[ref];
if (pointers) {
var idx = pointers.indexOf(keypath);
if (idx > -1) {
pointers.splice(idx, 1);
}
if (!pointers.length) {
delete map.pointers[ref];
}
this.cleanupWeakReference(map, obj.__rv);
}
}
}
},
observe: function observe(obj, keypath, callback) {
var _this2 = this;
var value;
var callbacks = this.weakReference(obj).callbacks;
if (!callbacks[keypath]) {
callbacks[keypath] = [];
var desc = Object.getOwnPropertyDescriptor(obj, keypath);
if (!desc || !(desc.get || desc.set || !desc.configurable)) {
value = obj[keypath];
Object.defineProperty(obj, keypath, {
enumerable: true,
get: function get() {
return value;
},
set: function set(newValue) {
if (newValue !== value) {
_this2.unobserveMutations(value, obj.__rv, keypath);
value = newValue;
var map = _this2.weakmap[obj.__rv];
if (map) {
var _callbacks = map.callbacks[keypath];
if (_callbacks) {
_callbacks.forEach(function (cb) {
cb.sync();
});
}
_this2.observeMutations(newValue, obj.__rv, keypath);
}
}
}
});
}
}
if (callbacks[keypath].indexOf(callback) === -1) {
callbacks[keypath].push(callback);
}
this.observeMutations(obj[keypath], obj.__rv, keypath);
},
unobserve: function unobserve(obj, keypath, callback) {
var map = this.weakmap[obj.__rv];
if (map) {