Add mail context menu

This commit is contained in:
DonVoo 2026-07-13 15:38:18 +02:00
parent a64a6d187b
commit 4f8a441d51
3 changed files with 208 additions and 16 deletions

View file

@ -132,8 +132,8 @@ func renderStartPage(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260713-16">
<script src="/static/app.js?v=20260713-16" defer></script></head>
<link rel="stylesheet" href="/static/style.css?v=20260713-17">
<script src="/static/app.js?v=20260713-17" defer></script></head>
<body class="ol2013">
<header class="ribbon">
%s
@ -170,9 +170,9 @@ func renderShell(w http.ResponseWriter, r *http.Request, active, readingPane str
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260713-16">
<link rel="stylesheet" href="/static/style.css?v=20260713-17">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260713-16" defer></script></head>
<script src="/static/app.js?v=20260713-17" defer></script></head>
<body class="ol2013">
<header class="ribbon">
%s
@ -752,9 +752,9 @@ func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Accou
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Konten-Verwaltung - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260713-16">
<link rel="stylesheet" href="/static/style.css?v=20260713-17">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260713-16" defer></script></head>
<script src="/static/app.js?v=20260713-17" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
@ -798,9 +798,9 @@ func renderArchivesPage(w http.ResponseWriter, r *http.Request, archives []strin
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Archiv-mbox Verwaltung - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260713-16">
<link rel="stylesheet" href="/static/style.css?v=20260713-17">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260713-16" defer></script></head>
<script src="/static/app.js?v=20260713-17" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
@ -846,9 +846,9 @@ func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, ed
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Benutzerverwaltung - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260713-16">
<link rel="stylesheet" href="/static/style.css?v=20260713-17">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260713-16" defer></script></head>
<script src="/static/app.js?v=20260713-17" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
@ -920,9 +920,9 @@ func renderTransferPage(w http.ResponseWriter, r *http.Request, mode transferMod
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%s - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260713-16">
<link rel="stylesheet" href="/static/style.css?v=20260713-17">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260713-16" defer></script></head>
<script src="/static/app.js?v=20260713-17" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
@ -1381,9 +1381,9 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Quell-Postfach - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260713-16">
<link rel="stylesheet" href="/static/style.css?v=20260713-17">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260713-16" defer></script></head>
<script src="/static/app.js?v=20260713-17" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
@ -1425,9 +1425,9 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Ziel-Postfach - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260713-16">
<link rel="stylesheet" href="/static/style.css?v=20260713-17">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260713-16" defer></script></head>
<script src="/static/app.js?v=20260713-17" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>

View file

@ -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 = `
<button type="button" role="menuitem" data-context-action="open"><span aria-hidden="true">&#x26F6;</span> Oeffnen</button>
<button type="button" role="menuitem" data-context-action="reply"><span aria-hidden="true">&#x21A9;</span> Antworten</button>
<button type="button" role="menuitem" data-context-action="forward"><span aria-hidden="true">&#x21AA;</span> Weiterleiten</button>
<div class="sep"></div>
<button type="button" role="menuitem" data-context-action="copy-subject"><span aria-hidden="true">&#x2398;</span> Betreff kopieren</button>
<button type="button" role="menuitem" data-context-action="copy-from"><span aria-hidden="true">&#x2709;</span> Absender kopieren</button>
<button type="button" role="menuitem" data-context-action="copy-body"><span aria-hidden="true">&#x2630;</span> Text kopieren</button>
`;
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 () {

View file

@ -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;