Files
emailtemplate.it/public/assets/js/bridge/blocks-api.js
2026-02-09 01:38:39 +01:00

800 lines
29 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* /assets/js/bridge/blocks-api.js (UI-KERN UND KOMPONENTEN-SCHICHT) */
(function (B) {
const PluginName = 'bridge-blocks-api';
if (!B || typeof grapesjs === 'undefined') {
console.warn(`%c[${PluginName}] %cBridgeParts (B) oder GrapesJS fehlt. Exit.`, 'color:orange; font-weight:bold;', 'color:inherit;');
return;
}
B.LOG_CONFIG = B.LOG_CONFIG || { PLUGINS: {} };
B.LOG_CONFIG.PLUGINS[PluginName] = true;
const log = (message, color = '#1E90FF', type = 'info', force = false) => B.log(PluginName, message, color, type, force);
const qs = new URLSearchParams(location.search);
const requestedMode = (qs.get('mode') || 'templates').toLowerCase();
B.EDITOR_MODE = (B.EDITOR_MODE || requestedMode.toUpperCase());
const EDITOR_MODE = (B.EDITOR_MODE || 'TEMPLATES').toLowerCase();
const SECTION_ID = Number(qs.get('section_id') || B.CURRENT_SECTION_ID || 0);
log(`START: SKRIPT-AUSFÜHRUNG GESTARTET. Editor Modus: ${EDITOR_MODE}.`, '#DC143C');
const TARGET_CAT_ID = 'custom';
const PLACEHOLDER_ID = 'api-placeholder-loading';
const REFERENCE_COMPONENT_TYPE = 'library-reference';
// --- NEUE KONSTANTEN FÜR SPEICHERN-LOGIK ---
// Annahme: ID der aktuellen Seite/Template ist global in B verfügbar
const CURRENT_ENTITY_ID = Number(B.CURRENT_ENTITY_ID || qs.get('id') || 0);
// Annahme: Basis-URL der API ist in B verfügbar
const API_KERNEL_URL = B.API_KERNEL_URL || B.API_BASE || '/api.php';
// -------------------------------------------
// --------------------------------------------------------
// (1) Kern-Logik: Platzhalter und Kategorien registrieren (SYNCHRON)
// --------------------------------------------------------
const preRegisterCategoriesAndPlaceholders = (editor) => {
const bm = editor.BlockManager;
bm.add(PLACEHOLDER_ID, {
label: 'Lade Custom-Blöcke...',
category: TARGET_CAT_ID,
content: '<div style="padding: 10px; color: #1e3a8a; background-color: #eef2ff; border: 1px solid #c7d2fe; text-align: center;">⚙️ Custom-Blöcke werden geladen...</div>',
attributes: { class: 'gjs-block__api-placeholder' },
});
const cat = bm.getCategories().get(TARGET_CAT_ID);
if (!cat) {
bm.addCategory(TARGET_CAT_ID, { label: 'Custom', open: true, order: 1 });
}
log('Platzhalter und Kategorie registriert.', '#008000');
};
// --------------------------------------------------------
// (2) Komponenten-Logik (ASYNCHRONER WORKAROUND & FIX)
// --------------------------------------------------------
const rehydrateLegacyReferences = (editor) => {
try {
const wrapper = editor.DomComponents?.getWrapper?.();
if (!wrapper) return;
const candidates = wrapper.find('[data-lib-kind][data-lib-id]');
if (!candidates || !candidates.length) return;
let patched = 0;
candidates.forEach((component) => {
const attrs = (typeof component.getAttributes === 'function') ? (component.getAttributes() || {}) : {};
const attrKind = component.get('lib-kind') || attrs['data-lib-kind'] || '';
const attrId = component.get('lib-id') || attrs['data-lib-id'] || '';
if (!attrKind || !attrId) return;
if (typeof component.syncReferenceAttributes === 'function') {
component.syncReferenceAttributes();
} else if (component.get && component.set && component.get('type') !== REFERENCE_COMPONENT_TYPE) {
const parent = component.parent && component.parent();
if (!parent || typeof parent.components !== 'function') return;
const atIndex = parent.components().indexOf(component);
const startContent = typeof component.toHTML === 'function' ? component.toHTML() : '';
const classes = component.get && typeof component.get === 'function'
? (component.get('classes') || [])
: [];
const normalizedClasses = Array.isArray(classes)
? classes
: (classes.models || classes.collection || []);
const newComponent = {
type: REFERENCE_COMPONENT_TYPE,
'lib-kind': attrKind,
'lib-id': attrId,
startContent,
attributes: {
...attrs,
'data-lib-kind': attrKind,
'data-lib-id': attrId,
'data-lib-ref': attrs['data-lib-ref'] || '1',
},
};
if (normalizedClasses && normalizedClasses.length) {
newComponent.classes = normalizedClasses.map((cls) => {
if (typeof cls === 'string') return { name: cls };
if (cls && typeof cls.get === 'function') return { name: cls.get('name') };
if (cls && cls.name) return { name: cls.name };
return null;
}).filter(Boolean);
}
component.remove();
parent.components().add(newComponent, { at: atIndex });
patched++;
}
});
if (patched) {
log(`REHYDRATE`, `${patched} Legacy-Referenzen in Referenz-Komponenten umgewandelt.`, '#228B22');
}
} catch (error) {
log('REF REHYDRATE ERROR', error?.message || String(error), '#dc3545', 'error', true);
}
};
const registerReferenceComponent = (editor) => {
const domc = editor.DomComponents;
const defaultType = domc.getType('default');
if (!defaultType) return;
log(`Starte Registrierung des Komponententyps '${REFERENCE_COMPONENT_TYPE}'.`, '#1E90FF');
const referenceRegistry = {};
const makeKey = (kind, id) => (kind && id) ? `${kind}::${id}` : null;
const registerReference = (key, model) => {
if (!key || !model) return;
if (!referenceRegistry[key]) referenceRegistry[key] = new Set();
referenceRegistry[key].add(model);
};
const unregisterReference = (key, model) => {
if (!key) return;
const set = referenceRegistry[key];
if (!set) return;
set.delete(model);
if (set.size === 0) delete referenceRegistry[key];
};
const cascadeReferenceUpdate = (kind, id, source) => {
const key = makeKey(kind, id);
if (!key) return;
const set = referenceRegistry[key];
if (!set) return;
set.forEach((model) => {
if (!model || model === source) return;
if (typeof model.reloadComponentContent === 'function') {
model.reloadComponentContent({ forced: true, reason: 'CASCADE', skipCascade: true });
}
});
};
setTimeout(() => {
const ReferenceModel = defaultType.model.extend({
initialize(props = {}, opts = {}) {
defaultType.model.prototype.initialize.apply(this, [props, opts]);
this.applyReferenceDefaults();
this.on('change:lib-kind change:lib-id', () => {
this.ensureReferenceMetadata();
this.updateReferenceRegistration();
this.reloadComponentContent();
});
this.ensureReferenceMetadata();
this.updateReferenceRegistration();
this.on('destroy remove', () => this.unregisterReferenceInstance());
const id = this.get('lib-id');
const kind = this.get('lib-kind');
const startContent = this.get('startContent');
log(`INIT LÄUFT. lib-kind: ${kind}, lib-id: ${id}.`, '#8A2BE2');
if (startContent) {
this.setPreviewHtml(startContent);
this.unset('startContent');
} else if (kind && id) {
this.reloadComponentContent({ forced: true, reason: 'INIT_LOAD' });
}
},
applyReferenceDefaults() {
const enforced = {
editable: false,
removable: true,
draggable: true,
copyable: true,
droppable: false,
'lib-id': this.get('lib-id') || '',
'lib-kind': this.get('lib-kind') || '',
rawHtml: this.get('rawHtml') || '',
};
Object.entries(enforced).forEach(([key, value]) => {
if (typeof this.get(key) === 'undefined') {
this.set(key, value, { silent: true });
}
});
const traits = this.get('traits');
const hasLibTraits = Array.isArray(traits) && traits.some(t => t?.name === 'lib-id' || t?.name === 'lib-kind');
if (!hasLibTraits) {
this.set('traits', [
{ type: 'text', name: 'lib-id', label: 'Library ID', changeProp: true },
{ type: 'text', name: 'lib-kind', label: 'Library Kind', changeProp: true },
], { silent: true });
}
const comps = this.components?.();
if (comps && typeof comps.reset === 'function' && comps.length) {
comps.reset([]);
}
},
ensureReferenceMetadata() {
const attrsCurrent = this.get('attributes') || {};
let attrs = Array.isArray(attrsCurrent) ? {} : { ...attrsCurrent };
const kind = this.get('lib-kind') || attrs['data-lib-kind'] || '';
const id = this.get('lib-id') || attrs['data-lib-id'] || '';
let changed = false;
if (!this.get('lib-kind') && kind) {
this.set('lib-kind', kind, { silent: true });
}
if (!this.get('lib-id') && id) {
this.set('lib-id', id, { silent: true });
}
if (attrs['data-lib-kind'] !== kind) {
attrs['data-lib-kind'] = kind;
changed = true;
}
if (attrs['data-lib-id'] !== id) {
attrs['data-lib-id'] = id;
changed = true;
}
if (attrs['data-lib-ref'] !== '1') {
attrs['data-lib-ref'] = '1';
changed = true;
}
if (changed) {
this.set('attributes', attrs);
}
},
getReferenceKey() {
return makeKey(this.get('lib-kind'), this.get('lib-id'));
},
updateReferenceRegistration() {
const nextKey = this.getReferenceKey();
if (this._refKey === nextKey) return;
if (this._refKey) unregisterReference(this._refKey, this);
if (nextKey) registerReference(nextKey, this);
this._refKey = nextKey;
},
unregisterReferenceInstance() {
if (this._refKey) {
unregisterReference(this._refKey, this);
this._refKey = null;
}
},
getCachedApiItem(kind, id) {
const key = `${kind}-${id}`;
const item = B.ApiItemCache?.[key];
return item || null;
},
fetchReference(kind, id) {
if (!kind || !id) return Promise.resolve(null);
const key = `${kind}-${id}`;
const cached = this.getCachedApiItem(kind, id);
if (cached && cached.html) return Promise.resolve(cached);
if (typeof B.getApiItem !== 'function') return Promise.resolve(cached);
return B.getApiItem(kind, id)
.then((data) => {
if (!data) return cached;
const normalized = {
html: data.html || data.item?.html || '',
content: data.content || data.item?.content || '',
};
B.ApiItemCache = B.ApiItemCache || {};
B.ApiItemCache[key] = { ...(B.ApiItemCache[key] || {}), ...normalized };
return B.ApiItemCache[key];
})
.catch(() => cached);
},
renderError(message) {
return `<div class="lib-ref-placeholder">${message}</div>`;
},
setPreviewHtml(html) {
const safeHtml = html || this.renderError('Referenz lädt …');
const alreadyDecorated = /^\s*<[^>]+data-lib-ref="1"/i.test(safeHtml);
let decoratedHtml = safeHtml;
if (!alreadyDecorated) {
const kind = this.get('lib-kind') || '';
const id = this.get('lib-id') || '';
const attrs = [
'data-lib-ref="1"',
kind ? `data-lib-kind="${kind}"` : '',
id ? `data-lib-id="${id}"` : '',
].filter(Boolean).join(' ');
decoratedHtml = `<div class="lib-ref-wrapper" ${attrs}>${safeHtml}</div>`;
}
this.set('rawHtml', decoratedHtml);
const comps = this.components();
if (comps && comps.length) comps.reset([]);
this.trigger('preview:update');
},
populatePlaceholder(el) {
if (!el || el.__libHydrated) return;
const kind = el.getAttribute('data-lib-kind');
const id = el.getAttribute('data-lib-id');
if (!kind || !id) return;
el.setAttribute('data-lib-ref', '1');
const applyHtml = (html) => {
if (typeof html === 'string' && html.length) {
el.innerHTML = html;
this.decoratePlaceholder(el, kind, id);
this.hydrateNestedReferences(el);
el.__libHydrated = true;
}
};
const cached = this.getCachedApiItem(kind, id);
if (cached && cached.html) {
applyHtml(cached.html);
}
this.fetchReference(kind, id).then((item) => {
if (item && item.html) applyHtml(item.html);
});
},
decoratePlaceholder(el, kind, id) {
el.setAttribute('data-lib-ref', '1');
if (kind) el.setAttribute('data-lib-kind', kind);
if (id) el.setAttribute('data-lib-id', id);
el.setAttribute('contenteditable', 'false');
el.style.pointerEvents = 'none';
el.style.userSelect = 'none';
},
hydrateNestedReferences(root) {
if (!root) return;
const placeholders = root.querySelectorAll('[data-lib-kind][data-lib-id]');
placeholders.forEach((node) => this.populatePlaceholder(node));
},
reloadComponentContent(opts = {}) {
const kind = this.get('lib-kind');
const id = this.get('lib-id');
const reason = opts.reason || (opts.forced ? 'FORCED' : 'AUTO');
log(`RELOAD START (${reason}). Kind: ${kind}, ID: ${id}.`, '#8A2BE2');
if (!kind || !id) {
log('RELOAD FEHLER: lib-kind oder lib-id fehlt.', '#dc3545', 'error', true);
this.setPreviewHtml(this.renderError('🛑 Fehler: Referenz unvollständig.'));
return;
}
const cached = this.getCachedApiItem(kind, id);
if (cached && cached.html) {
this.setPreviewHtml(cached.html);
}
this.fetchReference(kind, id)
.then((item) => {
if (item && item.html) {
this.setPreviewHtml(item.html);
log(`INHALT erfolgreich für ${kind}/${id} geladen.`, '#008000');
if (!opts.skipCascade) {
cascadeReferenceUpdate(kind, id, this);
}
} else {
log(`RELOAD FEHLER: Inhalt ${kind}/${id} nicht gefunden.`, '#dc3545', 'error', true);
this.setPreviewHtml(
this.renderError(`🛑 Fehler: Inhalt für ${kind}/${id} nicht gefunden.`)
);
}
})
.catch((error) => {
log('RELOAD FETCH ERROR', error?.message || String(error), '#dc3545', 'error', true);
this.setPreviewHtml(this.renderError('🛑 Fehler beim Laden der Referenz.'));
});
},
toHTML(opts = {}) {
const raw = this.get('rawHtml');
if (raw) return raw;
return defaultType.model.prototype.toHTML.call(this, opts);
},
syncReferenceAttributes() {
this.ensureReferenceMetadata();
},
}, {
isComponent: (el) => el && el.nodeType === 1 && el.hasAttribute('lib-id'),
});
const ReferenceView = defaultType.view.extend({
initialize(opts = {}) {
defaultType.view.prototype.initialize.apply(this, [opts]);
this.listenTo(this.model, 'preview:update', this.renderPreview);
},
render() {
defaultType.view.prototype.render.apply(this, arguments);
this.el.classList.add('lib-ref');
this.renderPreview();
return this;
},
renderPreview() {
const html = this.model.get('rawHtml') || this.model.renderError('Referenz lädt …');
this.el.innerHTML = '';
const wrap = document.createElement('div');
wrap.className = 'lib-ref-inner';
wrap.innerHTML = html;
wrap.setAttribute('contenteditable', 'false');
wrap.style.pointerEvents = 'none';
wrap.style.userSelect = 'none';
this.el.appendChild(wrap);
this.model.hydrateNestedReferences(wrap);
},
});
domc.addType(REFERENCE_COMPONENT_TYPE, {
model: ReferenceModel,
view: ReferenceView,
});
log(`Komponententyp '${REFERENCE_COMPONENT_TYPE}' registriert.`, '#008000');
}, 0);
};
// --------------------------------------------------------
// (3) HINZUGEFÜGT: Speichern-Befehl (Command)
// --------------------------------------------------------
const registerSaveCommand = (editor) => {
editor.Commands.add('save-data', {
run: function(editor, sender) {
const writeDebugLog = (payload) => {
try {
const base = API_KERNEL_URL || '/api.php';
const line = JSON.stringify({ time: new Date().toISOString(), ...payload });
fetch(base, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'debug.log.write',
name: 'ui_save_sync.log',
append: 1,
line,
}),
}).catch(() => {});
} catch {}
};
// 💡 FIX: Sicherstellen, dass sender existiert und die 'set'-Methode hat (nur bei Buttons)
if (sender && typeof sender.set === 'function') {
sender.set('active', 0); // Schaltet den Button nach dem Klick ab
}
if (!CURRENT_ENTITY_ID) {
log('SAVE ABORT', 'Speichern abgebrochen: Keine Entity ID verfügbar (B.CURRENT_ENTITY_ID fehlt oder ist 0).', 'red', 'error', true);
alert('Speichern fehlgeschlagen: Die ID des aktuellen Elements fehlt.');
return;
}
// Kein aktiver Sync vor dem Speichern, nur Logging
try {
const doc = editor.Canvas && editor.Canvas.getDocument ? editor.Canvas.getDocument() : null;
const active = doc && doc.activeElement;
const selectionNode = doc && doc.getSelection ? (doc.getSelection().focusNode || doc.getSelection().anchorNode) : null;
const selectionEl = selectionNode ? (selectionNode.nodeType === 1 ? selectionNode : selectionNode.parentElement) : null;
const selected = editor.getSelected && editor.getSelected();
writeDebugLog({
event: 'save:sync:skipped',
entityId: CURRENT_ENTITY_ID,
sectionId: SECTION_ID || null,
active: active ? {
tag: active.tagName,
isEditable: !!(active.isContentEditable || (active.getAttribute && active.getAttribute('contenteditable') === 'true')),
} : null,
selection: selectionEl ? {
tag: selectionEl.tagName,
isEditable: !!(selectionEl.isContentEditable || (selectionEl.getAttribute && selectionEl.getAttribute('contenteditable') === 'true')),
} : null,
selected: selected ? {
id: selected.getId ? selected.getId() : (selected.get && selected.get('id')),
type: selected.get ? selected.get('type') : null,
contentLen: selected.get ? String(selected.get('content') || '').length : 0,
} : null,
editorHtmlLen: (editor.getHtml && String(editor.getHtml() || '').length) || 0,
editorHtmlPreview: (editor.getHtml && String(editor.getHtml() || '').slice(0, 300)) || '',
});
} catch {}
// 1. Daten extrahieren
const fontCss = (B && typeof B.RTE_FONT_FACE_CSS === 'string' && B.RTE_FONT_FACE_CSS.trim())
? B.RTE_FONT_FACE_CSS.trim()
: '';
const cssPayload = (fontCss ? fontCss + '\n' : '') + editor.getCss();
const serializeHtml = () => {
let rawHtml = editor.getHtml() || '';
try {
const doc = editor.Canvas && editor.Canvas.getDocument ? editor.Canvas.getDocument() : null;
const body = doc && doc.body;
const liveHtml = body ? String(body.innerHTML || '') : '';
if (liveHtml && liveHtml.length >= rawHtml.length * 0.8) {
// Remove contenteditable artifacts from live DOM HTML
rawHtml = liveHtml.replace(/\scontenteditable="[^"]*"/gi, '');
}
} catch {}
if (B && B.BridgeRTE && typeof B.BridgeRTE.serializeHtml === 'function') {
try {
let patched = B.BridgeRTE.serializeHtml(editor) || '';
if (patched) {
const bodyMatch = patched.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
if (bodyMatch) patched = bodyMatch[1];
}
if (!patched || (rawHtml && patched.length < rawHtml.length * 0.6)) {
return rawHtml;
}
return patched;
} catch {
return rawHtml;
}
}
return rawHtml;
};
const cleanEmptyDefaultPsHtml = (html) => {
try {
const wrap = document.createElement('div');
wrap.innerHTML = html || '';
const nodes = wrap.querySelectorAll('p[data-gjs-type="default"]');
nodes.forEach((p) => {
const inner = (p.innerHTML || '').trim();
// Remove only truly empty default paragraphs (no content, no <br>, no &nbsp;)
if (!inner) p.remove();
});
return wrap.innerHTML || '';
} catch {
return html || '';
}
};
const htmlBody = cleanEmptyDefaultPsHtml(serializeHtml());
const extractIdsFromHtml = (html) => {
try {
const wrap = document.createElement('div');
wrap.innerHTML = html || '';
const ids = new Set();
wrap.querySelectorAll('[id]').forEach((el) => {
const id = el.getAttribute('id');
if (id) ids.add(id);
});
return ids;
} catch {
return new Set();
}
};
const idsInHtml = extractIdsFromHtml(htmlBody);
const stripUnusedIdCss = (css, ids) => {
if (!css || !ids || !ids.size) return css || '';
return String(css).replace(/#([A-Za-z0-9_-]+)\s*\{[^}]*\}/g, (m, id) => (ids.has(id) ? m : ''));
};
const cssPayloadClean = stripUnusedIdCss(cssPayload, idsInHtml);
const htmlContent = htmlBody + '<style>' + cssPayloadClean + '</style>';
// 2. KRITISCH: Holt die JSON-Repräsentation des Editors
let jsonProjectDataRaw = '';
try {
const jsonProjectData = editor.getProjectData();
const cleanEmptyDefaultPsJson = (node) => {
if (!node || typeof node !== 'object') return;
const walkArray = (arr, assignTo) => {
if (!Array.isArray(arr)) return;
const cleaned = [];
for (const child of arr) {
let remove = false;
if (child && child.type === 'default' && child.tagName === 'p') {
const attrs = child.attributes || {};
const attrKeys = Object.keys(attrs);
const allowedAttrs = new Set(['id', 'draggable']);
const onlyAllowedAttrs = attrKeys.every((k) => allowedAttrs.has(k));
const hasContent = (child.content && String(child.content).trim()) || (child.components && child.components.length);
const hasStyle = child.style && Object.keys(child.style).length > 0;
if (!hasContent && !hasStyle && onlyAllowedAttrs) {
remove = true;
}
}
if (!remove) {
cleanEmptyDefaultPsJson(child);
cleaned.push(child);
}
}
assignTo(cleaned);
};
if (Array.isArray(node.pages)) {
walkArray(node.pages, (v) => { node.pages = v; });
}
if (Array.isArray(node.frames)) {
walkArray(node.frames, (v) => { node.frames = v; });
}
if (node.component && Array.isArray(node.component.components)) {
walkArray(node.component.components, (v) => { node.component.components = v; });
}
if (Array.isArray(node.components)) {
walkArray(node.components, (v) => { node.components = v; });
}
};
cleanEmptyDefaultPsJson(jsonProjectData);
if (idsInHtml && idsInHtml.size && Array.isArray(jsonProjectData.styles)) {
jsonProjectData.styles = jsonProjectData.styles.filter((rule) => {
if (!rule || rule.atRuleType) return true;
const sels = Array.isArray(rule.selectors) ? rule.selectors : [];
const selAdd = rule.selectorsAdd || '';
if (sels.length === 1 && typeof sels[0] === 'string' && sels[0].startsWith('#')) {
const id = sels[0].slice(1);
return idsInHtml.has(id);
}
if (!sels.length && typeof selAdd === 'string' && selAdd.startsWith('#')) {
const id = selAdd.slice(1);
return idsInHtml.has(id);
}
return true;
});
}
jsonProjectDataRaw = JSON.stringify(jsonProjectData);
} catch (e) {
console.error('[bridge-blocks-api] getProjectData stringify failed', e);
jsonProjectDataRaw = '';
}
// Wenn gerade aktiv editiert wird, JSON durch HTML ersetzen,
// damit der Live-Text ohne Blur gespeichert wird.
try {
const doc = editor.Canvas && editor.Canvas.getDocument ? editor.Canvas.getDocument() : null;
const selectionNode = doc && doc.getSelection ? (doc.getSelection().focusNode || doc.getSelection().anchorNode) : null;
const selectionEl = selectionNode ? (selectionNode.nodeType === 1 ? selectionNode : selectionNode.parentElement) : null;
const isEditable = !!(selectionEl && (selectionEl.isContentEditable || (selectionEl.getAttribute && selectionEl.getAttribute('contenteditable') === 'true')));
if (isEditable && htmlBody) {
jsonProjectDataRaw = htmlBody;
}
} catch {}
const resource = 'content';
const action = `${resource}.update`;
const debugSave = (() => {
try {
const params = new URLSearchParams(window.location.search || '');
if (params.get('debug_save') === '1') return true;
return localStorage.getItem('et_debug_save') === '1';
} catch (e) {
return false;
}
})();
log('SAVE START', 'Starte Speichern des Inhalts an die API...', '#FF4500');
// 3. Daten für den POST-Request vorbereiten
const dataToSend = {
action,
id: CURRENT_ENTITY_ID,
html: htmlContent,
// 🚨 KRITISCH: Server erwartet das Feld 'json'
json: jsonProjectDataRaw,
};
const versionId = Number(B.CURRENT_VERSION_ID || window.CURRENT_VERSION_ID || 0);
if (versionId > 0) {
dataToSend.version_id = versionId;
}
const activateNext = B.NEXT_ACTIVATE_VERSION || window.NEXT_ACTIVATE_VERSION;
if (activateNext) {
dataToSend.activate_version = 1;
B.NEXT_ACTIVATE_VERSION = 0;
window.NEXT_ACTIVATE_VERSION = 0;
}
if (SECTION_ID) {
dataToSend.section_id = SECTION_ID;
}
if (debugSave) {
dataToSend.debug = 1;
console.log('[ET DEBUG] save-data payload', {
id: CURRENT_ENTITY_ID,
mode: resource,
htmlLength: htmlContent.length,
jsonLength: jsonProjectDataRaw.length,
htmlPreview: htmlContent.slice(0, 200),
jsonPreview: jsonProjectDataRaw.slice(0, 200),
});
}
if (B && B.DEBUG_RTE) {
console.group('[RTE DEBUG] save-data');
console.log('htmlLength', htmlContent.length);
console.log('jsonLength', jsonProjectDataRaw.length);
console.log('html', htmlContent);
console.log('json', jsonProjectDataRaw);
console.groupEnd();
}
if (B.CURRENT_ENTITY_NAME) {
dataToSend.name = B.CURRENT_ENTITY_NAME;
}
// 4. API-Aufruf (fetch)
fetch(API_KERNEL_URL, {
method: 'POST',
headers: {
// Wichtig: JSON-Daten senden
'Content-Type': 'application/json', 
},
body: JSON.stringify(dataToSend),
})
.then(response => {
if (!response.ok) {
log('SAVE FAILED (HTTP)', `Speichern fehlgeschlagen: HTTP-Status ${response.status}.`, 'red', 'error', true);
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.ok === false) {
log('SAVE FAILED (API)', `Speichern fehlgeschlagen: API-Fehler: ${data.error || 'Unbekannt'}`, 'red', 'error', true);
alert(`Speichern fehlgeschlagen: ${data.error || 'API-Fehler'}`);
} else {
log('SAVE SUCCESS', `Speichern erfolgreich für Aktion ${action}.`, '#008000', 'info');
// 💡 HINZUGEFÜGT: Bestätigung an das Elternfenster senden
window.parent.postMessage({ source: 'editor', type: 'save:success' }, '*');
// Kein refresh, um UI-Text nicht zu verlieren falls noch nicht gesynct
}
})
.catch(error => {
log('SAVE FAILED (FETCH)', `FEHLER beim Speichern: ${error.message}`, 'red', 'error', true);
alert('Speichern fehlgeschlagen. Netzwerk- oder JSON-Parse-Fehler.');
});
}
});
// Eventuell den Button in der Toolbar registrieren (falls noch nicht geschehen)
editor.Panels.addButton('options', {
id: 'save-data',
className: 'fa fa-floppy-o',
command: 'save-data', 
attributes: { title: 'Speichern (Strg/Cmd + S)' }
});
// Tastenkürzel für Speichern hinzufügen
editor.Keymaps.add('save-data-ctrl', 'ctrl+s', 'save-data');
editor.Keymaps.add('save-data-cmd', 'cmd+s', 'save-data');
log('Speichern-Command und Button/Keymap registriert.', '#FF4500');
};
// --------------------------------------------------------
// (4) Plugin-Funktion (AKTUALISIERT)
// --------------------------------------------------------
const plugin = (editor) => {
preRegisterCategoriesAndPlaceholders(editor);
registerReferenceComponent(editor);
registerSaveCommand(editor); // HINZUGEFÜGT: Speichern-Logik
editor.on('load', () => {
rehydrateLegacyReferences(editor);
log("GrapesJS 'load' Event: Delegiere asynchrones Laden der API-Blöcke an library-api.", '#1E90FF');
if (B.loadAndRegisterApiBlocks) { 
setTimeout(() => {
B.loadAndRegisterApiBlocks(editor);
}, 500);
} else {
log(`FEHLER: B.loadAndRegisterApiBlocks ist nicht definiert. library-api.js wurde nicht geladen oder nicht richtig initialisiert.`, 'red', 'error', true);
editor.BlockManager.remove(PLACEHOLDER_ID);
}
});
};
// --------------------------------------------------------
// (5) Export an Bridge Core (unverändert)
// --------------------------------------------------------
if (B.registerGrapesJSPlugin) {
B.registerGrapesJSPlugin(PluginName, plugin);
log(`PLUGIN REGISTER: '${PluginName}' zur Bridge Plugin Registry hinzugefügt.`, '#008000');
} else {
log(`FEHLER: B.registerGrapesJSPlugin fehlt. Plugin-Registrierung gescheitert.`, 'red', 'error', true);
}
})(window.BridgeParts || (window.BridgeParts = {}));