initial files
This commit is contained in:
244
public/javascript/contextMenu.js
Normal file
244
public/javascript/contextMenu.js
Normal file
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user