From dee6dd8cd8a2ea7a0ef50a8bf0f5f6c5c7a22cf8 Mon Sep 17 00:00:00 2001 From: DonVoo Date: Mon, 13 Jul 2026 09:39:29 +0200 Subject: [PATCH] Add modal mail preview --- backend/00-router.go | 42 ++++++++++++---------- backend/08-viewer.go | 2 +- frontend-js/dist/app.js | 62 ++++++++++++++++++++++++++++++++ frontend-js/dist/style.css | 73 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 20 deletions(-) diff --git a/backend/00-router.go b/backend/00-router.go index 788ba39..d7a0ba8 100644 --- a/backend/00-router.go +++ b/backend/00-router.go @@ -73,8 +73,8 @@ func renderStartPage(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, ` Mail-Graveyard - - + +
@@ -121,9 +121,9 @@ func renderShell(w http.ResponseWriter, r *http.Request, active, readingPane str fmt.Fprintf(w, ` Mail-Graveyard - + - +
@@ -697,9 +697,9 @@ func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Accou fmt.Fprintf(w, ` Konten-Verwaltung - Mail-Graveyard - + - +
@@ -743,9 +743,9 @@ func renderArchivesPage(w http.ResponseWriter, r *http.Request, archives []strin fmt.Fprintf(w, ` Archiv-mbox Verwaltung - Mail-Graveyard - + - +
@@ -791,9 +791,9 @@ func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, ed fmt.Fprintf(w, ` Benutzerverwaltung - Mail-Graveyard - + - +
@@ -865,9 +865,9 @@ func renderTransferPage(w http.ResponseWriter, r *http.Request, mode transferMod fmt.Fprintf(w, ` %s - Mail-Graveyard - + - +
@@ -1124,7 +1124,7 @@ func renderInitialTransferPreview(leftKind, leftValue, rightKind, rightValue str return renderTransferPreview(rightKind, rightValue, folder, 0) } } - return `
Vorschau
Nachricht links oder rechts waehlen.
` + return renderEmptyPreviewPane("Nachricht links oder rechts waehlen.") } func transferEmptyReason(kind string, err error) string { @@ -1255,7 +1255,7 @@ func renderTransferRawMessages(b *strings.Builder, side, kind, value, folder str func renderTransferPreview(kind, value, folder string, index int) string { raw, label, err := transferRawMessage(kind, value, folder, index) if err != nil { - return fmt.Sprintf(`
Vorschau
%s
`, html.EscapeString(err.Error())) + return renderEmptyPreviewPane(err.Error()) } msg, err := mail.ReadMessage(bytes.NewReader(raw)) var b strings.Builder @@ -1317,9 +1317,9 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [ fmt.Fprintf(w, ` Quell-Postfach - Mail-Graveyard - + - +
@@ -1361,9 +1361,9 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [ fmt.Fprintf(w, ` Ziel-Postfach - Mail-Graveyard - + - +
@@ -1792,11 +1792,15 @@ func renderMailboxReadPane(w io.Writer, title, account, folder string, index int if subject == "" { subject = "(ohne Betreff)" } - fmt.Fprint(w, `
Vorschau
`) + fmt.Fprint(w, `
Vorschau
`) fmt.Fprintf(w, `

%s

Von: %s
Datum: %s
Postfach: %s / %s / #%d
%s
`, html.EscapeString(subject), html.EscapeString(from), html.EscapeString(date), html.EscapeString(account), html.EscapeString(folder), index+1, html.EscapeString(body)) } +func renderEmptyPreviewPane(message string) string { + return fmt.Sprintf(`
Vorschau
%s
`, html.EscapeString(message)) +} + func accountsByTarget(accounts []Account) []Account { seen := map[string]bool{} out := make([]Account, 0, len(accounts)) diff --git a/backend/08-viewer.go b/backend/08-viewer.go index 886b893..7a97217 100644 --- a/backend/08-viewer.go +++ b/backend/08-viewer.go @@ -394,7 +394,7 @@ func renderReadContent(w io.Writer, subject, from, date, body string) { if subject == "" { subject = "(ohne Betreff)" } - fmt.Fprintf(w, `

%s

Von: %s
Datum: %s
%s
`, + fmt.Fprintf(w, `

%s

Von: %s
Datum: %s
%s
`, html.EscapeString(subject), html.EscapeString(from), html.EscapeString(date), html.EscapeString(body)) } diff --git a/frontend-js/dist/app.js b/frontend-js/dist/app.js index 099d32a..8fbac24 100644 --- a/frontend-js/dist/app.js +++ b/frontend-js/dist/app.js @@ -235,3 +235,65 @@ document.querySelector("#read").innerHTML = html; }); })(); + +(function () { + function ensureModal() { + let modal = document.getElementById("mailPreviewModal"); + if (modal) return modal; + modal = document.createElement("div"); + modal.className = "mail-modal"; + modal.id = "mailPreviewModal"; + modal.setAttribute("role", "dialog"); + modal.setAttribute("aria-modal", "true"); + modal.setAttribute("aria-label", "Mail-Grossansicht"); + modal.innerHTML = ` +
+
+

Mail

+ +
+
+
`; + document.body.appendChild(modal); + return modal; + } + + function closeModal() { + const modal = document.getElementById("mailPreviewModal"); + if (modal) modal.classList.remove("open"); + } + + function openFromPreview(button) { + const pane = button.closest(".pane-read") || button.closest("#read") || button.closest("#transfer-preview"); + if (!pane) return; + const readBody = pane.querySelector(".read-body"); + if (!readBody) return; + const modal = ensureModal(); + const title = readBody.querySelector(".read-subject")?.textContent?.trim() || "Mail"; + const body = document.getElementById("mailPreviewModalBody"); + document.getElementById("mailPreviewModalTitle").textContent = title; + body.innerHTML = ""; + body.appendChild(readBody.cloneNode(true)); + body.querySelectorAll("[data-open-mail-modal]").forEach((el) => el.remove()); + modal.classList.add("open"); + } + + document.addEventListener("click", (event) => { + const open = event.target.closest("[data-open-mail-modal]"); + if (open) { + event.preventDefault(); + openFromPreview(open); + return; + } + if (event.target.closest("[data-mail-modal-close]")) { + closeModal(); + return; + } + const modal = event.target.closest(".mail-modal"); + if (modal && event.target === modal) closeModal(); + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") closeModal(); + }); +})(); diff --git a/frontend-js/dist/style.css b/frontend-js/dist/style.css index e250872..2ff9bdc 100644 --- a/frontend-js/dist/style.css +++ b/frontend-js/dist/style.css @@ -379,6 +379,7 @@ a.tree-node { display: block; color: inherit; text-decoration: none; } gap: 10px; } .read-head { border-bottom: 1px solid var(--ol-border); padding-bottom: 10px; margin-bottom: 12px; } +.read-head.with-popout { position: relative; padding-right: 34px; } .read-subject { font-size: 17px; font-weight: 600; margin: 0 0 6px; } .read-meta { color: var(--ol-muted); font-size: 12px; } .read-pre { @@ -388,6 +389,78 @@ a.tree-node { display: block; color: inherit; text-decoration: none; } line-height: 1.45; margin: 0; } +.preview-popout { + justify-self: end; + width: 26px; + height: 24px; + border: 1px solid var(--ol-border); + background: #fff; + color: var(--ol-blue-dark); + cursor: pointer; + font-size: 15px; + line-height: 1; +} +.preview-popout:hover { + background: #eef6fd; + border-color: var(--ol-blue); +} +.preview-popout.inline { + position: absolute; + top: 0; + right: 0; +} +.mail-modal { + position: fixed; + inset: 0; + z-index: 60; + display: none; + padding: 28px; + background: rgba(0, 0, 0, .38); +} +.mail-modal.open { display: flex; } +.mail-modalbox { + width: min(1180px, 100%); + height: min(860px, 100%); + margin: auto; + display: flex; + flex-direction: column; + overflow: hidden; + background: #fff; + border: 1px solid var(--ol-border); + box-shadow: 0 20px 70px rgba(0, 0, 0, .32); +} +.mail-modalhead { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + border-bottom: 1px solid var(--ol-border); + background: #eef3f8; +} +.mail-modalhead h2 { + flex: 1; + min-width: 0; + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 17px; +} +.mail-modalclose { + width: 30px; + height: 28px; + border: 1px solid var(--ol-border); + background: #fff; + cursor: pointer; + font-size: 20px; + line-height: 1; +} +.mail-modalbody { + flex: 1; + min-height: 0; + overflow: auto; + padding: 20px 24px; +} .ef-empty { color: var(--ol-muted); padding: 24px; } .audit-note { padding: 10px 16px; border-bottom: 1px solid var(--ol-border); font-size: 12px; } .drop-zone {