import * as FloatingUI from "@floating-ui/dom"; import { cancelPropagation, dom, svg, type Class, type NodeChildren } from "./dom.util"; export interface CommonProperties { placement?: FloatingUI.Placement; offset?: number; arrow?: boolean; class?: Class; content?: NodeChildren; } export interface PopperProperties extends CommonProperties { delay?: number; onShow?: (element: HTMLDivElement) => boolean | void; onHide?: (element: HTMLDivElement) => boolean | void; } let teleport: HTMLDivElement; export function init() { teleport = dom('div', { attributes: { id: 'popper-container' }, class: 'absolute top-0 left-0' }); document.body.appendChild(teleport); } export function popper(container: HTMLElement, properties?: PopperProperties): HTMLElement { let shown = false, timeout: Timer; const arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "10", height: "7", viewBox: "0 0 30 10" } }, [svg('polygon', { attributes: { points: "0,0 30,0 15,10" } })]); const content = dom('div', { class: ['fixed hidden', properties?.class], attributes: { 'data-state': 'closed' } }, [...(properties?.content ?? []), arrow]); function update() { FloatingUI.computePosition(container, content, { placement: properties?.placement, strategy: 'fixed', middleware: [ properties?.offset ? FloatingUI.offset(properties?.offset) : undefined, FloatingUI.flip(), properties?.offset ? FloatingUI.shift({ padding: properties?.offset }) : undefined, properties?.offset && properties?.arrow ? FloatingUI.arrow({ element: arrow, padding: 8 }) : undefined, ] }).then(({ x, y, placement, middlewareData }) => { Object.assign(content.style, { left: `${x}px`, top: `${y}px`, }); const side = placement.split('-')[0] as FloatingUI.Side; content.setAttribute('data-side', side); if(properties?.offset && properties?.arrow) { const { x: arrowX, y: arrowY } = middlewareData.arrow!; const staticSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right', }[side]!; const rotation = { top: "0", bottom: "180", left: "270", right: "90" }[side]!; Object.assign(arrow.style, { left: arrowX != null ? `${arrowX}px` : '', top: arrowY != null ? `${arrowY}px` : '', right: '', bottom: '', [staticSide]: `-6px`, transform: `rotate(${rotation}deg)`, }); } }); } let stop: () => void | undefined; function show() { if(shown || !properties?.onShow || properties?.onShow(content) !== false) { clearTimeout(timeout); timeout = setTimeout(() => { if(!shown) { teleport!.appendChild(content); content.setAttribute('data-state', 'open'); content.classList.toggle('hidden', false); update(); stop = FloatingUI.autoUpdate(container, content, update, { animationFrame: true, layoutShift: false, elementResize: false, ancestorScroll: false, ancestorResize: false, }); } shown = true; }, properties?.delay ?? 0); } } function hide() { if(!properties?.onHide || properties?.onHide(content) !== false) { clearTimeout(timeout); timeout = setTimeout(() => { content.remove(); stop && stop(); shown = false; }, shown ? properties?.delay ?? 0 : 0); } } function link(element: HTMLElement) { Object.entries({ 'mouseenter': show, 'mouseleave': hide, 'focus': show, 'blur': hide, } as Record void>).forEach(([event, listener]) => { element.addEventListener(event, listener); }); } link(container); link(content); return container; } export function contextmenu(x: number, y: number, properties?: CommonProperties): () => void { const virtual = { getBoundingClientRect() { return { x: x, y: y, top: y, left: x, bottom: y, right: x, width: 0, height: 0, }; }, }; const arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "10", height: "7", viewBox: "0 0 30 10" } }, [svg('polygon', { attributes: { points: "0,0 30,0 15,10" } })]); const container = dom('div', { class: ['fixed bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 z-50', properties?.class] }, [...(properties?.content ?? [])]); function update() { FloatingUI.computePosition(virtual, container, { placement: properties?.placement, strategy: 'fixed', middleware: [ properties?.offset ? FloatingUI.offset(properties?.offset) : undefined, FloatingUI.flip(), properties?.offset ? FloatingUI.shift({ padding: properties?.offset }) : undefined, properties?.offset && properties?.arrow ? FloatingUI.arrow({ element: arrow, padding: 8 }) : undefined, ] }).then(({ x, y, placement, middlewareData }) => { Object.assign(container.style, { left: `${x}px`, top: `${y}px`, }); const side = placement.split('-')[0] as FloatingUI.Side; container.setAttribute('data-side', side); if(properties?.offset && properties?.arrow) { const { x: arrowX, y: arrowY } = middlewareData.arrow!; const staticSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right', }[side]!; const rotation = { top: "0", bottom: "180", left: "270", right: "90" }[side]!; Object.assign(arrow.style, { left: arrowX != null ? `${arrowX}px` : '', top: arrowY != null ? `${arrowY}px` : '', right: '', bottom: '', [staticSide]: `-6px`, transform: `rotate(${rotation}deg)`, }); } }); } update(); document.addEventListener('mousedown', close); container.addEventListener('mousedown', cancelPropagation); const stop = FloatingUI.autoUpdate(virtual, container, update, { animationFrame: true, layoutShift: false, elementResize: false, ancestorScroll: false, ancestorResize: false, }); teleport!.appendChild(container); function close() { document.removeEventListener('mousedown', close); container.removeEventListener('mousedown', cancelPropagation); container.remove(); stop(); } return close; } //TODO export function modal() { }