/** * @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} 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} 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} 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} 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} 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.|Array.} 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.} 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.} 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.} * 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.} * 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.} dataset * The dataset object must be keyed by window width. * @param {Object.} 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} 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} 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} 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 * @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|string} selector * A NodeList, array of elements, or string. * @param {Document|Element} ctx * An element to use as context for querySelectorAll. * * @return {Array.} * 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} 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} 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|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.} * 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.} 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