import { generateId, wait } from "./shared_utils.js"; import { createElement as $el, getClosestOrSelf, setAttributes} from "./utils_dom.js"; /** * A menu. Mimics the comfy menu. */ class Menu { private element: HTMLMenuElement = $el('menu.rgthree-menu'); private callbacks: Map Promise> = new Map(); private handleWindowPointerDownBound = this.handleWindowPointerDown.bind(this); constructor(options: MenuOption[]) { this.setOptions(options); this.element.addEventListener('pointerup', async (e) => { const target = getClosestOrSelf(e.target as HTMLElement, "[data-callback],menu"); if (e.which !== 1) { return; } const callback = target?.dataset?.['callback']; if (callback) { const halt = await this.callbacks.get(callback)?.(e); if (halt !== false) { this.close(); } } e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }); } setOptions(options: MenuOption[]) { for (const option of options) { if (option.type === 'title') { this.element.appendChild($el(`li`, { html: option.label })); } else { const id = generateId(8); this.callbacks.set(id, async (e: PointerEvent) => { return option?.callback?.(e); }); this.element.appendChild($el(`li[role="button"][data-callback="${id}"]`, { html: option.label })); } } } toElement() { return this.element; } async open(e: PointerEvent) { const parent = (e.target as HTMLElement).closest('div,dialog,body') as HTMLElement parent.appendChild(this.element); setAttributes(this.element, { style: { left: `${e.clientX + 16}px`, top: `${e.clientY - 16}px`, } }); this.element.setAttribute('state', 'measuring-open'); await wait(16); const rect = this.element.getBoundingClientRect(); if (rect.right > window.innerWidth) { this.element.style.left = `${e.clientX - rect.width - 16}px`; await wait(16); } this.element.setAttribute('state', 'open'); setTimeout(() => { window.addEventListener('pointerdown', this.handleWindowPointerDownBound); }); } handleWindowPointerDown(e:PointerEvent) { if (!this.element.contains(e.target as HTMLElement)) { this.close(); } } async close() { window.removeEventListener('pointerdown', this.handleWindowPointerDownBound); this.element.setAttribute('state', 'measuring-closed'); await wait(16); this.element.setAttribute('state', 'closed'); this.element.remove(); } isOpen() { return (this.element.getAttribute('state') || '').includes('open'); } } type MenuOption = { label: string; type?: 'title'|'item'|'separator'; callback?: (e: PointerEvent) => void; } type MenuButtonOptions = { icon: string; options: MenuOption[]; } export class MenuButton { private options: MenuButtonOptions; private menu: Menu; private element: HTMLButtonElement = $el('button.rgthree-button[data-action="open-menu"]') constructor(options: MenuButtonOptions) { this.options = options; this.element.innerHTML = options.icon; this.menu = new Menu(options.options); this.element.addEventListener('pointerdown', (e) => { if (!this.menu.isOpen()) { this.menu.open(e); } }); } toElement() { return this.element; } }