diff --git a/backend/00-router.go b/backend/00-router.go index 09569f9..bed369c 100644 --- a/backend/00-router.go +++ b/backend/00-router.go @@ -132,8 +132,8 @@ func renderStartPage(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, ` Mail-Graveyard - - + +
%s @@ -170,9 +170,9 @@ func renderShell(w http.ResponseWriter, r *http.Request, active, readingPane str fmt.Fprintf(w, ` Mail-Graveyard - + - +
%s @@ -752,9 +752,9 @@ func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Accou fmt.Fprintf(w, ` Konten-Verwaltung - Mail-Graveyard - + - +
@@ -798,9 +798,9 @@ func renderArchivesPage(w http.ResponseWriter, r *http.Request, archives []strin fmt.Fprintf(w, ` Archiv-mbox Verwaltung - Mail-Graveyard - + - +
@@ -846,9 +846,9 @@ func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, ed fmt.Fprintf(w, ` Benutzerverwaltung - Mail-Graveyard - + - +
@@ -920,9 +920,9 @@ func renderTransferPage(w http.ResponseWriter, r *http.Request, mode transferMod fmt.Fprintf(w, ` %s - Mail-Graveyard - + - +
@@ -1381,9 +1381,9 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [ fmt.Fprintf(w, ` Quell-Postfach - Mail-Graveyard - + - +
@@ -1425,9 +1425,9 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [ fmt.Fprintf(w, ` Ziel-Postfach - Mail-Graveyard - + - +
diff --git a/frontend-js/dist/app.js b/frontend-js/dist/app.js index 9cbd261..b7f3c84 100644 --- a/frontend-js/dist/app.js +++ b/frontend-js/dist/app.js @@ -129,6 +129,98 @@ applyListSearch(input.value); } + function ensureContextMenu() { + let menu = document.getElementById("mailContextMenu"); + if (menu) return menu; + menu = document.createElement("div"); + menu.id = "mailContextMenu"; + menu.className = "mail-context-menu"; + menu.setAttribute("role", "menu"); + menu.setAttribute("aria-label", "Nachrichten-Aktionen"); + menu.innerHTML = ` + + + +
+ + + + `; + document.body.appendChild(menu); + return menu; + } + + function closeContextMenu() { + const menu = document.getElementById("mailContextMenu"); + if (menu) menu.classList.remove("open"); + } + + function openContextMenu(x, y, row) { + const menu = ensureContextMenu(); + menu.dataset.rowContext = row ? "1" : "0"; + const pad = 8; + menu.classList.add("open"); + menu.style.left = "0px"; + menu.style.top = "0px"; + const rect = menu.getBoundingClientRect(); + menu.style.left = `${Math.max(pad, Math.min(x, window.innerWidth - rect.width - pad))}px`; + menu.style.top = `${Math.max(pad, Math.min(y, window.innerHeight - rect.height - pad))}px`; + } + + function rowMessage(row) { + if (!row) return null; + return { + subject: row.querySelector(".msg-subject")?.textContent?.trim() || "", + from: row.querySelector(".msg-from")?.textContent?.trim() || "", + date: row.querySelector(".msg-date")?.textContent?.trim() || "", + body: "" + }; + } + + function activeMessageFallback() { + return rowMessage(document.querySelector(".msg-row.active")) || selectedPreview(); + } + + function waitForPreview() { + return new Promise((resolve) => { + let done = false; + const finish = () => { + if (done) return; + done = true; + document.removeEventListener("htmx:afterSwap", finish); + resolve(); + }; + document.addEventListener("htmx:afterSwap", finish); + setTimeout(finish, 350); + }); + } + + async function activateContextRow() { + const row = document.querySelector(".msg-row.context-active"); + if (!row) return null; + row.click(); + await waitForPreview(); + return row; + } + + async function copyText(text) { + const value = text || ""; + if (!value) return; + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(value); + return; + } + const area = document.createElement("textarea"); + area.value = value; + area.style.position = "fixed"; + area.style.left = "-9999px"; + document.body.appendChild(area); + area.focus(); + area.select(); + document.execCommand("copy"); + area.remove(); + } + document.addEventListener("DOMContentLoaded", ensureActionBar); function closeActionMenus() { document.querySelectorAll(".action-menu.open").forEach((menu) => { @@ -216,6 +308,63 @@ const input = event.target.closest("[data-action-search]"); if (input) runSearch(input); }); + document.addEventListener("contextmenu", (event) => { + const row = event.target.closest(".msg-row"); + const readBody = event.target.closest(".read-body"); + if (!row && !readBody) return; + event.preventDefault(); + document.querySelectorAll(".msg-row.context-active").forEach((item) => item.classList.remove("context-active")); + if (row) { + row.classList.add("context-active"); + row.closest(".pane")?.querySelectorAll(".msg-row.active").forEach((item) => item.classList.remove("active")); + row.classList.add("active"); + } + openContextMenu(event.clientX, event.clientY, row); + }); + document.addEventListener("click", async (event) => { + const menu = event.target.closest("#mailContextMenu"); + if (!menu) { + closeContextMenu(); + return; + } + const item = event.target.closest("[data-context-action]"); + if (!item) return; + event.preventDefault(); + const action = item.dataset.contextAction; + const contextRow = await activateContextRow(); + closeContextMenu(); + const rowMessageData = rowMessage(contextRow); + const previewMessage = selectedPreview(); + const message = previewMessage || rowMessageData || activeMessageFallback(); + if (!message && action !== "open") { + window.alert("Bitte zuerst eine Nachricht waehlen."); + return; + } + if (action === "open") { + const button = document.querySelector("#read [data-open-mail-modal], #transfer-preview [data-open-mail-modal], .pane-read [data-open-mail-modal]"); + if (button) button.click(); + return; + } + if (action === "reply") { + openCompose({ to: message.from, subject: prefixedSubject("Re", message.subject), body: quoteBody(message) }); + return; + } + if (action === "forward") { + openCompose({ to: "", subject: prefixedSubject("Fw", message.subject), body: forwardBody(message) }); + return; + } + if (action === "copy-subject") { + await copyText((rowMessageData || message).subject); + return; + } + if (action === "copy-from") { + await copyText((rowMessageData || message).from); + return; + } + if (action === "copy-body") { + await copyText(message.body); + } + }); })(); (function () { diff --git a/frontend-js/dist/style.css b/frontend-js/dist/style.css index ac6d5fb..9718bfc 100644 --- a/frontend-js/dist/style.css +++ b/frontend-js/dist/style.css @@ -238,6 +238,49 @@ body.ol2013 { background: transparent; border-color: transparent; } +.mail-context-menu { + position: fixed; + z-index: 80; + min-width: 220px; + display: none; + padding: 6px; + color: var(--ol-text); + background: #fff; + border: 1px solid var(--ol-border-dark); + box-shadow: 0 12px 34px rgba(20, 25, 35, .22); +} +.mail-context-menu.open { display: block; } +.mail-context-menu button { + width: 100%; + height: 30px; + display: flex; + align-items: center; + gap: 9px; + padding: 0 10px; + border: 0; + color: var(--ol-text); + background: transparent; + font: inherit; + text-align: left; + cursor: pointer; +} +.mail-context-menu button:hover { + color: #fff; + background: var(--ol-blue); +} +.mail-context-menu button span { + width: 18px; + text-align: center; +} +.mail-context-menu .sep { + height: 1px; + margin: 5px 4px; + background: var(--ol-border); +} +.msg-row.context-active { + outline: 1px solid var(--ol-blue); + outline-offset: -1px; +} .action-menu { position: relative; height: 30px;