/* /assets/js/ui-editor.js (KORRIGIERT: Speichern wird an iFrame-Editor delegiert) */
// Öffnen, Befüllen, Speichern (mit Live-HTML), Preview – Race-Schutz & Lade-Overlay.
import { apiUpdate, toast, apiAction } from './api.js';
import { initCraftEditor } from './craft-editor.js';
export function initEditor() {
// ... (Alle Konstanten bleiben unverändert) ...
const dlg = document.getElementById('editorDialog');
const iframe = document.getElementById('editorFrame');
const btnSave = document.getElementById('btn-save');
const btnPreview = document.getElementById('btn-preview');
const btnTest = document.getElementById('btn-test');
const btnClose = document.getElementById('btn-close');
const btnClear = document.getElementById('btn-clear-main');
const editorSelect = document.getElementById('editorTypeSelect');
const versionSelect = document.getElementById('versionSelect');
const btnRestoreVersion = document.getElementById('btn-restore-version');
const craftEditor = initCraftEditor();
const prevDlg = document.getElementById('previewDialog');
const sendDlg = document.getElementById('sendTestDialog');
const sendForm = document.getElementById('sendTestForm');
const sendTo = document.getElementById('send_to');
const sendSubject = document.getElementById('send_subject');
const sendInfo = document.getElementById('send_template_info');
const btnCancelSend= document.getElementById('btn-cancel-send');
const btnSendNow = document.getElementById('btn-send-now');
const sendSender = document.getElementById('send_sender');
const sendSenderHint = document.getElementById('send_sender_hint');
const prevFrame = document.getElementById('previewFrame');
const btnPrevClose = document.getElementById('btn-close-preview');
let current = null; // { resource, id, name, section }
let bridgeListener = null;
let reqToken = 0; // steigender Token pro Öffnen -> ignoriert verspätete Events
let senderOptions = [];
let senderLoadPromise = null;
let currentEditorType = 'grapesjs';
let versionItems = [];
let savedSnapshot = '';
const ok = (m) => toast(m, true);
const err = (m) => toast(m, false);
// ---------- Hilfen ----------
function activeMode() {
const activeSection = window.__activeSection || current?.section || null;
return (activeSection?.slug) || (current?.resource) || 'emailtemplate';
}
function setSendContext(id, name = '') {
if (sendDlg) {
if (id) {
sendDlg.dataset.templateId = String(id);
} else {
delete sendDlg.dataset.templateId;
}
if (name) sendDlg.dataset.templateName = name;
}
if (sendInfo) {
if (!id) {
sendInfo.textContent = 'Kein Template ausgewählt.';
sendInfo.classList.add('text-rose-600');
} else {
const label = name ? `${name} – Template #${id}` : `Template #${id}`;
sendInfo.textContent = label;
sendInfo.classList.remove('text-rose-600');
}
}
}
function formatVersionDate(value) {
if (!value) return '';
try {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString('de-DE');
} catch {
return value;
}
}
function setVersionUiVisible(show) {
if (versionSelect) versionSelect.classList.toggle('hidden', !show);
if (btnRestoreVersion) btnRestoreVersion.classList.toggle('hidden', !show);
}
setVersionUiVisible(false);
function normalizeSnapshotValue(value) {
return value === null || value === undefined ? '' : String(value);
}
function buildSnapshot({ editorType, html, json, craftJson }) {
return JSON.stringify({
editorType: normalizeSnapshotValue(editorType),
html: normalizeSnapshotValue(html),
json: normalizeSnapshotValue(json),
craftJson: normalizeSnapshotValue(craftJson),
});
}
function extractContentFields(payload = {}) {
const base = payload || {};
const item = base.item || {};
return {
html: base.html ?? base.html_content ?? item.html ?? item.html_content ?? '',
json: base.content ?? base.json_content ?? item.content ?? item.json_content ?? '',
craftJson: base.craft_json ?? item.craft_json ?? '',
editorType: String(base.editor_type ?? item.editor_type ?? currentEditorType ?? 'grapesjs').toLowerCase(),
};
}
async function buildCurrentSnapshot() {
if (currentEditorType === 'craftjs') {
return buildSnapshot({
editorType: 'craftjs',
html: craftEditor ? craftEditor.getContent() : '',
json: '',
craftJson: craftEditor && craftEditor.getCraftJson ? craftEditor.getCraftJson() : '',
});
}
try {
const editor = await waitForEditor(2000);
const win = iframe?.contentWindow;
const fontCss = win?.BridgeParts?.RTE_FONT_FACE_CSS || '';
const cssPayload = (fontCss ? fontCss + '\n' : '') + (editor.getCss() || '');
const htmlContent = (editor.getHtml() || '') + '';
let jsonRaw = '';
try {
jsonRaw = JSON.stringify(editor.getProjectData());
} catch {}
return buildSnapshot({
editorType: 'grapesjs',
html: htmlContent,
json: jsonRaw,
craftJson: '',
});
} catch {
return buildSnapshot({ editorType: currentEditorType, html: '', json: '', craftJson: '' });
}
}
function setSavedSnapshotFromData(payload) {
const fields = extractContentFields(payload);
savedSnapshot = buildSnapshot(fields);
}
async function hasUnsavedChanges() {
if (!savedSnapshot) return false;
const currentSnapshot = await buildCurrentSnapshot();
return currentSnapshot !== savedSnapshot;
}
async function confirmUnsavedChanges() {
const dirty = await hasUnsavedChanges();
if (!dirty) return true;
const shouldSave = window.confirm('Ungespeicherte Änderungen gefunden. Jetzt speichern?');
if (shouldSave) {
const okSave = await save();
if (!okSave) return false;
return true;
}
return true;
}
function renderVersionOptions(items) {
versionItems = items || [];
if (!versionSelect) return;
const rows = Array.isArray(versionItems) ? versionItems : [];
versionSelect.innerHTML = '';
if (!rows.length) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'Keine Versionen vorhanden';
opt.disabled = true;
versionSelect.appendChild(opt);
versionSelect.disabled = true;
return;
}
versionSelect.disabled = false;
rows.forEach((item) => {
const opt = document.createElement('option');
const label = `#${item.version_no} – ${formatVersionDate(item.created_at)}`;
opt.value = String(item.id);
opt.textContent = label;
versionSelect.appendChild(opt);
});
}
async function loadVersionsForCurrent() {
if (!current?.id) {
renderVersionOptions([]);
return;
}
try {
const res = await apiAction('content_versions.list', { method: 'GET', data: { content_id: current.id, id: current.id } });
if (!res?.ok) throw new Error(res?.error || 'Versionen konnten nicht geladen werden');
renderVersionOptions(Array.isArray(res?.items) ? res.items : []);
} catch {
renderVersionOptions([]);
}
}
async function applyVersionPayload(payload) {
const data = extractContentFields(payload);
const targetType = data.editorType === 'craftjs' ? 'craftjs' : 'grapesjs';
setEditorType(targetType);
if (targetType === 'craftjs') {
craftEditor?.setContent(data.html || '', data.craftJson || '');
return;
}
const editor = await waitForEditor(3000);
const jsonRaw = normalizeSnapshotValue(data.json).trim();
if (jsonRaw) {
try {
const project = JSON.parse(jsonRaw);
editor.loadProjectData(project);
return;
} catch {}
}
const html = normalizeSnapshotValue(data.html);
editor.setComponents(html);
}
async function loadLatestContentFromServer() {
const res = await apiAction('content.get', { method: 'GET', data: { id: current.id, section_id: current.section.id } });
await applyVersionPayload(res || {});
setSavedSnapshotFromData(res || {});
}
function writeHtmlToFrame(html) {
iframe.srcdoc = `
${html || ''}
`;
}
async function readEditedHtml() {
if (currentEditorType === 'craftjs') {
return craftEditor ? craftEditor.getContent() : '';
}
const win = iframe?.contentWindow;
const doc = iframe?.contentDocument;
if (!win || !doc) return '';
const ed = win.__gjs || (win.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
if (ed && typeof ed.getHtml === 'function') {
const html = ed.getHtml();
const css = (typeof ed.getCss === 'function') ? ed.getCss() : '';
return css ? `\n${html}` : html;
}
const root = doc.querySelector('#gjs') || doc.body || doc.documentElement;
return root ? root.innerHTML : '';
}
function waitForEditor(maxMs = 8000) {
return new Promise((resolve, reject) => {
const start = Date.now();
(function poll() {
const win = iframe?.contentWindow;
const ed = win?.__gjs || (win?.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
if (ed) return resolve(ed);
if (Date.now() - start > maxMs) return reject(new Error('Editor not ready'));
setTimeout(poll, 120);
})();
});
}
// 🚨 NEUE FUNKTION: Delegiert das Kommando an den Editor im iFrame
async function delegateCommand(commandName) {
try {
const editor = await waitForEditor(3000);
if (editor.Commands.has(commandName)) {
// Führt den Command im iFrame aus (z.B. 'save-data')
editor.runCommand(commandName);
return true;
} else {
err(`Delegieren fehlgeschlagen: Command '${commandName}' nicht gefunden.`);
return false;
}
} catch (e) {
err(`Delegieren fehlgeschlagen: Editor nicht bereit (${commandName}).`);
console.error(e);
return false;
}
}
// ... (hideReadyBadge bleibt unverändert) ...
function hideReadyBadge(doc) {
if (!doc) return;
const kill = () => {
const el = doc.getElementById('badge');
if (el) el.style.display = 'none';
};
kill();
const style = doc.createElement('style');
style.textContent = `
#badge { display:none !important; }
.gjs-pn-status { display:none !important; }
.ready-badge,
.status-badge.ready,
[data-status="ready"],
[data-badge="ready"],
.gjs-ready,
.gjs-badge-ready { display:none !important; }
`;
doc.head.appendChild(style);
const mo = new MutationObserver(() => { kill(); /* hideByText(doc); */ });
mo.observe(doc.documentElement, { childList: true, subtree: true });
setTimeout(() => { kill(); /* hideByText(doc); */ }, 150);
setTimeout(() => { kill(); /* hideByText(doc); */ }, 500);
setTimeout(() => { kill(); /* hideByText(doc); */ }, 1200);
}
function extractCraftHtml(craftJson, fallbackHtml) {
if (!craftJson) return fallbackHtml || '';
try {
const parsed = typeof craftJson === 'string' ? JSON.parse(craftJson) : craftJson;
if (parsed && typeof parsed.html === 'string') {
return parsed.html;
}
} catch {}
return fallbackHtml || '';
}
function looksCraftSerialized(payload) {
if (!payload) return false;
try {
const parsed = typeof payload === 'string' ? JSON.parse(payload) : payload;
return !!(parsed && typeof parsed === 'object' && parsed.ROOT);
} catch {
return false;
}
}
function setEditorType(nextType) {
currentEditorType = nextType === 'craftjs' ? 'craftjs' : 'grapesjs';
if (editorSelect) editorSelect.value = currentEditorType;
if (currentEditorType === 'craftjs') {
iframe?.classList?.add('hidden');
craftEditor?.show();
} else {
craftEditor?.hide();
iframe?.classList?.remove('hidden');
}
}
// ... (Lade-Overlay bleibt unverändert) ...
let veilEl = null;
function ensureVeil() {
if (veilEl) return veilEl;
veilEl = document.createElement('div');
Object.assign(veilEl.style, {
position:'absolute', inset:'0', background:'rgba(248,250,252,.85)',
display:'flex', alignItems:'center', justifyContent:'center',
zIndex:'2147483000', fontFamily:'system-ui, -apple-system, Segoe UI, Roboto, Arial',
fontSize:'14px', color:'#0f172a'
});
veilEl.innerHTML = `
`;
const host = dlg?.querySelector('.h-full, .flex, .flex-col') || dlg;
(host || document.body).appendChild(veilEl);
return veilEl;
}
function showVeil(){ ensureVeil().style.display = 'flex'; }
function hideVeil(){ if (veilEl) veilEl.style.display = 'none'; }
async function buildRefLibForContext() {
return { sections: [], blocks: [] };
}
async function buildSnippetsForContext() {
return [];
}
async function loadSenderOptions(force = false) {
if (!sendSender) return;
if (senderLoadPromise && !force) return senderLoadPromise;
senderLoadPromise = apiAction('account.senders.list', { method: 'GET' })
.then(res => {
senderOptions = res?.items || [];
renderSenderOptions();
})
.catch(() => {
senderOptions = [];
renderSenderOptions();
})
.finally(() => {
senderLoadPromise = null;
});
return senderLoadPromise;
}
function renderSenderOptions() {
if (!sendSender) return;
const previous = sendSender.value;
let html = '';
senderOptions.forEach(opt => {
const label = opt.label || opt.from_name || opt.from_email;
html += ``;
});
sendSender.innerHTML = html;
if (previous && senderOptions.some(opt => String(opt.id) === previous)) {
sendSender.value = previous;
} else {
sendSender.value = '';
}
if (sendSenderHint) {
sendSenderHint.classList.toggle('hidden', senderOptions.length > 0);
}
}
// ---------- Initialen HTML-Inhalt in Editor pushen (mit Token/Race-Schutz) ----------
async function pushInitialHtmlToEditor({ mode, html, snippets, ref, token, hasJson, json }) {
if (token !== reqToken) return; // veraltete Anfrage ignorieren
const win = iframe?.contentWindow;
const doc = iframe?.contentDocument;
// NEU: HTML wird NUR über postMessage gesendet. Die Bridge im iFrame ist verantwortlich
// dafür, das HTML in GrapesJS zu setzen, NACHDEM ihre Plugins fertig sind.
try {
win?.postMessage({ source:'admin', type:'init', mode, html: html || '', snippets: snippets || [], ref: ref || {}, hasJson: !!hasJson, json: json || '' }, '*');
} catch {}
try {
// Warten auf Editor ist noch sinnvoll, um das Lade-Badge zu unterdrücken,
// aber wir manipulieren den Editor NICHT MEHR direkt von hier aus.
await waitForEditor(6000);
if (token !== reqToken) return;
// ... (Gelöschte Logik: ed.setComponents(html) ist nun in der Bridge-Logik) ...
} catch {
/* Falls GJS noch nicht bereit ist, arbeiten wir nur via postMessage. */
}
try { hideReadyBadge(doc); } catch {}
if (token === reqToken) hideVeil();
}
// ---------- Öffnen ----------
async function open(item, resource, sectionOverride) {
const section = item?.section || sectionOverride || window.__activeSection || null;
current = {
resource: 'content',
id: Number(item?.id || 0),
name: item?.name || '',
section: section,
};
if (!current.id) return err('Ungültige ID');
if (!current.section) return err('Section nicht gefunden');
// globaler Kontext
window.__currentItemId = current.id;
window.__currentEditorCtx = { id: current.id, mode: current.section.slug, section: current.section };
setSendContext(current.section?.is_template ? current.id : 0, current.name);
if (btnTest) btnTest.classList.toggle('hidden', !current.section?.is_template);
setVersionUiVisible(true);
// Neuen Token erzeugen & alten Listener entfernen
reqToken++;
const myToken = reqToken;
if (bridgeListener) window.removeEventListener('message', bridgeListener);
bridgeListener = null;
// Overlay zeigen
showVeil();
// Daten parallel laden (fresh HTML + kontextgefilterte Snippets + Referenzen)
let fresh = '';
let snippets = [];
let refLib = { sections: [], blocks: [] };
let hasJson = false;
let jsonState = '';
let editorType = 'grapesjs';
let craftJson = '';
await Promise.all([
(async() => {
try {
const row = await apiAction('content.get', { method: 'GET', data: { id: current.id, section_id: current.section.id } });
const rawContent = row?.content ?? row?.item?.content ?? '';
const trimmed = String(rawContent || '').trim();
const looksJson = trimmed.startsWith('{') || trimmed.startsWith('[');
jsonState = looksJson ? rawContent : '';
hasJson = looksJson;
// API liefert jetzt top-level html/content; fallback auf item.*
fresh = row?.html ?? row?.item?.html ?? '';
if (!fresh && !looksJson) {
fresh = rawContent;
}
editorType = String(row?.editor_type ?? row?.item?.editor_type ?? 'grapesjs').toLowerCase();
craftJson = row?.craft_json ?? row?.item?.craft_json ?? '';
} catch {}
})(),
(async() => { snippets = await buildSnippetsForContext(current); })(),
(async() => { refLib = await buildRefLibForContext(current); })(),
(async() => { await loadVersionsForCurrent(); })()
]);
editorType = editorType === 'craftjs' ? 'craftjs' : 'grapesjs';
setSavedSnapshotFromData({ html: fresh, content: jsonState, editor_type: editorType, craft_json: craftJson });
setEditorType(editorType);
if (editorType === 'craftjs') {
const craftHtml = extractCraftHtml(craftJson, fresh);
craftEditor?.setContent(craftHtml, craftJson);
hideVeil();
if (dlg && typeof dlg.showModal === 'function') dlg.showModal();
if (!looksCraftSerialized(craftJson) && craftEditor?.serializeFromHtml) {
const seed = craftEditor.serializeFromHtml(craftHtml);
try {
await apiUpdate('content', current.id, {
editor_type: 'craftjs',
html: craftHtml,
craft_json: seed,
section_id: current.section.id,
});
} catch {}
}
return;
}
// iFrame-Load -> Bridge-Ready abhören
iframe.onload = function () {
if (myToken !== reqToken) return;
try { hideReadyBadge(iframe.contentDocument); } catch {}
bridgeListener = (ev) => {
const d = ev?.data || {};
if (!d) return;
if (d.source === 'bridge-core' && d.type === 'rte-blur') {
try { console.log(d.detail || '[RTE BLUR]'); } catch {}
return;
}
// wir erwarten Nachrichten aus der Bridge/Editor
if (d.source !== 'bridge' && d.source !== 'editor') return;
if (myToken !== reqToken) return;
// NEU: Wenn der Editor meldet, dass er *gespeichert* hat,
// aktualisieren wir die Liste im Elternfenster
if (d.type === 'save:success') {
ok('Gespeichert');
try {
if (typeof window.reloadActiveList === 'function') window.reloadActiveList();
else if (typeof window.__reloadList === 'function') window.__reloadList(current.section);
} catch {}
return;
}
// neue Bridge meldet gjs:ready; ältere evtl. core-ready/bridge:ready
if (d.type === 'gjs:ready' || d.type === 'core-ready' || d.type === 'bridge:ready' || d.type === 'bridge:booted') {
pushInitialHtmlToEditor({
mode: current.section.slug,
html: fresh,
snippets,
ref: {
sections: (refLib.sections || []).map(r => ({ id:r.id, name:r.name, html:r.html || '' })),
blocks: (refLib.blocks || []).map(r => ({ id:r.id, name:r.name, html:r.html || '' }))
},
token: myToken,
hasJson,
json: jsonState
});
}
};
window.addEventListener('message', bridgeListener);
// Fallback, falls kein Ready ankommt
setTimeout(() => {
pushInitialHtmlToEditor({
mode: current.section.slug,
html: fresh,
snippets,
ref: {
sections: (refLib.sections || []).map(r => ({ id:r.id, name:r.name, html:r.html || '' })),
blocks: (refLib.blocks || []).map(r => ({ id:r.id, name:r.name, html:r.html || '' }))
},
token: myToken,
hasJson,
json: jsonState
});
}, 1200);
};
// Jetzt den Editor-Core laden (erst NACH about:blank)
iframe.src = `editor/editor-core.php?mode=${encodeURIComponent(current.section.slug)}&id=${current.id}§ion_id=${current.section.id}&t=${Date.now()}`;
dlg?.showModal?.();
}
// ---------- Speichern (DELEGIERT) ----------
// 🚨 KORRIGIERT: Delegiert Speichern an den iFrame, der die JSON-Daten holt!
async function save() {
if (!current?.id) return err('Keine aktive ID');
if (currentEditorType === 'craftjs') {
const html = craftEditor ? craftEditor.getContent() : '';
const craftJson = craftEditor && craftEditor.getCraftJson
? craftEditor.getCraftJson()
: JSON.stringify({ html });
const payload = { html, craft_json: craftJson, editor_type: 'craftjs', section_id: current.section.id };
const res = await apiUpdate('content', current.id, payload);
if (res?.ok) ok('Gespeichert');
else err(res?.error || 'Speichern fehlgeschlagen');
if (res?.ok) setTimeout(loadVersionsForCurrent, 300);
return res?.ok;
}
const okSave = await delegateCommand('save-data');
if (okSave) {
setTimeout(async () => {
await loadVersionsForCurrent();
try {
const res = await apiAction('content.get', { method: 'GET', data: { id: current.id, section_id: current.section.id } });
setSavedSnapshotFromData(res || {});
} catch {}
}, 800);
}
return okSave;
}
// ... (Der Rest der Funktionen bleibt unverändert) ...
async function clearEditor() {
if (currentEditorType === 'craftjs') {
craftEditor?.clear();
return;
}
const win = iframe?.contentWindow;
const ed = win?.__gjs || (win?.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
if (ed) {
ed.setComponents('');
ed.setStyle('');
} else {
writeHtmlToFrame('');
}
}
async function openPreview() {
const html = await readEditedHtml();
prevFrame.srcdoc = `${html || '(leer)'}`;
prevDlg?.showModal?.();
}
async function openSend(ctx = null) {
if (!current?.section?.is_template) {
err('Kein Template geladen');
return;
}
const ctxId = ctx?.id ? Number(ctx.id) : (window.__currentItemId || current?.id || 0);
if (!ctxId) {
err('Kein Template geladen');
return;
}
window.__currentItemId = ctxId;
const ctxName = ctx?.name ?? sendDlg?.dataset?.templateName ?? current?.name ?? '';
setSendContext(ctxId, ctxName);
if (sendSubject) sendSubject.value = ctx?.subject || 'Testversand';
if (sendTo) sendTo.value = ctx?.to || '';
await loadSenderOptions(true);
sendDlg?.showModal?.();
}
function closeSend(){
sendDlg?.close?.();
if (sendSender) sendSender.value = '';
}
async function doSend(ev){
ev?.preventDefault?.();
const to = (sendTo?.value || '').trim();
if(!to){ err('Bitte Empfänger angeben'); return; }
const win = iframe?.contentWindow;
const ctx = (win && win.__currentEditorCtx) || {};
const id = (window.__currentItemId || ctx?.id || 0);
if(!id){ toast("Kein Template geladen", false); return; }
// Hier wird der gespeicherte HTML-Code verwendet, nicht der Live-HTML, da apiAction
// keine Live-Daten erwartet. Es geht um template_id.
const payload = {
template_id: id,
to,
subject: (sendSubject?.value || 'Testversand'),
};
if (sendSender && sendSender.value) {
payload.sender_id = Number(sendSender.value);
}
const r = await apiAction('templates.test_send', { method:'POST', data: payload });
if(r?.ok){ toast("Testversand ausgelöst"); closeSend(); } else { toast("Senden fehlgeschlagen", false); }
}
function closePreview(){ prevDlg?.close?.(); }
async function close() {
const proceed = await confirmUnsavedChanges();
if (!proceed) return;
// nächstes Öffnen invalidiert laufende asyncs
reqToken++;
try { iframe.contentWindow?.postMessage({source:'admin',type:'reset'}, '*'); } catch {}
if (bridgeListener) window.removeEventListener('message', bridgeListener);
bridgeListener = null;
hideVeil();
dlg?.close?.();
// iFrame zurück auf blank
iframe.src = 'about:blank#' + Date.now();
// Kontext leeren
current = null;
window.__currentItemId = undefined;
window.__currentEditorCtx = undefined;
}
async function switchEditor(nextType) {
if (!current?.id) return;
const target = nextType === 'craftjs' ? 'craftjs' : 'grapesjs';
if (target === currentEditorType) return;
const confirmed = window.confirm('Editor wechseln? Ungespeicherte Änderungen gehen verloren.');
if (!confirmed) {
if (editorSelect) editorSelect.value = currentEditorType;
return;
}
if (currentEditorType === 'grapesjs' && target === 'craftjs') {
const html = await readEditedHtml();
const craftJson = craftEditor && craftEditor.serializeFromHtml
? craftEditor.serializeFromHtml(html)
: JSON.stringify({ html });
const res = await apiUpdate('content', current.id, {
editor_type: 'craftjs',
html,
craft_json: craftJson,
section_id: current.section.id,
});
if (!res?.ok) {
err(res?.error || 'Editorwechsel fehlgeschlagen');
if (editorSelect) editorSelect.value = currentEditorType;
return;
}
setEditorType('craftjs');
craftEditor?.setContent(html, craftJson);
iframe.src = 'about:blank#' + Date.now();
ok('Editor gewechselt');
return;
}
if (currentEditorType === 'craftjs' && target === 'grapesjs') {
const html = craftEditor ? craftEditor.getContent() : '';
const res = await apiUpdate('content', current.id, {
editor_type: 'grapesjs',
html,
section_id: current.section.id,
});
if (!res?.ok) {
err(res?.error || 'Editorwechsel fehlgeschlagen');
if (editorSelect) editorSelect.value = currentEditorType;
return;
}
ok('Editor gewechselt');
await open({ id: current.id, name: current.name, section: current.section }, 'content', current.section);
}
}
// Buttons
btnSave && (btnSave.onclick = save);
btnClear && (btnClear.onclick = clearEditor);
btnClose && (btnClose.onclick = close);
btnPrevClose && (btnPrevClose.onclick = closePreview);
btnPreview && (btnPreview.onclick = openPreview);
btnTest && (btnTest.onclick = openSend);
btnCancelSend&& (btnCancelSend.onclick= closeSend);
sendForm && (sendForm.onsubmit = doSend);
editorSelect && (editorSelect.onchange = () => switchEditor(editorSelect.value));
versionSelect && (versionSelect.onchange = async () => {
if (!current?.id) return;
const proceed = await confirmUnsavedChanges();
if (!proceed) {
versionSelect.value = '';
return;
}
const versionId = Number(versionSelect.value || 0);
if (!versionId) {
await loadLatestContentFromServer();
return;
}
try {
const res = await apiAction('content_versions.get', { method: 'GET', data: { id: versionId, content_id: current.id } });
if (!res?.ok) throw new Error(res?.error || 'Version konnte nicht geladen werden');
await applyVersionPayload(res?.item || res);
} catch (e) {
err(e.message || 'Version konnte nicht geladen werden');
}
});
btnRestoreVersion && (btnRestoreVersion.onclick = async () => {
if (!current?.id) return;
const versionId = Number(versionSelect?.value || 0);
if (!versionId) {
err('Bitte eine Version auswählen');
return;
}
if (!confirm('Version wiederherstellen? Der aktuelle Stand wird überschrieben.')) return;
try {
const res = await apiAction('content_versions.restore', { method: 'POST', data: { id: versionId, content_id: current.id } });
if (!res?.ok) throw new Error(res?.error || 'Wiederherstellen fehlgeschlagen');
ok('Version wiederhergestellt');
await open({ id: current.id, name: current.name, section: current.section }, null, current.section);
} catch (e) {
err(e.message || 'Wiederherstellen fehlgeschlagen');
}
});
window.AdminTestSend = window.AdminTestSend || {};
window.AdminTestSend.open = (opts = {}) => {
const targetId = Number(opts.id || window.__currentItemId || 0);
if (!targetId) {
err('Testversand: Keine ID vorhanden');
return;
}
if (!current?.section?.is_template) {
err('Kein Template geladen');
return;
}
window.__currentItemId = targetId;
setSendContext(targetId, opts.name || '');
openSend({ id: targetId, name: opts.name || '' });
};
// Public API
window.EditorUI = { open, save, close, clear: clearEditor, preview: openPreview };
}
function escapeHtml(str) {
return String(str || '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// Default-Export + globaler Fallback
export default initEditor;
window.initEditor = initEditor;