This commit is contained in:
2026-01-14 01:06:48 +01:00
parent eb77124049
commit 62da2a67af

View File

@@ -263,6 +263,319 @@
autosave: false, autosave: false,
}; };
const execRteCommand = (rte, cmd, value) => {
try {
if (rte && typeof rte.exec === 'function') {
rte.exec(cmd, value);
return true;
}
} catch {}
try {
const ok = document.execCommand(cmd, false, value);
if (ok === false && cmd === 'insertText') {
document.execCommand('insertHTML', false, String(value || '').replace(/</g, '&lt;').replace(/>/g, '&gt;'));
}
return true;
} catch {}
return false;
};
const normalizeRteActionList = (editor, names) => {
const cfg = editor.getConfig ? editor.getConfig() : {};
cfg.richTextEditor = cfg.richTextEditor || {};
const base = Array.isArray(cfg.richTextEditor.actions)
? cfg.richTextEditor.actions.slice()
: ['bold', 'italic', 'underline', 'strikethrough', 'link'];
names.forEach((name) => {
const exists = base.some((item) => (typeof item === 'string' ? item === name : item && item.name === name));
if (!exists) base.push(name);
});
cfg.richTextEditor.actions = base;
};
const ensureTextToolbarButton = (editor, component) => {
if (!component || !component.is || !component.is('text')) return;
const toolbar = (component.get && component.get('toolbar')) || [];
if (toolbar.some((item) => item && item.command === 'bridge-open-richtext')) return;
toolbar.push({
attributes: { class: 'fa fa-pencil', title: 'Richtext bearbeiten' },
command: 'bridge-open-richtext',
});
component.set && component.set('toolbar', toolbar);
};
const openRichTextModal = (editor, component) => {
if (!component || !component.is || !component.is('text')) {
log('RTE', 'Bitte zuerst ein Text-Element auswaehlen.', '#888');
return;
}
const modal = editor.Modal;
if (!modal) return;
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 = (label, title, cmd, valueGetter) => {
const btn = doc.createElement('button');
btn.type = 'button';
btn.textContent = label;
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);
});
toolbar.appendChild(btn);
};
const addSelect = (options, title, onChange) => {
const select = doc.createElement('select');
select.title = title;
select.style.padding = '4px 8px';
select.style.border = '1px solid #cbd5f5';
select.style.borderRadius = '4px';
select.style.background = '#ffffff';
options.forEach((opt) => {
const optEl = doc.createElement('option');
optEl.value = opt.value;
optEl.textContent = opt.label;
select.appendChild(optEl);
});
select.addEventListener('change', () => {
const value = select.value;
content.focus();
if (value) onChange(value);
});
toolbar.appendChild(select);
return select;
};
const insertText = (text) => {
content.focus();
if (!execRteCommand(null, 'insertText', text)) {
execRteCommand(null, 'insertHTML', String(text).replace(/</g, '&lt;').replace(/>/g, '&gt;'));
}
};
addButton('B', 'Fett', 'bold');
addButton('I', 'Kursiv', 'italic');
addButton('U', 'Unterstrichen', 'underline');
addButton('S', 'Durchgestrichen', 'strikethrough');
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');
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));
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));
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', () => 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;
if (component.is && component.is('text')) {
component.set && component.set('content', html);
} else 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('Richtext Editor');
modal.setContent(container);
modal.open();
};
const setupRichTextEditor = (editor) => {
if (!editor || !editor.RichTextEditor) return;
const rte = editor.RichTextEditor;
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', '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-emoji', ':-)', 'Emoticon einfuegen', 'insertText', () => prompt('Emoticon eingeben', ':)'));
addAction('bridge-font-family', 'F', 'Schriftart', 'fontName', () => prompt('Schriftart (z.B. Arial, Georgia)', '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;
});
addAction('bridge-open-richtext', 'RTE', 'Richtext Editor', 'execCommand', () => {
const component = editor.getSelected && editor.getSelected();
openRichTextModal(editor, component);
return null;
});
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',
]);
editor.on('component:selected', (model) => ensureTextToolbarButton(editor, model));
editor.on('component:add', (model) => ensureTextToolbarButton(editor, model));
};
var ed = grapesjs.init({ var ed = grapesjs.init({
container: '#gjs', container: '#gjs',
height: '100vh', height: '100vh',
@@ -309,6 +622,8 @@
log('CORE WARN', `textTags Konfiguration fehlgeschlagen: ${e.message}`, 'orange', 'warn'); log('CORE WARN', `textTags Konfiguration fehlgeschlagen: ${e.message}`, 'orange', 'warn');
} }
setupRichTextEditor(ed);
// Entfernt: jegliche Blur/RTE-Handler, die Inhalte verändern. // Entfernt: jegliche Blur/RTE-Handler, die Inhalte verändern.
// Keep the fix off live updates to avoid cursor jumps; run only on RTE close. // Keep the fix off live updates to avoid cursor jumps; run only on RTE close.