/**
* @license
* BSD 3-Clause License
*
* Copyright (c) 2019, Alexis Puga Ruíz
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @preserve © 2019-2022 {@link https://github.com/AlexisPuga|Alexis Puga} and {@link https://github.com/justjs/just/contributors|contributors}. {@link https://github.com/justjs/just|JustJs} is released under the {@link https://github.com/justjs/just/blob/master/LICENSE|BSD-3-Clause License}.
* @file Full suite for browser environments. Includes core utilities, {@link just.View} & {@link just.Router}.
* @version 1.2.0
*/
(function (fn) {
if (typeof define === 'function' && define.amd) { define('just', [], fn); }
else if (typeof module === 'object' && module.exports) { module.exports = fn(); }
else { this.just = fn(); }
}).call(this, function () {
'use strict';
/* eslint-disable no-unused-vars */
/**
* An absolute, relative or blob url.
*
* @typedef {string} url
* @global
*/
/**
* The full parts of an url.
*
* @typedef {Object} url_parts
* @property {string} protocol - A protocol (including ":", like "ftp:") or ":".
* @property {string} href - An absolute url (like "ftp://username:password@www.example.com:80/a?b=1#c").
* @property {string} host - The host (like "www.example.com:80") or an empty string.
* @property {string} hostname - A hostname (like "www.example.com").
* @property {string} port - The GIVEN port as a number (like "80") or an empty string.
* @property {string} pathname - A pathname (like "/a").
* @property {string} origin - The origin (like "ftp://www.example.com").
* @property {string} search - The query arguments (including "?", like "?b=1") or an empty string.
* @property {string} hash - The hash (including '#', like "#c") or an empty string.
* @property {string} username - The given username or an empty string.
* @property {string} password - The given password or an empty string.
*/
/**
* Same as the second param for <var>Object.defineProperties</var>
*
* @typedef {!object} propertiesDescriptor
* @global
*/
/**
* Same as the third param for <var>Object.defineProperty</var>
*
* @typedef {!object} propertyDescriptor
* @global
*/
/**
* A tagName of an Element (such as "link").
*
* @typedef {string} element_tag
*/
/**
* @mixin just
* @global
*/
var just = {};
/**
* Register non-writable, non-enumerable and non-configurable members on Just.
* @package
*/
var set = function registerMember (name, value) { Object.defineProperty(just, name, {'value': value}); };
set('register', set);
/**
* Same as <var>Array.prototype.reduce</var>, but for
* the own enumerable properties of <var>object</var>,
* and with an extra param (<var>thisArg</var>).<br>
*
* <aside class='note'>
* <h3>A few things to consider:</h3>
* <p>Don't trust the order of the properties.</p>
* <p>Instead of using an index for the 3rd argument, we use the current <var>key</var>.</p>
* </aside>
*
* @namespace
* @memberof just
* @param {object} object - The target object.
* @param {function} fn - The transform function. It's called with the same arguments as the <var>Array.prototype.reduce</var> function.
* @param {*} accumulator - The initial value for the accumulator.
* @param {*} thisArg - <var>this</var> argument for <var>fn</var>.
* @returns <var>accumulator</var>
* @example <caption>Get object keys (when Object.keys is unavailable).</caption>
* just.reduce({'a': 1}, function (keys, value, key, object) { return keys.concat(key); }, []);
* // > ['a']
*/
function reduce (object, fn, accumulator, thisArg) {
var hasOwnProperty = Object.prototype.hasOwnProperty;
var target = Object(object);
var key, value;
for (key in target) {
if (hasOwnProperty.call(target, key)) {
value = target[key];
accumulator = fn.call(thisArg, accumulator, value, key, target);
}
}
return accumulator;
}
set('reduce', reduce);
var hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* Same as <var>Object.assign</var>, but for ES5 environments.<br>
*
* <aside class='note'>
* <h3>A few things to consider</h3>
* <p>This is most likely to be removed in the future, if we
* decide to transpile our code and use the spread sintax instead.</p>
* <p>This will be used internally, instead of <var>Object.assign</var>,
* to prevent users from loading a polyfill. So, it's most likely
* that it will be included in your final bundle.</p>
* </aside>
*
* @namespace
* @memberof just
* @param {object} target - What to apply the sources' properties to.
* @param {?object} [...sources] - Objects containing the properties you want to apply.
* @throws if <var>target</var> is null or not an object.
* @returns {object} <var>target</var>.
*/
function assign (target/*, ...sources */) {
'use strict'; // Make non writable properties throwable.
var to, i, f, from, key;
if (target === null || target === void 0) {
throw new TypeError('can\'t convert ' + target + ' to object');
}
to = Object(target);
for (i = 1, f = arguments.length; i < f; i++) {
from = Object(arguments[i]);
for (key in from) {
if (hasOwnProperty.call(from, key)) {
to[key] = from[key];
}
}
}
return to;
}
set('assign', assign);
/**
* Show a warning in the console, or throw an Error if called.
*
* @since 1.0.0-rc.23
* @namespace
* @memberof just
* @param {string} member - The member's name. E.g.: ".deprecate()"
* @param {string} type - "warning" or "error". Otherwise it logs the error using console.error().
* @param {?object} options
* @param {?string} options.since - Some string.
* @param {?string} options.message - A message to append to the default message.
* @throws {Error} If <var>type</var> is set to "error".
*/
function deprecate (member, type, options) {
var opts = assign({}, options || {});
var message = ('just' + member + ' is deprecated' +
(opts.since ? ' since ' + opts.since : '') + '. ' +
(opts.message || '')
).trim();
if (type === 'warning') { console.warn(message); }
else if (type === 'error') { throw new Error(message); }
else { console.error(message); }
}
set('deprecate', deprecate);
/**
* The given object (if <var>mutate</var> evals to `true`)
* or a copy of each own property of the given object.
*
* @typedef {!object} just.access~handler_this
*/
/**
* A function to call when {@link just.access} reaches the deep property of an object.
*
* @typedef {function} just.access~handler
* @this just.access~handler_this
* @param {!object} lastObject - The object containing the <var>lastKey</var>.
* @param {string} lastKey - The last value given in <var>path</var>.
* @param {boolean} hasProperty - `false` if some key of <var>path</var>
* was created, `true` otherwise.
* @param {string[]} path - The given keys.
* @return {*} The return value for {@link just.access|the main function}.
*/
/**
* Accesses to a deep property in a new <var>object</var>
* (or <var>object</var> if <var>mutate</var> evals to `true`).
*
* @namespace
* @memberof just
* @param {!object} object - The base object.
* @param {string[]} [path=[path]] - The ordered keys.
* @param {just.access~handler} [handler=returnValue] - A custom function.
* @param {object} opts
* @param {boolean} [opts.mutate=false] - If `true`, it will use
* the given object as the base object, otherwise it will
* copy all the owned properties to a new object.
* @param {boolean} [opts.override=true] - If `true`, and the
* current value is different to `null` or `undefined`,
* the function will throw a TypeError.
* If `false`, the current value will be overriden by
* an empty object if it's not an object nor `undefined`.
*
* @throws {TypeError} If some property causes access problems.
*
* @example <caption>Accessing to some existent property</caption>
* just.access({a: {b: {c: {d: 4}}}}, ['a', 'b', 'c', 'd'], function (currentObject, currentKey, hasProperty, path) {
* return hasProperty ? currentObject[currentKey] : null;
* }); // returns 4.
*
* @example <caption>Accessing to some property with a non-JSON-like-object as a value</caption>
* just.access({a: 1}, ['a', 'b', 'c']); // throws TypeError.
* just.access({a: 1}, ['a', 'b', 'c'], null, {
* 'override': true
* }); // returns undefined.
* // Doesn't throw because it replaces `1` with an empty object
* // and keeps accessing to the next properties.
*
* @example <caption>Accessing to some non-existent property</caption>
* var obj = {z: 1, prototype: [...]};
* var newObj = just.access(obj, 'a.b.c'.split('.'), function (currentObject, currentKey, hasProperty, path) {
*
* if (!hasProperty) {
* currentObject[currentKey] = path.length;
* }
*
* // At this point:
* // `obj` is {z: 1},
* // `currentObject` has a value in `currentKey`,
* // and `this` has all the added keys (even the ones modified in `currentObject`).
* return this;
*
* }); // returns {z: 1, a: {b: {c: 3}}}
*
* // if you want the prototype chain of obj, just copy it.
* just.assign(newObj.prototype, obj.prototype);
*
* @example <caption>Modifying the base object</caption>
* var obj = {a: {b: false}, b: {b: false}, prototype: [...]};
*
* just.access(obj, 'a.b'.split('.'), function (currentObject, currentKey, hasProperty, path) {
* currentObject[currentKey] = 2;
* }, true);
*
* // now `obj` is {a: {a: true}, b: {b: true}, prototype: [...]}.
*
* @return {*} If <var>handler</var> is given: the returned value of that function,
* otherwise: the last value of <var>path</var> in the copied object.
*/
function access (object, path, handler, opts) {
var options = assign({
'override': true,
'mutate': false
}, opts);
var properties = (Array.isArray(path)
? path
: [path]
);
var initialObject = (options.mutate
? object
: assign({}, object)
);
var currentObject = initialObject;
var isNewProperty = false;
var lastKey = properties[properties.length - 1];
var hasProperty;
properties.slice(0, -1).forEach(function (key, i) {
var value = currentObject[key];
if (!(value instanceof Object)) {
if (typeof value !== 'undefined' && value !== null && !options.override) {
throw new TypeError(key + ' is not an object.');
}
isNewProperty = true;
currentObject[key] = value = {};
}
currentObject = value;
});
hasProperty = lastKey in currentObject && !isNewProperty;
return (handler
? handler.call(initialObject, currentObject, lastKey, hasProperty, properties)
: currentObject[lastKey]
);
}
set('access', access);
/**
* @mixin just
* @borrows just.access as prop
*/
var prop = access;
set('prop', prop);
/**
* Checks if <var>value</var> looks like the other values.
*
* @namespace
* @memberof just
* @param {*} value - Comparison value.
* @param {...*} [otherValues] - Values to check against.
*
* @example
* just.check(null, {}, "null", []); // false. Neither is `null`.
* just.check({}, [], {}); // true. {} is {}.
*
* @return {boolean} `true` if some other value looks like <var>value</var>.
*/
function check (value, otherValues) {
var exceptFirstArg = [].slice.call(arguments, 1);
return exceptFirstArg.some(function checkAgainst (arg) {
return ([arg, value].some(function hasNoProps (v) { return v === null || v === void 0; })
? arg === value
: arg.constructor === value.constructor
);
});
}
defineProperties(check, /** @lends just.check */{
/**
* A custom message to throw.
*
* @typedef {string} just.check~throwable_message
*/
/**
* A function that {@link just.check|checks} a value against others and
* throws if the result is `false`.
*
* @function
* @deprecated Since 1.0.0-rc.24
* @this just.check~throwable_message
* @param {*} value - Comparison value.
* @param {...*} [otherValues] - Values to check against.
*
* @throws {TypeError} If <var>check</var> returns `false`.
* @returns {value} <var>value</var> if <var>check</var> returns `true`.
*/
'throwable': function (value, otherValues) {
var args = [].slice.call(arguments);
var throwableMessage = this;
deprecate('.check.throwable()', 'warning', {
'since': '1.0.0-rc.24'
});
if (!check.apply(this, args)) {
if (!(throwableMessage instanceof String)) {
throwableMessage = (value
+ ' must be like one of the following values: '
+ args.slice(1).map(function (v) { return v + ''; }).join(', ')
);
}
throw new TypeError(throwableMessage);
}
return value;
}
});
set('check', check);
/**
* @mixin just
* @borrows just.check as is
*/
var is = check;
set('is', is);
/**
* Chainable methods for the classList property.
*
* @namespace
* @memberof just
*
* @constructor
* @param {Element} element - The target.
*
* @example
* let force;
*
* just.ClassList(button)
* .add('a', 'b', 'c')
* .remove('b')
* .toggle('c', (force = true))
* .replace('a', 'z')
* .contains('b'); // false
*/
function ClassList (element) {
if (!(this instanceof ClassList)) { return new ClassList(element); }
defineProperties(this, /** @lends just.ClassList# */{
/** @member {!Element} */
'element': element
});
}
defineProperties(ClassList, /** @lends just.ClassList */{
/**
* Simulate Element.classList.prototype.method.apply(element, args)
* since it's not possible to call a classList-method that way.
*
* @param {Element} element - The target.
* @param {string} methodName - The name of the classList method to call.
* @param {array[]|*} [methodArgs=[methodArgs]] - Arguments for the classList method.
* @return Whatever the method returns.
*
* @example
* ClassList.apply(this, 'add', ['x', 'b']); // > undefined
* ClassList.apply(this, 'remove', 'c'); // > undefined
* ClassList.apply(this, 'toggle', ['a', true]); // > true
*/
'apply': function (element, methodName, methodArgs) {
var args = (typeof methodArgs === 'number'
? [methodArgs]
: [].slice.call(methodArgs)
);
var classList = element.classList;
if (/(?:add|remove)/.test(methodName)) {
args.forEach(function (arg) { classList[methodName](arg); });
/** These methods return undefined. */
return void 0;
}
/*
* Passing undefined arguments instead of manually
* adding more conditionals to call the method with
* the correct amount shouldn't be a problem.
*
* I.e:
* classList.contains('a', undefined);
* classList.contains('a', 'some other value');
*
* Should be the same as calling...
* classList.contains('a');
*/
return classList[methodName](args[0], args[1]);
}
});
defineProperties(ClassList.prototype, /** @lends just.ClassList.prototype */{
/**
* @alias Element.classList.add
* @chainable
*/
'add': function () {
ClassList.apply(this.element, 'add', arguments);
return this;
},
/**
* @alias Element.classList.remove
* @chainable
*/
'remove': function () {
ClassList.apply(this.element, 'remove', arguments);
return this;
},
/**
* @alias Element.classList.toggle
* @chainable
*/
'toggle': function () {
ClassList.apply(this.element, 'toggle', arguments);
return this;
},
/**
* @alias Element.classList.replace
* @chainable
*/
'replace': function () {
ClassList.apply(this.element, 'replace', arguments);
return this;
},
/**
* @alias Element.classList.contains
* @return {boolean}
*/
'contains': function () { return ClassList.apply(this.element, 'contains', arguments); },
/**
* @alias Element.classList.item
* @return {?string}
*/
'item': function () { return ClassList.apply(this.element, 'item', arguments); }
});
set('ClassList', ClassList);
/**
* Defaults to <var>defaultValue</var> if <var>value</var> looks like
* <var>defaultValue</var>.
*
* @namespace
* @memberof just
* @param {*} value - Any value.
* @param {*} [defaultValue] - A value with a desired type for <var>value</var>.
* If an object literal is given, all the keys of <var>value</var> will <var>default</var>
* to his corresponding key in this object.
* @param {object} opts - Some options.
* @param {boolean} [opts.ignoreDefaultKeys=false] - If `false` and <var>defaultValue</var>
* is an object literal, the default keys will be added to <var>value</var>
* or checked against this function for each repeated key.
* @param {boolean} [opts.checkLooks=true]
* If `true`:
* `[]` will match ONLY with another Array.
* `{}` will match ONLY with another object literal.
* If `false`
* `[]` and `{}` will match with any other object.
* @param {boolean} [opts.checkDeepLooks=true]
* Same as <var>checkLooks</var> but it works with the inner values
* of the objects.
* @param {boolean} [opts.ignoreNull=false]
* If `true`, <var>defaultValue</var>s with null as a value won't be checked
* and any <var>value</var> (except `undefined`) will be allowed.
*
* @example
* just.defaults([1, 2], {a: 1}); // {a: 1}
*
* @example
* just.defaults({}, null); // null: null is not an object literal.
* just.defaults([], null, {'checkLooks': false}); // []: null is an object.
* just.defaults(null, {}); // {}: null is not an object literal.
* just.defaults(null, []); // []: null is not an Array.
*
* @example
* just.defaults(1, NaN); // 1 (NaN is an instance of a Number)
*
* @example
* just.defaults({'a': 1, 'b': 2}, {'a': 'some string'}, {'ignoreDefaultKeys': false}); // {'a': 'some string', 'b': 2}
*
* @example
* just.defaults({'a': 1}, {'b': 2}, {'ignoreDefaultKeys': false}); // {'a': 1, 'b': 2}
* just.defaults({'a': 1}, {'b': 2}, {'ignoreDefaultKeys': true}); // {'a': 1}
*
* @example
* just.defaults(1, null, {'ignoreNull': false}) // null (1 is not an object)
* just.defaults(1, null, {'ignoreNull': true}) // 1
* just.defaults(undefined, null, {'ignoreNull': true}) // null
* just.defaults({a: 1}, {a: null}, {'ignoreNull': true}) // {a: 1}
*
* @returns {value} <var>value</var> if it looks like <var>defaultValue</var> or <var>defaultValue</var> otherwise.
*/
function defaults (value, defaultValue, opts) {
var options = assign({
'ignoreDefaultKeys': false,
'checkLooks': true,
'checkDeepLooks': true,
'ignoreNull': false
}, opts);
if (options.ignoreNull && defaultValue === null && value !== void 0) { return value; }
if (options.checkLooks) {
if (!check(value, defaultValue)) { return defaultValue; }
if (check(value, {}) && options.checkDeepLooks) {
eachProperty(defaultValue, function (v, k) {
if (typeof value[k] !== 'undefined') {
value[k] = defaults(value[k], v, options);
}
else if (!options.ignoreDefaultKeys) {
value[k] = v;
}
});
}
return value;
}
return (typeof value === typeof defaultValue
? value
: defaultValue
);
}
set('defaults', defaults);
/**
* @mixin just
* @borrows just.defaults as from
*/
var from = defaults;
set('from', from);
/**
* Alternative to <var>Object.defineProperties</var>.
*
* @see {@link defineProperty} for more details.
* @namespace
* @memberof just
* @param {!object} object
* @param {!object.<key, propertyDescriptor>|!object.<key, value>} properties
* @return <var>object</var>
*/
function defineProperties (object, properties) {
eachProperty(properties, function define (value, key) {
defineProperty(this, key, value);
}, object);
return object;
}
set('defineProperties', defineProperties);
/**
* @mixin just
* @borrows just.defineProperties as defProps
*/
var defProps = defineProperties;
set('defProps', defProps);
/**
* Alternative to <var>Object.defineProperty</var> with more enhancements.
*
* <br>
* <aside class='note'>
* <h3>A few things to consider (see examples):</h3>
* <ul>
* <li>If <var>object</var> contains invalid {@link propertyDescriptor|property descriptor attributes},
* the value WON'T be used as a property descriptor.</li>
* <li>Empty objects will be considered values.</li>
* </ul>
* </aside>
*
* @namespace
* @memberof just
* @throws <var>Object.defineProperty</var> exceptions.
* @param {!object} object - The target.
* @param {string} key - Key for the property.
* @param {!object} [value={'value': value}] - A {@link propertyDescriptor|property descriptor} or some value.
* @example <caption>Define a property using a value.</caption>
* just.defineProperty({}, 'a', 1); // Same as Object.defineProperty({}, 'a', {'value': 1})
*
* @example <caption>Define a property using a {@link propertyDescriptor|property descriptor}.</caption>
* just.defineProperty({}, 'a', {'writable': true}); // Same as Object.defineProperty({}, 'a', {'writable': true})
*
* @example <caption>Define a property with an empty object.</caption>
* just.defineProperty({}, 'a', {}); // Same as Object.defineProperty({}, 'a', {'value': {}});
*
* @return <var>object</var>.
*/
function defineProperty (object, key, value) {
var descriptor = Object(value);
var defaultDescriptors = ['value', 'writable', 'get', 'set', 'configurable', 'enumerable'];
var descriptorKeys = reduce(descriptor, function (keys, value, key) { return keys.concat(key); }, []);
var isEmptyObject = !descriptorKeys.length;
var notADescriptor = isEmptyObject || descriptorKeys.some(
function notInDescriptors (key) { return this.indexOf(key) === -1; },
defaultDescriptors
);
if (notADescriptor) {
descriptor = {
'value': value
};
}
Object.defineProperty(object, key, descriptor);
return object;
}
set('defineProperty', defineProperty);
/**
* @mixin just
* @borrows just.defineProperty as defProp
*/
var defProp = defineProperty;
set('defProp', defProp);
/**
* @typedef {function} just.eachProperty~fn
*
* @this {@link just.eachProperty|<var>thisArg</var> from the main function}.
*
* @param {*} value - The current value.
* @param {*} key - The current key.
* @param {!object} object - The current object being iterated.
*
* @return {boolean} If `true`, the current loop will stop.
*/
/**
* Converts <var>object</var> to an Object, iterates over it,
* calls a function on each iteration, and if a truthy value
* is returned from that function, the loop will stop.
*
* @namespace
* @memberof just
* @param {*} object - Some value.
* @param {just.eachProperty~fn} fn - The function that will be
* called on each iteration.
* @param {*} [thisArg] - <var>this</var> for <var>fn</var>.
* @param {object} opts - Some options.
* @param {boolean} [opts.addNonOwned=false] - Include non-owned properties.
* `false`: iterate only the owned properties.
* `true`: iterate the (enumerable) inherited properties too.
*
* @throws {TypeError} If <var>fn</var> is not a function.
* @return {boolean} `true` if the function was interrupted, `false` otherwise.
*/
function eachProperty (object, fn, thisArg, opts) {
var properties = Object(object);
var options = assign({
'addNonOwned': false
}, opts);
var wasInterrupted = false;
var key;
if (typeof fn !== 'function') { throw new TypeError(fn + ' is not a function.'); }
for (key in properties) {
if (wasInterrupted) { break; }
if (options.addNonOwned || ({}).hasOwnProperty.call(properties, key)) {
wasInterrupted = Boolean(fn.call(thisArg, properties[key], key, properties));
}
}
return wasInterrupted;
}
set('eachProperty', eachProperty);
/**
* @mixin just
* @borrows just.eachProperty as eachProp
*/
var eachProp = eachProperty;
set('eachProp', eachProp);
/**
* Gets DOM Elements by a CSS Selector and always
* returns an array.
*
* @namespace
* @memberof just
* @param {DOMString} selector - A CSS selector.
* @param {Node} [parent=document] - The parent node.
*
* @return {!Array} The found elements.
*/
function findElements (selector, parent) {
return [].slice.call((parent || document).querySelectorAll(selector));
}
set('findElements', findElements);
/**
* @mixin just
* @borrows just.findElements as el
*/
var el = findElements;
set('el', el);
/**
* A function that checks if <var>this</var> is the Node that you're looking for.
*
* @typedef {function} just.getRemoteParent~fn
*
* @this Node
* @param {!Number} deepLevel - A counter that indicates how many elements have checked.
* @param {Node} rootContainer - The root container.
*
* @return {boolean}
*/
/**
* Goes up through the <var>childNode</var> parents, until <var>fn</var> returns `true`
* or a non-Node is found.
*
* @namespace
* @memberof just
* @param {Node} childNode - Some child.
* @param {just.getRemoteParent~fn} fn - Some custom handler.
* @param {Node} [rootContainer=html] - The farthest parent.
* @param {boolean} [includeChild=false] - If `true`, it calls <var>fn</var> with <var>childNode</var> too.
*
* @example
* just.getRemoteParent(just.body, function () {
* return this.tagName === 'HTML';
* }); // returns the <html> Element.
*
* @return {Node|null} The current Node when <var>fn</var> returns `true`.
*/
function getRemoteParent (childNode, fn, rootContainer, includeChild) {
var currentNode = childNode;
var deepLevel = 0;
if (!(childNode instanceof Node)) { throw new TypeError(childNode + ' is not a Node.'); }
if (!(rootContainer instanceof Node)) { rootContainer = document.documentElement; }
while (currentNode) {
if ((deepLevel > 0 || includeChild)
&& fn.call(currentNode, deepLevel, rootContainer)) {
return currentNode;
}
if (currentNode === rootContainer) { return null; }
currentNode = currentNode.parentNode;
deepLevel++;
}
return null;
}
set('getRemoteParent', getRemoteParent);
/**
* @mixin just
* @borrows just.access as parent
*/
var parent = getRemoteParent;
set('parent', parent);
/**
* Checks if an object has no direct keys.
*
* @namespace
* @memberof just
* @param {*} [object=Object(object)] - Some object.
* @return {boolean} `true` if the object doesn't contain owned properties,
* `false` otherwise.
*/
function isEmptyObject (object) {
return !eachProperty(object, function () { return true; });
}
set('isEmptyObject', isEmptyObject);
/**
* @mixin just
* @borrows just.isEmptyObject as emptyObj
*/
var emptyObj = isEmptyObject;
set('emptyObj', emptyObj);
/* global DocumentTouch */
/**
* Checks if the screen <em>supports</em> touch.
*
* @namespace
* @memberof just
* @return {boolean}
*/
function isTouchSupported () {
return Boolean('ontouchstart' in document.body
|| window.navigator.maxTouchPoints > 0
|| window.navigator.msMaxTouchPoints > 0
|| 'DocumentTouch' in window
&& document instanceof DocumentTouch
);
}
set('isTouchSupported', isTouchSupported);
/**
* @mixin just
* @borrows just.isTouchSupported as supportsTouch
*/
var supportsTouch = isTouchSupported;
set('supportsTouch', supportsTouch);
/**
* Checks if an object is the window global by checking <var>window</var> or
* some common properties of <var>window</var>.
*
* @namespace
* @memberof just
* @deprecated Since 1.0.0-rc.24
* @param {*} object - Some object.
* @return {boolean} `true` if <var>object</var> is <var>window</var> or contains the common properties,
* `false` otherwise.
*/
function isWindow (object) {
deprecate('.isWindow()', 'warning', {
'since': '1.0.0-rc.24'
});
return Boolean(object === window
|| object instanceof Object
&& object.document
&& object.setInterval
);
}
set('isWindow', isWindow);
/**
* A listener for the "onload" or "onerror" events.
*
* @typedef {function} just.loadElement~listener
*
* @this Element
* @param {!Event} event - The triggered event.
* @return {*}
*/
/**
* A custom function to append the created element.
*
* @typedef {function} just.loadElement~handler
* @this {!Element} - The element that loads the url.
* @param {?Element} loadedElement - An identical element that has been loaded previously.
* @param {url} url - The given url to load.
*
* @return {*} Some value.
*/
/**
* Loads an external file if no other similar element is
* found.
*
* @namespace
* @memberof just
* @throws document.createElement exception or TypeError if <var>url</var> is missing.
* @param {!element_tag} tagName - A tag name.
* @param {?url|?object} [properties] - The url of the file or the properties for the new element.
* @param {?Node|just.loadElement~handler} [container=document.head]
* A custom function to append the element by yourself or a Node
* to append the created element to it.
* @param {just.loadElement~listener} [listener] - A function to trigger on element load/error.
*
* @return {Element|*} The created element, a similar element, or the returned value of
* {@link just.loadElement~handler|handler}.
*/
function loadElement (tagName, properties, listener, container) {
var props = Object(typeof properties === 'object' ? properties : null);
var urlProperty = /link/i.test(tagName) ? 'href' : 'src';
var url = typeof arguments[1] === 'string' ? (props[urlProperty] = arguments[1]) : props[urlProperty];
var isCrossOriginRequest = parseUrl(url).origin !== window.location.origin;
var needsCrossOriginProperty = !('crossOrigin' in props) && ['video', 'img', 'script', 'link'].indexOf(tagName) !== -1;
var isLinkElement = /link/i.test(tagName);
var needsRelProperty = !('rel' in props);
var similarElementSelector = tagName + '[' + urlProperty + '="' + (url || '') + '"]';
var similarElement = findElements(similarElementSelector)[0] || null;
var element = createElement(tagName, props);
var listenerWrapper = function listenerWrapper (e) {
removeEventListener(this, ['load', 'error'], listenerWrapper);
return listener.call(this, e);
};
var invalidUrl = typeof url !== 'string' || url.trim() === '';
if (invalidUrl) { throw new TypeError(url + ' is not valid url.'); }
if (listener) { addEventListener(element, ['load', 'error'], listenerWrapper); }
if (isCrossOriginRequest && needsCrossOriginProperty) { element.crossOrigin = 'anonymous'; }
if (isLinkElement && needsRelProperty) { element.rel = 'stylesheet'; }
if (typeof container === 'function') { return container.call(element, similarElement, url); }
/*else */if (similarElement) { return similarElement; }
/*else */if (!container) { container = document.head; }
container.appendChild(element);
return element;
}
set('loadElement', loadElement);
/**
* @mixin just
* @borrows just.loadElement as load
*/
var load = loadElement;
set('load', load);
/**
* A mixin of properties that access to some kind of storage
* in the browser.
*
* @class
* @memberof just
* @param {boolean} [consent=false] - A boolean indicating that
* the user allowed the access to some kind of local storage.
* @param {boolean} [isExplicit=typeof consent !== 'undefined'] -
* A value to indicate if the given consent was specified by the
* user.
*/
function LocalStorage (consent, isExplicit) {
if (!(this instanceof LocalStorage)) { return new LocalStorage(consent, isExplicit); }
defineProperties(this, {
'consent': Boolean(consent),
'isExplicit': defaults(isExplicit, typeof consent !== 'undefined')
});
}
defineProperties(LocalStorage, /** @lends just.LocalStorage */{
/**
* The DoNotTrack header formatted as `true`, `false` or `undefined`
* (for "unspecified").
*
* @static
* @type {boolean|undefined}
*/
'DNT': {
'get': function DNT () {
var dnt = [navigator.doNotTrack, navigator.msDoNotTrack, window.doNotTrack];
var consent = ',' + dnt + ',';
return (/,(yes|1),/i.test(consent)
? true
: /,(no|0),/i.test(consent)
? false
: void 0
);
}
},
/**
* Checks if <var>cookie</var> is in <var>document.cookie</var>.
*
* @function
* @static
* @param {string} cookie - The name of the cookie or the cookie itself.
*
* @example
* document.cookie += 'a=b; c=d;';
* cookieExists('a'); // true
* cookieExists('b'); // false
* cookieExists('a=b'); // true
* cookieExists('a=d'); // false
*
* @return {boolean} `true` if it exists, `false` otherwise.
* @readOnly
*/
'cookieExists': function cookieExists (cookie) {
return new RegExp('; ' + cookie + '(=|;)').test('; ' + document.cookie + ';');
},
/**
* Returns a cookie from <var>document.cookie</var>.
*
* @function
* @static
* @param {string} name - The cookie name.
*
* @return {string|null} The cookie if it exists or null.
* @readOnly
*/
'getCookie': function getCookie (name) {
return (!/=/.test(name) && LocalStorage.cookieExists(name)
? ('; ' + document.cookie).split('; ' + name + '=').pop().split(';')[0]
: null
);
}
});
defineProperties(LocalStorage.prototype, /** @lends just.LocalStorage.prototype */{
/**
* Concatenates a value to <var>document.cookie</var>.
*
* @function
* @param {string} name - The name of the cookie.
* @param {string} value - The value of the cookie.
* @param {!object} [opts] - Cookie options.
* @param {string} [opts.secure=location.protocol === 'https:'] - "secure" flag for the cookie.
*
* @return {boolean} `true` if was set, `false` otherwise.
* @readOnly
*/
'setCookie': function setCookie (name, value, opts) {
var cookie = '';
var set = function (k, v) { cookie += k + (typeof v !== 'undefined' ? '=' + v : '') + '; '; };
var options = defaults(opts, {
'secure': location.protocol === 'https:'
});
if (!this.consent) { return false; }
if (options.secure) { set('secure'); }
delete options.secure;
if (options.expires) { options.expires = new Date(options.expires).toGMTString(); }
set(name, value);
eachProperty(options, function (v, k) { set(k, v); });
document.cookie = cookie.trim();
return true;
},
/**
* Overrides a cookie by setting an empty value and expiring it.
*
* @function
* @param {string} name - The name of the cookie.
* @param {object} [opts] - Some extra options.
* @param {Date} [opts.expires=new Date(0)] - A date in the past.
*
* @return {boolean} `true` if was overriden or the cookie
* does not exist, `false` otherwise.
*/
'removeCookie': function removeCookie (name, opts) {
var options = defaults(opts, {
'expires': new Date(0)
});
if (!LocalStorage.cookieExists(name)) { return true; }
return this.setCookie(name, '', options);
},
/**
* Any of "cookie", "localStorage", "sessionStorage"...
*
* @typedef {string} just.LocalStorage~isStorageAvailable_type
*/
/**
* Tests if the specified storage does not throw.
*
* @function
* @param {just.LocalStorage~isStorageAvailable_type} type
* A type of storage.
* @param {string} [tempKey='_'] - Storage will save this key with <var>tempValue</var> as a value.
* @param {string} [tempValue='_'] - Storage will save this value with <var>tempKey</var> as a key.
*
* @return {boolean} `true` if the function does not throw
* and is allowed by the user, `false` otherwise.
*/
'isStorageAvailable': function isStorageAvailable (type, tempKey, tempValue) {
var k = defaults(tempKey, '_');
var v = defaults(tempValue, '_');
var storage;
if (!this.consent) { return false; }
if (/cookie/i.test(type)) {
return this.setCookie(k, v)
&& LocalStorage.getCookie(k) === v
&& this.removeCookie(k);
}
try {
storage = window[type];
storage.setItem(k, v);
storage.removeItem(k);
}
catch (exception) {
try { storage.removeItem(k); }
catch (wrongImplementation) { return false; }
return false;
}
/* istanbul ignore next */
return true;
}
});
set('LocalStorage', LocalStorage);
/**
* Add an event listener to multiple elements.
*
* @namespace
* @memberof just
* @param {string|Element[]} elements - The targets.
* @param {string|string[]} eventNames - The event types.
* @param {function} listener - The handler for the event.
* @param {object|boolean} [options=false] - Options for addEventListener
* @return {Element[]} elements
*/
function addEventListener (elements, eventNames, listener, options) {
var opts = options || false;
// Prefer results.push() over elements.filter().map() to avoid increasing complexity.
var results = [];
if (typeof elements === 'string') { elements = findElements(elements); }
if (!Array.isArray(eventNames)) { eventNames = [eventNames]; }
if (!Array.isArray(elements)) { elements = [elements]; }
elements.forEach(function (element) {
if (!('addEventListener' in Object(element))) { return; }
eventNames.forEach(function (eventType) {
this.addEventListener(eventType, listener, opts);
}, element);
this.push(element);
}, results);
return results;
}
set('addEventListener', addEventListener);
/**
* @mixin just
* @borrows just.addEventListener as on
*/
var on = addEventListener;
set('on', on);
/**
* Remove multiple events from multiple targets using
* EventTarget#removeEventListener().
*
* @namespace
* @memberof just
* @since 1.0.0-rc.24
* @param {Element|Element[]} targets - The targets of the attached events.
* @param {string|string[]} eventTypes - Name of the attached events, like "click", "focus", ...
* @param {function} listener - The same listener passed to EventTarget#addEventListener().
* @param {*} [options=false] - The same options passed to EventTarget#addEventListener().
*/
function removeEventListener (targets, eventTypes, listener, options) {
var opts = options || false;
if (!Array.isArray(targets)) { targets = [targets]; }
if (!Array.isArray(eventTypes)) { eventTypes = [eventTypes]; }
targets.forEach(function (target) {
eventTypes.forEach(function (eventType) {
this.removeEventListener(eventType, listener, opts);
}, target);
});
}
set('removeEventListener', removeEventListener);
/**
* @mixin just
* @since 1.0.0-rc.24
* @borrows just.removeEventListener as off
*/
var off = removeEventListener;
set('off', off);
/**
* Parses <var>url</var> without checking if it's a valid url.
*
* Note that this function uses <var>window.location</var> to make relative urls, so
* weird values in there will give weird results.
*
* @namespace
* @memberof just
* @param {url} [url=window.location.href] - A relative, an absolute or a blob url.
*
* @example <caption>An absolute url:</caption>
* just.parseUrl(window.location.href);
*
* @example <caption>A relative url:</caption>
* just.parseUrl('/?a#c?d'); // "/" is the pathname, "?a" the search and "#c?d" the hash.
*
* @example <caption>A blob url:</caption>
* just.parseUrl('blob:'); // Same as 'blob:' + `window.location.href`
*
* @example <caption>Some good-to-know urls:</caption>
* just.parseUrl(); // Same as `window.location`.
* just.parseUrl('a'); // Something that doesn't start with "/", "?", or "#" is evaluated as a host.
* just.parseUrl('a:b'); // "a:b" is a host, since "b" is not a number.
* just.parseUrl('//'); // evals as the current origin.
* just.parseUrl('blob://'); // Same as 'blob:' + `window.location.origin`.
* // [...]
*
* @return {url_parts}
*/
function parseUrl (url) {
var parts = {};
var loc = window.location;
var optionalParts, hrefParts, id, uriParts, domainParts, hostParts,
userParts, passwordParts;
var blob;
url = url || loc.href;
if (/^blob:/i.test(url)) {
blob = parseUrl(url.replace(/^blob:/i, ''));
return assign(blob, {
'protocol': 'blob:',
'href': 'blob:' + blob.href,
'host': '',
'hostname': '',
'port': '',
'pathname': blob.origin + blob.pathname
});
}
if (/^(:)?\/\//.test(url)) {
url = ((url = url.replace(/^:/, '')) === '//'
? loc.origin
: loc.protocol + url
);
}
else if (/^(\?|#|\/)/.test(url)) {
url = loc.origin + url;
}
else if (!/:\/\//.test(url)) {
url = loc.protocol + '//' + url;
}
hrefParts = (url || '').split(/(\?.*#?|#.*\??).*/);
optionalParts = (hrefParts[1] || '').split('#');
id = optionalParts[1] || '';
parts.search = optionalParts[0] || '';
parts.hash = (id ? '#' + id : id);
uriParts = (hrefParts[0] || '').split('://');
hostParts = (uriParts[1] || '').split(/(\/.*)/);
parts.username = '';
parts.password = '';
if (/@/.test(hostParts[0])) {
userParts = hostParts[0].split('@');
passwordParts = userParts[0].split(':');
parts.username = passwordParts[0] || '';
parts.password = passwordParts[1] || '';
hostParts[0] = userParts[1];
}
parts.host = hostParts[0] || '';
parts.pathname = hostParts[1] || '';
domainParts = parts.host.split(/:([0-9]+)/);
parts.hostname = domainParts[0] || '';
parts.port = (typeof domainParts[1] !== 'undefined'
? domainParts[1]
: ''
);
parts.protocol = uriParts[0] + ':';
parts.origin = parts.protocol + '//' + parts.host;
parts.href = (userParts
? parts.protocol + '//' + parts.username + ':' + parts.password + '@' + parts.host
: parts.origin
) + parts.pathname + parts.search + parts.hash;
return parts;
}
set('parseUrl', parseUrl);
/**
* If <var>value</var> is an object, return <var>value</var>,
* otherwise parse <var>value</var> using JSON.parse().
* Return null if an exception occurs.
*
* @since 1.0.0-rc.23
* @namespace
* @memberof just
* @param {*} value - Some value.
*
* @example
* just.parseJSON('{"a": 1}'); // > {a: 1}
*
* @example
* just.parseJSON('[]'); // > []
*
* @example
* just.parseJSON(''); // > null
*
* @example
* just.parseJSON({}); // > null
*
* @example
* just.parseJSON(1); // > 1
*
* @return {*} The given value (if it's an object), the parsed value or null.
*/
function parseJSON (value) {
var parsedValue;
if (typeof value === 'object') { return value; }
try { parsedValue = JSON.parse(value); }
catch (exception) { parsedValue = null; }
return parsedValue;
}
set('parseJSON', parseJSON);
/**
* Parses an stringified JSON (<code>'{"a": 1}'</code>)
* into an object literal (<code>{a: 1}</code>).
* If you need to parse any other value, use {@link just.parseJSON} instead.
*
* @namespace
* @memberof just
* @param {*} string - Some string to parse.
*
* @example
* just.stringToJSON('{"a": 1}'); // returns {a: 1}.
*
* @example
* just.stringToJSON(1); // returns {}.
*
* @return {!object} An object literal.
*/
function stringToJSON (string) {
var json;
if (!/(?:^\{|\}$)/.test(String(string).trim())) { return {}; }
json = parseJSON(string) || {};
return json;
}
set('stringToJSON', stringToJSON);
/**
* @version 1.0.0-rc.23
* @mixin just
* @borrows just.parseJSON as toJSON
*/
var toJSON = parseJSON;
set('toJSON', toJSON);
/**
* Converts <code>[[k0, v0], {k1: v1}]</code> to <code>{k0: v0, k1: v1}</code>.
*
* @deprecated since 1.0.0-rc.22
* @namespace
* @memberof just
* @param {!object[]|!object} array - An array containing sub-arrays
* with object literal pairs, or object literals: <code>[[k, v], {k: v}]</code>.
*
* @return {!object} An object literal.
*/
function toObjectLiteral (array) {
var objectLiteral = {};
deprecate('.toObjectLiteral()', 'warning', {
'since': '1.0.0-rc.22'
});
if (check(array, {}, null)) {
return assign({}, array);
}
if (!check(array, [])) {
throw new TypeError(array + ' must be either null, an object literal or an Array.');
}
array.forEach(function (subArray) {
var key, value;
if (check(subArray, [])) {
key = subArray[0];
value = subArray[1];
this[key] = value;
}
else if (check(subArray, {})) {
assign(this, subArray);
}
else {
throw new TypeError(subArray + ' must be either ' +
'an object literal or an Array.');
}
}, objectLiteral);
return objectLiteral;
}
set('toObjectLiteral', toObjectLiteral);
/**
* @deprecated since 1.0.0-rc.22
* @mixin just
* @borrows just.toObjectLiteral as toObj
*/
var toObj = toObjectLiteral;
set('toObj', toObj);
/**
* Call a function when the HTML document has been loaded.
* Source: http://youmightnotneedjquery.com/#ready
*
* @param {function} fn - The callback.
*/
function onDocumentReady (fn) {
if (document.readyState !== 'loading') { return setTimeout(fn), void 0; }
addEventListener(document, 'DOMContentLoaded', function onDocumentReady () {
removeEventListener(document, 'DOMContentLoaded', onDocumentReady);
fn();
});
}
set('onDocumentReady', onDocumentReady);
/**
* Create an element with the given properties.
*
* @namespace
* @memberof just
* @since 1.0.0-rc.23
* @param {string} tag - The tag name for the element.
* @param {?object} properties - Properties for the created element.
* @return {Element} The created element.
*/
function createElement (tag, properties) {
var element = document.createElement(tag);
assign(element, properties);
return element;
}
set('createElement', createElement);
/**
* @mixin just
* @since 1.0.0-rc.23
* @borrows just.createElement as create
*/
var create = createElement;
set('create', create);
/**
* A function to intercept and send the request.
*
* @this {XMLHttpRequest}
* @param {*} data - The data ready to be sent.
* @param {!object} options - The options for the request.
* @typedef {function} just.request~send
*/
/**
* A function to call on "load"/"error" event.
*
* @this {XMLHttpRequest}
* @param {?Error} error - Bad status error or <code>null</code>.
* @param {*} response - Either #response or #responseText property.
* On JSON request, the property parsed as a JSON.
* @typedef {function} just.request~fn
*/
/**
* Default request headers.
*
* @property {string} [X-Requested-With="XMLHttpRequest"]
* @property {string} [Content-Type="application/json"] - Only on JSON requests.
* @typedef {!object} just.request~defaultHeaders
*/
/**
* Default request properties.
*
* @property {string} [responseType="json"] - Only on JSON requests.
* @typedef {object} just.request~defaultProps
*/
/**
* Make a request using XMLHttpRequest.
*
* @namespace
* @memberof just
* @since 1.0.0-rc.23
* @param {!url} url - Some url.
* @param {just.request~fn} [fn] - Hook for onreadystatechange listener.
* @param {object} [options]
* @param {string} [options.method="GET"] - An HTTP Request Method: GET, POST, HEAD, ...
* @param {boolean} [options.json=/.json$/.test(url)] - If <code>true</code>,
* <code>"Content-Type"</code> will be set to <code>"application/json"</code>,
* #responseType to <code>"json"</code>, and the #response/#responseText
* will be parsed to a JSON.
* @param {*} [options.data=null] - Data to send.
* @param {function} [options.send={@link just.request~send}] - A custom function to intercept and send the request.
* @param {boolean} [options.async=true] - "async" param for XMLHttpRequest#open().
* @param {string} [options.user=null] - User name to use for authentication purposes.
* @param {string} [options.pwd=null] - Password to use for authentication purposes.
* @param {object} [options.props={@link just.request~defaultProps}] - Properties for the xhr instance.
* @param {object} [options.headers={@link just.request~defaultHeaders}] - Custom headers for the request.
* @returns {*} The retuned value of {@link just.request~send}.
*/
function request (url, fn, options) {
var isJSON = ('json' in Object(options)
? options.json
: /\.json$/i.test(url)
);
var opts = defaults(options, {
'json': isJSON,
'data': null,
'method': 'GET',
'async': true,
'user': null,
'pwd': null,
'headers': assign({
'X-Requested-With': 'XMLHttpRequest'
}, (isJSON ? {
'Content-Type': 'application/json'
} : null)),
'props': assign({}, (isJSON ? {
'responseType': 'json'
} : null)),
'send': function send (data) { return this.send(data); }
}, {'ignoreNull': true});
var data = opts.data;
var method = opts.method;
var async = opts.async;
var user = opts.user;
var password = opts.pwd;
var props = opts.props;
var headers = opts.headers;
var xhr = new XMLHttpRequest();
if (/GET/i.test(method) && data) {
url = request.appendData(url, data);
data = null;
}
xhr.open(method, url, async, user, password);
eachProperty(headers, function setHeaders (value, key) { this.setRequestHeader(key, value); }, xhr);
assign(xhr, props);
xhr.onreadystatechange = function onReadyStateChange (e) {
var status, response, error;
if (this.readyState === XMLHttpRequest.DONE) {
this.onreadystatechange = null;
status = this.status;
response = ('response' in this
? this.response
: this.responseText
);
error = ((status < 200 || status >= 400) && status !== 0
? new Error('Bad status: ' + status)
: null
);
if (isJSON && typeof response !== 'object') { response = parseJSON(response); }
if (fn) { fn.call(this, error, response); }
}
};
return opts.send.call(xhr, data, options);
}
defineProperties(request, /** @lends just.request */{
/**
* Append data to the search params of the given url.
*
* @function
* @param {string} url - Some url.
* @param {?object} data - An object to be appended.
* @example
* appendData('/some', {'data': 1}); // > '/some?data=1'
* appendData('/some?url', {'data': 1}); // > '/some?url&data=1'
*
* @returns {string}
*/
'appendData': function appendDataToUrl (url, data) {
var parsedUrl = parseUrl(url);
var searchParams = request.dataToUrl(data);
var search = ((/\?.+/.test(parsedUrl.search)
? parsedUrl.search + '&'
: '?'
) + searchParams).replace(/[?&]$/, '');
return [
parsedUrl.origin,
parsedUrl.pathname,
search,
parsedUrl.hash
].join('');
},
/**
* Convert data into search params.
*
* @function
* @param {?object} data - Expects an object literal.
* @throws {TypeError} If <var>data</var> is not an object.
* @example
* dataToUrl({'a': '&a', 'b': 2}); // > 'a=%26&b=2'
*
* @returns {string}
*/
'dataToUrl': function convertDataIntoUrl (data) {
var dataObject = Object(data);
if (typeof data !== 'object') { throw new TypeError(data + ' is not an object.'); }
return reduce(dataObject, function (params, value, key) {
var param = encodeURIComponent(key) + '=' + encodeURIComponent(value);
return params.concat(param);
}, []).join('&');
}
});
set('request', request);
/**
* @mixin just
* @since 1.0.0-rc.23
* @borrows just.request as ajax
*/
var ajax = request;
set('ajax', ajax);
var Define = (function () {
var modules = {};
var defaultErrorHandler = function (exception) { throw exception; };
var timeout;
function defineKnownValue (id, context, alias) {
var value = context[id];
if (!(id in context) || isModuleDefined(id)) { return false; }
new Define(id, [], value);
return true;
}
function defineAlias (id, alias) {
if (isModuleDefined(alias)) { return false; }
new Define(alias, [id], function (value) { return value; });
return true;
}
function defineGlobal (id) { return defineKnownValue(id, Define.globals); }
function defineNonScript (id) { return defineKnownValue(id, Define.nonScripts); }
function defineKnownModule (id, isLoaded) {
if (isModuleDefined(id)) { return false; }
if (id in Define.urls && !isLoaded) { return loadModule(id); }
else if (id in Define.nonScripts) { return defineNonScript(id); }
else if (id in Define.globals) { return defineGlobal(id); }
return false;
}
function loadModule (id, onLoad) {
var urlDetails = Define.urls[id];
var urlDetailsObject = Object(urlDetails);
var url = (typeof urlDetails === 'object'
? urlDetailsObject.src || urlDetailsObject.href
: typeof urlDetails === 'string'
? urlDetails
: id
);
var URL = parseUrl(url);
var urlExtension = (URL.pathname.match(/\.(.+)$/) || ['js'])[0];
var type = ('tagName' in urlDetailsObject
? urlDetailsObject.tagName
: 'src' in urlDetailsObject
? 'script'
: 'href' in urlDetailsObject
? 'link'
: (/css$/i.test(urlExtension) ? 'link' : 'script')
);
var properties = (typeof urlDetails === 'object'
? (delete urlDetails.tagName, urlDetails)
: null
);
function listenerWrapper (e) {
var isError = Object(e).type === 'error';
removeEventListener(this, ['load', 'error'], listenerWrapper);
if (!isError) { defineKnownModule(id, true); }
if (typeof onLoad === 'function') { return onLoad.call(this, e); }
if (isError) { Define.handleError.call(null, new Error('Error loading ' + url)); }
}
if (!(id in Define.urls)) { Define.urls[id] = url; }
if (url !== id) { defineAlias(id, url); }
return loadElement(type, properties || url, listenerWrapper, function (similarScript) {
if (type !== 'script' && !(id in Define.nonScripts)) { Define.nonScripts[id] = this; }
if (similarScript) {
addEventListener(similarScript, ['load', 'error'], listenerWrapper);
return false;
}
document.head.appendChild(this);
return true;
});
}
function getModule (id) {
defineKnownModule(id);
return modules[id] || null;
}
function getModules (ids) {
return ids.map(function (id) { return getModule(id); });
}
function wasCalled (module) {
return Object(module).state === Define.STATE_CALLED;
}
function wereCalled (modules) {
return modules.every(function (module) { return wasCalled(module); });
}
function hasID (id, module) {
return Object(module).id === id;
}
function hasCircularDependencies (module) {
return module && module.dependencies.some(
hasID.bind(null, module.id)
);
}
function hasRecursiveDependencies (module) {
return module && module.dependencies.some(function (d) {
return d && d.dependencies.some(
hasID.bind(null, this.id)
);
}, module);
}
function callModule (module) {
var handler = module.handler;
var dependencies = module.dependencies;
var errorHandlerResult;
var isEveryDependencyWaiting;
var args;
if (wasCalled(module)) { return false; }
module.state = Define.STATE_NON_CALLED;
isEveryDependencyWaiting = dependencies.every(
function (d) { return d && d.state !== Define.STATE_DEFINED; }
);
if (wereCalled(dependencies) || isEveryDependencyWaiting && (
hasCircularDependencies(module)
|| hasRecursiveDependencies(module)
)) {
args = dependencies.map(function (d) { return d.exports; });
module.state = Define.STATE_CALLING;
try { module.exports = handler.apply(module, args); }
catch (exception) { errorHandlerResult = Define.handleError.call(module, exception); }
module.state = Define.STATE_CALLED;
return (typeof errorHandlerResult === 'boolean'
? errorHandlerResult
: true
);
}
return false;
}
function isValidID (id) {
return typeof id === 'string' && id.trim() !== '';
}
function isModuleDefined (id) {
return id in modules;
}
function isNullOrUndefined (value) {
return value === null || typeof value === 'undefined';
}
/**
* Define a value after all dependencies became available.
*
* <br/>
* <aside class='note'>
* <h3>A few things to consider: </h3>
* <ul>
* <li>This is not intended to be AMD compliant.</li>
*
* <li>This does not check file contents, so it won't check if the
* file defines an specific id.</li>
*
* <li>Urls passed as dependencies are considered ids, so they must
* be defined in {@link just.Define.urls} in order to be loaded first.</li>
*
* <li><var>require</var>, <var>module</var> and <var>exports</var>
* are not present in this loader, but you can emulate them.</li>
*
* <li>Recursive and circular dependencies pass a recursive module
* as argument within another recursive module (instead of the returned value).
* Please, avoid using them or use them carefully.</li>
* </ul>
* </aside>
*
* @class
* @memberof just
* @param {?string} id - The module id.
* @param {string[]|string} dependencyIDs - Required module ids.
* @param {*} value - The module value.
*
* @example
* // https://some.cdn/js/just.js
* window.just = {'Define': function () {}};
*
* // index.html
* < !DOCTYPE html>
* < html data-just-Define='{"main": "/js/main.js"}'>
* < head>
* < title>Test</title>
* < script src='https://some.cdn/js/just.js' async></script>
* < /head>
* < body>
* < /body>
* < /html>
*
* // /js/main.js
* just.Define.configure({
* 'globals': {
* // Set justJs to window.just
* 'justJs': function () { return just; }
* },
* 'urls': {
* // Load "/css/index.css" when "index.css" is required.
* 'index.css': '/css/index.css'
* },
* 'nonScripts': {
* // Call when "main.html" is required.
* 'main.html': function () { return '<main></main>'; }
* }
* });
*
* // Load when document, justJs and index.css are ready:
* just.Define('main', ['justJs', 'index.css'], function (j) {
*
* if (j.supportsTouch()) {
* j.Define('mobile', 'https://example/m');
* return;
* }
*
* j.Define('non-mobile', ['main.html']);
*
* });
*
* // Call only if j.supportsTouch()
* just.Define(['mobile'], function (url) {
* window.redirect = url;
* });
*
* // Call when main.html is ready.
* just.Define(['non-mobile'], function (html) {
* document.body.innerHTML = html;
* });
*/
function Define (id, dependencyIDs, value) {
if (!arguments.length || typeof value !== 'function' && [id, dependencyIDs].every(
function (value) { return isNullOrUndefined(value); }
)) {
throw new TypeError('Not enough arguments.');
}
if (arguments.length === 2) {
value = arguments[1];
if (Array.isArray(arguments[0])) {
dependencyIDs = arguments[0];
id = void 0;
}
else {
dependencyIDs = void 0;
}
}
else if (arguments.length === 1) {
if (typeof arguments[0] === 'function') {
value = arguments[0];
id = void 0;
dependencyIDs = void 0;
}
}
if (!isValidID(id)) {
if (isNullOrUndefined(id)) { id = Math.random().toString(); }
else { throw new TypeError('The given id (' + id + ') is invalid.'); }
}
if (isNullOrUndefined(dependencyIDs)) { dependencyIDs = []; }
else if (!Array.isArray(dependencyIDs)) { dependencyIDs = [dependencyIDs]; }
if (dependencyIDs.some(
function (id) { return !isValidID(id); }
)) { throw new TypeError('If present, the ids for the dependencies must be valid ids.'); }
if (!(this instanceof Define)) { return new Define(id, dependencyIDs, value); }
modules[id] = this;
defineProperties(this, {
'id': id,
'handler': (typeof value === 'function'
? value
: function () { return value; }
),
'dependencyIDs': dependencyIDs,
'dependencies': {
'get': function () { return getModules(this.dependencyIDs); }
},
'state': {
'value': Define.STATE_DEFINED,
'writable': true
},
'exports': {
'value': (typeof value === 'function' ? this : value),
'writable': true
}
});
clearTimeout(timeout);
timeout = setTimeout(function updateModules () {
eachProperty(modules, function (module) {
if (callModule(module)) { return updateModules(), true; }
});
});
}
defineProperties(Define, /** @lends just.Define */{
/**
* The initial value for all defined modules.
*
* @type {number}
* @readOnly
*/
'STATE_DEFINED': -1,
/**
* The value for all modules that had been queued
* prior to be called.
*
* @type {number}
* @readOnly
*/
'STATE_NON_CALLED': 0,
/**
* The value for all modules that are being called.
*
* @type {number}
* @readOnly
*/
'STATE_CALLING': 1,
/**
* The value for all modules that were called.
*
* @type {number}
* @readOnly
*/
'STATE_CALLED': 2,
/**
* A list of urls that will be used (instead of ids) to load
* files before defining globals or non-script values.<br/>
*
* <aside class='note'>
* <h3>Note:</h3>
* <p>If you need to load files when you require some id,
* you need to specify those urls here. If you do so, you
* must {@link just.Define|Define} that id/url within that file.</p>
* <p>Starting from v1.0.0-rc.23, you can pass an object to specify
* the attributes for the loaded element.</p>
* </aside>
*
* @example
* // js/b.js
* just.Define('b', 1); // or: just.Define('js/b.js', 1);
*
* // index.js
* just.Define.urls['b'] = 'js/b.js';
* just.Define('a', ['b'], function (b) {
* // b === 1; > true
* });
*
* @example <caption>Using multiple ids with the same url</caption>
* // js/index.js
* just.Define('foo', 1);
* just.Define('bar', 1);
*
* // index.js
* just.assign(just.Define.urls, {
* 'foo': 'js/index.js',
* 'bar': 'js/index.js'
* });
*
* just.Define('foo-bar', ['foo', 'bar'], function () {
* // Will load js/index.js once.
* });
*
* @example <caption>Since v1.0.0-rc.23: Adding custom attributes to the loaded element.</caption>
* just.assign(just.Define.urls, {
* 'id': {
* 'src': 'https://some.cdn.com',
* 'integrity': 'sha512-...',
* 'crossorigin': '',
* 'data-some-other': 'attribute'
* }
* });
*
* just.Define(['id'], function () {
* // Will load a <script> with the given attributes ("integrity", "crossorigin", ...).
* });
*
* @example <caption>Since v1.0.0-rc.23: Load a custom element.</caption>
* just.assign(just.Define.urls, {
* 'id': {
* 'tagName': 'iframe',
* 'src': 'https://example.org'
* }
* });
*
* just.Define(['id'], function () {
* // Will load an <iframe> with the given attributes.
* });
*
* @type {!object.<just.Define~id, url>|!object.<just.Define~id, object.<elementAttributes>>}
*/
'urls': {
'value': {},
'writable': true
},
/**
* A writable object literal that contains values for non script
* resources, like css. Since {@link just.Define|Define} won't
* check for file contents when loads a new file, you must add
* the value here.</br>
*
* <aside class='note'>
* <h3>Note:</h3>
* <p>If a module is defined with the same id, the module will take
* precedence.</p>
* </aside>
*
* @example
* just.Define.nonScripts['/css/index.css'] = function () {};
* just.Define('load css', ['/css/index.css'], function (css) {
* // by default, `css` is an HTMLElement (the link element that loaded the file).
* // but for now, `css` is a function since the id wasn't defined in Define.urls
* });
*
* @type {!object.<just.Define~id, *>}
*/
'nonScripts': {
'value': {},
'writable': true
},
/**
* A writable object literal that contains all the values that
* will be defined when required.<br/>
*
* <aside class='note'>
* <h3>Notes:</h3>
* <ul>
* <li><strong>Deprecated since 1.0.0-rc.24. It raises a security error over a CDN.</strong>
* If the value for the global is a string, the property
* will be accessed from window. I.e.:
* <var>'some.property'</var> will access to <var>window.some.property</var>.
* </li>
* <li>If a module is defined with the same id, the module will take
* precedence.</li>
* <li>If a non-script is defined with the same id, a non-script value
* will take precedence.</li>
* </ul>
* </aside>
*
* @example
* // index.js
* just.Define.globals['just'] = 1;
* just.Define('index', ['just'], function (just) {
* // just === 1; > true
* });
*
* @example <caption>Defining a global on file load.</caption>
* // https://some.cdn/js/just.js
* window.just = {Define: 1};
*
* // main.js
* just.Define.globals['JustJs'] = function () { return just; };
* just.Define.urls['JustJs'] = 'https://some.cdn/js/just.js';
* just.Define('main', ['JustJs'], function (just) {
* // just === {Define: 1};
* });
*
* @example <caption>Defining a global after a file has loaded already.</caption>
* // https://some.cdn/js/just.js
* window.just = {Define: 1};
*
* // index.html
* <script src='https://some.cdn/js/just.js'
* data-just-Define='{"JustJs": "[src]"}' async></script>
*
* // main.js
* if ('just' in window) { just.Define('JustJs', just); }
* else { just.Define.globals['JustJs'] = function () { return just; }; }
*
* just.Define(['JustJs'], function (just) {
* // just === {Define: 1};
* });
*
* @type {!object.<just.Define~id, *>}
*/
'globals': {
'value': {},
'writable': true
},
/**
* Check if a module is defined.
*
* @function
* @return {boolean}
*/
'isDefined': isModuleDefined,
/**
* Load a module explicitly.
*
* @function
* @param {url|just.Define~id} id - Some url or an alias defined in {@link just.Define.urls}.
* @param {?function} onLoad - Some listener to call when the function loads.
*/
'load': loadModule,
/**
* Configure any writable option in {@link just.Define} using an object.
*
* @example
* just.Define.configure({
* 'urls': {}, // Same as Define.urls = {}
* 'handleError': function () {}, // Same as Define.handleError = function () {}
* 'load': 1 // Same as Define.load = 1 > throws Define.load is read-only.
* })('id', [], function () {}); // Define afterwards.
*
* @function
* @param {!object} properties - Writable properties from {@link just.Define}.
* @chainable
*/
'configure': function (properties) {
assign(Define, properties);
return Define;
},
/**
* Empty all internal variables and writable properties.
*
* @function
* @chainable
*/
'clear': function () {
Define.globals = {};
Define.nonScripts = {};
Define.urls = {};
Define.handleError = defaultErrorHandler;
Define.clearModules();
return Define;
},
/**
* Remove all modules.
*
* @function
* @chainable
*/
'clearModules': function () { return (modules = {}), this; },
/**
* Remove some module.
*
* @function
* @param {just.Define~id} id - The id for the module.
* @chainable
*/
'clearModule': function (id) { return (delete modules[id]), this; },
/**
* A function to be called when an async error occur.
*
* @function
* @param {*} exception - Some throwable exception.
* @this just.Define
* @return {boolean} <var>true</var> if you want to keep updating modules.
*/
'handleError': {
'value': defaultErrorHandler,
'writable': true
},
/**
* Finds {@link just.Define.urls|urls} within the document, adds them, and
* loads them.<br/>
*
* <aside class='note'>
* <h3>Note</h3>
* <p>This function is called when the file is loaded.</p>
* </aside>
*
* @function
* @chainable
*/
'init': function loadUrlsFromDocument () {
Define.configure({
'urls': Define.findUrlsInDocument('data-just-Define')
});
eachProperty(Define.urls, function (url, id) { Define.load(id); });
return Define;
},
/**
* Finds {@link just.Define.urls|urls} within the document
* by selecting all the elements that contain an specific
* attribute and parsing that attribute as a JSON.
* <br/>
* <aside class='note'>
* <h3>Note</h3>
* <p>Values within brackets will be replaced with
* actual attributes for that element.</p>
* <p>I.e.: <span a='123' data-urls='{"[a]456": "[a]456"}'></span>
* will become: {123456: '123456'}</p>
* </aside>
*
* @function
* @param {string} attributeName - The attribute which defines the
* {@link just.Define.urls|urls} to be loaded.
* @param {Element} [container=document]
*
* @example
* // Considering the following document:
* < body>
* < div id='a' data-urls='{"[id]": "link a.css"}'>< /div>
* < script src='b.js' data-urls='{"b": "script [src]"}'>< /script>
* < /body>
*
* // then, in js:
* findUrlsInDocument('data-urls');
* // Should return {a: 'link a.css', b: 'script b.js'}.
*
* @return {!just.Define.urls}
*/
'findUrlsInDocument': function (attributeName, container) {
var urls = {};
findElements('*[' + attributeName + ']', container).forEach(function (element) {
var attribute = element.getAttribute(attributeName) + '';
var urls = stringToJSON(attribute.replace(/\[([^\]]+)\]/ig,
function (_, key) { return element.getAttribute(key); }
));
assign(this, urls);
}, urls);
return urls;
}
});
defineProperties(Define.prototype, /** @lends just.Define# */{
/**
* Same as {@link Define.load}, but chainable.
*
* @function
* @chainable
*/
'load': function () {
loadModule.apply(null, [].slice.call(arguments));
return this;
}
});
return Define;
})();
onDocumentReady(Define.init);
set('Define', Define);
/**
* @mixin just
* @borrows just.Define as Def
*/
var Def = Define;
set('Def', Def);
var Router = (function () {
var location = window.location;
var history = window.history;
/**
* Route a SPA easily.
*
* @class
* @memberof just
*
* @example
* const router = new just.Router();
*
* router.route('home', {
* 'pathname': '/',
* 'search': /\breload=([^&]+)/
* }, (event, {route, data}) => {
*
* const {action, by} = route;
* let reload;
*
* if (by === 'search') {
* reload = RegExp.$1;
* }
*
* if (/init|popstate/.test(action)) {
* // @TODO Call controllers.
* }
*
* });
*/
function Router () {
assign(this, {
'routes': {}
});
}
function testRoute (a, b) {
return (a instanceof RegExp
? a.test(b)
: a === b
);
}
function callMatchingRoute (route, path, by, e) {
var detail = e.detail;
var detailObj = Object(detail);
var routeArgObj = Object(detailObj.route);
var handler = route.handler;
var options = route.options;
var url = location[by];
var ignore = options.ignore;
var only = options.only;
var actions = options.actions;
var action = routeArgObj.action;
var allowAction = actions.some(
function (value) { return testRoute(value, action); }
);
/**
* Make sure to call this just before calling the handler
* to include the matched tokens in RegExp.
* i.e: /(some)-route/ -> RegExp.$1 // > some
*/
var isCurrentPath = testRoute(path, url);
var result, stop;
if (isCurrentPath
&& only.call(route)
&& !ignore.call(route)
&& allowAction) {
if (!routeArgObj.by || routeArgObj.action === 'init') { routeArgObj.by = by; }
result = handler.call(route, e, detail);
stop = !result;
return stop;
}
}
function callMatchingRoutes (route, e) {
var pathObj = route.path;
eachProperty(pathObj, function (path, by) {
return callMatchingRoute(this, path, by, e);
}, route);
}
function onRoute (e) {
var route = this;
if (e.type === 'popstate') {
e.detail = {
'data': null,
'route': {
'by': null,
'action': e.type
}
};
}
callMatchingRoutes(route, e);
}
defineProperties(Router, /** @lends just.Router */{
/**
* Call a <var>window.history</var>'s function if available.
* Otherwise, change the current url using <var>location.hash</var>
* and prepending a hashbang (#!) to the url state.
*
* <aside class='note'>
* <p>Note: This function does not accept any arguments
* for pushState/replaceState because it's only intended
* to change the url without reloading.</p>
* </aside>
*
* @param {string} action - "pushState" or "replaceState".
* @param {url} url - A same-origin url.
*
* @return {boolean} `false` if something fails, `true` otherwise.
*/
'changeState': function changeState (action, url) {
var currentOrigin = location.origin;
var sameOrigin = parseUrl(url).origin === currentOrigin;
var sameOriginPath = url.replace(currentOrigin, '');
if (!sameOrigin) { return false; }
try {
if (action in history) { history[action](null, '', sameOriginPath); }
else { location.hash = '#!' + sameOriginPath; }
}
catch (exception) { return false; }
return true;
},
/**
* Do a <var>history.replaceState</var> calling {@link just.Router.changeState}.
*
* @param {url} url - {@link just.Router.changeState}'s url param.
* @return {boolean} {@link just.Router.changeState}'s returned value.
*/
'replaceState': function replaceState (url) {
return Router.changeState('replaceState', url);
},
/**
* Do a <var>history.pushState</var> calling {@link just.Router.changeState}.
*
* @param {url} url - {@link just.Router.changeState}'s url param.
* @return {boolean} {@link just.Router.changeState}'s returned value.
*/
'pushState': function pushState (url) {
return Router.changeState('pushState', url);
}
});
defineProperties(Router.prototype, /** @lends just.Router# */{
/**
* Call a custom action on the current route by
* triggering a CustomEvent on each given route.
*
* @param {string} action - Some string.
* @param {*} data - Data for the triggered event.
* @param {CustomEventInit} eventInit - Options for CustomEvent's eventInit argument.
* @example
* import router from 'routes/router.js';
*
* router.route('all', /./, (e, {route, data}) => {
*
* const {action} = route;
*
* if (action === 'my-action') {
* console.log(`triggered ${data} on any route!`); // > "triggered my-data on any route!"
* }
*
* });
*
* router.route('home', '/', () => {
*
* if (action === 'my-action') {
* // ignored.
* }
*
* });
*
* router.constructor.pushState('/item');
* router.trigger('my-action', 'my-data');
*
* @example <caption>You can go even further by converting all anchors
* on your html into actions.</caption>
*
* // index.html with "/item/a/b/c" as a url.
* <a href='#close'></a>
*
* // controllers/item.js
* function closeItem ({event, target}) {
* console.log("I'm closing...");
* // @TODO Close.
* }
*
* export {closeItem as close}
*
* // routes/item.js
* import router from 'router.js';
* import * as controller from '../controllers/item.js';
*
* router.route('item', {
* 'pathname': /^\/item\//,
* 'hash': /^#!\/item\// // Set backwards compability.
* }, (e, {route, data}) => {
*
* if (action === 'close') {
* controller.close(data);
* }
*
* });
*
* // listeners/link.js
* import router from 'routes/router.js';
*
* // This is only for demostration purposes.
* just.on(document, 'click', (event) => {
*
* const {target} = event;
* const {hash} = target;
*
* if (hash) {
*
* let action = hash.slice(1);
* let data = {'event': e, target};
*
* router.trigger(action, data);
*
* }
*
* });
*
* // Then, click an anchor link and the corresponding controller
* // will be called.
* @chainable
*/
'trigger': function triggerAction (action, data, eventInit) {
var routesObj = this.routes;
eachProperty(routesObj, function (route, id) {
var eventOptions = route.options.event;
var eventTarget = eventOptions.target;
var eventName = eventOptions.name;
var defaultEventInit = eventOptions.init;
var isInit = action === 'init';
var event;
if (isInit) {
if (route.init) { return false; }
route.init = true;
}
eventInit = defaults(eventInit, defaultEventInit, {'ignoreNull': true});
eventInit.detail.data = data;
assign(eventInit.detail.route, {
'by': 'action',
'action': action
});
event = new CustomEvent(eventName, eventInit);
eventTarget.dispatchEvent(event);
});
return this;
},
/**
* @typedef {function} just.Router~route_ignore
*
* @this {just.Router~route}
*
* @return {boolean} If `true`, the route won't be called.
*/
/**
* @typedef {function} just.Router~route_only
*
* @this {just.Router~route}
*
* @return {boolean} If `true`, the route will be called.
*/
/**
* Define a route, attach listeners (for "popstate"
* and custom events), and trigger an "init" event.
*
* @param {string} id - Some unique string to identify the current route.
* @param {string|RegExp|?object} path - A value that will match the current location.
* A string/RegExp is the same as passing {"pathname": string/RegExp}.
* An object must contain any <var>window.location</var>'s keys, like "search", "hash", ...
* @param {function} handler - Some function that will be called when the route matches the current url.
* @param {!object} options
* @param {just.Router~route_ignore} [options.ignore=allowEverything()]
* @param {just.Router~route_only} [options.only=allowEverything()]
* @param {Array} [options.actions] - An array of allowed actions.
* You can pass an array of string/RegExps.
* @param {!object} options.event
* @param {string} [options.event.name=`just:Router:route:${id}`] - event.type for the CustomEvent.
* @param {Node} [options.event-target=document] - The element to attach the event to.
* @param {CustomEventInit} options.event.init - Values for the CustomEvent's eventInit argument.
* You can pass custom values for the "init" event in here.
* @param {!object} options.event.init.detail
* @param {*} [options.event.init.detail.data=null] - Custom data for the "init" event.
* @param {!object} options.event.init.detail.route - Internal properties.
* @param {?string} [options.event.init.detail.route.by=null] - A <var>window.location</var> key that matched the route ("pathname", ...).
* @param {?string} [options.event.init.detail.route.action=null] - The triggered action to call this route.
* Actions triggered by default include "init" and "popstate".
*
* @chainable
*/
'route': function route (id, path, handler, options) {
var opts = defaults(options, {
'ignore': function allowEverything () {},
'only': function allowEverything () { return true; },
'actions': [/.+/], // any.
'event': {
'name': 'just:Router:route:' + id,
'target': document,
'init': {
'detail': {
'data': null,
'route': {
'by': null,
'action': null
}
}
}
}
}, {'ignoreNull': true});
var eventOptions = opts.event;
var eventName = eventOptions.name;
var eventInit = eventOptions.init;
var eventInitData = eventInit.detail.data;
var eventTarget = eventOptions.target;
var pathObj = (check(path, {})
? path
: {'pathname': path}
);
var route = {
'id': id,
'path': pathObj,
'originalPath': path,
'handler': handler,
'options': opts,
'init': false
};
var listener = onRoute.bind(route);
this.routes[id] = route;
addEventListener(eventTarget, eventName, listener);
addEventListener(window, 'popstate', function (e) {
/**
* Trigger at the end of the browser event loop,
* as stated in MDN, to avoid calling it before
* any other event.
*/
setTimeout(function () {
listener.call(this, e);
}.bind(this), 0);
});
this.trigger('init', eventInitData, eventInit);
return this;
},
/**
* Do a {@link just.Router.changeState} and trigger an action
* if it succeds.
*
* @param {string} action - A valid {@link just.Router.changeState}'s action.
* @param {url} url - The new url.
* @param {*} data - Some data.
* @param {CustomEventInit} eventInit - Options for the event.
* @chainable
*/
'change': function changeState (action, url, data, eventInit) {
if (Router.changeState(action, url)) {
this.trigger(action, data, eventInit);
}
return this;
},
/**
* Trigger a "pushState" action by calling {@link just.Router#change}.
*
* @param {url} url - {@link just.Router#change}'s url argument.
* @param {*} data - {@link just.Router#change}'s data argument.
* @param {CustomEventInit} - {@link just.Router#change}'s eventInit argument.
* @chainable
*/
'push': function pushState (url, data, eventInit) {
return this.change('pushState', url, data, eventInit);
},
/**
* Trigger a "replaceState" action by calling {@link just.Router#change}.
*
* @param {url} url - {@link just.Router#change}'s url argument.
* @param {*} data - {@link just.Router#change}'s data argument.
* @param {CustomEventInit} - {@link just.Router#change}'s eventInit argument.
* @chainable
*/
'replace': function replaceState (url, data, eventInit) {
return this.change('replaceState', url, data, eventInit);
}
});
return Router;
})();
set('Router', Router);
var View = (function () {
function matchNested (string, openSymbol, closeSymbol, transform) {
if (typeof transform !== 'function') {
transform = function (matched) { return matched; };
}
return string.split(closeSymbol).reduce(function (left, right) {
var matched = left + right;
var openSymbolIndex = matched.lastIndexOf(openSymbol);
var hasOpenSymbol = openSymbolIndex > -1;
var enclosed = null;
var result = matched;
if (hasOpenSymbol) {
enclosed = matched.slice(openSymbolIndex + 1);
result = matched.slice(0, openSymbolIndex);
}
return transform(result, {
'enclosed': enclosed,
'index': openSymbolIndex
});
}, '');
}
/**
* Parse argument-like strings ("a, b, c") and return valid JSON values.<br>
*
* <aside class='note'>
* <h3>A few things to consider:</h3>
* <ul>
* <li>It supports dot notation on each argument. Eg: <code>a.b, b.c</code>.</li>
* <li>It doesn't support functions as arguments. Eg: <code>fn(function () {})</code>.</li>
* <li>It doesn't support variables in objects yet. Eg: <code>fn({"a": var}, [var])</code>).</li>
* </ul>
* <p>It uses JSON.parse internally.</p>
* </aside>
*
* @typedef {function} just.View~parseArguments
* @param {string} string - Argument-like string without quotes, like "a, b, c".
* @param {object} data - Accessable data.
* @example
* parseArguments("a, b, c", {})
* @returns {Array} Arguments.
*/
function parseArguments (string, data) {
var invalidJSONValues = {};
var unparsedArgs = (',' + string + ',')
// Match numbers, variables and reserved keywords.
.replace(/,\s*([\w.-]+)\s*(?=,)/ig, function ($0, value, index) {
var arg = (/^\d/.test(value)
? parseFloat(value)
: value === 'this'
? value
: access(value, data)
);
var requiresQuotes = typeof arg === 'object' && arg !== null
|| typeof arg === 'string' && arg !== 'this';
return ',' + (requiresQuotes
? JSON.stringify(arg)
: arg
);
})
// Remove extra commas.
.replace(/^,|,$/g, '')
// Replace invalid JSON values with a random/unique string.
.replace(/(\b)(undefined|this)(\b)/g, function ($0, $1, value, $2) {
var uniqueString = Math.random() + '';
invalidJSONValues[uniqueString] = (value === 'this'
? data[value]
: void 0
);
return $1 + JSON.stringify(uniqueString) + $2;
});
// Parse as an array.
var parseableArgs = '[' + unparsedArgs + ']';
// Parse args as JSON and replace invalid values back.
var args = parseJSON(parseableArgs).map(function (arg, i) {
return (arg in invalidJSONValues
? invalidJSONValues[arg]
: arg
);
});
return args;
}
/**
* Access to properties using the dot notation.
*
* Supports nested function arguments replacements and
* reserved keywords. See {@link just.View~parseArguments}.
*
* @typedef {function} just.View~access
* @param {string} keys - Accessable properties using the dot notation.
* @param {?object} data - Accessable data.
* @example
* access('a.b(c(d))', {
* 'a': {'b': function (v) { return v + 'b'; }},
* 'c': function (v) { return v + 'c'; },
* 'd': 'd'
* }); // > 'dcb';
*/
function access (keys, data) {
var allArgsSorted = [];
if (!keys) { return; }
if (typeof keys !== 'string') { return keys; }
/**
* The way it works is by JSON.parsing things within parenthesis,
* store each result in an array (<var>allArgsSorted</var>),
* and removing parenthesis from the final string (<var>keysNoArgs</var>).
*
* Then, once we removed all parenthesis, we access
* each property in the final string (using the dot notation)
* and start replacing values. And since arguments were
* removed from the final string, we just have to worry
* for checking if the accessed property is a function and
* evaluate them with the stored arguments in order.
*/
return matchNested(keys, '(', ')', function (matched, detail) {
var enclosed = detail.enclosed;
var args;
if (enclosed !== null) {
args = parseArguments(enclosed, data);
// Store apart.
allArgsSorted.push(args);
return matched;
}
// Replace values using the dot notation.
return matched.split('.').reduce(function (context, keyNoArgs) {
var contextObj = Object(context);
var key = keyNoArgs.trim();
var value = (key in contextObj
? contextObj[key]
: key in String.prototype
? String.prototype[key]
// Replace reserved keywords, numbers, objects, ...
: (
// If the given data is an object and the key is not undefined.
(typeof context === 'object' && context !== null)
&& typeof key !== 'undefined' && key !== 'undefined'
)
? parseJSON(key)
// Otherwise, return undefined.
: void 0
);
var result = value;
var fn, args;
/**
* Since parenthesis where removed, replacing
* arguments is as simple as evaluating this
* value with the arguments stored previously.
*/
if (typeof value === 'function') {
fn = value;
args = allArgsSorted.pop();
result = fn.apply(contextObj, args);
}
return result;
}, data);
});
}
/**
* Templarize elements easily.<br>
*
* <aside class='note'>
* <h3>Be careful:</h3>
* <p>Use <var>just.View</var> carefully and/or with a
* strong <a href='https://content-security-policy.com' rel='noopener noreferrer' target='_blank'>CSP policy</a>,
* as it replicates DOM elements found on each update. If templates are modified
* after writing them, all clones will suffer from those modifications.</p>
* </aside>
*
* @class
* @memberof just
*
* @param {?object} options - Any {@link just.View} property.
* @param {just.View#id} options.id - Use either this or <var>element</var>.
* @param {just.View#element} options.element - Use either this or <var>id</var>.
* @param {just.View#data} options.data - Data available on all updates for this view.
* @param {?string|just.View#attributes} [options.attributes=data-var] - <span id='~options~attributes'></span>Set it to a string to use it as a prefix or set it to an object with {@link just.View#attributes|this properties}.
*
* @example <caption>Generate elements based on one element.</caption>
* <html>
* <body>
* <ol>
* <li
* id='item'
* class='template'
* data-var-for='item in items as data-item'
* data-item-if='visible'
* hidden>
* <span
* data-item='${loop.index}. ${capitalize(item.text)} is visible!'
* data-item-attr='{
* "title": "Updated: ${updated}."
* }'></span>
* </li>
* </ol>
* <script src='/just.js'></script>
* <script>
* var view = new just.View({
* id: 'item',
* data: {
* capitalize: function (string) { return string[0].toUpperCase() + string.slice(1).toLowerCase(); },
* items: [{
* visible: true,
* text: 'first item'
* }, {
* visible: false,
* text: 'second item'
* }, {
* visible: true,
* text: 'third item'
* }]
* }
* });
*
* // Do some work...
*
* view.refresh({
* updated: new Date().toString()
* });
* </script>
* </body>
* </html>
*/
function View (options) {
var attributes = Object(options).attributes;
var attributesPrefix = (typeof attributes === 'string'
? attributes
: 'data-var'
);
var props = defaults(options, /** @lends just.View# */{
/**
* Id for the template element.
* @type {?string}
*/
'id': null,
/**
* The template element.
* @type {?Node}
*/
'element': null,
/**
* Data for this instance. Available on all updates.
* @type {?object}
*/
'data': {},
/**
* Updatable attributes. I.e: attributes that will be
* updated when {@link just.View#update} gets called.
*
* <aside class='note'>
* <p><code>${prefix}</code> is the <a href='#~options~attributes'><var>attributes</var> argument's string</a> or "data-var".</p>
* </note>
*
* @type {?object}
* @property {string} [var=${prefix}] - The attribute for text replacements.
* @property {string} [html=${prefix}-html] - The attribute for html replacements.
* @property {string} [attr=${prefix}-attr] - The attribute for attribute replacements.
* @property {string} [if=${prefix}-if] - The attribute for conditional/if replacements.
* @property {string} [as=${prefix}-as] - The attribute for alias replacements. Scoping is not supported yet.
* @property {string} [for=${prefix}-for] - The attribute for loops replacements. Only arrays are supported now.
* @property {string} [on=${prefix}-on] - The attribute for listener replacements.
*/
'attributes': {
'var': attributesPrefix,
'html': attributesPrefix + '-html',
'attr': attributesPrefix + '-attr',
'if': attributesPrefix + '-if',
'alias': attributesPrefix + '-as',
'for': attributesPrefix + '-for',
'on': attributesPrefix + '-on'
}
}, {'ignoreNull': true});
assign(this, props, /** @lends just.View# */{
/**
* Previous data set after a {@link just.View#update}.
* @type {?object}
*/
'previousData': null
});
/**
* Original properties for this instance.
* @type {?object}
*/
this.original = assign({}, this);
}
defineProperties(View, /** @lends just.View */{
/**
* Default attribute to query elements in {@link just.View.init}.
*
* @type {string}
* @readonly
*/
'INIT_ATTRIBUTE_NAME': 'data-just-View',
/**
* Data available for all instances of {@link just.View}.
*
* @type {object}
* @readonly
*/
'globals': {},
/**
* Find elements with the {@link just.View.INIT_ATTRIBUTE_NAME} attribute,
* parse its value as json, and call {@link just.View} with those options.<br>
*
* <aside class='note'>
* <h3>A few things to consider:</h3>
* <ul>
* <li>
* <p><var>options</var> support (nested) data replacement, using the <code>${}</code> sintax:</p>
* <ul>
* <li>
* <p>You can use <code>{"data": {"${key}": ["${get.value(0)}"]}}</code>
* to replace <code>${key}</code>, and <code>${get.value(0)}</code> with your own values
* defined in <var>View.globals</var> and <var>options.listeners</var>.</p>
* </li>
* <li>
* <p>You can use <var>this</var> to replace it with the current element. E.g: <code>${this.id}</code>.</p>
* </li>
* <li>
* <p>In current versions, you don't need to enclose <code>${}</code>
* in quotes to replace variables, since that's the only
* way you can replace them on stringified objects. I.e:</p>
* <ul>
* <li><code>{${var}: [${var}]}</code> is valid. (Equivalent to <code>{[var]: var})</code>.</li>
* <li><code>{"${var}": ["${var}"]}</code> is also valid, but different. (Equivalent to <code>{[`${var}`]: `${var}`}</code>).</li>
* <li><code>{var: [var]}</code> is invalid (for now), and will throw an error.</li>
* </ul>
* <p><em>Since replacements are sometimes required, you can use
* that sintax for now, but in the future, that sintax
* is likely to be removed.</em></p>
* </li>
* </ul>
* </li>
* <li>
* <p>Default values for undefined replacements are <code>null</code>:</p>
* <p>E.g: <code>["one", "two", ${three}]</code>. If <var>three</var> is undefined, the
* result will be <code>["one", "two", null]</code>.</p>
* <p>E.g: <code>"Hello ${world}!"</code>. If <var>world</var> is undefined, the result
* will be <code>"Hello null!"</code>.</p>
* </li>
* </ul>
* </aside>
*
* @param {object} options
* @param {object} options.listeners - Listeners for the {@link View#attachListeners} call.
* @example <caption>Generate elements based on one element using minimum javascript.</caption>
* <html>
* <body
* data-just-View='{"element": ${this}}'
* data-var-on='{"init": "onInit"}'>
* <ol>
* <li
* id='item'
* class='template'
* data-var-for='item in items as data-item'
* data-item-if='visible'
* data-just-View='{
* "element": ${this},
* "data": {
* "items": [{
* "visible": true,
* "text": "first item"
* }, {
* "visible": false,
* "text": "second item"
* }, {
* "visible": true,
* "text": "third item"
* }]
* }
* }'
* hidden>
* <span
* data-item='${loop.index}. ${capitalize(item.text)} is visible!'
* data-item-attr='{
* "title": "Updated: ${updated}."
* }'></span>
* </li>
* </ol>
* <script src='/just.js'></script>
* <script>
* just.View.init({
* listeners: {
* onInit: function (e) {
* // This will refresh all [data-var] attributes.
* this.view.refresh(e.detail);
* }
* }
* });
*
* // Trigger the "init" event:
* document.body.dispatchEvent(
* new CustomEvent('init', {
* detail: {
* updated: new Date().toString()
* }
* })
* );
* </script>
* </body>
* </html>
* @returns {View[]} The created views.
*/
'init': function (options) {
var opts = defaults(options, {
'listeners': {}
});
var listeners = opts.listeners;
var attributeName = View.INIT_ATTRIBUTE_NAME;
var elements = findElements('[' + attributeName + ']');
var data = assign({}, View.globals, listeners);
return elements.map(function (element) {
var attributeValue = element.getAttribute(attributeName);
var nestedVarsData = assign({'this': element}, data);
var stringifiedJSON = View.replaceVars(attributeValue, nestedVarsData, null);
var options = stringToJSON(stringifiedJSON);
var view = new View(options).attachListeners(listeners);
// Store/Cache it.
element.view = view;
return view;
});
},
/**
* Parse an attribute as a json and set keys as
* event names/types and values as listeners.
*
* Values are {@link just.View~access|accessable} properties
* that require a function as final value.
*
* @param {Node} element - The target element.
* @param {!object} data - Data for the accessable properties, containing the listeners.
* @param {!string} attributeName - Name of the attribute that contains the parseable json.
* @returns {!object} The attached listeners, with <var>event.type</var>s as keys.
*/
'attachListeners': function attachListeners (element, data, attributeName) {
var attributeValue = element.getAttribute(attributeName);
var attachedListeners = {};
var json;
if (attributeValue) {
json = stringToJSON(attributeValue);
eachProperty(json, function (value, eventType) {
var properties = value.split('.');
// Remove the last property to prevent executing the function on View~access().
var lastProperty = properties.pop();
var property = properties.join('.');
// Once accessed, we can safely access the last property to return the listener.
var listener = (property
? access(property, data)[lastProperty]
: data[lastProperty]
);
addEventListener(this, eventType, listener);
attachedListeners[eventType] = listener;
}, element);
}
return attachedListeners;
},
/**
* Access to an object and return its value.
*
* @param {?string} condititional - Expected keys splitted by ".".
* Use "!" to negate a expression.
* Use "true" to return true.
* @parma {?object} data - An object with all the properties.
*
* @returns {*|boolean} if the conditional is negated, a boolean.
* Else, the accessed value.
*/
'resolveConditional': function resolveConditional (conditional, data) {
var negate = /^!/.test(conditional);
var nonNegatedConditional = (conditional + '').replace('!', '');
var properties = nonNegatedConditional;
var value = (/^true$/.test(nonNegatedConditional)
? true
: access(properties, data)
);
if (negate) { value = !value; }
return value;
},
/**
* Access to multiple conditionals and return the first value
* that is truthy.
*
* @param {?object|?string} conditionals - An object containing conditions
* (expected properties) as keys, or a string containing a condition
* (a expected property).
* @param {?object} data - An object with all the properties.
* @returns {*} View.resolveConditional()'s returned value.'
*/
'resolveConditionals': function resolveConditionals (conditionals, data) {
var conditionalsObj = Object(conditionals);
var conditional;
var resolvedValue;
if (typeof conditionals === 'string') {
conditional = conditionals;
resolvedValue = View.resolveConditional(conditional, data);
return resolvedValue;
}
eachProperty(conditionalsObj, function (value, conditional) {
var isTruthy = View.resolveConditional(conditional, data);
// Return at first match.
if (isTruthy) { return (resolvedValue = value); }
});
return resolvedValue;
},
/**
* Replace placeholders (<code>${}</code>, eg. <code>${deep.deeper}</code>) within a string.<br>
*
* <aside class='note'>
* <h3>A few things to consider:</h3>
* <p>The following is supported:</p>
* <ul>
* <li>Functions<sup><a href='#.replaceVars[1]'>[1]</a></sup>: <code>${deep.deeper(1, "", myVar, ...)}</code></li>
* <li>String methods: <code>${do.trim().replace('', '')}</code>.</li>
* <li>Deep replacements: <code>${a.b.c}</code> or <code>${a.b().c()}</code></li>
* </ul>
* <footer><p><span id='.replaceVars[1]'>[1]</span>: Neither functions nor ES6+ things as arguments
* (like Symbols or all that stuff) are supported yet.</p></footer>
* </aside>
*
* @param {?string|?object} value - Some text or an object.
* If an object is given, it will {@link just.View.resolveConditionals} first,
* then replace <code>${placeholders}</code> within the accessed value.
* @param {?object} data - An object containing the data to be replaced.
* @param {*} defaultValue - By default, it skips replacements if some accessed value is undefined.
* Any other value will be stringified (returned to the String#replace function).
* @example <caption>Using a string</caption>
* just.View.replaceVars('${splitted.property}!', {
* 'splitted': {'property': 'hey'}
* }); // > "hey!"
*
* @example <caption>Using an object</caption>
* just.View.replaceVars({
* 'a': 'Show ${a}',
* 'b': 'Show ${b}'
* }, {'b': 'me (b)'}); // > "Show me (b)"
*
* @example <caption>Inexistent property</caption>
* just.View.replaceVars("Don't replace ${me}!") // "Don't replace ${me}!"
*
* @example <caption>Setting a default value for an inexistent property</caption>
* just.View.replaceVars('Replace ${me}', null, 'who?') // "Replace who?"
*
* @returns {string} The replaced string.
* If some value is undefined, it won't be replaced at all.
*/
'replaceVars': function replaceVars (value, data, defaultValue) {
var text = String(typeof value === 'object'
? View.resolveConditionals(value, data)
: value
);
return text.replace(/(\$\{[^(]+\()([^)]+)(\)\})/g, function encodeFnArgs (
$0, $1, $2, $3) {
return $1 + encodeURI($2) + $3;
}).replace(/\$\{([^}]+)\}/g, function replacePlaceholders ($0, $1) {
var key = decodeURI($1);
var value = access(key, data);
var placeholder = decodeURI($0);
var defaultReplacement = (typeof defaultValue !== 'undefined'
? defaultValue
: placeholder
);
var isDefined = typeof value !== 'undefined';
var requiresQuotes = isDefined
&& typeof value !== 'string'
&& typeof value !== 'number'
&& value !== null;
return (isDefined
? (requiresQuotes ? JSON.stringify(value) : value)
: defaultReplacement
);
});
},
/**
* A function to set the updated value.
*
* @param {Element} element - The target element.
* @param {string} text - The updated text.
* @return {boolean} true if updated, false otherwise.
* @typedef {function} just.View~updateVars_setter
*/
/**
* Update the element's text if the attribute's value
* is different from the accessed value.
*
* @param {Element} element - The target element.
* @param {?object} data - Some object.
* @param {?string} attributeName - The name for the queried attribute.
* @param {?just.View~updateVars_setter} [setter=element.textContent] - If set,
* a function to update the element's text. Expects a boolean to be returned.
* Else, element.textContent will be used to set the updated value and return true.
*
* @return {boolean} true if the value was updated, false otherwise.
* Any other value will be casted to a Boolean.
*/
'updateVars': function updateVars (element, data, attributeName, setter) {
var set = defaults(setter, function (element, text) {
element.textContent = text;
return true;
});
var attribute = element.getAttribute(attributeName);
var text;
if (!attribute) { return false; }
data = Object(data);
if (!('this' in data)) { data.this = element; }
text = View.replaceVars(attribute, data);
return Boolean(text !== attribute
? set(element, text)
: false
);
},
/**
* Update the element's markup using {@link just.View.updateVars}
* and <code>element.innerHTML</code>.
*
* @param {Element} element - The target element.
* @param {?object} data - Some object.
* @param {?string} attributeName - The name for the queried attribute.
*
* @return {boolean} true if the value was updated, false otherwise.
*/
'updateHtmlVars': function updateHtmlVars (element, data, attributeName) {
return View.updateVars(element, data, attributeName, function (element, html) {
element.innerHTML = html;
return true;
});
},
/**
* Show/Hide an element (by setting/removing the [hidden] attribute)
* after {@link just.View.resolveConditionals|evaluating the conditional}
* found in the given <var>attribute</var>.
*
* @param {Element} element - The target element.
* @param {?object} data - Some object.
* @param {string} attributeName - The name for the queried attribute.
*
* @return {boolean} True if resolved (and hidden), false otherwise.
*/
'updateConditionals': function updateConditionals (element, data, attributeName) {
var attribute = element.getAttribute(attributeName);
var parentNode = element.parentNode;
var value;
if (!parentNode || !attribute) { return false; }
value = View.resolveConditionals(attribute, data);
if (!value) { element.setAttribute('hidden', ''); }
else { element.removeAttribute('hidden'); }
return Boolean(value);
},
/**
* Create dynamic attributes after {@link just.View.replaceVars|replacing variables}
* in values.
*
* Please note that null/undefined values won't be replaced.
*
* @param {Element} element - The target element.
* @param {?object} data - Some object.
* @param {?string} attributeName - The name for attribute containing a stringified json.
*
* @return {boolean} true if the attribute contains some value, false otherwise.
*/
'updateAttributes': function updateAttributes (element, data, attributeName) {
var attribute = element.getAttribute(attributeName);
var attributes;
if (!attribute) { return false; }
attributes = stringToJSON(attribute);
eachProperty(attributes, function (attribute, key) {
var value = View.replaceVars(attribute, data);
// Don't save null or undefined values on attributes.
if (/^(?:null|undefined)$/.test(value)) { return; }
this.setAttribute(key, value);
}, element);
return true;
},
/**
* Define aliases using a stringified JSON from an element attribute;
* access object values and set keys as alias.
*
* @param {Element} element - The target element.
* @param {?object} data - Some object.
* @param {?string} attributeName - The name for attribute containing a stringified json.
*
* @return {!object} An object containing keys as alias.
*/
'getAliases': function getAliases (element, data, attributeName) {
var attribute = element.getAttribute(attributeName);
var aliases = {};
var json;
if (attribute) {
json = stringToJSON(attribute);
eachProperty(json, function (value, key) {
this[key] = access(value, data);
}, aliases);
}
return aliases;
},
/**
* Expression in the format:
* <code>"currentItem in accessed.array[ as updatableAttribute][,[ cache=true]]"</code>.
*
* <p>Where text enclosed in brackets is optional, and:</p>
* <ul>
* <li><var>currentItem</var> is the property containing the current iteration data.
* <li><var>accessed.array</var> is a property to be {@link just.View.access|accessed} that contains an array as value.
* <li><var>updatableAttribute</var> is the name of the attribute that will be updated afterwards by {@link View#update}.
* <li>Setting <var>cache</var> will update existing elements, using each element as template. By default, this is <var>true</var>
* because it improves performance, but it also means that new modifications to each element will be replicated. If you set this
* to <var>false</var>, all generated elements will be removed before updating them, causing new modifications to be removed,
* but also hurting performance.
* </ul>
*
* @example
* "item in some.items"
* // Will iterate over `some.items`, set `item` to each element (some.items[0], some.items[1], and so on...), and make `item` available under the default attribute.
*
* @example
* "item in some.items as data-item"
* // Will iterate over `some.items`, set `item` to each element, and make `item` available under the [data-item] attribute only.
*
* @example
* "item in some.items as data-item, cache=false"
* // Will iterate over `some.items`, set `item` to each element, make `item` available under the [data-item] attribute only, and recreate each element instead of updating it.
*
* @typedef {string} just.View.updateLoops_expression
*/
/**
* Loop data. Contains loop data for the current iteration.
*
* @typedef {!object} just.View.updateLoops_loopData
* @property {number} index - The current index.
* @property {array} array - The array.
* @property {number} length - The array's length.
* @property {number} left - Left elements' count.
*/
/**
* Iterate over an array to create multiple elements
* based on a given template (<var>element</var>),
* append them in order, and update each generated element.<br>
*
* <aside class='note'>
* <h3>A few things to consider:</h3>
* <ul>
* <li>New elements will contain the <var>element</var>'s id as a class,
* the "template" class will be removed, and the "hidden" attribute
* will be removed too.</li>
* <li>Loop data is exposed under the <var>loop</var> property on the updatable elements.
* See {@link just.View.updateLoops_loopData}.</li>
* </ul>
* </aside>
*
* @param {Node} element - The target element.
* @param {Object} data - The data.
* @param {string} attributeName - The attribute containing the {@link just.View.updateLoops_expression|loop expression}.
*
* @returns {?View[]} The updated views or null.
*/
'updateLoops': function updateLoops (element, data, attributeName) {
var attributeValue = element.getAttribute(attributeName);
var attributeParts = (attributeValue || '').split(/\s*,\s*/);
var expression = attributeParts[0];
var opts = (attributeParts[1] || '').split(' ').reduce(function (options, option) {
var parts = option.split('=');
var key = parts[0];
var value = parts[1];
options[key] = (/false/.test(value)
? false
: true
);
return options;
}, {
'cache': true // Cache by default.
});
var cache = opts.cache;
var objectProperty, varName, newAttributeName, object;
// var in data.property as attribute
if (/(\S+)\s+in\s+(\S+)(?:\s+as\s+(\S+))?/i.test(expression)) {
varName = RegExp.$1;
objectProperty = RegExp.$2;
/*
* Use "data-x" attribute in 'data-var-for="value in values as data-x"'
* or "data-var-for-value" in 'data-var-for="value in values"'.
*/
newAttributeName = RegExp.$3 || attributeName + '-' + varName;
object = access(objectProperty, data) || [];
if (Array.isArray(object)) {
// Loop each element of data.property
return (function (array) {
/**
* Prefer the template's parent node over the element's parent node
* to avoid empty values for element.parentNode (when element
* is not connected to the DOM, for example).
*/
var parent = (element.view
? element.view.original.element.parentNode
: element.parentNode
);
var arrayLength = array.length;
var children = [].slice.call(parent.children);
var viewData = assign({}, data);
var cachedViews = children.reduce(function (views, child) {
var cachedView = child.view;
if (cachedView && cachedView.element !== element) { views.push(cachedView); }
return views;
}, []);
var cachedView, view, child;
while (cache
// Remove extra elements to match array.length.
? cachedViews.length > arrayLength
// Remove all elements to recreate them.
: cachedViews.length > 0
) {
cachedView = cachedViews.pop();
child = cachedView.element;
try { child.parentNode.removeChild(child); }
catch (e) { console.error(e); }
}
// Create necessary elements to match array.length.
while (cachedViews.length < arrayLength) {
view = new View({
'element': element,
'attributes': newAttributeName
}).create().append(parent);
// Remove loop attribute to prevent recursive looping on update.
view.element.removeAttribute(attributeName);
// Cache.
view.element.view = view;
cachedViews.push(view);
}
// Once cachedViews.length and array.length are the same, update views as usual.
return cachedViews.map(function (cachedView, i) {
viewData[varName] = array[i];
viewData.this = cachedView.element;
viewData.loop = {
'index': i,
'array': array,
'length': arrayLength,
'left': arrayLength - i
};
return cachedView.update(viewData);
});
})(object);
}
}
return null;
}
});
defineProperties(View.prototype, /** @lends just.View# */{
/**
* @returns {@link just.View#element} or query element by {@link just.View#id}.
*/
'getElement': function getElement () {
return this.element || document.getElementById(this.id);
},
/**
* Merges all available data into one object in the following
* order: {@link just.View.globals}, {@link just.View#data}, <var>currentData</var>, and {@link just.View.getAliases|aliases}.
*
* @param {!object} currentData - It merges this object after globals, and locals, and before setting aliases.
* @returns {!object}
*/
'getData': function getAvailableData (currentData) {
var element = this.getElement();
var globals = View.globals;
var locals = this.data;
var attributeForAliases = this.attributes.alias;
var elementsWithAliases = findElements('[' + attributeForAliases + ']', element)
.concat(element);
return elementsWithAliases.reduce(function (data, element) {
return assign(data, View.getAliases(
element,
data,
attributeForAliases
));
}, assign({}, globals, locals, currentData));
},
/**
* Find all elements that contain a {@link just.View.attributes|supported attribute}
* and return them.
*
* @param {?Node} [container|document]
* @returns {Node[]} All matching elements within the given container
*/
'getUpdatables': function queryAllUpdatables (container) {
var attributes = this.attributes;
var attributeForVars = attributes.var;
var attributeForHtml = attributes.html;
var attributeForIf = attributes.if;
var attributeForAttributes = attributes.attr;
var attributeForLoops = attributes.for;
return findElements([
'[' + attributeForVars + ']',
'[' + attributeForHtml + ']',
'[' + attributeForIf + ']',
'[' + attributeForAttributes + ']',
'[' + attributeForLoops + ']'
].join(','), container);
},
/**
* Create a clone of an {@link just.View#getElement|element},
* remove the .template class, the [hidden] attribute, and the [id]
* after setting it as a class on the clon.
*
* Set {@link just.View#element} to the new clon and return
* the current instance.
*
* @throws {TypeError} if {@link just.View#getElement|the template} is not a Node.
* @chainable
*/
'create': function createTemplate () {
var template = this.getElement();
var templateClon;
var templateClonID;
if (!(template instanceof Node)) { throw new TypeError('You must provide a valid Node using #id or #template.'); }
templateClon = template.cloneNode(true);
templateClonID = templateClon.id;
templateClon.classList.remove('template');
if (templateClonID) { templateClon.classList.add(templateClonID); }
templateClon.removeAttribute('hidden');
templateClon.removeAttribute('id');
this.element = templateClon;
return this;
},
/**
* Update all updatable elements by querying them and calling
* all possible update* functions, like:
* - {@link just.View.updateConditionals}.
* - {@link just.View.updateAttributes}.
* - {@link just.View.updateHtmlVars}.
* - {@link just.View.updateVars}.
* - {@link just.View.updateLoops}.
* (In that order).
*
* @param {*} data - Data for the update. Merged with {@link just.View#getData()}.
* @param {?function} skip - A function called on each updatable element.
* if a truthy value is returned, the update won't take place.
* @chainable
*/
'update': function updateTemplate (data, skip) {
var element = this.getElement();
var allData = this.getData(data);
var attributes = this.attributes;
var attributeForIf = attributes.if;
var attributeForAttributes = attributes.attr;
var attributeForHtml = attributes.html;
var attributeForVars = attributes.var;
var attributeForLoops = attributes.for;
var updatableElements;
if (!element) { return this; }
this.previousData = data;
updatableElements = this
.getUpdatables(element)
.concat(element);
updatableElements.forEach(function (element) {
if (typeof skip === 'function' && skip(element)) { return; }
View.updateConditionals(element, allData, attributeForIf);
View.updateAttributes(element, allData, attributeForAttributes);
View.updateHtmlVars(element, allData, attributeForHtml);
View.updateVars(element, allData, attributeForVars);
View.updateLoops(element, allData, attributeForLoops);
});
return this;
},
/**
* Update the view using {@link just.View#previousData} (set
* on {@link just.View#update|update}) and <var>newData</var>.
*
* Useful to update the view with previous values or
* update only some properties, after a normal {@link just.View#update|update}.
*
* @param {*} newData - Any new data.
* @param {just.View#update~skip} skip - {@link just.View#update|skip} argument.
* @chainable
*/
'refresh': function (newData, skip) {
var previousData = this.previousData;
var data = assign({}, previousData, newData);
this.update(data, skip);
return this;
},
/**
* Insert {@link just.View#element} at the given <var>position</var>
* into the given <var>container</var>.
*
* @param {string|object<{before: Node}>} position -
* - <code>"before"</code> will insert the element before the first child.
* - <code>{"before": Node}</code> will insert the element before the given Node.
* - else (other values): will use <code>appendChild()</code> by default.
* @param {?Node} container - The Node that will contain the element.
*
* @throws {TypeError} if a container can't be guessed.
* @chainable
*/
'insert': function insert (position, container) {
var element = this.getElement();
var wrapper = container || Object(this.id
? document.getElementById(this.id)
: this.element
).parentNode;
var positionObj = Object(position);
if (!(wrapper instanceof Node)) { throw new TypeError('Please provide a container.'); }
else if (position === 'before' || 'before' in positionObj) { wrapper.insertBefore(element, positionObj.before || wrapper.firstChild); }
else { wrapper.appendChild(element); }
return this;
},
/**
* Call {@link just.View#insert} to perform an append
* of the current template element.
*
* @param {?Node} container
*/
'append': function appendTo (container) {
return this.insert('after', container);
},
/**
* Call {@link just.View#insert} to perform an prepend
* of the current template element, at the beginning.
*
* @param {?Node} container
*/
'prepend': function prependTo (container) {
return this.insert('before', container);
},
/**
* Restore constructor to its default value.
* Use the #original property to restore it.
*
* @chainable
*/
'reset': function resetProperties () {
var originalProperties = this.original;
assign(this, originalProperties);
return this;
},
/**
* Call {@link just.View.attachListeners}.
*
* @param {?object} listeners - An object in the format: <code>{eventType: fn}</code>.
* @chainable
*/
'attachListeners': function attachListeners (listeners) {
var element = this.getElement();
var attributeName = this.attributes.on;
var data = this.getData(listeners);
View.attachListeners(element, data, attributeName);
return this;
}
});
return View;
})();
set('View', View);
set('version', '1.2.0'); set('just', just);
return just;
});