/* /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 = B.DISABLE_EDITOR_EXTENSIONS
? [base + 'general-functions.js']
: [...coreFiles];
function recursiveLoader(list, index = 0) {
if (index >= list.length) {
log('LOAD END', 'Initial-Bridge-Skripte geladen.', 'green');
if (B.DISABLE_EDITOR_EXTENSIONS) {
return cb && cb(B);
}
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,
];
if (!B.DISABLE_EDITOR_EXTENSIONS) {
pluginsList.push(
// 🛑 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, '>'));
}
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: '',
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, '>'));
}
};
const icon = (path) => ``;
addButton('B', 'Fett', 'bold');
addButton('I', 'Kursiv', 'italic');
addButton('U', 'Unterstrichen', 'underline');
addButton('S', 'Durchgestrichen', 'strikethrough');
addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList');
addButton(icon('M4 7h14v2H4zM4 11h14v2H4zM4 15h14v2H4z') + '1.', '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) => ``;
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: '',
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 {}
}
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(/
/gi, '')
.replace(/ /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));
};
const setupTableBuilder = (editor) => {
const domc = editor.DomComponents;
if (!domc) return;
const icon = (path) => ``;
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 += '
Neues DokumentInhalt ... |