Files
emailtemplate.it/public/assets/js/bridge/table-builder.js
2026-02-09 01:14:54 +01:00

388 lines
16 KiB
JavaScript

/* /assets/js/bridge/table-builder.js */
(function () {
const PluginName = 'bridge-table-builder';
const B = window.BridgeParts || (window.BridgeParts = {});
if (B.DISABLE_TABLE_BUILDER) return;
if (B.__tableBuilderLoaded) return;
B.__tableBuilderLoaded = true;
const log = (type, message, color = '#94a3b8', logType = 'info', force = false) => {
if (typeof B.log === 'function') {
B.log(PluginName, `[${type}] ${message}`, color, logType, force);
}
};
B.setupTableBuilder = (editor) => {
const domc = editor && 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');
if (!domc.getType || !domc.getType('bridge-table-cell')) {
domc.addType('bridge-table-cell', {
isComponent(el) {
if (!el || !el.tagName) return;
const tag = el.tagName.toUpperCase();
if (tag !== 'TD' && tag !== 'TH') return;
const table = el.closest && el.closest('table');
if (!table || table.getAttribute('data-bridge-table') !== '1') return;
return { type: 'bridge-table-cell' };
},
model: {
defaults: {
editable: true,
selectable: true,
hoverable: true,
highlightable: true,
draggable: false,
droppable: false,
copyable: true,
classes: [],
},
},
});
}
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 buildTableComponents = (rows, cols, existing) => {
const safeRows = Math.max(1, Math.min(20, Number(rows) || 1));
const safeCols = Math.max(1, Math.min(6, 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";
const tbody = {
tagName: 'tbody',
selectable: false,
hoverable: false,
draggable: false,
highlightable: false,
components: [],
};
for (let r = 0; r < safeRows; r++) {
const row = {
tagName: 'tr',
selectable: false,
hoverable: false,
draggable: false,
highlightable: false,
components: [],
};
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) {
row.components.push({
type: 'text',
tagName: 'th',
editable: true,
selectable: true,
hoverable: true,
draggable: false,
attributes: {
style: headStyle,
'data-gjs-draggable': 'false',
},
content: label,
});
} else {
row.components.push({
type: 'text',
tagName: 'td',
editable: true,
selectable: true,
hoverable: true,
draggable: false,
attributes: {
style: cellStyle,
'data-gjs-draggable': 'false',
},
content: label,
});
}
}
tbody.components.push(row);
}
return [tbody];
};
const normalizeTableComponent = (component) => {
if (!isBridgeTableComponent(component)) return false;
const attrs = (component.get && component.get('attributes')) || {};
const rows = Number(attrs['data-bridge-rows'] || 3) || 3;
const cols = Number(attrs['data-bridge-cols'] || 2) || 2;
const existing = collectTableCells(component);
const components = buildTableComponents(rows, cols, existing);
component.addAttributes && component.addAttributes({
'data-bridge-rows': String(rows),
'data-bridge-cols': String(cols),
});
component.components && component.components(components);
component.view && component.view.render && component.view.render();
return true;
};
const openTableModal = (component) => {
if (!component) return;
const modal = editor.Modal;
if (!modal) return;
const attrs = (component.get && component.get('attributes')) || {};
const rows = Number(attrs['data-bridge-rows'] || 3) || 3;
const cols = Number(attrs['data-bridge-cols'] || 2) || 2;
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '12px';
container.style.minWidth = '280px';
container.style.maxWidth = '360px';
container.style.width = '100%';
const label = document.createElement('label');
label.textContent = 'Anzahl Zeilen';
label.style.fontSize = '13px';
label.style.fontWeight = '600';
label.style.display = 'flex';
label.style.flexDirection = 'column';
label.style.gap = '6px';
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';
input.style.boxSizing = 'border-box';
label.appendChild(input);
container.appendChild(label);
const colLabel = document.createElement('label');
colLabel.textContent = 'Anzahl Spalten';
colLabel.style.fontSize = '13px';
colLabel.style.fontWeight = '600';
colLabel.style.display = 'flex';
colLabel.style.flexDirection = 'column';
colLabel.style.gap = '6px';
const colInput = document.createElement('input');
colInput.type = 'number';
colInput.min = '1';
colInput.max = '6';
colInput.value = String(cols);
colInput.style.width = '100%';
colInput.style.padding = '6px 8px';
colInput.style.border = '1px solid #cbd5f5';
colInput.style.borderRadius = '4px';
colInput.style.boxSizing = 'border-box';
colLabel.appendChild(colInput);
container.appendChild(colLabel);
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', (event) => {
event.preventDefault();
event.stopPropagation();
modal.close();
const mdl = modal.getModel && modal.getModel();
if (mdl && typeof mdl.set === 'function') {
mdl.set('open', false);
}
return false;
});
const saveBtn = document.createElement('button');
saveBtn.type = 'button';
saveBtn.textContent = 'Uebernehmen';
saveBtn.className = 'btn';
saveBtn.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const nextRows = Math.max(1, Math.min(20, Number(input.value) || 1));
const nextCols = Math.max(1, Math.min(6, Number(colInput.value) || 1));
const existing = collectTableCells(component);
const components = buildTableComponents(nextRows, nextCols, existing);
component.addAttributes && component.addAttributes({
'data-bridge-rows': String(nextRows),
'data-bridge-cols': String(nextCols),
});
if (component.components) {
component.components(components);
}
if (component.view && component.view.render) {
component.view.render();
}
modal.close();
const mdl = modal.getModel && modal.getModel();
if (mdl && typeof mdl.set === 'function') {
mdl.set('open', false);
}
});
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();
};
const isBridgeTableComponent = (model) => {
if (!model) return false;
const el = (model.view && model.view.el) || (model.getEl && model.getEl());
if (el && el.tagName && el.tagName.toLowerCase() === 'table') {
return el.getAttribute('data-bridge-table') === '1';
}
const attrs = (model.get && model.get('attributes')) || {};
return attrs && attrs['data-bridge-table'] === '1';
};
let lastClickedCellComp = null;
let tableCellClickBound = false;
const bindTableCellClick = () => {
if (tableCellClickBound) return;
const body = editor.Canvas && editor.Canvas.getBody && editor.Canvas.getBody();
if (!body) return;
tableCellClickBound = true;
body.addEventListener('click', (evt) => {
const target = evt.target && evt.target.closest ? evt.target.closest('td,th') : null;
if (!target) return;
const tableEl = target.closest && target.closest('table');
if (!tableEl || tableEl.getAttribute('data-bridge-table') !== '1') return;
const comp = editor.DomComponents && editor.DomComponents.getComponent
? editor.DomComponents.getComponent(target)
: null;
if (comp) {
lastClickedCellComp = comp;
editor.select(comp);
evt.preventDefault();
evt.stopPropagation();
}
}, true);
};
editor.on('load', bindTableCellClick);
setTimeout(bindTableCellClick, 0);
if (editor.Commands && editor.Commands.add) {
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 (isBridgeTableComponent(component)) {
openTableModal(component);
}
},
});
}
editor.on('component:selected', (model) => {
if (!model) return;
if (model.get && model.get('tagName') === 'tr') {
if (lastClickedCellComp) {
editor.select(lastClickedCellComp);
return;
}
const parent = model.parent && model.parent();
const table = parent && (parent.get && parent.get('tagName') === 'table' ? parent : parent.parent && parent.parent());
if (isBridgeTableComponent(table)) {
const cells = model.components && model.components();
const firstCell = cells && cells.length ? cells.at(0) : null;
if (firstCell) {
editor.select(firstCell);
return;
}
}
}
if (isBridgeTableComponent(model)) {
model.set && model.set({
selectable: true,
hoverable: true,
highlightable: true,
});
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:click', (model, evt) => {
if (!model || !evt || !evt.target) return;
const target = evt.target.closest && evt.target.closest('td,th');
if (!target) return;
const tableEl = target.closest && target.closest('table');
if (!tableEl || tableEl.getAttribute('data-bridge-table') !== '1') return;
const comp = editor.DomComponents && editor.DomComponents.getComponent
? editor.DomComponents.getComponent(target)
: null;
if (comp) {
lastClickedCellComp = comp;
editor.select(comp);
return;
}
const id = target.getAttribute('id');
if (id && editor.getWrapper && editor.getWrapper().find) {
const found = editor.getWrapper().find(`#${id}`);
if (found && found[0]) {
lastClickedCellComp = found[0];
editor.select(found[0]);
return;
}
}
if (model.get && model.get('tagName') === 'tr') {
const cells = model.components && model.components();
const firstCell = cells && cells.length ? cells.at(0) : null;
if (firstCell) {
lastClickedCellComp = firstCell;
editor.select(firstCell);
}
}
});
editor.on('component:add', (model) => {
try {
if (isBridgeTableComponent(model)) {
setTimeout(() => normalizeTableComponent(model), 0);
}
} catch {}
});
editor.on('component:dblclick', (model) => {
if (isBridgeTableComponent(model)) {
openTableModal(model);
}
});
log('INIT', 'Table-Builder geladen.', '#10b981');
};
})();