Files
emailtemplate.it/public/editor/bridge-core.js
2025-12-06 02:50:45 +01:00

636 lines
34 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.STORAGE_URL_BASE = B.STORAGE_URL_BASE || B.API_BASE;
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"')) {
                    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');
        }
        
        // Speicherkonfiguration extrahieren, um die URL in onLoad zu verwenden.
        // 🎯 KORREKTUR für mehr Flexibilität: Verwende B.STORAGE_URL_BASE, falls gesetzt, anstatt window.location.href.
        // Verwenden Sie B.API_BASE (Standard /api/editor) als Fallback für die Storage-URL
        const storageBase = B.STORAGE_URL_BASE || B.API_BASE; // B.API_BASE sollte jetzt korrekt sein
        // Robustes Anhängen von Query-Parametern.
        // Prüft, ob 'storageBase' bereits Query-Parameter enthält ('?')
        const actionSeparator = storageBase.indexOf('?') === -1 ? '?' : '&';
const resourceName = (window.__editorMode || 'templates');
const entityId = (window.__editorId || 0);
const loadUrl = `${storageBase}${actionSeparator}action=${resourceName}.get&id=${entityId}`;
const storeUrl = `${storageBase}${actionSeparator}action=${resourceName}.update&id=${entityId}`;
        const storageConf = {
            type: 'remote',
            // urlLoad: loadUrl, // ENTFERNT (korrekt, da customFetch verwendet wird)
            urlStore: storeUrl,
            
            // 🛑 KRITISCHER ABSCHNITT: customFetch MUSS DIE ERWARTETE SIGNATUR HABEN: customFetch(url, options)
            customFetch: async (url, options) => { // <<< KORREKTUR DER SIGNATUR
                // 1. Log Start
                log('STORAGE START', 'Template wird geladen.', '#008080', 'info', true); 
                // 2. Log Link
                log('API REQUEST', `Link für den API Request: ${loadUrl}`, '#4682B4', 'log', false); 
const fetchOptions = {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
// Wichtig: Die übergebenen Optionen nicht vergessen zu mergen
...options
};
fetchOptions.headers = {
'Content-Type': 'application/json',
...(options?.headers || {}),
};
if (!fetchOptions.credentials || fetchOptions.credentials === 'omit') {
fetchOptions.credentials = 'include';
}
                
                let data = {};
                let rawResponse = '';
                try {
                    // Verwendung der intern definierten loadUrl
                    const response = await fetch(loadUrl, fetchOptions); 
                    
                    if (!response.ok) {
                        const errorText = await response.text();
                        throw new Error(`HTTP-Fehler ${response.status}: ${errorText}`);
                    }
                    // Holen des Raw Texts, um ihn loggen und parsen zu können
                    rawResponse = await response.text();
                    
                    // 3. Log Result
                    log('API RESPONSE', 'Result vom API Request (Raw Text/JSON):', '#4682B4', 'log', false);
                    console.log(rawResponse); // Loggt den reinen String für die Analyse
                    
                    // Versuch der JSON-Analyse (um den GrapesJS-Fehler zu vermeiden)
                    try {
                        data = JSON.parse(rawResponse);
                        log('STORAGE PARSE', 'Raw Response als JSON geparst.', 'green');
                    } catch (e) {
                        log('STORAGE PARSE ERROR', `Fehler beim Parsen der Antwort: ${e.message}. Antwort war wahrscheinlich kein gültiges JSON.`, 'red', 'error', true);
                        // Im Falle eines Parsing-Fehlers, leeres Objekt für Fallback-Logik
                        data = {};
                    }
                } catch (e) {
                    log('STORAGE FETCH ERROR', `Fehler beim Abruf: ${e.message}`, 'red', 'error', true);
                    // Sicherstellen, dass die Promise mit einem leeren Zustand erfüllt wird
                    // Wir müssen dennoch den Log End ausführen, bevor wir zurückkehren
                }
                
                // 4. Log End
                log('STORAGE END', 'Template wurde geladen.', '#008080', 'info', true); 
                
                // --- Logik zur Extraktion des GrapesJS States aus der API-Antwort ---
                let state = {};
                if (data && data.gjs_data) {
                    log('STORAGE LOAD', 'Voller GrapesJS State aus "gjs_data" geladen.', 'green');
                    state = data.gjs_data; 
                }
                else if (data && data.content) {
                    try {
                        const parsedState = JSON.parse(data.content);
                        log('STORAGE LOAD', 'Voller GrapesJS State aus "content" (JSON-String) geladen.', 'yellow');
                        state = parsedState;
                    } catch (e) {
                        log('STORAGE ERROR', `Fehler beim Parsen von "content": ${e.message}.`, 'red', 'error');
                    }
                }
                // HINWEIS: Füge Fallback für "topContent" hinzu, basierend auf dem Server-Log
                else if (data && data.topContent) {
                    try {
                        const parsedState = JSON.parse(data.topContent);
                        log('STORAGE LOAD', 'Voller GrapesJS State aus "topContent" (JSON-String) geladen.', 'green');
                        state = parsedState;
                    } catch (e) {
                        log('STORAGE ERROR', `Fehler beim Parsen von "topContent": ${e.message}.`, 'red', 'error');
                    }
                }
                else {
                    log('STORAGE WARNING', 'Kein vollständiger GrapesJS State gefunden. Editor lädt leeren State.', 'orange', 'warn');
                }
                // customFetch MUSS den geladenen State zurückgeben
                return state;
            },
            // --- ENDE customFetch ---
            // onLoad ist bei customFetch nicht mehr nötig
            // onLoad: (response) => { ... }, 
            
            // KRITISCH: Speichert den vollen State als JSON-String im Feld 'json_content'.
            onStore: (data) => {
                // ACHTUNG: ed existiert hier nicht, muss über window.__gjs geladen werden ODER ed als Argument akzeptiert werden
                const ed = window.__gjs;
                return {
                    json_content: JSON.stringify(data), 
                    html: ed ? ed.getHtml() : '' // Fügen Sie den HTML-Output zur Abwärtskompatibilität hinzu
                };
            },
        };
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 
            }
        });
        
        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' && 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);
                            } 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');
                        }
                    });
                }
            });
        }
        // ---------------------------------------------------
        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);
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'); 
                            
// 🛑 KRITISCHE KORREKTUR: Entferne das erzwungene ed.setComponents(html)
// Das Laden des Inhalts wird jetzt vom storageManager übernommen (via customFetch).
if (!ed.__contentLoaded) {
log('CONTENT', 'Erster Ladevorgang (storageManager) ist abgeschlossen.', 'orange');
// HINWEIS: Wenn der Editor initial leer lädt (z.B. neue Vorlage),
// MUSS hier der initiale HTML-Code eingefügt werden.
// Da der storageManager aber automatisch lädt,
// sollte dieser Block nur für den Initialfall "Neu" greifen.
if (!hasJson && html && !ed.getComponents().length) {
                                    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; 
    };
    
})();