Baseline für Vision 2026: Veranstaltungsmodul + ausstehende Änderungen
Alle bestehenden, nicht commiteten Änderungen als Ausgangsbasis für den vision-2026 Branch übernommen (Veranstaltungsmodul, Serienbrief, etc.). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
250
app/static/stiftung/js/briefvorlage_editor.js
Normal file
250
app/static/stiftung/js/briefvorlage_editor.js
Normal file
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Briefvorlage-Editor: Minimal-WYSIWYG + Vorschau-Panel + Vorlagen-Loader
|
||||
* für Django Admin – keine externen Abhängigkeiten.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Warte auf DOM
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var textareas = document.querySelectorAll("textarea.briefvorlage-textarea");
|
||||
textareas.forEach(function (textarea) {
|
||||
initEditor(textarea);
|
||||
});
|
||||
});
|
||||
|
||||
function initEditor(textarea) {
|
||||
var wrapper = document.createElement("div");
|
||||
wrapper.style.cssText = "border:1px solid #ccc;border-radius:4px;overflow:hidden;margin-top:4px;";
|
||||
|
||||
// ---- Toolbar ----
|
||||
var toolbar = document.createElement("div");
|
||||
toolbar.style.cssText = "background:#f5f5f5;border-bottom:1px solid #ccc;padding:5px 8px;display:flex;flex-wrap:wrap;gap:4px;align-items:center;";
|
||||
|
||||
var buttons = [
|
||||
{ label: "B", cmd: "bold", title: "Fett (Strg+B)", style: "font-weight:bold;" },
|
||||
{ label: "I", cmd: "italic", title: "Kursiv (Strg+I)", style: "font-style:italic;" },
|
||||
{ label: "U", cmd: "underline", title: "Unterstrichen (Strg+U)", style: "text-decoration:underline;" },
|
||||
{ label: "¶", cmd: "insertParagraph", title: "Absatz einfügen" },
|
||||
{ label: "• Liste", cmd: "insertUnorderedList", title: "Aufzählung" },
|
||||
{ label: "1. Liste", cmd: "insertOrderedList", title: "Nummerierte Liste" },
|
||||
];
|
||||
|
||||
buttons.forEach(function (b) {
|
||||
var btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.title = b.title;
|
||||
btn.innerHTML = b.label;
|
||||
btn.style.cssText = "padding:3px 8px;cursor:pointer;border:1px solid #ccc;border-radius:3px;background:#fff;font-size:13px;" + (b.style || "");
|
||||
btn.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
editor.focus();
|
||||
document.execCommand(b.cmd, false, null);
|
||||
syncToTextarea();
|
||||
});
|
||||
toolbar.appendChild(btn);
|
||||
});
|
||||
|
||||
// Trennlinie
|
||||
var sep = document.createElement("span");
|
||||
sep.style.cssText = "border-left:1px solid #ccc;height:20px;margin:0 4px;";
|
||||
toolbar.appendChild(sep);
|
||||
|
||||
// Tab-Buttons: Editor / HTML / Vorschau
|
||||
var tabEditor = createTabBtn("Editor", true);
|
||||
var tabHtml = createTabBtn("HTML", false);
|
||||
var tabVorschau = createTabBtn("Vorschau", false);
|
||||
toolbar.appendChild(tabEditor);
|
||||
toolbar.appendChild(tabHtml);
|
||||
toolbar.appendChild(tabVorschau);
|
||||
|
||||
// Vorlage-Loader (nur wenn BriefVorlage-API verfügbar)
|
||||
var sep2 = document.createElement("span");
|
||||
sep2.style.cssText = "border-left:1px solid #ccc;height:20px;margin:0 4px;";
|
||||
toolbar.appendChild(sep2);
|
||||
|
||||
var vorlagenSelect = document.createElement("select");
|
||||
vorlagenSelect.style.cssText = "font-size:12px;padding:2px 6px;border:1px solid #ccc;border-radius:3px;max-width:200px;";
|
||||
var defaultOption = document.createElement("option");
|
||||
defaultOption.value = "";
|
||||
defaultOption.textContent = "– Vorlage laden –";
|
||||
vorlagenSelect.appendChild(defaultOption);
|
||||
toolbar.appendChild(vorlagenSelect);
|
||||
|
||||
// Vorlagen asynchron laden
|
||||
loadVorlagen(vorlagenSelect);
|
||||
|
||||
var ladeBtn = document.createElement("button");
|
||||
ladeBtn.type = "button";
|
||||
ladeBtn.textContent = "Laden";
|
||||
ladeBtn.title = "Ausgewählte Vorlage in den Editor laden";
|
||||
ladeBtn.style.cssText = "padding:3px 8px;cursor:pointer;border:1px solid #0d6efd;border-radius:3px;background:#0d6efd;color:#fff;font-size:12px;";
|
||||
ladeBtn.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
var val = vorlagenSelect.value;
|
||||
if (!val) return;
|
||||
var opt = vorlagenSelect.querySelector("option[value='" + val + "']");
|
||||
if (!opt) return;
|
||||
var html = opt.dataset.briefvorlage || "";
|
||||
var betreff = opt.dataset.betreff || "";
|
||||
if (confirm("Vorlage \"" + opt.textContent + "\" laden?\nDer aktuelle Brieftext wird überschrieben.")) {
|
||||
editor.innerHTML = html;
|
||||
textarea.value = html;
|
||||
// Betreff-Feld befüllen falls vorhanden und nicht leer
|
||||
if (betreff) {
|
||||
var betreffField = document.getElementById("id_betreff");
|
||||
if (betreffField && !betreffField.value) {
|
||||
betreffField.value = betreff;
|
||||
}
|
||||
}
|
||||
updatePreview();
|
||||
}
|
||||
});
|
||||
toolbar.appendChild(ladeBtn);
|
||||
|
||||
// ---- Editor-Div (WYSIWYG) ----
|
||||
var editor = document.createElement("div");
|
||||
editor.contentEditable = "true";
|
||||
editor.style.cssText = "min-height:300px;padding:12px;font-family:Times New Roman,serif;font-size:11pt;line-height:1.4;outline:none;background:#fff;";
|
||||
editor.innerHTML = textarea.value;
|
||||
editor.addEventListener("input", syncToTextarea);
|
||||
editor.addEventListener("keyup", syncToTextarea);
|
||||
|
||||
// ---- HTML-Textarea (Quelltext) ----
|
||||
textarea.style.cssText += "display:none;width:100%;box-sizing:border-box;border:none;padding:12px;font-family:monospace;font-size:13px;";
|
||||
textarea.addEventListener("input", function () {
|
||||
editor.innerHTML = textarea.value;
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// ---- Vorschau-Panel ----
|
||||
var preview = document.createElement("div");
|
||||
preview.style.cssText = "display:none;min-height:300px;padding:12px;background:#fff;font-family:'Times New Roman',serif;font-size:11pt;line-height:1.4;";
|
||||
|
||||
// Tab-Logik
|
||||
function showTab(which) {
|
||||
editor.style.display = "none";
|
||||
textarea.style.display = "none";
|
||||
preview.style.display = "none";
|
||||
tabEditor.style.background = "#f5f5f5";
|
||||
tabHtml.style.background = "#f5f5f5";
|
||||
tabVorschau.style.background = "#f5f5f5";
|
||||
if (which === "editor") {
|
||||
editor.style.display = "block";
|
||||
tabEditor.style.background = "#fff";
|
||||
tabEditor.style.fontWeight = "bold";
|
||||
} else if (which === "html") {
|
||||
textarea.style.display = "block";
|
||||
tabHtml.style.background = "#fff";
|
||||
tabHtml.style.fontWeight = "bold";
|
||||
} else {
|
||||
preview.style.display = "block";
|
||||
tabVorschau.style.background = "#fff";
|
||||
tabVorschau.style.fontWeight = "bold";
|
||||
updatePreview();
|
||||
}
|
||||
}
|
||||
|
||||
tabEditor.addEventListener("click", function (e) { e.preventDefault(); showTab("editor"); });
|
||||
tabHtml.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
syncToTextarea();
|
||||
showTab("html");
|
||||
});
|
||||
tabVorschau.addEventListener("click", function (e) { e.preventDefault(); showTab("vorschau"); });
|
||||
|
||||
// Zusammenbauen
|
||||
wrapper.appendChild(toolbar);
|
||||
wrapper.appendChild(editor);
|
||||
wrapper.appendChild(preview);
|
||||
|
||||
// Textarea hinter Editor platzieren
|
||||
textarea.parentNode.insertBefore(wrapper, textarea);
|
||||
wrapper.appendChild(textarea);
|
||||
|
||||
// Initial: Editor-Tab aktiv
|
||||
showTab("editor");
|
||||
|
||||
// ---- Hilfsfunktionen ----
|
||||
function syncToTextarea() {
|
||||
textarea.value = editor.innerHTML;
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
// Platzhalter durch Beispielwerte ersetzen für Vorschau
|
||||
var html = textarea.value;
|
||||
var replacements = {
|
||||
"{{ anrede }}": "Frau",
|
||||
"{{ vorname }}": "Maria",
|
||||
"{{ nachname }}": "Mustermann",
|
||||
"{{ strasse }}": "Musterstraße 12",
|
||||
"{{ plz }}": "46499",
|
||||
"{{ ort }}": "Hamminkeln",
|
||||
"{{ datum }}": "Freitag, 17. April 2026",
|
||||
"{{ uhrzeit }}": "19:00 Uhr",
|
||||
"{{ veranstaltungsort }}": "Marienthaler Gasthof",
|
||||
"{{ gasthaus_adresse }}": "Pastor-Winkelmann-Str. 2, 46499 Hamminkeln",
|
||||
};
|
||||
for (var key in replacements) {
|
||||
html = html.split(key).join(replacements[key]);
|
||||
}
|
||||
preview.innerHTML = html || "<em style='color:#999;'>Kein Brieftext eingegeben.</em>";
|
||||
}
|
||||
|
||||
function createTabBtn(label, active) {
|
||||
var btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.textContent = label;
|
||||
btn.style.cssText = "padding:3px 10px;cursor:pointer;border:1px solid #ccc;border-radius:3px;font-size:12px;background:" + (active ? "#fff" : "#f5f5f5") + ";";
|
||||
if (active) btn.style.fontWeight = "bold";
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
|
||||
function loadVorlagen(selectEl) {
|
||||
// Lese Vorlagen über einfachen Admin-API-Aufruf
|
||||
fetch("/admin/stiftung/briefvorlage/?format=json", {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" }
|
||||
})
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (data) {
|
||||
if (!data || !data.results) return;
|
||||
data.results.forEach(function (v) {
|
||||
var opt = document.createElement("option");
|
||||
opt.value = v.id || v.pk;
|
||||
opt.textContent = v.name || v.fields && v.fields.name;
|
||||
opt.dataset.briefvorlage = v.briefvorlage || v.fields && v.fields.briefvorlage || "";
|
||||
opt.dataset.betreff = v.betreff || v.fields && v.fields.betreff || "";
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
// Kein API-Endpunkt – Vorlage-Loader deaktivieren
|
||||
});
|
||||
|
||||
// Alternativ: REST-API
|
||||
fetch("/api/v1/briefvorlagen/", {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" }
|
||||
})
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (data) {
|
||||
if (!data) return;
|
||||
var results = Array.isArray(data) ? data : data.results;
|
||||
if (!results) return;
|
||||
// Bereits vorhandene Optionen nicht doppeln
|
||||
var existing = Array.from(selectEl.options).map(function (o) { return o.value; });
|
||||
results.forEach(function (v) {
|
||||
var id = String(v.id || v.pk || "");
|
||||
if (existing.includes(id)) return;
|
||||
var opt = document.createElement("option");
|
||||
opt.value = id;
|
||||
opt.textContent = v.name;
|
||||
opt.dataset.briefvorlage = v.briefvorlage || "";
|
||||
opt.dataset.betreff = v.betreff || "";
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
})
|
||||
.catch(function () { /* kein REST-Endpunkt */ });
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user