Files
TK-Wiki-Newsletter/app/static/js/newsletter.js
smueller 3f3223f17c Implement newsletter preview copy functionality and UI enhancements
- Add a new preview banner to indicate the live newsletter display.
- Introduce `copyFromPreviewFrame` function to enable copying the newsletter preview directly.
- Update copy status messages for clarity and improved user guidance.
- Revise button labels in the dashboard for better usability.
- Modify CSS for the newsletter preview frame and add styles for the new preview banner.
2026-07-03 13:48:51 +02:00

390 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function setCopyStatus(message, isError) {
const status = document.getElementById("copy-status");
if (!status) return;
status.textContent = message;
status.className = isError ? "copy-status error" : "copy-status success";
}
function getNewsletterHtml() {
const htmlSource = document.getElementById("newsletter-html-source");
const htmlDisplay = document.getElementById("newsletter-html-display");
return htmlSource?.value.trim() || htmlDisplay?.value.trim() || "";
}
function getNewsletterOutlookHtml() {
const outlookSource = document.getElementById("newsletter-outlook-source");
const outlook = outlookSource ? outlookSource.value.trim() : "";
if (outlook) return outlook;
return getNewsletterHtml();
}
function getNewsletterPlain() {
const plainSource = document.getElementById("newsletter-plain-source");
return plainSource ? plainSource.value : "";
}
function byteLength(text) {
return new TextEncoder().encode(text).length;
}
function wrapCfHtml(html) {
const fragmentStart = "<!--StartFragment-->";
const fragmentEnd = "<!--EndFragment-->";
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
const inner = bodyMatch ? bodyMatch[1].trim() : html;
const documentHtml = `<html><body>${fragmentStart}${inner}${fragmentEnd}</body></html>`;
const pad = (value) => String(value).padStart(10, "0");
const headerTemplate =
"Version:0.9\r\n" +
"StartHTML:0000000000\r\n" +
"EndHTML:0000000000\r\n" +
"StartFragment:0000000000\r\n" +
"EndFragment:0000000000\r\n";
const startHtml = byteLength(headerTemplate);
const endHtml = startHtml + byteLength(documentHtml);
const startFragment = startHtml + byteLength(documentHtml.slice(0, documentHtml.indexOf(fragmentStart) + fragmentStart.length));
const endFragment = startHtml + byteLength(documentHtml.slice(0, documentHtml.indexOf(fragmentEnd)));
return (
"Version:0.9\r\n" +
`StartHTML:${pad(startHtml)}\r\n` +
`EndHTML:${pad(endHtml)}\r\n` +
`StartFragment:${pad(startFragment)}\r\n` +
`EndFragment:${pad(endFragment)}\r\n` +
documentHtml
);
}
function selectTextarea(textarea) {
if (!textarea || !textarea.value) return false;
textarea.scrollIntoView({ behavior: "smooth", block: "center" });
textarea.focus({ preventScroll: true });
textarea.select();
textarea.setSelectionRange(0, textarea.value.length);
return true;
}
function copyFromTextarea(textarea) {
if (!textarea || !textarea.value) return false;
const wasReadonly = textarea.hasAttribute("readonly");
if (wasReadonly) textarea.removeAttribute("readonly");
selectTextarea(textarea);
let ok = false;
try {
ok = document.execCommand("copy");
} catch (err) {
console.warn("execCommand copy from textarea failed.", err);
}
if (wasReadonly) textarea.setAttribute("readonly", "");
if (!ok) {
selectTextarea(textarea);
}
return ok;
}
function copyTextWithFallback(text) {
if (!text) return false;
const pageTextareas = [
document.getElementById("newsletter-html-display"),
document.getElementById("newsletter-html-source"),
document.getElementById("newsletter-outlook-source"),
document.getElementById("newsletter-plain-source"),
];
for (const textarea of pageTextareas) {
if (textarea && textarea.value === text && copyFromTextarea(textarea)) {
return true;
}
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("aria-hidden", "true");
textarea.style.cssText = "position:fixed;top:0;left:0;width:2px;height:2px;opacity:0;border:none;padding:0;";
document.body.appendChild(textarea);
let ok = false;
try {
selectTextarea(textarea);
ok = document.execCommand("copy");
} catch (err) {
console.warn("execCommand copy from temporary textarea failed.", err);
} finally {
document.body.removeChild(textarea);
}
return ok;
}
let copyDialogKeyHandler = null;
function closeCopyDialog() {
const overlay = document.getElementById("copy-dialog-overlay");
if (overlay) overlay.remove();
if (copyDialogKeyHandler) {
document.removeEventListener("keydown", copyDialogKeyHandler);
copyDialogKeyHandler = null;
}
}
function showCopyDialog(text, title) {
closeCopyDialog();
const overlay = document.createElement("div");
overlay.id = "copy-dialog-overlay";
overlay.className = "copy-dialog-overlay";
overlay.innerHTML = `
<div class="copy-dialog" role="dialog" aria-modal="true" aria-labelledby="copy-dialog-title">
<h4 id="copy-dialog-title">${title}</h4>
<p class="copy-dialog-hint">Der Text ist markiert. Jetzt <strong>Strg+C</strong> drücken (Mac: <strong>Cmd+C</strong>).</p>
<textarea class="copy-dialog-textarea" spellcheck="false"></textarea>
<div class="copy-dialog-actions">
<button type="button" class="btn-secondary" id="copy-dialog-select-btn">Alles markieren</button>
<button type="button" class="btn-secondary" id="copy-dialog-close-btn">Schließen</button>
</div>
</div>
`;
document.body.appendChild(overlay);
const textarea = overlay.querySelector(".copy-dialog-textarea");
textarea.value = text;
selectTextarea(textarea);
let copied = false;
try {
copied = document.execCommand("copy");
} catch (err) {
console.warn("execCommand copy in dialog failed.", err);
}
if (copied) {
setCopyStatus("HTML-Quelltext kopiert.");
} else {
setCopyStatus("Bitte im Fenster Strg+C drücken.", true);
}
overlay.querySelector("#copy-dialog-close-btn").addEventListener("click", closeCopyDialog);
overlay.querySelector("#copy-dialog-select-btn").addEventListener("click", () => selectTextarea(textarea));
overlay.addEventListener("click", (event) => {
if (event.target === overlay) closeCopyDialog();
});
copyDialogKeyHandler = (event) => {
if (event.key === "Escape") closeCopyDialog();
};
document.addEventListener("keydown", copyDialogKeyHandler);
}
function copyFromPreviewFrame() {
const frame = document.getElementById("newsletter-preview-frame");
const doc = frame?.contentDocument;
const win = frame?.contentWindow;
if (!doc?.body || !win || !doc.body.innerHTML.trim()) {
return false;
}
const range = doc.createRange();
range.selectNodeContents(doc.body);
const selection = win.getSelection();
selection.removeAllRanges();
selection.addRange(range);
let ok = false;
try {
ok = doc.execCommand("copy");
} catch (err) {
console.warn("Copy from preview frame failed.", err);
} finally {
selection.removeAllRanges();
}
return ok;
}
function loadPreviewFrame() {
const frame = document.getElementById("newsletter-preview-frame");
const html = getNewsletterOutlookHtml();
if (frame && html) {
frame.srcdoc = html;
}
}
function copyViaIframe(html) {
const iframe = document.createElement("iframe");
iframe.setAttribute("aria-hidden", "true");
iframe.style.cssText = "position:fixed;width:700px;height:500px;left:-9999px;top:0;border:none;";
document.body.appendChild(iframe);
const doc = iframe.contentWindow.document;
doc.open();
doc.write(html);
doc.close();
const range = doc.createRange();
range.selectNodeContents(doc.body);
const selection = iframe.contentWindow.getSelection();
selection.removeAllRanges();
selection.addRange(range);
let ok = false;
try {
ok = doc.execCommand("copy");
} catch (err) {
console.warn("execCommand copy via iframe failed.", err);
} finally {
selection.removeAllRanges();
document.body.removeChild(iframe);
}
return ok;
}
function copyHtmlWithFallback(html) {
if (copyViaIframe(html)) return true;
const container = document.createElement("div");
container.innerHTML = html;
container.contentEditable = "true";
container.setAttribute("aria-hidden", "true");
container.style.cssText =
"position:fixed;left:0;top:0;width:700px;max-height:1px;overflow:hidden;opacity:0;pointer-events:none;background:#FFFFFF;";
document.body.appendChild(container);
const range = document.createRange();
range.selectNodeContents(container);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
let ok = false;
try {
ok = document.execCommand("copy");
} catch (err) {
console.warn("execCommand HTML copy failed.", err);
} finally {
selection.removeAllRanges();
document.body.removeChild(container);
}
return ok;
}
async function copyForOutlook() {
const html = getNewsletterOutlookHtml();
const plain = getNewsletterPlain();
if (!html) {
setCopyStatus("Bitte zuerst einen Newsletter generieren.", true);
return;
}
if (copyFromPreviewFrame()) {
setCopyStatus("Vorschau kopiert in Outlook mit Strg+V einfügen (Rechtsklick → „Format beibehalten“).");
return;
}
if (copyViaIframe(html)) {
setCopyStatus("Newsletter kopiert in Outlook mit Strg+V einfügen (Rechtsklick → „Format beibehalten“).");
return;
}
const cfHtml = wrapCfHtml(html);
if (navigator.clipboard && window.ClipboardItem && window.isSecureContext) {
try {
await navigator.clipboard.write([
new ClipboardItem({
"text/html": new Blob([cfHtml], { type: "text/html" }),
"text/plain": new Blob([plain], { type: "text/plain" }),
}),
]);
setCopyStatus("Newsletter kopiert in Outlook mit Strg+V einfügen.");
return;
} catch (err) {
console.warn("Clipboard API HTML copy failed.", err);
}
}
setCopyStatus("Kopieren fehlgeschlagen. Bitte „Im Browser öffnen“ nutzen.", true);
}
function copyHtmlSource() {
const html = getNewsletterHtml();
if (!html) {
setCopyStatus("Bitte zuerst einen Newsletter generieren.", true);
return;
}
showCopyDialog(html, "HTML-Quelltext kopieren");
}
function downloadHtml() {
const html = getNewsletterOutlookHtml();
if (!html) {
setCopyStatus("Bitte zuerst einen Newsletter generieren.", true);
return;
}
downloadFile(html, `wiki-newsletter-${new Date().toISOString().slice(0, 10)}.html`, "text/html;charset=utf-8");
setCopyStatus("Outlook-HTML heruntergeladen.");
}
function downloadHtmlSource() {
const html = getNewsletterHtml();
if (!html) {
setCopyStatus("Bitte zuerst einen Newsletter generieren.", true);
return;
}
downloadFile(html, `wiki-newsletter-source-${new Date().toISOString().slice(0, 10)}.html`, "text/html;charset=utf-8");
setCopyStatus("HTML-Quelltext heruntergeladen.");
}
function downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
function openOutlookInBrowser() {
const html = getNewsletterOutlookHtml();
if (!html) {
setCopyStatus("Bitte zuerst einen Newsletter generieren.", true);
return;
}
const popup = window.open("", "_blank");
if (!popup) {
setCopyStatus("Pop-up blockiert. Bitte Pop-ups erlauben oder „HTML herunterladen“ nutzen.", true);
return;
}
popup.document.open();
popup.document.write(html);
popup.document.close();
popup.document.title = "Wiki-Newsletter zum Kopieren";
setCopyStatus("Im neuen Tab: Alles markieren (Strg+A) → kopieren (Strg+C) → in Outlook einfügen.");
}
document.addEventListener("DOMContentLoaded", () => {
loadPreviewFrame();
const htmlDisplay = document.getElementById("newsletter-html-display");
htmlDisplay?.addEventListener("focus", () => selectTextarea(htmlDisplay));
htmlDisplay?.addEventListener("click", () => selectTextarea(htmlDisplay));
document.getElementById("copy-outlook-btn")?.addEventListener("click", copyForOutlook);
document.getElementById("open-outlook-btn")?.addEventListener("click", openOutlookInBrowser);
document.getElementById("copy-html-source-btn")?.addEventListener("click", copyHtmlSource);
document.getElementById("copy-html-source-btn-2")?.addEventListener("click", copyHtmlSource);
document.getElementById("download-html-btn")?.addEventListener("click", downloadHtml);
document.getElementById("download-html-source-btn")?.addEventListener("click", downloadHtmlSource);
});