314 lines
12 KiB
TypeScript
314 lines
12 KiB
TypeScript
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 FloatingProperties
|
|
{
|
|
placement?: FloatingUI.Placement;
|
|
offset?: FloatingUI.OffsetOptions;
|
|
arrow?: boolean;
|
|
class?: Class;
|
|
style?: Record<string, string | undefined | boolean | number> | string;
|
|
viewport?: HTMLElement;
|
|
cover?: 'width' | 'height' | 'all' | 'none';
|
|
}
|
|
export interface FollowerProperties extends FloatingProperties
|
|
{
|
|
blur?: () => void;
|
|
priority?: boolean;
|
|
}
|
|
export interface PopperProperties extends FloatingProperties
|
|
{
|
|
content?: NodeChildren;
|
|
delay?: number;
|
|
|
|
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: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]);
|
|
const content = dom('div', { class: ['fixed hidden', properties?.class], style: properties?.style, 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.hide({ rootBoundary: rect, strategy: "escaped" }),
|
|
FloatingUI.hide({ rootBoundary: rect }),
|
|
FloatingUI.shift({ rootBoundary: rect }),
|
|
FloatingUI.flip({ rootBoundary: rect }),
|
|
properties?.cover && properties?.cover !== 'none' && FloatingUI.size({ rootBoundary: rect, apply: ({ availableWidth, availableHeight }) => {
|
|
if(properties?.cover === 'width' || properties?.cover === 'all')
|
|
content.style.width = `${availableWidth}px`;
|
|
if(properties?.cover === 'height' || properties?.cover === 'all')
|
|
content.style.height = `${availableHeight}px`;
|
|
} }),
|
|
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`,
|
|
visibility: middlewareData.hide?.referenceHidden || middlewareData.hide?.escaped ? 'hidden' : 'visible',
|
|
});
|
|
|
|
const side = placement.split('-')[0] as FloatingUI.Side;
|
|
|
|
content.setAttribute('data-side', side);
|
|
|
|
if(middlewareData.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]: `-8px`,
|
|
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 followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties)
|
|
{
|
|
const rect = properties?.viewport?.getBoundingClientRect() ?? 'viewport';
|
|
|
|
const arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,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], style: properties?.style }, content);
|
|
|
|
function update()
|
|
{
|
|
FloatingUI.computePosition(target, container, {
|
|
placement: properties?.placement,
|
|
strategy: 'fixed',
|
|
middleware: [
|
|
properties?.offset ? FloatingUI.offset(properties?.offset) : undefined,
|
|
FloatingUI.hide({ rootBoundary: rect, strategy: "escaped" }),
|
|
FloatingUI.hide({ rootBoundary: rect }),
|
|
FloatingUI.shift({ rootBoundary: rect }),
|
|
FloatingUI.flip({ rootBoundary: rect }),
|
|
properties?.cover && properties?.cover !== 'none' && FloatingUI.size({ rootBoundary: rect, apply: ({ availableWidth, availableHeight }) => {
|
|
if(properties?.cover === 'width' || properties?.cover === 'all')
|
|
container.style.width = `${availableWidth}px`;
|
|
if(properties?.cover === 'height' || properties?.cover === 'all')
|
|
container.style.height = `${availableHeight}px`;
|
|
} }),
|
|
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`,
|
|
visibility: middlewareData.hide?.referenceHidden || middlewareData.hide?.escaped ? 'hidden' : 'visible',
|
|
});
|
|
|
|
const side = placement.split('-')[0] as FloatingUI.Side;
|
|
|
|
container.setAttribute('data-side', side);
|
|
|
|
if(middlewareData.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]: `-8px`,
|
|
transform: `rotate(${rotation}deg)`,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
update();
|
|
|
|
properties?.priority || document.addEventListener('mousedown', close);
|
|
container.addEventListener('mousedown', cancelPropagation);
|
|
|
|
const stop = FloatingUI.autoUpdate(target, container, update, {
|
|
animationFrame: true,
|
|
layoutShift: false,
|
|
elementResize: false,
|
|
ancestorScroll: false,
|
|
ancestorResize: false,
|
|
});
|
|
teleport!.appendChild(container);
|
|
|
|
function close()
|
|
{
|
|
properties?.priority || document.removeEventListener('mousedown', close);
|
|
container.removeEventListener('mousedown', cancelPropagation);
|
|
|
|
container.remove();
|
|
stop();
|
|
properties?.blur && properties.blur();
|
|
}
|
|
|
|
return { close, container, content };
|
|
}
|
|
export function contextmenu(x: number, y: number, content: NodeChildren, properties?: FollowerProperties)
|
|
{
|
|
return followermenu({
|
|
getBoundingClientRect() {
|
|
return {
|
|
x: x,
|
|
y: y,
|
|
top: y,
|
|
left: x,
|
|
bottom: y,
|
|
right: x,
|
|
width: 0,
|
|
height: 0,
|
|
};
|
|
},
|
|
}, content, properties);
|
|
}
|
|
export function tooltip(container: HTMLElement, txt: string, placement: FloatingUI.Placement, delay?: number): HTMLElement
|
|
{
|
|
return popper(container, {
|
|
arrow: true,
|
|
offset: 8,
|
|
delay: delay,
|
|
content: [ text(txt) ],
|
|
placement: placement,
|
|
class: "fixed hidden TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50"
|
|
});
|
|
}
|
|
|
|
export function fullblocker(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 _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, ...content]);
|
|
|
|
teleport.appendChild(_modal);
|
|
|
|
return {
|
|
close: () => _modal.remove(),
|
|
}
|
|
}
|
|
export function modal(content: NodeChildren, properties?: ModalProperties)
|
|
{
|
|
return fullblocker([ 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) ], properties);
|
|
}
|
|
|
|
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,
|
|
});
|
|
})
|
|
} |