// retoor <retoor@molodetz.nl>
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());
}
bindGlobal() {
document.addEventListener("click", (e) => {
if (!this.menu.contains(e.target)) this.close();
});
document.addEventListener("scroll", () => this.close(), true);
window.addEventListener("resize", () => 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";
items.forEach((item) => {
const li = document.createElement("li");
if (item.separator) {
li.className = "context-menu-sep";
list.appendChild(li);
return;
}
const btn = document.createElement("button");
btn.type = "button";
btn.className = "context-menu-item" + (item.danger ? " danger" : "");
btn.disabled = !!item.disabled;
const icon = item.icon ? `<span class="context-menu-icon">${item.icon}</span>` : "";
btn.innerHTML = icon + '<span class="context-menu-label"></span>';
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();
let left = x;
let top = y;
if (left + rect.width > window.innerWidth) left = window.innerWidth - rect.width - 8;
if (top + rect.height > window.innerHeight) top = window.innerHeight - rect.height - 8;
this.menu.style.left = Math.max(8, left) + "px";
this.menu.style.top = Math.max(8, top) + "px";
}
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);