439 lines
18 KiB
JavaScript
439 lines
18 KiB
JavaScript
/* /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 }) {
|
||
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 = `<!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;
|