UNPKG

@schukai/monster

Version:

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

253 lines (218 loc) 6.42 kB
/** * Copyright © schukai GmbH 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 schukai GmbH. * * SPDX-License-Identifier: AGPL-3.0 */ import { arrow, autoPlacement, autoUpdate, detectOverflow, computePosition, offset, flip, size, shift, } from "@floating-ui/dom"; import { Processing } from "../../../util/processing.mjs"; import { getDocument } from "../../../dom/util.mjs"; import { isString, isArray, isObject, isFunction } from "../../../types/is.mjs"; export { positionPopper }; /** * @private * @param controlElement * @param popperElement * @param options * @return {Promise|*} */ function positionPopper(controlElement, popperElement, options) { const body = getDocument().body; return new Processing(() => { const arrowElement = controlElement.querySelector( "[data-monster-role=arrow]", ); const config = Object.assign( {}, { placement: "top", }, options, ); const middleware = config?.["middleware"]; if (!isArray(middleware)) { if (isString(middleware)) { config["middleware"] = middleware.split(",").filter((line) => { return line.trim().length > 0; }); } else { config["middleware"] = []; } } for (const key in middleware) { const line = middleware[key]; if (isFunction(line)) { continue; } if (isObject(line)) { continue; } if (!isString(line)) { throw new Error( `Middleware must be a string, a function or an object. Got ${typeof line}`, ); } const kv = line.split(":"); const fn = kv.shift(); switch (fn) { case "flip": config["middleware"][key] = flip(); break; case "shift": config["middleware"][key] = shift(); break; case "autoPlacement": let defaultAllowedPlacements = ["top", "bottom", "left", "right"]; const defPlacement = kv?.shift(); if (isString(defPlacement) && defPlacement.trim().length > 0) { defaultAllowedPlacements = defPlacement .split(",") .filter((line) => { return line.trim().length > 0; }); } if (defaultAllowedPlacements.includes(config.placement)) { defaultAllowedPlacements.splice( defaultAllowedPlacements.indexOf(config.placement), 1, ); } defaultAllowedPlacements.unshift(config.placement); config["middleware"][key] = autoPlacement({ crossAxis: true, autoAlignment: true, allowedPlacements: defaultAllowedPlacements, }); break; case "detectOverflow": config["middleware"][key] = detectOverflow(); break; case "arrow": if (arrowElement) { config["middleware"][key] = arrow({ element: arrowElement }); } break; case "size": config["middleware"][key] = size({ apply({ availableWidth, availableHeight, elements }) { const maxWidth = body.clientWidth; const maxHeight = body.clientHeight; if (availableWidth < 0) { availableWidth = 0; } if (availableHeight < 0) { availableHeight = 0; } if (availableWidth > maxWidth) { availableWidth = maxWidth; } if (availableHeight > maxHeight) { availableHeight = maxHeight; } Object.assign(elements.floating.style, { boxSizing: "border-box", maxWidth: `${availableWidth}px`, maxHeight: `${availableHeight}px`, }); }, }); break; case "offset": const o = kv?.shift(); config["middleware"][key] = offset(parseInt(o) || 10); break; case "hide": config["middleware"][key] = hide(); break; default: throw new Error(`Unknown function: ${fn}`); } } popperElement.style.removeProperty("visibility"); popperElement.style.display = "block"; autoUpdate(controlElement, popperElement, () => { computePosition(controlElement, popperElement, config).then( ({ x, y, placement, middlewareData }) => { Object.assign(popperElement.style, { top: "0", left: "0", transform: `translate(${roundByDPR(x)}px,${roundByDPR(y)}px)`, }); if (middlewareData.arrow) { const side = placement.split("-")[0]; const staticSide = { top: "bottom", right: "left", bottom: "top", left: "right", }[side]; // monster-border-width = + 4 (2*2) (should come from css) const arrowLen = arrowElement.offsetWidth + 4; const borderStyle = { borderLeft: "transparent", borderRight: "transparent", borderBottom: "transparent", borderTop: "transparent", }; const defaultBorder = "var(--monster-border-width) var(--monster-border-style) var(--monster-bg-color-primary-4)"; switch (side) { case "top": borderStyle.borderRight = defaultBorder; borderStyle.borderBottom = defaultBorder; break; case "bottom": borderStyle.borderTop = defaultBorder; borderStyle.borderLeft = defaultBorder; break; case "left": borderStyle.borderRight = defaultBorder; borderStyle.borderTop = defaultBorder; break; case "right": borderStyle.borderBottom = defaultBorder; borderStyle.borderLeft = defaultBorder; break; } const { x, y } = middlewareData.arrow; Object.assign( arrowElement.style, { left: x != null ? `${x}px` : "", top: y != null ? `${y}px` : "", // Ensure the static side gets unset when // flipping to other placements' axes. right: "", bottom: "", [staticSide]: `${-arrowLen / 2}px`, transform: "rotate(45deg)", }, borderStyle, ); } }, ); }); }).run(); } function roundByDPR(value) { const dpr = window.devicePixelRatio || 1; return Math.round(value * dpr) / dpr; }