/* /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 = `
`;
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 = `${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;