This commit is contained in:
2025-12-04 22:33:05 +01:00
parent 316175e158
commit 9dee06cdd6
145 changed files with 16865 additions and 88 deletions

View File

@@ -0,0 +1,207 @@
/* /assets/js/bridge/blocks-api.js (SCHRITT 16: Finaler Stabilitäts-Fix) */
(function (B) {
// 🛑 KRITISCHER FIX: Nur minimale Prüfung, um synchrone Initialisierung zu garantieren
if (!B || typeof grapesjs === 'undefined') {
console.warn("[BRIDGE-API] blocks-api.js: BridgeParts (B) oder GrapesJS fehlt. Exit.");
return;
}
const PluginName = 'bridge-blocks-api';
const qs = new URLSearchParams(location.search);
B.EDITOR_MODE = (qs.get('mode') || 'templates').toUpperCase();
console.log(`%c[${PluginName} - INIT] Editor Modus: ${B.EDITOR_MODE} (SCHRITT 16 - FINAL STABLE)`, 'color: #1E90FF; font-weight: bold;');
const TARGET_CAT_ID = 'custom';
const PLACEHOLDER_ID = 'api-placeholder-loading';
const REFERENCE_COMPONENT_TYPE = 'library-reference';
// --------------------------------------------------------
// (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 });
}
console.log(`%c[${PluginName}] Platzhalter und Kategorie registriert.`, 'color: #008000;');
};
// --------------------------------------------------------
// (2) Komponenten-Logik (ASYNCHRONER WORKAROUND & FIX)
// --------------------------------------------------------
const registerReferenceComponent = (editor) => {
const domc = editor.DomComponents;
const defaultType = domc.getType('default');
if (!defaultType) return;
// KRITISCHER WORKAROUND: Registrierung wird minimal verzögert
setTimeout(() => {
domc.addType(REFERENCE_COMPONENT_TYPE, {
model: defaultType.model.extend({
init() {
// Setze die Attribute sicher im init() (Fix für "defaults" TypeError)
if (this.get('type') !== REFERENCE_COMPONENT_TYPE) {
this.set('type', REFERENCE_COMPONENT_TYPE);
this.set('tagName', 'div');
this.set('lib-kind', '');
this.set('lib-id', '');
}
this.on('change:lib-kind change:lib-id', this.reloadComponentContent);
const editorInstance = this.em.get('Editor');
if (editorInstance && this.get('lib-id')) {
// Prüft hier nur auf getApiItem, da es für die Referenz-Komponente essenziell ist
if(B.getApiItem) {
editorInstance.on('load', this.reloadComponentContent.bind(this), { once: true });
} else {
console.warn(`[${PluginName}] B.getApiItem fehlt. Inhalte für 'library-reference' können nicht geladen werden.`);
}
}
},
reloadComponentContent(opts = {}) {
const kind = this.get('lib-kind');
const id = this.get('lib-id');
if (!kind || !id) {
this.set('content', '<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler: API-Referenz unvollständig.</div>');
return;
}
if (!B.getApiItem) {
this.set('content', '<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler: API-Kernfunktion getApiItem fehlt.</div>');
return;
}
B.getApiItem(kind, id)
.then(item => {
if (item && item.html) {
this.set('content', item.html);
console.log(`[${PluginName}] Geladenen Inhalt für ${kind}/${id} gesetzt.`);
} else {
this.set('content', `<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler: Inhalt für ${kind}/${id} nicht gefunden.</div>`);
}
})
.catch(error => {
console.error(`[${PluginName}] Fehler beim Abruf von ${kind}/${id}:`, error);
this.set('content', `<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler beim Laden von ${kind}/${id}.</div>`);
});
},
}, {}),
view: defaultType.view,
});
console.log(`%c[${PluginName}] Komponententyp '${REFERENCE_COMPONENT_TYPE}' ASYNCHRON registriert.`, 'color: #008000;');
}, 0);
};
// --------------------------------------------------------
// (3) Asynchrone Logik: API-Blöcke registrieren (FINAL CLEAN)
// --------------------------------------------------------
const loadAndRegisterApiBlocks = (editor) => {
const bm = editor.BlockManager;
const targetCatId = TARGET_CAT_ID;
// KRITISCHER FIX: Stelle sicher, dass der Platzhalter existiert.
if (!bm.get(PLACEHOLDER_ID)) {
bm.add(PLACEHOLDER_ID, {
label: 'Lade Custom-Blöcke...',
category: targetCatId,
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' },
});
console.log(`%c[${PluginName}] Platzhalter erneut hinzugefügt (Überlebens-Check).`, 'color: orange;');
}
// 🛑 NEUER CHECK: Prüfe die fetch*-Funktionen erst HIER.
if (!B.fetchSections || !B.fetchBlocks || !B.fetchSnippets) {
console.error(`%c[${PluginName}] FEHLER: Eine der API-Ladefunktionen (fetchSections/Blocks/Snippets) fehlt.`, 'color: #dc3545; font-weight: bold;');
// Platzhalter bleibt, da die Kategorie sonst verschwindet.
return;
}
// Explizite Promise.all mit allen fetch*-Funktionen
Promise.all([
B.fetchSections().then(items => items.map(i => ({ ...i, kind: 'sections' }))),
B.fetchBlocks().then(items => items.map(i => ({ ...i, kind: 'blocks' }))),
B.fetchSnippets().then(items => items.map(i => ({ ...i, kind: 'snippets' })))
])
.then(results => {
// Führe alle Ergebnisse zu einem flachen Array zusammen
const apiItems = results.flat().filter(item => item && item.id);
// Array-Ausgabe zur Bestätigung der Daten
console.log(`%c[${PluginName}] API-Daten Array:`, 'color: #9400D3; font-weight: bold;', apiItems);
console.log(`%c[${PluginName}] API-Daten geladen: ${apiItems.length} Blöcke/Sektionen gefunden.`, 'color: #1E90FF; font-weight: bold;');
if (apiItems.length === 0) {
// Platzhalter bleibt, um die leere Kategorie sichtbar zu halten.
console.warn(`[${PluginName}] Keine API-Daten gefunden, Platzhalter bleibt (leer) erhalten.`);
} else {
apiItems.forEach(item => {
const blockId = `lib-${item.kind}-${item.id}`;
const label = item.name || item.label || 'Unbenannter Block';
const itemKindUpper = item.kind.toUpperCase();
const blockDefinition = {
label: label,
category: targetCatId,
content: {
type: REFERENCE_COMPONENT_TYPE,
attributes: { 'lib-kind': item.kind, 'lib-id': item.id },
},
attributes: { 'title': itemKindUpper },
media: item.preview_url ? `<img src="${item.preview_url}">` : '',
};
bm.add(blockId, blockDefinition);
});
// Platzhalter entfernen, da Blöcke erfolgreich geladen wurden
bm.remove(PLACEHOLDER_ID);
console.log(`%c[${PluginName}] ${apiItems.length} API-Blöcke registriert. Platzhalter entfernt.`, 'color: #008000; font-weight: bold;');
}
})
.catch(error => {
console.error(`%c[${PluginName}] FEHLER beim Laden der API-Blöcke:`, 'color: #dc3545; font-weight: bold;', error);
// Platzhalter entfernen, um nicht im ewigen Ladezustand zu bleiben.
bm.remove(PLACEHOLDER_ID);
});
};
// --------------------------------------------------------
// (4) Plugin-Funktion
// --------------------------------------------------------
const plugin = (editor) => {
preRegisterCategoriesAndPlaceholders(editor);
registerReferenceComponent(editor);
editor.on('load', () => {
console.log(`%c[${PluginName}] GrapesJS 'load' Event: Starte asynchrones Laden der API-Blöcke.`, 'color: #1E90FF; font-weight: bold;');
loadAndRegisterApiBlocks(editor);
});
};
// --------------------------------------------------------
// (5) Export an Bridge Core
// --------------------------------------------------------
if (B.registerGrapesJSPlugin) {
B.registerGrapesJSPlugin(PluginName, plugin);
}
})(window.BridgeParts || (window.BridgeParts = {}));

View File

@@ -0,0 +1,270 @@
/* /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);
B.EDITOR_MODE = (qs.get('mode') || 'templates').toUpperCase();
log(`START: SKRIPT-AUSFÜHRUNG GESTARTET. Editor Modus: ${B.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 = 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 || '/api/ApiKernel.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 registerReferenceComponent = (editor) => {
const domc = editor.DomComponents;
const defaultType = domc.getType('default');
if (!defaultType) return;
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', '<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler: API-Referenz unvollständig.</div>');
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', `<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler: Inhalt für ${kind}/${id} nicht im Cache gefunden.</div>`);
}
},
}, {
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
}
}
}),
// 💡 WICHTIG: Die View muss den Content als reinen HTML-Inhalt rendern (defaultType macht das).
view: defaultType.view, 
});
log(`Komponententyp '${REFERENCE_COMPONENT_TYPE}' ASYNCHRON 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
const jsonProjectData = editor.getProjectData(); 
log('SAVE START', 'Starte Speichern des Inhalts an die API...', '#FF4500');
// 3. Daten für den POST-Request vorbereiten
const dataToSend = {
action: 'blocks.update', // Oder 'templates.update', je nach Entity
id: CURRENT_ENTITY_ID, 
html: htmlContent,
// 🚨 KRITISCH: Korrigiert auf 'json_content' für das PHP-Backend
json_content: jsonProjectData, 
name: B.CURRENT_ENTITY_NAME || 'Unbenannt', // Optional
};
// 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. JSON-Daten wurden gesendet.', '#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', () => {
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 = {}));

View File

@@ -0,0 +1,131 @@
/* /assets/js/bridge/blocks-custom.js (FINAL & LOG-KONTROLLIERT) */
(function () {
const PluginName = 'blocks-custom';
const B = window.BridgeParts || (window.BridgeParts = {});
// ----------------------------------------------------------------------
// 🎯 NEU: LOKALE LOG-KONFIGURATION UND WRAPPER
// ----------------------------------------------------------------------
// Setzen Sie dies auf 'false' in der config.js oder hier, um alle Logs NUR für dieses Plugin zu deaktivieren.
if (B.LOG_CONFIG && B.LOG_CONFIG.PLUGINS) {
B.LOG_CONFIG.PLUGINS[PluginName] = false; // <-- HIER IST IHR SCHALTER
}
// NEUER LOKALER WRAPPER, der die zentrale B.log Funktion verwendet:
const log = (type, message, color = '#FFD700', logType = 'info', force = false) => {
if (typeof B.log === 'function') {
B.log(PluginName, `[${type}] ${message}`, color, logType, force);
} else if (logType === 'error') {
// Fallback für kritische Fehler, wenn B.log fehlt
console.error(`%c[${PluginName} - ${type}] %c${message}`, `color:red; font-weight:bold;`, 'color:inherit;');
}
};
// ----------------------------------------------------------------------
log('FILE CHECK', 'Datei-IIFE startet.'); // NEU: Kontrollierbarer Start-Log
if (window.__CUSTOM_BLOCKS_LOADED) return;
window.__CUSTOM_BLOCKS_LOADED = true;
const TARGET_CAT_ID = 'bausteine';
const ALL_CUSTOM_BLOCK_IDS = [];
function addOnce(bm, id, def) {
// Hinzufügen des Blocks und Sicherstellen der Kategorie-Zuweisung
try {
bm.add(id, { ...def, category: TARGET_CAT_ID });
ALL_CUSTOM_BLOCK_IDS.push(id);
log('BLOCK ADD', `Block '${id}' erfolgreich hinzugefügt.`, '#B8860B');
} catch (e) {
log('BLOCK ERROR', `Fehler beim Hinzufügen von Block '${id}': ${e.message}`, 'red', 'error');
}
}
const css = o => Object.entries(o).map(([k,v]) => `${k}:${v}`).join(';');
function register(editor) {
log('EXECUTION', `Starte Block-Registrierung für ${TARGET_CAT_ID}.`, '#DAA520');
const bm = editor.BlockManager;
// --- Custom-Blöcke DEFINIEREN ---
// TEXT
addOnce(bm, 'cust-text', { id:'cust-text', label:'📝 Text',
content:`<div style="${css({'font-family':'Arial,sans-serif','font-size':'14px','line-height':'1.5',color:'#0f172a',margin:'0 0 12px'})}">
<p style="${css({margin:'0 0 12px'})}">Dies ist ein Absatz. Doppelklick zum Bearbeiten.</p></div>` });
// IMAGE
addOnce(bm, 'cust-image', { id:'cust-image', label:'🖼️ Bild',
content:`<div style="${css({'text-align':'center',margin:'0 0 16px'})}">
<img src="https://placehold.co/600x300" alt="Bild" style="${css({width:'100%',height:'auto','max-width':'600px',border:'0',display:'inline-block'})}"></div>` });
// BUTTON
addOnce(bm, 'cust-button', { id:'cust-button', label:'🔘 Button',
content:`<div style="${css({'text-align':'center',margin:'0 0 16px'})}">
<a href="#" style="${css({display:'inline-block','background-color':'#0ea5e9',color:'#fff','text-decoration':'none',padding:'10px 18px','border-radius':'6px','font-family':'Arial,sans-serif','font-size':'14px'})}">Call To Action</a></div>` });
// DIVIDER
addOnce(bm, 'cust-divider',{ id:'cust-divider',label:'⎯ Divider',
content:`<hr style="${css({border:'0',height:'1px','background-color':'#e2e8f0',margin:'16px 0'})}">` });
// SPACER
addOnce(bm, 'cust-spacer', { id:'cust-spacer', label:'↕ Spacer',
content:`<div style="${css({height:'24px'})}"></div>` });
// 2 COL
addOnce(bm, 'cust-2col', { id:'cust-2col', label:'🧩 2 Spalten',
content:`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="${css({'font-family':'Arial,sans-serif','border-collapse':'collapse','margin-bottom':'16px'})}">
<tr><td width="50%" valign="top" style="${css({padding:'0 8px 0 0'})}">
<div style="${css({'font-size':'14px','line-height':'1.5',color:'#0f172a'})}"><p style="${css({margin:'0 0 12px'})}">Linke Spalte Inhalt hier.</p></div>
</td><td width="50%" valign="top" style="${css({padding:'0 0 0 8px'})}">
<div style="${css({'font-size':'14px','line-height':'1.5',color:'#0f172a'})}"><p style="${css({margin:'0 0 12px'})}">Rechte Spalte Inhalt hier.</p></div>
</td></tr></table>` });
// MEDIA LEFT
addOnce(bm, 'cust-media-left', { id:'cust-media-left', label:'🖼️◀ Text',
content:`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="${css({'font-family':'Arial,sans-serif','border-collapse':'collapse','margin-bottom':'16px'})}">
<tr><td width="40%" valign="top" style="${css({padding:'0 8px 0 0'})}">
<img src="https://placehold.co/400x260" alt="Bild" style="${css({width:'100%',height:'auto',border:'0',display:'block'})}">
</td><td width="60%" valign="top" style="${css({padding:'0 0 0 8px'})}">
<h3 style="${css({margin:'0 0 8px','font-size':'18px',color:'#0f172a'})}">Überschrift</h3>
<p style="${css({margin:'0 0 12px','font-size':'14px',color:'#0f172a','line-height':'1.5'})}">Beschreibungstext …</p>
<a href="#" style="${css({display:'inline-block','background-color':'#0ea5e9',color:'#fff','text-decoration':'none',padding:'8px 14px','border-radius':'6px','font-size':'14px'})}">Mehr erfahren</a>
</td></tr></table>` });
// HERO
addOnce(bm, 'cust-hero', { id:'cust-hero', label:'🌄 Hero',
content:`<div style="${css({'text-align':'center',margin:'0 0 16px',padding:'12px','background-color':'#eef2ff',color:'#1e3a8a','border':'1px solid #c7d2fe','border-radius':'8px'})}">
<img src="https://placehold.co/640x240" alt="Hero" style="${css({width:'100%',height:'auto','max-width':'640px',border:'0',display:'inline-block','border-radius':'6px'})}">
<h2 style="${css({'font-family':'Arial,sans-serif',margin:'12px 0 8px','font-size':'22px'})}">Titel des Newsletters</h2>
<p style="${css({'font-size':'14px',margin:'0 0 12px'})}">Kurzer Untertitel oder Einleitung.</p>
</div>` });
// FOOTER
addOnce(bm, 'cust-footer', { id:'cust-footer', label:'⚓ Footer',
content:`<div style="${css({'font-family':'Arial,sans-serif','font-size':'12px',color:'#475569','line-height':'1.5','border-top':'1px solid #e2e8f0',padding:'12px 0','text-align':'center'})}">
<p style="${css({margin:'0 0 6px'})}"><strong>Dein Unternehmen GmbH</strong> • Musterstraße 1 • 12345 Berlin</p>
<p style="${css({margin:'0'})}"><a href="#" style="${css({color:'#0ea5e9','text-decoration':'none'})}">Abmelden</a> ·
<a href="#" style="${css({color:'#0ea5e9','text-decoration':'none'})}">Impressum</a> ·
<a href="#" style="${css({color:'#0ea5e9','text-decoration':'none'})}">Datenschutz</a></p>
</div>` });
log('SUCCESS', `Registrierung abgeschlossen. ${ALL_CUSTOM_BLOCK_IDS.length} Blöcke erstellt.`, '#008000', 'info', true);
}
// 🛑 KRITISCHE EXPORT-KORREKTUR: Exportiere 'register', um den Fehler in bridge-core.js zu beheben
window.BridgeBlocksCustom = {
IDS: ALL_CUSTOM_BLOCK_IDS,
register: register // <--- NEU: Exportiert die Register-Funktion
};
// Registriere das Modul als GrapesJS Plugin
if (B && B.registerGrapesJSPlugin && typeof register === 'function') {
B.registerGrapesJSPlugin('bridge-blocks-custom', register);
log('PLUGIN REGISTER', `'bridge-blocks-custom' erfolgreich zur Bridge Plugin Registry hinzugefügt.`, '#008000');
} else {
log('CRITICAL ERROR', `BridgeParts oder registerGrapesJSPlugin fehlt! Plugin-Registrierung gescheitert.`, 'red', 'error');
}
})();

View File

@@ -0,0 +1,139 @@
/* /assets/js/bridge/blocks-standard.js (FINAL & LOG-KONTROLLIERT) */
(function () {
const PluginName = 'blocks-standard';
const B = window.BridgeParts || (window.BridgeParts = {});
// ----------------------------------------------------------------------
// 🎯 NEU: LOKALE LOG-KONFIGURATION UND WRAPPER
// ----------------------------------------------------------------------
// Setzen Sie dies auf 'false' in der config.js oder hier, um alle Logs NUR für dieses Plugin zu deaktivieren.
if (B.LOG_CONFIG && B.LOG_CONFIG.PLUGINS) {
B.LOG_CONFIG.PLUGINS[PluginName] = false; // <-- HIER IST IHR SCHALTER
}
// NEUER LOKALER WRAPPER, der die zentrale B.log Funktion verwendet:
const log = (type, message, color = '#0000FF', logType = 'info', force = false) => {
if (typeof B.log === 'function') {
B.log(PluginName, `[${type}] ${message}`, color, logType, force);
} else if (logType === 'error') {
// Fallback für kritische Fehler, wenn B.log fehlt
console.error(`%c[${PluginName} - ${type}] %c${message}`, `color:red; font-weight:bold;`, 'color:inherit;');
} else {
// Fallback für sonstige Logs
console.log(`%c[${PluginName} - ${type}] %c${message}`, `color:${color}; font-weight:bold;`, 'color:inherit;');
}
};
// ----------------------------------------------------------------------
log('FILE CHECK', 'Datei-IIFE startet.');
// Kritische Prüfung, um doppelte Ausführung zu verhindern
if (window.__STANDARD_BLOCKS_LOADED) return;
window.__STANDARD_BLOCKS_LOADED = true;
const TARGET_CAT_ID = 'mysnips';
const TARGET_CAT_LABEL = 'Bibliothek';
const ALL_STANDARD_BLOCK_IDS = [];
const css = o => Object.entries(o).map(([k,v]) => `${k}:${v}`).join(';');
/**
* Fügt einen Block hinzu oder aktualisiert ihn SICHER
*/
function addOrUpdate(bm, id, def) {
if (bm.get(id)) {
bm.remove(id);
log('UPDATE', `Entferne alte Block-Definition: ${id}`, 'gray');
}
const finalDef = {
...def,
category: TARGET_CAT_ID,
force: true
};
try {
bm.add(id, finalDef);
} catch (e) {
log('CRITICAL ERROR', `KRITISCHER FEHLER beim Hinzufügen von '${id}': ${e.message}`, 'red', 'error');
return;
}
ALL_STANDARD_BLOCK_IDS.push(id);
}
/**
* Die eigentliche Plugin-Funktion, die von GrapesJS/Bridge aufgerufen wird.
*/
const pluginFunction = (editor) => {
// Aggressiver Log zur Prüfung der Ausführung
log('EXECUTION CHECK', `Starte Block-Registrierung für ${TARGET_CAT_ID}.`, '#993300');
if (!editor || !editor.BlockManager) {
log('EXECUTION CHECK', 'Fehler: GrapesJS Editor Instanz ist ungültig.', 'red', 'error');
return;
}
const bm = editor.BlockManager;
// =======================================================
// I. GRAPESJS DEFAULT BLÖCKE (ALLE STANDARD ELEMENTE)
// =======================================================
// TEXT (Registriert als 'std-text')
addOrUpdate(bm, 'std-text', { label:'Text (Basis)',
content:`<div data-gjs-type="text" style="${css({'font-family':'Arial,sans-serif','font-size':'14px',color:'#0f172a',margin:'0 0 12px'})}">Absatztext.</div>` });
// IMAGE (Registriert als 'std-image')
addOrUpdate(bm, 'std-image', { label:'Bild (Basis)',
content:`<img data-gjs-type="image" src="https://placehold.co/600x300" alt="Bild" style="${css({width:'100%',height:'auto','max-width':'600px',border:'0',display:'block'})}">` });
// LINK (Registriert als 'std-link')
addOrUpdate(bm, 'std-link', { label:'Link (Basis)',
content:`<a href="#" data-gjs-type="link" style="${css({color:'#0ea5e9','text-decoration':'none','font-family':'Arial,sans-serif','font-size':'14px'})}">Hyperlink</a>` });
// SECTION (Registriert als 'std-section')
addOrUpdate(bm, 'std-section', { label:'Sektion',
content:`<section style="${css({padding:'20px'})}" data-gjs-type="section">
<div style="${css({'font-family':'Arial,sans-serif','font-size':'14px',color:'#0f172a'})}">Inhalt der Sektion.</div>
</section>` });
// COLUMN (Registriert als 'std-column')
addOrUpdate(bm, 'std-column', { label:'Spalte',
content:`<div style="${css({padding:'10px','min-height':'50px','border':'1px dashed #ccc'})}" data-gjs-type="column">
<div style="${css({'font-family':'Arial,sans-serif','font-size':'12px',color:'#555'})}">Spalteninhalt</div>
</div>` });
// BUTTON (Registriert als 'std-button')
addOrUpdate(bm, 'std-button', { label:'Button (Basis)',
content:`<a href="#" data-gjs-type="button" style="${css({display:'inline-block','background-color':'#0ea5e9',color:'#fff','text-decoration':'none',padding:'10px 18px','border-radius':'6px','font-family':'Arial,sans-serif','font-size':'14px'})}">Button</a>` });
// DIVIDER (Registriert als 'std-divider')
addOrUpdate(bm, 'std-divider',{ label:'Trenner (Basis)',
content:`<hr data-gjs-type="divider" style="${css({border:'0',height:'1px','background-color':'#e2e8f0',margin:'16px 0'})}">` });
// MAP (Registriert als 'std-map')
addOrUpdate(bm, 'std-map', { label:'Karte',
content:`<iframe data-gjs-type="map" src="https://maps.google.com/maps?width=100%25&height=600&hl=de&q=Berlin&t=&z=14&ie=UTF8&iwloc=B&output=embed" width="100%" height="300" frameborder="0" style="${css({'border':'0',width:'100%',height:'300px'})}"></iframe>` });
// Löst die notwendigen Events für den Bridge Core / Cleanup aus.
editor.trigger('block:add');
log('SUCCESS', `Erfolgreich ${ALL_STANDARD_BLOCK_IDS.length} Standardblöcke in Kategorie '${TARGET_CAT_LABEL}' registriert.`, '#008000', 'info', true);
};
// Exportiere für den manuellen Aufruf in bridge-core.js
window.BridgeBlocksStandard = {
IDS: ALL_STANDARD_BLOCK_IDS,
register: pluginFunction,
};
// Registriere das Modul als GrapesJS Plugin (für den Fall, dass es doch anderswo benötigt wird)
if (B && B.registerGrapesJSPlugin) {
B.registerGrapesJSPlugin('bridge-blocks-standard', window.BridgeBlocksStandard.register);
log('PLUGIN REGISTER', `'bridge-blocks-standard' erfolgreich zur Bridge Plugin Registry hinzugefügt.`, '#008000');
} else {
log('CRITICAL ERROR', `BridgeParts oder registerGrapesJSPlugin fehlt! Plugin-Registrierung gescheitert.`, 'red', 'error');
}
})();

View File

@@ -0,0 +1,265 @@
/* /assets/js/bridge/categorization-cleanup.js (FINAL & LOG-KONTROLLIERT) */
(function(B){
if (!B || typeof grapesjs === 'undefined') return;
// 🛑 NEUER NAME: Dies wird das Plugin in GrapesJS registrieren
const PluginName = 'bridge-categorization-cleanup';
// ----------------------------------------------------------------------
// 🎯 NEU: LOKALE LOG-KONFIGURATION UND WRAPPER
// ----------------------------------------------------------------------
// Setzen Sie dies auf 'false' in der config.js oder hier, um alle Logs NUR für dieses Plugin zu deaktivieren.
if (B.LOG_CONFIG && B.LOG_CONFIG.PLUGINS) {
B.LOG_CONFIG.PLUGINS[PluginName] = false; // <-- HIER IST IHR SCHALTER
}
// NEUER LOKALER WRAPPER, der die zentrale B.log Funktion verwendet:
const log = (type, message, color = '#228B22', logType = 'info', force = false) => {
// Wir verwenden B.log, das die B.LOG_CONFIG.PLUGINS[PluginName] prüft
if (typeof B.log === 'function') {
B.log(PluginName, `[${type}] ${message}`, color, logType, force);
} else if (logType === 'error') {
// Fallback für kritische Fehler, wenn B.log fehlt (sollte nicht passieren)
console.error(`%c[${PluginName} - ${type}] %c${message}`, `color:red; font-weight:bold;`, 'color:inherit;');
}
};
// ----------------------------------------------------------------------
// 🛑 WICHTIG: Liste aller unerwünschten IDs/Labels
const UNWANTED_UNCATEGORIZED_ID = 'Uncategorized';
// Fügen Sie die gängigen IDs des gjs-preset-newsletter hinzu
const PRESET_UNWANTED_IDS = ['Basic', 'Layout', 'Extra', 'Components', 'Forms'];
// Alle IDs, die gelöscht werden müssen. Enthält NICHT mehr 'Bibliothek'.
const ALL_FORBIDDEN_CAT_IDS = [UNWANTED_UNCATEGORIZED_ID, ...PRESET_UNWANTED_IDS];
const UNWANTED_BLOCK_ID = 'gjs-lbr-block-disabled';
const UNWANTED_BLOCK_LABEL = 'Bibliothek-disabled';
const FALLBACK_CATEGORY_ID = 'mysnips';
const CUSTOM_BLOCK_IDS = (window.BridgeBlocksCustom && window.BridgeBlocksCustom.IDS) || [];
let normalizationRunCount = 0;
let normalizationIsRunning = false;
let maxNormalizationRuns = 5;
// ----------------------------------------------------------------------
// HILFSFUNKTION: Entfernt hartnäckige Kategorie-DOM-Elemente
// ----------------------------------------------------------------------
const zapUnwantedCategoryDom = (editor) => {
const blocksPanelEl = editor.BlockManager.getContainer();
if (blocksPanelEl) {
let removedCount = 0;
blocksPanelEl.querySelectorAll('.gjs-block-cat').forEach(catEl => {
const catTitleEl = catEl.querySelector('.gjs-title');
if (catTitleEl) {
const catTitle = catTitleEl.textContent.trim();
// Prüft, ob der Titel eine der unerwünschten IDs ist (z.B. 'Basic')
if (ALL_FORBIDDEN_CAT_IDS.includes(catTitle)) {
catEl.remove();
removedCount++;
}
}
});
if (removedCount > 0) {
log('DOM FIX', `${removedCount} unerwünschte Kategorie-DOM-Elemente entfernt.`, 'orange', 'warn');
}
}
};
// ----------------------------------------------------------------------
// Hilfsfunktion: Erzwingt das Neu-Rendern der Block-View
// ----------------------------------------------------------------------
const renderBlocks = (editor) => {
zapUnwantedCategoryDom(editor);
log('RENDER', 'DOM-Cleanup ausgeführt.', 'green');
};
// ----------------------------------------------------------------------
// 1. Funktion zum Ausblenden/Normalisieren der Kategorien (Kernlogik)
// ----------------------------------------------------------------------
const normalizeCategories = (editor) => {
if (normalizationIsRunning || normalizationRunCount >= maxNormalizationRuns) {
if (normalizationRunCount >= maxNormalizationRuns) {
log('SKIP', `normalizeCategories übersprungen: Maximale Läufe (${maxNormalizationRuns}) erreicht.`, 'red', 'warn');
} else {
log('SKIP', 'normalizeCategories übersprungen: Läuft bereits.', 'red', 'warn');
}
return;
}
normalizationIsRunning = true;
normalizationRunCount++;
// Nur das Start-Log kann eine Gruppen-Markierung sein
log('START', `Starte normalizeCategories Run #${normalizationRunCount}`, '#191970');
const bm = editor.BlockManager;
const config = B.CATEGORY_CONFIG || {};
const configuredCategoryIds = Object.keys(config);
log('CONFIG', `Konfigurierte Kategorie-IDs: ${configuredCategoryIds.join(', ')}`, '#555555');
// --- A. Explizites Erstellen der Kategorien (Sicherheits-Fallback) ---
const catsToEnsure = new Set(configuredCategoryIds);
catsToEnsure.forEach(catId => {
const catConf = config[catId];
if (!catConf) return;
if (!bm.getCategories().get(catId)) {
bm.getCategories().add({
id: catId,
label: catConf.label,
open: catConf.open !== false,
order: catConf.ord || 999
});
log('CAT FALLBACK', `Kategorie '${catId}' fehlte und wurde JETZT erstellt!`, 'red', 'error');
}
});
// --- B. Zwangszuweisung der Blöcke und Bereinigung von Blöcken ---
bm.getAll().each(block => {
const id = block.get('id');
const label = block.get('label');
let catId = block.get('category');
if (typeof catId === 'object' && catId.id) {
catId = catId.id;
}
// 1. Lösche unerwünschten hartnäckigen Block (DEAKTIVIERT)
if (id === UNWANTED_BLOCK_ID || label === UNWANTED_BLOCK_LABEL) {
// ... (Block removal logic commented out)
// log('BLOCK REMOVE', `Lösche unerwünschten Block: ${id}`, 'red', 'warn');
// bm.remove(id);
}
// 2. Setze Blöcke ohne oder mit unerwünschter/unbekannter Kategorie auf den Fallback (mysnips)
const isUnconfiguredOrForbidden = !catId || !configuredCategoryIds.includes(catId) || ALL_FORBIDDEN_CAT_IDS.includes(catId);
if (isUnconfiguredOrForbidden) {
if (id) {
log('BLOCK FIX', `Block '${id}' ('${label}') verschoben nach '${FALLBACK_CATEGORY_ID}' (Ursprüngliche Kat ID: ${catId || 'keine/leer'}).`, 'orange', 'warn');
block.set('category', FALLBACK_CATEGORY_ID);
}
} else {
log('BLOCK OK', `Block '${id}' ('${label}') bleibt in Kategorie '${catId}'.`, 'green');
}
// 3. Custom Blocks schützen
if (CUSTOM_BLOCK_IDS.includes(id) && catId !== 'bausteine') {
log('BLOCK FIX', `Custom Block '${id}' auf 'bausteine' korrigiert.`, 'orange', 'warn');
block.set('category', 'bausteine');
}
});
// --- C. Kategorien erzwingen, Label korrigieren und Löschen von Modellen ---
const categories = bm.getCategories().models || bm.getCategories();
let visibleCategories = [];
categories.forEach(catModel => {
const catId = catModel.get('id');
const catConf = config[catId];
// 1. Aggressives Löschen von unerwünschten Preset-Kategorien
if (ALL_FORBIDDEN_CAT_IDS.includes(catId)) {
log('CAT REMOVE', `Lösche unerwünschtes Category Model: ${catId} (Da in ALL_FORBIDDEN_CAT_IDS).`, 'red', 'error');
bm.getCategories().remove(catModel);
return;
}
const activeConf = catConf;
// 2. Finde eine existierende, aber nicht konfigurierte Kategorie, und blende sie aus
if (!activeConf && catId) {
log('CAT HIDE', `Kategorie '${catId}' existiert, ist aber nicht in CATEGORY_CONFIG. Wird ausgeblendet.`, 'orange', 'warn');
catModel.set('visible', false);
catModel.set('open', false);
return;
}
// 3. Korrigiere Label, Sortierung und Sichtbarkeit (Konfigurierte Kategorien)
if (activeConf) {
const oldLabel = catModel.get('label');
const newLabel = activeConf.label;
const visibility = true;
if (oldLabel !== newLabel) {
log('CAT UPDATE', `Korrigiere Label von '${catId}' von '${oldLabel}' auf '${newLabel}'.`, '#00BFFF');
catModel.set('label', newLabel, { silent: true });
}
catModel.set('visible', visibility);
catModel.set('open', activeConf.open !== false);
catModel.set('order', activeConf.ord || 999);
visibleCategories.push(catId);
log('CAT FINAL', `Kategorie '${catId}' auf Visible: ${visibility}, Order: ${catModel.get('order')}.`, 'green');
}
});
// --- D. Cleanup und Neu-Sortierung erzwingen ---
categories.sort((a, b) => (a.get('order') || 999) - (b.get('order') || 999));
B.sortBlocksByPrefixAndLabel && B.sortBlocksByPrefixAndLabel(bm.getAll().models);
// DOM Cleanup wird über renderBlocks aufgerufen
renderBlocks(editor);
log('END', `Kategorisierung abgeschlossen. Sichtbare Kategorien (Modelle): ${visibleCategories.sort().join(', ')}.`, 'green', 'info', true); // FINAL Log ist forced=true für Abschlussmeldung
normalizationIsRunning = false;
};
// ----------------------------------------------------------------------
// GrapesJS Plugin Registrierung
// ----------------------------------------------------------------------
grapesjs.plugins.add(PluginName, (editor, opts = {}) => {
const bm = editor.BlockManager;
// 1. Initialer, verspäteter Lauf bei Ladevorgang
editor.on('load', () => {
setTimeout(() => {
log('FINAL RUN', `Starte finalen Normalisierungslauf nach 2500ms.`, 'orange', 'warn');
normalizeCategories(editor);
}, 2500);
});
// 2. WATCHDOG gegen Label-Überschreibung oder unerwünschte Adds
bm.getCategories().on('add change:label', (categoryModel) => {
const catId = categoryModel.get('id');
const newLabel = categoryModel.get('label');
const expectedLabel = B.CATEGORY_CONFIG?.[catId]?.label;
// WATCHDOG-ADD
if (ALL_FORBIDDEN_CAT_IDS.includes(catId)) {
log('WATCHDOG-ADD', `Unerwünschte Kategorie '${catId}' wurde hinzugefügt! Starte Sofort-Korrektur.`, 'red', 'error');
bm.getCategories().remove(categoryModel);
setTimeout(() => normalizeCategories(editor), 1);
}
// WATCHDOG-LABEL
if (expectedLabel && newLabel !== expectedLabel) {
log('WATCHDOG-CHANGE', `Externe Label-Manipulation von '${catId}' erkannt: Korrigiere von '${newLabel}' auf '${expectedLabel}'.`, 'orange', 'warn');
categoryModel.set('label', expectedLabel, { silent: true });
setTimeout(() => normalizeCategories(editor), 1);
}
});
// Exporte beibehalten, falls sie in bridge-core.js verwendet werden
B.normalizeCategories = normalizeCategories;
B.renderBlocks = renderBlocks;
log('INIT', 'Master-Koordinator registriert.', '#008080');
});
})(window.BridgeParts || (window.BridgeParts = {}));

View File

@@ -0,0 +1,239 @@
/* /assets/js/bridge/categorization-master.js (FINALE KORREKTUR V3: Entfernt aggressives bm.render()) */
(function(B){
if (!B || typeof grapesjs === 'undefined') return;
const PluginName = 'bridge-categorization-master';
// 🛑 WICHTIG: Liste aller unerwünschten IDs/Labels
const UNWANTED_CATEGORY_ID = 'Bibliothek';
const UNWANTED_UNCATEGORIZED_ID = 'Uncategorized';
// Fügen Sie die gängigen IDs des gjs-preset-newsletter hinzu
const PRESET_UNWANTED_IDS = ['Basic', 'Layout', 'Extra', 'Components', 'Forms'];
// Alle IDs, die gelöscht werden müssen
const ALL_FORBIDDEN_CAT_IDS = [UNWANTED_CATEGORY_ID, UNWANTED_UNCATEGORIZED_ID, ...PRESET_UNWANTED_IDS];
const UNWANTED_BLOCK_ID = 'gjs-lbr-block';
const UNWANTED_BLOCK_LABEL = 'Bibliothek';
const FALLBACK_CATEGORY_ID = 'mysnips';
const CUSTOM_BLOCK_IDS = (window.BridgeBlocksCustom && window.BridgeBlocksCustom.IDS) || [];
let normalizationRunCount = 0;
let normalizationIsRunning = false;
let maxNormalizationRuns = 5;
// ----------------------------------------------------------------------
// HILFSFUNKTION: Entfernt hartnäckige Kategorie-DOM-Elemente
// ----------------------------------------------------------------------
const zapUnwantedCategoryDom = (editor) => {
const blocksPanelEl = editor.Panels.getPanel('blocks')?.get('el');
if (blocksPanelEl) {
let removedCount = 0;
blocksPanelEl.querySelectorAll('.gjs-block-cat').forEach(catEl => {
const catTitleEl = catEl.querySelector('.gjs-title');
if (catTitleEl) {
const catTitle = catTitleEl.textContent.trim();
// Prüfe auf unerwünschte Titel (sowohl Standard als auch Presets)
if (ALL_FORBIDDEN_CAT_IDS.includes(catTitle) || catTitle === UNWANTED_UNCATEGORIZED_ID || catTitle === UNWANTED_CATEGORY_ID) {
catEl.remove();
removedCount++;
}
}
});
if (removedCount > 0) {
console.warn(`[${PluginName}][DOM Fix] ${removedCount} unerwünschte Kategorie-DOM-Elemente entfernt.`);
}
}
};
// ----------------------------------------------------------------------
// Hilfsfunktion: Erzwingt das Neu-Rendern der Block-View (NUR DOM-CLEANUP)
// ----------------------------------------------------------------------
const renderBlocks = (editor) => {
// 🛑 KRITISCHE KORREKTUR: Entferne bm.render() nur DOM-Cleanup ist hier nötig,
// da das Setzen der Model-Eigenschaften (label, visible) das Rendering übernehmen sollte.
zapUnwantedCategoryDom(editor);
console.log(`[${PluginName}][Render] DOM-Cleanup ausgeführt.`);
};
// ----------------------------------------------------------------------
// 1. Funktion zum Ausblenden/Normalisieren der Kategorien (Kernlogik)
// ----------------------------------------------------------------------
const normalizeCategories = (editor) => {
if (normalizationIsRunning || normalizationRunCount >= maxNormalizationRuns) {
if (normalizationRunCount >= maxNormalizationRuns) {
console.warn(`[${PluginName}] normalizeCategories übersprungen: Maximale Läufe (${maxNormalizationRuns}) erreicht.`);
} else {
console.warn(`[${PluginName}] normalizeCategories übersprungen: Läuft bereits.`);
}
return;
}
normalizationIsRunning = true;
normalizationRunCount++;
console.group(`[${PluginName}] normalizeCategories Run #${normalizationRunCount}`);
const bm = editor.BlockManager;
const config = B.CATEGORY_CONFIG || {};
const configuredCategoryIds = Object.keys(config);
// 🛑 NEUER FALLBACK-FIX: Stellen Sie sicher, dass alle konfigurierten Kategorien existieren,
// bevor Blöcke zugewiesen werden.
configuredCategoryIds.forEach(catId => {
const catConf = config[catId];
if (!bm.getCategories().get(catId)) {
bm.getCategories().add({
id: catId,
label: catConf.label,
open: catConf.open !== false,
order: catConf.ord || 999
});
// Nur als Warnung, da dies bei 'mysnips' oft passiert, wenn es leer ist.
console.warn(`[${PluginName}][Cat Fallback] Kategorie '${catId}' wurde nachträglich erstellt.`);
}
});
// --- A. Zwangszuweisung der Blöcke und Bereinigung von Blöcken ---
bm.getAll().each(block => {
const id = block.get('id');
const label = block.get('label');
let catId = block.get('category');
if (typeof catId === 'object' && catId.id) {
catId = catId.id; // Behandle Category-Objekte
}
// 1. Lösche unerwünschten hartnäckigen Block (z.B. gjs-lbr-block)
if (id === UNWANTED_BLOCK_ID || label === UNWANTED_BLOCK_LABEL) {
console.log(`[${PluginName}][Block Fix] Unerwünschter Block '${id}' ('${label}') entfernt.`);
bm.remove(id);
return;
}
// 2. Setze Blöcke ohne oder mit unerwünschter/unbekannter Kategorie auf den Fallback (mysnips)
// HINWEIS: 'custom' ist in configuredCategoryIds, falls es noch keine Blöcke von der API hat.
if (!catId || !configuredCategoryIds.includes(catId) || ALL_FORBIDDEN_CAT_IDS.includes(catId)) {
// Nur wenn der Block nicht leer ist
if (id) {
console.log(`[${PluginName}][Block Fix] Block '${id}' ('${label}') verschoben nach '${FALLBACK_CATEGORY_ID}' (von Kat: ${catId || 'keine'}).`);
block.set('category', FALLBACK_CATEGORY_ID);
}
}
// 3. Custom Blocks schützen
if (CUSTOM_BLOCK_IDS.includes(id) && catId !== 'bausteine') {
block.set('category', 'bausteine');
}
});
// --- B. Kategorien erzwingen, Label korrigieren und Löschen von Modellen ---
const categories = bm.getCategories().models || bm.getCategories();
let visibleCategories = [];
// Gehe alle Category Models durch
categories.forEach(catModel => {
const catId = catModel.get('id');
const catConf = config[catId];
// Aggressives Löschen von unerwünschten Preset-Kategorien
if (ALL_FORBIDDEN_CAT_IDS.includes(catId)) {
console.warn(`[${PluginName}][Cat Fix] Lösche unerwünschtes Category Model: ${catId}`);
bm.getCategories().remove(catModel);
return;
}
// Finde eine existierende, aber nicht konfigurierte Kategorie, und blende sie aus
// WICHTIG: mysnips ist der FALLBACK_CATEGORY_ID, hier darf es NICHT ausgeblendet werden.
if (!catConf && catId && catId !== FALLBACK_CATEGORY_ID) {
catModel.set('visible', false);
catModel.set('open', false);
return;
}
// Korrigiere Label, Sortierung und Sichtbarkeit der konfigurierten Kategorien
if (catConf) {
// 🛑 KRITISCHER FIX: Garantiertes Setzen des korrekten Labels (LÖST KLEINSCHREIBUNGS-PROBLEM)
if (catModel.get('label') !== catConf.label) {
console.log(`[${PluginName}][Cat Fix] Korrigiere Label von '${catId}' auf '${catConf.label}'.`);
catModel.set('label', catConf.label);
}
// ** Das Setzen von 'visible' und 'open' sollte die UI-Aktualisierung (Kategorie anzeigen) auslösen. **
catModel.set('visible', true);
catModel.set('open', catConf.open !== false);
catModel.set('order', catConf.ord || 999);
visibleCategories.push(catId);
}
});
// --- C. Cleanup und Neu-Rendern erzwingen ---
categories.sort((a, b) => (a.get('order') || 999) - (b.get('order') || 999));
B.sortBlocksByPrefixAndLabel && B.sortBlocksByPrefixAndLabel(bm.getAll().models);
// 🛑 KRITISCH: Rendering WIRD NICHT mehr erzwungen nur DOM Cleanup.
renderBlocks(editor);
console.log(`Kategorisierung abgeschlossen. Sichtbare Kategorien: ${visibleCategories.sort().join(', ')}.`);
console.groupEnd();
normalizationIsRunning = false;
};
// ----------------------------------------------------------------------
// GrapesJS Plugin Registrierung
// ----------------------------------------------------------------------
grapesjs.plugins.add(PluginName, (editor, opts = {}) => {
const bm = editor.BlockManager;
// 1. Initialer, verspäteter Lauf bei Ladevorgang
editor.on('load', () => {
// FINALER LAUF: Läuft, wenn ALLE Standard-Plugins fertig sind
setTimeout(() => {
console.warn(`[${PluginName}][FINAL RUN] Starte finalen Normalisierungslauf nach 2500ms.`);
normalizeCategories(editor);
}, 2500);
});
// 2. WATCHDOG gegen Label-Überschreibung oder unerwünschte Adds
bm.getCategories().on('add change:label', (categoryModel) => {
const catId = categoryModel.get('id');
const newLabel = categoryModel.get('label');
const expectedLabel = B.CATEGORY_CONFIG?.[catId]?.label;
// WATCHDOG-ADD: Entfernt unerwünschte Kategorien sofort, falls sie erstellt werden
if (ALL_FORBIDDEN_CAT_IDS.includes(catId)) {
console.error(`[${PluginName}][WATCHDOG-ADD] Unerwünschte Kategorie '${catId}' wurde hinzugefügt! Starte Sofort-Korrektur.`);
bm.getCategories().remove(categoryModel);
setTimeout(() => normalizeCategories(editor), 1);
}
// WATCHDOG-LABEL: Korrigiert falsche Labels (z.B. "bausteine" -> "🧱 Bausteine")
if (expectedLabel && newLabel !== expectedLabel) {
console.warn(`[${PluginName}][WATCHDOG-CHANGE] Externe Label-Manipulation von '${catId}' erkannt: Korrigiere von '${newLabel}' auf '${expectedLabel}'.`);
// Sofortiges Zurücksetzen des Labels auf den korrekten Wert
categoryModel.set('label', expectedLabel, { silent: true });
// Triggere einen Normalize-Lauf, damit die UI die Korrektur sieht und die Sortierung passt.
setTimeout(() => normalizeCategories(editor), 1);
}
});
B.normalizeCategories = normalizeCategories;
B.renderBlocks = renderBlocks;
console.log(`[${PluginName}] Master-Koordinator registriert.`);
});
})(window.BridgeParts || (window.BridgeParts = {}));

View File

@@ -0,0 +1,14 @@
/* /assets/js/bridge/categorization-master.js (PLATZHALTER - LOGIK IN CLEANUP.JS VERSCHOBEN) */
(function(B){
if (!B || typeof grapesjs === 'undefined') return;
const PluginName = 'bridge-categorization-master';
// Dies ist nun ein leeres Plugin. Die Logik wurde nach categorization-cleanup.js verschoben.
grapesjs.plugins.add(PluginName, (editor, opts = {}) => {
// Leere Plugin-Funktion. Führt keine Aufräumarbeiten, Normalisierung oder Exporte durch.
console.log(`[${PluginName}] Plugin existiert (Logik nach cleanup.js verschoben).`);
});
})(window.BridgeParts || (window.BridgeParts = {}));

View File

@@ -0,0 +1,55 @@
/* /assets/js/bridge/category-config.js (FINALE KORREKTUR DER BLAUPASE) */
(function(B) {
    if (!B) return;
// NEU: Map, die Ressourcentyp ('kind') zu API-Basis-URL zuordnet. Wird von library-api.js gelesen.
B.RESOURCE_API_BASES = B.RESOURCE_API_BASES || {};
const API_BASE_DEFAULT = (B.API_BASE || '/api/editor');
    // DEFINITION DER ZIEL-KATEGORIEN
    B.CATEGORY_CONFIG = {
        // --- 1. BIBLIOTHEK (mysnips) ---
        mysnips: {
            // ... (Bleibt unverändert, da es kein API-Async-Laden nutzt)
            ord: 20, 
            open: false,
            label: '📚 Bibliothek',
            files: ['blocks-standard.js'],
registration_mode: 'sync',
        },
        // --- 2. BAUSTEINE (bausteine) ---
        bausteine: {
            // ... (Bleibt unverändert, da es kein API-Async-Laden nutzt)
            ord: 10,
            open: true,
            label: '🧱 Bausteine',
            files: ['blocks-custom.js'],
            registration_mode: 'sync',
        },
        // --- 3. API Custom-Blocks (Standard-API) ----
        custom: {
            ord: 1,
            label: 'Custom',
            open: true,
            files: ['library-api.js','blocks-api.js'],
            registration_mode: 'async',
// NEU: API-Konfiguration für diese Kategorie
api_config: {
base_url: '/api/editor', // Nutzt die Standard-API
resources: ['templates','sections', 'blocks', 'snippets'] // Ressourcen, die von dort geladen werden
}
        }
    };
// --- Initialisierung der zentralen RESOURCE_API_BASES Map ---
// Diese Logik stellt sicher, dass library-api.js weiß, welchen Endpunkt es für welchen "kind" nutzen muss.
Object.values(B.CATEGORY_CONFIG).forEach(config => {
if (config.api_config && Array.isArray(config.api_config.resources)) {
const baseUrl = config.api_config.base_url || API_BASE_DEFAULT;
config.api_config.resources.forEach(resourceKind => {
B.RESOURCE_API_BASES[resourceKind] = baseUrl;
});
}
});
    
})(window.BridgeParts || (window.BridgeParts = {}));

View File

@@ -0,0 +1,28 @@
/* /assets/js/bridge/category-config.js (FINAL: Zentrale Konfiguration) */
(function(w){
var B = w.BridgeParts = w.BridgeParts || {};
/**
* Zentrale Konfiguration für Block-Kategorien und deren Sortierprioritäten.
*/
B.CATEGORY_CONFIG = {
// Prio 1
'lib-templates':   { label:'Bibliothek: Templates (Ref)', ord: 1, open: true },
// Prio 2 (Custom)A
'custom':           { label:'Custom',                     ord: 2, open: true },
'custom-fix':       { label:'Custom',                     ord: 2, open: true },
'custom-flex':      { label:'Custom',                     ord: 2, open: true },
// Prio 3 (Bausteine)
'bausteine':       { label:'Bausteine',                   ord: 3, open: true },
// Prio 4 (Bibliothek)
'mysnips':         { label:'Bibliothek',                  ord: 4, open: true },
// INTERNE (Werden später im Plugin auf Prio 2 umgeleitet und sortiert)
'lib-sections':    { label:'Bibliothek: Sections',       ord: 99, open: true },
'lib-blocks':      { label:'Bibliothek: Blöcke',         ord: 99, open: true },
};
})(window);

View File

@@ -0,0 +1,143 @@
/* /assets/js/bridge/custom-blocks-plugin.js (FINALE VERSION 2.0: Erzwungene Sortierung) */
(function(gjs, B){
if (!gjs || !B || !B.CATEGORY_CONFIG) return;
// --- 1. Block-Sortierlogik (Wieder logische Gewichte: 1 < 2 < 3) -----------------------
const getSortWeight = (id) => {
// Logisch: Section (1) < Block (2) < Snippet (3)
if (id.startsWith('custom-section-') || id.startsWith('lib-sec-')) return 1;
if (id.startsWith('custom-block-') || id.startsWith('lib-blk-')) return 2;
if (id.startsWith('custom-snippet-') || id.startsWith('snip-')) return 3;
return 99;
};
const sortBlocksByPrefixAndLabel = (blocks) => {
blocks.sort((a, b) => {
const aId = String((a.get ? a.get('id') : a.id) || '');
const bId = String((b.get ? b.get('id') : b.id) || '');
// Hier nutzen wir die Rohdaten (aSnippet, bSnippet)
const aLabel = String((a.get ? a.get('label') : a.label) || '').toLowerCase();
const bLabel = String((b.get ? b.get('label') : b.label) || '').toLowerCase();
const aWeight = getSortWeight(aId);
const bWeight = getSortWeight(bId);
// 1. Sortierung nach Gewicht (1, 2, 3)
if (aWeight !== bWeight) return aWeight - bWeight;
// 2. Sortierung alphabetisch (a vor b)
if (aLabel < bLabel) return -1;
if (aLabel > bLabel) return 1;
return 0;
});
};
// -----------------------------------------------------------------------------------------
gjs.plugins.add('bridge-custom-blocks', (editor, opts = {}) => {
const config = B.CATEGORY_CONFIG;
const validCatIds = Object.keys(config);
// IDs zur internen Zuweisung
const CAT_CUSTOM_MAIN_ID = 'custom';
const CAT_BAUSTEINE_ID = 'bausteine';
const CAT_BIBLIOTHEK_ID = 'mysnips';
const CAT_LIB_TEMPLATES_ID = 'lib-templates';
const normalizeAndSort = (ed) => {
try {
const bm = ed.BlockManager;
const categories = bm.getCategories ? bm.getCategories() : null;
let allBlocks = bm.getAll().models || bm.getAll();
let customBlocksArray = [];
let otherBlocksArray = [];
// 1. Blöcke neu kategorisieren & trennen
(allBlocks || []).forEach(b => {
const id = String((b.get ? b.get('id') : b.id) || '');
let targetCatId = null;
if (id.startsWith('lib-tpl-ref-')) {
targetCatId = CAT_LIB_TEMPLATES_ID;
} else if (id.startsWith('custom-') || id.startsWith('lib-sec-') || id.startsWith('lib-blk-') || id.startsWith('snip-')) {
targetCatId = CAT_CUSTOM_MAIN_ID;
} else if (id.startsWith('blk-') || id.startsWith('std-')) {
targetCatId = CAT_BAUSTEINE_ID;
} else if (!b.get('category')) {
targetCatId = CAT_BIBLIOTHEK_ID;
}
if (targetCatId) {
b.set('category', targetCatId);
if (targetCatId === CAT_CUSTOM_MAIN_ID) {
customBlocksArray.push(b);
} else {
otherBlocksArray.push(b);
}
} else {
// Blöcke, die nicht zugewiesen wurden (z.B. basic/extra), behalten
otherBlocksArray.push(b);
}
});
// 2. Block-Sortierung INNERHALB der "custom" Kategorie
if (customBlocksArray.length > 0) {
sortBlocksByPrefixAndLabel(customBlocksArray);
console.log('[DEBUG PLUGIN] Custom Blocks intern sortiert.');
}
// NEU: Gesamte BlockManager-Kollektion mit sortierten Custom-Blöcken überschreiben
// Wir nehmen alle sortierten Custom-Blöcke und fügen die anderen Blöcke danach an.
const newBlockOrder = customBlocksArray.concat(otherBlocksArray);
// Dieser Hack sollte die Reihenfolge in der Seitenleiste erzwingen.
if (bm.getAll().reset) {
bm.getAll().reset(newBlockOrder);
console.log('[DEBUG PLUGIN] Gesamte Block-Kollektion mit neuer Sortierung überschrieben.');
}
// 3. Kategorien Aufräumen & Sortieren (wie zuvor)
if (categories && categories.models) {
// Aufräumen
categories.models.slice().forEach(cat => {
const catId = (cat.get('id') || cat.id || '').toLowerCase();
if (!validCatIds.includes(catId) && catId !== 'basic' && catId !== 'extra') {
categories.remove(cat);
}
});
// Labels korrigieren und Kategorie-Sortierung erzwingen
categories.models.forEach(cat => {
const catId = (cat.get('id') || cat.id || '').toLowerCase();
if (config[catId]) {
cat.set('label', config[catId].label);
cat.set('open', config[catId].open ?? true);
}
});
const catOrder = (m) => config[String((m.get('id') || m.id)).toLowerCase()]?.ord || 99;
const arr = categories.models.slice().sort((a,b) => catOrder(a) - catOrder(b));
categories.reset(arr);
}
// 4. Finaler DOM-Sweep
B.enforceCategoryOrder && B.enforceCategoryOrder(ed);
B.renderBlocks && B.renderBlocks(ed);
} catch(e) {
console.error('[CustomPlugin] Error during normalize:', e);
}
};
// 5. Listener
editor.on('block:add block:remove block:reset', () => normalizeAndSort(editor));
editor.on('load', () => {
normalizeAndSort(editor);
setTimeout(() => normalizeAndSort(editor), 100);
setTimeout(() => normalizeAndSort(editor), 800);
setTimeout(() => normalizeAndSort(editor), 1500);
});
});
})(window.grapesjs, window.BridgeParts);

View File

@@ -0,0 +1,143 @@
/* /assets/js/bridge/custom-blocks-plugin.js (FINALE VERSION 2.0: Erzwungene Sortierung) */
(function(gjs, B){
if (!gjs || !B || !B.CATEGORY_CONFIG) return;
// --- 1. Block-Sortierlogik (Wieder logische Gewichte: 1 < 2 < 3) -----------------------
const getSortWeight = (id) => {
// Logisch: Section (1) < Block (2) < Snippet (3)
if (id.startsWith('custom-section-') || id.startsWith('lib-sec-')) return 1;
if (id.startsWith('custom-block-') || id.startsWith('lib-blk-')) return 2;
if (id.startsWith('custom-snippet-') || id.startsWith('snip-')) return 3;
return 99;
};
const sortBlocksByPrefixAndLabel = (blocks) => {
blocks.sort((a, b) => {
const aId = String((a.get ? a.get('id') : a.id) || '');
const bId = String((b.get ? b.get('id') : b.id) || '');
// Hier nutzen wir die Rohdaten (aSnippet, bSnippet)
const aLabel = String((a.get ? a.get('label') : a.label) || '').toLowerCase();
const bLabel = String((b.get ? b.get('label') : b.label) || '').toLowerCase();
const aWeight = getSortWeight(aId);
const bWeight = getSortWeight(bId);
// 1. Sortierung nach Gewicht (1, 2, 3)
if (aWeight !== bWeight) return aWeight - bWeight;
// 2. Sortierung alphabetisch (a vor b)
if (aLabel < bLabel) return -1;
if (aLabel > bLabel) return 1;
return 0;
});
};
// -----------------------------------------------------------------------------------------
gjs.plugins.add('bridge-custom-blocks', (editor, opts = {}) => {
const config = B.CATEGORY_CONFIG;
const validCatIds = Object.keys(config);
// IDs zur internen Zuweisung
const CAT_CUSTOM_MAIN_ID = 'custom';
const CAT_BAUSTEINE_ID = 'bausteine';
const CAT_BIBLIOTHEK_ID = 'mysnips';
const CAT_LIB_TEMPLATES_ID = 'lib-templates';
const normalizeAndSort = (ed) => {
try {
const bm = ed.BlockManager;
const categories = bm.getCategories ? bm.getCategories() : null;
let allBlocks = bm.getAll().models || bm.getAll();
let customBlocksArray = [];
let otherBlocksArray = [];
// 1. Blöcke neu kategorisieren & trennen
(allBlocks || []).forEach(b => {
const id = String((b.get ? b.get('id') : b.id) || '');
let targetCatId = null;
if (id.startsWith('lib-tpl-ref-')) {
targetCatId = CAT_LIB_TEMPLATES_ID;
} else if (id.startsWith('custom-') || id.startsWith('lib-sec-') || id.startsWith('lib-blk-') || id.startsWith('snip-')) {
targetCatId = CAT_CUSTOM_MAIN_ID;
} else if (id.startsWith('blk-') || id.startsWith('std-')) {
targetCatId = CAT_BAUSTEINE_ID;
} else if (!b.get('category')) {
targetCatId = CAT_BIBLIOTHEK_ID;
}
if (targetCatId) {
b.set('category', targetCatId);
if (targetCatId === CAT_CUSTOM_MAIN_ID) {
customBlocksArray.push(b);
} else {
otherBlocksArray.push(b);
}
} else {
// Blöcke, die nicht zugewiesen wurden (z.B. basic/extra), behalten
otherBlocksArray.push(b);
}
});
// 2. Block-Sortierung INNERHALB der "custom" Kategorie
if (customBlocksArray.length > 0) {
sortBlocksByPrefixAndLabel(customBlocksArray);
console.log('[DEBUG PLUGIN] Custom Blocks intern sortiert.');
}
// NEU: Gesamte BlockManager-Kollektion mit sortierten Custom-Blöcken überschreiben
// Wir nehmen alle sortierten Custom-Blöcke und fügen die anderen Blöcke danach an.
const newBlockOrder = customBlocksArray.concat(otherBlocksArray);
// Dieser Hack sollte die Reihenfolge in der Seitenleiste erzwingen.
if (bm.getAll().reset) {
bm.getAll().reset(newBlockOrder);
console.log('[DEBUG PLUGIN] Gesamte Block-Kollektion mit neuer Sortierung überschrieben.');
}
// 3. Kategorien Aufräumen & Sortieren (wie zuvor)
if (categories && categories.models) {
// Aufräumen
categories.models.slice().forEach(cat => {
const catId = (cat.get('id') || cat.id || '').toLowerCase();
if (!validCatIds.includes(catId) && catId !== 'basic' && catId !== 'extra') {
categories.remove(cat);
}
});
// Labels korrigieren und Kategorie-Sortierung erzwingen
categories.models.forEach(cat => {
const catId = (cat.get('id') || cat.id || '').toLowerCase();
if (config[catId]) {
cat.set('label', config[catId].label);
cat.set('open', config[catId].open ?? true);
}
});
const catOrder = (m) => config[String((m.get('id') || m.id)).toLowerCase()]?.ord || 99;
const arr = categories.models.slice().sort((a,b) => catOrder(a) - catOrder(b));
categories.reset(arr);
}
// 4. Finaler DOM-Sweep
B.enforceCategoryOrder && B.enforceCategoryOrder(ed);
B.renderBlocks && B.renderBlocks(ed);
} catch(e) {
console.error('[CustomPlugin] Error during normalize:', e);
}
};
// 5. Listener
editor.on('block:add block:remove block:reset', () => normalizeAndSort(editor));
editor.on('load', () => {
normalizeAndSort(editor);
setTimeout(() => normalizeAndSort(editor), 100);
setTimeout(() => normalizeAndSort(editor), 800);
setTimeout(() => normalizeAndSort(editor), 1500);
});
});
})(window.grapesjs, window.BridgeParts);

View File

@@ -0,0 +1,6 @@
/* /assets/js/bridge/custom-plugin.js (FINALE VERSION: API-Logik Hülle) */
(function(B){
if (!B || typeof grapesjs === 'undefined') return;
})(window.BridgeParts || (window.BridgeParts = {}));

View File

@@ -0,0 +1,118 @@
/* /assets/js/bridge/general-functions.js (LOGIK-FIX: GLOBAL_DEBUG prüft als erstes) */
(function(B){
if (!B) return;
// --- 🎯 1. ZENTRALE LOG-KONTROLLE (Konfiguration & Defaults) ---
B.LOG_CONFIG = B.LOG_CONFIG || {};
// Globale Steuerung: Deaktiviert ALLE Logs (außer force=true)
// 🛑 KRITISCHE KORREKTUR: Wir setzen den Wert nur, wenn er noch nicht gesetzt wurde (z.B. durch bridge-core.js)
B.LOG_CONFIG.GLOBAL_DEBUG = B.LOG_CONFIG.GLOBAL_DEBUG !== undefined ? B.LOG_CONFIG.GLOBAL_DEBUG : true;
// Steuerung nach Log-Ebenen (wirken nur, wenn GLOBAL_DEBUG = true)
B.LOG_CONFIG.INFO_ENABLED = B.LOG_CONFIG.INFO_ENABLED !== undefined ? B.LOG_CONFIG.INFO_ENABLED : true;
B.LOG_CONFIG.WARN_ENABLED = B.LOG_CONFIG.WARN_ENABLED !== undefined ? B.LOG_CONFIG.WARN_ENABLED : true;
B.LOG_CONFIG.ERROR_ENABLED = B.LOG_CONFIG.ERROR_ENABLED !== undefined ? B.LOG_CONFIG.ERROR_ENABLED : true;
// Steuerung für große Datenmengen (B.logData)
B.LOG_CONFIG.DATA_ENABLED = B.LOG_CONFIG.DATA_ENABLED !== undefined ? B.LOG_CONFIG.DATA_ENABLED : true;
// NEU: Objekt zur Speicherung des individuellen Log-Status pro Plugin (Standard: leeres Objekt)
B.LOG_CONFIG.PLUGINS = B.LOG_CONFIG.PLUGINS || {};
/**
* Zentrale Log-Funktion mit Prüfung auf globale Schalter, Log-Ebenen und Plugin-spezifische Schalter.
* @param {string} pluginName - Der Name des aufrufenden Plugins (KRITISCH für die neue Logik).
* @param {string} message - Die zu loggende Nachricht.
* @param {string} color - CSS-Farbe für die Nachricht (optional).
* @param {string} type - Log-Typ ('info', 'warn', 'error').
* @param {boolean} force - Wenn true, wird geloggt, auch wenn GLOBAL_DEBUG/Plugin-Log false ist.
*/
B.log = (pluginName, message, color = 'inherit', type = 'info', force = false) => {
const config = B.LOG_CONFIG;
// 1. Prüfe auf force (immer loggen)
if (!force) {
// 🛑 KRITISCHE KORREKTUR: Prüfe auf GLOBAL_DEBUG an 2. Stelle (wenn nicht 'force')
if (!config.GLOBAL_DEBUG) {
return;
}
// 2. Prüfe den PLUGIN-SPEZIFISCHEN SCHALTER
// Wenn der Schalter im PLUGINS-Objekt existiert UND auf false gesetzt ist, abbrechen.
const pluginStatus = config.PLUGINS[pluginName];
if (pluginStatus === false) {
return;
}
// 3. Prüfe auf spezifische Log-Ebenen-Schalter
if (type === 'info' && !config.INFO_ENABLED) return;
if (type === 'warn' && !config.WARN_ENABLED) return;
if (type === 'error' && !config.ERROR_ENABLED) return;
}
// Führe das Logging aus
const stylePlugin = `color:orange; font-weight:bold;`;
const styleMessage = `color:${color}; font-weight:normal;`;
const logFn = (type === 'error') ? console.error : (type === 'warn' ? console.warn : console.log);
logFn(`%c[${pluginName}] %c${message}`, stylePlugin, styleMessage);
};
/**
* Spezielle Funktion zum Loggen großer Datenmengen (prüft B.LOG_CONFIG.DATA_ENABLED und Plugin-Schalter).
* Wird jetzt als Wrapper für B.log verwendet.
*/
B.logData = (pluginName, data) => {
// 1. Prüfe, ob das Daten-Logging global erlaubt ist
if (!B.LOG_CONFIG.DATA_ENABLED) return;
// 2. Den "Daten-Ausgabe..." Log durch B.log schicken, um die Filter zu durchlaufen
// Wir verwenden force=false, damit GLOBAL_DEBUG und Plugin-Schalter angewendet werden
B.log(pluginName, 'Daten-Ausgabe (nächste Zeile):', 'gray', 'info', false);
// 3. Wenn B.log den Filter passiert hätte, loggen wir hier das eigentliche Objekt (nur wenn GLOBAL_DEBUG true)
const pluginStatus = B.LOG_CONFIG.PLUGINS?.[pluginName];
if (B.LOG_CONFIG.GLOBAL_DEBUG && pluginStatus !== false) {
console.log(data); // Das eigentliche Objekt-Log ohne Formatierung
}
};
// --- 2. Hilfsfunktion zur Sortiergewichtung ---
const getSortWeight = (id) => {
if (['text', 'image', 'link', 'section', 'column', 'button', 'divider', 'map'].includes(id)) return 99;
if (id.startsWith('cust-')) return 1;
if (id.startsWith('lib-')) return 2;
if (id.endsWith('-fix') || id.endsWith('-flex')) {
return 3;
}
if (!id.includes('-')) return 99;
return 50;
};
// --- 3. Hilfsfunktion zur Sortierung ---
const sortBlocksByPrefixAndLabel = (blocks) => {
blocks.sort((a, b) => {
const aId = String((a.get ? a.get('id') : a.id) || '');
const bId = String((b.get ? b.get('id') : b.id) || '');
const aLabel = String((a.get ? a.get('label') : a.label) || '').toLowerCase();
const bLabel = String((b.get ? b.get('label') : b.label) || '').toLowerCase();
const aWeight = getSortWeight(aId);
const bWeight = getSortWeight(bId);
if (aWeight !== bWeight) return aWeight - bWeight;
if (aLabel < bLabel) return -1;
if (aLabel > bLabel) return 1;
return 0;
});
};
// --- 4. Funktionen zum BridgeParts-Objekt hinzufügen ---
B.getSortWeight = getSortWeight;
B.sortBlocksByPrefixAndLabel = sortBlocksByPrefixAndLabel;
})(window.BridgeParts || (window.BridgeParts = {}));

View File

@@ -0,0 +1,39 @@
/* /assets/js/bridge/helpers.js — Namespace + Utilities (kein ES-Module) */
(function(w){
var B = w.BridgeParts = w.BridgeParts || {};
B.post = function(m){ try { parent.postMessage(m,'*'); } catch{} };
B.send = function(type, payload){ B.post(Object.assign({ source:'email-editor', type:type }, payload||{})); };
B.log = function(msg){ B.post({ source:'bridge', type:'log', detail:String(msg||'') }); };
B.ready = function(cb){ (function t(){ if (w.grapesjs) return cb(); setTimeout(t,40); })(); };
B.BADGE = function(){ return document.getElementById('badge'); };
B.badgeSay = function(txt, tone){
var b = B.BADGE(); if(!b) return;
b.textContent = txt;
var cfg = {
ok: ['#ecfeff','#155e75','#a5f3fc'],
warn: ['#fef3c7','#92400e','#fde68a'],
err: ['#fee2e2','#7f1d1d','#fecaca'],
base: ['#eef2ff','#1e3a8a','#c7d2fe']
}[tone||'base'];
b.style.background = cfg[0]; b.style.color = cfg[1]; b.style.borderColor = cfg[2];
};
B.waitForBlocks = function(ed, opt){
opt = opt || {};
var timeoutMs = opt.timeoutMs || 4000, interval = opt.interval || 80;
var bm = ed.BlockManager, t0 = Date.now();
return new Promise(function(resolve){
(function tick(){
var n = (bm && bm.getAll && bm.getAll().length) || 0;
if (n > 0 || Date.now() - t0 > timeoutMs) return resolve(n);
setTimeout(tick, interval);
})();
});
};
B.renderBlocks = function(ed){ try { ed.BlockManager.render(); } catch{} };
})(window);

View File

@@ -0,0 +1,246 @@
/* /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) => {
        const mode = (B.EDITOR_MODE || 'TEMPLATES').toUpperCase();
        
// HINWEIS: Hier muss für neue Ressourcen (wie 'products') ggf. der mode angepasst werden,
// falls sie nicht in TEMPLATES geladen werden sollen.
        switch (mode) {
            case 'TEMPLATES':
                const templateResources = ['templates', 'sections', 'blocks', 'snippets', 'products']; // Beispiel: products hinzugefügt
                return templateResources.includes(resource);
            case 'SECTIONS':
                const sectionResources = ['blocks', 'snippets'];
                return sectionResources.includes(resource);
            case 'BLOCKS':
                return resource === 'snippets';
            
            default:
                log('MODE WARN', `Unbekannter Editor Modus '${mode}' festgestellt.`, 'orange', 'warn');
                return resource === 'snippets';
        }
    };
    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;
                const 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}.`);
                
                // 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 (!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;
// NEU: Ressourcen-Kinds aus der Konfiguration sammeln
const resourceKindsToLoad = Object.keys(B.RESOURCE_API_BASES || {});
if (resourceKindsToLoad.length === 0) {
log('FEHLER', 'Keine Ressourcen-Kind-Konfiguration (B.RESOURCE_API_BASES) gefunden.', '#dc3545', 'error', true);
bm.remove(PLACEHOLDER_ID);
return;
}
// Map aller Fetch-Promises erstellen
const fetchPromises = resourceKindsToLoad.map(kind =>
B.fetchResource(kind).then(items => items.map(i => ({ ...i, kind: kind })))
);
log('API START', `Starte Promise.all für API-Abruf der Blöcke/Sektionen (${resourceKindsToLoad.join(', ')})...`, '#1E90FF');
Promise.all(fetchPromises)
.then(results => {
const apiItems = results.flat().filter(item => item && item.id);
 
log(`API SUCCESS`, `${apiItems.length} Elemente gefunden.`, '#9400D3');
logApiData(apiItems); 
 
if (apiItems.length === 0) {
log('NO DATA', 'Keine API-Daten gefunden.', 'orange', 'warn', true);
} else {
apiItems.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,
                                 // 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 = {}));

View File

@@ -0,0 +1,17 @@
/* /assets/js/bridge/library-parts.js (BEREINIGT) */
(function(B){
// Alle API-spezifischen Funktionen (fetchData, fetchTemplates, etc.)
// und der apiItemCache wurden nach library-api.js verschoben.
if (!B || typeof grapesjs === 'undefined') return;
const PluginName = 'bridge-library-parts-core';
if (B.LOG_CONFIG && B.LOG_CONFIG.PLUGINS) {
B.LOG_CONFIG.PLUGINS[PluginName] = false;
}
// Zusätzliche Core-Funktionen, die nicht API-spezifisch sind, würden hier verbleiben.
})(window.BridgeParts || (window.BridgeParts = {}));

View File

@@ -0,0 +1,19 @@
/* /assets/js/bridge/library-plugin.js (Plugin für Standard-Bausteine) */
(function(B){
if (!B || typeof grapesjs === 'undefined') return;
const PluginName = 'bridge-library-plugin';
/**
* GrapesJS Plugin Registrierung: bridge-library-plugin
* Dieses Plugin dient als Platzhalter für die zukünftige Konfiguration
* der Standard-Blöcke (Bausteine). Es fügt aktuell keine Blöcke hinzu,
* sondern wartet auf die Blöcke aus dem Preset (gjs-preset-newsletter).
* Der Categorization Master wird diese Blöcke später in die Kategorie 'bausteine' verschieben.
*/
grapesjs.plugins.add(PluginName, (editor, opts = {}) => {
console.log(`[${PluginName}] Plugin registriert. Erwartet Blöcke vom Preset.`);
// ToDo: Zukünftige Standardblöcke hier hinzufügen
});
})(window.BridgeParts || (window.BridgeParts = {}));

View File

@@ -0,0 +1,166 @@
/* /assets/js/bridge/library.js — Kategorien, Defaults, Snippets (FINAL UND KORRIGIERT) */
(function(w){
  var B = w.BridgeParts = w.BridgeParts || {};
if (!B.CATEGORY_CONFIG) B.CATEGORY_CONFIG = {}; // Muss vorhanden sein
  /* Panels/Views sicherstellen (Unverändert) */
  B.ensureViews = function(ed){
    var pn = ed.Panels;
    if (!pn.getPanel('views')) pn.addPanel({ id:'views' });
    if (!pn.getButton('views','open-blocks'))
      pn.addButton('views',[{ id:'open-blocks', command:'open-blocks', togglable:1, className:'gjs-pn-btn' }]);
    if (!pn.getButton('views','open-layers'))
      pn.addButton('views',[{ id:'open-layers', command:'open-layers', togglable:1, className:'gjs-pn-btn' }]);
    if (!pn.getButton('views','open-sm'))
      pn.addButton('views',[{ id:'open-sm', command:'open-sm', togglable:1, className:'gjs-pn-btn' }]);
    try{ var b=pn.getButton('views','open-blocks'); b && b.set('active',true); }catch{}
  };
  /* Helpers --------------------------------------------------------------- */
  function addOnce(bm, id, def){
    if (!id || typeof id !== 'string') return;
    try{ if (bm.get && bm.get(id)) return; bm.add(id, def); }catch{}
  }
  // Kategorien erzeugen (Funktion unverändert)
  function forceCategory(bm, id, label, open){
    try{
      if (typeof bm.addCategory === 'function') {
        var cts = bm.getCategories && bm.getCategories();
        var find = function(){
          if (!cts) return null;
          if (typeof cts.findWhere === 'function') {
            return cts.findWhere({ id }) || cts.findWhere({ label });
          }
          var arr = cts.models || cts || [];
          for (var i=0;i<arr.length;i++){
            var m = arr[i], lid = (m.get?m.get('id'):m.id), lbl=(m.get?m.get('label'):m.label);
            if (lid===id || lbl===label) return m;
          }
          return null;
        };
        var c = find();
        if (!c) c = bm.addCategory({ id:id, label:label, open:!!open });
        try { c.set && c.set('open', !!open); } catch {}
        return c;
      }
    }catch{}
    return { id:id, label:label, open:!!open, __labelOnly:true };
  }
  function ensureCategories(bm){
    // Stellt sicher, dass die vier Hauptkategorien erstellt werden.
    var C_CUSTOM = forceCategory(bm,'custom', B.CATEGORY_CONFIG.custom.label, true);
    var C_STD  = forceCategory(bm,'bausteine', B.CATEGORY_CONFIG.bausteine.label, true);
    var C_LIB  = forceCategory(bm,'mysnips', B.CATEGORY_CONFIG.mysnips.label, true);
    var C_TPLS = forceCategory(bm,'lib-templates', B.CATEGORY_CONFIG['lib-templates'].label, true);
   
    return { C_CUSTOM, C_STD, C_LIB, C_TPLS };
  }
  B._ensureCategories = ensureCategories;
  /* Harte Zuordnung von Custom-IDs → Kategorien (ENTFERNT) */
  B.forceFixFlexCategories = function(ed){
    // Logik liegt jetzt im Plugin
  };
  // **Harte** Sortierung der Kategorien Sammlung + DOM (DOM-Fallback)
  B.enforceCategoryOrder = function(ed){
    try{
      var bm  = ed.BlockManager;
      setTimeout(function(){
        try{
          var cont = bm.getContainer && bm.getContainer();
          if (!cont) return;
          // Notfall-DOM-Sortierung (falls das Plugin versagt)
          var nodes = Array.prototype.slice.call(cont.querySelectorAll('.gjs-block-category'));
          if (!nodes.length) return;
          function nodeRank(n){
            var t = (n.querySelector('.gjs-title') || n).textContent || '';
            var s = t.trim().toLowerCase();
            if (s==='bibliothek: templates (ref)') return 1; // Prio 1
            if (s==='custom')                       return 2; // Prio 2
            if (s==='bausteine')                    return 3; // Prio 3
            if (s.startsWith('bibliothek'))         return 4; // Prio 4+
            return 99;
          }
          nodes.sort(function(a,b){ return nodeRank(a)-nodeRank(b); })
               .forEach(function(n){ cont.appendChild(n); });
        }catch{}
      },0);
    }catch{}
  };
  /* Kategorien normalisieren + sortieren (ENTFERNT) */
  B.normalizeCategories = function(ed){
    // Logik liegt jetzt im Plugin
  };
  /* Bausteine (Explizite Zuweisung zur ID 'bausteine') */
  B.addDefaultBlocks = function(ed){
    var bm = ed.BlockManager;
    B._ensureCategories(bm); // Stellen Sie sicher, dass alle Hauptkategorien existieren
    // Explizite Kategorie-Definition basierend auf der ID 'bausteine'
    var cat_bausteine = { id:'bausteine', label:B.CATEGORY_CONFIG.bausteine.label, open:true };
  addOnce(bm,'blk-img', { label:'Bild', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><rect x="3" y="5" width="18" height="14" fill="none" stroke="currentColor" stroke-width="2"/><path d="M8 13l3-3 5 6" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="9" cy="9" r="1.5" fill="currentColor"/></svg>', content:'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-family:Arial,sans-serif"><tr><td align="center"><img src="https://via.placeholder.com/600x200" alt="" width="600" style="max-width:100%;display:block;border:0;" /></td></tr></table>' });
  addOnce(bm,'blk-btn', { label:'Button', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><rect x="4" y="7" width="16" height="10" rx="5" fill="none" stroke="currentColor" stroke-width="2"/></svg>', content:'<table role="presentation" cellpadding="0" cellspacing="0" align="center" style="font-family:Arial,sans-serif;margin:16px auto;"><tr><td><a href="#" style="background:#0ea5e9;color:#fff;text-decoration:none;padding:12px 20px;border-radius:6px;display:inline-block;">Call to Action</a></td></tr></table>' });
  addOnce(bm,'blk-text', { label:'Text', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><path d="M4 7h16M4 12h10M4 17h8" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>', content:'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-family:Arial,sans-serif"><tr><td style="font-size:16px;line-height:1.5;color:#0f172a;"><p style="margin:0 0 12px 0;">Überschrift</p><p style="margin:0;">Fließtext …</p></td></tr></table>' });
  addOnce(bm,'blk-2cols', { label:'2 Spalten', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><rect x="4" y="6" width="7" height="12" fill="none" stroke="currentColor" stroke-width="2"/><rect x="13" y="6" width="7" height="12" fill="none" stroke="currentColor" stroke-width="2"/></svg>', content:'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-family:Arial,sans-serif"><tr><td width="50%" valign="top" style="padding:8px;"><p style="margin:0;">Linke Spalte</p></td><td width="50%" valign="top" style="padding:8px;"><p style="margin:0;">Rechte Spalte</p></td></tr></table>' });
  addOnce(bm,'blk-600', { label:'Container 600px', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><rect x="3" y="7" width="18" height="10" rx="2" fill="none" stroke="currentColor" stroke-width="2"/></svg>', content:'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-family:Arial,sans-serif"><tr><td align="center"><table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px;max-width:100%;background:#ffffff;border:1px solid #e5e7eb;border-radius:6px;"><tr><td style="padding:16px;"><p style="margin:0;">Inhalt hier hinein …</p></td></tr></table></td></tr></table>' });
  addOnce(bm,'blk-hr', { label:'Trenner', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><path d="M4 12h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>', content:'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:8px 0;"><hr style="border:none;border-top:1px solid #e5e7eb;margin:0;" /></td></tr></table>' });
  addOnce(bm,'blk-spacer', { label:'Abstand', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><path d="M12 6v12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="6" r="1.5" fill="currentColor"/><circle cx="12" cy="18" r="1.5" fill="currentColor"/></svg>', content:'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="height:16px;line-height:16px;font-size:0;">&nbsp;</td></tr></table>' });
  };
  /* Snippets → Custom (Initiale Zuweisung zur ID 'custom') */
  B.replaceSnippetBlocks = function(ed, list){
    try{
      var bm = ed.BlockManager;
      B._ensureCategories(bm);
      var all = (bm.getAll && bm.getAll()) || [];
      (all.models || all).forEach(function(b){
        if (!b) return;
        var id=b.get&&b.get('id');
        // Entferne alte Snippets
        if(id && String(id).startsWith('snip-')) try{ bm.remove(id); }catch{}
      });
      // Explizite Kategorie-Definition basierend auf der ID 'custom'
      var cat_custom = { id:'custom', label:B.CATEGORY_CONFIG.custom.label, open:true };
      (list||[]).forEach(function(raw){
        if(!raw) return;
        var html = raw.html || raw.content || '';
        if(!html) return;
        var id = 'snip-'+(raw.id ?? ('x'+Math.random().toString(36).slice(2)));
        addOnce(bm, id, {
          label: raw.name || ('Snippet '+(raw.id ?? '')),
          // Zuweisung zur 'Custom' Kategorie
          category: cat_custom,
          media:'<svg viewBox="0 0 24 24" width="22" height="22"><path d="M5 7h14M5 12h10M5 17h8" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>',
          content: html
        });
      });
    }catch{}
    B.renderBlocks && B.renderBlocks(ed);
  };
  /* Snippets nachladen (für Buttons) */
  B.fetchSnippets = async function(){
    try{
      var res = await fetch('../api.php?resource=snippets&action=list&t='+Date.now(), {
        credentials:'same-origin', cache:'no-store', headers:{'Cache-Control':'no-cache'}
      });
      var rows = await res.json();
      rows = rows && rows.items ? rows.items : (Array.isArray(rows) ? rows : []);
      return rows.map(function(r){ return { id:r.id, name:r.name, html:r.content||r.html||'' }; });
    }catch(e){ B.log && B.log('reload-snippets-error:'+e); return []; }
  };
})(window);

View File

@@ -0,0 +1,166 @@
/* /assets/js/bridge/refs.js — Referenzen & Custom Fix/Flex (FINAL KORRIGIERT: Snippet Cleanup) */
(function (w) {
  var B = w.BridgeParts = w.BridgeParts || {};
  if (!B.CATEGORY_CONFIG) B.CATEGORY_CONFIG = {}; 
  /* ---------- Basis-Konfig (unverändert) ---------- */
  var SHOW_SNIPPETS_IN_FIX_DEFAULT = false;
  var MODE = (window.__editorMode || 'templates').toLowerCase();
  /* ---------- Hilfs-UI (unverändert) ---------- */
  B.editorRefPlaceholder = function (type, id, name) {
    var safe = (name || '').replace(/[<>&"]/g, '');
    return {
      html:
        '<div data-ref-type="' + type + '" data-ref-id="' + id + '" data-ref-name="' + safe + '" ' +
        'style="border:1px dashed #94a3b8;padding:8px;border-radius:6px;background:#f8fafc;margin:8px 0;">' +
        '<strong style="font:600 12px system-ui,Arial">Ref: ' + type + ' #' + id + '</strong>' +
        '<div style="font:12px system-ui,Arial;opacity:.8">' + safe + '</div>' +
        '</div>'
    };
  };
  /* ---------- Loader (REST) (unverändert) ---------- */
  async function jsonList(url){
    try{
      var res = await fetch(url, { credentials:'same-origin', cache:'no-store' });
      var data = await res.json();
      return data && data.items ? data.items : (Array.isArray(data) ? data : []);
    }catch(e){ return []; }
  }
  B.fetchTemplates = async function(){ 
    var rows = await jsonList('../api.php?action=templates.list&t='+Date.now());
    return rows.map(r => ({ id:r.id, name:r.name }));
  };
  B.fetchTemplateFull = async function (id) {
    try {
      var url = '../api.php?action=templates.get&id=' + encodeURIComponent(id) + '&t=' + Date.now();
      var res = await fetch(url, { credentials: 'same-origin', cache: 'no-store' });
      var data = await res.json();
      var it = data && (data.item || data);
      return (it && (it.html || it.content)) ? (it.html || it.content) : '';
    } catch (e) { return ''; }
  };
  B.fetchSections = async function(){
    var rows = await jsonList('../api.php?action=sections.list&t='+Date.now());
    return rows.map(r => ({ id:r.id, name:r.name, html:r.html || '' }));
  };
  B.fetchBlocks = async function(){
    var rows = await jsonList('../api.php?action=blocks.list&t='+Date.now());
    return rows.map(r => ({ id:r.id, name:r.name, html:r.html || '' }));
  };
  B.fetchSnippets = async function(){
    var rows = await jsonList('../api.php?action=snippets.list&t='+Date.now());
    return rows.map(r => ({ id:r.id, name:r.name, html:r.html || r.content || '' }));
  };
  /* ---------- lokale Helfer (unverändert) ---------- */
  function addOnce(bm, id, def){
    if (!id || typeof id !== 'string') return;
    try{ if (bm.get && bm.get(id)) return; bm.add(id, def); }catch{}
  }
  function removeByPrefix(bm, prefix){
    try{
      var all = (bm.getAll && bm.getAll()) || [];
      (all.models || all).forEach(function(b){
        if (!b) return;
        var id = (b.get && b.get('id')) || b.id || '';
        if (id && String(id).startsWith(prefix)) {
          try{ bm.remove(id); }catch{}
        }
      });
    }catch{}
  }
  /* ---------- Referenzbibliothek (lib-*) (angepasst: Zuweisung zu 'custom') ---------- */
  B.addReferenceLibrary = function (ed, payload) {
    payload = payload || {};
    var templates = payload.templates || [];
    var sections  = payload.sections  || [];
    var blocks    = payload.blocks    || [];
// Aggressive Bereinigung aller lib-tpl-ref-* Blöcke
removeByPrefix(ed.BlockManager, 'lib-tpl-ref-');
    var bm = ed.BlockManager;
    if (B._ensureCategories) B._ensureCategories(bm); // Stellt sicher, dass die Hauptkategorien existieren
    // Explizite Kategorie-Definitionen
    var cat_templates = { id:'lib-templates', label:B.CATEGORY_CONFIG['lib-templates'].label, open:true };
    var cat_custom = { id:'custom', label:B.CATEGORY_CONFIG.custom.label, open:true };
    
    // Template-Referenzen (Prio 1) - NUR IM TEMPLATE-MODUS HINZUFÜGEN
if (MODE === 'templates') {
      templates.forEach(function (t) {
        addOnce(bm, 'lib-tpl-ref-' + t.id, {
          label: (t.name || ('Vorlage #' + t.id)),
          category: cat_templates, // Zuweisung zur Prio 1
          media: '<svg viewBox="0 0 24 24" width="22" height="22"><rect x="4" y="5" width="16" height="14" rx="2" stroke="currentColor" fill="none" stroke-width="2"/></svg>',
          content: B.editorRefPlaceholder('template', t.id, t.name).html
        });
      });
}
    // Sections-Referenzen (werden zu Custom umgeleitet, Prio 2)
    sections.forEach(function (s) {
      addOnce(bm, 'lib-sec-' + s.id, {
        label: s.name || ('Section #' + s.id),
        category: cat_custom, // Explizit Custom
        media: '<svg viewBox="0 0 24 24" width="22" height="22"><rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" fill="none" stroke-width="2"/></svg>',
        content: B.editorRefPlaceholder('section', s.id, s.name).html
      });
    });
    // Blocks-Referenzen (werden zu Custom umgeleitet, Prio 2)
    blocks.forEach(function (b) {
      addOnce(bm, 'lib-blk-' + b.id, {
        label: b.name || ('Block #' + b.id),
        category: cat_custom, // Explizit Custom
        media: '<svg viewBox="0 0 24 24" width="22" height="22"><rect x="6" y="7" width="12" height="10" rx="2" stroke="currentColor" fill="none" stroke-width="2"/></svg>',
        content: B.editorRefPlaceholder('block', b.id, b.name).html
      });
    });
  };
  /* ---------- Custom Fix/Flex (ENTFERNT / GELEERT) ---------- */
  B.addCustomLibrary = function (ed, payload, mode) {
    /* (Keine Aktion nötig.) */
  };
// WICHTIGE NEUE FUNKTION: Entfernt alle alten Snippet-Blöcke
B.addEditableTemplatesLibrary = function(ed) {
// Aggressive Bereinigung aller alten flexiblen Snippet-Blöcke,
// um Konflikte mit den neuen custom-snippet-* Blöcken zu vermeiden.
removeByPrefix(ed.BlockManager, 'snip-');
return Promise.resolve();
};
  /* ---------- Ref-Sammlung für Speichern/Render (unverändert) ---------- */
  B.collectRefs = function (ed) {
    var root = ed.getWrapper && ed.getWrapper();
    var els = root && root.find ? root.find('[data-ref-type]') : [];
    var out = [];
    if (Array.isArray(els) && els.length) {
      els.forEach(function (el) {
        try {
          var m = el.getAttributes ? el.getAttributes() : {};
          var t = (m['data-ref-type'] || '').toString().toLowerCase();
          var i = parseInt(m['data-ref-id'] || '0', 10);
          if (!t || !i) return;
          if (!/^(template|section|block|snippet)$/.test(t)) return;
          if (t === 'snippet') return; // Snippets immer flex/by value
          out.push({
            sort: out.length,
            ref_type: t === 'template' ? 'section' : t,
            ref_id: i,
            overrides_json: null,
            lock_to_version: null
          });
        } catch {}
      });
    }
    return out;
  };
})(window);