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 ? `
` : '',