/** * @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 Core utilities for browser environments. * @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); set('version', '1.2.0'); set('just', just); return just; });