/* /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: 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 `
${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); }; // -------------------------------------------------------- // (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 aktives Sync mehr 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(); <<<<<<< ours let syncResult = null; let syncReason = 'no-editable-selection'; const syncFromElement = (comp, el, reason) => { if (!comp || !comp.set || !el) return false; const html = el.innerHTML || ''; const before = typeof comp.get === 'function' ? (comp.get('content') || '') : ''; if (html.trim() || !String(before || '').trim()) { comp.set('content', html); comp.trigger && comp.trigger('change:content'); syncResult = { compId: comp.getId ? comp.getId() : (comp.get && comp.get('id')), beforeLen: String(before || '').length, afterLen: String(html || '').length, reason, }; syncReason = reason; return true; } return false; }; <<<<<<< ours <<<<<<< ours <<<<<<< ours // 1) Selection im DOM suchen (bevorzugt) ======= ======= >>>>>>> theirs // 1) Bevorzugt: ausgewähltes Component-Element aus der View if (selected) { const selEl = (selected.getEl && selected.getEl()) || (selected.view && selected.view.el); if (selEl && (selEl.isContentEditable || (selEl.getAttribute && selEl.getAttribute('contenteditable') === 'true'))) { syncFromElement(selected, selEl, 'selected:view'); } <<<<<<< ours } // 2) Fallback: Selection im DOM suchen >>>>>>> theirs ======= // 1) Wenn ein ContentEditable aktiv ist, Blur auslösen, damit GrapesJS den Model-Content übernimmt if (active && (active.isContentEditable || (active.getAttribute && active.getAttribute('contenteditable') === 'true'))) { try { active.dispatchEvent(new Event('blur', { bubbles: true })); syncReason = 'blur:dispatched'; } catch {} ======= >>>>>>> theirs } // 2) Fallback: Selection im DOM suchen >>>>>>> theirs if (!syncResult && doc && selectionEl && (selectionEl.isContentEditable || (selectionEl.getAttribute && selectionEl.getAttribute('contenteditable') === 'true'))) { const root = (selectionEl.closest && selectionEl.closest('[data-gjs-type="text"]')) || selectionEl; const id = root && root.getAttribute ? root.getAttribute('id') : null; const wrapper = editor.getWrapper && editor.getWrapper(); if (id && wrapper && wrapper.find) { const found = wrapper.find(`#${id}`); const comp = found && found[0]; syncFromElement(comp, root, 'selection:root'); } } ======= >>>>>>> theirs 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 = () => { const rawHtml = editor.getHtml() || ''; 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 htmlContent = serializeHtml() + ''; // 2. KRITISCH: Holt die JSON-Repräsentation des Editors let jsonProjectDataRaw = ''; try { const jsonProjectData = editor.getProjectData(); jsonProjectDataRaw = JSON.stringify(jsonProjectData); } catch (e) { console.error('[bridge-blocks-api] getProjectData stringify failed', e); jsonProjectDataRaw = ''; } 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' }, '*'); editor.refresh(); // Optional: Editor-Ansicht aktualisieren } }) .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 = {}));