Files
emailtemplate.it/public/assets/js/bridge/blocks-placeholder.js
2025-12-10 22:22:00 +01:00

1049 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* /assets/js/bridge/blocks-placeholder.js (LOG-KONTROLLIERT) */
(function () {
const PluginName = 'blocks-placeholder';
const B = window.BridgeParts || (window.BridgeParts = {});
if (B.LOG_CONFIG && B.LOG_CONFIG.PLUGINS) {
B.LOG_CONFIG.PLUGINS[PluginName] = false; // Lokaler Schalter für dieses Plugin
}
const log = (type, message, color = '#FFD700', logType = 'info', force = false) => {
if (typeof B.log === 'function') {
B.log(PluginName, `[${type}] ${message}`, color, logType, force);
} else if (logType === 'error') {
console.error(`%c[${PluginName} - ${type}] %c${message}`, `color:red; font-weight:bold;`, 'color:inherit;');
}
};
log('FILE CHECK', 'Placeholder-Datei-IIFE startet.');
if (window.__PLACEHOLDER_BLOCKS_LOADED) return;
window.__PLACEHOLDER_BLOCKS_LOADED = true;
const TARGET_CAT_ID = 'placeholders';
const PLACEHOLDER_COMPONENT = 'placeholder-block';
const ALL_PLACEHOLDER_BLOCK_IDS = [];
const INLINE_PLACEHOLDER_CLASS = 'bridge-placeholder-inline';
const placeholderSchemaStore = {
promise: null,
tables: [],
status: 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 el = document.createElement(tag);
Object.entries(props || {}).forEach(([key, value]) => {
if (value === undefined || value === null) return;
if (key === 'className') {
el.className = value;
} else if (key === 'text') {
el.textContent = value;
} else if (key === 'html') {
el.innerHTML = value;
} else if (key in el) {
el[key] = value;
} else {
el.setAttribute(key, value);
}
});
(Array.isArray(children) ? children : [children]).forEach(child => {
if (child) el.appendChild(child);
});
return el;
};
const applyInputStyles = (el) => {
if (!el) return el;
el.style.width = '100%';
el.style.boxSizing = 'border-box';
el.style.padding = '6px 8px';
el.style.border = '1px solid #cbd5f5';
el.style.borderRadius = '4px';
el.style.fontSize = '13px';
el.style.fontFamily = 'inherit';
return el;
};
const syncSelectOptions = (selectEl, options, selected) => {
if (!selectEl) return;
while (selectEl.firstChild) {
selectEl.removeChild(selectEl.firstChild);
}
(options || []).forEach(opt => {
const optionEl = document.createElement('option');
optionEl.value = opt.id ?? opt.value ?? '';
optionEl.textContent = opt.label ?? opt.text ?? '';
if (opt.disabled) optionEl.disabled = true;
selectEl.appendChild(optionEl);
});
if (selected !== undefined && selected !== null) {
selectEl.value = selected;
}
};
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) => {
if (!component) return;
if (typeof component.updatePlaceholderState === 'function') {
try {
component.updatePlaceholderState();
} catch (err) {
log('PLACEHOLDER WARN', `updatePlaceholderState Fehler: ${err && err.message ? err.message : err}`, '#b45309');
}
} else if (typeof component.trigger === 'function') {
component.trigger('change:attributes');
}
if (component.view && typeof component.view.render === 'function') {
component.view.render();
}
if (component.em && typeof component.em.trigger === 'function') {
component.em.trigger('component:update', component);
}
};
const buildPlaceholderLabel = (payload) => {
const type = payload.type === 'database' ? 'database' : 'custom';
if (type === 'database') {
const table = (payload.table || 'TABELLE').toUpperCase();
const column = (payload.column || 'FELD').toUpperCase();
return `${table}.${column}`;
}
return (payload.key || 'PLATZHALTER').toUpperCase();
};
const buildPlaceholderHTML = (payload) => {
const type = payload.type === 'database' ? 'database' : 'custom';
const attrs = [
`data-gjs-type="${PLACEHOLDER_COMPONENT}"`,
`data-placeholder-type="${type}"`,
`contenteditable="false"`,
`class="${INLINE_PLACEHOLDER_CLASS}"`,
];
if (type === 'database') {
attrs.push(`data-placeholder-table="${payload.table || ''}"`);
attrs.push(`data-placeholder-column="${payload.column || ''}"`);
} else {
attrs.push(`data-placeholder-key="${payload.key || ''}"`);
}
const label = buildPlaceholderLabel(payload);
return `<span ${attrs.join(' ')}>{{${label}}}</span>`;
};
const buildField = (labelText, control) => {
const controlId = `bridge-placeholder-field-${Math.random().toString(36).slice(2)}`;
control.id = controlId;
const label = createEl('label', { htmlFor: controlId, text: labelText });
label.style.display = 'block';
label.style.fontWeight = '600';
label.style.fontSize = '13px';
label.style.marginBottom = '4px';
const wrapper = createEl('div', { className: 'bridge-placeholder-field' }, [label, control]);
wrapper.style.marginBottom = '12px';
return wrapper;
};
const reparseTextComponent = (component, editor) => {
if (!component || typeof component.components !== 'function') return;
const viewEl = component.view && component.view.el;
if (!viewEl) return;
const html = viewEl.innerHTML;
try {
component.components(html);
if (component.em && typeof component.em.trigger === 'function') {
component.em.trigger('component:update', component);
} else if (editor && typeof editor.trigger === 'function') {
editor.trigger('component:update', component);
}
} catch (err) {
log('PLACEHOLDER ERROR', `Reparsing des Text-Elements fehlgeschlagen: ${err && err.message ? err.message : err}`, 'red', 'error');
}
};
const insertPlaceholderIntoSelection = (rteInstance, html) => {
const doc = rteInstance && rteInstance.doc
? rteInstance.doc
: (rteInstance && rteInstance.el && rteInstance.el.ownerDocument) || document;
if (!doc) return false;
const sel = doc.getSelection && doc.getSelection();
if (!sel || !sel.rangeCount) return false;
const range = sel.getRangeAt(0);
if (!range) return false;
range.deleteContents();
const temp = doc.createElement('div');
temp.innerHTML = html;
const fragment = doc.createDocumentFragment();
while (temp.firstChild) {
fragment.appendChild(temp.firstChild);
}
range.insertNode(fragment);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
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 = {}) => {
if (!editor) return;
const modal = editor.Modal;
if (!modal) return;
ensureInlinePlaceholderStyles();
if (component && (!component.is || !component.is(PLACEHOLDER_COMPONENT))) {
log('PLACEHOLDER WARN', 'openPlaceholderModal wurde ohne gültige Placeholder-Komponente aufgerufen.', '#b45309');
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 initialType = attrs['data-placeholder-type'] || 'custom';
const initialKey = attrs['data-placeholder-key'] || 'UEBERSCHRIFT';
const initialTable = attrs['data-placeholder-table'] || '';
const initialColumn = attrs['data-placeholder-column'] || '';
const container = createEl('div', { className: 'bridge-placeholder-modal' });
container.style.padding = '8px 4px';
const form = createEl('form', { className: 'bridge-placeholder-form' });
form.style.display = 'flex';
form.style.flexDirection = 'column';
const typeSelect = applyInputStyles(createEl('select'));
[
{ id: 'custom', label: 'Placeholder (Text)' },
{ id: 'database', label: 'Placeholder (DB)' },
].forEach(opt => {
const optionEl = createEl('option', { value: opt.id, text: opt.label });
typeSelect.appendChild(optionEl);
});
typeSelect.value = initialType;
const keyInput = applyInputStyles(createEl('input', { type: 'text', value: initialKey }));
keyInput.placeholder = 'z.B. UEBERSCHRIFT';
const tableSelect = applyInputStyles(createEl('select'));
const columnSelect = applyInputStyles(createEl('select'));
tableSelect.disabled = true;
columnSelect.disabled = true;
const dbMessage = createEl('p', { className: 'bridge-placeholder-hint', text: '' });
dbMessage.style.fontSize = '12px';
dbMessage.style.margin = '4px 0 0';
dbMessage.style.color = '#475569';
const columnMessage = createEl('p', { className: 'bridge-placeholder-hint', text: '' });
columnMessage.style.fontSize = '12px';
columnMessage.style.margin = '4px 0 0';
columnMessage.style.color = '#475569';
const customWrap = createEl('div', { className: 'bridge-placeholder-section' }, [
buildField('Bezeichner', keyInput),
]);
const dbWrap = createEl('div', { className: 'bridge-placeholder-section' }, [
buildField('Tabelle', tableSelect),
dbMessage,
buildField('Feld', columnSelect),
columnMessage,
]);
const errorBox = createEl('p', { className: 'bridge-placeholder-error', text: '' });
errorBox.style.fontSize = '12px';
errorBox.style.color = '#b91c1c';
errorBox.style.margin = '4px 0 0';
form.appendChild(buildField('Typ', typeSelect));
form.appendChild(customWrap);
form.appendChild(dbWrap);
form.appendChild(errorBox);
const actions = createEl('div', { className: 'bridge-placeholder-actions' });
actions.style.marginTop = '16px';
actions.style.display = 'flex';
actions.style.justifyContent = 'flex-end';
actions.style.gap = '8px';
const cancelBtn = createEl('button', { type: 'button', text: 'Abbrechen' });
cancelBtn.style.padding = '6px 12px';
cancelBtn.style.border = '1px solid #cbd5f5';
cancelBtn.style.borderRadius = '4px';
cancelBtn.style.background = '#f8fafc';
cancelBtn.style.cursor = 'pointer';
const saveBtn = createEl('button', { type: 'submit', text: 'Speichern' });
saveBtn.style.padding = '6px 16px';
saveBtn.style.border = '1px solid #0ea5e9';
saveBtn.style.background = '#0ea5e9';
saveBtn.style.color = '#fff';
saveBtn.style.borderRadius = '4px';
saveBtn.style.cursor = 'pointer';
actions.appendChild(cancelBtn);
actions.appendChild(saveBtn);
form.appendChild(actions);
container.appendChild(form);
let tablesCache = placeholderSchemaStore.tables || [];
const toggleSections = () => {
const type = typeSelect.value || 'custom';
customWrap.style.display = type === 'custom' ? '' : 'none';
dbWrap.style.display = type === 'database' ? '' : 'none';
};
const populateColumns = (tableName, preferredColumn) => {
const table = (tablesCache || []).find(tbl => (tbl.name || '').toLowerCase() === (tableName || '').toLowerCase());
const columns = table && Array.isArray(table.columns) ? table.columns : [];
if (!columns.length) {
syncSelectOptions(columnSelect, [{ id: '', label: columns.length ? ' Feld wählen ' : 'Keine Felder verfügbar' }], '');
columnSelect.disabled = true;
columnMessage.textContent = table ? 'Diese Tabelle enthält keine Felder.' : 'Bitte zuerst eine Tabelle wählen.';
return;
}
const opts = columns.map(col => ({
id: col.name,
label: col.name + (col.type ? ` (${col.type})` : ''),
}));
syncSelectOptions(columnSelect, opts, preferredColumn || initialColumn || opts[0].id);
columnSelect.disabled = false;
columnMessage.textContent = '';
};
const populateTables = (tables) => {
tablesCache = Array.isArray(tables) ? tables : [];
if (!tablesCache.length) {
syncSelectOptions(tableSelect, [{ id: '', label: 'Keine Tabellen gefunden' }], '');
tableSelect.disabled = true;
columnSelect.disabled = true;
dbMessage.textContent = 'Kein Tabellen-Schema verfügbar. Bitte Bridge-Setup prüfen.';
columnMessage.textContent = '';
return;
}
const opts = tablesCache.map(tbl => ({ id: tbl.name, label: tbl.name }));
syncSelectOptions(tableSelect, opts, initialTable || opts[0].id);
tableSelect.disabled = false;
dbMessage.textContent = '';
populateColumns(tableSelect.value, initialColumn);
};
const loadTables = () => {
if (placeholderSchemaStore.tables && placeholderSchemaStore.tables.length) {
populateTables(placeholderSchemaStore.tables);
return;
}
dbMessage.textContent = 'Tabellen werden geladen …';
fetchPlaceholderSchema()
.then(populateTables)
.catch(() => {
populateTables([]);
});
};
tableSelect.addEventListener('change', () => {
populateColumns(tableSelect.value);
});
typeSelect.addEventListener('change', () => {
toggleSections();
if (typeSelect.value === 'database' && (!tablesCache || !tablesCache.length)) {
loadTables();
}
});
cancelBtn.addEventListener('click', (e) => {
e.preventDefault();
fireCancel('cancel-button');
modal.close();
});
const applyPayload = (payload) => {
if (typeof opts.onSubmit === 'function') {
const res = opts.onSubmit(payload, { component, modal });
return res !== false;
}
if (!component || typeof component.addAttributes !== 'function') {
log('PLACEHOLDER INFO', 'Keine Ziel-Komponente gefunden Placeholder-Daten werden verworfen.', '#888');
return true;
}
if (payload.type === 'custom') {
component.addAttributes({
'data-placeholder-type': 'custom',
'data-placeholder-key': payload.key,
'data-placeholder-table': '',
'data-placeholder-column': '',
});
} else {
component.addAttributes({
'data-placeholder-type': 'database',
'data-placeholder-key': '',
'data-placeholder-table': payload.table,
'data-placeholder-column': payload.column,
});
}
refreshPlaceholderComponent(component);
component.__bridgePlaceholderNew = false;
return true;
};
form.addEventListener('submit', (e) => {
e.preventDefault();
errorBox.textContent = '';
const type = typeSelect.value || 'custom';
if (type === 'custom') {
const key = (keyInput.value || '').trim();
if (!key) {
errorBox.textContent = 'Bitte einen Bezeichner eingeben.';
keyInput.focus();
return;
}
const payload = {
type: 'custom',
key,
table: '',
column: '',
};
if (!applyPayload(payload)) {
return;
}
} else {
const table = tableSelect.value || '';
const column = columnSelect.value || '';
if (!table || !column) {
errorBox.textContent = 'Bitte Tabelle und Feld auswählen.';
if (!table) {
tableSelect.focus();
} else {
columnSelect.focus();
}
return;
}
const payload = {
type: 'database',
key: '',
table,
column,
};
if (!applyPayload(payload)) {
return;
}
}
didSave = true;
modal.close();
});
toggleSections();
if (typeSelect.value === 'database') {
loadTables();
}
modal.setTitle('Placeholder konfigurieren');
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();
if (component && editor.getSelected && editor.getSelected() !== component) {
editor.select && editor.select(component);
}
};
const ensureRtePlaceholderButton = (editor) => {
const rte = editor && editor.RichTextEditor;
if (!rte || rte.__bridgePlaceholderButton) return;
rte.__bridgePlaceholderButton = true;
rte.add('bridge-placeholder', {
icon: '<i class="fa fa-bookmark"></i>',
attributes: { title: 'Placeholder einfügen' },
result: (rteInstance) => {
const target = editor && editor.getSelected && editor.getSelected();
if (!target || !target.is || !target.is('text')) {
log('PLACEHOLDER INFO', 'Bitte zuerst ein Text-Element auswählen.', '#888');
return;
}
const selectionSnapshot = captureRteSelection(rteInstance);
openPlaceholderModal(editor, null, {
onCancel: () => {
if (selectionSnapshot) {
restoreRteSelection(selectionSnapshot);
}
if (rteInstance && typeof rteInstance.focus === 'function') {
setTimeout(() => rteInstance.focus(), 0);
}
},
onSubmit: (payload) => {
if (selectionSnapshot) {
restoreRteSelection(selectionSnapshot);
}
const html = buildPlaceholderHTML(payload);
let inserted = false;
if (insertPlaceholderIntoSelection(rteInstance, html)) {
inserted = true;
} else if (rteInstance && typeof rteInstance.insertHTML === 'function') {
rteInstance.insertHTML(html);
inserted = true;
} else if (typeof document !== 'undefined' && document.execCommand) {
document.execCommand('insertHTML', false, html);
inserted = true;
} else {
log('PLACEHOLDER ERROR', 'Placeholder konnte nicht eingefügt werden (kein RTE).', 'red', 'error');
return false;
}
if (rteInstance && typeof rteInstance.focus === 'function') {
rteInstance.focus();
}
setTimeout(() => reparseTextComponent(target, editor), 0);
return true;
}
});
},
});
log('RTE', 'Placeholder-Button im RichTextEditor registriert.', '#DAA520');
};
function addOnce(bm, id, def, category = TARGET_CAT_ID) {
try {
bm.add(id, { ...def, category });
ALL_PLACEHOLDER_BLOCK_IDS.push(id);
log('BLOCK ADD', `Block '${id}' erfolgreich hinzugefügt.`, '#B8860B');
} catch (e) {
log('BLOCK ERROR', `Fehler beim Hinzufügen von Block '${id}': ${e.message}`, 'red', 'error');
}
}
const ensureBridgeAvailability = () => {
if (placeholderSchemaStore.status !== null) {
return Promise.resolve(placeholderSchemaStore.status);
}
if (placeholderSchemaStore.statusPromise) {
return placeholderSchemaStore.statusPromise;
}
const base = B.API_KERNEL_URL || '/api.php';
const sep = base.includes('?') ? '&' : '?';
const url = `${base}${sep}action=placeholders.status`;
placeholderSchemaStore.statusPromise = fetch(url, { credentials: 'include' })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => {
const available = !!(data && (data.available || (data.settings && data.settings.available)));
placeholderSchemaStore.status = available;
placeholderSchemaStore.statusPromise = null;
if (!available) {
log('PLACEHOLDER INFO', 'Bridge-Placeholders nicht konfiguriert DB-Funktionen deaktiviert.', '#64748b');
}
return available;
})
.catch(err => {
placeholderSchemaStore.status = false;
placeholderSchemaStore.statusPromise = null;
log('PLACEHOLDER WARN', `Bridge-Status konnte nicht geprüft werden: ${err && err.message ? err.message : err}`, '#b45309');
return false;
});
return placeholderSchemaStore.statusPromise;
};
const fetchPlaceholderSchema = () => {
if (placeholderSchemaStore.promise) return placeholderSchemaStore.promise;
placeholderSchemaStore.promise = ensureBridgeAvailability().then(isAvailable => {
if (!isAvailable) throw new Error('Bridge not available');
const base = B.API_KERNEL_URL || '/api.php';
const sep = base.includes('?') ? '&' : '?';
const url = `${base}${sep}action=placeholders.schema`;
return fetch(url, { credentials: 'include' })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => {
const tbls = data && Array.isArray(data.tables) ? data.tables : [];
placeholderSchemaStore.tables = tbls;
return placeholderSchemaStore.tables;
});
}).catch(err => {
placeholderSchemaStore.tables = [];
placeholderSchemaStore.promise = null;
const msg = err && err.message ? err.message : err;
if (msg === 'Bridge not available') {
log('PLACEHOLDER INFO', 'Schema-Abfrage übersprungen (keine Bridge verfügbar).', '#64748b');
} else {
log('PLACEHOLDER ERROR', `Schema konnte nicht geladen werden: ${msg}`, 'red', 'error');
}
throw err;
});
return placeholderSchemaStore.promise;
};
const getTraitByName = (model, name) => {
if (typeof model.getTrait === 'function') return model.getTrait(name);
const traits = model.get('traits');
if (!traits) return null;
if (typeof traits.where === 'function') {
const found = traits.where({ name });
return found && found[0];
}
if (Array.isArray(traits.models)) {
return traits.models.find(t => t.get && t.get('name') === name) || null;
}
return null;
};
const cloneValue = (value) => {
if (Array.isArray(value)) {
return value.map(cloneValue);
}
if (value && typeof value === 'object') {
const copy = {};
Object.keys(value).forEach(key => {
copy[key] = cloneValue(value[key]);
});
return copy;
}
return value;
};
const defaultPlaceholderProps = {
name: 'Placeholder',
tagName: 'span',
droppable: false,
attributes: {
'data-placeholder-type': 'custom',
'data-placeholder-key': 'UEBERSCHRIFT',
'contenteditable': 'false',
'class': INLINE_PLACEHOLDER_CLASS,
},
traits: [
{
type: 'select',
name: 'data-placeholder-type',
label: 'Typ',
options: [
{ id: 'custom', label: 'Allgemein' },
{ id: 'database', label: 'Datenbank' },
],
changeProp: true,
},
{
type: 'text',
name: 'data-placeholder-key',
label: 'Bezeichner',
placeholder: 'UEBERSCHRIFT',
changeProp: true,
},
{
type: 'select',
name: 'data-placeholder-table',
label: 'Tabelle',
options: [],
changeProp: true,
},
{
type: 'select',
name: 'data-placeholder-column',
label: 'Feld',
options: [],
changeProp: true,
},
],
toolbar: [
{
attributes: { class: 'fa fa-edit', title: 'Placeholder bearbeiten' },
command: 'bridge-placeholder:edit',
},
],
};
const applyPlaceholderDefaults = (model) => {
if (!model || typeof model.get !== 'function' || typeof model.set !== 'function') {
return;
}
Object.entries(defaultPlaceholderProps).forEach(([key, value]) => {
if (typeof model.get(key) === 'undefined') {
model.set(key, cloneValue(value));
}
});
};
const ensurePlaceholderComponent = (editor) => {
const domc = editor.DomComponents;
if (domc.getType(PLACEHOLDER_COMPONENT)) return;
ensureInlinePlaceholderStyles();
const baseType = domc.getType('text') || domc.getType('default') || {};
const BaseModel = baseType.model || editor.DomComponents.Component;
const BaseView = baseType.view || editor.DomComponents.View;
const PlaceholderModel = BaseModel.extend({
init() {
applyPlaceholderDefaults(this);
this.listenTo(this, 'change:attributes', this.updatePlaceholderState);
this.updatePlaceholderState();
fetchPlaceholderSchema()
.then(() => this.updateSchemaTraits())
.catch(() => this.updateSchemaTraits([]));
},
updatePlaceholderState() {
const attrs = this.getAttributes();
if (!attrs['data-placeholder-type']) {
this.addAttributes({ 'data-placeholder-type': 'custom' });
}
if (attrs['contenteditable'] !== 'false') {
this.addAttributes({ 'contenteditable': 'false' });
}
this.updateTraitVisibility();
this.updateSchemaTraits();
this.updateLabel();
},
updateTraitVisibility() {
const attrs = this.getAttributes();
const type = attrs['data-placeholder-type'] || 'custom';
const isDb = type === 'database';
const tableTrait = getTraitByName(this, 'data-placeholder-table');
const columnTrait = getTraitByName(this, 'data-placeholder-column');
const keyTrait = getTraitByName(this, 'data-placeholder-key');
if (tableTrait && tableTrait.view && tableTrait.view.el) {
tableTrait.view.el.style.display = isDb ? '' : 'none';
}
if (columnTrait && columnTrait.view && columnTrait.view.el) {
columnTrait.view.el.style.display = isDb ? '' : 'none';
}
if (keyTrait && keyTrait.view && keyTrait.view.el) {
keyTrait.view.el.style.display = isDb ? 'none' : '';
}
},
updateSchemaTraits(tablesOverride) {
const tables = Array.isArray(tablesOverride) ? tablesOverride : placeholderSchemaStore.tables;
const tableTrait = getTraitByName(this, 'data-placeholder-table');
const columnTrait = getTraitByName(this, 'data-placeholder-column');
const loading = !tablesOverride && placeholderSchemaStore.promise && !tables.length;
if (tableTrait) {
let opts;
if (tables.length) {
opts = tables.map(function (tbl) { return { id: tbl.name, label: tbl.name }; });
} else if (loading) {
opts = [{ id: '', label: 'Tabellen werden geladen…', disabled: true }];
} else {
opts = [{ id: '', label: 'Keine Tabellen verfügbar', disabled: true }];
}
setTraitOptions(tableTrait, opts);
}
if (columnTrait) {
const attrs = this.getAttributes();
const tableName = (attrs['data-placeholder-table'] || '').toLowerCase();
const table = tables.find(function (tbl) {
return (tbl.name || '').toLowerCase() === tableName;
});
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 }];
setTraitOptions(columnTrait, colOpts);
}
},
updateLabel() {
const attrs = this.getAttributes();
const type = attrs['data-placeholder-type'] || 'custom';
let label;
if (type === 'database') {
const table = attrs['data-placeholder-table'] || 'TABELLE';
const column = attrs['data-placeholder-column'] || 'FELD';
label = (table + '.' + column).toUpperCase();
} else {
label = (attrs['data-placeholder-key'] || 'PLATZHALTER').toUpperCase();
}
const text = '{{' + label + '}}';
const comps = this.components();
const onlyText = comps.length === 1 && comps.at(0).is('textnode');
if (onlyText) {
comps.at(0).set('content', text);
} else {
comps.reset([{ type: 'textnode', content: text }]);
}
}
});
const PlaceholderView = BaseView.extend({
render() {
BaseView.prototype.render.apply(this, arguments);
this.el.classList.add('placeholder-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.border = '1px dashed #94a3b8';
this.el.style.borderRadius = '6px';
this.el.style.background = '#f1f5f9';
this.el.style.fontFamily = 'monospace';
this.el.style.fontSize = '12px';
return this;
},
});
domc.addType(PLACEHOLDER_COMPONENT, {
model: PlaceholderModel,
view: PlaceholderView,
isComponent(el) {
if (el && el.hasAttribute && el.hasAttribute('data-placeholder-type')) {
return { type: PLACEHOLDER_COMPONENT };
}
return false;
},
});
};
const patchTextComponentDroppable = (component) => {
if (!component || !component.is || !component.is('text')) {
return;
}
if (component.__bridgePlaceholderDroppable) {
return;
}
component.__bridgePlaceholderDroppable = true;
const originalDroppable = component.get('droppable');
const allowFn = function (source, cmp) {
if (cmp && cmp.get && cmp.get('type') === PLACEHOLDER_COMPONENT) {
return true;
}
if (typeof originalDroppable === 'function') {
return originalDroppable.call(this, source, cmp);
}
if (typeof originalDroppable === 'undefined') {
return true;
}
return originalDroppable;
};
component.set('droppable', allowFn);
};
const ensureTextSupportsPlaceholders = (editor) => {
const wrapper = editor.getWrapper && editor.getWrapper();
if (wrapper) {
if (typeof wrapper.findType === 'function') {
wrapper.findType('text').forEach(patchTextComponentDroppable);
} else if (typeof wrapper.find === 'function') {
wrapper.find('[data-gjs-type="text"]').forEach(patchTextComponentDroppable);
}
}
editor.on('component:add', (cmp) => patchTextComponentDroppable(cmp));
};
function setTraitOptions(trait, options) {
if (!trait) return;
trait.set('options', options);
if (trait.view && typeof trait.view.render === 'function') {
trait.view.render();
}
}
function register(editor) {
log('EXECUTION', `Starte Placeholder-Registrierung für ${TARGET_CAT_ID}.`, '#DAA520');
const bm = editor.BlockManager;
ensurePlaceholderComponent(editor);
ensureTextSupportsPlaceholders(editor);
if (!editor.Commands.get('bridge-placeholder:edit')) {
editor.Commands.add('bridge-placeholder:edit', {
run(ed, sender, opts = {}) {
const component = opts.component || ed.getSelected();
if (component && component.is && component.is(PLACEHOLDER_COMPONENT)) {
openPlaceholderModal(ed, component);
} else {
log('PLACEHOLDER INFO', 'Kein Placeholder ausgewählt.', '#888');
}
},
});
}
if (!editor.__bridgePlaceholderEventsBound) {
editor.__bridgePlaceholderEventsBound = true;
editor.on('component:dblclick', (cmp) => {
if (cmp && cmp.is && cmp.is(PLACEHOLDER_COMPONENT)) {
openPlaceholderModal(editor, cmp);
return false;
}
return undefined;
});
editor.on('component:add', (cmp) => {
if (!cmp || !cmp.is || !cmp.is(PLACEHOLDER_COMPONENT)) return;
if (window.__GJS_IS_PARSING) return;
if (cmp.__bridgePlaceholderPrompted) return;
cmp.__bridgePlaceholderPrompted = true;
cmp.__bridgePlaceholderNew = true;
setTimeout(() => openPlaceholderModal(editor, cmp, {
onCancel: () => {
if (cmp.__bridgePlaceholderNew) {
safeRemoveComponent(cmp);
}
}
}), 50);
});
}
const bindRteButton = () => ensureRtePlaceholderButton(editor);
if (editor.RichTextEditor) {
bindRteButton();
} else if (typeof editor.on === 'function') {
editor.on('load', bindRteButton, { once: true });
}
addOnce(bm, 'cust-placeholder-custom', {
id: 'cust-placeholder-custom',
label: '🔖 Placeholder (Text)',
content: `<span data-gjs-type="${PLACEHOLDER_COMPONENT}" data-placeholder-type="custom" data-placeholder-key="UEBERSCHRIFT">{{UEBERSCHRIFT}}</span>`
});
const ensureDbBlock = (tables) => {
const fallback = { table: 'tabelle', column: 'feld' };
let tableName = fallback.table;
let columnName = fallback.column;
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 payload = { type: 'database', table: tableName, column: columnName };
const content = buildPlaceholderHTML(payload);
const blockId = 'cust-placeholder-db';
const label = '🗄️ Placeholder (DB)';
const existing = typeof bm.get === 'function' ? bm.get(blockId) : null;
if (existing && typeof existing.set === 'function') {
existing.set('content', content);
existing.set('label', label);
} else {
addOnce(bm, blockId, {
id: blockId,
label,
content,
});
}
};
ensureDbBlock();
fetchPlaceholderSchema()
.then((tables) => {
ensureDbBlock(tables);
})
.catch(() => {
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);
}
window.BridgeBlocksPlaceholder = {
IDS: ALL_PLACEHOLDER_BLOCK_IDS,
register,
openModal: openPlaceholderModal
};
if (B && B.registerGrapesJSPlugin && typeof register === 'function') {
B.registerGrapesJSPlugin('bridge-blocks-placeholder', register);
log('PLUGIN REGISTER', `'bridge-blocks-placeholder' erfolgreich registriert.`, '#008000');
} else {
log('CRITICAL ERROR', `BridgeParts oder registerGrapesJSPlugin fehlt! Placeholder Plugin-Registrierung gescheitert.`, 'red', 'error');
}
})();