way-js
Version:
Simple, lightweight, persistent, framework-agnostic two-way databinding Javascript library
1,756 lines (1,526 loc) • 48.1 kB
JavaScript
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.way = factory();
}
})(this, function() {
'use strict';
var way, w, tagPrefix = 'way';
//////////////////////////////
// EVENT EMITTER DEFINITION //
//////////////////////////////
var EventEmitter = function() {
this._watchers = {};
this._watchersAll = {};
};
EventEmitter.prototype.constructor = EventEmitter;
EventEmitter.prototype.watchAll = function(handler) {
this._watchersAll = this._watchersAll || [];
if (!_w.contains(this._watchersAll, handler)) {
this._watchersAll.push(handler);
}
};
EventEmitter.prototype.watch = function(selector, handler) {
if (!this._watchers) {
this._watchers = {};
}
this._watchers[selector] = this._watchers[selector] || [];
this._watchers[selector].push(handler);
};
EventEmitter.prototype.findWatcherDeps = function(selector) {
// Go up to look for parent watchers
// ex: if "some.nested.value" is the selector, it should also trigger for "some"
var result = [];
var watchers = _w.keys(this._watchers);
watchers.forEach(function(watcher) {
if (startsWith(selector, watcher)) {
result.push(watcher);
}
});
return result;
};
EventEmitter.prototype.emitChange = function(selector /* , arguments */) {
if (!this._watchers) {
this._watchers = {};
}
var self = this;
// Send data down to the local watchers
var deps = self.findWatcherDeps(selector);
deps.forEach(function(item) {
if (self._watchers[item]) {
self._watchers[item].forEach(function(handler) {
handler.apply(self, [self.get(item)]);
});
}
});
// Send data down to the global watchers
if (!self._watchersAll || !_w.isArray(self._watchersAll)) {
return;
}
self._watchersAll.forEach(function(watcher) {
if (_w.isFunction(watcher)) {
watcher.apply(self, [selector, self.get(selector)]);
}
});
};
////////////////////
// WAY DEFINITION //
////////////////////
var WAY = function() {
this.data = {};
this._bindings = {};
this.options = {
persistent: true,
timeoutInput: 50,
timeoutDOM: 500
};
};
// Inherit from EventEmitter
WAY.prototype = Object.create(EventEmitter.prototype);
WAY.constructor = WAY;
//////////////////////////
// DOM METHODS CHAINING //
//////////////////////////
WAY.prototype.dom = function(element) {
this._element = w.dom(element).get(0);
return this;
};
//////////////////////////////
// DOM METHODS: DOM -> JSON //
//////////////////////////////
WAY.prototype.toStorage = function(options, element) {
var self = this,
element = element || self._element,
options = options || self.dom(element).getOptions(),
data = self.dom(element).toJSON(options),
scope = self.dom(element).scope(),
selector = scope ? scope + '.' + options.data : options.data;
if (options.readonly) {
return false;
}
self.set(selector, data, options);
};
WAY.prototype.toJSON = function(options, element) {
var self = this,
element = element || self._element,
data = self.dom(element).getValue(),
options = options || self.dom(element).getOptions();
if (_w.isArray(options.pick)) {
data = selectNested(data, options.pick, true);
}
if (_w.isArray(options.omit)) {
data = selectNested(data, options.omit, false);
}
return data;
};
//////////////////////////////
// DOM METHODS: JSON -> DOM //
//////////////////////////////
WAY.prototype.fromStorage = function(options, element) {
var self = this,
element = element || self._element,
options = options || self.dom(element).getOptions();
if (options.writeonly) {
return false;
}
var scope = self.dom(element).scope(),
selector = scope ? scope + '.' + options.data : options.data,
data = self.get(selector);
self.dom(element).fromJSON(data, options);
};
WAY.prototype.fromJSON = function(data, options, element) {
var self = this,
element = element || self._element,
options = options || self.dom(element).getOptions();
if (options.writeonly) {
return false;
}
if (_w.isObject(data)) {
if (_w.isArray(options.pick)) {
data = selectNested(data, options.pick, true);
}
if (_w.isArray(options.omit)) {
data = selectNested(data, options.omit, false);
}
var currentData = _w.isObject(self.dom(element).toJSON())
? self.dom(element).toJSON()
: {};
data = _w.extend(currentData, data);
}
if (options.json) {
data = _json.isStringified(data) ? data : _json.prettyprint(data);
}
self.dom(element).setValue(data, options);
};
/////////////////////////////////
// DOM METHODS: GET - SET HTML //
/////////////////////////////////
WAY.prototype.getValue = function(element) {
var self = this, element = element || self._element;
var getters = {
SELECT: function() {
return w.dom(element).val();
},
INPUT: function() {
var type = w.dom(element).type();
if (_w.contains(['text', 'password'], type)) {
return w.dom(element).val();
}
if (_w.contains(['checkbox', 'radio'], type)) {
return w.dom(element).prop('checked') ? w.dom(element).val() : null;
}
},
TEXTAREA: function() {
return w.dom(element).val();
}
};
var defaultGetter = function(a) {
return w.dom(element).html();
};
var elementType = w.dom(element).get(0).tagName;
var getter = getters[elementType] || defaultGetter;
return getter();
};
WAY.prototype._transforms = {
uppercase: function(data) {
return _w.isString(data) ? data.toUpperCase() : data;
},
lowercase: function(data) {
return _w.isString(data) ? data.toLowerCase() : data;
},
reverse: function(data) {
return data && data.split && _w.isFunction(data.split)
? data.split('').reverse().join('')
: data;
}
};
WAY.prototype.registerTransform = function(name, transform) {
var self = this;
if (_w.isFunction(transform)) {
self._transforms[name] = transform;
}
};
WAY.prototype.setValue = function(data, options, element) {
var self = this,
element = element || self._element,
options = options || self.dom(element).getOptions();
options.transform = options.transform || [];
options.transform.forEach(function(transformName) {
var transform =
self._transforms[transformName] ||
function(data) {
return data;
};
data = transform(data);
});
var setters = {
SELECT: function(a) {
w.dom(element).val(a);
},
INPUT: function(a) {
if (!_w.isString(a)) {
a = JSON.stringify(a);
}
var type = w.dom(element).get(0).type;
if (_w.contains(['text', 'password'], type)) {
w.dom(element).val(a || '');
}
if (_w.contains(['checkbox', 'radio'], type)) {
if (a === w.dom(element).val()) {
w.dom(element).prop('checked', true);
} else {
w.dom(element).prop('checked', false);
}
}
},
TEXTAREA: function(a) {
if (!_w.isString(a)) {
a = JSON.stringify(a);
}
w.dom(element).val(a || '');
},
PRE: function(a) {
if (options.html) {
w.dom(element).html(a);
} else {
w.dom(element).text(a);
}
},
IMG: function(a) {
if (!a) {
a = options.default || '';
w.dom(element).attr('src', a);
return false;
}
var isValidImageUrl = function(url, cb) {
w.dom(element).addClass('way-loading');
w.dom('img', {
src: url,
onerror: function() {
cb(false);
},
onload: function() {
cb(true);
}
});
};
isValidImageUrl(a, function(response) {
w.dom(element).removeClass('way-loading');
if (response) {
w.dom(element).removeClass('way-error').addClass('way-success');
} else {
if (a) {
w.dom(element).addClass('way-error');
} else {
w
.dom(element)
.removeClass('way-error')
.removeClass('way-success');
}
a = options.default || '';
}
w.dom(element).attr('src', a);
});
}
};
var defaultSetter = function(a) {
if (options.html) {
w.dom(element).html(a);
} else {
w.dom(element).text(a);
}
};
var elementType = w.dom(element).get(0).tagName;
var setter = setters[elementType] || defaultSetter;
setter(data);
};
WAY.prototype.setDefault = function(force, options, element) {
var self = this,
element = element || self._element,
force = force || false,
options = options
? _w.extend(self.dom(element).getOptions(), options)
: self.dom(element).getOptions();
// Should we just set the default value in the DOM, or also in the datastore?
if (!options.default) {
return false;
}
if (force) {
self.set(options.data, options.default, options);
} else {
self.dom(element).setValue(options.default, options);
}
};
WAY.prototype.setDefaults = function() {
var self = this, dataSelector = '[' + tagPrefix + '-default]';
var elements = w.dom(dataSelector).get();
for (var i in elements) {
var element = elements[i],
options = self.dom(element).getOptions(),
selector = options.data || null,
data = selector ? self.get(selector) : null;
if (!data) {
self.dom(element).setDefault();
}
}
};
/////////////////////////////////////
// DOM METHODS: GET - SET BINDINGS //
/////////////////////////////////////
// Scans the DOM to look for new bindings
WAY.prototype.registerBindings = function() {
// Dealing with bindings removed from the DOM by just resetting all the bindings all the time.
// Isn't there a better way?
// One idea would be to add a "way-bound" class to bound elements
// self._bindings = {};
var self = this;
var selector = '[' + tagPrefix + '-data]';
self._bindings = {};
var elements = w.dom(selector).get();
for (var i in elements) {
var element = elements[i],
options = self.dom(element).getOptions(),
scope = self.dom(element).scope(),
selector = scope ? scope + '.' + options.data : options.data;
self._bindings[selector] = self._bindings[selector] || [];
if (!_w.contains(self._bindings[selector], w.dom(element).get(0))) {
self._bindings[selector].push(w.dom(element).get(0));
}
}
};
WAY.prototype.updateBindings = function(selector) {
var self = this;
self._bindings = self._bindings || {};
// Set bindings for the data selector
var bindings = pickAndMergeParentArrays(self._bindings, selector);
bindings.forEach(function(element) {
var focused = w.dom(element).get(0) === w.dom(':focus').get(0)
? true
: false;
if (!focused) {
self.dom(element).fromStorage();
}
});
// Set bindings for the global selector
if (self._bindings['__all__']) {
self._bindings['__all__'].forEach(function(element) {
self.dom(element).fromJSON(self.data);
});
}
};
////////////////////////////////////
// DOM METHODS: GET - SET REPEATS //
////////////////////////////////////
WAY.prototype.registerRepeats = function() {
// Register repeats
var self = this;
var selector = '[' + tagPrefix + '-repeat]';
self._repeats = self._repeats || {};
self._repeatsCount = self._repeatsCount || 0;
var elements = w.dom(selector).get();
for (var i in elements) {
var element = elements[i], options = self.dom(element).getOptions();
self._repeats[options.repeat] = self._repeats[options.repeat] || [];
var wrapperAttr =
tagPrefix + '-repeat-wrapper="' + self._repeatsCount + '"',
parent = w.dom(element).parent('[' + wrapperAttr + ']');
if (!parent.length) {
self._repeats[options.repeat].push({
id: self._repeatsCount,
element: w
.dom(element)
.clone(true)
.removeAttr(tagPrefix + '-repeat')
.removeAttr(tagPrefix + '-filter')
.get(0),
selector: options.repeat,
filter: options.filter
});
var wrapper = document.createElement('div');
w.dom(wrapper).attr(tagPrefix + '-repeat-wrapper', self._repeatsCount);
w.dom(wrapper).attr(tagPrefix + '-scope', options.repeat);
if (options.filter) {
w.dom(wrapper).attr(tagPrefix + '-filter', options.filter);
}
w.dom(element).replaceWith(wrapper);
self.updateRepeats(options.repeat);
self._repeatsCount++;
}
}
};
/*
WAY.prototype._filters = {
noFalsy: function(item ) {
if (!item) {
return false;
} else {
return true;
}
}
};
WAY.prototype.registerFilter = function(name, filter) {
var self = this;
if (_w.isFunction(filter)) { self._filters[name] = filter; }
}
*/
WAY.prototype.updateRepeats = function(selector) {
var self = this;
self._repeats = self._repeats || {};
var repeats = pickAndMergeParentArrays(self._repeats, selector);
repeats.forEach(function(repeat) {
var wrapper = '[' + tagPrefix + '-repeat-wrapper="' + repeat.id + '"]',
data = self.get(repeat.selector),
items = [];
repeat.filter = repeat.filter || [];
w.dom(wrapper).empty();
for (var key in data) {
/*
var item = data[key],
test = true;
for (var i in repeat.filter) {
var filterName = repeat.filter[i];
var filter = self._filters[filterName] || function(data) { return data };
test = filter(item);
if (!test) { break; }
}
if (!test) { continue; }
*/
w.dom(repeat.element).attr(tagPrefix + '-scope', key);
var html = w.dom(repeat.element).get(0).outerHTML;
html = html.replace(/\$\$key/gi, key);
items.push(html);
}
w.dom(wrapper).html(items.join(''));
self.registerBindings();
self.updateBindings();
});
};
////////////////////////
// DOM METHODS: FORMS //
////////////////////////
WAY.prototype.updateForms = function() {
// If we just parse the forms with form2js (see commits before 08/19/2014) and set the data with way.set(),
// we reset the entire data for this pathkey in the datastore. It causes the bug
// reported here: https://github.com/gwendall/way.js/issues/10
// Solution:
// 1. watch new forms with a [way-data] attribute
// 2. remove this attribute
// 3. attach the appropriate attributes to its child inputs
// -> so that each input is set separately to way.js' datastore
var self = this;
var selector = 'form[' + tagPrefix + '-data]';
var elements = w.dom(selector).get();
for (var i in elements) {
var form = elements[i],
options = self.dom(form).getOptions(),
formDataSelector = options.data;
w.dom(form).removeAttr(tagPrefix + '-data');
// Reverse needed to set the right index for "[]" names
var inputs = w.dom(form).find('[name]').reverse().get();
for (var i in inputs) {
var input = inputs[i], name = w.dom(input).attr('name');
if (endsWith(name, '[]')) {
var array = name.split('[]')[0],
arraySelector = "[name^='" + array + "']",
arrayIndex = w.dom(form).find(arraySelector).get().length;
name = array + '.' + arrayIndex;
}
var selector = formDataSelector + '.' + name;
options.data = selector;
self.dom(input).setOptions(options);
w.dom(input).removeAttr('name');
}
}
};
/////////////////////////////////////////////
// DOM METHODS: GET - SET ALL DEPENDENCIES //
/////////////////////////////////////////////
WAY.prototype.registerDependencies = function() {
this.registerBindings();
this.registerRepeats();
};
WAY.prototype.updateDependencies = function(selector) {
this.updateBindings(selector);
this.updateRepeats(selector);
this.updateForms(selector);
};
//////////////////////////////////
// DOM METHODS: OPTIONS PARSING //
//////////////////////////////////
WAY.prototype.setOptions = function(options, element) {
var self = this, element = self._element || element;
for (var k in options) {
var attr = tagPrefix + '-' + k, value = options[k];
w.dom(element).attr(attr, value);
}
};
WAY.prototype.getOptions = function(element) {
var self = this,
element = element || self._element,
defaultOptions = {
data: null,
html: false,
readonly: false,
writeonly: false,
persistent: false
};
return _w.extend(defaultOptions, self.dom(element).getAttrs(tagPrefix));
};
WAY.prototype.getAttrs = function(prefix, element) {
var self = this, element = element || self._element;
var parseAttrValue = function(key, value) {
var attrTypes = {
pick: 'array',
omit: 'array',
readonly: 'boolean',
writeonly: 'boolean',
json: 'boolean',
html: 'boolean',
persistent: 'boolean'
};
var parsers = {
array: function(value) {
return value.split(',');
},
boolean: function(value) {
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
return true;
}
};
var defaultParser = function() {
return value;
};
var valueType = attrTypes[key] || null;
var parser = parsers[valueType] || defaultParser;
return parser(value);
};
var attributes = {};
var attrs = [].slice.call(w.dom(element).get(0).attributes);
attrs.forEach(function(attr) {
var include = prefix && startsWith(attr.name, prefix + '-')
? true
: false;
if (include) {
var name = prefix
? attr.name.slice(prefix.length + 1, attr.name.length)
: attr.name;
var value = parseAttrValue(name, attr.value);
if (_w.contains(['transform', 'filter'], name)) {
value = value.split('|');
}
attributes[name] = value;
}
});
return attributes;
};
//////////////////////////
// DOM METHODS: SCOPING //
//////////////////////////
WAY.prototype.scope = function(options, element) {
var self = this,
element = element || self._element,
scopeAttr = tagPrefix + '-scope',
scopeBreakAttr = tagPrefix + '-scope-break',
scopes = [],
scope = '';
var parentsSelector = '[' + scopeBreakAttr + '], [' + scopeAttr + ']';
var elements = w.dom(element).parents(parentsSelector).get();
for (var i in elements) {
var el = elements[i];
if (w.dom(el).attr(scopeBreakAttr)) {
break;
}
var attr = w.dom(el).attr(scopeAttr);
scopes.unshift(attr);
}
if (w.dom(element).attr(scopeAttr)) {
scopes.push(w.dom(element).attr(scopeAttr));
}
if (w.dom(element).attr(scopeBreakAttr)) {
scopes = [];
}
scope = _w.compact(scopes).join('.');
return scope;
};
//////////////////
// DATA METHODS //
//////////////////
WAY.prototype.get = function(selector) {
var self = this;
if (selector !== undefined && !_w.isString(selector)) {
return false;
}
if (!self.data) {
return {};
}
return selector ? _json.get(self.data, selector) : self.data;
};
WAY.prototype.set = function(selector, value, options) {
if (!selector) {
return false;
}
if (selector.split('.')[0] === 'this') {
console.log('Sorry, "this" is a reserved word in way.js');
return false;
}
var self = this;
options = options || {};
if (selector) {
if (!_w.isString(selector)) {
return false;
}
self.data = self.data || {};
self.data = selector ? _json.set(self.data, selector, value) : {};
self.updateDependencies(selector);
self.emitChange(selector, value);
if (options.persistent) {
self.backup(selector);
}
}
};
WAY.prototype.push = function(selector, value, options) {
if (!selector) {
return false;
}
var self = this;
options = options || {};
if (selector) {
self.data = selector ? _json.push(self.data, selector, value, true) : {};
}
self.updateDependencies(selector);
self.emitChange(selector, null);
if (options.persistent) {
self.backup(selector);
}
};
WAY.prototype.remove = function(selector, options) {
var self = this;
options = options || {};
if (selector) {
self.data = _json.remove(self.data, selector);
} else {
self.data = {};
}
self.updateDependencies(selector);
self.emitChange(selector, null);
if (options.persistent) {
self.backup(selector);
}
};
WAY.prototype.clear = function() {
this.remove(null, { persistent: true });
};
//////////////////////////
// LOCALSTORAGE METHODS //
//////////////////////////
WAY.prototype.backup = function() {
var self = this;
if (!self.options.persistent) {
return;
}
try {
var data = self.data || {};
localStorage.setItem(tagPrefix, JSON.stringify(data));
} catch (e) {
console.log('Your browser does not support localStorage.');
}
};
WAY.prototype.restore = function() {
var self = this;
if (!self.options.persistent) {
return;
}
try {
var data = localStorage.getItem(tagPrefix);
try {
data = JSON.parse(data);
for (var key in data) {
self.set(key, data[key]);
}
} catch (e) {}
} catch (e) {
console.log('Your browser does not support localStorage.');
}
};
//////////
// MISC //
//////////
var matchesSelector = function(el, selector) {
var matchers = [
'matches',
'matchesSelector',
'webkitMatchesSelector',
'mozMatchesSelector',
'msMatchesSelector',
'oMatchesSelector'
],
fn = null;
for (var i in matchers) {
fn = matchers[i];
if (_w.isFunction(el[fn])) {
return el[fn](selector);
}
}
return false;
};
var startsWith = function(str, starts) {
if (starts === '') {
return true;
}
if (str === null || starts === null) {
return false;
}
str = String(str);
starts = String(starts);
return (
str.length >= starts.length && str.slice(0, starts.length) === starts
);
};
var endsWith = function(str, ends) {
if (ends === '') {
return true;
}
if (str === null || ends === null) {
return false;
}
str = String(str);
ends = String(ends);
return (
str.length >= ends.length &&
str.slice(str.length - ends.length, str.length) === ends
);
};
var cleanEmptyKeys = function(object) {
return _w.pick(object, _w.compact(_w.keys(object)));
};
var filterStartingWith = function(object, string, type) {
// true: pick - false: omit
var keys = _w.keys(object);
keys.forEach(function(key) {
if (type) {
if (!startsWith(key, string)) {
delete object[key];
}
} else {
if (startsWith(key, string)) {
delete object[key];
}
}
});
return object;
};
var selectNested = function(data, keys, type) {
// true: pick - false: omit
// Flatten / unflatten to allow for nested picks / omits (doesn't work with regular pick)
// ex: data = {something:{nested:"value"}}
// keys = ['something.nested']
var flat = _json.flatten(data);
for (var i in keys)
flat = filterStartingWith(flat, keys[i], type);
var unflat = _json.unflatten(flat);
// Unflatten returns an object with an empty property if it is given an empty object
return cleanEmptyKeys(unflat);
};
var pickAndMergeParentArrays = function(object, selector) {
// Example:
// object = { a: [1,2,3], a.b: [4,5,6], c: [7,8,9] }
// fn(object, "a.b")
// > [1,2,3,4,5,6]
var keys = [];
if (selector) {
// Set bindings for the specified selector
// (bindings that are repeat items)
var split = selector.split('.'),
lastKey = split[split.length - 1],
isArrayItem = !isNaN(lastKey);
if (isArrayItem) {
split.pop();
var key = split.join('.');
keys = object[key] ? _w.union(keys, object[key]) : keys;
}
// (bindings with keys starting with, to include nested bindings)
for (var key in object) {
if (startsWith(key, selector)) {
keys = _w.union(keys, object[key]);
}
}
} else {
// Set bindings for all selectors
for (var key in object) {
keys = _w.union(keys, object[key]);
}
}
return keys;
};
var isPrintableKey = function(e) {
var keycode = e.keyCode;
if (!keycode) {
return true;
}
var valid =
keycode === 8 || // delete
(keycode > 47 && keycode < 58) || // number keys
keycode === 32 ||
keycode === 13 || // spacebar & return key(s) (if you want to allow carriage returns)
(keycode > 64 && keycode < 91) || // letter keys
(keycode > 95 && keycode < 112) || // numpad keys
(keycode > 185 && keycode < 193) || // ;=,-./` (in order)
(keycode > 218 && keycode < 223); // [\]' (in order)
return valid;
};
var escapeHTML = function(str) {
return str && _w.isString(str)
? str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
: str;
};
///////////////////////////////////////////////////
// _w (strip of the required underscore methods) //
///////////////////////////////////////////////////
var _w = {};
var ArrayProto = Array.prototype,
ObjProto = Object.prototype,
FuncProto = Function.prototype;
var nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeBind = FuncProto.bind;
var push = ArrayProto.push,
slice = ArrayProto.slice,
concat = ArrayProto.concat,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
var flatten = function(input, shallow, strict, output) {
if (shallow && _w.every(input, _w.isArray)) {
return concat.apply(output, input);
}
for (var i = 0, length = input.length; i < length; i++) {
var value = input[i];
if (!_w.isArray(value) && !_w.isArguments(value)) {
if (!strict) output.push(value);
} else if (shallow) {
push.apply(output, value);
} else {
flatten(value, shallow, strict, output);
}
}
return output;
};
var createCallback = function(func, context, argCount) {
if (context === void 0) return func;
switch (argCount == null ? 3 : argCount) {
case 1:
return function(value) {
return func.call(context, value);
};
case 2:
return function(value, other) {
return func.call(context, value, other);
};
case 3:
return function(value, index, collection) {
return func.call(context, value, index, collection);
};
case 4:
return function(accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
return function() {
return func.apply(context, arguments);
};
};
_w.compact = function(array) {
return _w.filter(array, _w.identity);
};
_w.filter = function(obj, predicate, context) {
var results = [];
if (obj == null) return results;
predicate = _w.iteratee(predicate, context);
_w.each(obj, function(value, index, list) {
if (predicate(value, index, list)) results.push(value);
});
return results;
};
_w.identity = function(value) {
return value;
};
_w.every = function(obj, predicate, context) {
if (obj == null) return true;
predicate = _w.iteratee(predicate, context);
var keys = obj.length !== +obj.length && _w.keys(obj),
length = (keys || obj).length,
index,
currentKey;
for (index = 0; index < length; index++) {
currentKey = keys ? keys[index] : index;
if (!predicate(obj[currentKey], currentKey, obj)) return false;
}
return true;
};
_w.union = function() {
return _w.uniq(flatten(arguments, true, true, []));
};
_w.uniq = function(array, isSorted, iteratee, context) {
if (array == null) return [];
if (!_w.isBoolean(isSorted)) {
context = iteratee;
iteratee = isSorted;
isSorted = false;
}
if (iteratee != null) iteratee = _w.iteratee(iteratee, context);
var result = [];
var seen = [];
for (var i = 0, length = array.length; i < length; i++) {
var value = array[i];
if (isSorted) {
if (!i || seen !== value) result.push(value);
seen = value;
} else if (iteratee) {
var computed = iteratee(value, i, array);
if (_w.indexOf(seen, computed) < 0) {
seen.push(computed);
result.push(value);
}
} else if (_w.indexOf(result, value) < 0) {
result.push(value);
}
}
return result;
};
_w.pick = function(obj, iteratee, context) {
var result = {}, key;
if (obj == null) return result;
if (_w.isFunction(iteratee)) {
iteratee = createCallback(iteratee, context);
for (key in obj) {
var value = obj[key];
if (iteratee(value, key, obj)) result[key] = value;
}
} else {
var keys = concat.apply([], slice.call(arguments, 1));
obj = new Object(obj);
for (var i = 0, length = keys.length; i < length; i++) {
key = keys[i];
if (key in obj) result[key] = obj[key];
}
}
return result;
};
_w.has = function(obj, key) {
return obj != null && hasOwnProperty.call(obj, key);
};
_w.keys = function(obj) {
if (!_w.isObject(obj)) return [];
if (nativeKeys) return nativeKeys(obj);
var keys = [];
for (var key in obj)
if (_w.has(obj, key)) keys.push(key);
return keys;
};
_w.contains = function(obj, target) {
if (obj == null) return false;
if (obj.length !== +obj.length) obj = _w.values(obj);
return _w.indexOf(obj, target) >= 0;
};
_w.sortedIndex = function(array, obj, iteratee, context) {
iteratee = _w.iteratee(iteratee, context, 1);
var value = iteratee(obj);
var low = 0, high = array.length;
while (low < high) {
var mid = (low + high) >>> 1;
if (iteratee(array[mid]) < value) low = mid + 1;
else high = mid;
}
return low;
};
_w.property = function(key) {
return function(obj) {
return obj[key];
};
};
_w.iteratee = function(value, context, argCount) {
if (value == null) return _w.identity;
if (_w.isFunction(value)) return createCallback(value, context, argCount);
if (_w.isObject(value)) return _w.matches(value);
return _w.property(value);
};
_w.pairs = function(obj) {
var keys = _w.keys(obj);
var length = keys.length;
var pairs = Array(length);
for (var i = 0; i < length; i++) {
pairs[i] = [keys[i], obj[keys[i]]];
}
return pairs;
};
_w.matches = function(attrs) {
var pairs = _w.pairs(attrs), length = pairs.length;
return function(obj) {
if (obj == null) return !length;
obj = new Object(obj);
for (var i = 0; i < length; i++) {
var pair = pairs[i], key = pair[0];
if (pair[1] !== obj[key] || !(key in obj)) return false;
}
return true;
};
};
_w.indexOf = function(array, item, isSorted) {
if (array == null) return -1;
var i = 0, length = array.length;
if (isSorted) {
if (typeof isSorted == 'number') {
i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted;
} else {
i = _w.sortedIndex(array, item);
return array[i] === item ? i : -1;
}
}
for (; i < length; i++)
if (array[i] === item) return i;
return -1;
};
_w.values = function(obj) {
var keys = _w.keys(obj);
var length = keys.length;
var values = Array(length);
for (var i = 0; i < length; i++) {
values[i] = obj[keys[i]];
}
return values;
};
_w.extend = function(obj) {
if (!_w.isObject(obj)) return obj;
var source, prop;
for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
if (hasOwnProperty.call(source, prop)) {
obj[prop] = source[prop];
}
}
}
return obj;
};
_w.isArray = function(obj) {
return toString.call(obj) === '[object Array]';
};
_w.isBoolean = function(obj) {
return (
obj === true || obj === false || toString.call(obj) === '[object Boolean]'
);
};
_w.isUndefined = function(obj) {
return obj === void 0;
};
_w.isObject = function(obj) {
var type = typeof obj;
return type === 'function' || (type === 'object' && !!obj);
};
_w.each = function(obj, iteratee, context) {
if (obj == null) return obj;
iteratee = createCallback(iteratee, context);
var i, length = obj.length;
if (length === +length) {
for (i = 0; i < length; i++) {
iteratee(obj[i], i, obj);
}
} else {
var keys = _w.keys(obj);
for ((i = 0), (length = keys.length); i < length; i++) {
iteratee(obj[keys[i]], keys[i], obj);
}
}
return obj;
};
_w.each(
['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'],
function(name) {
_w['is' + name] = function(obj) {
return toString.call(obj) === '[object ' + name + ']';
};
}
);
///////////////////////////////////////////////////////////
// _json (strip of the required underscore.json methods) //
///////////////////////////////////////////////////////////
var deepJSON = function(obj, key, value, remove) {
var keys = key
.replace(/\[(["']?)([^\1]+?)\1?\]/g, '.$2')
.replace(/^\./, '')
.split('.'),
root,
i = 0,
n = keys.length;
// Set deep value
if (arguments.length > 2) {
root = obj;
n--;
while (i < n) {
key = keys[i++];
obj = obj[key] = _w.isObject(obj[key]) ? obj[key] : {};
}
if (remove) {
if (_w.isArray(obj)) {
obj.splice(keys[i], 1);
} else {
delete obj[keys[i]];
}
} else {
obj[keys[i]] = value;
}
value = root;
// Get deep value
} else {
while ((obj = obj[keys[i++]]) != null && i < n) {
}
value = i < n ? void 0 : obj;
}
return value;
};
var _json = {};
_json.VERSION = '0.1.0';
_json.debug = true;
_json.exit = function(source, reason, data, value) {
if (!_json.debug) return;
var messages = {};
messages.noJSON = 'Not a JSON';
messages.noString = 'Not a String';
messages.noArray = 'Not an Array';
messages.missing = 'Missing argument';
var error = { source: source, data: data, value: value };
error.message = messages[reason]
? messages[reason]
: 'No particular reason';
console.log('Error', error);
return;
};
_json.is = function(json) {
return toString.call(json) == '[object Object]';
};
_json.isStringified = function(string) {
var test = false;
try {
test = /^[\],:{}\s]*$/.test(
string
.replace(/\\["\\\/bfnrtu]/g, '@')
.replace(
/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,
']'
)
.replace(/(?:^|:|,)(?:\s*\[)+/g, '')
);
} catch (e) {}
return test;
};
_json.get = function(json, selector) {
if (json == undefined) return _json.exit('get', 'missing', 'json', json);
if (selector == undefined)
return _json.exit('get', 'missing', 'selector', selector);
if (!_w.isString(selector))
return _json.exit('get', 'noString', 'selector', selector);
return deepJSON(json, selector);
};
_json.set = function(json, selector, value) {
if (json == undefined) return _json.exit('set', 'missing', 'json', json);
if (selector == undefined)
return _json.exit('set', 'missing', 'selector', selector);
if (!_w.isString(selector))
return _json.exit('set', 'noString', 'selector', selector);
return value
? deepJSON(json, selector, value)
: _json.remove(json, selector);
// return deepJSON(json, selector, value); // Now removes the property if the value is empty. Maybe should keep it instead?
};
_json.remove = function(json, selector) {
if (json == undefined) return _json.exit('remove', 'missing', 'json', json);
if (selector == undefined)
return _json.exit('remove', 'missing', 'selector', selector);
if (!_w.isString(selector))
return _json.exit('remove', 'noString', 'selector', selector);
return deepJSON(json, selector, null, true);
};
_json.push = function(json, selector, value, force) {
if (json == undefined) return _json.exit('push', 'missing', 'json', json);
if (selector == undefined)
return _json.exit('push', 'missing', 'selector', selector);
var array = _json.get(json, selector);
if (!_w.isArray(array)) {
if (force) {
array = [];
} else {
return _json.exit('push', 'noArray', 'array', array);
}
}
array.push(value);
return _json.set(json, selector, array);
};
_json.unshift = function(json, selector, value) {
if (json == undefined)
return _json.exit('unshift', 'missing', 'json', json);
if (selector == undefined)
return _json.exit('unshift', 'missing', 'selector', selector);
if (value == undefined)
return _json.exit('unshift', 'missing', 'value', value);
var array = _json.get(json, selector);
if (!_w.isArray(array))
return _json.exit('unshift', 'noArray', 'array', array);
array.unshift(value);
return _json.set(json, selector, array);
};
_json.flatten = function(json) {
if (json.constructor.name != 'Object')
return _json.exit('flatten', 'noJSON', 'json', json);
var result = {};
function recurse(cur, prop) {
if (Object(cur) !== cur) {
result[prop] = cur;
} else if (Array.isArray(cur)) {
for (var i = 0, l = cur.length; i < l; i++) {
recurse(cur[i], prop ? prop + '.' + i : '' + i);
if (l == 0) result[prop] = [];
}
} else {
var isEmpty = true;
for (var p in cur) {
isEmpty = false;
recurse(cur[p], prop ? prop + '.' + p : p);
}
if (isEmpty) result[prop] = {};
}
}
recurse(json, '');
return result;
};
_json.unflatten = function(data) {
if (Object(data) !== data || Array.isArray(data)) return data;
var result = {}, cur, prop, idx, last, temp;
for (var p in data) {
(cur = result), (prop = ''), (last = 0);
do {
idx = p.indexOf('.', last);
temp = p.substring(last, idx !== -1 ? idx : undefined);
cur = cur[prop] || (cur[prop] = !isNaN(parseInt(temp)) ? [] : {});
prop = temp;
last = idx + 1;
} while (idx >= 0);
cur[prop] = data[p];
}
return result[''];
};
_json.prettyprint = function(json) {
return JSON.stringify(json, undefined, 2);
};
//////////////////////////////////////////
// wQuery (mini replacement for jQuery) //
//////////////////////////////////////////
var wQuery = function() {};
wQuery.constructor = wQuery;
wQuery.prototype.dom = function(selector, createOptions) {
var self = this, elements = [];
if (createOptions) {
var element = document.createElement(selector);
for (var k in createOptions) {
element[k] = createOptions[k];
}
} else {
if (_w.isString(selector)) {
elements = [].slice.call(document.querySelectorAll(selector));
} else {
if (_w.isObject(selector) && selector.attributes) {
elements = [selector];
}
}
self._elements = elements;
self.length = elements.length;
return self;
}
};
wQuery.prototype.on = function(events, fn) {
var self = this, elements = self._elements;
events = events.split(' ');
for (var i = 0, lenEl = elements.length; i < lenEl; i++) {
var element = elements[i];
for (var j = 0, lenEv = events.length; j < lenEv; j++) {
if (element.addEventListener) {
element.addEventListener(events[j], fn, false);
}
}
}
};
wQuery.prototype.find = function(selector) {
var self = this, element = self.get(0), elements = [];
if (_w.isString(selector)) {
elements = [].slice.call(element.querySelectorAll(selector));
}
self._elements = elements;
return self;
};
wQuery.prototype.get = function(index, chain) {
var self = this,
elements = self._elements || [],
element = elements[index] || {};
if (chain) {
self._element = element;
return self;
} else {
return _w.isNumber(index) ? element : elements;
}
};
wQuery.prototype.reverse = function() {
this._elements = this._elements.reverse();
return this;
};
wQuery.prototype.val = function(value) {
return this.prop('value', value);
};
wQuery.prototype.type = function(value) {
return this.prop('type', value);
};
wQuery.prototype.html = function(value) {
return this.prop('innerHTML', value);
};
wQuery.prototype.text = function(value) {
return this.prop('innerHTML', escapeHTML(value));
};
wQuery.prototype.prop = function(prop, value) {
var self = this, elements = self._elements;
for (var i in elements) {
if (_w.isUndefined(value)) {
return elements[i][prop];
} else {
elements[i][prop] = value;
}
}
};
wQuery.prototype.attr = function(attr, value) {
var self = this, elements = self._elements;
for (var i in elements) {
if (value === undefined) {
return elements[i].getAttribute(attr);
} else {
elements[i].setAttribute(attr, value);
}
}
return self;
};
wQuery.prototype.removeAttr = function(attr) {
var self = this;
for (var i in self._elements)
self._elements[i].removeAttribute(attr);
return self;
};
wQuery.prototype.addClass = function(c) {
var self = this;
for (var i in self._elements)
self._elements[i].classList.add(c);
return self;
};
wQuery.prototype.removeClass = function(c) {
var self = this;
for (var i in self._elements)
self._elements[i].classList.remove(c);
return self;
};
wQuery.prototype.parents = function(selector) {
var self = this,
element = self.get(0),
parent = element.parentNode,
parents = [];
while (parent !== null) {
var o = parent,
matches = matchesSelector(o, selector),
isNotDomRoot = o.doctype === undefined ? true : false;
if (!selector) {
matches = true;
}
if (matches && isNotDomRoot) {
parents.push(o);
}
parent = o.parentNode;
}
self._elements = parents;
return self;
};
wQuery.prototype.parent = function(selector) {
var self = this,
element = self.get(0),
o = element.parentNode,
matches = matchesSelector(o, selector);
if (!selector) {
matches = true;
}
return matches ? o : {};
};
wQuery.prototype.clone = function(chain) {
var self = this, element = self.get(0), clone = element.cloneNode(true);
self._elements = [clone];
return chain ? self : clone;
};
wQuery.prototype.empty = function(chain) {
var self = this, element = self.get(0);
if (!element || !element.hasChildNodes) {
return chain ? self : element;
}
while (element.hasChildNodes()) {
element.removeChild(element.lastChild);
}
return chain ? self : element;
};
wQuery.prototype.replaceWith = function(newDOM) {
var self = this, oldDOM = self.get(0), parent = oldDOM.parentNode;
parent.replaceChild(newDOM, oldDOM);
};
wQuery.prototype.ready = function(callback) {
if (document && _w.isFunction(document.addEventListener)) {
document.addEventListener('DOMContentLoaded', callback, false);
} else if (window && _w.isFunction(window.addEventListener)) {
window.addEventListener('load', callback, false);
} else {
document.onreadystatechange = function() {
if (document.readyState === 'complete') {
callback();
}
};
}
};
//////////////////////
// WATCH DOM EVENTS //
//////////////////////
way = new WAY();
var timeoutInput = null;
var eventInputChange = function(e) {
if (timeoutInput) {
clearTimeout(timeoutInput);
}
timeoutInput = setTimeout(function() {
var element = w.dom(e.target).get(0);
way.dom(element).toStorage();
}, way.options.timeout);
};
var eventClear = function(e) {
e.preventDefault();
var options = way.dom(this).getOptions();
way.remove(options.data, options);
};
var eventPush = function(e) {
e.preventDefault();
var options = way.dom(this).getOptions();
if (!options || !options['action-push']) {
return false;
}
var split = options['action-push'].split(':'),
selector = split[0] || null,
value = split[1] || null;
way.push(selector, value, options);
};
var eventRemove = function(e) {
e.preventDefault();
var options = way.dom(this).getOptions();
if (!options || !options['action-remove']) {
return false;
}
way.remove(options['action-remove'], options);
};
var timeoutDOM = null;
var eventDOMChange = function() {
// We need to register dynamically added bindings so we do it by watching DOM changes
// We use a timeout since "DOMSubtreeModified" gets triggered on every change in the DOM (even input value changes)
// so we can limit the number of scans when a user is typing something
if (timeoutDOM) {
clearTimeout(timeoutDOM);
}
timeoutDOM = setTimeout(function() {
way.registerDependencies();
setEventListeners();
}, way.options.timeoutDOM);
};
//////////////
// INITIATE //
//////////////
w = new wQuery();
way.w = w;
var setEventListeners = function() {
w.dom('body').on('DOMSubtreeModified', eventDOMChange);
w.dom('[' + tagPrefix + '-data]').on('input change', eventInputChange);
w.dom('[' + tagPrefix + '-clear]').on('click', eventClear);
w.dom('[' + tagPrefix + '-action-remove]').on('click', eventRemove);
w.dom('[' + tagPrefix + '-action-push]').on('click', eventPush);
};
var eventInit = function() {
setEventListeners();
way.restore();
way.setDefaults();
way.registerDependencies();
way.updateDependencies();
};
w.ready(eventInit);
return way;
});