MTShop/web/modules/contrib/blazy/js/dblazy.js

2111 lines
52 KiB
JavaScript

/**
* @file
* This file contains common jQuery replacement methods for vanilla ones to DRY.
*
* Cherries by @toddmotto, @cferdinandi, @adamfschwartz, @daniellmb, Cash,
* underscore.
*
* Some dup wrappers are meant to DRY with null checks aka poorman null safety.
* The rest are convenient to avoid object instantiation ($()) and to preserve
* old behaviors pre Blazy 2.6 till all codebase are migrated as needed.
* A few dups are still valid for single vs. chained element loop or queries.
*
* @todo use Cash for better DOM queries, or any core libraries when available.
* @todo remove unneeded dup methods once all codebase migrated.
* @todo move more DOM methods into blazy.dom.js to make it ditchable for Cash.
* @todo when IE gone, https://caniuse.com/dom-manip-convenience
*/
/* global define, module */
(function (_win, _doc) {
'use strict';
var ns = 'dblazy';
var extend = Object.assign;
var _aProto = Array.prototype;
var _oProto = Object.prototype;
var _toString = _oProto.toString;
var _splice = _aProto.splice;
var _some = _aProto.some;
var _symbol = typeof Symbol !== 'undefined' && Symbol;
var _isJq = 'jQuery' in _win;
var _isCash = 'cash' in _win;
var _class = 'class';
var _add = 'add';
var _remove = 'remove';
var _has = 'has';
var _get = 'get';
var _set = 'set';
var _width = 'width';
var _uWidth = 'Width';
var _clientWidth = 'client' + _uWidth;
var _scroll = 'scroll';
var _iterator = 'iterator';
var _observer = 'Observer';
var _dashAlphaRe = /-([a-z])/g;
var _cssVariableRe = /^--/;
var _wsRe = /[\11\12\14\15\40]+/;
var _dataOnce = 'data-once';
var _storage = _win.localStorage;
var _events = {};
// The largest integer that can be represented exactly.
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
/**
* Object for public APIs where dBlazy stands for drupalBlazy.
*
* @namespace
*
* @return {dBlazy}
* Returns this instance.
*/
var dBlazy = function () {
function dBlazy(selector, ctx) {
var me = this;
me.name = ns;
if (!selector) {
return;
}
if (isMe(selector)) {
return selector;
}
var els = selector;
if (isStr(selector)) {
els = findAll(context(ctx), selector);
if (!els.length) {
return;
}
}
else if (isFun(selector)) {
return me.ready(selector);
}
if (els.nodeType || els === _win) {
els = [els];
}
var len = me.length = els.length;
for (var i = 0; i < len; i++) {
me[i] = els[i];
}
}
dBlazy.prototype.init = function (selector, ctx) {
var instance = new dBlazy(selector, ctx);
if (isElm(selector)) {
if (!selector.idblazy) {
selector.idblazy = instance;
}
return selector.idblazy;
}
return instance;
};
return dBlazy;
}();
// Cache our prototype.
var fn = dBlazy.prototype;
// Alias instantiation for a shortcut like jQuery $(selector, context).
var db = fn.init;
db.fn = db.prototype = fn;
fn.length = 0;
// Ensuring a db collection gets printed as array-like in Chrome's devtools.
fn.splice = _splice;
// IE9 knows not this.
if (_symbol) {
// Ensuring a db collection is iterable.
// @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator
fn[_symbol[_iterator]] = _aProto[_symbol[_iterator]];
}
/**
* Excecutes chainable callback to avoid unnecessary loop unless required.
*
* @private
*
* @param {!Function} cb
* The calback function.
*
* @return {Object}
* The current dBlazy collection object.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
*/
function chain(cb) {
var me = this;
// Ok, this is insanely me.
me = isMe(me) ? me : db(me);
var ln = me.length;
if (isFun(cb)) {
if (!ln || ln === 1) {
cb(me[0], 0);
}
else {
me.each(cb);
}
}
return me;
}
/**
* Returns a `toString`-based type tester, based on underscore.js.
*
* @private
*
* @param {string} name
* The name to test for its type.
*
* @return {bool}
* True if name matches the _toString result.
*/
function isTag(name) {
var tag = '[object ' + name + ']';
return function (obj) {
return _toString.call(obj) === tag;
};
}
/**
* Generate a function to obtain property `key` from `obj`.
*
* @private
*
* @param {string} key
* The key to test in an object.
*
* @return {mixed}
* String, object, undefined.
*/
function shallowProperty(key) {
return function (obj) {
return isNull(obj) ? void 0 : obj[key];
};
}
/**
* Returns true if the checked property is number.
*
* @private
*
* @param {function} cb
* The callback to test length property.
*
* @return {bool}
* True if argument is property is number.
*/
function checkLength(cb) {
return function (collection) {
var size = cb(collection);
return typeof size === 'number' && size >= 0 && size <= MAX_ARRAY_INDEX;
};
}
// Internal helper to obtain the `length` property of an object.
var getLength = shallowProperty('length');
/**
* Returns true if the argument is an array-like object, NodeList, etc.
*
* @private
*
* @return {bool}
* True if argument is an array-like object.
*/
var isArrayLike = checkLength(getLength);
/**
* Retrieve the names of an object's own properties.
*
* Delegates to ECMAScript 5's native `Object.keys`.
*
* @private
*
* @param {mixed} x
* The x to test for its properties.
*
* @return {array}
* The object keys, or empty array.
*/
function keys(x) {
return !isObj(x) ? [] : Object.keys(x);
}
/**
* Returns true if the x is a dBlazy.
*
* @private
*
* @param {Mixed} x
* The x to check for its type.
*
* @return {bool}
* True if x is an instanceof dBlazy.
*/
function isMe(x) {
return x instanceof dBlazy;
}
/**
* True if the supplied argument is an array.
*
* @private
*
* One of the weird behaviors in JavaScript is the typeof Array is Object.
*
* @param {Mixed} x
* The x to check for its type.
*
* @return {bool}
* True if the argument is an instanceof Array.
*
* @todo refine, like everything else.
*/
function isArr(x) {
// String has length.
if (isStr(x)) {
return false;
}
return x && (Array.isArray(x) || isArrayLike(x));
}
/**
* Returns true if the x is a boolean.
*
* @private
*
* @param {Mixed} x
* The x to check for its type truthy.
*
* @return {bool}
* True if x is an instanceof bool.
*/
function isBool(x) {
return x === true || x === false || _toString.call(x) === '[object Boolean]';
}
/**
* Returns true if the x is an Element.
*
* @private
*
* @param {Mixed} x
* The x to check for its type truthy.
*
* @return {bool}
* True if x is an instanceof Element.
*/
function isElm(x) {
return x && x instanceof Element;
}
/**
* Returns true if the argument is a function.
*
* @private
*
* @return {bool}
* True if argument is an instanceof Function.
*/
var isFun = isTag('Function');
/**
* Returns true if the x is anything falsy.
*
* All values are truthy unless they are defined as falsy (i.e., except for
* false, 0, -0, 0n, "", null, undefined, and NaN).
*
* @private
*
* @param {Mixed} x
* The x to check for its type truthy.
*
* @return {bool}
* True if null, undefined, false or empty string or array.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_NOT
*/
function isEmpty(x) {
if (isNull(x) || isUnd(x) || x === false) {
return true;
}
// Skip expensive `toString`-based checks if `obj` has no `.length`.
var length = getLength(x);
if (typeof length === 'number' && (isArr(x) || isStr(x))) {
return length === 0;
}
return getLength(keys(x)) === 0;
}
/**
* Returns true if the x is a null.
*
* To those curious why this very simple comparasion has a method, check
* out the minified one. It is called 7 times here, but called once at the
* minifid one to just 1 character + 7 (`=== null`) = 14, saving many byte
* codes. Otherwise `=== null` x 7 chracters = 49.
*
* @private
*
* @param {Mixed} x
* The x to check for its type truthy.
*
* @return {bool}
* True if null.
*/
function isNull(x) {
return x === null;
}
/**
* Returns true if the x is a number.
*
* @private
*
* @param {Mixed} x
* The x to check for its type truthy.
*
* @return {bool}
* True if number.
*/
function isNum(x) {
return !isNaN(parseFloat(x)) && isFinite(x);
}
/**
* Returns true if the x is an object.
*
* @private
*
* One of the weird behaviors in JavaScript is the typeof Array is Object.
*
* @param {Mixed} x
* The x to check for its type truthy.
*
* @return {bool}
* True if x is an instanceof Object.
*/
function isObj(x) {
if (!x || typeof x !== 'object') {
return false;
}
// var type = typeof x;
// return type === 'function' || type === 'object' && !!x;
var proto = Object.getPrototypeOf(x);
return isNull(proto) || proto === _oProto;
}
/**
* Returns true if the argument is a string.
*
* @private
*
* @param {Mixed} x
* The x to check for its type string.
*
* @return {bool}
* True if argument is a string.
*/
function isStr(x) {
return x && typeof x === 'string';
}
/**
* Returns true if the x is undefined.
*
* @private
*
* @param {Mixed} x
* The x to check for its type truthy.
*
* @return {bool}
* True if x is undefined.
*/
function isUnd(x) {
return typeof x === 'undefined';
}
/**
* Returns true if the x is window.
*
* @private
*
* @param {Mixed} x
* The x to check for its type truthy.
*
* @return {bool}
* True if x is window.
*/
function isWin(x) {
return !!x && x === x.window;
}
/**
* Returns true if the x is valid for querySelector.
*
* @private
*
* @param {Mixed} x
* The x to check for its type truthy.
*
* @return {bool}
* True if x is valid for querySelector.
*
* 1: Node.ELEMENT_NODE
* 9: Node.DOCUMENT_NODE
* 11: Node.DOCUMENT_FRAGMENT_NODE
* @see https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
*/
function isDoc(x) {
return [9, 11].indexOf(!!x && x.nodeType) !== -1;
}
/**
* Returns true if the x is valid for querySelector.
*
* @private
*
* @param {Mixed} x
* The x to check for its type truthy.
*
* @return {bool}
* True if x is valid for querySelector.
*
* 1: Node.ELEMENT_NODE
* 9: Node.DOCUMENT_NODE
* 11: Node.DOCUMENT_FRAGMENT_NODE
* @see https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
*/
function isQsa(x) {
return [1, 9, 11].indexOf(!!x && x.nodeType) !== -1;
}
/**
* Returns true if the x is valid for event listener.
*
* @private
*
* @param {Mixed} x
* The x to check for its type truthy.
*
* @return {bool}
* True if x is valid for event listener.
*/
function isEvt(x) {
return isQsa(x) || isWin(x);
}
/**
* A not simple forEach() implementation for Arrays, Objects and NodeLists.
*
* @private
*
* @param {Array|Object|NodeList} obj
* Collection of items to iterate.
* @param {Function} cb
* Callback function for each iteration.
* @param {Array|Object|NodeList} scope
* Object/NodeList/Array that forEach is iterating over (aka `this`).
*
* @return {Array}
* Returns this collection.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
* @see https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach
* @todo drop for native [].forEach post D10+ when IE gone from planet earth.
* @todo refactor, unreliable given unexpected properties.
*/
function each(obj, cb, scope) {
if (isFun(obj) || isStr(obj) || isBool(obj) || isNum(obj)) {
return [];
}
// Filter out useless empty array.
if (isArr(obj) && !isUnd(obj.length)) {
var length = obj.length;
if (!length || (length === 1 && obj[0] === ' ')) {
return [];
}
}
// Filter out useless empty object.
if (isObj(obj) && isEmpty(obj)) {
return [];
}
if (_toString.call(obj) === '[object Object]') {
for (var prop in obj) {
if (hasProp(obj, prop)) {
if (prop === 'length' || prop === 'name') {
continue;
}
if (cb.call(scope, obj[prop], prop, obj) === false) {
break;
}
}
}
}
else if (obj) {
if (obj instanceof HTMLCollection) {
obj = _aProto.slice.call(obj);
}
var len = obj.length;
if (len && len === 1 && !isUnd(obj[0])) {
cb.call(scope, obj[0], 0, obj);
}
else {
// Assumes array, at least non-expected objs were blacklisted above.
obj.forEach(cb, scope);
}
}
return obj;
}
/**
* A hasOwnProperty wrapper.
*
* @private
*
* @param {Array|Object|NodeList} obj
* Collection of items to iterate.
* @param {string} prop
* The property nane.
*
* @return {bool}
* Returns true if the property found.
*/
function hasProp(obj, prop) {
return _oProto.hasOwnProperty.call(obj, prop);
}
/**
* A simple wrapper for JSON.parse() for string within data-* attributes.
*
* @private
*
* @param {string} str
* The string to convert into JSON object.
*
* @return {Object}
* The JSON object, or empty in case invalid.
*/
function parse(str) {
try {
return str.length === 0 || str === '1' ? {} : JSON.parse(str);
}
catch (e) {
return {};
}
}
/**
* Converts string/ element to array.
*
* @private
*
* @param {Element|string} x
* The object to make array.
*
* @return {Array}
* The resulting array.
*/
function toArray(x) {
return isArr(x) ? x : [x];
}
function _op(el, op, name, value) {
return el[op + 'Attribute'](name, value);
}
/**
* A forgiving attribute wrapper with fallback mimicking jQuery.attr method.
*
* @private
*
* @param {dBlazy|Array.<Element>|Element} els
* The HTML element(s), or dBlazy instance.
* @param {string|Object} attr
* The attr name, can be a string or object.
* @param {string} defValue
* The default value, can be null or undefined for different intentions.
* @param {string|bool} withDefault
* True if should get with defValue. Or a prefix such as data- for removal.
*
* @return {Object|string}
* The attribute value, or fallback, for getters, or this for setters.
*/
function _attr(els, attr, defValue, withDefault) {
var me = this;
var _undefined = isUnd(defValue);
var _obj = isObj(attr);
var _getter = !_obj && (_undefined || isBool(withDefault));
var prefix = isStr(withDefault) ? withDefault : '';
// No defValue defined, or withDefault set, means a getter.
if (_getter) {
// @todo figure out multi-element getters. Ok for now, as hardly multiple.
var el = els && els.length ? els[0] : els;
if (_undefined) {
defValue = '';
}
return hasAttr(el, attr) ? _op(el, _get, attr) : defValue;
}
var chainCallback = function (el) {
if (!isQsa(el)) {
return _getter ? '' : me;
}
// Passing a key-value pair object means setting multiple attributes once.
if (isObj(attr)) {
each(attr, function (value, key) {
_op(el, _set, prefix + key, value);
});
}
// Since an attribute value null makes no sense, assumes nullify.
else if (isNull(defValue)) {
each(toArray(attr), function (value) {
var name = prefix + value;
if (hasAttr(el, name)) {
_op(el, _remove, name);
}
});
}
else {
// Else a setter.
if (attr === 'src') {
// To minimize unnecessary mutations.
el.src = defValue;
}
else {
_op(el, _set, attr, defValue);
}
}
};
return chain.call(els, chainCallback);
}
/**
* Checks if the element has attribute.
*
* @private
*
* @param {Element} el
* The HTML element.
* @param {string} name
* The attribute name.
*
* @return {bool}
* True if it has the attribute.
*/
function hasAttr(el, name) {
return isQsa(el) && _op(el, _has, name);
}
/**
* A removeAttribute wrapper.
*
* @private
*
* @param {dBlazy|Array.<Element>|Element} els
* The HTML element(s), or dBlazy instance.
* @param {string|Array} attr
* The attr name, or string array.
* @param {string} prefix
* The attribute prefix if any, normally `data-`.
*
* @return {Object}
* This dBlazy object.
*/
function removeAttr(els, attr, prefix) {
return _attr(els, attr, null, prefix || '');
}
/**
* Checks if the element has a class name.
*
* @private
*
* @param {Element} el
* The HTML element.
* @param {string} names
* The class name, can be space-delimited for multiple names.
*
* @return {bool}
* True if it has the class name.
*/
function hasClass(el, names) {
var found = 0;
if (isQsa(el) && isStr(names)) {
names = names.trim();
var _list = el.classList;
var verify = function (name) {
if (_list) {
if (_list.contains(name)) {
found++;
}
}
if (found === 0) {
// SVG may fail classList here.
var check = _attr(el, _class);
if (check && check.match(name)) {
found++;
}
}
};
each(names.trim().split(' '), verify);
}
return found > 0;
}
/**
* Toggles a class, or multiple from an element.
*
* @private
*
* @param {dBlazy|Array.<Element>|Element} els
* The HTML element(s), or dBlazy instance.
* @param {string} name
* The class name, or space-delimited class names.
* @param {string} op
* Whether to add or remove the class.
*
* @return {Object}
* This dBlazy object.
*/
function toggleClass(els, name, op) {
var chainCallback = function (el, i) {
if (isQsa(el)) {
var _list = el.classList;
if (isFun(name)) {
name = name(_op(el, _get, 'class'), i);
}
if (_list && isStr(name)) {
var names = name.trim().split(' ');
if (isUnd(op)) {
names.map(function (value) {
_list.toggle(value);
});
}
else {
_list[op].apply(_list, names);
}
}
}
};
return chain.call(els, chainCallback);
}
/**
* Adds a class, or space-delimited class names to an element.
*
* @private
*
* @param {dBlazy|Array.<Element>|Element} els
* The HTML element(s), or dBlazy instance.
* @param {string} name
* The class name, or space-delimited class names.
*
* @return {Object}
* This dBlazy object.
*/
function addClass(els, name) {
return toggleClass(els, name, _add);
}
/**
* Removes a class, or multiple from an element.
*
* @private
*
* @param {dBlazy|Array.<Element>|Element} els
* The HTML element(s), or dBlazy instance.
* @param {string} name
* The class name, or space-delimited class names.
*
* @return {Object}
* This dBlazy object.
*/
function removeClass(els, name) {
return toggleClass(els, name, _remove);
}
/**
* Checks if a string or element contains substring(s) or children.
*
* @private
*
* Similar to ES6 ::includes, only for oldies.
* Cannot use [].every() since it is not about all or nothing.
*
* @param {Array|Element|string} str
* The source string to test for.
* @param {Array.<Element>|Array.<string>} substr
* The target element(s) or sub-string to check for, can be a string array.
*
* @return {bool}
* True if it has the needle.
*/
function contains(str, substr) {
var found = 0;
if (isElm(str) && isElm(substr)) {
return str !== substr && str.contains(substr);
}
if (isArr(str)) {
return str.indexOf(substr) !== -1;
}
if (isStr(str)) {
each(toArray(substr), function (value) {
if (str.indexOf(value) !== -1) {
found++;
}
});
}
return found > 0;
}
/**
* Escapes special (meta) characters.
*
* @private
*
* @link https://stackoverflow.com/questions/1144783
*
* @param {string} string
* The original source string.
*
* @return {string}
* The modified string.
*/
function escape(string) {
// $& means the whole matched string.
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
}
/**
* Checks whether or not a string begins with another string, case-sensitive.
*
* @private
*
* @param {string} str
* The source string to test for.
* @param {Array.<string>} substr
* The target sub-string to check for, can be a string array.
*
* @return {bool}
* True if it starts with the needle.
*/
function startsWith(str, substr) {
var found = 0;
if (isStr(str)) {
each(toArray(substr), function (value) {
if (str.startsWith(value)) {
found++;
}
});
}
return found > 0;
}
/**
* Removes extra spaces so to keep readable template.
*
* @private
*
* @param {string} string
* The original source string.
*
* @return {string}
* The modified string.
*/
function trimSpaces(string) {
return string.replace(/\\s+/g, ' ').trim();
}
/**
* A forgiving closest for the lazy.
*
* @private
*
* @param {Element} el
* Starting element.
* @param {string} selector
* Selector to match against (class, ID, data attribute, or tag).
*
* @return {Element|Null}
* Returns null if no match found, else the element.
*/
function closest(el, selector) {
return (isElm(el) && isStr(selector)) ? el.closest(selector) : null;
}
/**
* A forgiving matches for the lazy ala jQuery.
*
* @private
*
* @param {Element} el
* The current element.
* @param {string} selector
* Selector to match against (class, ID, data attribute, or tag).
*
* @return {bool}
* Returns true if found, else false.
*
* @see http://caniuse.com/#feat=matchesselector
* @see https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
*/
function is(el, selector) {
if (isElm(el)) {
if (isStr(selector)) {
return el.matches(selector);
}
return isElm(selector) && el === selector;
}
return false;
}
/**
* Check if an element matches the specified HTML tag.
*
* @private
*
* @param {Element} el
* The element to compare.
* @param {string|Array.<string>} tags
* HTML tag(s) to match against.
*
* @return {bool}
* Returns true if matches, else false.
*/
function equal(el, tags) {
if (!el || !el.nodeName) {
return false;
}
return _some.call(toArray(tags), function (tag) {
return el.nodeName.toLowerCase() === tag.toLowerCase();
});
}
/**
* A simple querySelector wrapper.
*
* @private
*
* The only different from jQuery is if a single element found, it returns
* the element so to avoid ugly repeats like elms[0], also to preserve
* common vanilla practice which normally operates on the element directly.
* Alternatively flag the asArray to any value if an array is expected, or
* use the shortcut ::findAll() to be clear.
*
* To check if the expected element is found:
* - use $.isElm(el) which returns a bool.
*
* @param {Element} el
* The parent HTML element.
* @param {string} selector
* The CSS selector or HTML tag to query.
* @param {bool|int} asArray
* Force returning an array if expected to operate on.
*
* @return {?Array.<Element>}
* Empty array if not found, else the expected element(s).
*/
function find(el, selector, asArray) {
if (isQsa(el)) {
// Direct descendant.
var scope = ':scope';
if (isStr(selector) && startsWith(selector, '>')) {
if (!contains(selector, scope)) {
selector = scope + ' ' + selector;
}
}
return isUnd(asArray) && isStr(selector) ? (el.querySelector(selector) || []) : toElms(selector, el);
}
return [];
}
/**
* A simple querySelectorAll wrapper.
*
* To check if the expected elements are found:
* - use regular `els.length`. The length 0 means not found.
*
* @private
*
* @param {Element} el
* The parent HTML element.
* @param {string} selector
* The CSS selector or HTML tag to query.
*
* @return {?Array.<Element>}
* Empty array if not found, else the expected elements.
*/
function findAll(el, selector) {
return find(el, selector, 1);
}
/**
* A simple removeChild wrapper.
*
* @private
*
* @param {Element} el
* The HTML element to remove.
*/
function remove(el) {
if (isElm(el)) {
var cn = parent(el);
if (cn) {
cn.removeChild(el);
}
}
}
/**
* Returns true if an IE browser.
*
* @private
*
* @param {Element} el
* The element to check for more contextual property/ feature detection.
*
* @return {bool}
* True if an IE browser.
*/
function ie(el) {
return (isElm(el) && el.currentStyle) || !isUnd(_doc.documentMode);
}
/**
* Returns device pixel ratio.
*
* @private
*
* @return {number}
* Returns the device pixel ratio.
*/
function pixelRatio() {
return _win.devicePixelRatio || 1;
}
/**
* Returns cross-browser window width.
*
* @private
*
* @return {number}
* Returns the window width.
*/
function windowWidth() {
return _win.innerWidth || _doc.documentElement[_clientWidth] || _win.screen[_width];
}
/**
* Returns cross-browser window width and height.
*
* @private
*
* @return {Object}
* Returns the window width and height.
*/
function windowSize() {
return {
width: windowWidth(),
height: _win.innerHeight || _doc.documentElement.clientHeight
};
}
/**
* Returns data from the current active window.
*
* @private
*
* When being resized, the browser gave no data about pixel ratio from desktop
* to mobile, not vice versa. Unless delayed for 4s+, not less, which is of
* course unacceptable. Hence why Blazy never claims to support resizing. The
* best efforts were provided using ResizeObserver since 2.2. including this.
*
* @param {Object.<int, Object>} dataset
* The dataset object must be keyed by window width.
* @param {Object.<string, int|bool>} winData
* Containing ww: windowWidth, and up: to determine min-width or max-width.
*
* @return {Mixed}
* Returns data from the current active window.
*/
function activeWidth(dataset, winData) {
var mobileFirst = winData.up || false;
var _k = keys(dataset);
var xs = _k[0];
var xl = _k[_k.length - 1];
var ww = winData.ww || windowWidth();
var pr = (ww * pixelRatio());
var rw = mobileFirst ? ww : pr;
var mw = function (w) {
// The picture wants <= (approximate), non-picture wants >=, wtf.
return mobileFirst ? parseInt(w, 10) <= rw : parseInt(w, 10) >= rw;
};
var data = _k.filter(mw).map(function (v) {
return dataset[v];
})[mobileFirst ? 'pop' : 'shift']();
return isUnd(data) ? dataset[rw >= xl ? xl : xs] : data;
}
/**
* A simple wrapper for event delegation like jQuery.on().
*
* @private
*
* @param {dBlazy|Array.<Element>|Element} els
* The HTML element(s), or dBlazy instance.
* @param {string} eventName
* The event name to trigger.
* @param {string} selector
* Child selector to match against (class, ID, data attribute, or tag).
* @param {Function} cb
* The callback function.
* @param {Object|bool} params
* The optional param passed into a custom event.
* @param {bool} isCustom
* True, if a custom event, a namespaced like (blazy.done), but considered
* as a whole since there is no event name `blazy`.
*
* @return {Object}
* This dBlazy object.
*/
function on(els, eventName, selector, cb, params, isCustom) {
return toEvent(els, eventName, selector, cb, params, isCustom, _add);
}
/**
* A simple wrapper for event detachment.
*
* @private
*
* @param {dBlazy|Array.<Element>|Element} els
* The HTML element(s), or dBlazy instance.
* @param {string} eventName
* The event name to trigger.
* @param {string} selector
* Child selector to match against (class, ID, data attribute, or tag).
* @param {Function} cb
* The callback function.
* @param {Object|bool} params
* The optional param passed into a custom event.
* @param {bool} isCustom
* True, if a custom event.
*
* @return {Object}
* This dBlazy object.
*/
function off(els, eventName, selector, cb, params, isCustom) {
return toEvent(els, eventName, selector, cb, params, isCustom, _remove);
}
/**
* A simple wrapper for addEventListener once.
*
* @private
*
* @param {dBlazy|Array.<Element>|Element} els
* The HTML element(s), or dBlazy instance.
* @param {string} eventName
* The event name to remove.
* @param {Function} cb
* The callback function.
* @param {bool} isCustom
* True, if a custom event.
*
* @return {Object}
* This dBlazy object.
*/
function one(els, eventName, cb, isCustom) {
return on(els, eventName, cb, {
once: true
}, isCustom);
}
/**
* Checks if image is decoded/ completely loaded.
*
* @private
*
* @param {Image} img
* The Image object.
*
* @return {bool}
* True if the image is loaded.
*/
function isDecoded(img) {
// This is working fine, not a culprit.
return img.decoded || img.complete;
}
/**
* Executes the function once.
*
* @private
*
* @author Daniel Lamb <dlamb.open.source@gmail.com>
* @link https://github.com/daniellmb/once.js
*
* @param {Function} cb
* The executed function.
*
* @return {Object}
* The function result.
*/
function _once(cb) {
var result;
var ran = false;
return function proxy() {
if (ran) {
return result;
}
ran = true;
result = cb.apply(this, arguments);
// For garbage collection.
cb = null;
return result;
};
}
/**
* Process arguments, query the DOM if necessary. Adapted from core/once.
*
* @private
*
* @param {NodeList|Array.<Element>|Element|string} selector
* A NodeList, array of elements, or string.
* @param {Document|Element} ctx
* An element to use as context for querySelectorAll.
*
* @return {Array.<Element>}
* An array of elements to process.
*/
function toElms(selector, ctx) {
// Assume selector is an array-like element unless a string.
var elements = toArray(selector);
if (isStr(selector)) {
ctx = context(ctx);
var check = ctx.querySelector(selector);
elements = isNull(check) ? [] : ctx.querySelectorAll(selector);
}
// Ensures an array is returned and not a NodeList or an Array-like object.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from
return _aProto.slice.call(elements);
}
/**
* A not simple wrapper for the namespaced [add|remove]EventListener.
*
* @private
*
* @param {dBlazy|Array.<Element>|Element} els
* The HTML element(s), or dBlazy instance.
* @param {string} eventName
* The event name, optionally namespaced, to add or remove.
* @param {string|Function} selector
* Child selector to delegate (valid CSS selector). Or a callback.
* @param {Function|Object|bool} cb
* The callback function. Or params passed into on/off like.
* @param {Object|bool} params
* The optional param passed into a custom event. Or isCustom for on/off.
* @param {bool|string} isCustom
* Like namespaced, but not, LHS is not native event. Or add/remove op.
* @param {string|undefined} op
* Whether to add or remove the event. Or undefined for on/off like.
*
* @return {Object}
* This dBlazy object.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
* @see https://caniuse.com/once-event-listener
* @see https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
* @todo automatically handled by its return value.
*/
function toEvent(els, eventName, selector, cb, params, isCustom, op) {
var _cbt = cb;
var _ie = ie();
// Event delegation like on/off.
if (isStr(selector)) {
var shouldPassive = contains(eventName, ['touchstart', _scroll, 'wheel']);
if (isUnd(params)) {
params = _ie ? false : {
capture: !shouldPassive,
passive: shouldPassive
};
}
var onEvent = function (e) {
// @todo handle automatically by its return value.
// e.preventDefault();
// e.stopPropagation();
var t = e.target;
if (is(t, selector)) {
_cbt.call(t, e);
}
else {
while (t && t !== this) {
if (is(t, selector)) {
_cbt.call(t, e);
break;
}
t = t.parentElement || t.parentNode;
}
}
};
cb = onEvent;
}
else {
// Shift one argument if selector is expected as a callback function.
isCustom = params;
params = _cbt;
cb = selector;
}
var chainCallback = function (el) {
if (!isEvt(el)) {
return;
}
var defaults = {
capture: false,
passive: true
};
var _one = false;
var options = params || false;
if (isObj(params)) {
options = extend(defaults, params);
_one = options.once || false;
}
var process = function (e) {
isCustom = isCustom || startsWith(e, ['blazy.', 'bio.']);
var add = op === _add;
var type = (isCustom ? e : e.split('.')[0]).trim();
cb = cb || _events[e];
var _cb = cb;
if (isFun(cb)) {
// See https://caniuse.com/once-event-listener.
if (_one && add && _ie) {
var cbone = function cbone() {
el.removeEventListener(type, cbone, options);
_cb.apply(this, arguments);
};
cb = cbone;
add = false;
}
el[op + 'EventListener'](type, cb, options);
}
// @todo store as namespace to allow easy removal by namespaces.
if (add) {
_events[e] = cb;
}
else {
delete _events[e];
}
};
each(eventName.trim().split(' '), process);
};
return chain.call(els, chainCallback);
}
/**
* A not simple wrapper for triggering event like jQuery.trigger().
*
* @param {dBlazy|Array.<Element>|Element} els
* The HTML element(s), or dBlazy instance.
* @param {string} eventName
* The event name to trigger.
* @param {Object} details
* The optional detail object passed into a custom event detail property.
* @param {Object} param
* The optional param passed into a custom event.
*
* @return {CustomEvent|Event|undefined}
* The CustomEvent or Event object to dispatch.
*
* @see https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createEvent
* @todo namespaced event name, and more refined native event.
*/
function trigger(els, eventName, details, param) {
var chainCallback = function (el) {
var event;
if (!isEvt(el)) {
return event;
}
if (isUnd(details)) {
event = new Event(eventName);
}
else {
// Bubbles to be caught by ancestors. Cancelable to preventDefault.
var data = {
bubbles: true,
cancelable: true,
detail: details || {}
};
if (isObj(param)) {
data = extend(data, param);
}
event = new CustomEvent(eventName, data);
}
el.dispatchEvent(event);
return event;
};
return chain.call(els, chainCallback);
}
// Type methods.
// Wonder why ES6 has alt lambda `=>` for `function`? Compact, to save bytes.
// Kotlin has useless `fun` due to being compiled back to `function`. But ES6
// lambda is true savings unless being transpiled. So these stupid abbr are.
// The contract here is no rigid minds, fun, less bytes. Hail to Linux.
db.isTag = isTag;
db.isArr = isArr;
db.isBool = isBool;
db.isDoc = isDoc;
db.isElm = isElm;
db.isFun = isFun;
db.isEmpty = isEmpty;
db.isNull = isNull;
db.isNum = isNum;
db.isObj = isObj;
db.isStr = isStr;
db.isUnd = isUnd;
db.isEvt = isEvt;
db.isQsa = isQsa;
db.isIo = 'Intersection' + _observer in _win;
db.isMo = 'Mutation' + _observer in _win;
db.isRo = 'Resize' + _observer in _win;
db.isNativeLazy = 'loading' in HTMLImageElement.prototype;
db.isAmd = typeof define === 'function' && define.amd;
db.isWin = isWin;
db._er = -1;
db._ok = 1;
// Collection methods.
db.chain = function (els, cb) {
return chain.call(els, cb);
};
db.each = each;
db.extend = extend;
fn.extend = function (plugins, reverse) {
reverse = reverse || false;
return reverse ? extend(plugins, fn) : extend(fn, plugins);
};
db.hasProp = hasProp;
db.parse = parse;
db.toArray = toArray;
// Attribute methods.
db.hasAttr = hasAttr;
db.attr = _attr.bind(db);
db.removeAttr = removeAttr.bind(db);
// Class name methods.
db.hasClass = hasClass;
db.toggleClass = toggleClass;
db.addClass = addClass;
db.removeClass = removeClass;
// String methods.
db.contains = contains;
db.escape = escape;
db.startsWith = startsWith;
db.trimSpaces = trimSpaces;
// DOM query methods.
db.closest = closest;
db.is = is;
// @todo merge with ::is().
db.equal = equal;
db.find = find;
db.findAll = findAll;
db.remove = remove;
// Window methods.
db.ie = ie;
db.pixelRatio = pixelRatio;
db.windowWidth = windowWidth;
db.windowSize = windowSize;
db.activeWidth = activeWidth;
// Event methods.
db.toEvent = toEvent;
db.on = on;
db.off = off;
db.one = one;
db.trigger = trigger;
// Image methods.
db.isDecoded = isDecoded;
// Similar to core domReady, only public and generic.
function ready(callback) {
var cb = function () {
return setTimeout(callback, 0, db);
};
if (_doc.readyState !== 'loading') {
cb();
}
else {
_doc.addEventListener('DOMContentLoaded', cb);
}
return this;
}
db.ready = ready.bind(db);
/**
* Decodes the image.
*
* @param {Image} img
* The Image object.
*
* @return {Promise}
* The Promise object.
*
* @see https://caniuse.com/promises
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
* @see https://github.com/taylorhakes/promise-polyfill
* @see https://chromestatus.com/feature/5637156160667648
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-decode
*/
db.decode = function (img) {
if (isDecoded(img)) {
return Promise.resolve(img);
}
if ('decode' in img) {
img.decoding = 'async';
return img.decode();
}
return new Promise(function (resolve, reject) {
img.onload = function () {
resolve(img);
};
img.onerror = reject();
});
};
/**
* A wrapper for core/once until D9.2 is a minimum.
*
* @param {Function} cb
* The executed function.
* @param {string} id
* The id of the once call.
* @param {NodeList|Array.<Element>|Element|string} selector
* A NodeList, array of elements, single Element, or a string.
* @param {Document|Element} ctx
* An element to use as context for querySelectorAll.
*
* @return {Array.<Element>}
* An array of elements to process, or empty for old behavior.
*/
function onceCompat(cb, id, selector, ctx) {
var els = [];
// If a string, assumes find once like core/once.
if (isStr(cb)) {
return findOnce(cb, id);
}
// Original once.
if (isUnd(selector)) {
_once(cb);
}
// If extra arguments are provided, assumes regular loop over elements.
else {
els = initOnce(id, selector, ctx);
if (els.length) {
// Already avoids loop for a single item.
each(els, cb);
}
}
return els;
}
db.once = onceCompat;
/**
* A simple wrapper to delay callback function, taken out of blazy library.
*
* Alternative to core Drupal.debounce for D7 compatibility, and easy port.
*
* @param {Function} cb
* The callback function.
* @param {number} minDelay
* The execution delay in milliseconds.
* @param {Object} scope
* The scope of the function to apply to, normally this.
*
* @return {Function}
* The function executed at the specified minDelay.
*/
db.throttle = function (cb, minDelay, scope) {
minDelay = minDelay || 50;
var lastCall = 0;
return function () {
var now = +new Date();
if (now - lastCall < minDelay) {
return;
}
lastCall = now;
cb.apply(scope, arguments);
};
};
/**
* A simple wrapper to delay callback function on window resize.
*
* @link https://github.com/louisremi/jquery-smartresize
*
* @param {Function} cb
* The callback function.
* @param {number} t
* The timeout.
*
* @return {ResizeObserver|Function}
* The ResizeObserver instance, or callback function.
*/
db.resize = function (cb, t) {
// @todo enable later when old projects are updated: lory, extended, etc.
// if (this.isRo) {
// return new ResizeObserver(cb);
// }
_win.onresize = function (e) {
clearTimeout(t);
t = setTimeout(cb.bind(e), 200);
};
return cb;
};
/**
* Replaces string occurances to simplify string templating.
*
* @param {string} string
* The original source string.
* @param {Object.<string, string>} map
* The mapping object.
*
* @return {string}
* The modified string.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll
* @see https://caniuse.com/mdn-javascript_builtins_string_replaceall
* @see https://stackoverflow.com/questions/1144783
* @todo use template string or replaceAll for D10, or D11 at the latest.
*/
db.template = function (string, map) {
for (var key in map) {
if (hasProp(map, key)) {
string = string.replace(new RegExp(escape('$' + key), 'g'), map[key]);
}
}
return trimSpaces(string);
};
/**
* A simple wrapper for context insanity.
*
* Context is unreliable with AJAX contents like product variations, etc.
* This can be null after Colorbox close, or absurd <script> element, likely
* arbitrary, etc.
*
* @param {Document|Element} ctx
* Any element, including weird script element.
*
* @return {Element|Document|DocumentFragment}
* The Element|Document|DocumentFragment to not fail querySelector, etc.
*
* @todo refine core/once expects Element only, or patch it for [1,9,11].
*/
function context(ctx) {
// Weirdo: context may be null after Colorbox close.
ctx = ctx || _doc;
// Checks if a string is given as a context.
if (isStr(ctx)) {
ctx = is(ctx, 'html') ? _doc : _doc.querySelector(ctx);
}
// Prevents problematic _doc.documentElement as the context.
if (is(ctx, 'html')) {
ctx = _doc;
}
// jQuery may pass its array as non-expected context identified by length.
ctx = toElm(ctx);
// Absurd <script> elements which have no children may be spit on AJAX.
if (isQsa(ctx) && ctx.children && ctx.children.length) {
return ctx;
}
// IE9 knows not deprecated HTMLDocument, IE8 does.
return isDoc(ctx) ? ctx : _doc;
}
// Valid elements for querySelector with length: form, select, etc.
function toElm(el) {
var isJq = _isJq && el instanceof _win.jQuery;
var isCash = _isCash && el instanceof _win.cash;
return el && (isMe(el) || isJq || isCash) ? el[0] : el;
}
// Minimum common DOM methods taken and modified from cash.
// @todo refactor or remove dups when everyone uses cash, or vanilla alike.
function camelCase(str) {
return str.replace(_dashAlphaRe, function (match, letter) {
return letter.toUpperCase();
});
}
function isVar(prop) {
return _cssVariableRe.test(prop);
}
// @see https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle
function computeStyle(el, prop, isVariable) {
if (!isElm(el)) {
return;
}
var _style = getComputedStyle(el, null);
if (isUnd(prop)) {
return _style;
}
if (isVariable || isVar(prop)) {
return _style.getPropertyValue(prop) || null;
}
return _style[prop] || el.style[prop];
}
// https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
function rect(el) {
return isElm(el) ? el.getBoundingClientRect() : {};
}
function traverse(el, selector, relative) {
if (isElm(el)) {
var target = el[relative];
if (isUnd(selector)) {
return target;
}
while (target) {
if (is(target, selector) || equal(target, selector)) {
return target;
}
target = target[relative];
}
}
return null;
}
function parent(el, selector) {
return traverse(el, selector, 'parentElement');
}
function prevnext(el, selector, prefix) {
return traverse(el, selector, prefix + 'ElementSibling');
}
function prev(el, selector) {
return prevnext(el, selector, 'previous');
}
function next(el, selector) {
return prevnext(el, selector, 'next');
}
function empty(els) {
var chainCallback = function (el) {
if (isElm(el)) {
while (el.firstChild) {
el.removeChild(el.firstChild);
}
}
};
return chain.call(els, chainCallback);
}
function index(el, parents) {
var i = 0;
if (isElm(el)) {
if (!isUnd(parents)) {
each(toArray(parents), function (sel) {
var check = closest(el, sel);
if (isElm(check)) {
el = check;
return false;
}
});
}
while (!isNull(el = prev(el))) {
i++;
}
}
return i;
}
db.context = context;
db.toElm = toElm;
db.camelCase = camelCase;
db.isVar = isVar;
db.computeStyle = computeStyle;
db.rect = rect;
db.empty = empty;
db.parent = parent;
db.next = next;
db.prev = prev;
db.index = index;
db.keys = keys;
db.create = function (tagName, attrs, html) {
var el = _doc.createElement(tagName);
if (isStr(attrs) || isObj(attrs)) {
if (isStr(attrs)) {
el.className = attrs;
}
else {
_attr(el, attrs);
}
}
if (html) {
html = html.trim();
el.innerHTML = html;
if (tagName === 'template') {
el = el.content.firstChild || el;
}
}
return el;
};
// See https://caniuse.com/?search=localstorage
db.storage = function (key, value, defValue, restore) {
if (_storage) {
if (isUnd(value)) {
return _storage.getItem(key);
}
if (isNull(value)) {
_storage.removeItem(key);
}
else {
try {
_storage.setItem(key, value);
}
catch (e) {
// Reset if (2 - 10MB) quota is exceeded, if value is growing.
_storage.removeItem(key);
// Only makes sense if the value is incremental, not the quota limit.
if (restore) {
_storage.setItem(key, value);
}
}
}
}
return defValue || false;
};
// @todo merge with cash if available.
// if (_isCash) {
// fn.extend(cash.fn, true);
// }
// Collects base prototypes for clarity.
var objs = {
chain: function (cb) {
return chain.call(this, cb);
},
each: function (cb) {
return each(this, cb);
},
ready: function (callback) {
return ready.call(this, callback);
}
};
// Merge base prototypes.
fn.extend(objs);
// @deprecated for shorter ::is(). Hardly used, except lory.
db.matches = is;
// @tbd deprecated for db.each to save bytes. Used by many sub-modules.
db.forEach = each;
// @tbd deprecated for on/off with shifted arguments. Use on/ off instead.
db.bindEvent = on.bind(db);
db.unbindEvent = off.bind(db);
function _filter(selector, elements, apply) {
return elements.filter(function (el) {
var selected = is(el, selector);
if (selected && apply) {
apply(el);
}
return selected;
});
}
db.filter = _filter;
// @todo remove all these when min D9.2, or take the least minimum for BC.
// Be sure to make context Element, or patch it to work with [1,9,11] types
// which distinguish this from core/once as per 2022/2.
// When removed and context issue is fixed, it will be just:
// `db.once = extend(db.once, once);` + `db.once.removeSafely()`.
function elsOnce(selector, ctx) {
return findAll(context(ctx), selector);
}
function selOnce(id) {
return '[' + _dataOnce + '~="' + id + '"]';
}
function updateOnce(el, opts) {
var add = opts.add;
var remove = opts.remove;
var result = [];
if (hasAttr(el, _dataOnce)) {
var ids = _attr(el, _dataOnce).trim().split(_wsRe);
each(ids, function (id) {
if (!contains(result, id) && id !== remove) {
result.push(id);
}
});
}
if (add && !contains(result, add)) {
result.push(add);
}
var value = result.join(' ');
_op(el, value === '' ? _remove : _set, _dataOnce, value);
}
function initOnce(id, selector, ctx) {
return _filter(':not(' + selOnce(id) + ')', elsOnce(selector, ctx), function (el) {
updateOnce(el, {
add: id
});
});
}
function findOnce(id, ctx) {
return elsOnce(!id ? '[' + _dataOnce + ']' : selOnce(id), ctx);
}
if (!db.once.find) {
db.once.find = findOnce;
db.once.filter = function (id, selector, ctx) {
return _filter(selOnce(id), elsOnce(selector, ctx));
};
db.once.remove = function (id, selector, ctx) {
return _filter(
selOnce(id),
elsOnce(selector, ctx),
function (el) {
updateOnce(el, {
remove: id
});
}
);
};
db.once.removeSafely = function (id, selector, ctx) {
var me = this;
var jq = _win.jQuery;
if (me.find(id, ctx).length) {
me.remove(id, selector, ctx);
}
// @todo remove BC for pre core/once when min D9.2:
if (_isJq && jq && jq.fn && isFun(jq.fn.removeOnce)) {
jq(selector, context(ctx)).removeOnce(id);
}
};
}
if (typeof exports !== 'undefined') {
// Node.js.
module.exports = db;
}
else {
// Browser.
_win.dBlazy = db;
}
})(this, this.document);