sdadas
This commit is contained in:
@@ -469,7 +469,13 @@
|
|||||||
? B.RTE_FONT_FACE_CSS.trim()
|
? B.RTE_FONT_FACE_CSS.trim()
|
||||||
: '';
|
: '';
|
||||||
const cssPayload = (fontCss ? fontCss + '\n' : '') + editor.getCss();
|
const cssPayload = (fontCss ? fontCss + '\n' : '') + editor.getCss();
|
||||||
const htmlContent = editor.getHtml() + '<style>' + cssPayload + '</style>';
|
const serializeHtml = () => {
|
||||||
|
if (B && B.BridgeRTE && typeof B.BridgeRTE.serializeHtml === 'function') {
|
||||||
|
return B.BridgeRTE.serializeHtml(editor);
|
||||||
|
}
|
||||||
|
return editor.getHtml();
|
||||||
|
};
|
||||||
|
const htmlContent = serializeHtml() + '<style>' + cssPayload + '</style>';
|
||||||
// 2. KRITISCH: Holt die JSON-Repräsentation des Editors
|
// 2. KRITISCH: Holt die JSON-Repräsentation des Editors
|
||||||
let jsonProjectDataRaw = '';
|
let jsonProjectDataRaw = '';
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,22 +1,55 @@
|
|||||||
/* /assets/js/bridge/rte-editor.js */
|
/* /assets/js/bridge/rte-editor.js */
|
||||||
(function () {
|
(function () {
|
||||||
const PluginName = 'bridge-rte-editor';
|
const PluginName = 'bridge-rte-editor';
|
||||||
const B = window.BridgeParts || (window.BridgeParts = {});
|
const editorInstances = new WeakMap();
|
||||||
|
|
||||||
const log = (type, message, color = '#94a3b8', logType = 'info', force = false) => {
|
class BridgeRTE {
|
||||||
if (typeof B.log === 'function') {
|
constructor(bridgeParts) {
|
||||||
B.log(PluginName, `[${type}] ${message}`, color, logType, force);
|
this.B = bridgeParts || (window.BridgeParts || (window.BridgeParts = {}));
|
||||||
|
this.editor = null;
|
||||||
|
this.modalOpen = false;
|
||||||
|
this.allowClose = false;
|
||||||
|
this.lastContent = new WeakMap();
|
||||||
|
this.restoring = new WeakSet();
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const escapeHtml = (text) => String(text || '')
|
static serializeHtml(editor) {
|
||||||
|
if (!editor || typeof editor.getHtml !== 'function') return '';
|
||||||
|
let html = editor.getHtml();
|
||||||
|
try {
|
||||||
|
const wrapper = editor.getWrapper && editor.getWrapper();
|
||||||
|
if (wrapper && wrapper.find) {
|
||||||
|
const candidates = wrapper.find('[data-gjs-type=\"text\"], [data-gjs-type=\"link\"], [data-gjs-type=\"button\"]');
|
||||||
|
candidates.forEach((model) => {
|
||||||
|
const content = model && model.get ? String(model.get('content') || '') : '';
|
||||||
|
const id = model && (model.getId ? model.getId() : model.get && model.get('id'));
|
||||||
|
const tag = (model && model.get && (model.get('tagName') || model.get('tag'))) || 'div';
|
||||||
|
if (!content || !id) return;
|
||||||
|
const tagName = String(tag).toLowerCase();
|
||||||
|
const rx = new RegExp(`<${tagName}([^>]*\\\\bid=[\"']${id}[\"'][^>]*)>([\\\\s\\\\S]*?)<\\\\/${tagName}>`, 'i');
|
||||||
|
html = html.replace(rx, `<${tagName}$1>${content}</${tagName}>`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(type, message, color = '#94a3b8', logType = 'info', force = false) {
|
||||||
|
if (typeof this.B.log === 'function') {
|
||||||
|
this.B.log(PluginName, `[${type}] ${message}`, color, logType, force);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(text) {
|
||||||
|
return String(text || '')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
const sanitizeInlineHtml = (rawHtml, fallbackText) => {
|
sanitizeInlineHtml(rawHtml, fallbackText) {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.innerHTML = String(rawHtml || '');
|
wrapper.innerHTML = String(rawHtml || '');
|
||||||
|
|
||||||
@@ -71,14 +104,16 @@
|
|||||||
}
|
}
|
||||||
if (!html) {
|
if (!html) {
|
||||||
const text = String(fallbackText || wrapper.textContent || '').trim();
|
const text = String(fallbackText || wrapper.textContent || '').trim();
|
||||||
if (text) html = escapeHtml(text);
|
if (text) html = this.escapeHtml(text);
|
||||||
}
|
}
|
||||||
return html;
|
return html;
|
||||||
};
|
}
|
||||||
|
|
||||||
const isTextLike = (component) => !!(component && component.is && (component.is('text') || component.is('button') || component.is('link')));
|
isTextLike(component) {
|
||||||
|
return !!(component && component.is && (component.is('text') || component.is('button') || component.is('link')));
|
||||||
|
}
|
||||||
|
|
||||||
const logConsoleSnapshot = (editor, component, label) => {
|
logConsoleSnapshot(editor, component, label) {
|
||||||
try {
|
try {
|
||||||
const viewEl = component && component.view ? component.view.el : null;
|
const viewEl = component && component.view ? component.view.el : null;
|
||||||
const modelContent = component && component.get ? String(component.get('content') || '') : '';
|
const modelContent = component && component.get ? String(component.get('content') || '') : '';
|
||||||
@@ -97,9 +132,9 @@
|
|||||||
editorHtml: editorHtml.slice(0, maxLen),
|
editorHtml: editorHtml.slice(0, maxLen),
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
}
|
||||||
|
|
||||||
const applyContentToComponent = (editor, component, html) => {
|
applyContentToComponent(editor, component, html) {
|
||||||
if (!component) return;
|
if (!component) return;
|
||||||
const content = String(html || '');
|
const content = String(html || '');
|
||||||
try {
|
try {
|
||||||
@@ -126,9 +161,9 @@
|
|||||||
if (editor && typeof editor.trigger === 'function') {
|
if (editor && typeof editor.trigger === 'function') {
|
||||||
editor.trigger('component:update', component);
|
editor.trigger('component:update', component);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const collectFrameCss = (editor) => {
|
collectFrameCss(editor) {
|
||||||
const cssParts = [];
|
const cssParts = [];
|
||||||
try {
|
try {
|
||||||
const frameDoc = editor && editor.Canvas && editor.Canvas.getDocument ? editor.Canvas.getDocument() : null;
|
const frameDoc = editor && editor.Canvas && editor.Canvas.getDocument ? editor.Canvas.getDocument() : null;
|
||||||
@@ -148,9 +183,9 @@
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
return cssParts.join('\n');
|
return cssParts.join('\n');
|
||||||
};
|
}
|
||||||
|
|
||||||
const getEditableTag = (component) => {
|
getEditableTag(component) {
|
||||||
const viewEl = component && component.view ? component.view.el : null;
|
const viewEl = component && component.view ? component.view.el : null;
|
||||||
const raw = (component && component.get && (component.get('tagName') || component.get('tag')))
|
const raw = (component && component.get && (component.get('tagName') || component.get('tag')))
|
||||||
|| (viewEl && viewEl.tagName)
|
|| (viewEl && viewEl.tagName)
|
||||||
@@ -162,9 +197,9 @@
|
|||||||
'sub', 'sup', 'ul', 'ol', 'li',
|
'sub', 'sup', 'ul', 'ol', 'li',
|
||||||
]);
|
]);
|
||||||
return allowed.has(tag) ? tag : 'div';
|
return allowed.has(tag) ? tag : 'div';
|
||||||
};
|
}
|
||||||
|
|
||||||
const applyComponentPreviewStyles = (component, content) => {
|
applyComponentPreviewStyles(component, content) {
|
||||||
if (!component || !content) return;
|
if (!component || !content) return;
|
||||||
const mergeStyles = () => {
|
const mergeStyles = () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -202,9 +237,9 @@
|
|||||||
applyStyle('letterSpacing', pick(['letter-spacing', 'letterSpacing']));
|
applyStyle('letterSpacing', pick(['letter-spacing', 'letterSpacing']));
|
||||||
applyStyle('textAlign', pick(['text-align', 'textAlign']));
|
applyStyle('textAlign', pick(['text-align', 'textAlign']));
|
||||||
applyStyle('backgroundColor', pick(['background-color', 'backgroundColor']));
|
applyStyle('backgroundColor', pick(['background-color', 'backgroundColor']));
|
||||||
};
|
}
|
||||||
|
|
||||||
const applyComputedPreviewStyles = (component, content) => {
|
applyComputedPreviewStyles(component, content) {
|
||||||
try {
|
try {
|
||||||
const viewEl = component && component.view ? component.view.el : null;
|
const viewEl = component && component.view ? component.view.el : null;
|
||||||
if (!viewEl || !viewEl.ownerDocument) return;
|
if (!viewEl || !viewEl.ownerDocument) return;
|
||||||
@@ -231,31 +266,18 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
}
|
||||||
|
|
||||||
const openRichTextModal = (editor, component) => {
|
openRichTextModal(editor, component) {
|
||||||
if (!isTextLike(component)) {
|
if (!this.isTextLike(component)) {
|
||||||
log('RTE', 'Bitte zuerst ein Text-Element auswaehlen.', '#888');
|
this.log('RTE', 'Bitte zuerst ein Text-Element auswaehlen.', '#888');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modal = editor && editor.Modal;
|
const modal = editor && editor.Modal;
|
||||||
if (!modal || editor.__bridgeRteModalOpen) return;
|
if (!modal || this.modalOpen) return;
|
||||||
editor.__bridgeRteModalOpen = true;
|
this.modalOpen = true;
|
||||||
editor.__bridgeRteAllowClose = false;
|
this.allowClose = false;
|
||||||
|
|
||||||
if (!modal.__bridgeCloseGuarded) {
|
|
||||||
modal.__bridgeCloseGuarded = true;
|
|
||||||
modal.__bridgeOriginalClose = modal.close ? modal.close.bind(modal) : null;
|
|
||||||
if (modal.close) {
|
|
||||||
modal.close = function (...args) {
|
|
||||||
if (editor.__bridgeRteAllowClose && modal.__bridgeOriginalClose) {
|
|
||||||
return modal.__bridgeOriginalClose(...args);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const editing = editor.getEditing && editor.getEditing();
|
const editing = editor.getEditing && editor.getEditing();
|
||||||
@@ -268,17 +290,15 @@
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
editor.__bridgeRteAllowClose = true;
|
this.allowClose = true;
|
||||||
editor.__bridgeRteModalOpen = false;
|
this.modalOpen = false;
|
||||||
if (B.allowModalCloseOnce) B.allowModalCloseOnce();
|
if (this.B.allowModalCloseOnce) this.B.allowModalCloseOnce();
|
||||||
if (typeof modal.__bridgeOriginalClose === 'function') {
|
if (typeof modal.close === 'function') {
|
||||||
modal.__bridgeOriginalClose();
|
|
||||||
} else if (typeof modal.close === 'function') {
|
|
||||||
modal.close();
|
modal.close();
|
||||||
} else if (modal.getModel && modal.getModel().set) {
|
} else if (modal.getModel && modal.getModel().set) {
|
||||||
modal.getModel().set('open', false);
|
modal.getModel().set('open', false);
|
||||||
}
|
}
|
||||||
editor.__bridgeRteAllowClose = false;
|
this.allowClose = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const doc = document;
|
const doc = document;
|
||||||
@@ -288,14 +308,18 @@
|
|||||||
container.style.gap = '10px';
|
container.style.gap = '10px';
|
||||||
container.style.height = '100%';
|
container.style.height = '100%';
|
||||||
container.style.minHeight = '360px';
|
container.style.minHeight = '360px';
|
||||||
|
container.className = 'bridge-rte-container';
|
||||||
|
container.setAttribute('data-bridge-rte', 'container');
|
||||||
|
|
||||||
const toolbar = doc.createElement('div');
|
const toolbar = doc.createElement('div');
|
||||||
toolbar.style.display = 'flex';
|
toolbar.style.display = 'flex';
|
||||||
toolbar.style.flexWrap = 'wrap';
|
toolbar.style.flexWrap = 'wrap';
|
||||||
toolbar.style.gap = '6px';
|
toolbar.style.gap = '6px';
|
||||||
toolbar.style.alignItems = 'center';
|
toolbar.style.alignItems = 'center';
|
||||||
|
toolbar.className = 'bridge-rte-toolbar';
|
||||||
|
toolbar.setAttribute('data-bridge-rte', 'toolbar');
|
||||||
|
|
||||||
const content = doc.createElement(getEditableTag(component));
|
const content = doc.createElement(this.getEditableTag(component));
|
||||||
content.contentEditable = 'true';
|
content.contentEditable = 'true';
|
||||||
content.style.flex = '1';
|
content.style.flex = '1';
|
||||||
content.style.minHeight = '280px';
|
content.style.minHeight = '280px';
|
||||||
@@ -306,9 +330,13 @@
|
|||||||
content.style.overflow = 'auto';
|
content.style.overflow = 'auto';
|
||||||
content.style.fontFamily = 'Arial, sans-serif';
|
content.style.fontFamily = 'Arial, sans-serif';
|
||||||
content.style.fontSize = '14px';
|
content.style.fontSize = '14px';
|
||||||
|
content.className = [content.className, 'bridge-rte-content'].filter(Boolean).join(' ');
|
||||||
|
content.setAttribute('data-bridge-rte', 'content');
|
||||||
if (component && component.view && component.view.el) {
|
if (component && component.view && component.view.el) {
|
||||||
const viewEl = component.view.el;
|
const viewEl = component.view.el;
|
||||||
if (viewEl.className) content.className = viewEl.className;
|
if (viewEl.className) {
|
||||||
|
content.className = [content.className, viewEl.className].filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
if (viewEl.id) content.id = viewEl.id;
|
if (viewEl.id) content.id = viewEl.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +358,7 @@
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
const initialHtml = sanitizeInlineHtml(htmlSource, modelContent || '');
|
const initialHtml = this.sanitizeInlineHtml(htmlSource, modelContent || '');
|
||||||
content.innerHTML = initialHtml;
|
content.innerHTML = initialHtml;
|
||||||
const existingStyle = component && component.get && component.get('style') ? component.get('style') : null;
|
const existingStyle = component && component.get && component.get('style') ? component.get('style') : null;
|
||||||
if (existingStyle && typeof existingStyle === 'object') {
|
if (existingStyle && typeof existingStyle === 'object') {
|
||||||
@@ -341,8 +369,8 @@
|
|||||||
content.style.fontSize = existingStyle.fontSize;
|
content.style.fontSize = existingStyle.fontSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applyComponentPreviewStyles(component, content);
|
this.applyComponentPreviewStyles(component, content);
|
||||||
applyComputedPreviewStyles(component, content);
|
this.applyComputedPreviewStyles(component, content);
|
||||||
|
|
||||||
let savedRange = null;
|
let savedRange = null;
|
||||||
const saveSelection = () => {
|
const saveSelection = () => {
|
||||||
@@ -413,6 +441,8 @@
|
|||||||
btn.innerHTML = labelHtml;
|
btn.innerHTML = labelHtml;
|
||||||
btn.title = title;
|
btn.title = title;
|
||||||
btn.setAttribute('aria-label', title);
|
btn.setAttribute('aria-label', title);
|
||||||
|
btn.className = 'bridge-rte-btn';
|
||||||
|
btn.setAttribute('data-bridge-rte', 'button');
|
||||||
btn.style.padding = '4px 8px';
|
btn.style.padding = '4px 8px';
|
||||||
btn.style.border = '1px solid #cbd5f5';
|
btn.style.border = '1px solid #cbd5f5';
|
||||||
btn.style.borderRadius = '4px';
|
btn.style.borderRadius = '4px';
|
||||||
@@ -439,6 +469,8 @@
|
|||||||
const select = doc.createElement('select');
|
const select = doc.createElement('select');
|
||||||
select.title = title;
|
select.title = title;
|
||||||
select.setAttribute('aria-label', title);
|
select.setAttribute('aria-label', title);
|
||||||
|
select.className = 'bridge-rte-select';
|
||||||
|
select.setAttribute('data-bridge-rte', 'select');
|
||||||
select.style.padding = '4px 8px';
|
select.style.padding = '4px 8px';
|
||||||
select.style.border = '1px solid #cbd5f5';
|
select.style.border = '1px solid #cbd5f5';
|
||||||
select.style.borderRadius = '4px';
|
select.style.borderRadius = '4px';
|
||||||
@@ -460,6 +492,65 @@
|
|||||||
return select;
|
return select;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyComponentStyle = (styleObj, opts = {}) => {
|
||||||
|
try {
|
||||||
|
if (opts.preview && content) {
|
||||||
|
Object.entries(styleObj).forEach(([key, val]) => {
|
||||||
|
content.style[key] = val;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalizeKeys = (obj) => {
|
||||||
|
const out = {};
|
||||||
|
Object.entries(obj || {}).forEach(([key, val]) => {
|
||||||
|
if (key === 'fontFamily') out['font-family'] = val;
|
||||||
|
else if (key === 'fontSize') out['font-size'] = val;
|
||||||
|
else out[key] = val;
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
if (component && component.setStyle && component.getStyle) {
|
||||||
|
const current = component.getStyle() || {};
|
||||||
|
const safeCurrent = (current && typeof current === 'object' && !Array.isArray(current)) ? current : {};
|
||||||
|
component.setStyle({ ...safeCurrent, ...normalizeKeys(styleObj) });
|
||||||
|
} else if (component && component.set) {
|
||||||
|
const current = component.get && component.get('style') ? component.get('style') : {};
|
||||||
|
const safeCurrent = (current && typeof current === 'object' && !Array.isArray(current)) ? current : {};
|
||||||
|
component.set('style', { ...safeCurrent, ...normalizeKeys(styleObj) });
|
||||||
|
}
|
||||||
|
if (component && component.view && component.view.el) {
|
||||||
|
Object.entries(styleObj).forEach(([key, val]) => {
|
||||||
|
component.view.el.style[key] = val;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pendingComponentStyle = {};
|
||||||
|
const ui = this.B.RTE_UI || {};
|
||||||
|
const ctx = {
|
||||||
|
editor,
|
||||||
|
component,
|
||||||
|
modal,
|
||||||
|
toolbar,
|
||||||
|
content,
|
||||||
|
addButton,
|
||||||
|
addSelect,
|
||||||
|
exec,
|
||||||
|
applyInlineStyle,
|
||||||
|
getSelectionRange,
|
||||||
|
applyComponentStyle,
|
||||||
|
pendingComponentStyle,
|
||||||
|
saveSelection,
|
||||||
|
restoreSelection,
|
||||||
|
};
|
||||||
|
if (typeof ui.beforeOpen === 'function') {
|
||||||
|
try { ui.beforeOpen(ctx); } catch {}
|
||||||
|
}
|
||||||
|
if (typeof ui.buildToolbar === 'function') {
|
||||||
|
try { ui.buildToolbar(ctx); } catch {}
|
||||||
|
}
|
||||||
|
if (!ui.overrideToolbar) {
|
||||||
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(
|
addButton(
|
||||||
icon('M6 4h5a3 3 0 0 1 0 6H6V4zm0 8h6a3 3 0 0 1 0 6H6v-6z'),
|
icon('M6 4h5a3 3 0 0 1 0 6H6V4zm0 8h6a3 3 0 0 1 0 6H6v-6z'),
|
||||||
@@ -496,8 +587,8 @@
|
|||||||
addButton(icon('M13 4h4v4h-4V4zM7 16h4v4H7v-4zM7 10h10v2H7v-2z'), 'Ausruecken', 'outdent');
|
addButton(icon('M13 4h4v4h-4V4zM7 16h4v4H7v-4zM7 10h10v2H7v-2z'), 'Ausruecken', 'outdent');
|
||||||
addButton(icon('M5 5h14v2H5zM5 9h10v2H5zM5 13h14v2H5zM5 17h10v2H5z'), 'Formatierung entfernen', 'removeFormat');
|
addButton(icon('M5 5h14v2H5zM5 9h10v2H5zM5 13h14v2H5zM5 17h10v2H5z'), 'Formatierung entfernen', 'removeFormat');
|
||||||
|
|
||||||
const fontOptions = (B.RTE_FONTS && Array.isArray(B.RTE_FONTS) && B.RTE_FONTS.length)
|
const fontOptions = (this.B.RTE_FONTS && Array.isArray(this.B.RTE_FONTS) && this.B.RTE_FONTS.length)
|
||||||
? B.RTE_FONTS
|
? this.B.RTE_FONTS
|
||||||
: [
|
: [
|
||||||
{ label: 'Arial', value: 'Arial, sans-serif' },
|
{ label: 'Arial', value: 'Arial, sans-serif' },
|
||||||
{ label: 'Calibri', value: 'Calibri, sans-serif' },
|
{ label: 'Calibri', value: 'Calibri, sans-serif' },
|
||||||
@@ -508,41 +599,6 @@
|
|||||||
{ 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' },
|
||||||
];
|
];
|
||||||
const applyComponentStyle = (styleObj, opts = {}) => {
|
|
||||||
try {
|
|
||||||
if (opts.preview && content) {
|
|
||||||
Object.entries(styleObj).forEach(([key, val]) => {
|
|
||||||
content.style[key] = val;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const normalizeKeys = (obj) => {
|
|
||||||
const out = {};
|
|
||||||
Object.entries(obj || {}).forEach(([key, val]) => {
|
|
||||||
if (key === 'fontFamily') out['font-family'] = val;
|
|
||||||
else if (key === 'fontSize') out['font-size'] = val;
|
|
||||||
else out[key] = val;
|
|
||||||
});
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
if (component && component.setStyle && component.getStyle) {
|
|
||||||
const current = component.getStyle() || {};
|
|
||||||
const safeCurrent = (current && typeof current === 'object' && !Array.isArray(current)) ? current : {};
|
|
||||||
component.setStyle({ ...safeCurrent, ...normalizeKeys(styleObj) });
|
|
||||||
} else if (component && component.set) {
|
|
||||||
const current = component.get && component.get('style') ? component.get('style') : {};
|
|
||||||
const safeCurrent = (current && typeof current === 'object' && !Array.isArray(current)) ? current : {};
|
|
||||||
component.set('style', { ...safeCurrent, ...normalizeKeys(styleObj) });
|
|
||||||
}
|
|
||||||
if (component && component.view && component.view.el) {
|
|
||||||
Object.entries(styleObj).forEach(([key, val]) => {
|
|
||||||
component.view.el.style[key] = val;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const pendingComponentStyle = {};
|
|
||||||
const fontSelect = addSelect([{ label: 'Schriftart', value: '' }, ...fontOptions], 'Schriftart', (value) => {
|
const fontSelect = addSelect([{ label: 'Schriftart', value: '' }, ...fontOptions], 'Schriftart', (value) => {
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
pendingComponentStyle.fontFamily = value;
|
pendingComponentStyle.fontFamily = value;
|
||||||
@@ -583,19 +639,18 @@
|
|||||||
pendingComponentStyle.fontSize = `${value}px`;
|
pendingComponentStyle.fontSize = `${value}px`;
|
||||||
applyComponentStyle({ fontSize: `${value}px` }, { preview: true });
|
applyComponentStyle({ fontSize: `${value}px` }, { preview: true });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
// Emoji-Picker entfernt (auf Wunsch) – kann spaeter als echter Picker wiederkommen.
|
|
||||||
|
|
||||||
const injectedStyle = doc.createElement('style');
|
const injectedStyle = doc.createElement('style');
|
||||||
injectedStyle.setAttribute('data-bridge-rte-style', '1');
|
injectedStyle.setAttribute('data-bridge-rte-style', '1');
|
||||||
const fontCss = (B && typeof B.RTE_FONT_FACE_CSS === 'string' && B.RTE_FONT_FACE_CSS.trim())
|
const fontCss = (this.B && typeof this.B.RTE_FONT_FACE_CSS === 'string' && this.B.RTE_FONT_FACE_CSS.trim())
|
||||||
? B.RTE_FONT_FACE_CSS.trim()
|
? this.B.RTE_FONT_FACE_CSS.trim()
|
||||||
: '';
|
: '';
|
||||||
let editorCss = '';
|
let editorCss = '';
|
||||||
try {
|
try {
|
||||||
editorCss = editor && typeof editor.getCss === 'function' ? String(editor.getCss() || '') : '';
|
editorCss = editor && typeof editor.getCss === 'function' ? String(editor.getCss() || '') : '';
|
||||||
} catch {}
|
} catch {}
|
||||||
const frameCss = collectFrameCss(editor);
|
const frameCss = this.collectFrameCss(editor);
|
||||||
injectedStyle.textContent = `${fontCss}\n${editorCss}\n${frameCss}`.trim();
|
injectedStyle.textContent = `${fontCss}\n${editorCss}\n${frameCss}`.trim();
|
||||||
if (injectedStyle.textContent) {
|
if (injectedStyle.textContent) {
|
||||||
container.appendChild(injectedStyle);
|
container.appendChild(injectedStyle);
|
||||||
@@ -611,10 +666,13 @@
|
|||||||
actions.style.display = 'flex';
|
actions.style.display = 'flex';
|
||||||
actions.style.justifyContent = 'flex-end';
|
actions.style.justifyContent = 'flex-end';
|
||||||
actions.style.gap = '8px';
|
actions.style.gap = '8px';
|
||||||
|
actions.className = 'bridge-rte-actions';
|
||||||
|
actions.setAttribute('data-bridge-rte', 'actions');
|
||||||
|
|
||||||
const cancelBtn = doc.createElement('button');
|
const cancelBtn = doc.createElement('button');
|
||||||
cancelBtn.type = 'button';
|
cancelBtn.type = 'button';
|
||||||
cancelBtn.textContent = 'Abbrechen';
|
cancelBtn.textContent = 'Abbrechen';
|
||||||
|
cancelBtn.className = 'bridge-rte-btn bridge-rte-btn-cancel';
|
||||||
cancelBtn.style.padding = '6px 12px';
|
cancelBtn.style.padding = '6px 12px';
|
||||||
cancelBtn.style.border = '1px solid #cbd5f5';
|
cancelBtn.style.border = '1px solid #cbd5f5';
|
||||||
cancelBtn.style.borderRadius = '4px';
|
cancelBtn.style.borderRadius = '4px';
|
||||||
@@ -625,6 +683,7 @@
|
|||||||
const saveBtn = doc.createElement('button');
|
const saveBtn = doc.createElement('button');
|
||||||
saveBtn.type = 'button';
|
saveBtn.type = 'button';
|
||||||
saveBtn.textContent = 'Speichern';
|
saveBtn.textContent = 'Speichern';
|
||||||
|
saveBtn.className = 'bridge-rte-btn bridge-rte-btn-save';
|
||||||
saveBtn.style.padding = '6px 12px';
|
saveBtn.style.padding = '6px 12px';
|
||||||
saveBtn.style.border = '1px solid #0ea5e9';
|
saveBtn.style.border = '1px solid #0ea5e9';
|
||||||
saveBtn.style.borderRadius = '4px';
|
saveBtn.style.borderRadius = '4px';
|
||||||
@@ -633,15 +692,15 @@
|
|||||||
saveBtn.style.cursor = 'pointer';
|
saveBtn.style.cursor = 'pointer';
|
||||||
saveBtn.addEventListener('click', () => {
|
saveBtn.addEventListener('click', () => {
|
||||||
const rawHtml = content.innerHTML || '';
|
const rawHtml = content.innerHTML || '';
|
||||||
const html = sanitizeInlineHtml(rawHtml, content.textContent || '');
|
const html = this.sanitizeInlineHtml(rawHtml, content.textContent || '');
|
||||||
component.__bridgeRteLastContent = html;
|
this.lastContent.set(component, html);
|
||||||
logConsoleSnapshot(editor, component, 'before-save');
|
this.logConsoleSnapshot(editor, component, 'before-save');
|
||||||
const forceApply = () => {
|
const forceApply = () => {
|
||||||
if (Object.keys(pendingComponentStyle).length) {
|
if (Object.keys(pendingComponentStyle).length) {
|
||||||
applyComponentStyle(pendingComponentStyle);
|
applyComponentStyle(pendingComponentStyle);
|
||||||
}
|
}
|
||||||
applyContentToComponent(editor, component, html);
|
this.applyContentToComponent(editor, component, html);
|
||||||
logConsoleSnapshot(editor, component, 'after-save');
|
this.logConsoleSnapshot(editor, component, 'after-save');
|
||||||
};
|
};
|
||||||
forceApply();
|
forceApply();
|
||||||
setTimeout(forceApply, 0);
|
setTimeout(forceApply, 0);
|
||||||
@@ -656,8 +715,13 @@
|
|||||||
closeModal();
|
closeModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (typeof ui.buildFooter === 'function') {
|
||||||
|
try { ui.buildFooter({ ...ctx, actions, cancelBtn, saveBtn }); } catch {}
|
||||||
|
}
|
||||||
|
if (!ui.overrideFooter) {
|
||||||
actions.appendChild(cancelBtn);
|
actions.appendChild(cancelBtn);
|
||||||
actions.appendChild(saveBtn);
|
actions.appendChild(saveBtn);
|
||||||
|
}
|
||||||
container.appendChild(actions);
|
container.appendChild(actions);
|
||||||
|
|
||||||
modal.setTitle('Richtext Editor');
|
modal.setTitle('Richtext Editor');
|
||||||
@@ -666,14 +730,6 @@
|
|||||||
if (mdl && mdl.set) {
|
if (mdl && mdl.set) {
|
||||||
mdl.set('closeOnEsc', false);
|
mdl.set('closeOnEsc', false);
|
||||||
mdl.set('closeOnClick', false);
|
mdl.set('closeOnClick', false);
|
||||||
if (!mdl.__bridgeRteGuarded && typeof mdl.on === 'function') {
|
|
||||||
mdl.__bridgeRteGuarded = true;
|
|
||||||
mdl.on('change:open', () => {
|
|
||||||
if (!mdl.get('open') && !editor.__bridgeRteAllowClose) {
|
|
||||||
mdl.set('open', true, { silent: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (modal.el) {
|
if (modal.el) {
|
||||||
modal.el.classList.add('bridge-rte-modal');
|
modal.el.classList.add('bridge-rte-modal');
|
||||||
@@ -692,11 +748,14 @@
|
|||||||
if (!styleEl.parentNode) {
|
if (!styleEl.parentNode) {
|
||||||
document.head.appendChild(styleEl);
|
document.head.appendChild(styleEl);
|
||||||
}
|
}
|
||||||
|
if (typeof ui.afterOpen === 'function') {
|
||||||
|
try { ui.afterOpen(ctx); } catch {}
|
||||||
|
}
|
||||||
modal.open();
|
modal.open();
|
||||||
};
|
}
|
||||||
|
|
||||||
const ensureTextToolbarButton = (component) => {
|
ensureTextToolbarButton(component) {
|
||||||
if (!isTextLike(component) || !component.get) return;
|
if (!this.isTextLike(component) || !component.get) return;
|
||||||
const toolbar = component.get('toolbar') || [];
|
const toolbar = component.get('toolbar') || [];
|
||||||
if (toolbar.some((item) => item && item.command === 'bridge-open-richtext')) return;
|
if (toolbar.some((item) => item && item.command === 'bridge-open-richtext')) return;
|
||||||
toolbar.push({
|
toolbar.push({
|
||||||
@@ -705,41 +764,18 @@
|
|||||||
command: 'bridge-open-richtext',
|
command: 'bridge-open-richtext',
|
||||||
});
|
});
|
||||||
component.set('toolbar', toolbar);
|
component.set('toolbar', toolbar);
|
||||||
};
|
}
|
||||||
|
|
||||||
const setupRichTextEditor = (editor) => {
|
setupEditor(editor) {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
if (!editor.__bridgeGetHtmlPatched) {
|
this.editor = editor;
|
||||||
editor.__bridgeGetHtmlPatched = true;
|
|
||||||
const originalGetHtml = editor.getHtml ? editor.getHtml.bind(editor) : null;
|
|
||||||
if (originalGetHtml) {
|
|
||||||
editor.getHtml = function (...args) {
|
|
||||||
let html = originalGetHtml(...args);
|
|
||||||
try {
|
|
||||||
const wrapper = editor.getWrapper && editor.getWrapper();
|
|
||||||
if (wrapper && wrapper.find) {
|
|
||||||
const candidates = wrapper.find('[data-gjs-type="text"], [data-gjs-type="link"], [data-gjs-type="button"]');
|
|
||||||
candidates.forEach((model) => {
|
|
||||||
const content = model && model.get ? String(model.get('content') || '') : '';
|
|
||||||
const id = model && (model.getId ? model.getId() : model.get && model.get('id'));
|
|
||||||
const tag = (model && model.get && model.get('tagName') ? model.get('tagName') : (model && model.get && model.get('tag'))) || 'div';
|
|
||||||
if (!content || !id) return;
|
|
||||||
const tagName = String(tag).toLowerCase();
|
|
||||||
const rx = new RegExp(`<${tagName}([^>]*\\bid=["']${id}["'][^>]*)>([\\s\\S]*?)<\\/${tagName}>`, 'i');
|
|
||||||
html = html.replace(rx, `<${tagName}$1>${content}</${tagName}>`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
return html;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 = {}) => {
|
||||||
if (sender && sender.set) sender.set('active', 0);
|
if (sender && sender.set) sender.set('active', 0);
|
||||||
const component = opts.component || ed.getSelected();
|
const component = opts.component || ed.getSelected();
|
||||||
openRichTextModal(ed, component);
|
this.openRichTextModal(ed, component);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -753,27 +789,44 @@
|
|||||||
cfg.richTextEditor.actions = actions;
|
cfg.richTextEditor.actions = actions;
|
||||||
|
|
||||||
const restoreIfCollapsed = (model) => {
|
const restoreIfCollapsed = (model) => {
|
||||||
if (!model || model.__bridgeRteRestoring) return;
|
if (!model || this.restoring.has(model)) return;
|
||||||
const last = model.__bridgeRteLastContent;
|
const last = this.lastContent.get(model);
|
||||||
if (!last || !model.get) return;
|
if (!last || !model.get) return;
|
||||||
const current = String(model.get('content') || '').trim();
|
const current = String(model.get('content') || '').trim();
|
||||||
if (!current || current === '<div></div>' || current === '<div><br></div>' || current === '<div><br></div><br>') {
|
if (!current || current === '<div></div>' || current === '<div><br></div>' || current === '<div><br></div><br>') {
|
||||||
model.__bridgeRteRestoring = true;
|
this.restoring.add(model);
|
||||||
logConsoleSnapshot(editor, model, 'restore-start');
|
this.logConsoleSnapshot(editor, model, 'restore-start');
|
||||||
applyContentToComponent(editor, model, last);
|
this.applyContentToComponent(editor, model, last);
|
||||||
logConsoleSnapshot(editor, model, 'restore-end');
|
this.logConsoleSnapshot(editor, model, 'restore-end');
|
||||||
model.__bridgeRteRestoring = false;
|
this.restoring.delete(model);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
editor.on('component:update', (model) => restoreIfCollapsed(model));
|
editor.on('component:update', (model) => restoreIfCollapsed(model));
|
||||||
editor.on('component:input', (model) => restoreIfCollapsed(model));
|
editor.on('component:input', (model) => restoreIfCollapsed(model));
|
||||||
editor.on('component:selected', (model) => ensureTextToolbarButton(model));
|
editor.on('component:selected', (model) => this.ensureTextToolbarButton(model));
|
||||||
editor.on('component:add', (model) => ensureTextToolbarButton(model));
|
editor.on('component:add', (model) => this.ensureTextToolbarButton(model));
|
||||||
editor.on('component:dblclick', (model) => {
|
editor.on('component:dblclick', (model) => {
|
||||||
if (isTextLike(model)) openRichTextModal(editor, model);
|
if (this.isTextLike(model)) this.openRichTextModal(editor, model);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(editor) {
|
||||||
|
this.setupEditor(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupRichTextEditor = (editor) => {
|
||||||
|
if (!editor) return;
|
||||||
|
let instance = editorInstances.get(editor);
|
||||||
|
if (!instance) {
|
||||||
|
instance = new BridgeRTE(window.BridgeParts || (window.BridgeParts = {}));
|
||||||
|
editorInstances.set(editor, instance);
|
||||||
|
}
|
||||||
|
instance.mount(editor);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const B = window.BridgeParts || (window.BridgeParts = {});
|
||||||
|
B.BridgeRTE = BridgeRTE;
|
||||||
B.setupRichTextEditor = setupRichTextEditor;
|
B.setupRichTextEditor = setupRichTextEditor;
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -177,6 +177,15 @@ export function initEditor() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSerializedHtml(editor, win) {
|
||||||
|
if (!editor) return '';
|
||||||
|
const BridgeRTE = win?.BridgeParts?.BridgeRTE || win?.BridgeRTE || null;
|
||||||
|
if (BridgeRTE && typeof BridgeRTE.serializeHtml === 'function') {
|
||||||
|
return BridgeRTE.serializeHtml(editor);
|
||||||
|
}
|
||||||
|
return (typeof editor.getHtml === 'function') ? editor.getHtml() : '';
|
||||||
|
}
|
||||||
|
|
||||||
async function buildCurrentSnapshot() {
|
async function buildCurrentSnapshot() {
|
||||||
if (currentEditorType === 'craftjs') {
|
if (currentEditorType === 'craftjs') {
|
||||||
return buildSnapshot({
|
return buildSnapshot({
|
||||||
@@ -191,7 +200,7 @@ export function initEditor() {
|
|||||||
const win = iframe?.contentWindow;
|
const win = iframe?.contentWindow;
|
||||||
const fontCss = win?.BridgeParts?.RTE_FONT_FACE_CSS || '';
|
const fontCss = win?.BridgeParts?.RTE_FONT_FACE_CSS || '';
|
||||||
const cssPayload = (fontCss ? fontCss + '\n' : '') + (editor.getCss() || '');
|
const cssPayload = (fontCss ? fontCss + '\n' : '') + (editor.getCss() || '');
|
||||||
const htmlContent = (editor.getHtml() || '') + '<style>' + cssPayload + '</style>';
|
const htmlContent = (getSerializedHtml(editor, win) || '') + '<style>' + cssPayload + '</style>';
|
||||||
let jsonRaw = '';
|
let jsonRaw = '';
|
||||||
try {
|
try {
|
||||||
jsonRaw = JSON.stringify(editor.getProjectData());
|
jsonRaw = JSON.stringify(editor.getProjectData());
|
||||||
@@ -440,7 +449,7 @@ export function initEditor() {
|
|||||||
|
|
||||||
const ed = win.__gjs || (win.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
|
const ed = win.__gjs || (win.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
|
||||||
if (ed && typeof ed.getHtml === 'function') {
|
if (ed && typeof ed.getHtml === 'function') {
|
||||||
const html = ed.getHtml();
|
const html = getSerializedHtml(ed, win);
|
||||||
const css = (typeof ed.getCss === 'function') ? ed.getCss() : '';
|
const css = (typeof ed.getCss === 'function') ? ed.getCss() : '';
|
||||||
return css ? `<style>${css}</style>\n${html}` : html;
|
return css ? `<style>${css}</style>\n${html}` : html;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user