This commit is contained in:
2025-12-10 22:22:00 +01:00
parent d978b64d34
commit f9454605e1

View File

@@ -23,6 +23,7 @@
const TARGET_CAT_ID = 'placeholders'; const TARGET_CAT_ID = 'placeholders';
const PLACEHOLDER_COMPONENT = 'placeholder-block'; const PLACEHOLDER_COMPONENT = 'placeholder-block';
const ALL_PLACEHOLDER_BLOCK_IDS = []; const ALL_PLACEHOLDER_BLOCK_IDS = [];
const INLINE_PLACEHOLDER_CLASS = 'bridge-placeholder-inline';
const placeholderSchemaStore = { const placeholderSchemaStore = {
promise: null, promise: null,
@@ -30,6 +31,33 @@
status: null, status: null,
statusPromise: null, statusPromise: null,
}; };
let inlineStyleInjected = false;
const ensureInlinePlaceholderStyles = () => {
if (inlineStyleInjected || typeof document === 'undefined') return;
inlineStyleInjected = true;
const style = document.createElement('style');
style.id = 'bridge-placeholder-inline-style';
style.textContent = `
.${INLINE_PLACEHOLDER_CLASS}{
display:inline-flex;
align-items:center;
gap:4px;
padding:2px 8px;
border:1px dashed #94a3b8;
border-radius:6px;
background:#f1f5f9;
font-family:monospace;
font-size:12px;
cursor:pointer;
user-select:none;
}
.${INLINE_PLACEHOLDER_CLASS}:hover{
background:#e2e8f0;
}
`;
document.head && document.head.appendChild(style);
};
const createEl = (tag, props = {}, children = []) => { const createEl = (tag, props = {}, children = []) => {
const el = document.createElement(tag); const el = document.createElement(tag);
@@ -82,6 +110,15 @@
} }
}; };
const safeRemoveComponent = (component) => {
if (!component || typeof component.remove !== 'function') return;
try {
component.remove();
} catch (err) {
log('PLACEHOLDER WARN', `Komponente konnte nicht entfernt werden: ${err && err.message ? err.message : err}`, '#b45309');
}
};
const refreshPlaceholderComponent = (component) => { const refreshPlaceholderComponent = (component) => {
if (!component) return; if (!component) return;
if (typeof component.updatePlaceholderState === 'function') { if (typeof component.updatePlaceholderState === 'function') {
@@ -116,6 +153,8 @@
const attrs = [ const attrs = [
`data-gjs-type="${PLACEHOLDER_COMPONENT}"`, `data-gjs-type="${PLACEHOLDER_COMPONENT}"`,
`data-placeholder-type="${type}"`, `data-placeholder-type="${type}"`,
`contenteditable="false"`,
`class="${INLINE_PLACEHOLDER_CLASS}"`,
]; ];
if (type === 'database') { if (type === 'database') {
attrs.push(`data-placeholder-table="${payload.table || ''}"`); attrs.push(`data-placeholder-table="${payload.table || ''}"`);
@@ -181,16 +220,58 @@
return true; return true;
}; };
const captureRteSelection = (rteInstance) => {
const doc = rteInstance && rteInstance.doc
? rteInstance.doc
: (rteInstance && rteInstance.el && rteInstance.el.ownerDocument) || document;
if (!doc || !doc.getSelection) return null;
const sel = doc.getSelection();
if (!sel || !sel.rangeCount) return null;
return {
doc,
range: sel.getRangeAt(0).cloneRange()
};
};
const restoreRteSelection = (snapshot) => {
if (!snapshot || !snapshot.doc || !snapshot.range) return false;
const sel = snapshot.doc.getSelection && snapshot.doc.getSelection();
if (!sel) return false;
try {
sel.removeAllRanges();
sel.addRange(snapshot.range);
return true;
} catch (err) {
log('PLACEHOLDER WARN', `Auswahl konnte nicht wiederhergestellt werden: ${err && err.message ? err.message : err}`, '#b45309');
return false;
}
};
const openPlaceholderModal = (editor, component, opts = {}) => { const openPlaceholderModal = (editor, component, opts = {}) => {
if (!editor) return; if (!editor) return;
const modal = editor.Modal; const modal = editor.Modal;
if (!modal) return; if (!modal) return;
ensureInlinePlaceholderStyles();
if (component && (!component.is || !component.is(PLACEHOLDER_COMPONENT))) { if (component && (!component.is || !component.is(PLACEHOLDER_COMPONENT))) {
log('PLACEHOLDER WARN', 'openPlaceholderModal wurde ohne gültige Placeholder-Komponente aufgerufen.', '#b45309'); log('PLACEHOLDER WARN', 'openPlaceholderModal wurde ohne gültige Placeholder-Komponente aufgerufen.', '#b45309');
return; return;
} }
let didSave = false;
let didCancel = false;
const fireCancel = (reason) => {
if (didSave || didCancel) return;
didCancel = true;
if (typeof opts.onCancel === 'function') {
try {
opts.onCancel({ reason, component, modal });
} catch (err) {
log('PLACEHOLDER WARN', `onCancel Fehler: ${err && err.message ? err.message : err}`, '#b45309');
}
}
};
const attrs = component && component.getAttributes ? component.getAttributes() : (opts.initial || {}); const attrs = component && component.getAttributes ? component.getAttributes() : (opts.initial || {});
const initialType = attrs['data-placeholder-type'] || 'custom'; const initialType = attrs['data-placeholder-type'] || 'custom';
const initialKey = attrs['data-placeholder-key'] || 'UEBERSCHRIFT'; const initialKey = attrs['data-placeholder-key'] || 'UEBERSCHRIFT';
@@ -348,6 +429,7 @@
cancelBtn.addEventListener('click', (e) => { cancelBtn.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
fireCancel('cancel-button');
modal.close(); modal.close();
}); });
@@ -377,6 +459,7 @@
}); });
} }
refreshPlaceholderComponent(component); refreshPlaceholderComponent(component);
component.__bridgePlaceholderNew = false;
return true; return true;
}; };
@@ -422,6 +505,7 @@
return; return;
} }
} }
didSave = true;
modal.close(); modal.close();
}); });
@@ -432,6 +516,20 @@
modal.setTitle('Placeholder konfigurieren'); modal.setTitle('Placeholder konfigurieren');
modal.setContent(container); modal.setContent(container);
if (typeof modal.onceClose === 'function') {
modal.onceClose(() => fireCancel('modal-close'));
} else if (modal.getModel && typeof modal.getModel === 'function') {
const mdl = modal.getModel();
if (mdl && typeof mdl.on === 'function') {
const handler = () => {
if (!mdl.get('open')) {
mdl.off && mdl.off('change:open', handler);
fireCancel('modal-close');
}
};
mdl.on && mdl.on('change:open', handler);
}
}
modal.open(); modal.open();
if (component && editor.getSelected && editor.getSelected() !== component) { if (component && editor.getSelected && editor.getSelected() !== component) {
editor.select && editor.select(component); editor.select && editor.select(component);
@@ -452,8 +550,20 @@
log('PLACEHOLDER INFO', 'Bitte zuerst ein Text-Element auswählen.', '#888'); log('PLACEHOLDER INFO', 'Bitte zuerst ein Text-Element auswählen.', '#888');
return; return;
} }
const selectionSnapshot = captureRteSelection(rteInstance);
openPlaceholderModal(editor, null, { openPlaceholderModal(editor, null, {
onCancel: () => {
if (selectionSnapshot) {
restoreRteSelection(selectionSnapshot);
}
if (rteInstance && typeof rteInstance.focus === 'function') {
setTimeout(() => rteInstance.focus(), 0);
}
},
onSubmit: (payload) => { onSubmit: (payload) => {
if (selectionSnapshot) {
restoreRteSelection(selectionSnapshot);
}
const html = buildPlaceholderHTML(payload); const html = buildPlaceholderHTML(payload);
let inserted = false; let inserted = false;
if (insertPlaceholderIntoSelection(rteInstance, html)) { if (insertPlaceholderIntoSelection(rteInstance, html)) {
@@ -589,6 +699,8 @@
attributes: { attributes: {
'data-placeholder-type': 'custom', 'data-placeholder-type': 'custom',
'data-placeholder-key': 'UEBERSCHRIFT', 'data-placeholder-key': 'UEBERSCHRIFT',
'contenteditable': 'false',
'class': INLINE_PLACEHOLDER_CLASS,
}, },
traits: [ traits: [
{ {
@@ -645,6 +757,7 @@
const ensurePlaceholderComponent = (editor) => { const ensurePlaceholderComponent = (editor) => {
const domc = editor.DomComponents; const domc = editor.DomComponents;
if (domc.getType(PLACEHOLDER_COMPONENT)) return; if (domc.getType(PLACEHOLDER_COMPONENT)) return;
ensureInlinePlaceholderStyles();
const baseType = domc.getType('text') || domc.getType('default') || {}; const baseType = domc.getType('text') || domc.getType('default') || {};
const BaseModel = baseType.model || editor.DomComponents.Component; const BaseModel = baseType.model || editor.DomComponents.Component;
@@ -662,10 +775,12 @@
updatePlaceholderState() { updatePlaceholderState() {
const attrs = this.getAttributes(); const attrs = this.getAttributes();
const type = attrs['data-placeholder-type'] || 'custom'; if (!attrs['data-placeholder-type']) {
if (type === 'database' && placeholderSchemaStore.tables.length === 0) {
this.addAttributes({ 'data-placeholder-type': 'custom' }); this.addAttributes({ 'data-placeholder-type': 'custom' });
} }
if (attrs['contenteditable'] !== 'false') {
this.addAttributes({ 'contenteditable': 'false' });
}
this.updateTraitVisibility(); this.updateTraitVisibility();
this.updateSchemaTraits(); this.updateSchemaTraits();
this.updateLabel(); this.updateLabel();
@@ -711,9 +826,12 @@
if (columnTrait) { if (columnTrait) {
const attrs = this.getAttributes(); const attrs = this.getAttributes();
const tableName = (attrs['data-placeholder-table'] || '').toLowerCase(); const tableName = (attrs['data-placeholder-table'] || '').toLowerCase();
const table = tables.find(function (tbl) { return tbl.name.toLowerCase() === tableName; }); const table = tables.find(function (tbl) {
const colOpts = table return (tbl.name || '').toLowerCase() === tableName;
? table.columns.map(function (col) { return { id: col.name, label: col.name + ' (' + col.type + ')' }; }) });
const columns = table && Array.isArray(table.columns) ? table.columns : [];
const colOpts = columns.length
? columns.map(function (col) { return { id: col.name, label: col.name + (col.type ? ' (' + col.type + ')' : '') }; })
: [{ id: '', label: table ? 'Keine Felder' : 'Feld wählen', disabled: !table }]; : [{ id: '', label: table ? 'Keine Felder' : 'Feld wählen', disabled: !table }];
setTraitOptions(columnTrait, colOpts); setTraitOptions(columnTrait, colOpts);
} }
@@ -745,7 +863,11 @@
render() { render() {
BaseView.prototype.render.apply(this, arguments); BaseView.prototype.render.apply(this, arguments);
this.el.classList.add('placeholder-block'); this.el.classList.add('placeholder-block');
this.el.style.display = 'inline-block'; this.el.classList.add(INLINE_PLACEHOLDER_CLASS);
this.el.setAttribute('contenteditable', 'false');
this.el.style.display = 'inline-flex';
this.el.style.alignItems = 'center';
this.el.style.gap = '4px';
this.el.style.padding = '2px 8px'; this.el.style.padding = '2px 8px';
this.el.style.border = '1px dashed #94a3b8'; this.el.style.border = '1px dashed #94a3b8';
this.el.style.borderRadius = '6px'; this.el.style.borderRadius = '6px';
@@ -848,7 +970,14 @@
if (window.__GJS_IS_PARSING) return; if (window.__GJS_IS_PARSING) return;
if (cmp.__bridgePlaceholderPrompted) return; if (cmp.__bridgePlaceholderPrompted) return;
cmp.__bridgePlaceholderPrompted = true; cmp.__bridgePlaceholderPrompted = true;
setTimeout(() => openPlaceholderModal(editor, cmp), 50); cmp.__bridgePlaceholderNew = true;
setTimeout(() => openPlaceholderModal(editor, cmp, {
onCancel: () => {
if (cmp.__bridgePlaceholderNew) {
safeRemoveComponent(cmp);
}
}
}), 50);
}); });
} }
@@ -865,25 +994,40 @@
content: `<span data-gjs-type="${PLACEHOLDER_COMPONENT}" data-placeholder-type="custom" data-placeholder-key="UEBERSCHRIFT">{{UEBERSCHRIFT}}</span>` content: `<span data-gjs-type="${PLACEHOLDER_COMPONENT}" data-placeholder-type="custom" data-placeholder-key="UEBERSCHRIFT">{{UEBERSCHRIFT}}</span>`
}); });
fetchPlaceholderSchema() const ensureDbBlock = (tables) => {
.then(tables => { const fallback = { table: 'tabelle', column: 'feld' };
if (!tables || !tables.length) { let tableName = fallback.table;
log('PLACEHOLDER INFO', 'Keine Tabellen DB Placeholder Block wird nicht angezeigt.', '#888'); let columnName = fallback.column;
return; if (Array.isArray(tables) && tables.length) {
const tableWithColumns = tables.find(tbl => Array.isArray(tbl.columns) && tbl.columns.length) || tables[0];
tableName = tableWithColumns && tableWithColumns.name ? tableWithColumns.name : fallback.table;
const firstColumn = tableWithColumns && Array.isArray(tableWithColumns.columns) && tableWithColumns.columns[0];
columnName = firstColumn && firstColumn.name ? firstColumn.name : fallback.column;
} }
const firstTable = tables[0] || {}; const payload = { type: 'database', table: tableName, column: columnName };
const tableName = firstTable.name || 'tabelle'; const content = buildPlaceholderHTML(payload);
const columns = Array.isArray(firstTable.columns) ? firstTable.columns : []; const blockId = 'cust-placeholder-db';
const firstColumn = columns.length ? columns[0].name : 'feld'; const label = '🗄️ Placeholder (DB)';
const placeholderLabel = (tableName + '.' + firstColumn).toUpperCase(); const existing = typeof bm.get === 'function' ? bm.get(blockId) : null;
addOnce(bm, 'cust-placeholder-db', { if (existing && typeof existing.set === 'function') {
id: 'cust-placeholder-db', existing.set('content', content);
label: '🗄️ Placeholder (DB)', existing.set('label', label);
content: `<span data-gjs-type="${PLACEHOLDER_COMPONENT}" data-placeholder-type="database" data-placeholder-table="${tableName}" data-placeholder-column="${firstColumn}">{{${placeholderLabel}}}</span>` } else {
addOnce(bm, blockId, {
id: blockId,
label,
content,
}); });
}
};
ensureDbBlock();
fetchPlaceholderSchema()
.then((tables) => {
ensureDbBlock(tables);
}) })
.catch(() => { .catch(() => {
log('PLACEHOLDER WARN', 'DB Placeholder Block ausgeblendet (Schemafehler).', '#b45309'); log('PLACEHOLDER WARN', 'DB Placeholder Schema konnte nicht geladen werden Fallback bleibt aktiv.', '#b45309');
}); });
log('SUCCESS', `Placeholder-Registrierung abgeschlossen. ${ALL_PLACEHOLDER_BLOCK_IDS.length} Blöcke erstellt.`, '#008000', 'info', true); log('SUCCESS', `Placeholder-Registrierung abgeschlossen. ${ALL_PLACEHOLDER_BLOCK_IDS.length} Blöcke erstellt.`, '#008000', 'info', true);