/* /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 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'); const unsavedDialog = document.getElementById('unsavedDialog'); const btnUnsavedCancel = document.getElementById('btn-unsaved-cancel'); const btnUnsavedDiscard = document.getElementById('btn-unsaved-discard'); const btnUnsavedSave = document.getElementById('btn-unsaved-save'); const btnActivateVersion = document.getElementById('btn-activate-version'); const btnDeactivateVersion = document.getElementById('btn-deactivate-version'); const versionActiveBadge = document.getElementById('versionActiveBadge'); const activateDialog = document.getElementById('activateVersionDialog'); const btnActivateCancel = document.getElementById('btn-activate-cancel'); const btnActivateNo = document.getElementById('btn-activate-no'); const btnActivateYes = document.getElementById('btn-activate-yes'); 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 = ''; let lastVersionSelection = ''; let isDirty = false; let dirtyCleanup = null; let dialogCancelBound = false; let suppressDirty = false; let suppressTimer = null; let baselineReady = false; let versionMap = new Map(); let currentVersionId = 0; let currentVersionMeta = null; const ok  = (m) => toast(m, true); const err = (m) => toast(m, false); let lastDebugTs = 0; let lastDebugKey = ''; let lastSelectionInfo = null; const debugUiLog = (payload) => { const now = Date.now(); const key = payload && payload.event ? payload.event : 'log'; if (now - lastDebugTs < 400 && key === lastDebugKey) return; lastDebugTs = now; lastDebugKey = key; apiAction('debug.log.write', { method: 'POST', data: { name: 'ui_editor_dirty.log', append: 1, line: JSON.stringify({ time: new Date().toISOString(), ...payload, }), }, }).catch(() => {}); }; const isLibRefModel = (model) => { if (!model) return false; const attrs = typeof model.getAttributes === 'function' ? model.getAttributes() : (model.attributes && model.attributes.attributes) ? model.attributes.attributes : {}; if (attrs && (attrs['data-lib-ref'] || attrs['data-lib-kind'] || attrs['data-lib-id'])) return true; try { const classes = typeof model.getClasses === 'function' ? model.getClasses() : []; if (Array.isArray(classes) && classes.includes('lib-ref-wrapper')) return true; } catch {} return false; }; const showConfirmDialog = (() => { let dialog; let titleEl; let textEl; let btnOk; let btnCancel; let pendingResolve = null; const ensure = () => { if (dialog) return; dialog = document.createElement('dialog'); dialog.className = 'rounded-2xl p-0 w-[520px]'; dialog.innerHTML = `

`; document.body.appendChild(dialog); titleEl = dialog.querySelector('[data-confirm-title]'); textEl = dialog.querySelector('[data-confirm-text]'); btnOk = dialog.querySelector('[data-confirm-ok]'); btnCancel = dialog.querySelector('[data-confirm-cancel]'); btnOk?.addEventListener('click', () => { if (pendingResolve) pendingResolve(true); pendingResolve = null; dialog.close(); }); btnCancel?.addEventListener('click', () => { if (pendingResolve) pendingResolve(false); pendingResolve = null; dialog.close(); }); dialog.addEventListener('close', () => { if (pendingResolve) pendingResolve(false); pendingResolve = null; }); }; return async ({ title, text, html = false, confirmLabel = 'Bestätigen', cancelLabel = 'Abbrechen' }) => { ensure(); if (titleEl) titleEl.textContent = title || 'Bestätigung'; if (textEl) { if (html) { textEl.innerHTML = text || ''; } else { textEl.textContent = text || ''; } } if (btnOk) btnOk.textContent = confirmLabel; if (btnCancel) btnCancel.textContent = cancelLabel; if (!dialog.open) dialog.showModal(); return await new Promise(resolve => { pendingResolve = resolve; }); }; })(); function formatReferencesHtml(refs = []) { if (!refs.length) return ''; const lines = refs.map(ref => { const name = String(ref.name || 'Template') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); const id = Number(ref.id || 0); const versions = Array.isArray(ref.versions) && ref.versions.length ? ` – Versionen: ${ref.versions.join(', ')}` : ''; return `• ${name} #${id}${versions}`; }); return lines.join('
'); }   // ---------- 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); // restore button removed if (btnActivateVersion) btnActivateVersion.classList.toggle('hidden', !show); if (btnDeactivateVersion) btnDeactivateVersion.classList.toggle('hidden', !show); if (versionActiveBadge) versionActiveBadge.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(), }; } function getModelInfo(model) { if (!model) return null; const info = {}; try { info.id = (typeof model.getId === 'function' ? model.getId() : null) ?? (typeof model.get === 'function' ? model.get('id') : null) ?? model.id ?? null; info.type = (typeof model.get === 'function' ? model.get('type') : null) ?? (typeof model.get === 'function' ? model.get('tagName') : null) ?? (model.attributes ? model.attributes.type : null) ?? null; info.libRef = isLibRefModel(model); if (info.libRef) { const attrs = typeof model.getAttributes === 'function' ? model.getAttributes() : (model.attributes && model.attributes.attributes) ? model.attributes.attributes : {}; if (attrs) { info.lib = { ref: attrs['data-lib-ref'] ?? null, kind: attrs['data-lib-kind'] ?? null, id: attrs['data-lib-id'] ?? null, }; } } } catch {} return info; } function markDirty(reason = 'unknown', model = null, meta = {}) { const blocked = suppressDirty || !baselineReady; if (blocked) { debugUiLog({ event: 'dirty:ignored', reason, suppressDirty, baselineReady, model: getModelInfo(model), meta, selection: lastSelectionInfo, }); return; } isDirty = true; debugUiLog({ event: 'dirty:set', reason, model: getModelInfo(model), meta, selection: lastSelectionInfo, }); } function clearDirty() { isDirty = false; } function beginSuppressDirty(ms = 800) { suppressDirty = true; if (suppressTimer) clearTimeout(suppressTimer); suppressTimer = setTimeout(() => { suppressDirty = false; suppressTimer = null; }, ms); } function attachGjsDirtyTracker(editor) { if (!editor || typeof editor.on !== 'function') return () => {}; let selectionJustChanged = false; let selectionTimer = null; const onSelect = (model) => { selectionJustChanged = true; lastSelectionInfo = getModelInfo(model); debugUiLog({ event: 'component:selected', selection: lastSelectionInfo, }); if (selectionTimer) clearTimeout(selectionTimer); selectionTimer = setTimeout(() => { selectionJustChanged = false; selectionTimer = null; }, 800); }; const onUpdate = (reason, model) => { if (selectionJustChanged) { debugUiLog({ event: 'dirty:skip', reason, note: 'selectionJustChanged', model: getModelInfo(model), }); return; } markDirty(reason, model); }; const onComponentUpdate = (model) => { if (isLibRefModel(model)) { debugUiLog({ event: 'component:update:skip', reason: 'lib-ref', model: getModelInfo(model), }); return; } const changed = (model && typeof model.changedAttributes === 'function') ? model.changedAttributes() : (model && model.changed) ? model.changed : null; const keys = changed ? Object.keys(changed) : []; if (!keys.length && selectionJustChanged) { debugUiLog({ event: 'component:update:skip', reason: 'selectionJustChanged', model: getModelInfo(model), }); return; } const safeKeys = new Set(['status', 'toolbar', 'selected', 'hovered', 'highlighted', 'hoverable', 'selectable', 'editable', 'draggable', 'droppable', 'copyable', 'removable', 'locked', 'open']); if (keys.length && keys.every(k => safeKeys.has(k))) { debugUiLog({ event: 'component:update:skip', reason: 'safeKeysOnly', keys, model: getModelInfo(model), }); return; } if (selectionJustChanged && keys.length === 0) { debugUiLog({ event: 'component:update:skip', reason: 'selectionJustChangedNoKeys', model: getModelInfo(model), }); return; } markDirty('component:update', model, { keys }); }; const onComponentAdd = (model) => onUpdate('component:add', model); const onComponentRemove = (model) => onUpdate('component:remove', model); const onStyleUpdate = (model) => onUpdate('style:property:update', model); editor.on('component:update', onComponentUpdate); editor.on('component:add', onComponentAdd); editor.on('component:remove', onComponentRemove); editor.on('style:property:update', onStyleUpdate); editor.on('component:selected', onSelect); return () => { try { editor.off('component:update', onComponentUpdate); editor.off('component:add', onComponentAdd); editor.off('component:remove', onComponentRemove); editor.off('style:property:update', onStyleUpdate); editor.off('component:selected', onSelect); } catch {} }; } function attachCraftDirtyTracker() { const host = document.getElementById('craftEditor'); if (!host) return () => {}; const handler = () => markDirty('craft:input'); const events = ['input', 'keydown', 'paste', 'drop']; events.forEach(evt => host.addEventListener(evt, handler, true)); return () => { events.forEach(evt => host.removeEventListener(evt, handler, true)); }; } function getSerializedHtml(editor, win) { if (!editor) return ''; const BridgeRTE = win?.BridgeParts?.BridgeRTE || win?.BridgeRTE || null; if (BridgeRTE && typeof BridgeRTE.serializeHtml === 'function') { return BridgeRTE.serializeHtml(editor); } return (typeof editor.getHtml === 'function') ? editor.getHtml() : ''; } 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 = (getSerializedHtml(editor, win) || '') + ''; let jsonRaw = ''; try { jsonRaw = JSON.stringify(editor.getProjectData()); } catch {} return buildSnapshot({ editorType: 'grapesjs', html: htmlContent, json: jsonRaw, craftJson: '', }); } catch { return null; } } function setSavedSnapshotFromData(payload) { beginSuppressDirty(); const fields = extractContentFields(payload); savedSnapshot = buildSnapshot(fields); clearDirty(); baselineReady = false; } async function syncSnapshotFromEditor() { beginSuppressDirty(); const currentSnapshot = await buildCurrentSnapshot(); if (!currentSnapshot) return; savedSnapshot = currentSnapshot; clearDirty(); baselineReady = true; } function scheduleSnapshotSync(delay = 600) { baselineReady = false; beginSuppressDirty(); setTimeout(() => { syncSnapshotFromEditor().catch(() => {}); }, delay); } async function hasUnsavedChanges() { if (!baselineReady) return false; if (suppressDirty) return false; return isDirty; } function showUnsavedDialog() { return new Promise((resolve) => { if (!unsavedDialog || typeof unsavedDialog.showModal !== 'function') { resolve('discard'); return; } const cleanup = () => { btnUnsavedCancel && btnUnsavedCancel.removeEventListener('click', onCancel); btnUnsavedDiscard && btnUnsavedDiscard.removeEventListener('click', onDiscard); btnUnsavedSave && btnUnsavedSave.removeEventListener('click', onSave); }; const closeWith = (choice) => { cleanup(); unsavedDialog.close(); resolve(choice); }; const onCancel = () => closeWith('cancel'); const onDiscard = () => closeWith('discard'); const onSave = () => closeWith('save'); btnUnsavedCancel && btnUnsavedCancel.addEventListener('click', onCancel); btnUnsavedDiscard && btnUnsavedDiscard.addEventListener('click', onDiscard); btnUnsavedSave && btnUnsavedSave.addEventListener('click', onSave); unsavedDialog.showModal(); }); } async function confirmUnsavedChanges() { const dirty = await hasUnsavedChanges(); if (!dirty) return 'ok'; try { const currentSnapshot = await buildCurrentSnapshot(); if (currentSnapshot && savedSnapshot && currentSnapshot === savedSnapshot) { clearDirty(); return 'ok'; } } catch {} const choice = await showUnsavedDialog(); if (choice === 'save') { const okSave = await save(); if (!okSave) return 'cancel'; } return choice; } function showActivateDialog() { return new Promise((resolve) => { if (!activateDialog || typeof activateDialog.showModal !== 'function') { resolve('no'); return; } const cleanup = () => { btnActivateCancel && btnActivateCancel.removeEventListener('click', onCancel); btnActivateNo && btnActivateNo.removeEventListener('click', onNo); btnActivateYes && btnActivateYes.removeEventListener('click', onYes); }; const closeWith = (choice) => { cleanup(); activateDialog.close(); resolve(choice); }; const onCancel = () => closeWith('cancel'); const onNo = () => closeWith('no'); const onYes = () => closeWith('yes'); btnActivateCancel && btnActivateCancel.addEventListener('click', onCancel); btnActivateNo && btnActivateNo.addEventListener('click', onNo); btnActivateYes && btnActivateYes.addEventListener('click', onYes); activateDialog.showModal(); }); } function updateVersionMeta(id) { const selectedId = id || Number(versionSelect?.value || 0); const key = selectedId ? String(selectedId) : ''; currentVersionId = selectedId ? Number(selectedId) : 0; currentVersionMeta = key && versionMap.has(key) ? versionMap.get(key) : null; if (btnDeactivateVersion) { const isActive = !!(currentVersionMeta && Number(currentVersionMeta.is_active) === 1); btnDeactivateVersion.classList.toggle('hidden', !isActive); } if (btnActivateVersion) { const isActive = !!(currentVersionMeta && Number(currentVersionMeta.is_active) === 1); btnActivateVersion.classList.toggle('hidden', isActive || !currentVersionId); } if (versionActiveBadge) { const isActive = !!(currentVersionMeta && Number(currentVersionMeta.is_active) === 1); versionActiveBadge.classList.toggle('hidden', !isActive); } const win = iframe?.contentWindow; if (win && win.BridgeParts) { const canOverwrite = !!(currentVersionMeta && Number(currentVersionMeta.is_active) === 0 && Number(currentVersionMeta.was_active) === 0); win.BridgeParts.CURRENT_VERSION_ID = canOverwrite ? currentVersionId : 0; } else if (win) { const canOverwrite = !!(currentVersionMeta && Number(currentVersionMeta.is_active) === 0 && Number(currentVersionMeta.was_active) === 0); win.CURRENT_VERSION_ID = canOverwrite ? currentVersionId : 0; } } function renderVersionOptions(items, opts = {}) { const keepSelection = !!opts.keepSelection; const preferredId = opts.preferredId ? String(opts.preferredId) : ''; const selectionToKeep = preferredId || (keepSelection ? String(versionSelect?.value || lastVersionSelection || '') : ''); versionItems = items || []; versionMap = new Map(); if (!versionSelect) return ''; const rows = Array.isArray(versionItems) ? versionItems : []; versionSelect.innerHTML = ''; lastVersionSelection = ''; if (!rows.length) { const opt = document.createElement('option'); opt.value = ''; opt.textContent = 'Keine Versionen vorhanden'; opt.disabled = true; versionSelect.appendChild(opt); versionSelect.disabled = true; updateVersionMeta(0); return ''; } versionSelect.disabled = false; let activeId = ''; rows.forEach((item) => { const opt = document.createElement('option'); const label = `${Number(item.is_active) === 1 ? '✓ ' : ''}#${item.version_no} – ${formatVersionDate(item.created_at)}`; opt.value = String(item.id); opt.textContent = label; versionSelect.appendChild(opt); versionMap.set(String(item.id), item); if (Number(item.is_active) === 1 && !activeId) activeId = String(item.id); }); let fallbackId = ''; if (selectionToKeep && versionMap.has(selectionToKeep)) { fallbackId = selectionToKeep; } else { // Default to the latest version so reopening shows the most recent edits. fallbackId = (rows[0] ? String(rows[0].id) : '') || activeId; } if (fallbackId) { lastVersionSelection = fallbackId; versionSelect.value = fallbackId; updateVersionMeta(Number(fallbackId)); } return lastVersionSelection; } async function loadVersionsForCurrent(opts = {}) { if (!current?.id) { renderVersionOptions([], opts); 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'); return renderVersionOptions(Array.isArray(res?.items) ? res.items : [], opts); } catch { renderVersionOptions([], opts); return ''; } } async function applyVersionPayload(payload) { baselineReady = false; beginSuppressDirty(); const data = extractContentFields(payload); const targetType = data.editorType === 'craftjs' ? 'craftjs' : 'grapesjs'; setEditorType(targetType); if (targetType === 'craftjs') { craftEditor?.setContent(data.html || '', data.craftJson || ''); setSavedSnapshotFromData(payload); if (dirtyCleanup) dirtyCleanup(); dirtyCleanup = attachCraftDirtyTracker(); scheduleSnapshotSync(300); return; } const editor = await waitForEditor(3000); const jsonRaw = normalizeSnapshotValue(data.json).trim(); if (jsonRaw) { try { const project = JSON.parse(jsonRaw); editor.loadProjectData(project); setSavedSnapshotFromData(payload); if (dirtyCleanup) dirtyCleanup(); dirtyCleanup = attachGjsDirtyTracker(editor); scheduleSnapshotSync(300); return; } catch {} } const html = normalizeSnapshotValue(data.html); editor.setComponents(html); setSavedSnapshotFromData(payload); if (dirtyCleanup) dirtyCleanup(); dirtyCleanup = attachGjsDirtyTracker(editor); scheduleSnapshotSync(300); } async function loadLatestContentFromServer() { baselineReady = false; beginSuppressDirty(); const res = await apiAction('content.get', { method: 'GET', data: { id: current.id, section_id: current.section.id } }); await applyVersionPayload(res || {}); scheduleSnapshotSync(300); }   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 = getSerializedHtml(ed, win); 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 = `      
       
       
Lade Editor …
     
          `;     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); if (versionSelect) versionSelect.value = ''; lastVersionSelection = '';     // Neuen Token erzeugen & alten Listener entfernen     reqToken++;     const myToken = reqToken;     if (bridgeListener) window.removeEventListener('message', bridgeListener);     bridgeListener = null;     // Overlay zeigen     showVeil(); const requestedVersionId = Number(item?.version_id || item?.versionId || 0); // 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 = ''; let defaultVersionId = '';     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() => { defaultVersionId = await loadVersionsForCurrent(); })()     ]); const effectiveVersionId = requestedVersionId ? String(requestedVersionId) : defaultVersionId; if (effectiveVersionId) { try { const res = await apiAction('content_versions.get', { method: 'GET', data: { id: effectiveVersionId, content_id: current.id } }); if (res?.ok && res?.item) { const fields = extractContentFields(res.item); fresh = fields.html || ''; jsonState = fields.json || ''; editorType = fields.editorType || editorType; craftJson = fields.craftJson || ''; updateVersionMeta(Number(effectiveVersionId)); lastVersionSelection = String(effectiveVersionId); if (versionSelect) versionSelect.value = lastVersionSelection; } } catch {} } editorType = editorType === 'craftjs' ? 'craftjs' : 'grapesjs'; setSavedSnapshotFromData({ html: fresh, content: jsonState, editor_type: editorType, craft_json: craftJson }); setEditorType(editorType); scheduleSnapshotSync(1200); if (editorType === 'craftjs') { const craftHtml = extractCraftHtml(craftJson, fresh); beginSuppressDirty(); craftEditor?.setContent(craftHtml, craftJson); if (dirtyCleanup) dirtyCleanup(); dirtyCleanup = attachCraftDirtyTracker(); scheduleSnapshotSync(300); 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?.(); if (dlg && !dialogCancelBound) { dlg.addEventListener('cancel', async (ev) => { ev.preventDefault(); await close(); }); dialogCancelBound = true; } waitForEditor(6000) .then((ed) => { if (dirtyCleanup) dirtyCleanup(); beginSuppressDirty(); dirtyCleanup = attachGjsDirtyTracker(ed); scheduleSnapshotSync(1200); }) .catch(() => {}); } // ---------- Speichern (DELEGIERT) ---------- // 🚨 KORRIGIERT: Delegiert Speichern an den iFrame, der die JSON-Daten holt! async function save() { if (!current?.id) return err('Keine aktive ID'); let activateNext = false; if (currentVersionMeta && (Number(currentVersionMeta.is_active) === 1 || Number(currentVersionMeta.was_active) === 1)) { const decision = await showActivateDialog(); if (decision === 'cancel') return false; activateNext = decision === 'yes'; } const overwriteVersionId = (currentVersionMeta && Number(currentVersionMeta.is_active) === 0 && Number(currentVersionMeta.was_active) === 0) ? Number(currentVersionId || 0) : 0; 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 }; if (activateNext) payload.activate_version = 1; if (overwriteVersionId) payload.version_id = overwriteVersionId; 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; } if (activateNext) { const win = iframe?.contentWindow; if (win && win.BridgeParts) { win.BridgeParts.NEXT_ACTIVATE_VERSION = 1; } else if (win) { win.NEXT_ACTIVATE_VERSION = 1; } } if (overwriteVersionId) { const win = iframe?.contentWindow; if (win && win.BridgeParts) { win.BridgeParts.CURRENT_VERSION_ID = overwriteVersionId; } else if (win) { win.CURRENT_VERSION_ID = overwriteVersionId; } } 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 decision = await confirmUnsavedChanges(); if (decision === 'cancel') return; if (dirtyCleanup) dirtyCleanup(); dirtyCleanup = null; // 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; clearDirty(); } 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); } } function isTemplateSection() { if (current?.section?.is_template) return true; const slug = (current?.section?.slug || '').toString().toLowerCase(); return slug === 'emailtemplate' || slug.includes('template'); } async function confirmTemplateReferences(actionLabel) { if (!current?.id || !isTemplateSection()) return true; const res = await apiAction('templates.references', { method: 'GET', data: { template_id: current.id } }).catch(() => null); if (!res || res.ok === false) { return await showConfirmDialog({ title: 'Referenzen nicht geprüft', text: `Referenzen konnten nicht geprüft werden. ${actionLabel} trotzdem?`, confirmLabel: actionLabel, }); } const refs = Array.isArray(res?.references) ? res.references : []; if (!refs.length) return true; const preview = refs.slice(0, 6).map(r => `${r.name || 'Template'} #${r.id}`).join(', '); const more = refs.length > 6 ? ` und ${refs.length - 6} weitere` : ''; const escPreview = preview .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); const escMore = more .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); return await showConfirmDialog({ title: 'Template wird verwendet', html: true, text: `Dieses Template wird in ${refs.length} anderen Template(s) verwendet (${escPreview}${escMore}).
${formatReferencesHtml(refs)}
${actionLabel} trotzdem?`, confirmLabel: actionLabel, }); } // 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)); btnActivateVersion && (btnActivateVersion.onclick = async () => { const selectedId = Number(versionSelect?.value || currentVersionId || 0); if (!current?.id || !selectedId) return; try { const res = await apiAction('content_versions.activate', { method: 'POST', data: { id: selectedId } }); if (!res?.ok) throw new Error(res?.error || 'Aktivieren fehlgeschlagen'); await loadVersionsForCurrent(); toast('Version aktiviert', true); } catch (e) { err(e.message || 'Aktivieren fehlgeschlagen'); } }); btnDeactivateVersion && (btnDeactivateVersion.onclick = async () => { if (!current?.id) return; if (!currentVersionMeta || Number(currentVersionMeta.is_active) !== 1) return; try { const okRefs = await confirmTemplateReferences('Deaktivieren'); if (!okRefs) return; let res = await apiAction('content_versions.deactivate', { method: 'POST', data: { content_id: current.id } }); if (res && res.ok === false && Array.isArray(res.references) && res.references.length) { const refs = res.references || []; const ok = await showConfirmDialog({ title: 'Template wird verwendet', html: true, text: `Dieses Template wird in anderen Templates verwendet.
${formatReferencesHtml(refs)}
Deaktivieren trotzdem?`, confirmLabel: 'Deaktivieren', }); if (!ok) return; res = await apiAction('content_versions.deactivate', { method: 'POST', data: { content_id: current.id, force: 1 } }); } if (!res?.ok) throw new Error(res?.error || 'Deaktivieren fehlgeschlagen'); await loadVersionsForCurrent({ keepSelection: true, preferredId: currentVersionId }); toast('Aktive Version deaktiviert', true); } catch (e) { err(e.message || 'Deaktivieren fehlgeschlagen'); } }); versionSelect && (versionSelect.onchange = async () => { if (!current?.id) return; const previousSelection = lastVersionSelection; const decision = await confirmUnsavedChanges(); if (decision === 'cancel') { versionSelect.value = previousSelection; return; } const versionId = Number(versionSelect.value || 0); if (!versionId) { versionSelect.value = previousSelection; 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); lastVersionSelection = String(versionId); updateVersionMeta(versionId); } catch (e) { err(e.message || 'Version konnte nicht geladen werden'); versionSelect.value = previousSelection; } }); // restore button removed 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;