1236 lines
60 KiB
JavaScript
1236 lines
60 KiB
JavaScript
/* /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, '<').replace(/>/g, '>'));
|
||
}
|
||
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;
|
||
|
||
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, '<').replace(/>/g, '>'));
|
||
}
|
||
};
|
||
|
||
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', () => {
|
||
editor.__bridgeRteModalOpen = false;
|
||
modal.close();
|
||
});
|
||
|
||
const saveBtn = doc.createElement('button');
|
||
saveBtn.type = 'button';
|
||
saveBtn.textContent = 'Uebernehmen';
|
||
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;
|
||
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 {}
|
||
editor.__bridgeRteModalOpen = false;
|
||
modal.close();
|
||
});
|
||
|
||
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;
|
||
});
|
||
}
|
||
modal.open();
|
||
};
|
||
|
||
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 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.get('bridge-open-richtext')) {
|
||
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 };
|
||
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);
|
||
}
|
||
lastTextSelection = { id: model.cid, ts: now };
|
||
});
|
||
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 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 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.get('bridge-table:edit')) {
|
||
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);
|
||
}
|
||
});
|
||
};
|
||
|
||
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']
|
||
}
|
||
});
|
||
|
||
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);
|
||
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 (!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');
|
||
}
|
||
});
|
||
}
|
||
// ---------------------------------------------------
|
||
|
||
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');
|
||
} 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;
|
||
};
|
||
|
||
})();
|