Files
emailtemplate.it/public/assets/js/bridge/blocks-api.js
2025-12-06 02:16:30 +01:00

458 lines
16 KiB
JavaScript
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();
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');
setTimeout(() => {
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' });
}
},
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 `<div class="lib-ref-placeholder">${message}</div>`;
},
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'),
});
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');
}, 0);
};
// --------------------------------------------------------
// (3) HINZUGEFÜGT: Speichern-Befehl (Command)
// --------------------------------------------------------
const registerSaveCommand = (editor) => {
editor.Commands.add('save-data', {
run: function(editor, sender) {
// 💡 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;
}
// 1. Daten extrahieren
const htmlContent = editor.getHtml() + '<style>' + editor.getCss() + '</style>';
// 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 = EDITOR_MODE;
const action = `${resource}.update`;
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,
};
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', true);
// 💡 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('ctrl-s', 'save-data', 'ctrl+s');
editor.Keymaps.add('cmd-s', 'save-data', 'cmd+s');
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 = {}));