obsidian-visualiser/shared/floating.util.ts

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()
{
}