initial files

This commit is contained in:
2026-04-22 11:55:23 +02:00
commit 92444ff38c
85 changed files with 16324 additions and 0 deletions

View 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);
}
}