UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

368 lines (321 loc) 7.54 kB
/** * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact Volker Schukai. * * SPDX-License-Identifier: AGPL-3.0 */ import { arrow, autoPlacement, autoUpdate, computePosition, flip, hide, offset, shift, size, } from "@floating-ui/dom"; import { extend } from "../../../data/extend.mjs"; import { isArray, isElement, isFunction, isNumber, isObject, isString, } from "../../../types/is.mjs"; import { validateBoolean } from "../../../types/validate.mjs"; export { createFloatingPopper, setEventListenersModifiers, popperInstanceSymbol, }; /** * @private * @type {symbol} */ const popperInstanceSymbol = Symbol("popperInstance"); /** * @private * @type {symbol} */ const optionsSymbol = Symbol("options"); /** * @private * @type {symbol} */ const referenceElementSymbol = Symbol("referenceElement"); /** * @private * @type {symbol} */ const popperElementSymbol = Symbol("popperElement"); /** * @private * @type {symbol} */ const autoUpdateCleanupSymbol = Symbol("autoUpdateCleanup"); /** * @private * @param {HTMLElement} referenceElement * @param {HTMLElement} popperElement * @param {object} options * @return {{destroy: Function, setOptions: Function, update: Function}} */ function createFloatingPopper(referenceElement, popperElement, options = {}) { const instance = { [referenceElementSymbol]: referenceElement, [popperElementSymbol]: popperElement, [optionsSymbol]: normalizeOptions(options), [autoUpdateCleanupSymbol]: null, destroy() { stopAutoUpdate(instance); }, setOptions(nextOptions) { const normalizedOptions = isFunction(nextOptions) ? nextOptions(instance[optionsSymbol]) : nextOptions; instance[optionsSymbol] = normalizeOptions(normalizedOptions); syncAutoUpdate(instance); return instance.update(); }, update() { return updatePosition(instance); }, }; syncAutoUpdate(instance); return instance; } /** * @private * @this {CustomElement} * @param {Boolean} mode */ function setEventListenersModifiers(mode) { const options = extend({}, this.getOption("popper")); const modifiers = options?.modifiers; if (!isArray(modifiers)) { options.modifiers = []; } const existingModifier = options.modifiers.find((entry) => { return entry?.name === "eventListeners"; }); if (existingModifier) { existingModifier.enabled = validateBoolean(mode); } else { options.modifiers.push({ name: "eventListeners", enabled: validateBoolean(mode), }); } return this[popperInstanceSymbol].setOptions(options); } /** * @private * @param {object} instance * @return {Promise<void>} */ function updatePosition(instance) { const referenceElement = instance[referenceElementSymbol]; const popperElement = instance[popperElementSymbol]; const options = instance[optionsSymbol]; if (!isElement(referenceElement) || !isElement(popperElement)) { return Promise.resolve(); } return computePosition(referenceElement, popperElement, { placement: options.placement, strategy: options.strategy, middleware: options.middleware, }).then(({ x, y, strategy }) => { Object.assign(popperElement.style, { position: strategy, left: `${roundByDPR(x)}px`, top: `${roundByDPR(y)}px`, transform: "", }); }); } /** * @private * @param {object} instance * @return {void} */ function syncAutoUpdate(instance) { stopAutoUpdate(instance); if (instance[optionsSymbol].eventListeners !== true) { return; } instance[autoUpdateCleanupSymbol] = autoUpdate( instance[referenceElementSymbol], instance[popperElementSymbol], () => { void instance.update(); }, { elementResize: typeof ResizeObserver === "function", layoutShift: false, }, ); } /** * @private * @param {object} instance * @return {void} */ function stopAutoUpdate(instance) { const cleanup = instance[autoUpdateCleanupSymbol]; if (typeof cleanup === "function") { cleanup(); } instance[autoUpdateCleanupSymbol] = null; } /** * @private * @param {object} options * @return {object} */ function normalizeOptions(options) { const config = extend( { placement: "bottom", strategy: "absolute", modifiers: [], }, options || {}, ); config.eventListeners = normalizeEventListeners(config.modifiers); config.middleware = normalizeMiddleware(config); if (config.placement === "auto") { config.placement = "bottom"; config.middleware.unshift( autoPlacement({ crossAxis: true, autoAlignment: true, }), ); } return config; } /** * @private * @param {object[]} modifiers * @return {boolean} */ function normalizeEventListeners(modifiers) { if (!isArray(modifiers)) { return true; } let result = true; for (const entry of modifiers) { if (entry?.name === "eventListeners") { result = validateBoolean(entry.enabled); } } return result; } /** * @private * @param {object} config * @return {Array} */ function normalizeMiddleware(config) { const result = []; const middleware = []; if (isArray(config?.middleware)) { middleware.push(...config.middleware); } if (isArray(config?.modifiers)) { middleware.push(...config.modifiers); } for (const entry of middleware) { if (isFunction(entry)) { result.push(entry); continue; } if (isObject(entry) && isFunction(entry?.fn)) { result.push(entry); continue; } if (isObject(entry) && !isString(entry?.name)) { result.push(entry); continue; } if (!isObject(entry) || !isString(entry?.name)) { continue; } const normalizedEntry = normalizeModifier(entry); if (normalizedEntry) { result.push(normalizedEntry); } } return result; } /** * @private * @param {{name: string, options?: object, enabled?: boolean}} modifier * @return {object|null} */ function normalizeModifier(modifier) { if (modifier.enabled === false) { return null; } switch (modifier.name) { case "arrow": if (!isElement(modifier?.options?.element)) { return null; } return arrow(modifier.options); case "autoPlacement": return autoPlacement(modifier.options); case "eventListeners": return null; case "flip": return flip(modifier.options); case "hide": return hide(modifier.options); case "offset": return offset(normalizeOffset(modifier.options?.offset)); case "preventOverflow": return shift(modifier.options); case "shift": return shift(modifier.options); case "size": return size(modifier.options); default: return null; } } /** * @private * @param {number|Array<number>} rawOffset * @return {number|object} */ function normalizeOffset(rawOffset) { if (isArray(rawOffset)) { const [skidding = 0, distance = 0] = rawOffset; return { mainAxis: distance, crossAxis: skidding, }; } if (isNumber(rawOffset)) { return rawOffset; } return 0; } /** * @private * @param {number} value * @return {number} */ function roundByDPR(value) { const dpr = window.devicePixelRatio || 1; return Math.round(value * dpr) / dpr; }