Source: just.js

/**
 * @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.: &lt;span a='123' data-urls='{"[a]456": "[a]456"}'&gt;&lt;/span&gt;
             *     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;

});