/**
* @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;
});