commit
This commit is contained in:
438
public/assets/js/ui-editor.js
Normal file
438
public/assets/js/ui-editor.js
Normal file
@@ -0,0 +1,438 @@
|
||||
/* /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;
|
||||
Reference in New Issue
Block a user