UNPKG

@lisn.js/bundles

Version:
1,384 lines (1,321 loc) 546 kB
/*! * LISN.js v1.2.1 * (c) 2025 @AaylaSecura * Released under the MIT License. */ var LISN = (function (exports) { 'use strict'; /** * For minification optimization. * * @module * @ignore * @internal */ const PREFIX = "lisn"; const LOG_PREFIX = "[LISN.js]"; const OBJECT = Object; const SYMBOL = Symbol; const ARRAY = Array; const STRING = String; const MATH = Math; const NUMBER = Number; const PROMISE = Promise; const PI = MATH.PI; const INFINITY = Infinity; const S_ABSOLUTE = "absolute"; const S_FIXED = "fixed"; const S_WIDTH = "width"; const S_HEIGHT = "height"; const S_TOP = "top"; const S_BOTTOM = "bottom"; const S_UP = "up"; const S_DOWN = "down"; const S_LEFT = "left"; const S_RIGHT = "right"; const S_AT = "at"; const S_ABOVE = "above"; const S_BELOW = "below"; const S_IN = "in"; const S_OUT = "out"; const S_NONE = "none"; const S_AMBIGUOUS = "ambiguous"; const S_ADDED = "added"; const S_REMOVED = "removed"; const S_ATTRIBUTE = "attribute"; const S_KEY = "key"; const S_MOUSE = "mouse"; const S_POINTER = "pointer"; const S_TOUCH = "touch"; const S_WHEEL = "wheel"; const S_CLICK = "click"; const S_HOVER = "hover"; const S_PRESS = "press"; const S_SCROLL = "scroll"; const S_ZOOM = "zoom"; const S_DRAG = "drag"; const S_UNKNOWN = "unknown"; const S_SCROLL_TOP = `${S_SCROLL}Top`; const S_SCROLL_LEFT = `${S_SCROLL}Left`; const S_SCROLL_WIDTH = `${S_SCROLL}Width`; const S_SCROLL_HEIGHT = `${S_SCROLL}Height`; const S_CLIENT_WIDTH = "clientWidth"; const S_CLIENT_HEIGHT = "clientHeight"; const S_SCROLL_TOP_FRACTION = `${S_SCROLL}TopFraction`; const S_SCROLL_LEFT_FRACTION = `${S_SCROLL}LeftFraction`; const S_HORIZONTAL = "horizontal"; const S_VERTICAL = "vertical"; const S_SKIP_INITIAL = "skipInitial"; const S_DEBOUNCE_WINDOW = "debounceWindow"; const S_TOGGLE = "toggle"; const S_CANCEL = "cancel"; const S_KEYDOWN = `${S_KEY}${S_DOWN}`; const S_MOUSEUP = `${S_MOUSE}${S_UP}`; const S_MOUSEDOWN = `${S_MOUSE}${S_DOWN}`; const S_POINTERUP = `${S_POINTER}${S_UP}`; const S_POINTERDOWN = `${S_POINTER}${S_DOWN}`; const S_POINTERENTER = `${S_POINTER}enter`; const S_POINTERLEAVE = `${S_POINTER}leave`; const S_POINTERMOVE = `${S_POINTER}move`; const S_POINTERCANCEL = `${S_POINTER}${S_CANCEL}`; const S_TOUCHSTART = `${S_TOUCH}start`; const S_TOUCHEND = `${S_TOUCH}end`; const S_TOUCHMOVE = `${S_TOUCH}move`; const S_TOUCHCANCEL = `${S_TOUCH}${S_CANCEL}`; const S_SELECTSTART = "selectstart"; const S_ATTRIBUTES = "attributes"; const S_CHILD_LIST = "childList"; const S_REVERSE = "reverse"; const S_DISABLED = "disabled"; const S_ARROW = "arrow"; const S_ROLE = "role"; const S_AUTO = "auto"; const S_VISIBLE = "visible"; const ARIA_PREFIX = "aria-"; const S_ARIA_CONTROLS = ARIA_PREFIX + "controls"; const PREFIX_RELATIVE = `${PREFIX}-relative`; const PREFIX_WRAPPER$2 = `${PREFIX}-wrapper`; const PREFIX_WRAPPER_CONTENT = `${PREFIX_WRAPPER$2}-content`; const PREFIX_TRANSITION = `${PREFIX}-transition`; const PREFIX_TRANSITION_DISABLE = `${PREFIX_TRANSITION}__disable`; const PREFIX_HIDE = `${PREFIX}-hide`; const PREFIX_SHOW = `${PREFIX}-show`; const PREFIX_DISPLAY = `${PREFIX}-display`; const PREFIX_UNDISPLAY = `${PREFIX}-undisplay`; const PREFIX_ORIENTATION = `${PREFIX}-orientation`; const PREFIX_GHOST = `${PREFIX}-ghost`; const PREFIX_NO_SELECT = `${PREFIX}-no-select`; const PREFIX_NO_TOUCH_ACTION = `${PREFIX}-no-touch-action`; const PREFIX_NO_WRAP = `${PREFIX}-no-wrap`; const S_ANIMATE = "animate"; const ANIMATE_PREFIX = `${PREFIX}-${S_ANIMATE}__`; const PREFIX_ANIMATE_DISABLE = `${ANIMATE_PREFIX}disable`; const PREFIX_ANIMATE_PAUSE = `${ANIMATE_PREFIX}pause`; const PREFIX_ANIMATE_REVERSE = `${ANIMATE_PREFIX}${S_REVERSE}`; /** * @module Errors */ /** * Base error type emitted by LISN. */ class LisnError extends Error {} /** * Error type emitted for invalid input or incorrect usage of a function. */ class LisnUsageError extends LisnError { constructor(message = "") { super(`${LOG_PREFIX} Incorrect usage: ${message}`); this.name = "LisnUsageError"; } } /** * Error type emitted if an assertion is wrong => report bug. */ class LisnBugError extends LisnError { constructor(message = "") { super(`${LOG_PREFIX} Please report a bug: ${message}`); this.name = "LisnBugError"; } } /** * For minification optimization * * @module * @ignore * @internal */ // credit: underscore.js const root = typeof self === "object" && self.self === self && self || typeof global == "object" && global.global === global && global || Function("return this")() || {}; const kebabToCamelCase$1 = str => str.replace(/-./g, m => toUpperCase(m.charAt(1))); const camelToKebabCase$1 = str => str.replace(/[A-Z][a-z]/g, m => "-" + toLowerCase(m)).replace(/[A-Z]+/, m => "-" + toLowerCase(m)); const prefixName = name => `${PREFIX}-${name}`; const prefixCssVar = name => "--" + prefixName(name); const prefixCssJsVar = name => prefixCssVar("js--" + name); const toDataAttrName = name => `data-${camelToKebabCase$1(name)}`; const toLowerCase = s => s.toLowerCase(); const toUpperCase = s => s.toUpperCase(); const timeNow = Date.now.bind(Date); const timeSince = startTime => timeNow() - startTime; const hasDOM = () => typeof document !== "undefined"; const getWindow = () => window; const getDoc = () => document; const getDocElement = () => getDoc().documentElement; const getDocScrollingElement = () => getDoc().scrollingElement; const getBody = () => getDoc().body; const getReadyState = () => getDoc().readyState; const getPointerType = event => isPointerEvent(event) ? event.pointerType : isMouseEvent(event) ? "mouse" : null; const onAnimationFrame = callback => requestAnimationFrame(callback); const createElement = (tagName, options) => getDoc().createElement(tagName, options); const createButton = (label = "", tag = "button") => { const btn = createElement(tag); setTabIndex(btn); setAttr(btn, S_ROLE, "button"); setAttr(btn, ARIA_PREFIX + "label", label); return btn; }; const isNullish = v => v === undefined || v === null; const isEmpty = v => isNullish(v) || v === ""; const isOfType = (v, tag, checkLevelsUp = 0) => isInstanceOfByClassName(v, tag) || OBJECT.prototype.toString.call(v) === `[object ${tag}]` || checkLevelsUp > 0 && isOfType(getPrototypeOf(v), tag, checkLevelsUp - 1); // Not including function const isObject = v => v !== null && typeof v === "object"; const isPlainObject = v => isObject(v) && (getPrototypeOf(v) === null || getPrototypeOf(getPrototypeOf(v)) === null); const isIterableObject = v => isObject(v) && SYMBOL.iterator in v; const isArray = ARRAY.isArray.bind(ARRAY); const isPrimitive = v => isLiteralString(v) || isSymbol(v) || isLiteralNumber(v) || isBoolean(v) || isNullish(v); // only primitive number const isLiteralNumber = v => typeof v === "number"; const isNumber = v => isOfType(v, "Number"); const isString = v => isOfType(v, "String"); const isLiteralString = v => typeof v === "string"; const isSymbol = v => typeof v === "symbol"; const isBoolean = v => typeof v === "boolean"; const isFunction = v => typeof v === "function" || isOfType(v, "Function"); const isMap = v => isOfType(v, "Map"); const isSet = v => isOfType(v, "Set"); const isMouseEvent = event => isOfType(event, "MouseEvent"); const isPointerEvent = event => isOfType(event, "PointerEvent"); const isTouchPointerEvent = event => isPointerEvent(event) && getPointerType(event) === S_TOUCH; const isWheelEvent = event => isOfType(event, "WheelEvent"); const isKeyboardEvent = event => isOfType(event, "KeyboardEvent"); const isTouchEvent = event => isOfType(event, "TouchEvent"); const isDoc = target => isOfType(target, "HTMLDocument"); const isNode = target => typeof Node === "function" && isInstanceOf(target, Node) || target != null && typeof target.nodeType === "number" && typeof target.nodeName === "string"; const isElement = target => isNode(target) && target.nodeType === 1; const isStyledElement = (target, namespace) => isElement(target) && isObject(target.style) && (!namespace || target.namespaceURI === namespace); const isHTMLElement = target => typeof HTMLElement === "function" && isInstanceOf(target, HTMLElement) || isStyledElement(target, HTML_NS); const isHTMLInputElement = target => typeof HTMLInputElement === "function" && isInstanceOf(target, HTMLInputElement) || isHTMLElement(target) && hasTagName(target, "input"); const isAnimation = value => typeof Animation === "function" && isInstanceOf(value, Animation) || isOfType(value, "Animation", 2); const isCSSAnimation = value => typeof CSSAnimation === "function" && isInstanceOf(value, CSSAnimation) || isOfType(value, "CSSAnimation", 2); const isKeyframeEffect = value => typeof KeyframeEffect === "function" && isInstanceOf(value, KeyframeEffect) || isOfType(value, "KeyframeEffect"); const isNodeBAfterA = (nodeA, nodeB) => (nodeA.compareDocumentPosition(nodeB) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0; const strReplace = (s, match, replacement) => s.replace(match, replacement); const setTimer = root.setTimeout.bind(root); const clearTimer = root.clearTimeout.bind(root); const closestParent = (element, selector) => element.closest(selector); const getBoundingClientRect = element => element.getBoundingClientRect(); // Copy size properties explicitly to another object so they can be used with // the spread operator (DOMRect/DOMRectReadOnly's properties are not enumerable) const copyBoundingRectProps = rect => { return { x: rect.x, left: rect.left, right: rect.right, [S_WIDTH]: rect[S_WIDTH], y: rect.y, top: rect.top, bottom: rect.bottom, [S_HEIGHT]: rect[S_HEIGHT] }; }; const querySelector = (root, selector) => root.querySelector(selector); const querySelectorAll = (root, selector) => root.querySelectorAll(selector); const docQuerySelector = selector => querySelector(getDoc(), selector); const docQuerySelectorAll = selector => querySelectorAll(getDoc(), selector); const getElementById = id => getDoc().getElementById(id); const getAttr = (element, name) => element.getAttribute(name); const setAttr = (element, name, value = "true") => element.setAttribute(name, value); const unsetAttr = (element, name) => element.setAttribute(name, "false"); const delAttr = (element, name) => element.removeAttribute(name); const includes = (arr, v, startAt) => arr.indexOf(v, startAt) >= 0; const some = (array, predicate) => array.some(predicate); const filter = (array, filterFn) => array.filter(filterFn); const filterBlank = array => { const result = array ? filter(array, v => !isEmpty(v)) : undefined; return lengthOf(result) ? result : undefined; }; const sizeOf = obj => { var _obj$size; return (_obj$size = obj === null || obj === void 0 ? void 0 : obj.size) !== null && _obj$size !== void 0 ? _obj$size : 0; }; const lengthOf = obj => { var _obj$length; return (_obj$length = obj === null || obj === void 0 ? void 0 : obj.length) !== null && _obj$length !== void 0 ? _obj$length : 0; }; const lastOf = a => a === null || a === void 0 ? void 0 : a.slice(-1)[0]; const tagName = element => element.tagName; // case-insensitive const hasTagName = (element, tag) => toUpperCase(tagName(element)) === toUpperCase(tag); const preventDefault = event => event.preventDefault(); const arrayFrom = ARRAY.from.bind(ARRAY); const keysOf = obj => OBJECT.keys(obj); // use it in place of object spread const merge = (...a) => { return assign({}, ...a); }; // implementation (wide) — callers see the overloads, not this signature function copyObject(obj) { return merge(obj); } const promiseResolve = PROMISE.resolve.bind(PROMISE); const promiseAll = PROMISE.all.bind(PROMISE); const assign = OBJECT.assign.bind(OBJECT); OBJECT.freeze.bind(OBJECT); const hasOwnProp = (o, prop) => OBJECT.prototype.hasOwnProperty.call(o, prop); const defineProperty = OBJECT.defineProperty.bind(OBJECT); const getPrototypeOf = OBJECT.getPrototypeOf.bind(OBJECT); const preventExtensions = OBJECT.preventExtensions.bind(OBJECT); const stringify = JSON.stringify.bind(JSON); const floor = MATH.floor.bind(MATH); const ceil = MATH.ceil.bind(MATH); const log2 = MATH.log2.bind(MATH); const sqrt = MATH.sqrt.bind(MATH); const max = MATH.max.bind(MATH); const min = MATH.min.bind(MATH); const abs = MATH.abs.bind(MATH); const round = MATH.round.bind(MATH); const pow = MATH.pow.bind(MATH); const exp = MATH.exp.bind(MATH); MATH.cos.bind(MATH); MATH.sin.bind(MATH); MATH.tan.bind(MATH); const parseFloat = NUMBER.parseFloat.bind(NUMBER); NUMBER.isNaN.bind(NUMBER); const isInstanceOf = (value, classOrName, checkLisnBrand = false) => isLiteralString(classOrName) ? isInstanceOfByClassName(value, classOrName) : isInstanceOfByClass(value, classOrName, checkLisnBrand); const constructorOf = obj => obj.constructor; const typeOf = obj => typeof obj; const typeOrClassOf = obj => { var _constructorOf; return isObject(obj) ? (_constructorOf = constructorOf(obj)) === null || _constructorOf === void 0 ? void 0 : _constructorOf.name : typeOf(obj); }; const parentOf = element => { var _element$parentElemen; return (_element$parentElemen = element === null || element === void 0 ? void 0 : element.parentElement) !== null && _element$parentElemen !== void 0 ? _element$parentElemen : null; }; const childrenOf = element => (element === null || element === void 0 ? void 0 : element.children) || []; function targetOf(obj) { return obj && "target" in obj ? obj.target : undefined; } function currentTargetOf(obj) { return obj && "currentTarget" in obj ? obj.currentTarget : undefined; } function classList(element) { return element === null || element === void 0 ? void 0 : element.classList; } const S_TABINDEX = "tabindex"; const getTabIndex = element => getAttr(element, S_TABINDEX); const setTabIndex = (element, index = "0") => setAttr(element, S_TABINDEX, index); const unsetTabIndex = element => delAttr(element, S_TABINDEX); const remove = (obj, ...args) => obj === null || obj === void 0 ? void 0 : obj.remove(...args); const deleteObjKey = (obj, key) => delete obj[key]; const deleteKey = (map, key) => map === null || map === void 0 ? void 0 : map.delete(key); const elScrollTo = (element, coords, behavior = "instant") => element.scrollTo(merge({ behavior }, coords)); const newPromise = executor => new Promise(executor); const newMap = entries => new Map(entries); const newWeakMap = entries => new WeakMap(entries); const newSet = values => new Set(values); const newWeakSet = values => new WeakSet(values); const newIntersectionObserver = (callback, options) => new IntersectionObserver(callback, options); const newResizeObserver = callback => typeof ResizeObserver === "undefined" ? null : new ResizeObserver(callback); const newMutationObserver = callback => new MutationObserver(callback); const usageError = msg => new LisnUsageError(msg); const bugError = msg => new LisnBugError(msg); const illegalConstructorError = useWhat => usageError(`Illegal constructor. Use ${useWhat}.`); const CONSOLE = console; CONSOLE.debug.bind(CONSOLE); CONSOLE.log.bind(CONSOLE); CONSOLE.info.bind(CONSOLE); const consoleWarn = CONSOLE.warn.bind(CONSOLE); const consoleError = CONSOLE.error.bind(CONSOLE); // -------------------- const LISN_BRAND_PROP = SYMBOL.for("__lisn.js:brand"); const HTML_NS = "http://www.w3.org/1999/xhtml"; const isInstanceOfByClass = (value, Class, checkLisnBrand = false) => { if (!isFunction(Class) || !isObject(value)) { return false; } if (value instanceof Class) { return true; } return checkLisnBrand ? isInstanceOfLisnClass(value, Class) : false; }; const isInstanceOfLisnClass = (value, Class) => { if (!isFunction(Class) || !isObject(value)) { return false; } const proto = Class.prototype; const clsBrandSym = isObject(proto) ? proto[LISN_BRAND_PROP] : undefined; if (!clsBrandSym) { return false; } const valBrandSym = value[LISN_BRAND_PROP]; return valBrandSym == clsBrandSym || isInstanceOfLisnClass(getPrototypeOf(value), Class); }; const isInstanceOfByClassName = (value, className) => isInstanceOfByClass(value, globalThis[className]); /** * @module Settings */ // TODO add defaults for watchers' debounce window, thresholds /** * LISN's settings. * @readonly * * If you wish to modify them, then you need to do so immediately after loading * LISN before you instantiate any watchers, etc. For example: * * ```html * <!doctype html> * <html> * <head> * <meta charset="UTF-8" /> * <meta name="viewport" content="width=device-width" /> * <script src="lisn.js" charset="utf-8"></script> * <script charset="utf-8"> * // modify LISN settings, for example: * LISN.settings.deviceBreakpoints.desktop = 1024; * </script> * </head> * <body> * </body> * </html> * ``` */ const settings = preventExtensions({ /** * A unique selector (preferably `#some-id`) for the element that holds the * main page content, if other than `document.body`. * * E.g. if your main content is inside a custom scrollable container, rather * than directly in `document.body`, then pass a selector for it here. * * The element must be scrollable, i.e. have a fixed size and `overflow: scroll`. * * **IMPORTANT:** You must set this before initializing any watchers, widgets, * etc. If you are using the HTML API, then you must set this before the * document `readyState` becomes interactive. * * @defaultValue null // document.scrollingElement * @category Generic */ mainScrollableElementSelector: null, /** * This setting allows us to automatically wrap certain elements or groups of * elements into a single `div` or `span` element to allow for more reliable * or efficient working of certain features. In particular: * * 1. View tracking using relative offsets and a scrolling root **requires wrapping** * * When using view position tracking with a percentage offset specification * (e.g. `top: 50%`) _and_ a custom root element that is scrollable_ (and * obviously has a size smaller than the content), you **MUST** enable * content wrapping, otherwise the trigger offset elements cannot be * positioned relative to the scrolling _content size_. * * 2. Scroll tracking * * When using scroll tracking, including scrollbars, on a scrolling element * (that obviously has a size smaller than the content), it's recommended for * the content of the scrollable element to be wrapped in a single `div` * container, to allow for more efficient and reliable detection of changes * in the _scrollable content_ size. * * If content wrapping is disabled, when scroll tracking is used on a given * element (other than the root of the document), each of the immediate * children of the scrollable element have their sizes tracked, which could * lead to more resource usage. * * 3. Scrollbars on custom elements * * When you setup a {@link Widgets.Scrollbar} widget for a custom * scrollable element that may not be the main scrollable (and therefore * won't take up the full viewport all the time), then to be able to position * to scrollbar relative to the scrollable element, its content needs to be * wrapped. * * If this setting is OFF, then the scrollbars on custom elements have to * rely on position sticky which doesn't have as wide browser support as the * default option. * * 4. Animating on viewport enter/leave * * For elements that have transforms applied as part of an animation or * transition, if you wish to run or reverse the animation when the element * enters or leaves the viewport, then the transform can interfere with the * viewport tracking. For example, if undoing the animation as soon as the * element leaves the viewport makes it enter it again (because it's moved), * then this will result in a glitch. * * If content wrapping is disabled, then to get around such issues, a dummy * element is positioned on top of the actual element and is the one tracked * across the viewport instead. Either approach could cause issues depending * on your CSS, so it's your choice which one is applied. * * ---------- * * **IMPORTANT:** Certain widgets always require wrapping of elements or their * children. This setting only applies in cases where wrapping is optional. * If you can, it's recommended to leave this setting ON. You can still try to * disable wrapping on a per-element basis by setting `data-lisn-no-wrap` * attribute on it. Alternatively, if the elements that need wrapping are * already wrapped in an element with a class `lisn-wrapper`, this will be * used as the wrapper. * * @defaultValue true * @category Generic */ contentWrappingAllowed: true, // [TODO v2] rename this setting /** * The timeout in milliseconds for waiting for the `document.readyState` to * become `complete`. The timer begins _once the `readyState` becomes * `interactive`_. * * The page will be considered "ready" either when the `readyState` becomes * `complete` or this many milliseconds after it becomes `interactive`, * whichever is first. * * Set to 0 or a negative number to disable timeout. * * @defaultValue 2000 // i.e. 2s * @category Generic */ pageLoadTimeout: 2000, /** * This enables LISN's HTML API. Then the page will be parsed (and watched * for dynamically added elements at any time) for any elements matching a * widget selector. Any element that has a matching CSS class or data * attribute will be setup according to the relevant widget, which may wrap, * clone or add attributes to the element. * * This is enabled by default for bundles, and disabled otherwise. * * **IMPORTANT:** You must set this before the document `readyState` becomes * interactive. * * @defaultValue `false` in general, but `true` in browser bundles * @category Widgets */ autoWidgets: false, /** * Default lag value. Used by * - {@link Widgets.SmoothScroll} * - {@link Effects/FXController.FXController} * - {@link Watchers/ScrollWatcher.ScrollWatcher} (default scroll duration) * * @defaultValue 1000 * @category Effects */ effectLag: 1000, /** * Default setting for * {@link Widgets.ScrollbarConfig.hideNative | ScrollbarConfig.hideNative}. * * @defaultValue true * @category Widgets/Scrollbar */ scrollbarHideNative: true, /** * Default setting for * {@link Widgets.ScrollbarConfig.onMobile | ScrollbarConfig.onMobile}. * * @defaultValue false * @category Widgets/Scrollbar */ scrollbarOnMobile: false, /** * Default setting for * {@link Widgets.ScrollbarConfig.positionH | ScrollbarConfig.positionH}. * * @defaultValue "bottom" * @category Widgets/Scrollbar */ scrollbarPositionH: "bottom", /** * Default setting for * {@link Widgets.ScrollbarConfig.positionV | ScrollbarConfig.positionV}. * * @defaultValue "right" * @category Widgets/Scrollbar */ scrollbarPositionV: "right", /** * Default setting for * {@link Widgets.ScrollbarConfig.autoHide | ScrollbarConfig.autoHide}. * * @defaultValue -1 * @category Widgets/Scrollbar */ scrollbarAutoHide: -1, /** * Default setting for * {@link Widgets.ScrollbarConfig.clickScroll | ScrollbarConfig.clickScroll}. * * @defaultValue true * @category Widgets/Scrollbar */ scrollbarClickScroll: true, /** * Default setting for * {@link Widgets.ScrollbarConfig.dragScroll | ScrollbarConfig.dragScroll}. * * @defaultValue true * @category Widgets/Scrollbar */ scrollbarDragScroll: true, /** * Default setting for * {@link Widgets.ScrollbarConfig.useHandle | ScrollbarConfig.useHandle}. * * @defaultValue false * @category Widgets/Scrollbar */ scrollbarUseHandle: false, /** * Default setting for * {@link Widgets.SameHeightConfig.diffTolerance | SameHeightConfig.diffTolerance}. * * @defaultValue 15 * @category Widgets/SameHeight */ sameHeightDiffTolerance: 15, /** * Default setting for * {@link Widgets.SameHeightConfig.resizeThreshold | SameHeightConfig.resizeThreshold}. * * @defaultValue 5 * @category Widgets/SameHeight */ sameHeightResizeThreshold: 5, /** * Default setting for * {@link Widgets.SameHeightConfig.debounceWindow | SameHeightConfig.debounceWindow}. * * @defaultValue 100 * @category Widgets/SameHeight */ sameHeightDebounceWindow: 100, /** * Default setting for * {@link Widgets.SameHeightConfig.minGap | SameHeightConfig.minGap}. * * @defaultValue 30 * @category Widgets/SameHeight */ sameHeightMinGap: 30, /** * Default setting for * {@link Widgets.SameHeightConfig.maxFreeR | SameHeightConfig.maxFreeR}. * * @defaultValue 0.4 * @category Widgets/SameHeight */ sameHeightMaxFreeR: 0.4, /** * Default setting for * {@link Widgets.SameHeightConfig.maxWidthR | SameHeightConfig.maxWidthR}. * * @defaultValue 1.7 * @category Widgets/SameHeight */ sameHeightMaxWidthR: 1.7, /** * Set custom device breakpoints as width in pixels. * * The value of each sets its lower limit, i.e. it specifies a device whose * width is larger than the given value (and up to the next larger one). * * If you specify only some of the below devices, then the other ones will * keep their default breakpoint values. * * Adding device types, other than the ones listed below is not supported. * * @category Device layouts */ deviceBreakpoints: { /** * This should be left as 0 as it's the catch-all for anything narrower * than "mobile-wide". * * @defaultValue 0 */ mobile: 0, /** * Anything wider than the given value is "mobile-wide", up to the value of * "tablet". * * @defaultValue 576 */ "mobile-wide": 576, /** * Anything wider than the given value is "tablet", up to the value of * "desktop". * * @defaultValue 768 */ tablet: 768, // tablet is anything above this (up to desktop) /** * Anything wider than the given value is "desktop". * * @defaultValue 992 */ desktop: 992 // desktop is anything above this }, /** * Set custom aspect ratio breakpoints (as ratio of width to height). * * The value of each sets its lower limit, i.e. it specifies an aspect ratio * that is wider than the given value (and up to the next wider one). * * If you specify only some of the below aspect ratios, then the other ones * will keep their default breakpoint values. * * Adding aspect ratio types, other than the ones listed below is not * supported. * * @category Device layouts */ aspectRatioBreakpoints: { /** * This should be left as 0 as it's the catch-all for anything with * a narrower aspect ratio than "tall". * * @defaultValue 0 */ "very-tall": 0, // very tall is up to 9:16 /** * Anything with a wider aspect ratio than the given value is "tall", up to * the value of "square". * * @defaultValue 9 / 16 */ tall: 9 / 16, // tall is between 9:16 and 3:4 /** * Anything with a wider aspect ratio than the given value is "square", up * to the value of "wide". * * @defaultValue 3 / 4 */ square: 3 / 4, // square is between 3:4 and 4:3 /** * Anything with a wider aspect ratio than the given value is "wide", up to * the value of "very-wide". * * @defaultValue 4 / 3 */ wide: 4 / 3, // wide is between 4:3 and 16:9 /** * Anything with a wider aspect ratio than the given value is "very-wide". * * @defaultValue 16 / 9 */ "very-wide": 16 / 9 // very wide is above 16:9 }, /** * The CSS class that enables light theme. * * **IMPORTANT:** If you change this, you should also change the * `$light-theme-cls` variable in the SCSS configuration, or otherwise add the * following to your CSS: * * :root, * .custom-light-theme-cls { * --lisn-color-fg: some-dark-color; * --lisn-color-fg-t: some-dark-color-with-transparency; * --lisn-color-bg: some-light-color; * --lisn-color-bg-t: some-light-color-with-transparency; * } */ lightThemeClassName: "light-theme", /** * The CSS class that enables dark theme. * * **IMPORTANT:** If you change this, you should also change the * `$dark-theme-cls` variable in the SCSS configuration, or otherwise add the * following to your CSS: * * .custom-dark-theme-cls { * --lisn-color-fg: some-light-color; * --lisn-color-fg-t: some-light-color-with-transparency; * --lisn-color-bg: some-dark-color; * --lisn-color-bg-t: some-dark-color-with-transparency; * } */ darkThemeClassName: "dark-theme", /** * Used to determine the effective delta in pixels for gestures triggered by * some key (arrows) and wheel events (where the browser reports the delta * mode to be LINE). * * Value is in pixels. * * @defaultValue 40 * @category Gestures */ deltaLineHeight: 40, /** * Used to determine the effective delta in pixels for gestures triggered by * some wheel events (where the browser reports the delta mode to be PAGE). * * Value is in pixels. * * @defaultValue 1600 * @category Gestures */ deltaPageWidth: 1600, /** * Used to determine the effective delta in pixels for gestures triggered by * some key (PageUp/PageDown/Space) and wheel events (where the browser * reports the delta mode to be PAGE). * * Value is in pixels. * * @defaultValue 800 * @category Gestures */ deltaPageHeight: 800, /** * Controls the debugging verbosity level. Values from 0 (none) to 10 (insane) * are recognized. * * **Note:** Logging is not available in bundles except in the "debug" bundle. * * @defaultValue `0` except in the "debug" bundle where it defaults to 10 * @category Logging */ verbosityLevel: 0, /** * The URL of the remote logger to connect to. LISN uses * {@link https://socket.io/docs/v4/client-api/ | socket.io-client} * to talk to the client and emits messages on the following namespaces: * * - `console.debug` * - `console.log` * - `console.info` * - `console.warn` * - `console.error` * * There is a simple logging server that ships with LISN, see the source * code repository. * * You can always explicitly disable remote logging on a given page by * setting `disableRemoteLog=1` query parameter in the URL. * * **Note:** Logging is not available in bundles (except in the `debug` bundle). * * @defaultValue null * @category Logging */ remoteLoggerURL: null, /** * Enable remote logging only on mobile devices. * * You can always disable remote logging for any page by setting * `disableRemoteLog=1` URL query parameter. * * **Note:** Logging is not available in bundles (except in the `debug` bundle). * * @defaultValue false * @category Logging */ remoteLoggerOnMobileOnly: false }); // -------------------- /** * @module Utils */ /** * Round a number to the given decimal precision. * * @category Math */ const roundNumTo = (value, numDecimal = 0) => { const multiplicationFactor = pow(10, numDecimal); return round(value * multiplicationFactor) / multiplicationFactor; }; /** * Returns true if the given value is a valid _finite_ number. * * @category Validation */ const isValidNum = value => isNumber(value) && NUMBER.isFinite(value); /** * If the given value is a valid _finite_ number, it is returned, otherwise * the default is returned. * * @category Math */ const toNum = (value, defaultValue = 0) => { const numValue = isLiteralString(value) ? parseFloat(value) : value; // parseFloat will strip trailing non-numeric characters, so we check that // the parsed number is equal to the string, if it was a string, using loose // equality, in order to make sure the entire string was a number. return isValidNum(numValue) && numValue == value ? numValue : defaultValue; }; /** * If the given value is a valid _finite integer_ number, it is returned, * otherwise the default is returned. * * @category Math */ const toInt = (value, defaultValue = 0) => { let numValue = toNum(value, null); numValue = numValue === null ? numValue : floor(numValue); // Ensure that the parsed int equaled the original by loose equality. return isValidNum(numValue) && numValue == value ? numValue : defaultValue; }; /** * If the given value is a valid non-negative _finite_ number, it is returned, * otherwise the default is returned. * * @category Math */ const toNonNegNum = (value, defaultValue = 0) => { const numValue = toNum(value, null); return numValue !== null && numValue >= 0 ? numValue : defaultValue; }; /** * If the given value is a valid positive number, it is returned, otherwise the * default is returned. * * @category Math */ const toPosNum = (value, defaultValue = 0) => { const numValue = toNum(value, null); return numValue !== null && numValue > 0 ? numValue : defaultValue; }; /** * Returns the given number bound by min and/or max value. * * If the value is not a valid number, then `defaultValue` is returned if given * (_including if it is null_), otherwise `limits.min` if given and not null, * otherwise `limits.max` if given and not null, or finally 0. * * If the value is outside the bounds, then: * - if `defaultValue` is given, `defaultValue` is returned (_including if it * is null_) * - otherwise, the min or the max value (whichever one is violated) is * returned * * @category Math */ const toNumWithBounds = (value, limits, defaultValue) => { var _limits$min, _limits$max; const numValue = toNum(value, null); const min = (_limits$min = limits === null || limits === void 0 ? void 0 : limits.min) !== null && _limits$min !== void 0 ? _limits$min : null; const max = (_limits$max = limits === null || limits === void 0 ? void 0 : limits.max) !== null && _limits$max !== void 0 ? _limits$max : null; let result; if (!isValidNum(numValue)) { var _ref; result = (_ref = min !== null && min !== void 0 ? min : max) !== null && _ref !== void 0 ? _ref : 0; } else if (min !== null && numValue < min) { result = min; } else if (max !== null && numValue > max) { result = max; } else { result = numValue; } return result; }; /** * Returns the largest absolute value among the given ones. * * The result is always positive. * * @category Math */ const maxAbs = (...values) => max(...values.map(v => abs(v))); /** * Returns the value with the largest absolute value among the given ones. * * The result can be negative. * * @category Math */ const havingMaxAbs = (...values) => lengthOf(values) ? values.sort((a, b) => abs(b) - abs(a))[0] : -INFINITY; /** * Returns the angle (in radians) that the vector defined by the given x, y * makes with the positive horizontal axis. * * The angle returned is in the range -PI to PI, not including -PI. * * @category Math */ const hAngle = (x, y) => normalizeAngle(MATH.atan2(y, x)); // ensure that -PI is transformed to +PI /** * Normalizes the given angle (in radians) so that it's in the range -PI to PI, * not including -PI. * * @category Math */ const normalizeAngle = a => { // ensure it's positive in the range 0 to 2 PI while (a < 0 || a > PI * 2) { a += (a < 0 ? 1 : -1) * PI * 2; } // then, if > PI, offset by - 2PI return a > PI ? a - PI * 2 : a; }; /** * Converts the given angle in degrees to radians. * * @category Math */ const degToRad = a => a * PI / 180; /** * Returns true if the given vectors point in the same direction. * * @param angleDiffThreshold * Sets the threshold in degrees when comparing the angles of * two vectors. E.g. for 5 degrees threshold, directions * whose vectors are within 5 degrees of each other are * considered parallel. * It doesn't make sense for this value to be < 0 or >= 90 * degrees. If it is, it's forced to be positive (absolute) * and <= 89.99. * * @category Math */ const areParallel = (vA, vB, angleDiffThreshold = 0) => { const angleA = hAngle(vA[0], vA[1]); const angleB = hAngle(vB[0], vB[1]); angleDiffThreshold = min(89.99, abs(angleDiffThreshold)); return abs(normalizeAngle(angleA - angleB)) <= degToRad(angleDiffThreshold); }; /** * Returns true if the given vectors point in the opposite direction. * * @param angleDiffThreshold * Sets the threshold in degrees when comparing the angles of * two vectors. E.g. for 5 degrees threshold, directions * whose vectors are within 175-185 degrees of each other are * considered antiparallel. * It doesn't make sense for this value to be < 0 or >= 90 * degrees. If it is, it's forced to be positive (absolute) * and <= 89.99. * * @category Math */ const areAntiParallel = (vA, vB, angleDiffThreshold = 0) => areParallel(vA, [-vB[0], -vB[1]], angleDiffThreshold); /** * Returns the distance between two points on the screen. * * @category Math */ const distanceBetween = (ptA, ptB) => sqrt(pow(ptA[0] - ptB[0], 2) + pow(ptA[1] - ptB[1], 2)); /** * Returns an array of object's keys sorted by the numeric value they hold. * * @category Math */ const sortedKeysByVal = (obj, descending = false) => { if (descending) { return keysOf(obj).sort((x, y) => obj[y] - obj[x]); } return keysOf(obj).sort((x, y) => obj[x] - obj[y]); }; /** * Takes two integers and returns a bitmask that covers all values between * 1 << start and 1 << end, _including the starting and ending one_. * * If pStart > pEnd, they are reversed. * * getBitmask(start, start) always returns 1 << start * getBitmask(start, end) always returns same as getBitmask(end, start) * * @category Math */ const getBitmask = (start, end) => start > end ? getBitmask(end, start) : -1 >>> 32 - end - 1 + start << start; /** * @module * @ignore * @internal */ /** * Recursively copies the given value. Handles circular references. * * The following types are deeply copied: * - Plain objects (i.e. with prototype Object or null) * - Array * - ArrayBuffer, DataView and TypedArray * - Map * - Set * - Date * - RegExp * * Other values are returned as is * * @since v1.3.0 */ const deepCopy = (value, _seen = new WeakMap()) => { if (!isObject(value)) { // Primitive or function return value; } if (_seen.has(value)) { // Circular reference return _seen.get(value); } if (isArray(value)) { const out = new ARRAY(value.length); _seen.set(value, out); for (let i = 0; i < value.length; i++) { if (i in value) { out[i] = deepCopy(value[i], _seen); } } return out; } if (isMap(value)) { const out = newMap(); _seen.set(value, out); for (const [k, v] of value) { const kCopy = deepCopy(k, _seen); const vCopy = deepCopy(v, _seen); out.set(kCopy, vCopy); } return out; } if (isSet(value)) { const out = newSet(); _seen.set(value, out); for (const v of value) { out.add(deepCopy(v, _seen)); } return out; } if (isOfType(value, "ArrayBuffer")) { return value.slice(0); } if (isOfType(value, "DataView")) { const buf = deepCopy(value.buffer, _seen); return new DataView(buf, value.byteOffset, value.byteLength); } else if (ArrayBuffer.isView(value)) { // DataView already handled above, so this is TypedArray: // Int8Array, Uint8Array, Float32Array, etc. const Ctor = value.constructor; return new Ctor(value); } if (isOfType(value, "Date")) { return new Date(value.getTime()); } if (isOfType(value, "RegExp")) { const flags = value.flags !== undefined ? value.flags : (value.global ? "g" : "") + (value.ignoreCase ? "i" : "") + (value.multiline ? "m" : "") + (value.unicode ? "u" : "") + (value.sticky ? "y" : "") + (value.dotAll ? "s" : ""); const copy = new RegExp(value.source, flags); copy.lastIndex = value.lastIndex; return copy; } if (!isPlainObject(value)) { // Non-clonable return value; } // Plain object (preserve prototype, if it's null & property descriptors, // including symbols) const out = OBJECT.create(getPrototypeOf(value)); _seen.set(value, out); for (const key of Reflect.ownKeys(value)) { const desc = OBJECT.getOwnPropertyDescriptor(value, key); if (!desc) { continue; } if ("value" in desc) { // Data descriptor: deep copy the value desc.value = deepCopy(desc.value, _seen); } // Otherwise it's accessor descriptor: keep same getter/setter references // (cannot deep copy closures, so we redefine them as is) defineProperty(out, key, desc); } return out; }; /** * For all keys present in `toObj`, if the key is also in `fromObj`, it copies * the value recursively from `fromObj` into `toObj` in place. * * Plain objects are recursed into, but other values are copied as is. * * @since v1.3.0 Was previously called copyExistingKeys */ const copyExistingKeysTo = (fromObj, toObj) => { for (const key in toObj) { if (!hasOwnProp(toObj, key)) { continue; } if (key in fromObj) { const current = toObj[key]; const updated = fromObj[key]; if (isPlainObject(updated) && isPlainObject(current)) { copyExistingKeysTo(updated, current); } else if (updated !== undefined) { toObj[key] = updated; } } } }; /** * Omits the keys in object `keysToRm` from `obj`. This is to avoid hardcording * the key names as a string so as to allow minifier to mangle them, and to * avoid using object spread. */ const omitKeys = (obj, keysToRm) => { const res = {}; for (const key in obj) { if (!(key in keysToRm)) { res[key] = obj[key]; } } return res; }; /** * Returns true if the two objects are equal. If values are numeric, it will * round to the given number of decimal places. */ const compareValuesIn = (objA, objB, roundTo = 3) => { for (const key in objA) { if (!hasOwnProp(objA, key)) { continue; } const valA = objA[key]; const valB = objB[key]; if (isPlainObject(valA) && isPlainObject(valB)) { if (!compareValuesIn(valA, valB)) { return false; } } else if (isLiteralNumber(valA) && isLiteralNumber(valB)) { if (roundNumTo(valA, roundTo) !== roundNumTo(valB, roundTo)) { return false; } } else if (valA !== valB) { return false; } } return true; }; const toArrayIfSingle = value => isArray(value) ? value : !isNullish(value) ? [value] : []; const toBoolean = value => value === true || value === "true" || value === "" ? true : isNullish(value) || value === false || value === "false" ? false : null; /** * @module Utils */ /** * Formats an object as a string. It supports more meaningful formatting as * string for certain types rather than using the default string * representation. * * **NOTE:** This is not intended for serialization of data that needs to be * de-serialized. Only for debugging output. * * @param value The value to format as string. * @param [maxLen] Maximum length of the returned string. If not given or * is <= 0, the string is not truncated. Otherwise, if the * result is longer than maxLen, it is truncated to * `maxLen - 3` and added a suffix of "...". * Note that if `maxLen` is > 0 but <= 3, the result is * always "..." * * @category Text */ const formatAsString = (value, maxLen) => { const result = maybeConvertToString(value, false); return result; }; /** * Join an array of values as string using separator. It uses * {@link formatAsString} rather than the default string representation as * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join | Array:join} would. * * @param separator The separator to use to delimit each argument. * @param args Objects or values to convert to string and join. * * @category Text */ const joinAsString = (separator, ...args) => args.map(a => formatAsString(a)).join(separator); /** * Similar to * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split | String.prototype.split} * except that * 1. `limit` is interpreted as the maximum number of splits, and the * returned array contains `limit + 1` entries. Also if `limit` is given and * the number of substrings is greater than the limit, all the remaining * substrings are present in the final substring. * 2. If input is an empty string (or containing only whitespace), returns an * empty array. * * @example * ```javascript * splitOn('foo, bar, baz', RegExp(',\\s*'), 0); // -> ['foo, bar, baz'] * splitOn('foo, bar, baz', RegExp(',\\s*'), 1); // -> ['foo', 'bar, baz'] * splitOn('foo, bar, baz', RegExp(',\\s*'), 2); // -> ['foo', 'bar', 'baz'] * splitOn('foo, bar, baz', RegExp(',\\s*'), 3); // -> ['foo', 'bar', 'baz'] * ``` * * @param trim If true, entries will be trimmed for whitespace after splitting. * * @param limit If not given or < 0, the string will be split on every * occurrence of `separator`. Otherwise, it will be split on * the first `limit` number of occurrences of `separator`. * * @category Text */ const splitOn = (input, separator, trim, limit) => { if (!input.trim()) { return []; } limit !== null && limit !== void 0 ? limit : limit = -1; const output = []; const addEntry = s => output.push(trim ? s.trim() : s); while (limit--) { let matchIndex = -1, matchLength = 0; if (isLiteralString(separator)) { matchIndex = input.indexOf(separator); matchLength = lengthOf(separator); } else { var _match$index; const match = separator.exec(input); matchIndex = (_match$index = match === null || match === void 0 ? void 0 : match.index) !== null && _match$index !== void 0 ? _match$index : -1; matchLength = match ? lengthOf(match[0]) : 0; } if (matchIndex < 0) { break; } addEntry(input.slice(0, matchIndex)); input = input.slice(matchIndex + matchLength); } addEntry(input); return output; }; /** * Converts a kebab-cased-string to camelCase. * The result is undefined if the input string is not formatted in * kebab-case. * * @category Text */ const kebabToCamelCase = kebabToCamelCase$1; /** * Converts a camelCasedString to kebab-case. * The result is undefined if the input string is not formatted in * camelCase. * * @category Text */ const camelToKebabCase = camelToKebabCase$1; /** * Generates a random string of a fixed length. * * **IMPORTANT:** This is _not_ suitable for cryptographic applicati