/* /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: '
⚙️ Custom-Blöcke werden geladen...
', 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: true, '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 `
${message}
`; }, 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 = `
${safeHtml}
`; } 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); }; const ensureReferenceDropHandling = (editor) => { if (!editor || editor.__bridgeRefDropPatch) return; editor.__bridgeRefDropPatch = true; editor.on('component:add', (cmp) => { try { if (!cmp || typeof cmp.parent !== 'function') return; const parent = cmp.parent(); if (!parent || typeof parent.get !== 'function') return; if (parent.get('type') !== REFERENCE_COMPONENT_TYPE) return; const grand = parent.parent && parent.parent(); if (!grand || typeof grand.components !== 'function') { cmp.remove(); return; } const at = grand.components().indexOf(parent) + 1; grand.append(cmp, { at }); } catch {} }); }; // -------------------------------------------------------- // (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(/]*>([\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
, no  ) 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 + ''; // 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); ensureReferenceDropHandling(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 = {}));