Files
emailtemplate.it/public/editor/bridge-core.js
2026-01-16 01:46:58 +01:00

1527 lines
75 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* /editor/bridge-core.js — Loader + Orchestrator (FINAL & LOG-KONTROLLIERT) */
(function () {
    
    // --- Initialisierung BridgeParts (B) und Plugin-Registry ---
    if (!window.BridgeParts) window.BridgeParts = {};
    const B = window.BridgeParts;
    
    // ----------------------------------------------------------------------
    // 🎯 LOKALE LOG-KONFIGURATION & WRAPPER
    // ----------------------------------------------------------------------
    const PluginName = 'bridge-core';
    
    // Setzen Sie dies auf 'false', um alle Logs NUR für dieses Plugin zu deaktivieren.
    if (B.LOG_CONFIG) {
    B.LOG_CONFIG.PLUGINS[PluginName] = true; // bridge-core spezifisch deaktivieren (optional)
    }
    
    /**
     * NEUER LOKALER WRAPPER, der die zentrale B.log Funktion verwendet.
     */
    const log = (type, message, color = '#1E90FF', logType = 'info', force = false) => {
        // Loggt NUR, wenn B.log verfügbar ist (aus general-functions.js).
        if (typeof B.log === 'function') {
            B.log(PluginName, `[${type}] ${message}`, color, logType, force);
        }
        // Ansonsten wird NICHTS geloggt, bis general-functions.js geladen ist.
    };
    // ----------------------------------------------------------------------
    // 🛑 GLOBALER LOG ZUR BESTÄTIGUNG DER SKRIPT-AUSFÜHRUNG
    // log('START', `SKRIPT-AUSFÜHRUNG GESTARTET.`, '#DC143C', 'info', true); // DEAKTIVIERT/IGNORIERT DURCH FEHLENDEN B.log
    // ----------------------------------------------------------------------
    // 🛑 KONFIGURATION: NEWSLETTER-PRESET-TOGGLE
    // ----------------------------------------------------------------------
    const LOAD_NEWSLETTER_PRESET = false; // <<< KRITISCHER FIX: Auf FALSE gesetzt, um den "defaults" Konflikt zu beheben!
    // ----------------------------------------------------------------------
    
    if (window.__bridgeCoreInitialized) {
        log('INIT ABORT', 'Bridge Core wurde bereits initialisiert.', 'orange');
        return; 
    }
    window.__bridgeCoreInitialized = true;
    
    // --- Initialisierung BridgeParts (B) und Plugin-Registry ---
B.BASE_PATH_BRIDGE = '../assets/js/bridge/';
B.BASE_PATH_CONFIG = B.BASE_PATH_BRIDGE;
const apiFallback = '/api.php';
B.API_BASE = B.API_BASE || apiFallback;
B.API_KERNEL_URL = B.API_KERNEL_URL || B.API_BASE;
    
    B.GrapesJSPlugins = []; 
    
    B.registerGrapesJSPlugin = (name, pluginFn) => {
        B.GrapesJSPlugins.push({ name, pluginFn });
        log('PLUGIN REGISTER', `Plugin zur Registry hinzugefügt: ${name}`, 'yellow');
    };
    // --- DEBUG-HELPER UND LOADER-HELPER ---
    const badgeSay = (text, type = 'info') => {
        const b=document.getElementById('badge');
        if (!b) return;
        b.textContent = text;
        switch(type) {
            case 'ok': b.style.background = '#dcfce7'; b.style.color = '#15803d'; b.style.borderColor = '#bbf7d0'; break;
            case 'error': b.style.background = '#fee2e2'; b.style.color = '#7f1d1d'; b.style.borderColor = '#fecaca'; break;
            default: b.style.background = '#eef2ff'; b.style.color = '#1e3a8a'; b.style.borderColor = '#c7d2fe';
        }
    };
    function loadScript(url, done) {
        const filename = url.split('/').pop();
        var s = document.createElement('script');
        s.src = url + (url.indexOf('?') === -1 ? '?v=' : '&v=') + Date.now(); 
        s.async = false;
        
        s.onload = function(){ 
            log('LOAD SUCCESS', `Skript geladen: ${filename}`, 'green');
            try { 
                done && done(); 
            } catch(e){ 
                if (e.message.includes('setting getter-only property "defaults"') || e.message.includes('Cannot set property defaults')) {
                    log('RUNTIME WARNING', `IGNORIERE Block-Konflikt in ${filename}: ${e.message}`, 'orange', 'warn');
                } else {
                    // 🛑 KORREKTUR: force: true explizit auf false setzen (oder weglassen)
                    log('RUNTIME ERROR', `Fehler in Callback nach ${filename}: ${e.message}`, 'red', 'error', false); 
                }
            } 
        };
        
        s.onerror = function(){ 
            // 🛑 KORREKTUR: force: true explizit auf false setzen (oder weglassen)
            log('LOAD FAILED', `Skript FEHLT oder Pfad falsch: ${filename}`, 'red', 'error', false);
            badgeSay(`Ladefehler: ${filename}`, 'error');
            try { done && done(); } catch(e){} 
        };
        document.head.appendChild(s);
    }
    /**
     * HILFSFUNKTION: Wandelt den Dateinamen (z.B. blocks-standard.js) in den globalen
     * Objektnamen (z.B. BridgeBlocksStandard) um.
     */
    function getPluginObjectName(fileName) {
        // 1. Entferne Dateiendung (.js)
        let name = fileName.replace('.js', ''); // 'blocks-standard'
        
        // 2. Teile in Bestandteile zerlegen und den ersten Buchstaben groß schreiben
        const parts = name.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)); // ['Blocks', 'Standard']
        
        // 3. Mit 'Bridge' prefixen und zusammenfügen
        return 'Bridge' + parts.join(''); // 'BridgeBlocksStandard'
    }
    
    // 🛑 NEUE FUNKTION: Erstellt alle in der Konfiguration definierten Kategorien.
    function ensureConfiguredCategories(editor) {
        const bm = editor.BlockManager;
        // HINWEIS: B.CATEGORY_CONFIG wird in category-config.js befüllt (muss vorher geladen werden)
        const config = window.BridgeParts?.CATEGORY_CONFIG || {};
        Object.keys(config)
            .sort((a, b) => (config[a].ord || 999) - (config[b].ord || 999))
            .forEach(catId => {
                const catConf = config[catId];
                // Category wird nur erstellt, wenn sie noch nicht existiert
                if (!bm.getCategories().get(catId)) {
                    bm.getCategories().add({
                        id: catId,
                        label: catConf.label,
                        open: catConf.open !== false,
                        order: catConf.ord || 999
                    });
                    log('CAT INIT', `Kategorie '${catId}' explizit erstellt.`, 'green');
                }
            });
    }
    function loadBridgeParts(cb){
        const base = B.BASE_PATH_BRIDGE; 
        
        // 🛑 LOKALES LOGGING ENTFERNT, DA ES VOR B.log LIEGT.
        // log('LOAD START', 'Starte Laden der modularen Bridge-Teile (Geordnet).'); 
        
        const coreFiles = [
            base + 'general-functions.js', // <<< RE-AKTIVIERT: Für B.log
            base + 'category-config.js', // <<< RE-AKTIVIERT: Für B.CATEGORY_CONFIG (und damit API-Flexibilität)
            base + 'library-parts.js',
            base + 'categorization-master.js',
            base + 'categorization-cleanup.js',
        ];
        const initialLoadList = [...coreFiles];
        
        function recursiveLoader(list, index = 0) {
            if (index >= list.length) {
                log('LOAD END', 'Initial-Bridge-Skripte geladen.', 'green');
                
                const config = window.BridgeParts?.CATEGORY_CONFIG || {};
                let allBlockFiles = [];
                
                // Dynamisches Sammeln der Block-Dateien aus der Config
                Object.keys(config)
                    .sort((a, b) => (config[a].ord || 999) - (config[b].ord || 999))
                    .forEach(key => {
                        // Sammelt alle Dateien, egal ob sync oder async
                        if (Array.isArray(config[key].files)) {
                            allBlockFiles.push(...config[key].files.map(file => base + file)); 
                        }
                    });
                
                // Duplikate entfernen (falls eine Datei in mehreren Kategorien gelistet ist)
                allBlockFiles = Array.from(new Set(allBlockFiles));
                
                function loadBlockFiles(blockIndex = 0) {
                    if (blockIndex >= allBlockFiles.length) {
                        log('LOAD END', 'Alle Blöcke geladen.', 'green');
                        return cb && cb(B);
                    }
                    log('LOADING BLOCKS', `Lade Block-Skript [${blockIndex + 1}/${allBlockFiles.length}]: ${allBlockFiles[blockIndex].split('/').pop()}`);
                    loadScript(allBlockFiles[blockIndex], function(){
                        loadBlockFiles(blockIndex + 1);
                    });
                }
                loadBlockFiles();
                return;
            }
            
            // 🛑 LOKALES LOGGING ENTFERNT, DA ES VOR B.log LIEGT.
            log('LOADING CORE', `Lade Skript [${index + 1}/${initialLoadList.length}]: ${list[index].split('/').pop()}`); // Loggt ab dem 3. Skript, da general-functions.js an 2. Stelle geladen wird.
            loadScript(list[index], function(){
                recursiveLoader(list, index + 1);
            });
        }
        
        recursiveLoader(initialLoadList);
    }
    try { parent.postMessage({ source:'bridge', type:'boot' }, '*'); } catch {}
    var MODE = (window.__editorMode || 'templates').toLowerCase();
    const replaceReferenceLibrary = (editor, ref, mode) => { 
        (window.BridgeParts?.addReferenceLibrary || (()=>{}))(editor, ref, mode); 
    };
    const upsertCustomForBothCats = (editor, payload) => { 
        (window.BridgeParts?.upsertCustomForBothCats || (()=>{}))(editor, payload);
    };
    
    // --- Init & Events (Plugin integriert) ---------------------------------------------
    loadBridgeParts(function(B){
        log('INIT START', 'Alle Bridge-Teile geladen, starte GrapesJS-Initialisierung.', 'orange');
        
        if (typeof grapesjs === 'undefined' || !grapesjs.init) {
            // 🛑 KORREKTUR: force: true explizit auf false setzen (oder weglassen)
            log('CRITICAL ERROR', 'Das globale Objekt grapesjs ist nicht verfügbar! Laden von grapes.min.js ist fehlgeschlagen.', 'red', 'error', false);
            badgeSay('Fehler: GrapesJS nicht geladen!', 'error');
            return; 
        }
        // 🛑 KRITISCHER FIX TEIL 1: Registriere alle gesammelten Bridge-Plugins global.
        if (typeof grapesjs.plugins.add === 'function') {
            B.GrapesJSPlugins.forEach(p => {
                grapesjs.plugins.add(p.name, p.pluginFn);
                log('PLUGIN ACTIVATION', `GrapesJS Plugin global bereitgestellt: ${p.name}`, 'lime');
            });
        } else {
            // 🛑 KORREKTUR: force: true explizit auf false setzen (oder weglassen)
            log('PLUGIN ERROR', `GrapesJS Plugin-API (grapesjs.plugins.add) fehlt. Plugins können nicht registriert werden.`, 'red', 'error', false);
        }
        // 🛑 KRITISCHER FIX: Safety Plugin MUSS die fehlenden Views Panels hinzufügen.
        function safetyPlugin(editor){
            const pn = editor.Panels, orig = pn.getButton.bind(pn);
            pn.getButton = (pid, id) => orig(pid, id) || { set(){}, get(){ return null; } }; 
            
            // Fügen Sie das Panel 'views' hinzu, wenn es fehlt
            if(!pn.getPanel('views')) {
                pn.addPanel({ id: 'views', el: '.gjs-pn-views' }); 
                log('PANEL FIX', "Das 'views' Panel wurde nachträglich hinzugefügt.", 'yellow', 'warn');
            }
            
            // Stellen Sie sicher, dass der Block Manager in den Views-Container rendert
            editor.Config.blockManager = editor.Config.blockManager || {};
            editor.Config.blockManager.appendTo = editor.Config.blockManager.appendTo || '.gjs-blocks';
            
            // Der fehlerhafte Timeout-Block wurde entfernt.
        }
let pluginsList = [
safetyPlugin,
// 🛑 KRITISCHE ERGÄNZUNG: Aktiviert das registrierte API-Plugin
'bridge-blocks-api',
'bridge-categorization-master',
'bridge-categorization-cleanup',
];
        if (LOAD_NEWSLETTER_PRESET) {
            pluginsList.push('gjs-preset-newsletter');
        }
        
const storageConf = {
autoload: false,
autosave: false,
};
const execRteCommand = (rte, cmd, value, docOverride) => {
try {
if (rte && typeof rte.exec === 'function') {
rte.exec(cmd, value);
return true;
}
} catch {}
try {
const doc = docOverride
|| rte?.doc
|| rte?.el?.ownerDocument
|| document;
if (rte?.el && typeof rte.el.focus === 'function') {
rte.el.focus();
}
const ok = doc.execCommand(cmd, false, value);
if (ok === false && cmd === 'insertText') {
doc.execCommand('insertHTML', false, String(value || '').replace(/</g, '&lt;').replace(/>/g, '&gt;'));
}
return true;
} catch {}
return false;
};
const normalizeRteActionList = (editor, names) => {
const cfg = editor.getConfig ? editor.getConfig() : {};
cfg.richTextEditor = cfg.richTextEditor || {};
const base = Array.isArray(cfg.richTextEditor.actions)
? cfg.richTextEditor.actions.slice()
: ['bold', 'italic', 'underline', 'strikethrough', 'link'];
names.forEach((name) => {
const exists = base.some((item) => (typeof item === 'string' ? item === name : item && item.name === name));
if (!exists) base.push(name);
});
cfg.richTextEditor.actions = base;
};
const ensureTextToolbarButton = (editor, component) => {
if (!component || !component.is) return;
const isTextLike = component.is('text') || component.is('button') || component.is('link');
if (!isTextLike) return;
const toolbar = (component.get && component.get('toolbar')) || [];
if (toolbar.some((item) => item && item.command === 'bridge-open-richtext')) return;
toolbar.push({
label: '<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M4 20h4l10-10-4-4L4 16v4zm14.7-11.3c.4-.4.4-1 0-1.4l-2-2c-.4-.4-1-.4-1.4 0l-1.3 1.3 4 4 1.7-1.9z" fill="currentColor"/></svg>',
attributes: { title: 'Richtext bearbeiten' },
command: 'bridge-open-richtext',
});
component.set && component.set('toolbar', toolbar);
};
const openRichTextModal = (editor, component) => {
if (!component || !component.is || !(component.is('text') || component.is('button') || component.is('link'))) {
log('RTE', 'Bitte zuerst ein Text-Element auswaehlen.', '#888');
return;
}
const modal = editor.Modal;
if (!modal) return;
if (editor.__bridgeRteModalOpen) return;
editor.__bridgeRteModalOpen = true;
editor.__bridgeRteAllowClose = false;
let reopenGuard = false;
const closeModal = () => {
editor.__bridgeRteAllowClose = true;
editor.__bridgeRteModalOpen = false;
if (typeof modal.close === 'function') {
modal.close();
} else {
const mdl = modal.getModel && modal.getModel();
if (mdl && typeof mdl.set === 'function') {
mdl.set('open', false);
} else if (modal.el) {
modal.el.style.display = 'none';
}
}
};
if (!modal.__bridgeCloseLocked) {
modal.__bridgeCloseLocked = true;
modal.__bridgeOriginalClose = modal.close ? modal.close.bind(modal) : null;
modal.close = function (...args) {
if (!editor.__bridgeRteModalOpen && typeof modal.__bridgeOriginalClose === 'function') {
return modal.__bridgeOriginalClose(...args);
}
if (editor.__bridgeRteAllowClose && typeof modal.__bridgeOriginalClose === 'function') {
editor.__bridgeRteAllowClose = false;
return modal.__bridgeOriginalClose(...args);
}
if (!editor.__bridgeRteModalOpen && !modal.__bridgeOriginalClose) {
const mdl = modal.getModel && modal.getModel();
if (mdl && typeof mdl.set === 'function') {
mdl.set('open', false);
} else if (modal.el) {
modal.el.style.display = 'none';
}
}
if (editor.__bridgeRteAllowClose && !modal.__bridgeOriginalClose) {
editor.__bridgeRteAllowClose = false;
const mdl = modal.getModel && modal.getModel();
if (mdl && typeof mdl.set === 'function') {
mdl.set('open', false);
} else if (modal.el) {
modal.el.style.display = 'none';
}
}
};
}
const doc = document;
const container = doc.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '10px';
container.style.height = '100%';
container.style.minHeight = '360px';
const toolbar = doc.createElement('div');
toolbar.style.display = 'flex';
toolbar.style.flexWrap = 'wrap';
toolbar.style.gap = '6px';
toolbar.style.alignItems = 'center';
const content = doc.createElement('div');
content.contentEditable = 'true';
content.style.flex = '1';
content.style.minHeight = '280px';
content.style.border = '1px solid #cbd5f5';
content.style.borderRadius = '6px';
content.style.padding = '12px';
content.style.background = '#ffffff';
content.style.overflow = 'auto';
content.style.fontFamily = 'Arial, sans-serif';
content.style.fontSize = '14px';
const initialHtml = (component.view && component.view.el && component.view.el.innerHTML)
|| (component.get && component.get('content'))
|| '';
content.innerHTML = initialHtml;
const addButton = (labelHtml, title, cmd, valueGetter) => {
const btn = doc.createElement('button');
btn.type = 'button';
btn.innerHTML = labelHtml;
btn.title = title;
btn.style.padding = '4px 8px';
btn.style.border = '1px solid #cbd5f5';
btn.style.borderRadius = '4px';
btn.style.background = '#f8fafc';
btn.style.cursor = 'pointer';
btn.addEventListener('click', () => {
content.focus();
const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter;
if (value === null || value === undefined) return;
if (cmd === 'createLink' && !value) return;
execRteCommand(null, cmd, value, content.ownerDocument);
});
toolbar.appendChild(btn);
};
const addSelect = (options, title, onChange) => {
const select = doc.createElement('select');
select.title = title;
select.style.padding = '4px 8px';
select.style.border = '1px solid #cbd5f5';
select.style.borderRadius = '4px';
select.style.background = '#ffffff';
options.forEach((opt) => {
const optEl = doc.createElement('option');
optEl.value = opt.value;
optEl.textContent = opt.label;
select.appendChild(optEl);
});
select.addEventListener('change', () => {
const value = select.value;
content.focus();
if (value) onChange(value);
});
toolbar.appendChild(select);
return select;
};
const insertText = (text) => {
content.focus();
if (!execRteCommand(null, 'insertText', text)) {
execRteCommand(null, 'insertHTML', String(text).replace(/</g, '&lt;').replace(/>/g, '&gt;'));
}
};
const icon = (path) => `<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="${path}" fill="currentColor"/></svg>`;
addButton('<strong>B</strong>', 'Fett', 'bold');
addButton('<em>I</em>', 'Kursiv', 'italic');
addButton('<span style="text-decoration:underline">U</span>', 'Unterstrichen', 'underline');
addButton('<span style="text-decoration:line-through">S</span>', 'Durchgestrichen', 'strikethrough');
addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList');
addButton(icon('M4 7h14v2H4zM4 11h14v2H4zM4 15h14v2H4z') + '<span style="font-size:10px;margin-left:4px">1.</span>', 'Liste (geordnet)', 'insertOrderedList');
addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h12v2H4z'), 'Linksbundig', 'justifyLeft');
addButton(icon('M5 7h14v2H5zM4 11h16v2H4zM5 15h14v2H5z'), 'Zentriert', 'justifyCenter');
addButton(icon('M10 7h10v2H10zM4 11h16v2H4zM8 15h12v2H8z'), 'Rechtsbundig', 'justifyRight');
addButton(icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Blocksatz', 'justifyFull');
addButton('Link', 'Link einfuegen', 'createLink', () => prompt('Link-URL eingeben', 'https://'));
addButton('Unlink', 'Link entfernen', 'unlink');
addButton('Sub', 'Tiefgestellt', 'subscript');
addButton('Sup', 'Hochgestellt', 'superscript');
addButton('Einr.', 'Einzug', 'indent');
addButton('Aus.', 'Ausruecken', 'outdent');
addButton('Clear', 'Formatierung entfernen', 'removeFormat');
const fontOptions = (B.RTE_FONTS && Array.isArray(B.RTE_FONTS) && B.RTE_FONTS.length)
? B.RTE_FONTS
: [
{ label: 'Arial', value: 'Arial, sans-serif' },
{ label: 'Calibri', value: 'Calibri, sans-serif' },
{ label: 'Cambria', value: 'Cambria, serif' },
{ label: 'Georgia', value: 'Georgia, serif' },
{ label: 'Tahoma', value: 'Tahoma, sans-serif' },
{ label: 'Times New Roman', value: 'Times New Roman, serif' },
{ label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
{ label: 'Verdana', value: 'Verdana, sans-serif' },
];
addSelect([
{ label: 'Schriftart', value: '' },
...fontOptions,
], 'Schriftart', (value) => execRteCommand(null, 'fontName', value, content.ownerDocument));
addSelect([
{ label: 'Groesse', value: '' },
{ label: '10px', value: '1' },
{ label: '12px', value: '2' },
{ label: '14px', value: '3' },
{ label: '16px', value: '4' },
{ label: '18px', value: '5' },
{ label: '24px', value: '6' },
{ label: '32px', value: '7' },
], 'Schriftgroesse', (value) => execRteCommand(null, 'fontSize', value, content.ownerDocument));
const emojiBtn = doc.createElement('button');
emojiBtn.type = 'button';
emojiBtn.textContent = ':-)';
emojiBtn.title = 'Emoticon einfuegen';
emojiBtn.style.padding = '4px 8px';
emojiBtn.style.border = '1px solid #cbd5f5';
emojiBtn.style.borderRadius = '4px';
emojiBtn.style.background = '#f8fafc';
emojiBtn.style.cursor = 'pointer';
emojiBtn.addEventListener('click', () => {
const pick = prompt('Emoticon eingeben', ':)');
if (pick) insertText(pick);
});
toolbar.appendChild(emojiBtn);
container.appendChild(toolbar);
container.appendChild(content);
const actions = doc.createElement('div');
actions.style.display = 'flex';
actions.style.justifyContent = 'flex-end';
actions.style.gap = '8px';
const cancelBtn = doc.createElement('button');
cancelBtn.type = 'button';
cancelBtn.textContent = 'Abbrechen';
cancelBtn.style.padding = '6px 12px';
cancelBtn.style.border = '1px solid #cbd5f5';
cancelBtn.style.borderRadius = '4px';
cancelBtn.style.background = '#f8fafc';
cancelBtn.style.cursor = 'pointer';
cancelBtn.addEventListener('click', () => {
closeModal();
});
const saveBtn = doc.createElement('button');
saveBtn.type = 'button';
saveBtn.textContent = 'Speichern';
saveBtn.style.padding = '6px 12px';
saveBtn.style.border = '1px solid #0ea5e9';
saveBtn.style.borderRadius = '4px';
saveBtn.style.background = '#0ea5e9';
saveBtn.style.color = '#ffffff';
saveBtn.style.cursor = 'pointer';
saveBtn.addEventListener('click', () => {
const html = content.innerHTML;
const isTextLike = component && component.is && (component.is('text') || component.is('button') || component.is('link'));
const hasTags = /<[^>]+>/.test(html);
try {
if (component.components) {
component.components(html);
}
if (component.set) {
component.set('content', html);
component.trigger && component.trigger('change:content');
component.trigger && component.trigger('change:components');
}
if (component.view && component.view.render) {
component.view.render();
}
if (editor && typeof editor.trigger === 'function') {
editor.trigger('component:update', component);
}
} catch {}
closeModal();
});
actions.appendChild(cancelBtn);
actions.appendChild(saveBtn);
container.appendChild(actions);
modal.setTitle('Richtext Editor');
modal.setContent(container);
const mdl = modal.getModel && modal.getModel();
if (mdl && typeof mdl.set === 'function') {
mdl.set('closeOnEsc', false);
mdl.set('closeOnClick', false);
}
if (typeof modal.onceClose === 'function') {
modal.onceClose(() => {
editor.__bridgeRteModalOpen = false;
editor.__bridgeRteAllowClose = false;
});
}
if (mdl && typeof mdl.on === 'function') {
const handler = () => {
if (editor.__bridgeRteAllowClose) {
mdl.off && mdl.off('change:open', handler);
return;
}
if (!mdl.get('open') && !reopenGuard) {
reopenGuard = true;
mdl.set('open', true);
setTimeout(() => { reopenGuard = false; }, 0);
}
};
mdl.on('change:open', handler);
}
try {
modal.open({ closeOnEsc: false, closeOnClick: false });
} catch {
modal.open();
}
const modalEl = modal.getEl ? modal.getEl() : modal.el;
const closeBtn = modalEl && modalEl.querySelector ? modalEl.querySelector('.gjs-mdl-btn-close') : null;
if (closeBtn && !closeBtn.__bridgeRteBound) {
closeBtn.__bridgeRteBound = true;
closeBtn.addEventListener('click', () => {
editor.__bridgeRteAllowClose = true;
}, true);
}
};
const setupRichTextEditor = (editor) => {
if (!editor || !editor.RichTextEditor) return;
const rte = editor.RichTextEditor;
const icon = (path) => `<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="${path}" fill="currentColor"/></svg>`;
const logRteBlur = (label, detail) => {
const msg = `[RTE BLUR] ${label}${detail ? ' | ' + detail : ''}`;
try { console.log(msg); } catch {}
log('RTE BLUR', msg, '#888');
try {
if (window.parent && window.parent !== window) {
window.parent.postMessage({ source: 'bridge-core', type: 'rte-blur', detail: msg }, '*');
}
} catch {}
};
const resolveFontOptions = () => (B.RTE_FONTS && Array.isArray(B.RTE_FONTS) && B.RTE_FONTS.length)
? B.RTE_FONTS
: [
{ label: 'Arial', value: 'Arial, sans-serif' },
{ label: 'Calibri', value: 'Calibri, sans-serif' },
{ label: 'Cambria', value: 'Cambria, serif' },
{ label: 'Georgia', value: 'Georgia, serif' },
{ label: 'Tahoma', value: 'Tahoma, sans-serif' },
{ label: 'Times New Roman', value: 'Times New Roman, serif' },
{ label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
{ label: 'Verdana', value: 'Verdana, sans-serif' },
];
const addAction = (name, icon, title, command, valueGetter) => {
if (rte.get && rte.get(name)) return;
rte.add(name, {
icon,
attributes: { title },
result: (rteInstance) => {
const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter;
if (value === null || value === undefined) return;
if ((command === 'insertText' || command === 'fontName' || command === 'fontSize' || command === 'createLink') && !value) {
return;
}
execRteCommand(rteInstance, command, value);
},
});
};
addAction('bridge-align-left', icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h12v2H4z'), 'Linksbundig', 'justifyLeft');
addAction('bridge-align-center', icon('M5 7h14v2H5zM4 11h16v2H4zM5 15h14v2H5z'), 'Zentriert', 'justifyCenter');
addAction('bridge-align-right', icon('M10 7h10v2H10zM4 11h16v2H4zM8 15h12v2H8z'), 'Rechtsbundig', 'justifyRight');
addAction('bridge-align-justify', icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Blocksatz', 'justifyFull');
addAction('bridge-ul', icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList');
addAction('bridge-ol', icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Liste (geordnet)', 'insertOrderedList');
addAction('bridge-emoji', ':-)', 'Emoticon einfuegen', 'insertText', () => prompt('Emoticon eingeben', ':)'));
addAction('bridge-font-family', 'F', 'Schriftart', 'fontName', () => {
const fonts = resolveFontOptions();
const example = fonts.map((f) => f.label).slice(0, 5).join(', ');
return prompt(`Schriftart (z.B. ${example})`, fonts[0]?.label || 'Arial');
});
addAction('bridge-font-size', 'Px', 'Schriftgroesse', 'fontSize', () => {
const raw = prompt('Schriftgroesse in px (10-32)', '14');
const val = Number(raw || 14);
if (Number.isNaN(val)) return '3';
const map = [
{ px: 10, cmd: '1' },
{ px: 12, cmd: '2' },
{ px: 14, cmd: '3' },
{ px: 16, cmd: '4' },
{ px: 18, cmd: '5' },
{ px: 24, cmd: '6' },
{ px: 32, cmd: '7' },
];
let best = map[0];
map.forEach((entry) => {
if (Math.abs(entry.px - val) < Math.abs(best.px - val)) best = entry;
});
return best.cmd;
});
if (!(rte.get && rte.get('bridge-open-richtext'))) {
rte.add('bridge-open-richtext', {
icon: '<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M4 20h4l10-10-4-4L4 16v4zm14.7-11.3c.4-.4.4-1 0-1.4l-2-2c-.4-.4-1-.4-1.4 0l-1.3 1.3 4 4 1.7-1.9z" fill="currentColor"/></svg>',
attributes: { title: 'Richtext Editor' },
result: () => {
const component = editor.getSelected && editor.getSelected();
openRichTextModal(editor, component);
},
});
}
if (editor.Commands && editor.Commands.add) {
editor.Commands.add('bridge-open-richtext', {
run(ed, sender, opts = {}) {
if (sender && sender.set) sender.set('active', 0);
const component = opts.component || ed.getSelected();
openRichTextModal(ed, component);
},
});
}
normalizeRteActionList(editor, [
'bridge-open-richtext',
'bridge-font-family',
'bridge-font-size',
'bridge-align-left',
'bridge-align-center',
'bridge-align-right',
'bridge-align-justify',
'bridge-ul',
'bridge-ol',
'bridge-emoji',
'bridge-placeholder',
]);
const isTextLike = (model) => !!(model && model.is && (model.is('text') || model.is('button') || model.is('link')));
let lastTextSelection = { id: null, ts: 0 };
let lastTextComponent = null;
editor.on('component:selected', (model) => {
ensureTextToolbarButton(editor, model);
if (!isTextLike(model)) return;
const now = Date.now();
if (lastTextSelection.id === model.cid && (now - lastTextSelection.ts) < 450) {
openRichTextModal(editor, model);
}
lastTextComponent = model;
lastTextSelection = { id: model.cid, ts: now };
});
editor.on('component:deselected', (model) => {
if (isTextLike(model)) {
lastTextComponent = model;
}
});
editor.on('component:add', (model) => ensureTextToolbarButton(editor, model));
editor.on('component:dblclick', (model) => {
if (isTextLike(model)) {
openRichTextModal(editor, model);
}
});
editor.on('canvas:frame:load', () => {
const body = editor.Canvas && editor.Canvas.getBody && editor.Canvas.getBody();
if (!body || body.__bridgeRteDblclickBound) return;
body.__bridgeRteDblclickBound = true;
body.addEventListener('dblclick', () => {
const selected = editor.getSelected && editor.getSelected();
if (isTextLike(selected)) {
openRichTextModal(editor, selected);
}
}, true);
const blurHandler = (evt) => {
const target = evt && evt.target;
if (!target) return;
const isEditable = !!(target.isContentEditable || (target.getAttribute && target.getAttribute('contenteditable') === 'true'));
if (!isEditable) return;
const selected = lastTextComponent || (editor.getSelected && editor.getSelected());
const selectedEl = selected && selected.view && selected.view.el;
if (selected && selectedEl && (selectedEl === target || selectedEl.contains(target))) {
const html = String(target.innerHTML || '').trim();
try {
const content = selected && selected.get ? selected.get('content') : '';
console.warn('[RTE BLUR DEBUG]', {
tag: target.tagName,
htmlLen: html.length,
contentLen: String(content || '').length,
modelType: selected && selected.get ? selected.get('type') : undefined,
modelId: selected && (selected.getId ? selected.getId() : selected.get && selected.get('id')),
});
} catch {}
if (html) {
try {
if (selected.components) {
selected.components(html);
}
if (selected.set) {
selected.set('content', html);
selected.trigger && selected.trigger('change:content');
selected.trigger && selected.trigger('change:components');
}
if (selected.view && selected.view.render) {
selected.view.render();
}
if (editor && typeof editor.trigger === 'function') {
editor.trigger('component:update', selected);
}
try { console.log('[RTE BLUR UPDATE] content aktualisiert.'); } catch {}
} catch {}
}
}
let contentInfo = '';
try {
const content = selected && selected.get ? selected.get('content') : '';
contentInfo = String(content || '').trim() ? 'Content vorhanden' : 'Content leer';
} catch {}
logRteBlur('contenteditable blur', contentInfo);
};
body.addEventListener('blur', blurHandler, true);
body.addEventListener('focusout', blurHandler, true);
});
editor.on('rte:disable', (model) => {
const target = model || (editor.getSelected && editor.getSelected());
if (!isTextLike(target)) return;
const content = target && target.get ? target.get('content') : '';
const msg = String(content || '').trim() ? 'Content vorhanden' : 'Content leer';
logRteBlur('rte:disable fuer Text-Komponente', msg);
});
};
const setupPlainTextPreserver = (editor) => {
const isTextLike = (model) => !!(model && model.is && (model.is('text') || model.is('button') || model.is('link') || model.is('textnode')));
const resolveTextModel = (model) => {
if (!model) return model;
if (model.is && model.is('textnode') && typeof model.parent === 'function') {
return model.parent();
}
return model;
};
const lastContent = new Map();
const normalizeHtml = (value) => {
return String(value || '')
.replace(/<br\s*\/?>/gi, '')
.replace(/&nbsp;/g, '')
.trim();
};
const snapshotContent = (model) => {
const rawContent = model && model.get ? model.get('content') : '';
const el = model.view && model.view.el;
const html = String(rawContent || '').trim();
const inner = el && el.innerHTML ? String(el.innerHTML).trim() : '';
const text = el && el.textContent ? String(el.textContent).trim() : '';
const composite = html || inner || text;
return {
html: composite,
normalized: normalizeHtml(composite),
};
};
const storeSnapshot = (model, snap) => {
if (!model || !snap) return;
const key = model.cid || model;
lastContent.set(key, snap);
model.__bridgeLastHtml = snap.html;
model.__bridgeLastNormalized = snap.normalized;
const el = model.view && model.view.el;
if (el) {
el.__bridgeLastHtml = snap.html;
el.__bridgeLastNormalized = snap.normalized;
}
};
const rememberIfPresent = (model) => {
const target = resolveTextModel(model);
if (!isTextLike(target)) return;
const snap = snapshotContent(target);
if (!snap.normalized) return;
storeSnapshot(target, snap);
};
const restoreIfEmpty = (model) => {
const target = resolveTextModel(model);
if (!isTextLike(target) || !target.get) return;
const current = snapshotContent(target);
if (current.normalized) {
rememberIfPresent(target);
return;
}
const key = target.cid || target;
const stored = lastContent.get(key)
|| (target.__bridgeLastNormalized ? { html: target.__bridgeLastHtml, normalized: target.__bridgeLastNormalized } : null)
|| ((target.view && target.view.el && target.view.el.__bridgeLastNormalized)
? { html: target.view.el.__bridgeLastHtml, normalized: target.view.el.__bridgeLastNormalized }
: null);
if (!stored || !stored.normalized) return;
try {
if (target.components) {
target.components(stored.html);
}
target.set('content', stored.html);
target.trigger && target.trigger('change:content');
try { console.log('[PLAIN TEXT RESTORE] Inhalt wiederhergestellt.'); } catch {}
} catch {}
};
editor.on('component:update', (model) => restoreIfEmpty(model));
editor.on('component:input', (model) => restoreIfEmpty(model));
editor.on('component:deselected', (model) => restoreIfEmpty(model));
editor.on('component:add', (model) => rememberIfPresent(model));
editor.on('rte:disable', (model) => {
const target = model || (editor.getSelected && editor.getSelected());
rememberIfPresent(target);
});
};
const setupTableBuilder = (editor) => {
const domc = editor.DomComponents;
if (!domc) return;
const icon = (path) => `<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="${path}" fill="currentColor"/></svg>`;
const tableIcon = icon('M3 4h18v16H3V4zm2 2v3h6V6H5zm8 0v3h6V6h-6zM5 11v3h6v-3H5zm8 0v3h6v-3h-6zM5 16v2h6v-2H5zm8 0v2h6v-2h-6z');
const ensureTableTags = () => {
const baseType = domc.getType('default');
if (!baseType) return;
const BaseModel = baseType.model;
const BaseView = baseType.view;
const tags = ['table', 'tbody', 'thead', 'tfoot', 'tr', 'td', 'th'];
tags.forEach((tag) => {
if (domc.getType(tag)) return;
domc.addType(tag, {
model: BaseModel,
view: BaseView,
isComponent: el => {
if (el.tagName && el.tagName.toLowerCase() === tag) {
return { type: tag };
}
return '';
},
});
});
};
const collectTableCells = (component) => {
const el = component?.view?.el;
if (!el) return [];
return Array.from(el.querySelectorAll('tr')).map(row =>
Array.from(row.children || []).map(cell => cell.innerHTML || '')
);
};
const buildTableHtml = (rows, cols, existing) => {
const safeRows = Math.max(1, Math.min(20, Number(rows) || 1));
const safeCols = Math.max(2, Math.min(2, Number(cols) || 2));
const cellStyle = "padding:8px;border:1px solid #e2e8f0;font-size:13px";
const headStyle = "text-align:left;padding:8px;border:1px solid #e2e8f0;background-color:#f8fafc;font-size:13px";
let html = '';
for (let r = 0; r < safeRows; r++) {
html += '<tr>';
for (let c = 0; c < safeCols; c++) {
const existingVal = existing?.[r]?.[c] || '';
const label = existingVal || (r === 0 ? `Spalte ${String.fromCharCode(65 + c)}` : `Zeile ${r} / ${c + 1}`);
if (r === 0) {
html += `<th style="${headStyle}">${label}</th>`;
} else {
html += `<td style="${cellStyle}">${label}</td>`;
}
}
html += '</tr>';
}
return html;
};
const openTableModal = (component) => {
if (!component) return;
const modal = editor.Modal;
if (!modal) return;
const attrs = component.getAttributes ? component.getAttributes() : {};
const rows = Number(attrs['data-bridge-rows'] || 3) || 3;
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '12px';
container.style.minWidth = '280px';
const label = document.createElement('label');
label.textContent = 'Anzahl Zeilen';
label.style.fontSize = '13px';
label.style.fontWeight = '600';
const input = document.createElement('input');
input.type = 'number';
input.min = '1';
input.max = '20';
input.value = String(rows);
input.style.width = '100%';
input.style.padding = '6px 8px';
input.style.border = '1px solid #cbd5f5';
input.style.borderRadius = '4px';
label.appendChild(input);
container.appendChild(label);
const actions = document.createElement('div');
actions.style.display = 'flex';
actions.style.justifyContent = 'flex-end';
actions.style.gap = '8px';
const cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.textContent = 'Abbrechen';
cancelBtn.className = 'btn';
cancelBtn.addEventListener('click', () => modal.close());
const saveBtn = document.createElement('button');
saveBtn.type = 'button';
saveBtn.textContent = 'Uebernehmen';
saveBtn.className = 'btn';
saveBtn.addEventListener('click', () => {
const nextRows = Math.max(1, Math.min(20, Number(input.value) || 1));
const existing = collectTableCells(component);
const html = buildTableHtml(nextRows, 2, existing);
component.addAttributes && component.addAttributes({
'data-bridge-rows': String(nextRows),
'data-bridge-cols': '2',
});
if (component.components) {
component.components(html);
}
if (component.view && component.view.render) {
component.view.render();
}
modal.close();
});
actions.appendChild(cancelBtn);
actions.appendChild(saveBtn);
container.appendChild(actions);
modal.setTitle('Tabelle konfigurieren');
modal.setContent(container);
const mdl = modal.getModel && modal.getModel();
if (mdl && typeof mdl.set === 'function') {
mdl.set('closeOnEsc', false);
mdl.set('closeOnClick', false);
}
modal.open();
};
if (editor.Commands && editor.Commands.add) {
editor.Commands.add('bridge-table:edit', {
run(ed, sender, opts = {}) {
if (sender && sender.set) sender.set('active', 0);
const component = opts.component || ed.getSelected();
if (component && component.is && component.is('bridge-table')) {
openTableModal(component);
}
},
});
}
if (!domc.getType('bridge-table')) {
const baseType = domc.getType('table') || domc.getType('default');
const BaseModel = baseType.model;
const BaseView = baseType.view;
domc.addType('bridge-table', {
model: BaseModel.extend({
initialize(props = {}, opts = {}) {
if (BaseModel.prototype.initialize) {
BaseModel.prototype.initialize.apply(this, [props, opts]);
}
const attrs = this.getAttributes ? this.getAttributes() : {};
const nextAttrs = Object.assign({
'data-bridge-table': '1',
'data-bridge-rows': '3',
'data-bridge-cols': '2',
role: 'presentation',
}, attrs || {});
if (this.addAttributes) {
this.addAttributes(nextAttrs);
} else if (this.set) {
this.set('attributes', nextAttrs, { silent: true });
}
const toolbar = this.get && this.get('toolbar');
if (!Array.isArray(toolbar) || !toolbar.some(btn => btn && btn.command === 'bridge-table:edit')) {
this.set && this.set('toolbar', [
...(Array.isArray(toolbar) ? toolbar : []),
{
label: tableIcon,
command: 'bridge-table:edit',
attributes: { title: 'Tabelle bearbeiten' },
},
]);
}
},
}),
view: BaseView,
isComponent: el => {
if (el.tagName === 'TABLE' && el.getAttribute('data-bridge-table')) {
return { type: 'bridge-table' };
}
return '';
},
});
}
editor.on('component:selected', (model) => {
if (model && model.is && model.is('bridge-table')) {
const toolbar = model.get('toolbar') || [];
const exists = toolbar.some(btn => btn && btn.command === 'bridge-table:edit');
if (!exists) {
toolbar.push({
label: tableIcon,
command: 'bridge-table:edit',
attributes: { title: 'Tabelle bearbeiten' },
});
model.set('toolbar', toolbar);
}
}
});
editor.on('component:dblclick', (model) => {
if (model && model.is && model.is('bridge-table')) {
openTableModal(model);
}
});
ensureTableTags();
};
const loadDynamicFonts = async () => {
try {
const base = B.API_KERNEL_URL || '/api.php';
const sep = base.includes('?') ? '&' : '?';
const res = await fetch(`${base}${sep}action=account.fonts.list`, { credentials: 'include' });
if (!res.ok) return;
const data = await res.json();
if (!data || !data.ok) return;
const incoming = Array.isArray(data.fonts) ? data.fonts : [];
if (incoming.length) {
const merged = [];
const seen = new Set();
const addFont = (item) => {
if (!item || !item.label || !item.value) return;
const key = String(item.label).toLowerCase();
if (seen.has(key)) return;
seen.add(key);
merged.push({ label: String(item.label), value: String(item.value) });
};
(B.RTE_FONTS || []).forEach(addFont);
incoming.forEach(addFont);
B.RTE_FONTS = merged;
}
if (typeof data.font_face_css === 'string' && data.font_face_css.trim()) {
const existing = typeof B.RTE_FONT_FACE_CSS === 'string' ? B.RTE_FONT_FACE_CSS.trim() : '';
B.RTE_FONT_FACE_CSS = existing
? `${existing}\n${data.font_face_css.trim()}`
: data.font_face_css.trim();
const styleId = 'bridge-font-faces';
let styleEl = document.getElementById(styleId);
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = styleId;
document.head.appendChild(styleEl);
}
styleEl.textContent = B.RTE_FONT_FACE_CSS;
}
} catch {}
};
var ed = grapesjs.init({
container: '#gjs',
height: '100vh',
noticeOnUnload: 0,
// 🛑 KRITISCHE KORREKTUR: storageManager aktivieren und konfigurieren
storageManager: storageConf,
plugins: pluginsList,
pluginsOpts: {},
// 🛑 KRITISCHE ERGÄNZUNG: Verhindert das automatische Ausblenden leerer Kategorien
blockManager: {
hideEmpty: false
},
parser: {
textTags: ['p','span','div','br','b','strong','i','em','u','a','ul','ol','li'],
optionsHtml: {
keepEmptyTextNodes: true
}
},
domComponents: {
// Preserve plain text when editing text blocks.
textTags: ['p','span','div','br','b','strong','i','em','u','a','ul','ol','li']
}
});
const ensureCommandStubs = (editor) => {
if (!editor || !editor.Commands || !editor.Commands.add) return;
editor.Commands.add('bridge-placeholder:edit', {
__bridgePlaceholderStub: true,
run(ed, sender, opts = {}) {
if (sender && sender.set) sender.set('active', 0);
const component = opts.component || (ed.getSelected && ed.getSelected());
if (window.BridgeBlocksPlaceholder && typeof window.BridgeBlocksPlaceholder.openModal === 'function') {
window.BridgeBlocksPlaceholder.openModal(ed, component);
}
},
});
editor.Commands.add('bridge-open-richtext', {
__bridgeRteStub: true,
run(ed, sender, opts = {}) {
if (sender && sender.set) sender.set('active', 0);
const component = opts.component || ed.getSelected();
openRichTextModal(ed, component);
},
});
editor.Commands.add('bridge-table:edit', {
__bridgeTableStub: true,
run(ed, sender, opts = {}) {
if (sender && sender.set) sender.set('active', 0);
},
});
};
ensureCommandStubs(ed);
try {
const textTags = ['p','span','div','br','b','strong','i','em','u','a','ul','ol','li'];
if (ed.Config) {
ed.Config.domComponents = ed.Config.domComponents || {};
ed.Config.domComponents.textTags = textTags;
}
if (ed.DomComponents && ed.DomComponents.getConfig) {
ed.DomComponents.getConfig().textTags = textTags;
}
if (ed.Parser && ed.Parser.getConfig) {
const parserCfg = ed.Parser.getConfig();
parserCfg.textTags = textTags;
parserCfg.optionsHtml = parserCfg.optionsHtml || {};
parserCfg.optionsHtml.textTags = textTags;
parserCfg.optionsHtml.keepEmptyTextNodes = true;
}
} catch (e) {
log('CORE WARN', `textTags Konfiguration fehlgeschlagen: ${e.message}`, 'orange', 'warn');
}
setupRichTextEditor(ed);
setupTableBuilder(ed);
setupPlainTextPreserver(ed);
loadDynamicFonts();
// Entfernt: jegliche Blur/RTE-Handler, die Inhalte verändern.
// Keep the fix off live updates to avoid cursor jumps; run only on RTE close.
        
        window.__gjs = ed;
        
        // 🛑 KRITISCHE KORREKTUR 1: Explizite Erstellung aller konfigurierten Kategorien
        ensureConfiguredCategories(ed); 
        // 🛑 KRITISCHE KORREKTUR 2: Sofortige Label-Korrektur
        // Überschreibt den potenziell falschen, durch GrapesJS gesetzten Label-Namen
        Object.keys(B.CATEGORY_CONFIG || {}).forEach(catId => {
            const expectedLabel = B.CATEGORY_CONFIG[catId].label;
            const categoryModel = ed.BlockManager.getCategories().get(catId);
            
            if (categoryModel && categoryModel.get('label') !== expectedLabel) {
                // Setzen ohne das 'change:label' Event auszulösen (optional, aber sauber)
                categoryModel.set('label', expectedLabel, { silent: true }); 
                log('LABEL FIX', `Kategorie '${catId}' Label auf korrigiert: '${expectedLabel}'`, 'yellow', 'warn');
            }
        });
        // ---------------------------------------------------
        B.ensureViews && B.ensureViews(ed);
        
        log('BLOCK REGISTER', 'Registriere Bridge Blöcke, um Preset-Defaults zu überschreiben.', 'purple');
        // 🛑 DYNAMISCHE AKTIVIERUNG DER SYNCHRONEN BLÖCKE (Ersetzt die fixen Aufrufe)
if (B.CATEGORY_CONFIG && ed) {
log('DYNAMIC ACTIVATION', 'Starte Aktivierung synchroner Block-Plugins (via Config).', 'purple');
// Iteriere über die konfigurierten Kategorien
Object.keys(B.CATEGORY_CONFIG).forEach(catId => {
const config = B.CATEGORY_CONFIG[catId];
// Verarbeite nur SYNCHRONE Plugins, die Dateien angeben
if ((config.registration_mode || 'sync') === 'sync' && Array.isArray(config.files)) {
config.files.forEach(fileName => {
// Korrigierte Funktion liefert jetzt z.B. 'BridgeBlocksCustom'
const objectName = getPluginObjectName(fileName);
                        const plugin = window[objectName];
                        
                        // Prüfen, ob das Skript geladen wurde und die Register-Funktion vorhanden ist
                        if (plugin && typeof plugin.register === 'function') {
                            log('DYNAMIC ACTIVATION', `Registriere sync Plugin: ${objectName} (${fileName})`, 'lime');
try {
plugin.register(ed);
if (objectName === 'BridgeBlocksPlaceholder') {
ed.__bridgePlaceholderActive = true;
}
} catch(e) {
log('DYNAMIC ACTIVATION ERROR', `Fehler beim Registrieren von ${objectName}: ${e.message}`, 'red', 'error');
}
                        } else {
                            log('DYNAMIC ACTIVATION WARNING', `Sync Plugin Objekt oder .register() Methode nicht gefunden: ${objectName} (${fileName})`, 'orange', 'warn');
                        }
});
}
});
}
const ensurePlaceholderRte = () => {
if (ed.__bridgePlaceholderActive) {
return true;
}
if (window.BridgeBlocksPlaceholder && typeof window.BridgeBlocksPlaceholder.register === 'function') {
try {
window.BridgeBlocksPlaceholder.register(ed);
ed.__bridgePlaceholderActive = true;
log('PLACEHOLDER SYNC', 'Placeholder-RTE aktiv.', 'lime');
} catch (err) {
log('PLACEHOLDER ERROR', `Placeholder-RTE Fehler: ${err.message}`, 'red', 'error');
}
return true;
}
return false;
};
if (!B.DISABLE_PLACEHOLDERS) {
if (!ensurePlaceholderRte()) {
const fallbackSrc = B.BASE_PATH_BRIDGE + 'blocks-placeholder.js';
log('PLACEHOLDER LOAD', 'Placeholder-RTE wird geladen.', '#B45309', 'warn');
loadScript(fallbackSrc, () => {
if (!ensurePlaceholderRte()) {
log('PLACEHOLDER ERROR', 'Placeholder-RTE konnte nicht initialisiert werden.', 'red', 'error');
}
});
}
} else {
log('PLACEHOLDER SKIP', 'Placeholder-RTE deaktiviert (Test).', '#B45309', 'warn');
}
// ---------------------------------------------------
log('INIT API', 'API-Elemente werden nun durch das Plugin bridge-blocks-api geladen. (ASYNCHRON)', 'orange');
        // ----------------------------------------------------------------------
        // DEBUGGING: ZÄHLE REKURSIVE EVENTS
        // ----------------------------------------------------------------------
        let eventCounts = {};
        let isParsing = false; 
        const MAX_CALLS = 1000; 
        const debugEvents = [
            'component:add', 
            'component:update', 
            'change:components', 
            'block:add',
            'change:attributes',
            'comp:update:status'
        ];
        const debugListener = (event, model) => {
            if (!isParsing) return;
            if (!eventCounts[event]) {
                eventCounts[event] = 0;
            }
            eventCounts[event]++;
            if (eventCounts[event] === MAX_CALLS + 1) { 
                // Diese kritischen Debug-Meldungen bleiben DIREKT im console-Objekt, 
                // da sie immer sichtbar sein müssen, um Endlosschleifen zu erkennen.
                console.error(`%c[DEBUG RECURSION ALARM] 🚨 Event '${event}' hat den Grenzwert von ${MAX_CALLS} überschritten!`, 'color:red; font-size: 1.1em; font-weight: bold;');
            }
            
            if (eventCounts[event] > MAX_CALLS && eventCounts[event] < (MAX_CALLS + 10)) { 
                 const type = (model && typeof model.get === 'function') ? model.get('type') : 'N/A';
                 const parentType = (model && typeof model.parent === 'function' && model.parent()) ? model.parent().get('type') : 'N/A';
                 // Diese bleiben console.log aus demselben Grund
                 console.log(`%c [RECURSION SOURCE] Event: ${event}, Type: ${type}, Parent: ${parentType}`, 'color: #8b0000;');
            }
        };
        ed.on('load', function() {
            debugEvents.forEach(event => ed.on(event, (model) => debugListener(event, model)));
            
            setTimeout(() => {
                (B.waitForBlocks ? B.waitForBlocks(ed) : Promise.resolve()).then(function(){
                    try {
                        log('CORE WARN', 'Führe finalen, verzögerten Cleanup-Lauf durch (2000ms).', 'orange', 'warn');
                        
                        B.normalizeCategories && B.normalizeCategories(ed); 
                        B.renderBlocks && B.renderBlocks(ed); 
                    } catch(e) {
                        log('CORE ERROR', `Finaler Cleanup-Fehler: ${e.message}`, 'red', 'error');
                    }
                });
            }, 2000); 
        }, { once: true });
        // ----------------------------------------------------------------------
        // MESSAGE HANDLER
        // ----------------------------------------------------------------------
        window.addEventListener('message', async function(ev){ 
            var data = ev.data || {};
            if (data.source !== 'admin') return;
if (data.type === 'init'){
B.ensureViews && B.ensureViews(ed);
ed.__contentLoaded = false;
try {
ed.setComponents('');
ed.setStyle('');
} catch {}
var html = (data.html || '').trim();
var hasJson = !!data.hasJson;
                if (!html) html = '<table style="width:100%;font-family:Arial,sans-serif"><tr><td><h1>Neues Dokument</h1><p>Inhalt ...</p></td></tr></table>';
                
                const applySnips = function(arr){
                    const list = (Array.isArray(arr)?arr:[]).map(s => ({ id:s.id, name:s.name, html: s.html || s.content || '' }));
                    
                    B.replaceSnippetBlocks && B.replaceSnippetBlocks(ed, list); 
                    
                    upsertCustomForBothCats(ed, {
                        ref: (data.ref && (Array.isArray(data.ref.sections) || Array.isArray(data.ref.blocks))) ? {
                            sections: data.ref.sections || [],
                            blocks:      data.ref.blocks        || []
                        } : { sections: [], blocks: [] },
                        snippets: list
                    });
                    
                    setTimeout(() => {
                        try {
                            // Erneutes Normalisieren nach Laden der Snippets (falls nötig)
                            B.normalizeCategories && B.normalizeCategories(ed); 
                            B.ensureViews && B.ensureViews(ed);
                            B.renderBlocks && B.renderBlocks(ed);
                            log('CORE WARN', 'normalize/render nach applySnips ausgeführt (1ms).', 'orange', 'warn'); 
                        } catch(e) {
                            log('CORE ERROR', `applySnips-Cleanup-Fehler: ${e.message}`, 'red', 'error');
                        }
                    }, 1); 
                };
                if (Array.isArray(data.snippets) && data.snippets.length) applySnips(data.snippets);
                else (B.fetchSnippets ? B.fetchSnippets() : Promise.resolve([])).then(applySnips);
                if (data.ref && (Array.isArray(data.ref.sections) || Array.isArray(data.ref.blocks))) {
                    replaceReferenceLibrary(ed, {
                        sections: data.ref.sections || [],
                        blocks:      data.ref.blocks        || []
                    }, MODE);
                }
                // Finaler Aufruf nachrichtengesteuert (konsolidiert)
                setTimeout(() => {
                    (B.waitForBlocks ? B.waitForBlocks(ed) : Promise.resolve()).then(function(){
                        try {
                            log('CORE WARN', 'Führe nachrichtengesteuerten Final-Cleanup-Lauf durch (100ms).', 'orange', 'warn'); 
                            
if (!ed.__contentLoaded) {
log('CONTENT', 'Verarbeite initiale Editor-Daten (postMessage).', 'orange');
const jsonPayload = typeof data.json === 'string' ? data.json : '';
let applied = false;
if (jsonPayload) {
window.__GJS_IS_PARSING = true;
isParsing = true;
eventCounts = {};
try {
const parsedState = JSON.parse(jsonPayload);
ed.loadProjectData(parsedState);
applied = true;
log('CONTENT', 'JSON-Projektzustand angewendet.', 'orange');
const hasComponents = ed.getComponents && ed.getComponents().length > 0;
const hasHtml = (ed.getHtml && ed.getHtml().trim()) ? true : false;
if (!hasComponents && !hasHtml && html) {
log('CONTENT WARN', 'JSON geladen aber leer, wechsle auf HTML-Fallback.', 'orange', 'warn');
applied = false;
}
} catch (e) {
log('CONTENT ERROR', `JSON loadProjectData Fehler: ${e.message}`, 'red', 'error');
if (!html) {
html = jsonPayload;
}
} finally {
window.__GJS_IS_PARSING = false;
isParsing = false;
}
}
if (!applied && html) {
window.__GJS_IS_PARSING = true; 
isParsing = true;
eventCounts = {};
try {
ed.setComponents(html); 
                                    } catch (e) {
                                        log('SET COMPONENTS FAILED', `setComponents Fehler: ${e.message}. Aufgerufene Event-Zähler: ${JSON.stringify(eventCounts)}`, 'red', 'error');
                                        throw e; 
                                    } finally {
                                        window.__GJS_IS_PARSING = false;
                                        isParsing = false;
                                        log('CONTENT', 'HTML-Inhalt in den Editor geladen (FALLBACK).', 'orange');
                                    }
                                }
ed.__contentLoaded = true;
}
                            
                            // Normalisierung am Ende
                            B.normalizeCategories && B.normalizeCategories(ed); 
                            B.renderBlocks && B.renderBlocks(ed);
                            
                        } catch(e) {
                            log('CORE ERROR', `Nachrichten-Final-Cleanup-Fehler: ${e.message}. Event-Zähler (im Log-Objekt): ${JSON.stringify(eventCounts)}`, 'red', 'error');
                        }
                    });
                }, 100); 
                try { var b=ed.Panels.getButton('views','open-blocks'); b && b.set('active',true); } catch {}
                badgeSay('Inhalt geladen','ok');
                setTimeout(function(){ badgeSay('bereit'); }, 1200);
            }
        }, false);
        
        try { B.send && B.send('core-ready', { mode: MODE }); } catch {}
        try { var bd=document.getElementById('badge'); if (bd) bd.remove(); } catch {}
    });
    
    window.onerror = function(message, source, lineno, colno, error) { 
        // Diese kritische Funktion MUSS console.error verwenden.
        console.error(`%c[${PluginName} - GLOBAL ERROR] Uncaught JS Error: ${message} (Quelle: ${source}:${lineno})`, 'color:red; font-weight:bold;');
        return false; 
    };
    
})();