245 lines
7.7 KiB
JavaScript
245 lines
7.7 KiB
JavaScript
/*
|
|
const ctx = new ContextMenu();
|
|
|
|
ctx.setItems([
|
|
{ label: "Öffnen", disable: true, onClick: () => alert("Öffnen!") },
|
|
{ label: "Öffnen", onClick: () => alert("Öffnen!") },
|
|
{ label: "Bearbeiten", onClick: () => alert("Bearbeiten…") },
|
|
{ type: "divider" },
|
|
{
|
|
label: "Mehr Optionen",
|
|
color: 'rgb()|color-name|color-value',
|
|
children: [
|
|
{ label: "Duplizieren", onClick: () => alert("Dupliziert!") },
|
|
{ label: "Umbenennen", onClick: () => alert("Rename…") },
|
|
{
|
|
label: "Exportieren",
|
|
children: [
|
|
{ label: "PDF", onClick: () => alert("PDF export") },
|
|
{ label: "CSV", onClick: () => alert("CSV export") },
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{ label: "Löschen", onClick: () => alert("Gelöscht!") },
|
|
]);
|
|
|
|
ctx.show(element, or location, { position: "right" });
|
|
*/
|
|
|
|
|
|
class ContextMenu {
|
|
constructor() {
|
|
this.menu = document.createElement("div");
|
|
this.menu.className = "ctx-menu hidden";
|
|
document.body.appendChild(this.menu);
|
|
|
|
// close on click outside → sofort
|
|
document.addEventListener("click", (e) => {
|
|
if (!this.menu.contains(e.target)) {
|
|
this.closeAllSubmenus();
|
|
this.hide();
|
|
}
|
|
});
|
|
|
|
window.addEventListener("resize", () => this.hide());
|
|
window.addEventListener("scroll", () => this.hide());
|
|
}
|
|
|
|
setItems(items) {
|
|
this.menu.innerHTML = "";
|
|
this.menu.appendChild(this.buildMenu(items));
|
|
}
|
|
|
|
buildMenu(items) {
|
|
const ul = document.createElement("ul");
|
|
ul.className = "ctx-list";
|
|
|
|
items.forEach(item => {
|
|
if (item.type === "divider") {
|
|
const divider = document.createElement("li");
|
|
divider.className = "ctx-divider";
|
|
ul.appendChild(divider);
|
|
return;
|
|
}
|
|
|
|
const li = document.createElement("li");
|
|
li.className = "ctx-item";
|
|
|
|
if (item.color) li.style.boxShadow = `inset 5px 0px 0px 0px ${item.color}`;
|
|
if (item.disabled) li.classList.add("ctx-disabled");
|
|
|
|
li.innerHTML = `
|
|
<span class="ctx-label">${item.label}</span>
|
|
${item.children ? "<span class='ctx-arrow'>▶</span>" : ""}
|
|
`;
|
|
|
|
if (item.onClick && !item.children && !item.disabled) {
|
|
li.addEventListener("click", e => {
|
|
e.stopPropagation();
|
|
item.onClick(e);
|
|
this.hide(); // Hauptmenü sofort schließen
|
|
});
|
|
}
|
|
|
|
if (item.children) {
|
|
const submenu = this.buildMenu(item.children);
|
|
submenu.classList.add("ctx-submenu");
|
|
li.appendChild(submenu);
|
|
|
|
let hideTimeout;
|
|
|
|
const openSubmenu = () => {
|
|
clearTimeout(hideTimeout);
|
|
submenu.classList.add("open");
|
|
|
|
submenu.style.left = "";
|
|
submenu.style.top = "";
|
|
|
|
const liRect = li.getBoundingClientRect();
|
|
const submenuRect = submenu.getBoundingClientRect();
|
|
|
|
let left = li.offsetWidth;
|
|
if (liRect.right + submenuRect.width > window.innerWidth) left = -submenuRect.width;
|
|
submenu.style.left = left + "px";
|
|
|
|
let top = 0;
|
|
if (liRect.top + submenuRect.height > window.innerHeight) {
|
|
top = window.innerHeight - (liRect.top + submenuRect.height) - 4;
|
|
}
|
|
submenu.style.top = top + "px";
|
|
};
|
|
|
|
const closeSubmenu = (delay = 500) => {
|
|
clearTimeout(hideTimeout);
|
|
hideTimeout = setTimeout(() => {
|
|
submenu.classList.remove("open");
|
|
submenu.style.left = "";
|
|
submenu.style.top = "";
|
|
}, delay);
|
|
};
|
|
|
|
li.addEventListener("mouseenter", openSubmenu);
|
|
li.addEventListener("mouseleave", () => closeSubmenu(500));
|
|
submenu.addEventListener("mouseenter", () => clearTimeout(hideTimeout));
|
|
submenu.addEventListener("mouseleave", () => closeSubmenu(500));
|
|
}
|
|
|
|
ul.appendChild(li);
|
|
});
|
|
|
|
return ul;
|
|
}
|
|
|
|
closeAllSubmenus() {
|
|
this.menu.querySelectorAll(".ctx-submenu.open").forEach(sm => {
|
|
sm.classList.remove("open");
|
|
sm.style.left = "";
|
|
sm.style.top = "";
|
|
});
|
|
}
|
|
|
|
show(target, options = {}) {
|
|
this.closeAllSubmenus();
|
|
|
|
let x, y;
|
|
|
|
const position = options.position || "right"; // default
|
|
const offset = options.offset || 4;
|
|
|
|
// 🖱️ Mausposition
|
|
if (typeof target === "number") {
|
|
x = target;
|
|
y = options.y;
|
|
}
|
|
|
|
// 📦 Element als Anchor
|
|
else if (target instanceof HTMLElement) {
|
|
const rect = target.getBoundingClientRect();
|
|
|
|
switch (position) {
|
|
case "right":
|
|
x = rect.right + offset;
|
|
y = rect.top;
|
|
break;
|
|
|
|
case "left":
|
|
x = rect.left - this.menu.offsetWidth - offset;
|
|
y = rect.top;
|
|
break;
|
|
|
|
case "top":
|
|
x = rect.left;
|
|
y = rect.top - this.menu.offsetHeight - offset;
|
|
break;
|
|
|
|
case "bottom":
|
|
default:
|
|
x = rect.left;
|
|
y = rect.bottom + offset;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ❌ invalid fallback
|
|
else {
|
|
return;
|
|
}
|
|
|
|
// 👉 erstmal anzeigen (wichtig für width/height!)
|
|
this.menu.style.left = "0px";
|
|
this.menu.style.top = "0px";
|
|
this.menu.classList.add("show");
|
|
this.menu.classList.remove("hidden");
|
|
|
|
const rect = this.menu.getBoundingClientRect();
|
|
|
|
// 🧠 Reposition nach echten Maßen
|
|
if (target instanceof HTMLElement) {
|
|
const anchor = target.getBoundingClientRect();
|
|
|
|
switch (position) {
|
|
case "right":
|
|
x = anchor.right + offset;
|
|
y = anchor.top;
|
|
break;
|
|
|
|
case "left":
|
|
x = anchor.left - rect.width - offset;
|
|
y = anchor.top;
|
|
break;
|
|
|
|
case "top":
|
|
x = anchor.left;
|
|
y = anchor.top - rect.height - offset;
|
|
break;
|
|
|
|
case "bottom":
|
|
x = anchor.left;
|
|
y = anchor.bottom + offset;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 🧱 Screen Bounds
|
|
if (x + rect.width > window.innerWidth) {
|
|
x = window.innerWidth - rect.width - 4;
|
|
}
|
|
|
|
if (y + rect.height > window.innerHeight) {
|
|
y = window.innerHeight - rect.height - 4;
|
|
}
|
|
|
|
if (x < 0) x = 4;
|
|
if (y < 0) y = 4;
|
|
|
|
this.menu.style.left = x + "px";
|
|
this.menu.style.top = y + "px";
|
|
}
|
|
|
|
hide() {
|
|
this.menu.classList.remove("show");
|
|
setTimeout(() => this.menu.classList.add("hidden"), 200);
|
|
}
|
|
}
|