242 lines
7.9 KiB
TypeScript
242 lines
7.9 KiB
TypeScript
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<keyof HTMLElementEventMap, () => 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()
|
|
{
|
|
} |