@lisn.js/bundles
Version:
LISN.js browser bundles.
1,411 lines (1,347 loc) • 331 kB
JavaScript
/*!
* 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_SKIP_INITIAL = "skipInitial";
const S_DEBOUNCE_WINDOW = "debounceWindow";
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_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_AUTO = "auto";
const S_VISIBLE = "visible";
const PREFIX_RELATIVE = `${PREFIX}-relative`;
const PREFIX_WRAPPER$2 = `${PREFIX}-wrapper`;
const PREFIX_WRAPPER_CONTENT = `${PREFIX_WRAPPER$2}-content`;
const PREFIX_NO_SELECT = `${PREFIX}-no-select`;
const PREFIX_NO_TOUCH_ACTION = `${PREFIX}-no-touch-action`;
const PREFIX_NO_WRAP = `${PREFIX}-no-wrap`;
/**
* @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 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();
Date.now.bind(Date);
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 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 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 getAttr = (element, name) => element.getAttribute(name);
const setAttr = (element, name, value = "true") => element.setAttribute(name, value);
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);
}
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 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 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;
};
/**
* @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 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 are calculated relative to the width, and
* top/bottom margins are calculated relative to the height.
*
* **IMPORTANT:** For the margin property itself, 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 (width and height).
*
* @param absoluteSize The size of the parent. If you are calculating margins
* for the CSS margin property, only the width is used. In
* this case you can simply pass the width (a number) as
* this parameter.
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If any of the margins is not in the format `<number>`,
* `<number>px` or `<number>%`
*
* @returns [topMarginInPx, rightMarginInPx, bottomMarginInPx, leftMarginInPx]
*
* @since Since v1.3.0 `absoluteSize` can be a plain number. Previously it was
* required to be a {@link Size}.
*
* @category Text
*/
const toMargins = (value, absoluteSize) => {
let width, height;
if (isNumber(absoluteSize)) {
width = height = absoluteSize;
} else {
({
width,
height
} = absoluteSize);
height !== null && height !== void 0 ? height : height = width;
}
const toPxValue = (strValue, absValue) => {
let margin = parseFloat(strValue !== null && strValue !== void 0 ? strValue : "") || 0;
if (strValue === `${margin}%`) {
margin *= absValue;
} else if (strValue !== `${margin}px` && strValue !== `${margin}`) {
throw usageError("Converting margin string to pixels: margin values should be in pixel or percentage.");
}
return margin;
};
const parts = toFourMargins(splitOn(value, " ", true));
// Even indices (0 and 2) are for top/bottom, so relative to height, otherwise
// relative to width
return parts.map((v, i) => toPxValue(v, i % 2 ? width : height));
};
/**
* Like {@link toMargins} except it returns an object containing `top`, `right`,
* `bottom`, `left` properties with the numeric margins in pixels.
*
* @since v1.3.0
*
* @category Text
*/
const toMarginProps = (value, absoluteSize) => {
const margins = toMargins(value, absoluteSize);
return {
top: margins[0],
right: margins[1],
bottom: margins[2],
left: margins[3]
};
};
/**
* Converts the given margins as a string in the
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/margin | expected format}
*
* @param value If it's a number, it's used for all four sides.