/* /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, apiList, apiGet, toast, apiAction } from './api.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 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 btnCancelSend= document.getElementById('btn-cancel-send');   const btnSendNow   = document.getElementById('btn-send-now');   const prevFrame    = document.getElementById('previewFrame');   const btnPrevClose = document.getElementById('btn-close-preview');   let current = null;   // { resource, id, name }   let bridgeListener = null;   let reqToken = 0;     // steigender Token pro Öffnen -> ignoriert verspätete Events   const ok  = (m) => toast(m, true);   const err = (m) => toast(m, false);   // ---------- Hilfen ----------   function activeMode() {     const b = document.querySelector('nav [data-tab].bg-sky-50, nav [data-tab].text-sky-700, nav [data-tab].active');     return (b?.dataset?.tab) || (current?.resource) || 'templates';   }   function writeHtmlToFrame(html) {     iframe.srcdoc = `             ${html || ''}     `;   }   async function readEditedHtml() {     const win = iframe?.contentWindow;     const doc = iframe?.contentDocument;     if (!win || !doc) return '';     const ed = win.__gjs || (win.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;     if (ed && typeof ed.getHtml === 'function') {       const html = ed.getHtml();       const css  = (typeof ed.getCss === 'function') ? ed.getCss() : '';       return css ? `\n${html}` : html;     }     const root = doc.querySelector('#gjs') || doc.body || doc.documentElement;     return root ? root.innerHTML : '';   }   function waitForEditor(maxMs = 8000) {     return new Promise((resolve, reject) => {       const start = Date.now();       (function poll() {         const win = iframe?.contentWindow;         const ed  = win?.__gjs || (win?.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;         if (ed) return resolve(ed);         if (Date.now() - start > maxMs) return reject(new Error('Editor not ready'));         setTimeout(poll, 120);       })();     });   }      // 🚨 NEUE FUNKTION: Delegiert das Kommando an den Editor im iFrame   async function delegateCommand(commandName) {     try {       const editor = await waitForEditor(3000);       if (editor.Commands.has(commandName)) {         // Führt den Command im iFrame aus (z.B. 'save-data')         editor.runCommand(commandName);         return true;       } else {         err(`Delegieren fehlgeschlagen: Command '${commandName}' nicht gefunden.`);         return false;       }     } catch (e) {       err(`Delegieren fehlgeschlagen: Editor nicht bereit (${commandName}).`);       console.error(e);       return false;     }   }   // ... (hideReadyBadge bleibt unverändert) ...   function hideReadyBadge(doc) {     if (!doc) return;     const kill = () => {       const el = doc.getElementById('badge');       if (el) el.style.display = 'none';     };     kill();     const style = doc.createElement('style');     style.textContent = `       #badge { display:none !important; }       .gjs-pn-status { display:none !important; }       .ready-badge,       .status-badge.ready,       [data-status="ready"],       [data-badge="ready"],       .gjs-ready,       .gjs-badge-ready { display:none !important; }     `;     doc.head.appendChild(style);     const mo = new MutationObserver(() => { kill(); /* hideByText(doc); */ });     mo.observe(doc.documentElement, { childList: true, subtree: true });     setTimeout(() => { kill(); /* hideByText(doc); */ }, 150);     setTimeout(() => { kill(); /* hideByText(doc); */ }, 500);     setTimeout(() => { kill(); /* hideByText(doc); */ }, 1200);   }      // ... (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'; }   // ... (Kontext-Filter-Ladung bleibt unverändert) ...   async function listBlocksForTemplate(templateId){     try {       const direct = await apiList('blocks', { template_id: templateId });       if (Array.isArray(direct) && direct.length) return direct;     } catch {}     const sections = await apiList('sections', { template_id: templateId }).catch(()=>[]);     const out = [];     for (const s of (sections || [])) {       const b = await apiList('blocks', { section_id: s.id }).catch(()=>[]);       if (b?.length) out.push(...b);     }     return out;   }   // Snippets eines Templates (direkt oder via Sections->Blocks als Fallback)   async function listSnippetsForTemplate(templateId){     try {       const direct = await apiList('snippets', { template_id: templateId });       if (Array.isArray(direct) && direct.length) return direct;     } catch {}     const sections = await apiList('sections', { template_id: templateId }).catch(()=>[]);     const blocksAll = [];     for (const s of (sections || [])) {       const b = await apiList('blocks', { section_id: s.id }).catch(()=>[]);       if (b?.length) blocksAll.push(...b);     }     const out = [];     for (const b of blocksAll) {       const sn = await apiList('snippets', { block_id: b.id }).catch(()=>[]);       if (sn?.length) out.push(...sn);     }     return out;   }   // Referenz-Bibliothek (für „Custom – Fix“)   async function buildRefLibForContext(ctx){     const kind = (ctx.resource || 'templates').replace(/s$/,''); // template|section|block     const id   = ctx.id;     if (kind === 'template'){       const [sections, blocks] = await Promise.all([         apiList('sections', { template_id: id }).catch(()=>[]),         listBlocksForTemplate(id)       ]);       return { sections, blocks };     }     if (kind === 'section'){       const blocks = await apiList('blocks', { section_id: id }).catch(()=>[]);       return { sections: [], blocks };     }     return { sections: [], blocks: [] }; // block -> keine Sections/Blocks in Fix   }   // Snippets (für „Custom – Flex“) kontextabhängig   async function buildSnippetsForContext(ctx){     const kind = (ctx.resource || 'templates').replace(/s$/,'');     const id   = ctx.id;     let rows = [];     if (kind === 'template')      rows = await listSnippetsForTemplate(id);     else if (kind === 'section')  rows = await apiList('snippets', { section_id: id }).catch(()=>[]);     else if (kind === 'block')    rows = await apiList('snippets', { block_id: id }).catch(()=>[]);     else                          rows = await apiList('snippets').catch(()=>[]);     return (rows || []).map(r => ({ id: r.id, name: r.name, html: r.content || r.html || '' }));   }   // ---------- Initialen HTML-Inhalt in Editor pushen (mit Token/Race-Schutz) ----------   async function pushInitialHtmlToEditor({ mode, html, snippets, ref, token }) {     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 || {} }, '*');     } 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) {     current = {       resource: String(resource || activeMode() || 'templates').toLowerCase(),       id: Number(item?.id || 0),       name: item?.name || ''     };     if (!current.id) return err('Ungültige ID');     // globaler Kontext     window.__currentItemId    = current.id;     window.__currentEditorCtx = { id: current.id, mode: current.resource };     // Neuen Token erzeugen & alten Listener entfernen     reqToken++;     const myToken = reqToken;     if (bridgeListener) window.removeEventListener('message', bridgeListener);     bridgeListener = null;     // Overlay zeigen     showVeil();     // Daten parallel laden (fresh HTML + kontextgefilterte Snippets + Referenzen)     let fresh = '';     let snippets = [];     let refLib = { sections: [], blocks: [] };     await Promise.all([       (async() => {         try {           const row = await apiGet(current.resource, current.id);           // API liefert jetzt top-level html/content; fallback auf item.*           fresh = row?.html ?? row?.content ?? row?.item?.html ?? row?.item?.content ?? '';         } catch {}       })(),       (async() => { snippets = await buildSnippetsForContext(current); })(),       (async() => { refLib   = await buildRefLibForContext(current); })()     ]);     // 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;         // 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.resource);           } 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.resource,             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           });         }       };       window.addEventListener('message', bridgeListener);       // Fallback, falls kein Ready ankommt       setTimeout(() => {         pushInitialHtmlToEditor({           mode: current.resource,           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         });       }, 1200);     };     // Jetzt den Editor-Core laden (erst NACH about:blank)     iframe.src = `editor/editor-core.php?mode=${encodeURIComponent(current.resource)}&id=${current.id}&t=${Date.now()}`;     dlg?.showModal?.();   }   // ---------- Speichern (DELEGIERT) ----------   // 🚨 KORRIGIERT: Delegiert Speichern an den iFrame, der die JSON-Daten holt!   async function save() {     if (!current?.id) return err('Keine aktive ID');     const mode = activeMode();     if (mode !== 'snippets') { // Nur Templates/Blocks/Sections delegieren, Snippets behalten die alte Logik (NUR HTML)       return delegateCommand('save-data');     }          // Alte Snippet-Logik beibehalten (falls der Snippet-Editor nicht GrapesJS ist und nur HTML erwartet)     const liveHtml = await readEditedHtml();     const payload = { id: current.id, content: liveHtml };     const res = await apiUpdate(mode, current.id, payload);     if (!res?.ok) { err('Speichern fehlgeschlagen'); return; }     ok('Gespeichert');     try {       if (typeof window.reloadActiveList === 'function') await window.reloadActiveList();       else if (typeof window.__reloadList === 'function') window.__reloadList(mode);     } catch {}   }   // ... (Der Rest der Funktionen bleibt unverändert) ...   async function clearEditor() {     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() {     sendSubject.value = 'Testversand';     sendTo.value = '';     sendDlg?.showModal?.();   }   function closeSend(){ sendDlg?.close?.(); }   async function doSend(ev){     ev?.preventDefault?.();     const to = sendTo.value.trim();     if(!to){ toast("Bitte Empfänger angeben", false); 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 r = await apiAction('templates.test_send', { method:'POST', data:{ template_id: id, to, subject: sendSubject.value || 'Testversand' } });     if(r?.ok){ toast("Testversand ausgelöst"); closeSend(); } else { toast("Senden fehlgeschlagen", false); }   }   function closePreview(){ prevDlg?.close?.(); }   function close() {     // 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;   }   // 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);   // Public API   window.EditorUI = { open, save, close, clear: clearEditor, preview: openPreview }; } // Default-Export + globaler Fallback export default initEditor; window.initEditor = initEditor;