diff --git a/cptr/frontend/src/lib/components/DropdownMenu.svelte b/cptr/frontend/src/lib/components/DropdownMenu.svelte index 567f369..6740d98 100644 --- a/cptr/frontend/src/lib/components/DropdownMenu.svelte +++ b/cptr/frontend/src/lib/components/DropdownMenu.svelte @@ -62,8 +62,35 @@ let menuMaxHeight = $state(); let ready = $state(false); let frame: number | undefined; + let anchorFrame: number | undefined; let settleTimers: number[] = []; let lastViewportState = ''; + let lastAnchorState = ''; + + function portal(node: HTMLElement, enabled = true) { + const parent = node.parentNode; + const sibling = node.nextSibling; + + function move() { + if (enabled && node.parentNode !== document.body) { + document.body.appendChild(node); + } else if (!enabled && parent && node.parentNode === document.body) { + parent.insertBefore(node, sibling); + } + } + + move(); + + return { + update(nextEnabled: boolean) { + enabled = nextEnabled; + move(); + }, + destroy() { + node.remove(); + } + }; + } function visualViewportRect() { const vv = window.visualViewport; @@ -87,6 +114,25 @@ ].join(':'); } + function anchorState() { + if (!(anchor instanceof HTMLElement)) { + return `${anchor.x}:${anchor.y}:${viewportState()}`; + } + + const rect = anchor.getBoundingClientRect(); + return [ + rect.left, + rect.top, + rect.right, + rect.bottom, + rect.width, + rect.height, + viewportState() + ] + .map((value) => (typeof value === 'number' ? value.toFixed(2) : value)) + .join(':'); + } + function measureMenu() { if (!menuEl) return { width: 0, height: 0 }; @@ -204,12 +250,23 @@ } } + function trackAnchor() { + const nextAnchorState = anchorState(); + if (nextAnchorState !== lastAnchorState) { + lastAnchorState = nextAnchorState; + updatePosition(); + } + anchorFrame = requestAnimationFrame(trackAnchor); + } + onMount(() => { let dvhProbe: HTMLDivElement | undefined; let dvhObserver: ResizeObserver | undefined; lastViewportState = viewportState(); + lastAnchorState = anchorState(); scheduleUpdate(); + anchorFrame = requestAnimationFrame(trackAnchor); // Follow anchor on scroll/resize window.addEventListener('scroll', scheduleUpdate, true); @@ -235,6 +292,7 @@ return () => { if (frame != null) cancelAnimationFrame(frame); + if (anchorFrame != null) cancelAnimationFrame(anchorFrame); for (const timer of settleTimers) window.clearTimeout(timer); dvhObserver?.disconnect(); dvhProbe?.remove(); @@ -254,7 +312,8 @@
{ e.preventDefault(); @@ -264,10 +323,11 @@
+