Mail-Graveyard/frontend-js/dist/app.js
2026-07-13 12:44:07 +02:00

483 lines
18 KiB
JavaScript

(function () {
function ensureActionBar() {
const ribbon = document.querySelector(".ribbon");
if (!ribbon || document.querySelector(".mail-actionbar")) return;
const bar = document.createElement("div");
bar.className = "mail-actionbar";
bar.innerHTML = `
<button class="action-btn primary" type="button" data-action-new><span aria-hidden="true">&#x2709;</span> Neu</button>
<button class="action-btn" type="button" data-action-refresh><span aria-hidden="true">&#x21BB;</span> Aktualisieren</button>
<label class="action-search" aria-label="Suchen">
<span aria-hidden="true">&#x2315;</span>
<input type="search" placeholder="Suchen" data-action-search>
</label>
<span class="action-separator"></span>
<button class="action-btn" type="button" data-action-reply><span aria-hidden="true">&#x21A9;</span> Antworten</button>
<button class="action-btn" type="button" data-action-forward><span aria-hidden="true">&#x21AA;</span> Weiterleiten</button>
<button class="action-btn muted" type="button" disabled><span aria-hidden="true">&#x232B;</span> Loeschen</button>
<span class="action-separator"></span>
<button class="action-btn muted" type="button" disabled><span aria-hidden="true">&#x21C4;</span> Ordner Sync</button>
<button class="action-btn muted" type="button" disabled><span aria-hidden="true">&#x2709;</span> Email-Sync</button>
`;
ribbon.insertAdjacentElement("afterend", bar);
}
function selectedPreview() {
const read = document.querySelector("#read .read-body, #transfer-preview .read-body, .pane-read .read-body");
if (!read || read.querySelector(".ef-empty")) return null;
const subject = read.querySelector(".read-subject")?.textContent?.trim() || "";
const meta = read.querySelector(".read-meta")?.innerText || "";
const body = read.querySelector(".read-pre")?.innerText || "";
const from = (meta.match(/Von:\s*(.+)/i) || [])[1]?.trim() || document.querySelector(".msg-row.active .msg-from")?.textContent?.trim() || "";
const date = (meta.match(/Datum:\s*(.+)/i) || [])[1]?.trim() || document.querySelector(".msg-row.active .msg-date")?.textContent?.trim() || "";
return { subject, from, date, body };
}
function prefixedSubject(prefix, subject) {
const clean = subject || "";
if (new RegExp(`^${prefix}:`, "i").test(clean)) return clean;
return `${prefix}: ${clean || "(ohne Betreff)"}`;
}
function ensureComposeModal() {
let modal = document.getElementById("mailComposeModal");
if (modal) return modal;
modal = document.createElement("div");
modal.className = "mail-compose-modal";
modal.id = "mailComposeModal";
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
modal.setAttribute("aria-label", "Mail verfassen");
modal.innerHTML = `
<form class="mail-compose-box">
<div class="mail-compose-head">
<h2>Mail verfassen</h2>
<button class="mail-compose-close" type="button" aria-label="Schliessen" data-compose-close>&times;</button>
</div>
<div class="mail-compose-fields">
<label>An<input name="to" type="text" autocomplete="email" required placeholder="empfaenger@example.com"></label>
<label>Betreff<input name="subject" type="text" placeholder="Betreff"></label>
<label>Nachricht<textarea name="body" rows="14"></textarea></label>
</div>
<div class="mail-compose-status" aria-live="polite"></div>
<div class="mail-compose-actions">
<button class="btn secondary" type="button" data-compose-close>Abbrechen</button>
<button class="btn" type="submit">Senden</button>
</div>
</form>
`;
document.body.appendChild(modal);
return modal;
}
function closeComposeModal() {
document.getElementById("mailComposeModal")?.classList.remove("open");
}
function openCompose(mail) {
const modal = ensureComposeModal();
modal.querySelector("[name='to']").value = mail.to || "";
modal.querySelector("[name='subject']").value = mail.subject || "";
modal.querySelector("[name='body']").value = mail.body || "";
modal.querySelector(".mail-compose-status").textContent = "";
modal.classList.add("open");
setTimeout(() => modal.querySelector("[name='to']").focus(), 0);
}
function quoteBody(message) {
const lines = (message.body || "").split(/\r?\n/).map((line) => `> ${line}`).join("\n");
return `\n\nAm ${message.date || "unbekanntes Datum"} schrieb ${message.from || "unbekannter Absender"}:\n${lines}`;
}
function forwardBody(message) {
return `\n\n---------- Weitergeleitete Nachricht ----------\nVon: ${message.from || ""}\nDatum: ${message.date || ""}\nBetreff: ${message.subject || ""}\n\n${message.body || ""}`;
}
function applyListSearch(term) {
const q = term.trim().toLowerCase();
document.querySelectorAll(".msg-row").forEach((row) => {
row.hidden = q !== "" && !row.textContent.toLowerCase().includes(q);
});
}
function runSearch(input) {
const existing = document.querySelector(".searchbar .search-input");
const form = existing?.closest("form");
if (existing && form && input.value.trim()) {
existing.value = input.value;
form.requestSubmit();
return;
}
applyListSearch(input.value);
}
document.addEventListener("DOMContentLoaded", ensureActionBar);
document.addEventListener("click", (event) => {
const row = event.target.closest(".msg-row");
if (!row) return;
row.closest(".pane")?.querySelectorAll(".msg-row.active").forEach((item) => item.classList.remove("active"));
row.classList.add("active");
});
document.addEventListener("click", (event) => {
if (event.target.closest("[data-action-new]")) {
openCompose({ to: "", subject: "", body: "" });
}
if (event.target.closest("[data-action-refresh]")) window.location.reload();
if (event.target.closest("[data-action-reply]")) {
const message = selectedPreview();
if (!message) {
window.alert("Bitte zuerst eine Nachricht waehlen.");
return;
}
openCompose({ to: message.from, subject: prefixedSubject("Re", message.subject), body: quoteBody(message) });
}
if (event.target.closest("[data-action-forward]")) {
const message = selectedPreview();
if (!message) {
window.alert("Bitte zuerst eine Nachricht waehlen.");
return;
}
openCompose({ to: "", subject: prefixedSubject("Fw", message.subject), body: forwardBody(message) });
}
if (event.target.closest("[data-compose-close]")) closeComposeModal();
});
document.addEventListener("click", (event) => {
if (event.target.id === "mailComposeModal") closeComposeModal();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeComposeModal();
});
document.addEventListener("submit", async (event) => {
const form = event.target.closest(".mail-compose-box");
if (!form) return;
event.preventDefault();
const status = form.querySelector(".mail-compose-status");
const submit = form.querySelector("button[type='submit']");
status.textContent = "Sende...";
submit.disabled = true;
try {
const response = await fetch("/mail/send", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" },
body: new URLSearchParams(new FormData(form)).toString()
});
const text = await response.text();
if (!response.ok) throw new Error(text || "Senden fehlgeschlagen.");
status.textContent = "Gesendet.";
setTimeout(closeComposeModal, 700);
} catch (error) {
status.textContent = error.message || "Senden fehlgeschlagen.";
} finally {
submit.disabled = false;
}
});
document.addEventListener("input", (event) => {
const input = event.target.closest("[data-action-search]");
if (input) runSearch(input);
});
})();
(function () {
const handleWidth = 6;
function storageKey(container) {
if (container.classList.contains("transfer-workbench")) return "mail-graveyard-transfer-widths-v1";
if (container.classList.contains("four-pane")) return "mail-graveyard-four-pane-widths-v1";
return "mail-graveyard-pane-widths-v2";
}
function minWidths(container) {
const count = panes(container).length;
if (container.classList.contains("transfer-workbench")) return [220, 240, 320, 240, 220];
if (count === 4) return [160, 220, 280, 280];
return [160, 220, 300];
}
function panes(container) {
return Array.from(container.querySelectorAll(".pane"));
}
function widthsFromDOM(container) {
return panes(container).map((pane) => Math.round(pane.getBoundingClientRect().width));
}
function clampWidths(widths, total) {
const mins = minWidths(this);
const next = widths.map((w, i) => Math.max(mins[i] || mins[mins.length - 1], Math.round(w || mins[i] || mins[mins.length - 1])));
const minTotal = mins.slice(0, next.length).reduce((a, b) => a + b, 0);
const max = Math.max(total - handleWidth * (next.length - 1), minTotal);
const sum = next.reduce((a, b) => a + b, 0);
if (sum > max) {
let extra = sum - max;
for (let i = next.length - 1; i >= 0 && extra > 0; i--) {
const canTake = Math.max(0, next[i] - (mins[i] || mins[mins.length - 1]));
const take = Math.min(canTake, extra);
next[i] -= take;
extra -= take;
}
}
return next;
}
function applyWidths(container, widths) {
const total = container.getBoundingClientRect().width;
const w = clampWidths.call(container, widths, total);
const columns = [];
w.forEach((width, index) => {
columns.push(index === w.length - 1 ? `minmax(${width}px, 1fr)` : `${width}px`);
if (index < w.length - 1) columns.push(`${handleWidth}px`);
});
container.style.gridTemplateColumns = columns.join(" ");
localStorage.setItem(storageKey(container), JSON.stringify(w));
}
function restore(container) {
try {
const saved = JSON.parse(localStorage.getItem(storageKey(container)) || "null");
if (Array.isArray(saved) && saved.length === panes(container).length) {
applyWidths(container, saved);
}
} catch (_) {
localStorage.removeItem(storageKey(container));
}
}
function bind(container) {
if (!container || container.dataset.resizeBound === "1") return;
container.dataset.resizeBound = "1";
restore(container);
container.querySelectorAll(".pane-resizer").forEach((handle) => {
handle.addEventListener("pointerdown", (event) => {
if (window.matchMedia("(max-width: 820px)").matches) return;
event.preventDefault();
handle.setPointerCapture(event.pointerId);
handle.classList.add("dragging");
document.body.classList.add("resizing");
const index = Number(handle.dataset.resizeIndex || 0);
const startX = event.clientX;
const start = widthsFromDOM(container);
function move(moveEvent) {
const delta = moveEvent.clientX - startX;
const next = start.slice();
const mins = minWidths(container);
const left = Math.max(mins[index], start[index] + delta);
const used = left - start[index];
const right = Math.max(mins[index + 1], start[index + 1] - used);
const actual = start[index + 1] - right;
next[index] = start[index] + actual;
next[index + 1] = right;
applyWidths(container, next);
}
function up(upEvent) {
handle.releasePointerCapture(upEvent.pointerId);
handle.classList.remove("dragging");
document.body.classList.remove("resizing");
window.removeEventListener("pointermove", move);
window.removeEventListener("pointerup", up);
}
window.addEventListener("pointermove", move);
window.addEventListener("pointerup", up);
});
});
}
function bindAll() {
document.querySelectorAll(".transfer-workbench, .four-pane, .three-pane").forEach(bind);
}
window.addEventListener("DOMContentLoaded", bindAll);
document.addEventListener("htmx:afterSwap", bindAll);
})();
(function () {
function closeAll() {
document.querySelectorAll(".ribbon-menu.open").forEach((menu) => {
menu.classList.remove("open");
const button = menu.querySelector("[data-ribbon-menu-toggle]");
if (button) button.setAttribute("aria-expanded", "false");
});
}
document.addEventListener("click", (event) => {
const button = event.target.closest("[data-ribbon-menu-toggle]");
if (button) {
const menu = button.closest(".ribbon-menu");
const willOpen = !menu.classList.contains("open");
closeAll();
menu.classList.toggle("open", willOpen);
button.setAttribute("aria-expanded", willOpen ? "true" : "false");
return;
}
if (!event.target.closest(".ribbon-menu")) closeAll();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeAll();
});
})();
(function () {
function setOpen(open) {
document.body.classList.toggle("backstage-open", open);
document.querySelectorAll("[data-backstage-toggle]").forEach((button) => {
button.setAttribute("aria-expanded", open ? "true" : "false");
});
}
document.addEventListener("click", (event) => {
const toggle = event.target.closest("[data-backstage-toggle]");
if (toggle) {
setOpen(!document.body.classList.contains("backstage-open"));
return;
}
if (event.target.closest("[data-backstage-close]")) {
setOpen(false);
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") setOpen(false);
});
})();
(function () {
function rowPayload(row) {
return {
source: row.dataset.source || "",
folder: row.dataset.folder || "",
index: row.dataset.index || ""
};
}
document.addEventListener("dragstart", (event) => {
const row = event.target.closest(".msg-row[draggable='true']");
if (!row || !event.dataTransfer) return;
event.dataTransfer.effectAllowed = "copy";
event.dataTransfer.setData("application/json", JSON.stringify(rowPayload(row)));
event.dataTransfer.setData("text/plain", `${row.dataset.source || ""}/${row.dataset.folder || ""}#${row.dataset.index || ""}`);
row.classList.add("dragging");
});
document.addEventListener("dragend", (event) => {
const row = event.target.closest(".msg-row");
if (row) row.classList.remove("dragging");
});
document.addEventListener("dragover", (event) => {
const zone = event.target.closest("[data-drop-target='manual-copy']");
if (!zone) return;
event.preventDefault();
zone.classList.add("drag-over");
if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
});
document.addEventListener("dragleave", (event) => {
const zone = event.target.closest("[data-drop-target='manual-copy']");
if (zone) zone.classList.remove("drag-over");
});
document.addEventListener("drop", async (event) => {
const zone = event.target.closest("[data-drop-target='manual-copy']");
if (!zone) return;
event.preventDefault();
zone.classList.remove("drag-over");
let payload = {};
try {
payload = JSON.parse(event.dataTransfer.getData("application/json") || "{}");
} catch (_) {
payload = {};
}
if (!payload.source) {
payload = {
source: zone.dataset.source || "",
folder: zone.dataset.folder || "",
index: zone.dataset.index || ""
};
}
const select = document.querySelector("#target-account-select");
const form = new URLSearchParams();
form.set("source", payload.source);
form.set("folder", payload.folder);
form.set("index", payload.index);
form.set("target_account", select ? select.value : "");
zone.textContent = "Kopiere...";
const response = await fetch("/view/manual-copy", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" },
body: form.toString()
});
const html = await response.text();
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 = `
<div class="mail-modalbox">
<div class="mail-modalhead">
<h2 id="mailPreviewModalTitle">Mail</h2>
<button class="mail-modalclose" type="button" aria-label="Schliessen" data-mail-modal-close>&times;</button>
</div>
<div class="mail-modalbody" id="mailPreviewModalBody"></div>
</div>`;
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();
});
})();