sdsad
This commit is contained in:
@@ -9,119 +9,106 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const execRteCommand = (rte, cmd, value, docOverride) => {
|
const escapeHtml = (text) => String(text || '')
|
||||||
try {
|
.replace(/&/g, '&')
|
||||||
if (rte && typeof rte.exec === 'function') {
|
.replace(/</g, '<')
|
||||||
rte.exec(cmd, value);
|
.replace(/>/g, '>')
|
||||||
return true;
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
|
const sanitizeInlineHtml = (rawHtml, fallbackText) => {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.innerHTML = String(rawHtml || '');
|
||||||
|
|
||||||
|
wrapper.querySelectorAll('script,style').forEach((node) => node.remove());
|
||||||
|
|
||||||
|
const inlineTags = new Set(['A', 'B', 'STRONG', 'I', 'EM', 'U', 'S', 'BR', 'SUB', 'SUP', 'SPAN']);
|
||||||
|
const blockTags = new Set([
|
||||||
|
'DIV', 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
|
||||||
|
'UL', 'OL', 'LI', 'TABLE', 'TBODY', 'THEAD', 'TFOOT', 'TR', 'TD', 'TH',
|
||||||
|
'SECTION', 'ARTICLE', 'ASIDE', 'HEADER', 'FOOTER',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const unwrap = (el, addBreak) => {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
while (el.firstChild) {
|
||||||
|
frag.appendChild(el.firstChild);
|
||||||
}
|
}
|
||||||
} catch {}
|
if (addBreak) {
|
||||||
try {
|
frag.appendChild(document.createElement('br'));
|
||||||
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);
|
el.replaceWith(frag);
|
||||||
if (ok === false && cmd === 'insertText') {
|
|
||||||
doc.execCommand('insertHTML', false, String(value || '').replace(/</g, '<').replace(/>/g, '>'));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch {}
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeRteActionList = (editor, names) => {
|
Array.from(wrapper.querySelectorAll('*')).forEach((el) => {
|
||||||
const cfg = editor.getConfig ? editor.getConfig() : {};
|
const tag = el.tagName;
|
||||||
cfg.richTextEditor = cfg.richTextEditor || {};
|
if (inlineTags.has(tag)) {
|
||||||
const base = Array.isArray(cfg.richTextEditor.actions)
|
Array.from(el.attributes).forEach((attr) => {
|
||||||
? cfg.richTextEditor.actions.slice()
|
const name = attr.name.toLowerCase();
|
||||||
: ['bold', 'italic', 'underline', 'strikethrough', 'link'];
|
if (tag === 'a') {
|
||||||
names.forEach((name) => {
|
if (!['href', 'target', 'rel', 'style'].includes(name)) {
|
||||||
const exists = base.some((item) => (typeof item === 'string' ? item === name : item && item.name === name));
|
el.removeAttribute(attr.name);
|
||||||
if (!exists) base.push(name);
|
}
|
||||||
|
} else if (name !== 'style') {
|
||||||
|
el.removeAttribute(attr.name);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
cfg.richTextEditor.actions = base;
|
return;
|
||||||
|
}
|
||||||
|
unwrap(el, blockTags.has(tag));
|
||||||
|
});
|
||||||
|
|
||||||
|
let html = wrapper.innerHTML
|
||||||
|
.replace(/<br\s*\/?>/gi, '<br>')
|
||||||
|
.replace(/(<br>)+$/g, '')
|
||||||
|
.trim();
|
||||||
|
if (!html) {
|
||||||
|
const text = String(fallbackText || wrapper.textContent || '').trim();
|
||||||
|
if (text) html = escapeHtml(text);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureTextToolbarButton = (editor, component) => {
|
const isTextLike = (component) => !!(component && component.is && (component.is('text') || component.is('button') || component.is('link')));
|
||||||
if (!component || !component.is) return;
|
|
||||||
const isTextLike = component.is('text') || component.is('button') || component.is('link');
|
const applyContentToComponent = (editor, component, html) => {
|
||||||
if (!isTextLike) return;
|
if (!component) return;
|
||||||
const toolbar = (component.get && component.get('toolbar')) || [];
|
const content = String(html || '');
|
||||||
if (toolbar.some((item) => item && item.command === 'bridge-open-richtext')) return;
|
if (component.set) component.set('content', content);
|
||||||
toolbar.push({
|
if (component.view && component.view.el) {
|
||||||
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>',
|
component.view.el.innerHTML = content;
|
||||||
attributes: { title: 'Richtext bearbeiten' },
|
}
|
||||||
command: 'bridge-open-richtext',
|
if (component.view && typeof component.view.render === 'function') {
|
||||||
});
|
component.view.render();
|
||||||
component.set && component.set('toolbar', toolbar);
|
}
|
||||||
|
if (component.trigger) {
|
||||||
|
component.trigger('change:content');
|
||||||
|
component.trigger('change:components');
|
||||||
|
}
|
||||||
|
if (editor && typeof editor.trigger === 'function') {
|
||||||
|
editor.trigger('component:update', component);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openRichTextModal = (editor, component) => {
|
const openRichTextModal = (editor, component) => {
|
||||||
if (!component || !component.is || !(component.is('text') || component.is('button') || component.is('link'))) {
|
if (!isTextLike(component)) {
|
||||||
log('RTE', 'Bitte zuerst ein Text-Element auswaehlen.', '#888');
|
log('RTE', 'Bitte zuerst ein Text-Element auswaehlen.', '#888');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modal = editor.Modal;
|
const modal = editor && editor.Modal;
|
||||||
if (!modal) return;
|
if (!modal || editor.__bridgeRteModalOpen) return;
|
||||||
if (editor.__bridgeRteModalOpen) return;
|
|
||||||
editor.__bridgeRteModalOpen = true;
|
editor.__bridgeRteModalOpen = true;
|
||||||
editor.__bridgeRteAllowClose = false;
|
|
||||||
let reopenGuard = false;
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
editor.__bridgeRteAllowClose = true;
|
|
||||||
editor.__bridgeRteModalOpen = false;
|
editor.__bridgeRteModalOpen = false;
|
||||||
if (typeof modal.__bridgeOriginalClose === 'function') {
|
|
||||||
modal.__bridgeOriginalClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof modal.close === 'function') {
|
if (typeof modal.close === 'function') {
|
||||||
modal.close();
|
modal.close();
|
||||||
return;
|
} else if (modal.getModel && modal.getModel().set) {
|
||||||
}
|
modal.getModel().set('open', 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';
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 doc = document;
|
||||||
const container = doc.createElement('div');
|
const container = doc.createElement('div');
|
||||||
container.style.display = 'flex';
|
container.style.display = 'flex';
|
||||||
@@ -148,11 +135,19 @@
|
|||||||
content.style.fontFamily = 'Arial, sans-serif';
|
content.style.fontFamily = 'Arial, sans-serif';
|
||||||
content.style.fontSize = '14px';
|
content.style.fontSize = '14px';
|
||||||
|
|
||||||
const initialHtml = (component.view && component.view.el && component.view.el.innerHTML)
|
const initialHtml = (component.get && component.get('content'))
|
||||||
|| (component.get && component.get('content'))
|
|| (component.view && component.view.el && component.view.el.innerHTML)
|
||||||
|| '';
|
|| '';
|
||||||
content.innerHTML = initialHtml;
|
content.innerHTML = initialHtml;
|
||||||
|
|
||||||
|
const exec = (cmd, value) => {
|
||||||
|
try {
|
||||||
|
content.focus();
|
||||||
|
const docRef = content.ownerDocument || document;
|
||||||
|
docRef.execCommand(cmd, false, value);
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
const addButton = (labelHtml, title, cmd, valueGetter) => {
|
const addButton = (labelHtml, title, cmd, valueGetter) => {
|
||||||
const btn = doc.createElement('button');
|
const btn = doc.createElement('button');
|
||||||
btn.type = 'button';
|
btn.type = 'button';
|
||||||
@@ -164,11 +159,10 @@
|
|||||||
btn.style.background = '#f8fafc';
|
btn.style.background = '#f8fafc';
|
||||||
btn.style.cursor = 'pointer';
|
btn.style.cursor = 'pointer';
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
content.focus();
|
|
||||||
const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter;
|
const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter;
|
||||||
if (value === null || value === undefined) return;
|
if (value === null || value === undefined) return;
|
||||||
if (cmd === 'createLink' && !value) return;
|
if (cmd === 'createLink' && !value) return;
|
||||||
execRteCommand(null, cmd, value, content.ownerDocument);
|
exec(cmd, value);
|
||||||
});
|
});
|
||||||
toolbar.appendChild(btn);
|
toolbar.appendChild(btn);
|
||||||
};
|
};
|
||||||
@@ -188,20 +182,12 @@
|
|||||||
});
|
});
|
||||||
select.addEventListener('change', () => {
|
select.addEventListener('change', () => {
|
||||||
const value = select.value;
|
const value = select.value;
|
||||||
content.focus();
|
|
||||||
if (value) onChange(value);
|
if (value) onChange(value);
|
||||||
});
|
});
|
||||||
toolbar.appendChild(select);
|
toolbar.appendChild(select);
|
||||||
return 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>`;
|
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('<strong>B</strong>', 'Fett', 'bold');
|
||||||
addButton('<em>I</em>', 'Kursiv', 'italic');
|
addButton('<em>I</em>', 'Kursiv', 'italic');
|
||||||
@@ -233,11 +219,7 @@
|
|||||||
{ label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
|
{ label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
|
||||||
{ label: 'Verdana', value: 'Verdana, sans-serif' },
|
{ label: 'Verdana', value: 'Verdana, sans-serif' },
|
||||||
];
|
];
|
||||||
addSelect([
|
addSelect([{ label: 'Schriftart', value: '' }, ...fontOptions], 'Schriftart', (value) => exec('fontName', value));
|
||||||
{ label: 'Schriftart', value: '' },
|
|
||||||
...fontOptions,
|
|
||||||
], 'Schriftart', (value) => execRteCommand(null, 'fontName', value, content.ownerDocument));
|
|
||||||
|
|
||||||
addSelect([
|
addSelect([
|
||||||
{ label: 'Groesse', value: '' },
|
{ label: 'Groesse', value: '' },
|
||||||
{ label: '10px', value: '1' },
|
{ label: '10px', value: '1' },
|
||||||
@@ -247,7 +229,7 @@
|
|||||||
{ label: '18px', value: '5' },
|
{ label: '18px', value: '5' },
|
||||||
{ label: '24px', value: '6' },
|
{ label: '24px', value: '6' },
|
||||||
{ label: '32px', value: '7' },
|
{ label: '32px', value: '7' },
|
||||||
], 'Schriftgroesse', (value) => execRteCommand(null, 'fontSize', value, content.ownerDocument));
|
], 'Schriftgroesse', (value) => exec('fontSize', value));
|
||||||
|
|
||||||
const emojiBtn = doc.createElement('button');
|
const emojiBtn = doc.createElement('button');
|
||||||
emojiBtn.type = 'button';
|
emojiBtn.type = 'button';
|
||||||
@@ -260,7 +242,7 @@
|
|||||||
emojiBtn.style.cursor = 'pointer';
|
emojiBtn.style.cursor = 'pointer';
|
||||||
emojiBtn.addEventListener('click', () => {
|
emojiBtn.addEventListener('click', () => {
|
||||||
const pick = prompt('Emoticon eingeben', ':)');
|
const pick = prompt('Emoticon eingeben', ':)');
|
||||||
if (pick) insertText(pick);
|
if (pick) exec('insertText', pick);
|
||||||
});
|
});
|
||||||
toolbar.appendChild(emojiBtn);
|
toolbar.appendChild(emojiBtn);
|
||||||
|
|
||||||
@@ -280,9 +262,7 @@
|
|||||||
cancelBtn.style.borderRadius = '4px';
|
cancelBtn.style.borderRadius = '4px';
|
||||||
cancelBtn.style.background = '#f8fafc';
|
cancelBtn.style.background = '#f8fafc';
|
||||||
cancelBtn.style.cursor = 'pointer';
|
cancelBtn.style.cursor = 'pointer';
|
||||||
cancelBtn.addEventListener('click', () => {
|
cancelBtn.addEventListener('click', closeModal);
|
||||||
closeModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
const saveBtn = doc.createElement('button');
|
const saveBtn = doc.createElement('button');
|
||||||
saveBtn.type = 'button';
|
saveBtn.type = 'button';
|
||||||
@@ -293,60 +273,10 @@
|
|||||||
saveBtn.style.background = '#0ea5e9';
|
saveBtn.style.background = '#0ea5e9';
|
||||||
saveBtn.style.color = '#ffffff';
|
saveBtn.style.color = '#ffffff';
|
||||||
saveBtn.style.cursor = 'pointer';
|
saveBtn.style.cursor = 'pointer';
|
||||||
const normalizeRteHtml = (rawHtml) => {
|
|
||||||
const wrapper = doc.createElement('div');
|
|
||||||
wrapper.innerHTML = String(rawHtml || '').trim();
|
|
||||||
// Unwrap single empty div wrapper inserted by contenteditable
|
|
||||||
if (wrapper.children.length === 1 && wrapper.firstElementChild.tagName === 'DIV' && wrapper.firstElementChild.attributes.length === 0) {
|
|
||||||
wrapper.innerHTML = wrapper.firstElementChild.innerHTML;
|
|
||||||
}
|
|
||||||
// Drop empty div/br artifacts
|
|
||||||
wrapper.querySelectorAll('div').forEach((el) => {
|
|
||||||
if (!el.attributes.length && !el.textContent.trim() && !el.querySelector('img,br,span,b,strong,i,em,u,a,ul,ol,li')) {
|
|
||||||
el.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let html = wrapper.innerHTML.replace(/<br\s*\/?>/gi, '').trim();
|
|
||||||
if (!html) {
|
|
||||||
const fallbackText = wrapper.textContent.trim();
|
|
||||||
if (fallbackText) {
|
|
||||||
html = fallbackText.replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
};
|
|
||||||
|
|
||||||
saveBtn.addEventListener('click', () => {
|
saveBtn.addEventListener('click', () => {
|
||||||
const rawHtml = content.innerHTML || '';
|
const rawHtml = content.innerHTML || '';
|
||||||
let html = normalizeRteHtml(rawHtml);
|
const html = sanitizeInlineHtml(rawHtml, content.textContent || '');
|
||||||
try {
|
applyContentToComponent(editor, component, html);
|
||||||
console.warn('[RTE SAVE DEBUG]', {
|
|
||||||
rawHtml,
|
|
||||||
normalizedHtml: html,
|
|
||||||
textContent: String(content.textContent || '').trim()
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
const isTextLike = component && component.is && (component.is('text') || component.is('button') || component.is('link'));
|
|
||||||
try {
|
|
||||||
if (html) {
|
|
||||||
if (!isTextLike && component.components) {
|
|
||||||
component.components(html);
|
|
||||||
}
|
|
||||||
if (component.set) {
|
|
||||||
component.set('content', html);
|
|
||||||
component.trigger && component.trigger('change:content');
|
|
||||||
component.trigger && component.trigger('change:components');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('[RTE SAVE DEBUG] Skip empty content update.');
|
|
||||||
}
|
|
||||||
if (component.view && component.view.render) {
|
|
||||||
component.view.render();
|
|
||||||
}
|
|
||||||
if (editor && typeof editor.trigger === 'function') {
|
|
||||||
editor.trigger('component:update', component);
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
closeModal();
|
closeModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -357,131 +287,27 @@
|
|||||||
modal.setTitle('Richtext Editor');
|
modal.setTitle('Richtext Editor');
|
||||||
modal.setContent(container);
|
modal.setContent(container);
|
||||||
const mdl = modal.getModel && modal.getModel();
|
const mdl = modal.getModel && modal.getModel();
|
||||||
if (mdl && typeof mdl.set === 'function') {
|
if (mdl && mdl.set) {
|
||||||
mdl.set('closeOnEsc', false);
|
mdl.set('closeOnEsc', true);
|
||||||
mdl.set('closeOnClick', false);
|
mdl.set('closeOnClick', true);
|
||||||
}
|
}
|
||||||
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();
|
modal.open();
|
||||||
}
|
};
|
||||||
|
|
||||||
const modalEl = modal.getEl ? modal.getEl() : modal.el;
|
const ensureTextToolbarButton = (component) => {
|
||||||
const closeBtn = modalEl && modalEl.querySelector ? modalEl.querySelector('.gjs-mdl-btn-close') : null;
|
if (!isTextLike(component) || !component.get) return;
|
||||||
if (closeBtn && !closeBtn.__bridgeRteBound) {
|
const toolbar = component.get('toolbar') || [];
|
||||||
closeBtn.__bridgeRteBound = true;
|
if (toolbar.some((item) => item && item.command === 'bridge-open-richtext')) return;
|
||||||
closeBtn.addEventListener('click', () => {
|
toolbar.push({
|
||||||
editor.__bridgeRteAllowClose = true;
|
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>',
|
||||||
}, true);
|
attributes: { title: 'Richtext bearbeiten' },
|
||||||
}
|
command: 'bridge-open-richtext',
|
||||||
|
});
|
||||||
|
component.set('toolbar', toolbar);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setupRichTextEditor = (editor) => {
|
const setupRichTextEditor = (editor) => {
|
||||||
if (!editor || !editor.RichTextEditor) return;
|
if (!editor) 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 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: '<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 && editor.Commands.add) {
|
if (editor.Commands && editor.Commands.add) {
|
||||||
editor.Commands.add('bridge-open-richtext', {
|
editor.Commands.add('bridge-open-richtext', {
|
||||||
run(ed, sender, opts = {}) {
|
run(ed, sender, opts = {}) {
|
||||||
@@ -492,90 +318,18 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeRteActionList(editor, [
|
const cfg = editor.getConfig ? editor.getConfig() : {};
|
||||||
'bridge-open-richtext',
|
cfg.richTextEditor = cfg.richTextEditor || {};
|
||||||
'bridge-font-family',
|
const actions = Array.isArray(cfg.richTextEditor.actions)
|
||||||
'bridge-font-size',
|
? cfg.richTextEditor.actions.slice()
|
||||||
'bridge-align-left',
|
: ['bold', 'italic', 'underline', 'strikethrough', 'link'];
|
||||||
'bridge-align-center',
|
if (!actions.includes('bridge-open-richtext')) actions.push('bridge-open-richtext');
|
||||||
'bridge-align-right',
|
cfg.richTextEditor.actions = actions;
|
||||||
'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')));
|
editor.on('component:selected', (model) => ensureTextToolbarButton(model));
|
||||||
let lastTextSelection = { id: null, ts: 0 };
|
editor.on('component:add', (model) => ensureTextToolbarButton(model));
|
||||||
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) => {
|
editor.on('component:dblclick', (model) => {
|
||||||
if (isTextLike(model)) {
|
if (isTextLike(model)) openRichTextModal(editor, 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);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user