Files
Moku/src/lib/core/ui/selectPortal.ts
T

67 lines
2.1 KiB
TypeScript

/**
* position:fixed dropdown anchored to a trigger element.
*
* getBoundingClientRect() returns full viewport coords.
* position:fixed is also relative to the viewport.
* So we just divide by zoom — no sidebar/titlebar subtraction needed
* (those subtractions are only needed in ContextMenu because its x/y come
* from a MouseEvent which is relative to the zoomed content area, not the viewport).
*/
export function selectPortal(
node: HTMLElement,
trigger: HTMLElement | undefined,
): { update(t: HTMLElement | undefined): void; destroy(): void } {
let currentTrigger = trigger
node.style.visibility = 'hidden'
function getZoom(): number {
const raw = parseFloat(document.documentElement.style.zoom || '1') || 1
return raw > 10 ? raw / 100 : raw
}
function position() {
if (!currentTrigger) return
const zoom = getZoom()
const r = currentTrigger.getBoundingClientRect()
// Convert viewport px → CSS px by dividing by zoom
const left = r.left / zoom
const top = r.top / zoom
const bottom = r.bottom / zoom
const width = r.width / zoom
const vw = window.innerWidth / zoom
const vh = window.innerHeight / zoom
const menuH = node.offsetHeight
const menuW = node.offsetWidth
const above = menuH > 0 && (vh - bottom) < menuH + 8 && top > menuH + 8
const cssLeft = Math.min(left, vw - menuW - 4)
const cssTop = above ? top - menuH - 4 : bottom + 4
node.style.left = `${Math.max(4, cssLeft)}px`
node.style.top = `${cssTop}px`
node.style.minWidth = `${width}px`
node.style.visibility = 'visible'
}
requestAnimationFrame(() => position())
window.addEventListener('scroll', position, { capture: true, passive: true })
window.addEventListener('resize', position, { passive: true })
return {
update(t) {
currentTrigger = t
node.style.visibility = 'hidden'
requestAnimationFrame(() => position())
},
destroy() {
window.removeEventListener('scroll', position, true)
window.removeEventListener('resize', position)
},
}
}