@lisn.js/bundles
Version:
LISN.js browser bundles.
1,403 lines (1,330 loc) • 522 kB
JavaScript
/*!
* LISN.js v1.2.0
* (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 FUNCTION = Function;
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_WRAPPER$2 = `${PREFIX}-wrapper`;
const PREFIX_INLINE_WRAPPER = `${PREFIX_WRAPPER$2}-inline`;
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 prefixData = 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 isIterableObject = v => isNonPrimitive(v) && SYMBOL.iterator in v;
const isArray = v => isInstanceOf(v, ARRAY);
const isObject = v => isInstanceOf(v, OBJECT);
const isNonPrimitive = v => v !== null && typeOf(v) === "object";
// only primitive number
const isNumber = v => typeOf(v) === "number";
/* eslint-disable-next-line @typescript-eslint/no-wrapper-object-types */
const isString = v => typeOf(v) === "string" || isInstanceOf(v, STRING);
const isLiteralString = v => typeOf(v) === "string";
const isBoolean = v => typeOf(v) === "boolean";
/* eslint-disable-next-line @typescript-eslint/no-unsafe-function-type */
const isFunction = v => typeOf(v) === "function" || isInstanceOf(v, FUNCTION);
const isDoc = target => target === getDoc();
const isMouseEvent = event => isInstanceOf(event, MouseEvent);
const isPointerEvent = event => typeof PointerEvent !== "undefined" && isInstanceOf(event, PointerEvent);
const isTouchPointerEvent = event => isPointerEvent(event) && getPointerType(event) === S_TOUCH;
const isWheelEvent = event => isInstanceOf(event, WheelEvent);
const isKeyboardEvent = event => isInstanceOf(event, KeyboardEvent);
const isTouchEvent = event => typeof TouchEvent !== "undefined" && isInstanceOf(event, TouchEvent);
const isElement = target => isInstanceOf(target, Element);
const isHTMLElement = target => isInstanceOf(target, HTMLElement);
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 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;
const hasTagName = (element, tag) => toLowerCase(tagName(element)) === toLowerCase(tag);
const preventDefault = event => event.preventDefault();
const arrayFrom = ARRAY.from.bind(ARRAY);
const keysOf = obj => OBJECT.keys(obj);
const defineProperty = OBJECT.defineProperty.bind(OBJECT);
// use it in place of object spread
const merge = (...a) => {
return OBJECT.assign({}, ...a);
};
const copyObject = obj => 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 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);
const parseFloat = NUMBER.parseFloat.bind(NUMBER);
NUMBER.isNaN.bind(NUMBER);
const isInstanceOf = (value, Class) => value instanceof Class;
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) || [];
const targetOf = obj => obj === null || obj === void 0 ? void 0 : obj.target;
const currentTargetOf = obj => obj === null || obj === void 0 ? void 0 : obj.currentTarget;
const classList = element => 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 => obj === null || obj === void 0 ? void 0 : obj.remove();
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);
// --------------------
/**
* @module Settings
*/
/**
* 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 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 the new position and velocity for a critically damped user-driven
* spring state toward a current target position.
*
* @param [settings.lTarget] Target final position.
* @param [settings.dt] Time step in milliseconds since the last call.
* Must be small for the returned values to be
* meaningful.
* @param [settings.lag] Lag in milliseconds (how long it should take
* for it to reach the final position). Must be
* positive.
* @param [settings.l = 0] Current position (starting or one returned by
* previous call).
* @param [settings.v = 0] Current velocity (returned by previous call).
* @param [settings.precision = 2] Number of decimal places to round position to
* in order to determine when it's "done".
* @returns Updated position and velocity
*
* @since v1.2.0
*
* @category Math
*/
const criticallyDamped = settings => {
const {
lTarget,
precision = 2
} = settings;
const lag = toNumWithBounds(settings.lag, {
min: 1
}) / 1000; // to seconds
// Since the position only approaches asymptotically the target it never truly
// reaches it exactly we need an approximation to calculate w0. N determines
// how far away from the target position we are after `lag` milliseconds.
const N = 7;
const w0 = N / lag;
let {
l = 0,
v = 0,
dt
} = settings;
dt /= 1000; // to seconds
if (roundNumTo(l - lTarget, precision) === 0) {
// we're done
l = lTarget;
v = 0;
} else if (dt > 0) {
const A = l - lTarget;
const B = v + w0 * A;
const e = exp(-w0 * dt);
l = lTarget + (A + B * dt) * e;
v = (B - w0 * (A + B * dt)) * e;
}
return {
l,
v
};
};
/**
* 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
*/
const copyExistingKeys = (fromObj, toObj) => {
for (const key in toObj) {
if (!hasOwnProp(toObj, key)) {
continue;
}
if (key in fromObj) {
if (isNonPrimitive(fromObj[key]) && isNonPrimitive(toObj[key])) {
copyExistingKeys(fromObj[key], toObj[key]);
} else {
toObj[key] = fromObj[key];
}
}
}
};
// 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 = {};
let key;
for (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 (isNonPrimitive(valA) && isNonPrimitive(valB)) {
if (!compareValuesIn(valA, valB)) {
return false;
}
} else if (isNumber(valA) && isNumber(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 applications.
*
* @param nChars The length of the returned stirng.
*
* @category Text
*/
const randId = (nChars = 8) => {
const segment = () => floor(100000 + MATH.random() * 900000).toString(36);
let s = "";
while (lengthOf(s) < nChars) {
s += segment();
}
return s.slice(0, nChars);
};
/**
* Returns an array of numeric margins in pixels from the given margin string.
* The string should contain margins in either pixels or percentage; other
* units are not supported.
*
* Percentage values are converted to pixels relative to the given
* `absoluteSize`: left/right margins relative to the width, and top/bottom
* margins relative to the height.
*
* Note that for the margin property, percentages are always relative to the
* WIDTH of the parent, so you should pass the parent width as both the width
* and the height keys in `absoluteSize`. But for IntersectionObserver's
* `rootMargin`, top/bottom margin is relative to the height of the root, so
* pass the actual root size.
*
* @returns [topMarginInPx, rightMarginInPx, bottomMarginInPx, leftMarginInPx]
*
* @category Text
*/
const toMargins = (value, absoluteSize) => {
var _parts$, _parts$2, _ref, _parts$3;
const toPxValue = (strValue, index) => {
let margin = parseFloat(strValue !== null && strValue !== void 0 ? strValue : "") || 0;
if (strValue === margin + "%") {
margin *= index % 2 ? absoluteSize[S_HEIGHT] : absoluteSize[S_WIDTH];
}
return margin;
};
const parts = splitOn(value, " ", true);
const margins = [
// top
toPxValue(parts[0], 0),
// right
toPxValue((_parts$ = parts[1]) !== null && _parts$ !== void 0 ? _parts$ : parts[0], 1),
// bottom
toPxValue((_parts$2 = parts[2]) !== null && _parts$2 !== void 0 ? _parts$2 : parts[0], 2),
// left
toPxValue((_ref = (_parts$3 = parts[3]) !== null && _parts$3 !== void 0 ? _parts$3 : parts[1]) !== null && _ref !== void 0 ? _ref : parts[0], 3)];
return margins;
};
/**
* @ignore
* @internal
*/
const objToStrKey = obj => stringify(flattenForSorting(obj));
// --------------------
const flattenForSorting = obj => {
const array = isArray(obj) ? obj : keysOf(obj).sort().map(k => obj[k]);
return array.map(value => {
if (isArray(value) || isNonPrimitive(value) && constructorOf(value) === OBJECT) {
return flattenForSorting(value);
}
return value;
});
};
const stringifyReplacer = (key, value) => key ? maybeConvertToString(value, true) : value;
const maybeConvertToString = (value, nested) => {
let result = "";
if (isElement(value)) {
const classStr = classList(value).toString().trim();
result = value.id ? "#" + value.id : `<${tagName(value)}${classStr ? ' class="' + classStr + '"' : ""}>`;
//
} else if (isInstanceOf(value, Error)) {
/* istanbul ignore else */
if ("stack" in value && isString(value.stack)) {
result = value.stack;
} else {
result = `Error: ${value.message}`;
}
//
} else if (isArray(value)) {
result = "[" + value.map(v => isString(v) ? stringify(v) : maybeConvertToString(v, false)).join(",") + "]";
//
} else if (isIterableObject(value)) {
result = typeOrClassOf(value) + "(" + maybeConvertToString(arrayFrom(value), false) + ")";
//
} else if (isNonPrimitive(value)) {
result = nested ? value : stringify(value, stringifyReplacer);
//
} else {
// primitive
result = nested ? value : STRING(value);
}
return result;
};
/**
* @module Utils
*/
/**
* Returns an array of strings from the given list while validating each one
* using the `checkFn` function.
*
* If it returns without throwing, the input is necessarily valid.
* If the result is an empty array, it will return `null`.
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If the input is not a string or array of strings, or if any
* entries do not pass `checkFn`.
*
* @param key Used in the error message thrown
*
* @returns `undefined` if the input contains no non-empty values (after
* trimming whitespace on left/right from each), otherwise a non-empty array of
* values.
*
* @category Validation
*/
const validateStrList = (key, value, checkFn) => {
var _toArray;
return filterBlank((_toArray = toArray(value)) === null || _toArray === void 0 ? void 0 : _toArray.map(v => _