1495 lines
54 KiB
JavaScript
1495 lines
54 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, toast, apiAction } from './api.js';
|
||
import { initCraftEditor } from './craft-editor.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 editorSelect = document.getElementById('editorTypeSelect');
|
||
const versionSelect = document.getElementById('versionSelect');
|
||
const craftEditor = initCraftEditor();
|
||
|
||
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 sendInfo = document.getElementById('send_template_info');
|
||
const btnCancelSend= document.getElementById('btn-cancel-send');
|
||
const btnSendNow = document.getElementById('btn-send-now');
|
||
const sendSender = document.getElementById('send_sender');
|
||
const sendSenderHint = document.getElementById('send_sender_hint');
|
||
const prevFrame = document.getElementById('previewFrame');
|
||
const btnPrevClose = document.getElementById('btn-close-preview');
|
||
const unsavedDialog = document.getElementById('unsavedDialog');
|
||
const btnUnsavedCancel = document.getElementById('btn-unsaved-cancel');
|
||
const btnUnsavedDiscard = document.getElementById('btn-unsaved-discard');
|
||
const btnUnsavedSave = document.getElementById('btn-unsaved-save');
|
||
const btnActivateVersion = document.getElementById('btn-activate-version');
|
||
const btnDeactivateVersion = document.getElementById('btn-deactivate-version');
|
||
const versionActiveBadge = document.getElementById('versionActiveBadge');
|
||
const activateDialog = document.getElementById('activateVersionDialog');
|
||
const btnActivateCancel = document.getElementById('btn-activate-cancel');
|
||
const btnActivateNo = document.getElementById('btn-activate-no');
|
||
const btnActivateYes = document.getElementById('btn-activate-yes');
|
||
|
||
let current = null; // { resource, id, name, section }
|
||
let bridgeListener = null;
|
||
let reqToken = 0; // steigender Token pro Öffnen -> ignoriert verspätete Events
|
||
let senderOptions = [];
|
||
let senderLoadPromise = null;
|
||
let currentEditorType = 'grapesjs';
|
||
let versionItems = [];
|
||
let savedSnapshot = '';
|
||
let lastVersionSelection = '';
|
||
let isDirty = false;
|
||
let dirtyCleanup = null;
|
||
let dialogCancelBound = false;
|
||
let suppressDirty = false;
|
||
let suppressTimer = null;
|
||
let baselineReady = false;
|
||
let versionMap = new Map();
|
||
let currentVersionId = 0;
|
||
let currentVersionMeta = null;
|
||
let pendingSaveResolve = null;
|
||
let pendingSaveTimer = null;
|
||
|
||
const ok = (m) => toast(m, true);
|
||
const err = (m) => toast(m, false);
|
||
|
||
let lastDebugTs = 0;
|
||
let lastDebugKey = '';
|
||
let lastSelectionInfo = null;
|
||
|
||
const debugUiLog = (payload) => {
|
||
const now = Date.now();
|
||
const key = payload && payload.event ? payload.event : 'log';
|
||
if (now - lastDebugTs < 400 && key === lastDebugKey) return;
|
||
lastDebugTs = now;
|
||
lastDebugKey = key;
|
||
apiAction('debug.log.write', {
|
||
method: 'POST',
|
||
data: {
|
||
name: 'ui_editor_dirty.log',
|
||
append: 1,
|
||
line: JSON.stringify({
|
||
time: new Date().toISOString(),
|
||
...payload,
|
||
}),
|
||
},
|
||
}).catch(() => {});
|
||
};
|
||
|
||
const isLibRefModel = (model) => {
|
||
if (!model) return false;
|
||
const attrs = typeof model.getAttributes === 'function'
|
||
? model.getAttributes()
|
||
: (model.attributes && model.attributes.attributes) ? model.attributes.attributes : {};
|
||
if (attrs && (attrs['data-lib-ref'] || attrs['data-lib-kind'] || attrs['data-lib-id'])) return true;
|
||
try {
|
||
const classes = typeof model.getClasses === 'function' ? model.getClasses() : [];
|
||
if (Array.isArray(classes) && classes.includes('lib-ref-wrapper')) return true;
|
||
} catch {}
|
||
return false;
|
||
};
|
||
|
||
const showConfirmDialog = (() => {
|
||
let dialog;
|
||
let titleEl;
|
||
let textEl;
|
||
let btnOk;
|
||
let btnCancel;
|
||
let pendingResolve = null;
|
||
|
||
const ensure = () => {
|
||
if (dialog) return;
|
||
dialog = document.createElement('dialog');
|
||
dialog.className = 'rounded-2xl p-0 w-[520px]';
|
||
dialog.innerHTML = `
|
||
<div class="p-4 bg-white rounded-2xl space-y-4">
|
||
<h3 class="text-lg font-semibold" data-confirm-title></h3>
|
||
<p class="text-sm text-slate-600" data-confirm-text></p>
|
||
<div class="flex justify-end gap-2">
|
||
<button type="button" class="btn" data-confirm-cancel>Abbrechen</button>
|
||
<button type="button" class="btn btn-danger" data-confirm-ok>Bestätigen</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(dialog);
|
||
titleEl = dialog.querySelector('[data-confirm-title]');
|
||
textEl = dialog.querySelector('[data-confirm-text]');
|
||
btnOk = dialog.querySelector('[data-confirm-ok]');
|
||
btnCancel = dialog.querySelector('[data-confirm-cancel]');
|
||
|
||
btnOk?.addEventListener('click', () => {
|
||
if (pendingResolve) pendingResolve(true);
|
||
pendingResolve = null;
|
||
dialog.close();
|
||
});
|
||
btnCancel?.addEventListener('click', () => {
|
||
if (pendingResolve) pendingResolve(false);
|
||
pendingResolve = null;
|
||
dialog.close();
|
||
});
|
||
dialog.addEventListener('close', () => {
|
||
if (pendingResolve) pendingResolve(false);
|
||
pendingResolve = null;
|
||
});
|
||
};
|
||
|
||
return async ({ title, text, html = false, confirmLabel = 'Bestätigen', cancelLabel = 'Abbrechen' }) => {
|
||
ensure();
|
||
if (titleEl) titleEl.textContent = title || 'Bestätigung';
|
||
if (textEl) {
|
||
if (html) {
|
||
textEl.innerHTML = text || '';
|
||
} else {
|
||
textEl.textContent = text || '';
|
||
}
|
||
}
|
||
if (btnOk) btnOk.textContent = confirmLabel;
|
||
if (btnCancel) btnCancel.textContent = cancelLabel;
|
||
if (!dialog.open) dialog.showModal();
|
||
return await new Promise(resolve => {
|
||
pendingResolve = resolve;
|
||
});
|
||
};
|
||
})();
|
||
|
||
function formatReferencesHtml(refs = []) {
|
||
if (!refs.length) return '';
|
||
const lines = refs.map(ref => {
|
||
const name = String(ref.name || 'Template')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
const id = Number(ref.id || 0);
|
||
const versions = Array.isArray(ref.versions) && ref.versions.length
|
||
? ` – Versionen: ${ref.versions.join(', ')}`
|
||
: '';
|
||
return `• ${name} #${id}${versions}`;
|
||
});
|
||
return lines.join('<br>');
|
||
}
|
||
|
||
// ---------- Hilfen ----------
|
||
function activeMode() {
|
||
const activeSection = window.__activeSection || current?.section || null;
|
||
return (activeSection?.slug) || (current?.resource) || 'emailtemplate';
|
||
}
|
||
|
||
function setSendContext(id, name = '') {
|
||
if (sendDlg) {
|
||
if (id) {
|
||
sendDlg.dataset.templateId = String(id);
|
||
} else {
|
||
delete sendDlg.dataset.templateId;
|
||
}
|
||
if (name) sendDlg.dataset.templateName = name;
|
||
}
|
||
if (sendInfo) {
|
||
if (!id) {
|
||
sendInfo.textContent = 'Kein Template ausgewählt.';
|
||
sendInfo.classList.add('text-rose-600');
|
||
} else {
|
||
const label = name ? `${name} – Template #${id}` : `Template #${id}`;
|
||
sendInfo.textContent = label;
|
||
sendInfo.classList.remove('text-rose-600');
|
||
}
|
||
}
|
||
}
|
||
|
||
function formatVersionDate(value) {
|
||
if (!value) return '';
|
||
try {
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return value;
|
||
return date.toLocaleString('de-DE');
|
||
} catch {
|
||
return value;
|
||
}
|
||
}
|
||
|
||
function setVersionUiVisible(show) {
|
||
if (versionSelect) versionSelect.classList.toggle('hidden', !show);
|
||
// restore button removed
|
||
if (btnActivateVersion) btnActivateVersion.classList.toggle('hidden', !show);
|
||
if (btnDeactivateVersion) btnDeactivateVersion.classList.toggle('hidden', !show);
|
||
if (versionActiveBadge) versionActiveBadge.classList.toggle('hidden', !show);
|
||
}
|
||
|
||
setVersionUiVisible(false);
|
||
|
||
function normalizeSnapshotValue(value) {
|
||
return value === null || value === undefined ? '' : String(value);
|
||
}
|
||
|
||
function buildSnapshot({ editorType, html, json, craftJson }) {
|
||
return JSON.stringify({
|
||
editorType: normalizeSnapshotValue(editorType),
|
||
html: normalizeSnapshotValue(html),
|
||
json: normalizeSnapshotValue(json),
|
||
craftJson: normalizeSnapshotValue(craftJson),
|
||
});
|
||
}
|
||
|
||
function extractContentFields(payload = {}) {
|
||
const base = payload || {};
|
||
const item = base.item || {};
|
||
return {
|
||
html: base.html ?? base.html_content ?? item.html ?? item.html_content ?? '',
|
||
json: base.content ?? base.json_content ?? item.content ?? item.json_content ?? '',
|
||
craftJson: base.craft_json ?? item.craft_json ?? '',
|
||
editorType: String(base.editor_type ?? item.editor_type ?? currentEditorType ?? 'grapesjs').toLowerCase(),
|
||
};
|
||
}
|
||
|
||
function getModelInfo(model) {
|
||
if (!model) return null;
|
||
const info = {};
|
||
try {
|
||
info.id = (typeof model.getId === 'function' ? model.getId() : null)
|
||
?? (typeof model.get === 'function' ? model.get('id') : null)
|
||
?? model.id
|
||
?? null;
|
||
info.type = (typeof model.get === 'function' ? model.get('type') : null)
|
||
?? (typeof model.get === 'function' ? model.get('tagName') : null)
|
||
?? (model.attributes ? model.attributes.type : null)
|
||
?? null;
|
||
info.libRef = isLibRefModel(model);
|
||
if (info.libRef) {
|
||
const attrs = typeof model.getAttributes === 'function'
|
||
? model.getAttributes()
|
||
: (model.attributes && model.attributes.attributes) ? model.attributes.attributes : {};
|
||
if (attrs) {
|
||
info.lib = {
|
||
ref: attrs['data-lib-ref'] ?? null,
|
||
kind: attrs['data-lib-kind'] ?? null,
|
||
id: attrs['data-lib-id'] ?? null,
|
||
};
|
||
}
|
||
}
|
||
} catch {}
|
||
return info;
|
||
}
|
||
|
||
function markDirty(reason = 'unknown', model = null, meta = {}) {
|
||
const blocked = suppressDirty || !baselineReady;
|
||
if (blocked) {
|
||
debugUiLog({
|
||
event: 'dirty:ignored',
|
||
reason,
|
||
suppressDirty,
|
||
baselineReady,
|
||
model: getModelInfo(model),
|
||
meta,
|
||
selection: lastSelectionInfo,
|
||
});
|
||
return;
|
||
}
|
||
isDirty = true;
|
||
debugUiLog({
|
||
event: 'dirty:set',
|
||
reason,
|
||
model: getModelInfo(model),
|
||
meta,
|
||
selection: lastSelectionInfo,
|
||
});
|
||
}
|
||
|
||
function clearDirty() {
|
||
isDirty = false;
|
||
}
|
||
|
||
function beginSuppressDirty(ms = 800) {
|
||
suppressDirty = true;
|
||
if (suppressTimer) clearTimeout(suppressTimer);
|
||
suppressTimer = setTimeout(() => {
|
||
suppressDirty = false;
|
||
suppressTimer = null;
|
||
}, ms);
|
||
}
|
||
|
||
function attachGjsDirtyTracker(editor) {
|
||
if (!editor || typeof editor.on !== 'function') return () => {};
|
||
let selectionJustChanged = false;
|
||
let selectionTimer = null;
|
||
const onSelect = (model) => {
|
||
selectionJustChanged = true;
|
||
lastSelectionInfo = getModelInfo(model);
|
||
debugUiLog({
|
||
event: 'component:selected',
|
||
selection: lastSelectionInfo,
|
||
});
|
||
if (selectionTimer) clearTimeout(selectionTimer);
|
||
selectionTimer = setTimeout(() => {
|
||
selectionJustChanged = false;
|
||
selectionTimer = null;
|
||
}, 800);
|
||
};
|
||
const onUpdate = (reason, model) => {
|
||
if (reason === 'style:property:update') {
|
||
const isComponentModel = !!(model && (typeof model.getId === 'function' || (model.attributes && (model.attributes.tagName || model.attributes.type))));
|
||
if (!isComponentModel) {
|
||
debugUiLog({
|
||
event: 'dirty:skip',
|
||
reason,
|
||
note: 'style:update-non-component',
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
if (reason === 'style:property:update' && !model) {
|
||
debugUiLog({
|
||
event: 'dirty:skip',
|
||
reason,
|
||
note: 'no-model',
|
||
});
|
||
return;
|
||
}
|
||
if (selectionJustChanged) {
|
||
debugUiLog({
|
||
event: 'dirty:skip',
|
||
reason,
|
||
note: 'selectionJustChanged',
|
||
model: getModelInfo(model),
|
||
});
|
||
return;
|
||
}
|
||
markDirty(reason, model);
|
||
};
|
||
const onComponentUpdate = (model) => {
|
||
if (isLibRefModel(model)) {
|
||
debugUiLog({
|
||
event: 'component:update:skip',
|
||
reason: 'lib-ref',
|
||
model: getModelInfo(model),
|
||
});
|
||
return;
|
||
}
|
||
const changed = (model && typeof model.changedAttributes === 'function')
|
||
? model.changedAttributes()
|
||
: (model && model.changed) ? model.changed : null;
|
||
const keys = changed ? Object.keys(changed) : [];
|
||
if (!keys.length && selectionJustChanged) {
|
||
debugUiLog({
|
||
event: 'component:update:skip',
|
||
reason: 'selectionJustChanged',
|
||
model: getModelInfo(model),
|
||
});
|
||
return;
|
||
}
|
||
const safeKeys = new Set(['status', 'toolbar', 'selected', 'hovered', 'highlighted', 'hoverable', 'selectable', 'editable', 'draggable', 'droppable', 'copyable', 'removable', 'locked', 'open']);
|
||
if (keys.length && keys.every(k => safeKeys.has(k))) {
|
||
debugUiLog({
|
||
event: 'component:update:skip',
|
||
reason: 'safeKeysOnly',
|
||
keys,
|
||
model: getModelInfo(model),
|
||
});
|
||
return;
|
||
}
|
||
if (selectionJustChanged && keys.length === 0) {
|
||
debugUiLog({
|
||
event: 'component:update:skip',
|
||
reason: 'selectionJustChangedNoKeys',
|
||
model: getModelInfo(model),
|
||
});
|
||
return;
|
||
}
|
||
markDirty('component:update', model, { keys });
|
||
};
|
||
const onComponentAdd = (model) => onUpdate('component:add', model);
|
||
const onComponentRemove = (model) => onUpdate('component:remove', model);
|
||
const onStyleUpdate = (model) => onUpdate('style:property:update', model);
|
||
editor.on('component:update', onComponentUpdate);
|
||
editor.on('component:add', onComponentAdd);
|
||
editor.on('component:remove', onComponentRemove);
|
||
editor.on('style:property:update', onStyleUpdate);
|
||
editor.on('component:selected', onSelect);
|
||
return () => {
|
||
try {
|
||
editor.off('component:update', onComponentUpdate);
|
||
editor.off('component:add', onComponentAdd);
|
||
editor.off('component:remove', onComponentRemove);
|
||
editor.off('style:property:update', onStyleUpdate);
|
||
editor.off('component:selected', onSelect);
|
||
} catch {}
|
||
};
|
||
}
|
||
|
||
function attachCraftDirtyTracker() {
|
||
const host = document.getElementById('craftEditor');
|
||
if (!host) return () => {};
|
||
const handler = () => markDirty('craft:input');
|
||
const events = ['input', 'keydown', 'paste', 'drop'];
|
||
events.forEach(evt => host.addEventListener(evt, handler, true));
|
||
return () => {
|
||
events.forEach(evt => host.removeEventListener(evt, handler, true));
|
||
};
|
||
}
|
||
|
||
function getSerializedHtml(editor, win) {
|
||
if (!editor) return '';
|
||
const BridgeRTE = win?.BridgeParts?.BridgeRTE || win?.BridgeRTE || null;
|
||
if (BridgeRTE && typeof BridgeRTE.serializeHtml === 'function') {
|
||
return BridgeRTE.serializeHtml(editor);
|
||
}
|
||
return (typeof editor.getHtml === 'function') ? editor.getHtml() : '';
|
||
}
|
||
|
||
async function buildCurrentSnapshot() {
|
||
if (currentEditorType === 'craftjs') {
|
||
return buildSnapshot({
|
||
editorType: 'craftjs',
|
||
html: craftEditor ? craftEditor.getContent() : '',
|
||
json: '',
|
||
craftJson: craftEditor && craftEditor.getCraftJson ? craftEditor.getCraftJson() : '',
|
||
});
|
||
}
|
||
try {
|
||
const editor = await waitForEditor(2000);
|
||
const win = iframe?.contentWindow;
|
||
const fontCss = win?.BridgeParts?.RTE_FONT_FACE_CSS || '';
|
||
const cssPayload = (fontCss ? fontCss + '\n' : '') + (editor.getCss() || '');
|
||
const htmlContent = (getSerializedHtml(editor, win) || '') + '<style>' + cssPayload + '</style>';
|
||
let jsonRaw = '';
|
||
try {
|
||
jsonRaw = JSON.stringify(editor.getProjectData());
|
||
} catch {}
|
||
return buildSnapshot({
|
||
editorType: 'grapesjs',
|
||
html: htmlContent,
|
||
json: jsonRaw,
|
||
craftJson: '',
|
||
});
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function setSavedSnapshotFromData(payload) {
|
||
beginSuppressDirty();
|
||
const fields = extractContentFields(payload);
|
||
savedSnapshot = buildSnapshot(fields);
|
||
clearDirty();
|
||
baselineReady = true;
|
||
}
|
||
|
||
async function syncSnapshotFromEditor() {
|
||
beginSuppressDirty();
|
||
const currentSnapshot = await buildCurrentSnapshot();
|
||
if (!currentSnapshot) return;
|
||
savedSnapshot = currentSnapshot;
|
||
clearDirty();
|
||
baselineReady = true;
|
||
}
|
||
|
||
function scheduleSnapshotSync(delay = 600) {
|
||
baselineReady = false;
|
||
beginSuppressDirty();
|
||
setTimeout(() => {
|
||
syncSnapshotFromEditor().catch(() => {});
|
||
}, delay);
|
||
}
|
||
|
||
async function hasUnsavedChanges() {
|
||
if (!baselineReady) return false;
|
||
if (suppressDirty) return false;
|
||
return isDirty;
|
||
}
|
||
|
||
function showUnsavedDialog() {
|
||
return new Promise((resolve) => {
|
||
if (!unsavedDialog || typeof unsavedDialog.showModal !== 'function') {
|
||
resolve('discard');
|
||
return;
|
||
}
|
||
const cleanup = () => {
|
||
btnUnsavedCancel && btnUnsavedCancel.removeEventListener('click', onCancel);
|
||
btnUnsavedDiscard && btnUnsavedDiscard.removeEventListener('click', onDiscard);
|
||
btnUnsavedSave && btnUnsavedSave.removeEventListener('click', onSave);
|
||
};
|
||
const closeWith = (choice) => {
|
||
cleanup();
|
||
unsavedDialog.close();
|
||
resolve(choice);
|
||
};
|
||
const onCancel = () => closeWith('cancel');
|
||
const onDiscard = () => closeWith('discard');
|
||
const onSave = () => closeWith('save');
|
||
btnUnsavedCancel && btnUnsavedCancel.addEventListener('click', onCancel);
|
||
btnUnsavedDiscard && btnUnsavedDiscard.addEventListener('click', onDiscard);
|
||
btnUnsavedSave && btnUnsavedSave.addEventListener('click', onSave);
|
||
unsavedDialog.showModal();
|
||
});
|
||
}
|
||
|
||
async function confirmUnsavedChanges() {
|
||
const dirty = await hasUnsavedChanges();
|
||
if (!dirty) return 'ok';
|
||
try {
|
||
const currentSnapshot = await buildCurrentSnapshot();
|
||
if (currentSnapshot && savedSnapshot && currentSnapshot === savedSnapshot) {
|
||
clearDirty();
|
||
return 'ok';
|
||
}
|
||
} catch {}
|
||
const choice = await showUnsavedDialog();
|
||
if (choice === 'save') {
|
||
const okSave = await save();
|
||
if (!okSave) return 'cancel';
|
||
}
|
||
return choice;
|
||
}
|
||
|
||
function showActivateDialog() {
|
||
return new Promise((resolve) => {
|
||
if (!activateDialog || typeof activateDialog.showModal !== 'function') {
|
||
resolve('no');
|
||
return;
|
||
}
|
||
const cleanup = () => {
|
||
btnActivateCancel && btnActivateCancel.removeEventListener('click', onCancel);
|
||
btnActivateNo && btnActivateNo.removeEventListener('click', onNo);
|
||
btnActivateYes && btnActivateYes.removeEventListener('click', onYes);
|
||
};
|
||
const closeWith = (choice) => {
|
||
cleanup();
|
||
activateDialog.close();
|
||
resolve(choice);
|
||
};
|
||
const onCancel = () => closeWith('cancel');
|
||
const onNo = () => closeWith('no');
|
||
const onYes = () => closeWith('yes');
|
||
btnActivateCancel && btnActivateCancel.addEventListener('click', onCancel);
|
||
btnActivateNo && btnActivateNo.addEventListener('click', onNo);
|
||
btnActivateYes && btnActivateYes.addEventListener('click', onYes);
|
||
activateDialog.showModal();
|
||
});
|
||
}
|
||
|
||
function updateVersionMeta(id) {
|
||
const selectedId = id || Number(versionSelect?.value || 0);
|
||
const key = selectedId ? String(selectedId) : '';
|
||
currentVersionId = selectedId ? Number(selectedId) : 0;
|
||
currentVersionMeta = key && versionMap.has(key) ? versionMap.get(key) : null;
|
||
if (btnDeactivateVersion) {
|
||
const isActive = !!(currentVersionMeta && Number(currentVersionMeta.is_active) === 1);
|
||
btnDeactivateVersion.classList.toggle('hidden', !isActive);
|
||
}
|
||
if (btnActivateVersion) {
|
||
const isActive = !!(currentVersionMeta && Number(currentVersionMeta.is_active) === 1);
|
||
btnActivateVersion.classList.toggle('hidden', isActive || !currentVersionId);
|
||
}
|
||
if (versionActiveBadge) {
|
||
const isActive = !!(currentVersionMeta && Number(currentVersionMeta.is_active) === 1);
|
||
versionActiveBadge.classList.toggle('hidden', !isActive);
|
||
}
|
||
const win = iframe?.contentWindow;
|
||
if (win && win.BridgeParts) {
|
||
const canOverwrite = !!(currentVersionMeta
|
||
&& Number(currentVersionMeta.is_active) === 0
|
||
&& Number(currentVersionMeta.was_active) === 0);
|
||
win.BridgeParts.CURRENT_VERSION_ID = canOverwrite ? currentVersionId : 0;
|
||
} else if (win) {
|
||
const canOverwrite = !!(currentVersionMeta
|
||
&& Number(currentVersionMeta.is_active) === 0
|
||
&& Number(currentVersionMeta.was_active) === 0);
|
||
win.CURRENT_VERSION_ID = canOverwrite ? currentVersionId : 0;
|
||
}
|
||
}
|
||
|
||
function renderVersionOptions(items, opts = {}) {
|
||
const keepSelection = !!opts.keepSelection;
|
||
const preferredId = opts.preferredId ? String(opts.preferredId) : '';
|
||
const selectionToKeep = preferredId || (keepSelection ? String(versionSelect?.value || lastVersionSelection || '') : '');
|
||
versionItems = items || [];
|
||
versionMap = new Map();
|
||
if (!versionSelect) return '';
|
||
const rows = Array.isArray(versionItems) ? versionItems : [];
|
||
versionSelect.innerHTML = '';
|
||
lastVersionSelection = '';
|
||
if (!rows.length) {
|
||
const opt = document.createElement('option');
|
||
opt.value = '';
|
||
opt.textContent = 'Keine Versionen vorhanden';
|
||
opt.disabled = true;
|
||
versionSelect.appendChild(opt);
|
||
versionSelect.disabled = true;
|
||
updateVersionMeta(0);
|
||
return '';
|
||
}
|
||
versionSelect.disabled = false;
|
||
let activeId = '';
|
||
rows.forEach((item) => {
|
||
const opt = document.createElement('option');
|
||
const label = `${Number(item.is_active) === 1 ? '✓ ' : ''}#${item.version_no} – ${formatVersionDate(item.created_at)}`;
|
||
opt.value = String(item.id);
|
||
opt.textContent = label;
|
||
versionSelect.appendChild(opt);
|
||
versionMap.set(String(item.id), item);
|
||
if (Number(item.is_active) === 1 && !activeId) activeId = String(item.id);
|
||
});
|
||
let fallbackId = '';
|
||
if (selectionToKeep && versionMap.has(selectionToKeep)) {
|
||
fallbackId = selectionToKeep;
|
||
} else {
|
||
// Default to the latest version so reopening shows the most recent edits.
|
||
fallbackId = (rows[0] ? String(rows[0].id) : '') || activeId;
|
||
}
|
||
if (fallbackId) {
|
||
lastVersionSelection = fallbackId;
|
||
versionSelect.value = fallbackId;
|
||
updateVersionMeta(Number(fallbackId));
|
||
}
|
||
return lastVersionSelection;
|
||
}
|
||
|
||
async function loadVersionsForCurrent(opts = {}) {
|
||
if (!current?.id) {
|
||
renderVersionOptions([], opts);
|
||
return '';
|
||
}
|
||
try {
|
||
const res = await apiAction('content_versions.list', { method: 'GET', data: { content_id: current.id, id: current.id } });
|
||
if (!res?.ok) throw new Error(res?.error || 'Versionen konnten nicht geladen werden');
|
||
return renderVersionOptions(Array.isArray(res?.items) ? res.items : [], opts);
|
||
} catch {
|
||
renderVersionOptions([], opts);
|
||
return '';
|
||
}
|
||
}
|
||
|
||
async function applyVersionPayload(payload) {
|
||
baselineReady = false;
|
||
beginSuppressDirty();
|
||
const data = extractContentFields(payload);
|
||
const targetType = data.editorType === 'craftjs' ? 'craftjs' : 'grapesjs';
|
||
setEditorType(targetType);
|
||
if (targetType === 'craftjs') {
|
||
craftEditor?.setContent(data.html || '', data.craftJson || '');
|
||
setSavedSnapshotFromData(payload);
|
||
if (dirtyCleanup) dirtyCleanup();
|
||
dirtyCleanup = attachCraftDirtyTracker();
|
||
scheduleSnapshotSync(300);
|
||
return;
|
||
}
|
||
const editor = await waitForEditor(3000);
|
||
const jsonRaw = normalizeSnapshotValue(data.json).trim();
|
||
if (jsonRaw) {
|
||
try {
|
||
const project = JSON.parse(jsonRaw);
|
||
editor.loadProjectData(project);
|
||
setSavedSnapshotFromData(payload);
|
||
if (dirtyCleanup) dirtyCleanup();
|
||
dirtyCleanup = attachGjsDirtyTracker(editor);
|
||
scheduleSnapshotSync(300);
|
||
return;
|
||
} catch {}
|
||
}
|
||
const html = normalizeSnapshotValue(data.html);
|
||
editor.setComponents(html);
|
||
setSavedSnapshotFromData(payload);
|
||
if (dirtyCleanup) dirtyCleanup();
|
||
dirtyCleanup = attachGjsDirtyTracker(editor);
|
||
scheduleSnapshotSync(300);
|
||
}
|
||
|
||
async function loadLatestContentFromServer() {
|
||
baselineReady = false;
|
||
beginSuppressDirty();
|
||
const res = await apiAction('content.get', { method: 'GET', data: { id: current.id, section_id: current.section.id } });
|
||
await applyVersionPayload(res || {});
|
||
scheduleSnapshotSync(300);
|
||
}
|
||
|
||
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() {
|
||
if (currentEditorType === 'craftjs') {
|
||
return craftEditor ? craftEditor.getContent() : '';
|
||
}
|
||
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 = getSerializedHtml(ed, win);
|
||
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);
|
||
}
|
||
|
||
function extractCraftHtml(craftJson, fallbackHtml) {
|
||
if (!craftJson) return fallbackHtml || '';
|
||
try {
|
||
const parsed = typeof craftJson === 'string' ? JSON.parse(craftJson) : craftJson;
|
||
if (parsed && typeof parsed.html === 'string') {
|
||
return parsed.html;
|
||
}
|
||
} catch {}
|
||
return fallbackHtml || '';
|
||
}
|
||
|
||
function looksCraftSerialized(payload) {
|
||
if (!payload) return false;
|
||
try {
|
||
const parsed = typeof payload === 'string' ? JSON.parse(payload) : payload;
|
||
return !!(parsed && typeof parsed === 'object' && parsed.ROOT);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function setEditorType(nextType) {
|
||
currentEditorType = nextType === 'craftjs' ? 'craftjs' : 'grapesjs';
|
||
if (editorSelect) editorSelect.value = currentEditorType;
|
||
if (currentEditorType === 'craftjs') {
|
||
iframe?.classList?.add('hidden');
|
||
craftEditor?.show();
|
||
} else {
|
||
craftEditor?.hide();
|
||
iframe?.classList?.remove('hidden');
|
||
}
|
||
}
|
||
|
||
// ... (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'; }
|
||
|
||
async function buildRefLibForContext() {
|
||
return { sections: [], blocks: [] };
|
||
}
|
||
|
||
async function buildSnippetsForContext() {
|
||
return [];
|
||
}
|
||
|
||
async function loadSenderOptions(force = false) {
|
||
if (!sendSender) return;
|
||
if (senderLoadPromise && !force) return senderLoadPromise;
|
||
senderLoadPromise = apiAction('account.senders.list', { method: 'GET' })
|
||
.then(res => {
|
||
senderOptions = res?.items || [];
|
||
renderSenderOptions();
|
||
})
|
||
.catch(() => {
|
||
senderOptions = [];
|
||
renderSenderOptions();
|
||
})
|
||
.finally(() => {
|
||
senderLoadPromise = null;
|
||
});
|
||
return senderLoadPromise;
|
||
}
|
||
|
||
function renderSenderOptions() {
|
||
if (!sendSender) return;
|
||
const previous = sendSender.value;
|
||
let html = '<option value="">Standard (System)</option>';
|
||
senderOptions.forEach(opt => {
|
||
const label = opt.label || opt.from_name || opt.from_email;
|
||
html += `<option value="${opt.id}">${escapeHtml(label)} <${escapeHtml(opt.from_email)}></option>`;
|
||
});
|
||
sendSender.innerHTML = html;
|
||
if (previous && senderOptions.some(opt => String(opt.id) === previous)) {
|
||
sendSender.value = previous;
|
||
} else {
|
||
sendSender.value = '';
|
||
}
|
||
if (sendSenderHint) {
|
||
sendSenderHint.classList.toggle('hidden', senderOptions.length > 0);
|
||
}
|
||
}
|
||
|
||
// ---------- Initialen HTML-Inhalt in Editor pushen (mit Token/Race-Schutz) ----------
|
||
async function pushInitialHtmlToEditor({ mode, html, snippets, ref, token, hasJson, json }) {
|
||
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, json: json || '' }, '*');
|
||
} 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, sectionOverride) {
|
||
const section = item?.section || sectionOverride || window.__activeSection || null;
|
||
current = {
|
||
resource: 'content',
|
||
id: Number(item?.id || 0),
|
||
name: item?.name || '',
|
||
section: section,
|
||
};
|
||
if (!current.id) return err('Ungültige ID');
|
||
if (!current.section) return err('Section nicht gefunden');
|
||
|
||
// globaler Kontext
|
||
window.__currentItemId = current.id;
|
||
window.__currentEditorCtx = { id: current.id, mode: current.section.slug, section: current.section };
|
||
setSendContext(current.section?.is_template ? current.id : 0, current.name);
|
||
if (btnTest) btnTest.classList.toggle('hidden', !current.section?.is_template);
|
||
setVersionUiVisible(true);
|
||
if (versionSelect) versionSelect.value = '';
|
||
lastVersionSelection = '';
|
||
|
||
// Neuen Token erzeugen & alten Listener entfernen
|
||
reqToken++;
|
||
const myToken = reqToken;
|
||
if (bridgeListener) window.removeEventListener('message', bridgeListener);
|
||
bridgeListener = null;
|
||
|
||
// Overlay zeigen
|
||
showVeil();
|
||
|
||
const requestedVersionId = Number(item?.version_id || item?.versionId || 0);
|
||
|
||
// Daten parallel laden (fresh HTML + kontextgefilterte Snippets + Referenzen)
|
||
let fresh = '';
|
||
let snippets = [];
|
||
let refLib = { sections: [], blocks: [] };
|
||
let hasJson = false;
|
||
let jsonState = '';
|
||
|
||
let editorType = 'grapesjs';
|
||
let craftJson = '';
|
||
|
||
let defaultVersionId = '';
|
||
await Promise.all([
|
||
(async() => {
|
||
try {
|
||
const row = await apiAction('content.get', { method: 'GET', data: { id: current.id, section_id: current.section.id } });
|
||
const rawContent = row?.content ?? row?.item?.content ?? '';
|
||
const trimmed = String(rawContent || '').trim();
|
||
const looksJson = trimmed.startsWith('{') || trimmed.startsWith('[');
|
||
jsonState = looksJson ? rawContent : '';
|
||
hasJson = looksJson;
|
||
// API liefert jetzt top-level html/content; fallback auf item.*
|
||
fresh = row?.html ?? row?.item?.html ?? '';
|
||
if (!fresh && !looksJson) {
|
||
fresh = rawContent;
|
||
}
|
||
editorType = String(row?.editor_type ?? row?.item?.editor_type ?? 'grapesjs').toLowerCase();
|
||
craftJson = row?.craft_json ?? row?.item?.craft_json ?? '';
|
||
} catch {}
|
||
})(),
|
||
(async() => { snippets = await buildSnippetsForContext(current); })(),
|
||
(async() => { refLib = await buildRefLibForContext(current); })(),
|
||
(async() => { defaultVersionId = await loadVersionsForCurrent(); })()
|
||
]);
|
||
|
||
const effectiveVersionId = requestedVersionId ? String(requestedVersionId) : defaultVersionId;
|
||
if (effectiveVersionId) {
|
||
try {
|
||
const res = await apiAction('content_versions.get', { method: 'GET', data: { id: effectiveVersionId, content_id: current.id } });
|
||
if (res?.ok && res?.item) {
|
||
const fields = extractContentFields(res.item);
|
||
fresh = fields.html || '';
|
||
jsonState = fields.json || '';
|
||
editorType = fields.editorType || editorType;
|
||
craftJson = fields.craftJson || '';
|
||
updateVersionMeta(Number(effectiveVersionId));
|
||
lastVersionSelection = String(effectiveVersionId);
|
||
if (versionSelect) versionSelect.value = lastVersionSelection;
|
||
}
|
||
} catch {}
|
||
}
|
||
|
||
editorType = editorType === 'craftjs' ? 'craftjs' : 'grapesjs';
|
||
setSavedSnapshotFromData({ html: fresh, content: jsonState, editor_type: editorType, craft_json: craftJson });
|
||
setEditorType(editorType);
|
||
scheduleSnapshotSync(1200);
|
||
if (editorType === 'craftjs') {
|
||
const craftHtml = extractCraftHtml(craftJson, fresh);
|
||
beginSuppressDirty();
|
||
craftEditor?.setContent(craftHtml, craftJson);
|
||
if (dirtyCleanup) dirtyCleanup();
|
||
dirtyCleanup = attachCraftDirtyTracker();
|
||
scheduleSnapshotSync(300);
|
||
hideVeil();
|
||
if (dlg && typeof dlg.showModal === 'function') dlg.showModal();
|
||
if (!looksCraftSerialized(craftJson) && craftEditor?.serializeFromHtml) {
|
||
const seed = craftEditor.serializeFromHtml(craftHtml);
|
||
try {
|
||
await apiUpdate('content', current.id, {
|
||
editor_type: 'craftjs',
|
||
html: craftHtml,
|
||
craft_json: seed,
|
||
section_id: current.section.id,
|
||
});
|
||
} catch {}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 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;
|
||
if (d.source === 'bridge-core' && d.type === 'rte-blur') {
|
||
try { console.log(d.detail || '[RTE BLUR]'); } catch {}
|
||
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.section);
|
||
} catch {}
|
||
if (pendingSaveResolve) {
|
||
pendingSaveResolve(true);
|
||
pendingSaveResolve = null;
|
||
}
|
||
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.section.slug,
|
||
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,
|
||
json: jsonState
|
||
});
|
||
}
|
||
};
|
||
window.addEventListener('message', bridgeListener);
|
||
|
||
// Fallback, falls kein Ready ankommt
|
||
setTimeout(() => {
|
||
pushInitialHtmlToEditor({
|
||
mode: current.section.slug,
|
||
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,
|
||
json: jsonState
|
||
});
|
||
}, 1200);
|
||
};
|
||
|
||
// Jetzt den Editor-Core laden (erst NACH about:blank)
|
||
iframe.src = `editor/editor-core.php?mode=${encodeURIComponent(current.section.slug)}&id=${current.id}§ion_id=${current.section.id}&t=${Date.now()}`;
|
||
|
||
dlg?.showModal?.();
|
||
if (dlg && !dialogCancelBound) {
|
||
dlg.addEventListener('cancel', async (ev) => {
|
||
ev.preventDefault();
|
||
await close();
|
||
});
|
||
dialogCancelBound = true;
|
||
}
|
||
|
||
waitForEditor(6000)
|
||
.then((ed) => {
|
||
if (dirtyCleanup) dirtyCleanup();
|
||
beginSuppressDirty();
|
||
dirtyCleanup = attachGjsDirtyTracker(ed);
|
||
scheduleSnapshotSync(1200);
|
||
})
|
||
.catch(() => {});
|
||
}
|
||
|
||
// ---------- Speichern (DELEGIERT) ----------
|
||
// 🚨 KORRIGIERT: Delegiert Speichern an den iFrame, der die JSON-Daten holt!
|
||
async function save() {
|
||
if (!current?.id) return err('Keine aktive ID');
|
||
|
||
let activateNext = false;
|
||
if (currentVersionMeta && (Number(currentVersionMeta.is_active) === 1 || Number(currentVersionMeta.was_active) === 1)) {
|
||
const decision = await showActivateDialog();
|
||
if (decision === 'cancel') return false;
|
||
activateNext = decision === 'yes';
|
||
}
|
||
|
||
const overwriteVersionId = (currentVersionMeta
|
||
&& Number(currentVersionMeta.is_active) === 0
|
||
&& Number(currentVersionMeta.was_active) === 0)
|
||
? Number(currentVersionId || 0)
|
||
: 0;
|
||
|
||
if (currentEditorType === 'craftjs') {
|
||
const html = craftEditor ? craftEditor.getContent() : '';
|
||
const craftJson = craftEditor && craftEditor.getCraftJson
|
||
? craftEditor.getCraftJson()
|
||
: JSON.stringify({ html });
|
||
const payload = { html, craft_json: craftJson, editor_type: 'craftjs', section_id: current.section.id };
|
||
if (activateNext) payload.activate_version = 1;
|
||
if (overwriteVersionId) payload.version_id = overwriteVersionId;
|
||
const res = await apiUpdate('content', current.id, payload);
|
||
if (res?.ok) ok('Gespeichert');
|
||
else err(res?.error || 'Speichern fehlgeschlagen');
|
||
if (res?.ok) setTimeout(loadVersionsForCurrent, 300);
|
||
return res?.ok;
|
||
}
|
||
|
||
if (activateNext) {
|
||
const win = iframe?.contentWindow;
|
||
if (win && win.BridgeParts) {
|
||
win.BridgeParts.NEXT_ACTIVATE_VERSION = 1;
|
||
} else if (win) {
|
||
win.NEXT_ACTIVATE_VERSION = 1;
|
||
}
|
||
}
|
||
if (overwriteVersionId) {
|
||
const win = iframe?.contentWindow;
|
||
if (win && win.BridgeParts) {
|
||
win.BridgeParts.CURRENT_VERSION_ID = overwriteVersionId;
|
||
} else if (win) {
|
||
win.CURRENT_VERSION_ID = overwriteVersionId;
|
||
}
|
||
}
|
||
const okSave = await delegateCommand('save-data');
|
||
if (!okSave) return false;
|
||
if (pendingSaveTimer) {
|
||
clearTimeout(pendingSaveTimer);
|
||
pendingSaveTimer = null;
|
||
}
|
||
const saved = await new Promise((resolve) => {
|
||
pendingSaveResolve = resolve;
|
||
pendingSaveTimer = setTimeout(() => {
|
||
pendingSaveTimer = null;
|
||
if (pendingSaveResolve) {
|
||
pendingSaveResolve(false);
|
||
pendingSaveResolve = null;
|
||
}
|
||
}, 3000);
|
||
});
|
||
if (!saved) return false;
|
||
setTimeout(async () => {
|
||
await loadVersionsForCurrent();
|
||
try {
|
||
const res = await apiAction('content.get', { method: 'GET', data: { id: current.id, section_id: current.section.id } });
|
||
setSavedSnapshotFromData(res || {});
|
||
} catch {}
|
||
}, 200);
|
||
return true;
|
||
}
|
||
|
||
// ... (Der Rest der Funktionen bleibt unverändert) ...
|
||
async function clearEditor() {
|
||
if (currentEditorType === 'craftjs') {
|
||
craftEditor?.clear();
|
||
return;
|
||
}
|
||
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(ctx = null) {
|
||
if (!current?.section?.is_template) {
|
||
err('Kein Template geladen');
|
||
return;
|
||
}
|
||
const ctxId = ctx?.id ? Number(ctx.id) : (window.__currentItemId || current?.id || 0);
|
||
if (!ctxId) {
|
||
err('Kein Template geladen');
|
||
return;
|
||
}
|
||
window.__currentItemId = ctxId;
|
||
const ctxName = ctx?.name ?? sendDlg?.dataset?.templateName ?? current?.name ?? '';
|
||
setSendContext(ctxId, ctxName);
|
||
if (sendSubject) sendSubject.value = ctx?.subject || 'Testversand';
|
||
if (sendTo) sendTo.value = ctx?.to || '';
|
||
await loadSenderOptions(true);
|
||
sendDlg?.showModal?.();
|
||
}
|
||
function closeSend(){
|
||
sendDlg?.close?.();
|
||
if (sendSender) sendSender.value = '';
|
||
}
|
||
|
||
async function doSend(ev){
|
||
ev?.preventDefault?.();
|
||
const to = (sendTo?.value || '').trim();
|
||
if(!to){ err('Bitte Empfänger angeben'); 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 payload = {
|
||
template_id: id,
|
||
to,
|
||
subject: (sendSubject?.value || 'Testversand'),
|
||
};
|
||
if (sendSender && sendSender.value) {
|
||
payload.sender_id = Number(sendSender.value);
|
||
}
|
||
const r = await apiAction('templates.test_send', { method:'POST', data: payload });
|
||
if(r?.ok){ toast("Testversand ausgelöst"); closeSend(); } else { toast("Senden fehlgeschlagen", false); }
|
||
}
|
||
function closePreview(){ prevDlg?.close?.(); }
|
||
|
||
async function close() {
|
||
const decision = await confirmUnsavedChanges();
|
||
if (decision === 'cancel') return;
|
||
if (dirtyCleanup) dirtyCleanup();
|
||
dirtyCleanup = null;
|
||
// 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;
|
||
clearDirty();
|
||
}
|
||
|
||
async function switchEditor(nextType) {
|
||
if (!current?.id) return;
|
||
const target = nextType === 'craftjs' ? 'craftjs' : 'grapesjs';
|
||
if (target === currentEditorType) return;
|
||
const confirmed = window.confirm('Editor wechseln? Ungespeicherte Änderungen gehen verloren.');
|
||
if (!confirmed) {
|
||
if (editorSelect) editorSelect.value = currentEditorType;
|
||
return;
|
||
}
|
||
if (currentEditorType === 'grapesjs' && target === 'craftjs') {
|
||
const html = await readEditedHtml();
|
||
const craftJson = craftEditor && craftEditor.serializeFromHtml
|
||
? craftEditor.serializeFromHtml(html)
|
||
: JSON.stringify({ html });
|
||
const res = await apiUpdate('content', current.id, {
|
||
editor_type: 'craftjs',
|
||
html,
|
||
craft_json: craftJson,
|
||
section_id: current.section.id,
|
||
});
|
||
if (!res?.ok) {
|
||
err(res?.error || 'Editorwechsel fehlgeschlagen');
|
||
if (editorSelect) editorSelect.value = currentEditorType;
|
||
return;
|
||
}
|
||
setEditorType('craftjs');
|
||
craftEditor?.setContent(html, craftJson);
|
||
iframe.src = 'about:blank#' + Date.now();
|
||
ok('Editor gewechselt');
|
||
return;
|
||
}
|
||
if (currentEditorType === 'craftjs' && target === 'grapesjs') {
|
||
const html = craftEditor ? craftEditor.getContent() : '';
|
||
const res = await apiUpdate('content', current.id, {
|
||
editor_type: 'grapesjs',
|
||
html,
|
||
section_id: current.section.id,
|
||
});
|
||
if (!res?.ok) {
|
||
err(res?.error || 'Editorwechsel fehlgeschlagen');
|
||
if (editorSelect) editorSelect.value = currentEditorType;
|
||
return;
|
||
}
|
||
ok('Editor gewechselt');
|
||
await open({ id: current.id, name: current.name, section: current.section }, 'content', current.section);
|
||
}
|
||
}
|
||
|
||
function isTemplateSection() {
|
||
if (current?.section?.is_template) return true;
|
||
const slug = (current?.section?.slug || '').toString().toLowerCase();
|
||
return slug === 'emailtemplate' || slug.includes('template');
|
||
}
|
||
|
||
async function confirmTemplateReferences(actionLabel) {
|
||
if (!current?.id || !isTemplateSection()) return true;
|
||
const res = await apiAction('templates.references', { method: 'GET', data: { template_id: current.id } }).catch(() => null);
|
||
if (!res || res.ok === false) {
|
||
return await showConfirmDialog({
|
||
title: 'Referenzen nicht geprüft',
|
||
text: `Referenzen konnten nicht geprüft werden. ${actionLabel} trotzdem?`,
|
||
confirmLabel: actionLabel,
|
||
});
|
||
}
|
||
const refs = Array.isArray(res?.references) ? res.references : [];
|
||
if (!refs.length) return true;
|
||
const preview = refs.slice(0, 6).map(r => `${r.name || 'Template'} #${r.id}`).join(', ');
|
||
const more = refs.length > 6 ? ` und ${refs.length - 6} weitere` : '';
|
||
const escPreview = preview
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
const escMore = more
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
return await showConfirmDialog({
|
||
title: 'Template wird verwendet',
|
||
html: true,
|
||
text: `Dieses Template wird in ${refs.length} anderen Template(s) verwendet (${escPreview}${escMore}).<br>${formatReferencesHtml(refs)}<br>${actionLabel} trotzdem?`,
|
||
confirmLabel: actionLabel,
|
||
});
|
||
}
|
||
|
||
// 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);
|
||
editorSelect && (editorSelect.onchange = () => switchEditor(editorSelect.value));
|
||
btnActivateVersion && (btnActivateVersion.onclick = async () => {
|
||
const selectedId = Number(versionSelect?.value || currentVersionId || 0);
|
||
if (!current?.id || !selectedId) return;
|
||
try {
|
||
const res = await apiAction('content_versions.activate', { method: 'POST', data: { id: selectedId } });
|
||
if (!res?.ok) throw new Error(res?.error || 'Aktivieren fehlgeschlagen');
|
||
await loadVersionsForCurrent();
|
||
toast('Version aktiviert', true);
|
||
} catch (e) {
|
||
err(e.message || 'Aktivieren fehlgeschlagen');
|
||
}
|
||
});
|
||
btnDeactivateVersion && (btnDeactivateVersion.onclick = async () => {
|
||
if (!current?.id) return;
|
||
if (!currentVersionMeta || Number(currentVersionMeta.is_active) !== 1) return;
|
||
try {
|
||
const okRefs = await confirmTemplateReferences('Deaktivieren');
|
||
if (!okRefs) return;
|
||
let res = await apiAction('content_versions.deactivate', { method: 'POST', data: { content_id: current.id } });
|
||
if (res && res.ok === false && Array.isArray(res.references) && res.references.length) {
|
||
const refs = res.references || [];
|
||
const ok = await showConfirmDialog({
|
||
title: 'Template wird verwendet',
|
||
html: true,
|
||
text: `Dieses Template wird in anderen Templates verwendet.<br>${formatReferencesHtml(refs)}<br>Deaktivieren trotzdem?`,
|
||
confirmLabel: 'Deaktivieren',
|
||
});
|
||
if (!ok) return;
|
||
res = await apiAction('content_versions.deactivate', { method: 'POST', data: { content_id: current.id, force: 1 } });
|
||
}
|
||
if (!res?.ok) throw new Error(res?.error || 'Deaktivieren fehlgeschlagen');
|
||
await loadVersionsForCurrent({ keepSelection: true, preferredId: currentVersionId });
|
||
toast('Aktive Version deaktiviert', true);
|
||
} catch (e) {
|
||
err(e.message || 'Deaktivieren fehlgeschlagen');
|
||
}
|
||
});
|
||
versionSelect && (versionSelect.onchange = async () => {
|
||
if (!current?.id) return;
|
||
const previousSelection = lastVersionSelection;
|
||
const decision = await confirmUnsavedChanges();
|
||
if (decision === 'cancel') {
|
||
versionSelect.value = previousSelection;
|
||
return;
|
||
}
|
||
const versionId = Number(versionSelect.value || 0);
|
||
if (!versionId) {
|
||
versionSelect.value = previousSelection;
|
||
return;
|
||
}
|
||
try {
|
||
const res = await apiAction('content_versions.get', { method: 'GET', data: { id: versionId, content_id: current.id } });
|
||
if (!res?.ok) throw new Error(res?.error || 'Version konnte nicht geladen werden');
|
||
await applyVersionPayload(res?.item || res);
|
||
lastVersionSelection = String(versionId);
|
||
updateVersionMeta(versionId);
|
||
} catch (e) {
|
||
err(e.message || 'Version konnte nicht geladen werden');
|
||
versionSelect.value = previousSelection;
|
||
}
|
||
});
|
||
// restore button removed
|
||
|
||
window.AdminTestSend = window.AdminTestSend || {};
|
||
window.AdminTestSend.open = (opts = {}) => {
|
||
const targetId = Number(opts.id || window.__currentItemId || 0);
|
||
if (!targetId) {
|
||
err('Testversand: Keine ID vorhanden');
|
||
return;
|
||
}
|
||
if (!current?.section?.is_template) {
|
||
err('Kein Template geladen');
|
||
return;
|
||
}
|
||
window.__currentItemId = targetId;
|
||
setSendContext(targetId, opts.name || '');
|
||
openSend({ id: targetId, name: opts.name || '' });
|
||
};
|
||
|
||
// Public API
|
||
window.EditorUI = { open, save, close, clear: clearEditor, preview: openPreview };
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
return String(str || '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
// Default-Export + globaler Fallback
|
||
export default initEditor;
|
||
window.initEditor = initEditor;
|