Files
emailtemplate.it/public/assets/js/ui-editor.js
2025-12-06 01:22:42 +01:00

443 lines
18 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, 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 = `<!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() {
    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 ? `<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);
  }
  
  // ... (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'; }
  // ... (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, hasJson }) {
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 }, '*');
    } 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: [] };
let hasJson = false;
    await Promise.all([
      (async() => {
try {
const row = await apiGet(current.resource, current.id);
hasJson = !!(row?.content);
// 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,
hasJson
});
}
};
      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,
hasJson
});
}, 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 = `<!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() {
    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;