This commit is contained in:
2026-01-15 01:21:59 +01:00
parent d3c68bda4d
commit e98b4b4a44
6 changed files with 352 additions and 14 deletions

View File

@@ -301,18 +301,21 @@
};
const ensureTextToolbarButton = (editor, component) => {
if (!component || !component.is || !component.is('text')) return;
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({
attributes: { class: 'fa fa-pencil', title: 'Richtext bearbeiten' },
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>',
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')) {
if (!component || !component.is || !(component.is('text') || component.is('button') || component.is('link'))) {
log('RTE', 'Bitte zuerst ein Text-Element auswaehlen.', '#888');
return;
}
@@ -480,7 +483,10 @@
cancelBtn.style.borderRadius = '4px';
cancelBtn.style.background = '#f8fafc';
cancelBtn.style.cursor = 'pointer';
cancelBtn.addEventListener('click', () => modal.close());
cancelBtn.addEventListener('click', () => {
editor.__bridgeRteModalOpen = false;
modal.close();
});
const saveBtn = doc.createElement('button');
saveBtn.type = 'button';
@@ -509,6 +515,7 @@
editor.trigger('component:update', component);
}
} catch {}
editor.__bridgeRteModalOpen = false;
modal.close();
});
@@ -630,10 +637,11 @@
'bridge-placeholder',
]);
const isTextLike = (model) => !!(model && model.is && (model.is('text') || model.is('button') || model.is('link')));
let lastTextSelection = { id: null, ts: 0 };
editor.on('component:selected', (model) => {
ensureTextToolbarButton(editor, model);
if (!model || !model.is || !model.is('text')) return;
if (!isTextLike(model)) return;
const now = Date.now();
if (lastTextSelection.id === model.cid && (now - lastTextSelection.ts) < 450) {
openRichTextModal(editor, model);
@@ -642,7 +650,7 @@
});
editor.on('component:add', (model) => ensureTextToolbarButton(editor, model));
editor.on('component:dblclick', (model) => {
if (model && model.is && model.is('text')) {
if (isTextLike(model)) {
openRichTextModal(editor, model);
}
});
@@ -652,13 +660,193 @@
body.__bridgeRteDblclickBound = true;
body.addEventListener('dblclick', () => {
const selected = editor.getSelected && editor.getSelected();
if (selected && selected.is && selected.is('text')) {
if (isTextLike(selected)) {
openRichTextModal(editor, selected);
}
}, true);
});
};
const setupTableBuilder = (editor) => {
const domc = editor.DomComponents;
if (!domc) return;
const icon = (path) => `<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="${path}" fill="currentColor"/></svg>`;
const tableIcon = icon('M3 4h18v16H3V4zm2 2v3h6V6H5zm8 0v3h6V6h-6zM5 11v3h6v-3H5zm8 0v3h6v-3h-6zM5 16v2h6v-2H5zm8 0v2h6v-2h-6z');
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 += '<tr>';
for (let c = 0; c < safeCols; c++) {
const existingVal = existing?.[r]?.[c] || '';
const label = existingVal || (r === 0 ? `Spalte ${String.fromCharCode(65 + c)}` : `Zeile ${r} / ${c + 1}`);
if (r === 0) {
html += `<th style="${headStyle}">${label}</th>`;
} else {
html += `<td style="${cellStyle}">${label}</td>`;
}
}
html += '</tr>';
}
return html;
};
const openTableModal = (component) => {
if (!component) return;
const modal = editor.Modal;
if (!modal) return;
const attrs = component.getAttributes ? component.getAttributes() : {};
const rows = Number(attrs['data-bridge-rows'] || 3) || 3;
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '12px';
container.style.minWidth = '280px';
const label = document.createElement('label');
label.textContent = 'Anzahl Zeilen';
label.style.fontSize = '13px';
label.style.fontWeight = '600';
const input = document.createElement('input');
input.type = 'number';
input.min = '1';
input.max = '20';
input.value = String(rows);
input.style.width = '100%';
input.style.padding = '6px 8px';
input.style.border = '1px solid #cbd5f5';
input.style.borderRadius = '4px';
label.appendChild(input);
container.appendChild(label);
const actions = document.createElement('div');
actions.style.display = 'flex';
actions.style.justifyContent = 'flex-end';
actions.style.gap = '8px';
const cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.textContent = 'Abbrechen';
cancelBtn.className = 'btn';
cancelBtn.addEventListener('click', () => modal.close());
const saveBtn = document.createElement('button');
saveBtn.type = 'button';
saveBtn.textContent = 'Uebernehmen';
saveBtn.className = 'btn';
saveBtn.addEventListener('click', () => {
const nextRows = Math.max(1, Math.min(20, Number(input.value) || 1));
const existing = collectTableCells(component);
const html = buildTableHtml(nextRows, 2, existing);
component.addAttributes && component.addAttributes({
'data-bridge-rows': String(nextRows),
'data-bridge-cols': '2',
});
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('Tabelle konfigurieren');
modal.setContent(container);
const mdl = modal.getModel && modal.getModel();
if (mdl && typeof mdl.set === 'function') {
mdl.set('closeOnEsc', false);
mdl.set('closeOnClick', false);
}
modal.open();
};
if (!editor.Commands.get('bridge-table:edit')) {
editor.Commands.add('bridge-table:edit', {
run(ed, sender, opts = {}) {
if (sender && sender.set) sender.set('active', 0);
const component = opts.component || ed.getSelected();
if (component && component.is && component.is('bridge-table')) {
openTableModal(component);
}
},
});
}
if (!domc.getType('bridge-table')) {
const baseType = domc.getType('table') || domc.getType('default');
const BaseModel = baseType.model;
const BaseView = baseType.view;
domc.addType('bridge-table', {
model: BaseModel.extend({
defaults: Object.assign({}, (typeof BaseModel.prototype.defaults === 'function' ? BaseModel.prototype.defaults() : (BaseModel.prototype.defaults || {})), {
tagName: 'table',
attributes: {
'data-bridge-table': '1',
'data-bridge-rows': '3',
'data-bridge-cols': '2',
role: 'presentation',
},
toolbar: [
{
label: tableIcon,
command: 'bridge-table:edit',
attributes: { title: 'Tabelle bearbeiten' },
},
],
}),
}),
view: BaseView,
isComponent: el => {
if (el.tagName === 'TABLE' && el.getAttribute('data-bridge-table')) {
return { type: 'bridge-table' };
}
return '';
},
});
}
editor.on('component:selected', (model) => {
if (model && model.is && model.is('bridge-table')) {
const toolbar = model.get('toolbar') || [];
const exists = toolbar.some(btn => btn && btn.command === 'bridge-table:edit');
if (!exists) {
toolbar.push({
label: tableIcon,
command: 'bridge-table:edit',
attributes: { title: 'Tabelle bearbeiten' },
});
model.set('toolbar', toolbar);
}
}
});
editor.on('component:dblclick', (model) => {
if (model && model.is && model.is('bridge-table')) {
openTableModal(model);
}
});
};
const loadDynamicFonts = async () => {
try {
const base = B.API_KERNEL_URL || '/api.php';
@@ -746,6 +934,7 @@
}
setupRichTextEditor(ed);
setupTableBuilder(ed);
loadDynamicFonts();
// Entfernt: jegliche Blur/RTE-Handler, die Inhalte verändern.