/* ObsiGate — generic themed context menu for the XLSX grid (#155). * * Builds a small menu anchored at a screen position, closes it on outside * click / Escape / window blur. Labels and callbacks come from the caller, so * no i18n lives here. */ let _menu = null; let _openedAt = 0; let _dismissBound = false; export function closeContextMenu() { if (_menu) { _menu.remove(); _menu = null; } } export function isContextMenuOpen() { return _menu !== null; } function bindDismiss() { if (_dismissBound) return; _dismissBound = true; document.addEventListener("click", (e) => { if (!_menu) return; // Ignore the very gesture that opened the menu (touch long-press can emit // a click right after). if (Date.now() - _openedAt < 250) return; if (e.target && e.target.closest && e.target.closest(".xlsx-context-menu")) return; closeContextMenu(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeContextMenu(); }); if (typeof window !== "undefined") window.addEventListener("blur", closeContextMenu); } /** * @param {number} x - viewport X of the pointer * @param {number} y - viewport Y of the pointer * @param {Array<{label?: string, separator?: boolean, danger?: boolean, disabled?: boolean, onClick?: Function}>} items * @returns {HTMLElement} the menu element */ export function openContextMenu(x, y, items) { bindDismiss(); closeContextMenu(); const menu = document.createElement("div"); menu.className = "xlsx-context-menu"; menu.setAttribute("role", "menu"); for (const item of items) { if (item.separator) { const sep = document.createElement("div"); sep.className = "xlsx-context-sep"; menu.appendChild(sep); continue; } const btn = document.createElement("button"); btn.type = "button"; btn.className = `btn-action xlsx-context-item${item.danger ? " xlsx-context-danger" : ""}`; btn.setAttribute("role", "menuitem"); btn.textContent = item.label; btn.disabled = Boolean(item.disabled); btn.addEventListener("click", () => { closeContextMenu(); if (!item.disabled && item.onClick) item.onClick(); }); menu.appendChild(btn); } document.body.appendChild(menu); // Keep the menu inside the viewport (getBoundingClientRect is 0 in JSDOM, // which simply leaves the requested position). const rect = menu.getBoundingClientRect(); const vw = (typeof window !== "undefined" && window.innerWidth) || 1024; const vh = (typeof window !== "undefined" && window.innerHeight) || 768; const left = Math.max(4, Math.min(x, vw - rect.width - 4)); const top = Math.max(4, Math.min(y, vh - rect.height - 4)); menu.style.left = `${left}px`; menu.style.top = `${top}px`; _menu = menu; _openedAt = Date.now(); return menu; }