520 lines
20 KiB
JavaScript
520 lines
20 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">✉</span> Neu</button>
|
|
<button class="action-btn" type="button" data-action-refresh><span aria-hidden="true">↻</span> Aktualisieren</button>
|
|
<label class="action-search" aria-label="Suchen">
|
|
<span aria-hidden="true">⌕</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">↩</span> Antworten</button>
|
|
<button class="action-btn" type="button" data-action-forward><span aria-hidden="true">↪</span> Weiterleiten</button>
|
|
<button class="action-btn muted" type="button" disabled><span aria-hidden="true">⌫</span> Loeschen</button>
|
|
<span class="action-separator"></span>
|
|
<div class="action-menu">
|
|
<button class="action-btn action-menu-button" type="button" aria-expanded="false" data-action-menu-toggle><span aria-hidden="true">⇄</span> Ordner Sync</button>
|
|
<div class="action-dropdown">
|
|
<button type="button" class="action-dropdown-item" data-sync-kind="folder" data-sync-direction="push">Ordner --> Ordner</button>
|
|
<button type="button" class="action-dropdown-item" data-sync-kind="folder" data-sync-direction="both">Ordner <--> Ordner</button>
|
|
<button type="button" class="action-dropdown-item" data-sync-kind="folder" data-sync-direction="pull">Ordner <-- Ordner</button>
|
|
</div>
|
|
</div>
|
|
<div class="action-menu">
|
|
<button class="action-btn action-menu-button" type="button" aria-expanded="false" data-action-menu-toggle><span aria-hidden="true">✉</span> Email-Sync</button>
|
|
<div class="action-dropdown">
|
|
<button type="button" class="action-dropdown-item" data-sync-kind="email" data-sync-direction="push">Email --> Email</button>
|
|
<button type="button" class="action-dropdown-item" data-sync-kind="email" data-sync-direction="both">Email <--> Email</button>
|
|
<button type="button" class="action-dropdown-item" data-sync-kind="email" data-sync-direction="pull">Email <-- Email</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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>×</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);
|
|
function closeActionMenus() {
|
|
document.querySelectorAll(".action-menu.open").forEach((menu) => {
|
|
menu.classList.remove("open");
|
|
menu.querySelector("[data-action-menu-toggle]")?.setAttribute("aria-expanded", "false");
|
|
});
|
|
}
|
|
|
|
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) => {
|
|
const menuButton = event.target.closest("[data-action-menu-toggle]");
|
|
if (menuButton) {
|
|
const menu = menuButton.closest(".action-menu");
|
|
const willOpen = !menu.classList.contains("open");
|
|
closeActionMenus();
|
|
menu.classList.toggle("open", willOpen);
|
|
menuButton.setAttribute("aria-expanded", willOpen ? "true" : "false");
|
|
return;
|
|
}
|
|
if (!event.target.closest(".action-menu")) closeActionMenus();
|
|
const syncItem = event.target.closest("[data-sync-kind]");
|
|
if (syncItem) {
|
|
closeActionMenus();
|
|
window.alert("Sync-Funktion ist vorbereitet, aber noch nicht aktiv.");
|
|
return;
|
|
}
|
|
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>×</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();
|
|
});
|
|
})();
|