/* /assets/js/bridge/library-api.js (FINAL & KORRIGIERT FÜR FLEXIBLE API-BASES) */ (function(B){ // 🛑 WICHTIG: Globalen Cache-Speicher initialisieren (wird von blocks-api.js gelesen) B.ApiItemCache = B.ApiItemCache || {}; if (!B || typeof grapesjs === 'undefined') return; const PluginName = 'bridge-library-api'; // NEU: Standard-API-Basis für Abwärtskompatibilität, falls nichts konfiguriert const API_BASE_FALLBACK = (B.API_BASE || '/api/editor'); // Konstanten const TARGET_CAT_ID = 'custom'; const PLACEHOLDER_ID = 'api-placeholder-loading'; const REFERENCE_COMPONENT_TYPE = 'library-reference'; if (B.LOG_CONFIG && B.LOG_CONFIG.PLUGINS) { B.LOG_CONFIG.PLUGINS[PluginName] = true; } const log = (type, message, color = '#6A5ACD', logType = 'info', force = false) => { if (typeof B.log === 'function') { B.log(PluginName, `[${type}] ${message}`, color, logType, force); } else { if (logType === 'error') { console.error(`%c[${PluginName} - ${type}] %c${message}`, 'color:red; font-weight:bold;', 'color:inherit;'); } } }; const logApiData = (data) => B.logData(PluginName, data); log('INIT', 'API-Schicht initialisiert.'); // --- HILFSFUNKTIONEN --- /** * Gibt die korrekte API-Basis-URL für einen Ressourcentyp (kind) zurück. * Nutzt die zentrale Map B.RESOURCE_API_BASES, die in category-config.js gefüllt wurde. */ const getApiBase = (resource) => { // Fallback auf die konfigurierte Standard-Basis, falls die Map noch nicht existiert oder der Eintrag fehlt. return (B.RESOURCE_API_BASES && B.RESOURCE_API_BASES[resource]) || API_BASE_FALLBACK; }; const buildApiUrl = (resource, action='list', params = {}) => { // KORREKTUR: Nutzt jetzt die dynamisch ermittelte API-Basis const apiBase = getApiBase(resource); const url = new URL(apiBase, window.location.origin); url.searchParams.set('resource', resource); url.searchParams.set('action', action); Object.entries(params).forEach(([key, value]) => { if (value !== null && value !== undefined) url.searchParams.set(key, value); }); return url.toString(); }; const shouldLoad = (resource) => { if (Array.isArray(B.ALLOWED_SECTION_SLUGS) && B.ALLOWED_SECTION_SLUGS.length) { return B.ALLOWED_SECTION_SLUGS.includes(resource); } const mode = (B.EDITOR_MODE || 'TEMPLATES').toUpperCase(); switch (mode) { case 'TEMPLATES': return true; default: return true; } }; const fetchSectionsConfig = async () => { const rows = await fetchData('sections_config', 'list'); return Array.isArray(rows) ? rows : []; }; const resolveAllowedSections = async () => { const sections = await fetchSectionsConfig(); if (!sections.length) return []; const modeSlug = String(B.EDITOR_MODE || '').toLowerCase(); const current = sections.find(s => String(s.slug || '').toLowerCase() === modeSlug); if (!current) return sections; return sections.filter(s => Number(s.position) > Number(current.position)); }; const fetchData = (resource, action='list', params = {}) => { // ... (Rest der fetchData-Funktion bleibt unverändert, nutzt aber die korrigierte buildApiUrl) const url = buildApiUrl(resource, action, params); const cacheKey = action === 'get' ? `${resource}-${params.id}` : null; // Cache-Check verwendet B.ApiItemCache if (cacheKey && B.ApiItemCache.hasOwnProperty(cacheKey)) { log('CACHE HIT', `Cache Hit für /${resource}-${cacheKey}.`, '#708090', 'info'); return Promise.resolve(B.ApiItemCache[cacheKey]); } return fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }) .then(response => { if (!response.ok) { log('API ERROR', `API-Aufruf fehlgeschlagen für /${resource}/${action}: ${response.status} (${response.statusText})`, 'red', 'error'); // 💡 KORREKTUR: Bei HTTP-Fehler immer ein leeres Array für LIST und leeres Objekt für GET zurückgeben. return action === 'get' ? {} : { items: [] }; } return response.json(); }) .then(data => { if (data.ok === false) { log('API ERROR', `API-Fehler für /${resource}: ${data.error || 'Unbekannt'}`, 'red', 'error'); // 💡 KORREKTUR: Bei API-Fehler ('ok: false') immer leeres Array/Objekt zurückgeben. return action === 'get' ? {} : { items: [] }; } const result = data.items || data.data || data.item; let finalResult = result ? (Array.isArray(result) ? result : (action === 'list' ? (result.items || []) : result)) : (action === 'list' ? [] : {}); const resultIsArray = Array.isArray(finalResult); const resultLength = resultIsArray ? finalResult.length : (Object.keys(finalResult).length > 0 ? 1 : 0); log('EXTRACT SUCCESS', `Extrahiert ${resultLength} Elemente (Typ: ${action}) für /${resource}.`); // Bei GET-Antworten stellt der Server häufig html/content separat bereit. if (action === 'get' && finalResult && !resultIsArray) { if (data && typeof data.html !== 'undefined' && !('html' in finalResult)) { finalResult.html = data.html; } if (data && typeof data.content !== 'undefined' && !('content' in finalResult)) { finalResult.content = data.content; } } // Cache-Speicherung verwendet B.ApiItemCache if (cacheKey && resultLength > 0) { B.ApiItemCache[cacheKey] = finalResult; } // 💡 KORREKTUR: Bei LIST (action='list') geben wir immer ein Array zurück, sonst das Objekt return finalResult; }) .catch(error => { log('FETCH ERROR', `FEHLER beim Fetchen oder Parsen von /${resource}: ${error.message}`, 'red', 'error', true); return action === 'get' ? {} : []; }); }; // --- Exportierte Core-Funktionen (jetzt generisch) --- // NEU: Generische Fetch-Funktion für jeden Ressourcentyp ('kind') B.fetchResource = (kind) => { if (B.USE_DYNAMIC_SECTIONS && ['templates','sections','blocks','snippets'].includes(String(kind || '').toLowerCase())) { return Promise.resolve([]); } if (!shouldLoad(kind)) { log('BLOCKED', `Blockiert: ${kind} (Modus: ${B.EDITOR_MODE})`, '#708090', 'info'); return Promise.resolve([]); } return fetchData(kind).then(items => Array.isArray(items) ? items : []); }; // Die alten hardcodierten Funktionen verwenden jetzt die neue generische Funktion B.fetchTemplates = () => B.fetchResource('templates'); B.fetchSnippets = () => B.fetchResource('snippets'); B.fetchSections = () => B.fetchResource('sections'); B.fetchBlocks = () => B.fetchResource('blocks'); B.getApiItem = (kind, id) => fetchData(kind, 'get', { id: id }); B.clearApiCache = () => { B.ApiItemCache = {}; // Cache leeren log('CACHE CLEAR', `API-Cache geleert.`, 'orange', 'warn'); }; // 🚀 Zentrale Funktion zum Laden und Registrieren der Blöcke B.loadAndRegisterApiBlocks = (editor) => { const bm = editor.BlockManager; const loadDynamic = async () => { const sections = await resolveAllowedSections(); B.ALLOWED_SECTION_SLUGS = sections.map(s => String(s.slug || '').toLowerCase()); if (!sections.length) { bm.remove(PLACEHOLDER_ID); return []; } const promises = sections.map(section => fetchData('content', 'list', { section_id: section.id }) .then(items => (Array.isArray(items) ? items : []).map(i => ({ ...i, kind: String(section.slug || '').toLowerCase(), section_name: section.name || '', }))) ); const results = await Promise.all(promises); return results.flat(); }; const startLoad = (B.USE_DYNAMIC_SECTIONS ? loadDynamic() : Promise.resolve([])); log('API START', 'Starte API-Abruf für dynamische Sections...', '#1E90FF'); startLoad .then(apiItems => { const filtered = Array.isArray(apiItems) ? apiItems.filter(item => item && item.id) : []; log(`API SUCCESS`, `${filtered.length} Elemente gefunden.`, '#9400D3'); logApiData(filtered); if (filtered.length === 0) { log('NO DATA', 'Keine API-Daten gefunden.', 'orange', 'warn', true); bm.remove(PLACEHOLDER_ID); } else { filtered.forEach(item => { const blockId = `lib-${item.kind}-${item.id}`; const label = item.name || item.label || 'Unbenannter Block'; const itemKindUpper = item.kind.toUpperCase(); // Hier wird der Block-Manager-Block registriert // ... (Der Rest der Logik bleibt unverändert) ... 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. content: { type: REFERENCE_COMPONENT_TYPE, 'lib-kind': item.kind, 'lib-id': item.id, 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 || '