Files
emailtemplate.it/public/assets/js/ui-editor.js
2026-01-26 00:18:16 +01:00

1154 lines
42 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/* /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);
  // ---------- 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 markDirty() {
if (suppressDirty || !baselineReady) return;
isDirty = true;
}
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 () => {};
const onUpdate = () => markDirty();
editor.on('update', onUpdate);
editor.on('component:update', onUpdate);
return () => {
try {
editor.off('update', onUpdate);
editor.off('component:update', onUpdate);
} catch {}
};
}
function attachCraftDirtyTracker() {
const host = document.getElementById('craftEditor');
if (!host) return () => {};
const handler = () => markDirty();
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) || '') + '<style>' + cssPayload + '</style>';
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';
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 {
fallbackId = activeId || (rows[0] ? String(rows[0].id) : '');
}
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 = `<!doctype html><html>
      <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
      <body id="gjs">${html || ''}</body>
    </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 ? `<style>${css}</style>\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 = `
      <div style="display:flex;flex-direction:column;gap:.6rem;align-items:center;">
        <div class="spinner" style="width:28px;height:28px;border-radius:999px;border:3px solid #cbd5e1;border-top-color:#0ea5e9;animation:spin .8s linear infinite"></div>
        <div style="font-weight:500;">Lade Editor …</div>
      </div>
      <style>@keyframes spin{to{transform:rotate(360deg)}}</style>
    `;
    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 = '<option value="">Standard (System)</option>';
    senderOptions.forEach(opt => {
      const label = opt.label || opt.from_name || opt.from_email;
      html += `<option value="${opt.id}">${escapeHtml(label)} &lt;${escapeHtml(opt.from_email)}&gt;</option>`;
    });
    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}&section_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 = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${html || '<em>(leer)</em>'}</body></html>`;
    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);
}
}
// 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 res = await apiAction('content_versions.deactivate', { method: 'POST', data: { content_id: current.id } });
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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}
// Default-Export + globaler Fallback
export default initEditor;
window.initEditor = initEditor;