/* /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 () => {}; let selectionJustChanged = false; let selectionTimer = null; const onSelect = () => { selectionJustChanged = true; if (selectionTimer) clearTimeout(selectionTimer); selectionTimer = setTimeout(() => { selectionJustChanged = false; selectionTimer = null; }, 800); }; const onUpdate = () => { if (selectionJustChanged) return; markDirty(); }; const onComponentUpdate = (model) => { 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) return; const safeKeys = new Set(['status', 'toolbar', 'selected', 'hovered', 'highlighted', 'hoverable', 'selectable', 'editable', 'draggable', 'droppable', 'copyable', 'removable', 'locked']); if (keys.length && keys.every(k => safeKeys.has(k))) return; if (selectionJustChanged && keys.length === 0) return; markDirty(); }; editor.on('update', onUpdate); editor.on('component:update', onComponentUpdate); editor.on('component:selected', onSelect); return () => { try { editor.off('update', onUpdate); editor.off('component:update', onComponentUpdate); editor.off('component:selected', onSelect); } 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) || '') + ''; 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 = `