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