obsidian-visualiser/shared/floating.util.ts

271 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as FloatingUI from "@floating-ui/dom";
import { cancelPropagation, dom, svg, text, type Class, type NodeChildren } from "./dom.util";
import { button } from "./proses";
export interface ContextProperties
{
placement?: FloatingUI.Placement;
offset?: number;
arrow?: boolean;
class?: Class;
}
export interface PopperProperties extends ContextProperties
{
content?: NodeChildren;
delay?: number;
viewport?: HTMLElement;
onShow?: (element: HTMLDivElement) => boolean | void;
onHide?: (element: HTMLDivElement) => boolean | void;
}
export interface ModalProperties
{
priority?: boolean;
closeWhenOutside?: boolean;
}
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]);
const rect = properties?.viewport?.getBoundingClientRect() ?? 'viewport';
function update()
{
FloatingUI.computePosition(container, content, {
placement: properties?.placement,
strategy: 'fixed',
middleware: [
properties?.offset ? FloatingUI.offset(properties?.offset) : undefined,
FloatingUI.shift({ rootBoundary: rect }),
properties?.offset ? FloatingUI.shift({ padding: properties?.offset, rootBoundary: rect }) : undefined,
properties?.offset && properties?.arrow ? FloatingUI.arrow({ element: arrow, padding: 8 }) : undefined,
FloatingUI.hide({ rootBoundary: rect }),
]
}).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, content: NodeChildren, properties?: ContextProperties): () => 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] }, 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;
}
export function modal(content: NodeChildren, properties?: ModalProperties)
{
const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }], listeners: { click: properties?.closeWhenOutside ? (() => _modal.remove()) : undefined } });
const _closer = properties?.priority ? undefined : dom('span', { class: 'absolute top-4 right-4', text: '×', listeners: { click: () => _modal.remove() } });
const _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, dom('div', { class: 'max-h-[85vh] max-w-[450px] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 text-light-100 dark:text-dark-100 z-10 relative' }, content)])
teleport.appendChild(_modal);
return {
close: () => _modal.remove(),
}
}
export function confirm(title: string): Promise<boolean>
{
return new Promise(res => {
const mod = modal([ dom('div', { class: 'flex flex-col justify-start gap-4' }, [ dom('div', { class: 'text-xl' }, [ text(title) ]), dom('div', { class: 'flex flex-row gap-2' }, [ button(text("Non"), () => (mod.close(), res(false)), 'h-[35px] px-[15px]'), button(text("Oui"), () => (mod.close(), res(true)), 'h-[35px] px-[15px] !border-light-red dark:!border-dark-red text-light:red dark:text-dark-red') ]) ]) ], {
priority: true,
closeWhenOutside: false,
});
})
}