diff --git a/public/assets/js/bridge/blocks-api.js b/public/assets/js/bridge/blocks-api.js index afb1bf3..6471742 100644 --- a/public/assets/js/bridge/blocks-api.js +++ b/public/assets/js/bridge/blocks-api.js @@ -54,6 +54,73 @@ // -------------------------------------------------------- // (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'); @@ -62,96 +129,198 @@ log(`Starte Registrierung des Komponententyps '${REFERENCE_COMPONENT_TYPE}'.`, '#1E90FF'); - setTimeout(() => { - domc.addType(REFERENCE_COMPONENT_TYPE, { - model: defaultType.model.extend({ - - getCachedApiItem(kind, id) { - const key = `${kind}-${id}`; - const item = B.ApiItemCache?.[key];  - return item; - }, - - init() { - 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}. (Bestätigung des Element-Drops/Load)`, '#8A2BE2'); - - if (startContent) { - // 💡 NEUER FIX: Beim Drop nur die 'content'-Eigenschaft setzen, NICHT als Unterkomponenten parsen - this.set('content', startContent);   - this.unset('startContent'); - log(`INHALT erfolgreich als REINES HTML aus 'startContent' gesetzt: ${kind}/${id}`, '#008000'); - } - - this.on('change:lib-kind change:lib-id', this.reloadComponentContent); - - if (!startContent && kind && id) { - this.reloadComponentContent({ forced: true, reason: 'INIT_LOAD_FROM_CACHE' }); - } - }, - - reloadComponentContent(opts = {}) { - const kind = this.get('lib-kind'); - const id = this.get('lib-id'); - const reason = opts.reason || (opts.forced ? 'FORCED_INTERNAL' : 'EVENT_CHANGE'); - log(`RELOAD START (${reason}). Kind: ${kind}, ID: ${id}.`, '#8A2BE2'); - - if (!kind || !id) { - log('RELOAD FEHLER: lib-kind oder lib-id fehlt. Setze Fehler-Placeholder.', '#dc3545', 'error', true); - // 💡 FIX: Setze reinen HTML-String als content - this.set('content', '
🛑 Fehler: API-Referenz unvollständig.
'); - return; - } - - const item = this.getCachedApiItem(kind, id); - - if (item && (item.html || item.content)) { - const content = item.html || item.content; - // 💡 FIX: Verwende set('content', ...) statt components(...) - // Dadurch wird der Inhalt als reiner HTML-String in die Komponente gesetzt - // und nicht als neue, bearbeitbare GrapesJS-Komponenten geparst. - this.set('content', content); - log(`INHALT erfolgreich für ${kind}/${id} geladen und als REINER HTML-STRING gesetzt.`, '#008000'); - } else { - log(`RELOAD FEHLER: Inhalt für ${kind}/${id} NICHT im Cache gefunden.`, '#dc3545', 'error', true); - // 💡 FIX: Setze reinen HTML-HTML-String als content - this.set('content', `
🛑 Fehler: Inhalt für ${kind}/${id} nicht im Cache gefunden.
`); - } - }, - }, { - isComponent: el => el && el.nodeType === 1 && el.hasAttribute('lib-id'), - extend: 'default', - model: { - defaults: { - ...defaultType.model.prototype.defaults, - // 🛑 KRITISCHE FIXES FÜR REFERENZEN - components: '', // Darf keine Unterkomponenten haben, die geparst werden - editable: false, // ❌ Nicht bearbeitbar (Inline-Editierung verhindern) - removable: true, - draggable: true, - copyable: true, - droppable: false, // ❌ Darf keine anderen Komponenten aufnehmen - // --------------------------------- - traits: [ - { type: 'text', name: 'lib-id', label: 'Library ID', changeProp: true }, - { type: 'text', name: 'lib-kind', label: 'Library Kind', changeProp: true }, - ], - 'lib-id': '', - 'lib-kind': '', - startContent: '', - content: '', // Inhalt, der das gerenderte HTML hält - } + const ReferenceModel = defaultType.model.extend({ + defaults: { + ...defaultType.model.prototype.defaults, + components: [], + editable: false, + removable: true, + draggable: true, + copyable: true, + droppable: false, + traits: [ + { type: 'text', name: 'lib-id', label: 'Library ID', changeProp: true }, + { type: 'text', name: 'lib-kind', label: 'Library Kind', changeProp: true }, + ], + 'lib-id': '', + 'lib-kind': '', + startContent: '', + rawHtml: '', + }, + + initialize(props = {}, opts = {}) { + defaultType.model.prototype.initialize.apply(this, [props, opts]); + + this.on('change:lib-kind change:lib-id', () => { + this.ensureReferenceMetadata(); + this.reloadComponentContent(); + }); + + this.ensureReferenceMetadata(); + 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' }); } - }), - // 💡 WICHTIG: Die View muss den Content als reinen HTML-Inhalt rendern (defaultType macht das). - view: defaultType.view,  + }, + + 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); + } + }, + + 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 …'); + this.set('rawHtml', safeHtml); + const comps = this.components(); + if (comps && comps.length) comps.reset([]); + this.trigger('preview:update'); + }, + + 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'); + } 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'), }); - log(`Komponententyp '${REFERENCE_COMPONENT_TYPE}' ASYNCHRON registriert.`, '#008000'); - }, 0); + 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); + }, + }); + + domc.addType(REFERENCE_COMPONENT_TYPE, { + model: ReferenceModel, + view: ReferenceView, + }); + + log(`Komponententyp '${REFERENCE_COMPONENT_TYPE}' registriert.`, '#008000'); }; // -------------------------------------------------------- @@ -260,6 +429,7 @@ 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(() => { diff --git a/public/assets/js/bridge/library-api.js b/public/assets/js/bridge/library-api.js index e5eb60c..9eff466 100644 --- a/public/assets/js/bridge/library-api.js +++ b/public/assets/js/bridge/library-api.js @@ -203,16 +203,21 @@ const blockDefinition = { label: label, category: TARGET_CAT_ID, -                             // 💡 KORREKTUR: Immer die library-reference-Komponente verwenden, um die Referenz-Logik -                             // (mit editable: false) aus blocks-api.js zu erzwingen. +                            // 💡 KORREKTUR: Immer die library-reference-Komponente verwenden, um die Referenz-Logik +                            // (mit editable: false) aus blocks-api.js zu erzwingen. content: { type: REFERENCE_COMPONENT_TYPE, 'lib-kind': item.kind, 'lib-id': item.id, -                                 // NEU: startContent wird nur als reines HTML übergeben. -                                 // Die Logik in blocks-api.js (init/reloadComponentContent) kümmert sich um die Anzeige. + attributes: { + 'data-lib-kind': item.kind, + 'data-lib-id': item.id, + 'data-lib-ref': '1', + }, +                                  // NEU: startContent wird nur als reines HTML übergeben. +                                  // Die Logik in blocks-api.js (init/reloadComponentContent) kümmert sich um die Anzeige. startContent: item.html || item.content || '
🛑 Fehler: Inhalt fehlte beim Laden.
', -                                 content: '', // Wichtig: Beim Drop keinen GrapesJS-Content setzen +                                  content: '', // Wichtig: Beim Drop keinen GrapesJS-Content setzen }, attributes: { 'title': itemKindUpper }, media: item.preview_url ? `` : '',