276 lines
12 KiB
JavaScript
276 lines
12 KiB
JavaScript
/* /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, active_only: 1 })
|
||
.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 || '<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler: Inhalt fehlte beim Laden.</div>',
|
||
content: '', // Wichtig: Beim Drop keinen GrapesJS-Content setzen
|
||
},
|
||
attributes: { 'title': itemKindUpper },
|
||
media: item.preview_url ? `<img src="${item.preview_url}">` : '',
|
||
};
|
||
bm.add(blockId, blockDefinition);
|
||
});
|
||
|
||
bm.remove(PLACEHOLDER_ID);
|
||
log(`REGISTRATION`, `${apiItems.length} API-Blöcke registriert. Platzhalter entfernt.`, '#008000');
|
||
|
||
const reloadExistingComponents = () => {
|
||
const allComponents = editor.DomComponents.getWrapper().find(`[data-gjs-type="${REFERENCE_COMPONENT_TYPE}"]`);
|
||
allComponents.forEach(component => {
|
||
if (component.get('lib-id') && component.components().length === 0 && typeof component.reloadComponentContent === 'function') {
|
||
log(`RELOAD START`, `Lade ${component.get('lib-kind')}/${component.get('lib-id')} nach Cache-Füllung (Sicherheitsnetz).`, '#FF4500');
|
||
component.reloadComponentContent({ forced: true, reason: 'EXISTING_CONTENT_RELOAD' });
|
||
}
|
||
});
|
||
};
|
||
|
||
setTimeout(reloadExistingComponents, 100);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
// Hier wird der Fehler von fetchData oder map abgefangen
|
||
log('FETCH ERROR', `FEHLER beim Laden der API-Blöcke: ${error.message}`, '#dc3545', 'error', true);
|
||
bm.remove(PLACEHOLDER_ID);
|
||
});
|
||
};
|
||
|
||
})(window.BridgeParts || (window.BridgeParts = {}));
|