UNPKG

@schukai/monster

Version:

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

1,190 lines (1,016 loc) 31.3 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, offset, flip, hide, size, shift, } from "@floating-ui/dom"; import { isArray, isFunction, isObject, isString } from "../../../types/is.mjs"; import { Processing } from "../../../util/processing.mjs"; export { applyAdaptiveFloatingElementSize, closePositionedPopper, resolveClippingBoundaryElement, resolveParentPopperContentBoundary, isPositionedPopperOpen, openPositionedPopper, positionPopper, }; const autoUpdateCleanupMap = new WeakMap(); const settlingFrameMap = new WeakMap(); const floatingResizeObserverMap = new WeakMap(); const floatingSyncCycleMap = new WeakMap(); const floatingAppearanceFrameMap = new WeakMap(); const floatingAppearanceTimeoutMap = new WeakMap(); const adaptiveScrollHeightDatasetKey = "monsterAdaptiveScrollHeight"; /** * @private * @param controlElement * @param popperElement * @param options * @return {Promise|*} */ function positionPopper(controlElement, popperElement, options) { const config = normalizePopperConfig(options, controlElement, popperElement); return new Processing(() => { enableFloatingPositioning(controlElement, popperElement, config); }).run(); } function openPositionedPopper(controlElement, popperElement, options) { const config = normalizePopperConfig(options, controlElement, popperElement); stopAutoUpdate(popperElement); cancelFloatingAppearanceFrame(popperElement); popperElement.dataset.monsterAppearance = "opening"; popperElement.style.display = "block"; popperElement.style.position = config.strategy; popperElement.style.removeProperty("transform"); // Keep the call signature stable even though only Floating UI is used now. void controlElement; } function enableFloatingPositioning(controlElement, popperElement, config) { if (!isPositionedPopperOpen(popperElement)) { return; } popperElement.style.removeProperty("visibility"); scheduleFloatingAppearanceOpen(popperElement); startFloatingResizeObserver(controlElement, popperElement, config); startAutoUpdate(controlElement, popperElement, () => { if (!isPositionedPopperOpen(popperElement)) { return; } runFloatingUpdateHook(popperElement); syncFloatingPopover(controlElement, popperElement, config); }); runFloatingUpdateHook(popperElement); syncFloatingPopover(controlElement, popperElement, config); } function syncFloatingPopover( controlElement, popperElement, config, allowSettlingPass = true, ) { if (!isPositionedPopperOpen(popperElement)) { return; } const arrowElement = popperElement.querySelector("[data-monster-role=arrow]"); const syncCycleId = claimFloatingSyncCycle(popperElement); const floatingMiddleware = buildFloatingMiddleware( config.middleware, config.placement, config.detectOverflowOptions, popperElement, syncCycleId, ); resetAdaptiveFloatingElementSize(popperElement); if ( arrowElement instanceof HTMLElement && config.middlewareTokens.includes("arrow") ) { floatingMiddleware.push(arrow({ element: arrowElement })); } if (!config.middlewareTokens.includes("size")) { floatingMiddleware.push( createAdaptiveSizeMiddleware( config.detectOverflowOptions, popperElement, syncCycleId, ), ); } computePosition( controlElement, popperElement, Object.assign({}, config, { middleware: floatingMiddleware, }), ).then(({ x, y, placement, middlewareData }) => { if (!isActiveFloatingSyncCycle(popperElement, syncCycleId)) { return; } if (!isPositionedPopperOpen(popperElement)) { return; } Object.assign(popperElement.style, { top: "0", left: "0", position: config.strategy, transform: `translate(${roundByDPR(x)}px,${roundByDPR(y)}px)`, }); if (middlewareData.arrow) { applyFloatingArrowStyles(arrowElement, placement, middlewareData.arrow); } if (allowSettlingPass) { scheduleSettlingPass(controlElement, popperElement, config); } }); } function closePositionedPopper(popperElement) { stopAutoUpdate(popperElement); cancelFloatingAppearanceFrame(popperElement); delete popperElement.dataset.monsterAppearance; popperElement.style.display = "none"; popperElement.style.removeProperty("visibility"); popperElement.style.removeProperty("position"); popperElement.style.removeProperty("transform"); } function isPositionedPopperOpen(popperElement) { return popperElement.style.display === "block"; } function normalizePopperConfig(options, controlElement, popperElement) { const config = Object.assign( {}, { placement: "top", engine: "floating", strategy: "absolute", }, options, ); if (!(config.boundaryElement instanceof HTMLElement)) { config.boundaryElement = resolveClippingBoundaryElement( controlElement, popperElement, ); } config.detectOverflowOptions = buildDetectOverflowOptions( config.boundaryElement, ); config.middleware = normalizeMiddleware(config); config.middlewareTokens = config.middleware.filter((line) => isString(line)); return config; } function normalizeMiddleware(config) { const middleware = config?.middleware; if (isArray(middleware)) { return [...middleware]; } if (isString(middleware)) { return middleware.split(",").filter((line) => line.trim().length > 0); } return []; } function buildFloatingMiddleware( middleware, placement, detectOverflowOptions, popperElement, syncCycleId = null, ) { const result = [...middleware]; for (const key in result) { const line = result[key]; if (isFunction(line) || 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": result[key] = flip(detectOverflowOptions); break; case "shift": result[key] = shift(normalizeShiftOptions(kv, detectOverflowOptions)); break; case "autoPlacement": let defaultAllowedPlacements = ["top", "bottom", "left", "right"]; const defPlacement = kv?.shift(); if (isString(defPlacement) && defPlacement.trim().length > 0) { defaultAllowedPlacements = defPlacement .split(",") .filter((entry) => entry.trim().length > 0); } if (defaultAllowedPlacements.includes(placement)) { defaultAllowedPlacements.splice( defaultAllowedPlacements.indexOf(placement), 1, ); } defaultAllowedPlacements.unshift(placement); result[key] = autoPlacement( Object.assign({}, detectOverflowOptions, { crossAxis: true, autoAlignment: true, allowedPlacements: defaultAllowedPlacements, }), ); break; case "arrow": result[key] = null; break; case "size": result[key] = createAdaptiveSizeMiddleware( detectOverflowOptions, popperElement, syncCycleId, ); break; case "offset": result[key] = offset(parseInt(kv?.shift()) || 10); break; case "hide": result[key] = hide(detectOverflowOptions); break; default: throw new Error(`Unknown function: ${fn}`); } } return result.filter(Boolean); } function normalizeShiftOptions(tokens, detectOverflowOptions) { const options = Object.assign({}, detectOverflowOptions); const normalizedTokens = tokens .join(":") .split(",") .map((token) => token.trim()) .filter((token) => token.length > 0); for (const token of normalizedTokens) { switch (token) { case "crossAxis": options.crossAxis = true; break; case "mainAxis=false": options.mainAxis = false; break; default: throw new Error(`Unknown shift option: ${token}`); } } return options; } function createAdaptiveSizeMiddleware( detectOverflowOptions, popperElement, syncCycleId = null, ) { return size( Object.assign({}, detectOverflowOptions, { apply({ availableWidth, availableHeight, elements }) { const floatingElement = elements?.floating || popperElement; if (!(floatingElement instanceof HTMLElement)) { return; } if ( syncCycleId !== null && !isActiveFloatingSyncCycle(floatingElement, syncCycleId) ) { return; } applyAdaptiveFloatingElementSize(floatingElement, { availableWidth, availableHeight, }); }, }), ); } function applyAdaptiveFloatingElementSize( floatingElement, { availableWidth, availableHeight }, ) { const contentElement = getFloatingContentElement(floatingElement); const usesVisibleOverflow = contentElement instanceof HTMLElement && contentElement.dataset.monsterOverflowMode === "visible"; const maxWidth = clampAvailableDimension( availableWidth, readMaxDimension(floatingElement, "maxWidth"), ); const maxHeight = clampAvailableDimension( availableHeight, readMaxDimension(floatingElement, "maxHeight"), ); const nextStyle = { boxSizing: "border-box", }; if (Number.isFinite(maxWidth) && maxWidth > 0) { nextStyle.maxWidth = `${maxWidth}px`; } else { nextStyle.maxWidth = ""; } if (!usesVisibleOverflow && Number.isFinite(maxHeight) && maxHeight > 0) { nextStyle.maxHeight = `${maxHeight}px`; } else { nextStyle.maxHeight = ""; } Object.assign(floatingElement.style, nextStyle); syncPreferredFloatingWidth(floatingElement, maxWidth); syncPreferredFloatingMaxWidth(floatingElement, maxWidth); applyAdaptiveFloatingContentSize(floatingElement, maxHeight); } function applyAdaptiveFloatingContentSize(floatingElement, maxHeight) { const contentElement = getFloatingContentElement(floatingElement); if (!(contentElement instanceof HTMLElement)) { return; } const overflowMode = contentElement.dataset.monsterOverflowMode; if (overflowMode === "visible") { return; } const reservedHeight = getReservedFloatingHeight( floatingElement, contentElement, ); const contentMaxHeight = clampAvailableDimension( Number.isFinite(maxHeight) ? maxHeight - reservedHeight : null, readMaxDimension(contentElement, "maxHeight"), ); const minimumReadableHeight = getMinimumReadableContentHeight(contentElement); const nextContentMaxHeight = Number.isFinite(contentMaxHeight) && contentMaxHeight > 0 ? Math.max(contentMaxHeight, minimumReadableHeight) : contentMaxHeight; const preferredContentHeight = getRequiredContentHeight(contentElement); if (overflowMode === "horizontal" || overflowMode === "both") { // Overlay-aware modes keep the parent wrapper sized to the real inline content. // Nested poppers may still escape, but they must not inflate the outer popper height. const contentHeightConstrained = applyOverlayAwareContentHeight( contentElement, preferredContentHeight, nextContentMaxHeight, minimumReadableHeight, ); syncNestedScrollContainerHeight( contentElement, contentHeightConstrained ? nextContentMaxHeight : null, ); return; } if (Number.isFinite(nextContentMaxHeight) && nextContentMaxHeight > 0) { contentElement.style.maxHeight = `${nextContentMaxHeight}px`; } else { contentElement.style.maxHeight = ""; } syncNestedScrollContainerHeight(contentElement, nextContentMaxHeight); } function applyOverlayAwareContentHeight( contentElement, preferredContentHeight, contentMaxHeight, minimumReadableHeight, ) { const preferredHeight = Number.isFinite(preferredContentHeight) && preferredContentHeight > 0 ? Math.max(preferredContentHeight, minimumReadableHeight) : minimumReadableHeight; const constrainedHeight = Number.isFinite(contentMaxHeight) && contentMaxHeight > 0 ? Math.min(preferredHeight, contentMaxHeight) : preferredHeight; const isConstrained = Number.isFinite(contentMaxHeight) && contentMaxHeight > 0 && preferredHeight > contentMaxHeight; contentElement.style.removeProperty("overflow"); contentElement.style.overflowX = contentElement.dataset.monsterOverflowMode === "horizontal" ? "auto" : "visible"; contentElement.style.overflowY = isConstrained ? "auto" : "visible"; if ( isConstrained && Number.isFinite(constrainedHeight) && constrainedHeight > 0 ) { contentElement.style.height = `${constrainedHeight}px`; contentElement.style.maxHeight = `${constrainedHeight}px`; return true; } // overflow-x:auto computes overflow-y to auto in browsers, so an unconstrained // horizontal wrapper must keep its natural height to avoid a spurious scrollbar. contentElement.style.removeProperty("height"); if (Number.isFinite(contentMaxHeight) && contentMaxHeight > 0) { contentElement.style.maxHeight = `${contentMaxHeight}px`; } else { contentElement.style.removeProperty("maxHeight"); } return false; } function getFloatingContentElement(floatingElement) { for (const child of floatingElement.children) { if (!(child instanceof HTMLElement)) { continue; } const part = child.getAttribute("part"); if ( isString(part) && part .split(/\s+/) .filter((token) => token.length > 0) .includes("content") ) { return child; } } return null; } function getReservedFloatingHeight(floatingElement, contentElement) { const floatingStyle = getComputedStyle(floatingElement); let reservedHeight = 0; reservedHeight += readBoxDimension(floatingStyle.paddingTop); reservedHeight += readBoxDimension(floatingStyle.paddingBottom); reservedHeight += readBoxDimension(floatingStyle.borderTopWidth); reservedHeight += readBoxDimension(floatingStyle.borderBottomWidth); for (const child of floatingElement.children) { if (!(child instanceof HTMLElement) || child === contentElement) { continue; } const childStyle = getComputedStyle(child); reservedHeight += child.getBoundingClientRect().height; reservedHeight += readBoxDimension(childStyle.marginTop); reservedHeight += readBoxDimension(childStyle.marginBottom); } return Math.max(0, reservedHeight); } function readBoxDimension(rawValue) { const value = Number.parseFloat(rawValue); return Number.isFinite(value) ? value : 0; } function getMinimumReadableContentHeight(contentElement) { if (!(contentElement instanceof HTMLElement)) { return 0; } const measurementTarget = getPrimaryReadableContentElement(contentElement) || contentElement; const style = getComputedStyle(measurementTarget); const lineHeight = readLineHeight(style); const paddingHeight = readBoxDimension(getComputedStyle(contentElement).paddingTop) + readBoxDimension(getComputedStyle(contentElement).paddingBottom); return Math.max(0, lineHeight + paddingHeight); } function readLineHeight(style) { if (!style) { return 0; } const lineHeight = Number.parseFloat(style.lineHeight); if (Number.isFinite(lineHeight) && lineHeight > 0) { return lineHeight; } const fontSize = Number.parseFloat(style.fontSize); if (Number.isFinite(fontSize) && fontSize > 0) { return fontSize * 1.4; } return 0; } function getPrimaryReadableContentElement(contentElement) { if (!(contentElement instanceof HTMLElement)) { return null; } const slotElement = contentElement.querySelector("slot"); if (slotElement?.assignedElements instanceof Function) { for (const element of slotElement.assignedElements({ flatten: true })) { if (element instanceof HTMLElement) { return element; } } } for (const child of contentElement.children) { if (child instanceof HTMLElement) { return child; } } return null; } function syncNestedScrollContainerHeight(contentElement, contentMaxHeight) { const nestedScrollableElement = getNestedScrollableElement(contentElement); if (!(nestedScrollableElement instanceof HTMLElement)) { return; } if (Number.isFinite(contentMaxHeight) && contentMaxHeight > 0) { const nextNestedHeight = resolveNestedScrollContainerHeight( nestedScrollableElement, contentMaxHeight, ); nestedScrollableElement.style.height = `${nextNestedHeight}px`; nestedScrollableElement.style.maxHeight = `${nextNestedHeight}px`; nestedScrollableElement.dataset[adaptiveScrollHeightDatasetKey] = "true"; return; } if ( nestedScrollableElement.dataset[adaptiveScrollHeightDatasetKey] !== "true" ) { return; } nestedScrollableElement.style.height = ""; nestedScrollableElement.style.maxHeight = ""; delete nestedScrollableElement.dataset[adaptiveScrollHeightDatasetKey]; } function resolveNestedScrollContainerHeight( nestedScrollableElement, contentMaxHeight, ) { const declaredHeight = readDeclaredDimension( nestedScrollableElement, "height", ); const declaredMaxHeight = readDeclaredDimension( nestedScrollableElement, "maxHeight", ); const scrollHeight = nestedScrollableElement.scrollHeight; const preferredHeightCandidates = [ declaredHeight, declaredMaxHeight, scrollHeight, ]; const preferredHeight = preferredHeightCandidates.find((value) => { return Number.isFinite(value) && value > 0; }); if (Number.isFinite(preferredHeight) && preferredHeight > 0) { return Math.min(contentMaxHeight, preferredHeight); } return contentMaxHeight; } function readDeclaredDimension(element, property) { if (!(element instanceof HTMLElement)) { return NaN; } const value = Number.parseFloat(element.style?.[property] || ""); return Number.isFinite(value) ? value : NaN; } function syncPreferredFloatingWidth(floatingElement, maxWidth) { const preferredWidth = Number.parseFloat( floatingElement.dataset.monsterPreferredWidth || "", ); if (!Number.isFinite(preferredWidth) || preferredWidth <= 0) { return; } const widthBehavior = floatingElement.dataset.monsterWidthBehavior; if (widthBehavior === "preferred") { const nextFloatingWidth = clampAvailableDimension( preferredWidth, Number.isFinite(maxWidth) ? maxWidth : Infinity, ); if (Number.isFinite(nextFloatingWidth) && nextFloatingWidth > 0) { floatingElement.style.width = `${nextFloatingWidth}px`; floatingElement.style.minWidth = `${nextFloatingWidth}px`; } return; } const contentElement = getFloatingContentElement(floatingElement); if (!(contentElement instanceof HTMLElement)) { floatingElement.style.width = `${preferredWidth}px`; return; } const floatingRect = floatingElement.getBoundingClientRect(); const contentRect = contentElement.getBoundingClientRect(); const reservedWidth = Math.max(0, floatingRect.width - contentRect.width); const contentRequiredWidth = getRequiredContentWidth(contentElement); const preferredFloatingWidth = preferredWidth; const requiredFloatingWidth = Math.max( preferredFloatingWidth, contentRequiredWidth + reservedWidth, ); const nextFloatingWidth = clampAvailableDimension( requiredFloatingWidth, Number.isFinite(maxWidth) ? maxWidth : Infinity, ); if (Number.isFinite(nextFloatingWidth) && nextFloatingWidth > 0) { floatingElement.style.width = `${nextFloatingWidth}px`; } } function syncPreferredFloatingMaxWidth(floatingElement, maxWidth) { const preferredMaxWidth = Number.parseFloat( floatingElement.dataset.monsterPreferredMaxWidth || "", ); if (!Number.isFinite(preferredMaxWidth) || preferredMaxWidth <= 0) { return; } const nextFloatingMaxWidth = clampAvailableDimension( preferredMaxWidth, Number.isFinite(maxWidth) ? maxWidth : Infinity, ); if (Number.isFinite(nextFloatingMaxWidth) && nextFloatingMaxWidth > 0) { floatingElement.style.maxWidth = `${nextFloatingMaxWidth}px`; } } function resetAdaptiveFloatingElementSize(floatingElement) { if (!(floatingElement instanceof HTMLElement)) { return; } floatingElement.style.removeProperty("maxHeight"); const contentElement = getFloatingContentElement(floatingElement); if (!(contentElement instanceof HTMLElement)) { return; } contentElement.style.removeProperty("height"); contentElement.style.removeProperty("maxHeight"); contentElement.style.removeProperty("overflow"); contentElement.style.removeProperty("overflowX"); contentElement.style.removeProperty("overflowY"); const nestedScrollableElement = getNestedScrollableElement(contentElement); if (!(nestedScrollableElement instanceof HTMLElement)) { return; } nestedScrollableElement.style.removeProperty("height"); nestedScrollableElement.style.removeProperty("maxHeight"); delete nestedScrollableElement.dataset[adaptiveScrollHeightDatasetKey]; } function runFloatingUpdateHook(popperElement) { const hook = popperElement?.monsterBeforeFloatingUpdate; if (typeof hook === "function") { hook(); } } function getRequiredContentWidth(contentElement) { const nestedScrollableElement = getNestedScrollableElement(contentElement); if (nestedScrollableElement instanceof HTMLElement) { return Math.max( contentElement.scrollWidth, nestedScrollableElement.scrollWidth, contentElement.clientWidth, ); } return Math.max(contentElement.scrollWidth, contentElement.clientWidth); } function getRequiredContentHeight(contentElement) { const nestedScrollableElement = getNestedScrollableElement(contentElement); if (nestedScrollableElement instanceof HTMLElement) { const contentRect = contentElement.getBoundingClientRect(); const nestedRect = nestedScrollableElement.getBoundingClientRect(); const reservedHeight = Math.max(0, contentRect.height - nestedRect.height); return Math.max( contentElement.scrollHeight, nestedScrollableElement.scrollHeight + reservedHeight, contentElement.clientHeight, ); } return Math.max(contentElement.scrollHeight, contentElement.clientHeight); } function getNestedScrollableElement(contentElement) { for (const child of contentElement.children) { if (!(child instanceof HTMLElement)) { continue; } const style = getComputedStyle(child); if (["auto", "scroll"].includes(style.overflowY)) { return child; } } return null; } function buildDetectOverflowOptions(boundaryElement) { const result = { rootBoundary: "viewport", padding: 0, }; if (boundaryElement instanceof HTMLElement) { result.boundary = boundaryElement; } return result; } function resolveClippingBoundaryElement(...elements) { for (const element of elements) { const clippingBoundary = findNearestClippingContainer(element); if (clippingBoundary instanceof HTMLElement) { return clippingBoundary; } } return null; } function resolveParentPopperContentBoundary(...elements) { for (const element of elements) { const clippingBoundary = findNearestIgnoredClippingContainer(element); if (clippingBoundary instanceof HTMLElement) { return clippingBoundary; } } return null; } function findNearestClippingContainer(element) { let current = getComposedParent(element); while (current) { if ( current instanceof HTMLElement && isClippingContainer(getComputedStyle(current)) && !shouldIgnoreClippingContainer(current) ) { return current; } current = getComposedParent(current); } return null; } function findNearestIgnoredClippingContainer(element) { let current = getComposedParent(element); while (current) { if ( current instanceof HTMLElement && shouldIgnoreClippingContainer(current) && shouldEscapeParentPopperContentWrapper(current) ) { return current; } current = getComposedParent(current); } return null; } function shouldEscapeParentPopperContentWrapper(element) { if (!(element instanceof HTMLElement) || !isPopperContentWrapper(element)) { return false; } const overflowMode = element.getAttribute("data-monster-overflow-mode"); if (overflowMode === "both") { return true; } return isClippingContainer(getComputedStyle(element)); } function getComposedParent(node) { if (!node) { return null; } if (node instanceof Element && node.assignedSlot) { return node.assignedSlot; } if (node instanceof ShadowRoot) { return node.host || null; } if (node.parentElement instanceof HTMLElement) { return node.parentElement; } const rootNode = node.getRootNode?.(); if (rootNode instanceof ShadowRoot) { return rootNode.host || null; } return node.parentNode || null; } function shouldIgnoreClippingContainer(element) { if (!(element instanceof HTMLElement)) { return false; } if (!isPopperContentWrapper(element)) { return false; } const parent = getComposedParent(element); return ( parent instanceof HTMLElement && parent.getAttribute("data-monster-role") === "popper" ); } function isPopperContentWrapper(element) { if (!(element instanceof HTMLElement)) { return false; } if (!element.hasAttribute("data-monster-overflow-mode")) { return false; } const part = element.getAttribute("part"); return ( isString(part) && part .split(/\s+/) .filter((token) => token.length > 0) .includes("content") ); } function isClippingContainer(style) { if (!style) { return false; } return [style.overflow, style.overflowX, style.overflowY].some((value) => { return ["hidden", "auto", "scroll", "clip"].includes(value); }); } function readMaxDimension(element, property) { const rawValue = getComputedStyle(element)?.[property]; if (!rawValue || rawValue === "none") { return Infinity; } const value = Number.parseFloat(rawValue); return Number.isFinite(value) ? value : Infinity; } function clampAvailableDimension(available, configuredMax) { if (!Number.isFinite(available) || available <= 0) { return null; } if (!Number.isFinite(configuredMax)) { return available; } return Math.min(available, configuredMax); } function startAutoUpdate(controlElement, popperElement, callback) { stopAutoUpdate(popperElement); autoUpdateCleanupMap.set( popperElement, autoUpdate(controlElement, popperElement, callback, { elementResize: typeof ResizeObserver === "function", layoutShift: false, }), ); } function stopAutoUpdate(popperElement) { cancelSettlingPass(popperElement); stopFloatingResizeObserver(popperElement); const cleanup = autoUpdateCleanupMap.get(popperElement); if (typeof cleanup === "function") { cleanup(); } autoUpdateCleanupMap.delete(popperElement); } function startFloatingResizeObserver(controlElement, popperElement, config) { stopFloatingResizeObserver(popperElement); if (typeof ResizeObserver !== "function") { return; } const observer = new ResizeObserver(() => { if (!isPositionedPopperOpen(popperElement)) { return; } scheduleSettlingPass(controlElement, popperElement, config); }); observer.observe(popperElement); floatingResizeObserverMap.set(popperElement, observer); } function stopFloatingResizeObserver(popperElement) { const observer = floatingResizeObserverMap.get(popperElement); if ( typeof ResizeObserver === "function" && observer instanceof ResizeObserver ) { observer.disconnect(); } floatingResizeObserverMap.delete(popperElement); } function scheduleSettlingPass(controlElement, popperElement, config) { if (settlingFrameMap.has(popperElement)) { return; } const frameId = requestAnimationFrame(() => { settlingFrameMap.delete(popperElement); if (!isPositionedPopperOpen(popperElement)) { return; } runFloatingUpdateHook(popperElement); syncFloatingPopover(controlElement, popperElement, config, false); }); settlingFrameMap.set(popperElement, frameId); } function claimFloatingSyncCycle(popperElement) { const nextCycleId = (floatingSyncCycleMap.get(popperElement) || 0) + 1; floatingSyncCycleMap.set(popperElement, nextCycleId); return nextCycleId; } function isActiveFloatingSyncCycle(popperElement, syncCycleId) { return floatingSyncCycleMap.get(popperElement) === syncCycleId; } function cancelSettlingPass(popperElement) { const frameId = settlingFrameMap.get(popperElement); if (typeof frameId === "number") { cancelAnimationFrame(frameId); } settlingFrameMap.delete(popperElement); } function scheduleFloatingAppearanceOpen(popperElement) { if (!(popperElement instanceof HTMLElement)) { return; } if (popperElement.dataset.monsterAppearance !== "opening") { return; } if (floatingAppearanceFrameMap.has(popperElement)) { return; } const finishAppearanceOpen = () => { cancelFloatingAppearanceFrame(popperElement); if (!isPositionedPopperOpen(popperElement)) { return; } popperElement.dataset.monsterAppearance = "open"; }; const schedule = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (callback) => { return setTimeout(callback, 16); }; const frameId = schedule(() => { finishAppearanceOpen(); }); const timeoutId = setTimeout(() => { finishAppearanceOpen(); }, 24); floatingAppearanceFrameMap.set(popperElement, frameId); floatingAppearanceTimeoutMap.set(popperElement, timeoutId); } function cancelFloatingAppearanceFrame(popperElement) { const frameId = floatingAppearanceFrameMap.get(popperElement); if ( frameId !== undefined && frameId !== null && Number.isNaN(frameId) === false ) { if (typeof cancelAnimationFrame === "function") { cancelAnimationFrame(frameId); } else { clearTimeout(frameId); } } floatingAppearanceFrameMap.delete(popperElement); const timeoutId = floatingAppearanceTimeoutMap.get(popperElement); if ( timeoutId !== undefined && timeoutId !== null && Number.isNaN(timeoutId) === false ) { clearTimeout(timeoutId); } floatingAppearanceTimeoutMap.delete(popperElement); } function applyFloatingArrowStyles(arrowElement, placement, arrowData) { if (!(arrowElement instanceof HTMLElement)) { return; } const side = placement.split("-")[0]; const staticSide = { top: "bottom", right: "left", bottom: "top", left: "right", }[side]; 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; } Object.assign( arrowElement.style, { left: arrowData.x != null ? `${arrowData.x}px` : "", top: arrowData.y != null ? `${arrowData.y}px` : "", right: "", bottom: "", [staticSide]: `${-arrowLen / 2}px`, transform: "rotate(45deg)", }, borderStyle, ); } function roundByDPR(value) { const dpr = window.devicePixelRatio || 1; return Math.round(value * dpr) / dpr; }