// retoor import { Component } from "./Component.js"; export class AppContextMenu extends Component { connectedCallback() { if (this._built) { return; } this._built = true; this.build(); this.bindGlobal(); } build() { const menu = document.createElement("div"); menu.className = "context-menu"; menu.setAttribute("role", "menu"); this.appendChild(menu); this.menu = menu; menu.addEventListener("click", (e) => e.stopPropagation()); menu.addEventListener("keydown", (e) => this.onKeydown(e)); } onKeydown(e) { const items = [...this.menu.querySelectorAll(".context-menu-item:not([disabled])")]; if (!items.length) return; const index = items.indexOf(document.activeElement); let next = null; if (e.key === "ArrowDown") next = index + 1; else if (e.key === "ArrowUp") next = index - 1; else if (e.key === "Home") next = 0; else if (e.key === "End") next = items.length - 1; if (next === null) return; e.preventDefault(); items[(next + items.length) % items.length].focus(); } bindGlobal() { document.addEventListener("click", (e) => { if (!this.menu.contains(e.target)) this.close(); }); document.addEventListener("scroll", () => this.close(), true); this._lastWidth = window.innerWidth; window.addEventListener("resize", () => { if (window.innerWidth !== this._lastWidth) { this._lastWidth = window.innerWidth; this.close(); } }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") this.close(); }); } render(items) { this.menu.textContent = ""; const list = document.createElement("ul"); list.className = "context-menu-list"; list.setAttribute("role", "none"); items.forEach((item) => { const li = document.createElement("li"); if (item.separator) { li.className = "context-menu-sep"; li.setAttribute("role", "separator"); list.appendChild(li); return; } li.setAttribute("role", "none"); const btn = document.createElement("button"); btn.type = "button"; btn.className = "context-menu-item" + (item.danger ? " danger" : ""); btn.setAttribute("role", "menuitem"); btn.tabIndex = -1; btn.disabled = !!item.disabled; if (item.disabled) btn.setAttribute("aria-disabled", "true"); const icon = item.icon ? `` : ""; btn.innerHTML = icon + ''; btn.querySelector(".context-menu-label").textContent = item.label; if (!item.disabled && item.onSelect) { btn.addEventListener("click", () => { this.close(); item.onSelect(); }); } li.appendChild(btn); list.appendChild(li); }); this.menu.appendChild(list); } open(x, y, items) { if (!items || !items.length) return; this.render(items); this.menu.classList.add("visible"); const rect = this.menu.getBoundingClientRect(); const vv = window.visualViewport; const safeW = vv ? vv.width : window.innerWidth; const safeH = vv ? vv.height : window.innerHeight; const offX = vv ? vv.offsetLeft : 0; const offY = vv ? vv.offsetTop : 0; let left = x; let top = y; if (left + rect.width > offX + safeW) left = offX + safeW - rect.width - 8; if (top + rect.height > offY + safeH) top = offY + safeH - rect.height - 8; this.menu.style.left = Math.max(offX + 8, left) + "px"; this.menu.style.top = Math.max(offY + 8, top) + "px"; const first = this.menu.querySelector(".context-menu-item:not([disabled])"); if (first) setTimeout(() => first.focus(), 0); } close() { this.menu.classList.remove("visible"); } suppressNextClick() { const handler = (e) => { e.preventDefault(); e.stopPropagation(); }; document.addEventListener("click", handler, { capture: true, once: true }); } attach(host, builder) { host.addEventListener("contextmenu", (e) => { const items = builder(e); if (!items || !items.length) return; e.preventDefault(); this.open(e.clientX, e.clientY, items); }); let timer = null; let startX = 0; let startY = 0; let fired = false; const clear = () => { if (timer) { clearTimeout(timer); timer = null; } }; host.addEventListener("touchstart", (e) => { if (e.touches.length !== 1) { clear(); return; } const touch = e.touches[0]; startX = touch.clientX; startY = touch.clientY; fired = false; timer = setTimeout(() => { fired = true; this.open(startX, startY, builder(e) || []); }, 500); }, { passive: true }); host.addEventListener("touchmove", (e) => { const touch = e.touches[0]; if (touch && (Math.abs(touch.clientX - startX) > 10 || Math.abs(touch.clientY - startY) > 10)) { clear(); } }, { passive: true }); host.addEventListener("touchend", () => { clear(); if (fired) { fired = false; this.suppressNextClick(); } }); } } customElements.define("dp-context-menu", AppContextMenu);