This commit is contained in:
2026-01-14 01:50:50 +01:00
parent 62da2a67af
commit d49c326c90
9 changed files with 343 additions and 37 deletions

View File

@@ -263,7 +263,7 @@
autosave: false,
};
const execRteCommand = (rte, cmd, value) => {
const execRteCommand = (rte, cmd, value, docOverride) => {
try {
if (rte && typeof rte.exec === 'function') {
rte.exec(cmd, value);
@@ -271,9 +271,16 @@
}
} catch {}
try {
const ok = document.execCommand(cmd, false, value);
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') {
document.execCommand('insertHTML', false, String(value || '').replace(/</g, '&lt;').replace(/>/g, '&gt;'));
doc.execCommand('insertHTML', false, String(value || '').replace(/</g, '&lt;').replace(/>/g, '&gt;'));
}
return true;
} catch {}
@@ -344,10 +351,10 @@
|| '';
content.innerHTML = initialHtml;
const addButton = (label, title, cmd, valueGetter) => {
const addButton = (labelHtml, title, cmd, valueGetter) => {
const btn = doc.createElement('button');
btn.type = 'button';
btn.textContent = label;
btn.innerHTML = labelHtml;
btn.title = title;
btn.style.padding = '4px 8px';
btn.style.border = '1px solid #cbd5f5';
@@ -359,7 +366,7 @@
const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter;
if (value === null || value === undefined) return;
if (cmd === 'createLink' && !value) return;
execRteCommand(null, cmd, value);
execRteCommand(null, cmd, value, content.ownerDocument);
});
toolbar.appendChild(btn);
};
@@ -393,32 +400,41 @@
}
};
addButton('B', 'Fett', 'bold');
addButton('I', 'Kursiv', 'italic');
addButton('U', 'Unterstrichen', 'underline');
addButton('S', 'Durchgestrichen', 'strikethrough');
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('UL', 'Liste (ungeordnet)', 'insertUnorderedList');
addButton('OL', 'Liste (geordnet)', 'insertOrderedList');
addButton('L', 'Linksbundig', 'justifyLeft');
addButton('C', 'Zentriert', 'justifyCenter');
addButton('R', 'Rechtsbundig', 'justifyRight');
addButton('J', 'Blocksatz', 'justifyFull');
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: '' },
{ label: 'Arial', value: 'Arial, sans-serif' },
{ label: 'Georgia', value: 'Georgia, serif' },
{ label: 'Tahoma', value: 'Tahoma, sans-serif' },
{ label: 'Times New Roman', value: 'Times New Roman, serif' },
{ label: 'Verdana', value: 'Verdana, sans-serif' },
], 'Schriftart', (value) => execRteCommand(null, 'fontName', value));
...fontOptions,
], 'Schriftart', (value) => execRteCommand(null, 'fontName', value, content.ownerDocument));
addSelect([
{ label: 'Groesse', value: '' },
@@ -429,7 +445,7 @@
{ label: '18px', value: '5' },
{ label: '24px', value: '6' },
{ label: '32px', value: '7' },
], 'Schriftgroesse', (value) => execRteCommand(null, 'fontSize', value));
], 'Schriftgroesse', (value) => execRteCommand(null, 'fontSize', value, content.ownerDocument));
const emojiBtn = doc.createElement('button');
emojiBtn.type = 'button';
@@ -498,6 +514,19 @@
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;
@@ -515,14 +544,18 @@
});
};
addAction('bridge-align-left', 'L', 'Linksbundig', 'justifyLeft');
addAction('bridge-align-center', 'C', 'Zentriert', 'justifyCenter');
addAction('bridge-align-right', 'R', 'Rechtsbundig', 'justifyRight');
addAction('bridge-align-justify', 'J', 'Blocksatz', 'justifyFull');
addAction('bridge-ul', 'UL', 'Liste (ungeordnet)', 'insertUnorderedList');
addAction('bridge-ol', 'OL', 'Liste (geordnet)', 'insertOrderedList');
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', () => prompt('Schriftart (z.B. Arial, Georgia)', 'Arial'));
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);
@@ -542,11 +575,16 @@
});
return best.cmd;
});
addAction('bridge-open-richtext', 'RTE', 'Richtext Editor', 'execCommand', () => {
const component = editor.getSelected && editor.getSelected();
openRichTextModal(editor, component);
return null;
});
if (!(rte.get && rte.get('bridge-open-richtext'))) {
rte.add('bridge-open-richtext', {
icon: 'RTE',
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', {
@@ -576,6 +614,46 @@
editor.on('component:add', (model) => ensureTextToolbarButton(editor, 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',
@@ -623,6 +701,7 @@
}
setupRichTextEditor(ed);
loadDynamicFonts();
// Entfernt: jegliche Blur/RTE-Handler, die Inhalte verändern.

View File

@@ -2,6 +2,27 @@
$mode = strtolower($_GET['mode'] ?? 'templates');
$id = (int)($_GET['id'] ?? 0);
$assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$appBaseUrl = rtrim($GLOBALS['app_base_url'] ?? ($scheme . '://' . $host), '/');
$fontDir = __DIR__ . '/../assets/fonts';
$fontBase = $appBaseUrl . '/assets/fonts';
$customFontName = 'Kids Handwriting';
$customFontFiles = [
'woff2' => 'KidsHandwriting-Regular.woff2',
'woff' => 'KidsHandwriting-Regular.woff',
'ttf' => 'KidsHandwriting-Regular.ttf',
];
$fontSources = [];
foreach ($customFontFiles as $format => $file) {
if (is_file($fontDir . '/' . $file)) {
$fontSources[] = "url('" . htmlspecialchars($fontBase . '/' . $file, ENT_QUOTES) . "') format('{$format}')";
}
}
$fontFaceCss = '';
if ($fontSources) {
$fontFaceCss = "@font-face{font-family:'{$customFontName}';font-style:normal;font-weight:400;src:" . implode(',', $fontSources) . ";}";
}
?><!doctype html>
<html lang="de">
<head>
@@ -15,6 +36,7 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
.gjs-one-bg{background-color:#fff!important}.gjs-two-color{color:#0f172a!important}
.gjs-three-bg{background-color:#f8fafc!important}.gjs-four-color{color:#334155!important}
#badge{position:fixed;right:8px;top:8px;background:#eef2ff;color:#1e3a8a;border:1px solid #c7d2fe;border-radius:999px;padding:4px 10px;font:12px system-ui;z-index:2147483647;opacity:.9}
<?= $fontFaceCss ?>
</style>
</head>
<body>
@@ -31,6 +53,18 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
window.BridgeParts.API_KERNEL_URL = window.BridgeParts.API_KERNEL_URL || '/api.php';
window.BridgeParts.API_BASE = window.BridgeParts.API_BASE || window.BridgeParts.API_KERNEL_URL;
window.BridgeParts.STORAGE_URL_BASE = window.BridgeParts.STORAGE_URL_BASE || window.BridgeParts.API_BASE;
window.BridgeParts.RTE_FONTS = window.BridgeParts.RTE_FONTS || [
{ label: 'Kids Handwriting', value: "'Kids Handwriting', 'Comic Sans MS', cursive" },
{ 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' },
];
window.BridgeParts.RTE_FONT_FACE_CSS = window.BridgeParts.RTE_FONT_FACE_CSS || <?= json_encode($fontFaceCss) ?>;
function logToParent(type, detail){ try{ parent.postMessage({source:'editor-core',type:type,detail:String(detail||'')},'*'); }catch(e){} }
window.addEventListener('error', function(e){