Files
breadcrumb-the-shire/modules/helpdesk/web/js/handover-schema-editor.js

389 lines
14 KiB
JavaScript
Raw Normal View History

/**
* Handover protocol schema editor.
*
* Renders a structured field list from JSON and serializes changes
* back to a hidden input on form submit.
*/
const FIELD_TYPES = ['heading', 'paragraph', 'text', 'textarea', 'number', 'date', 'checkbox', 'select'];
const DISPLAY_ONLY_TYPES = ['heading', 'paragraph'];
/** @type {HTMLElement|null} */
const container = document.getElementById('handover-schema-editor');
if (container) {
init(container);
}
/**
* Remove all child nodes from an element (safe alternative to setting markup).
* @param {HTMLElement} el
*/
function clearElement(el) {
while (el.firstChild) {
el.removeChild(el.firstChild);
}
}
function init(root) {
const t = JSON.parse(root.dataset.translations || '{}');
const initialFields = JSON.parse(root.dataset.initialFields || '[]');
const hiddenInput = document.getElementById('handover-schema-json');
const previewContainer = document.getElementById('handover-schema-preview');
const form = hiddenInput?.closest('form');
/** @type {Array<object>} */
let fields = initialFields.map((f) => ({ ...f, options: f.options ? f.options.map((o) => ({ ...o })) : [] }));
render();
renderPreview();
syncHiddenInput();
if (form) {
form.addEventListener('submit', () => syncHiddenInput());
}
function render() {
clearElement(root);
if (fields.length === 0) {
const empty = document.createElement('p');
empty.className = 'handover-schema-empty';
empty.textContent = t.no_fields || 'No fields defined yet.';
root.appendChild(empty);
} else {
const list = document.createElement('div');
list.className = 'handover-schema-list';
fields.forEach((field, index) => {
if (index > 0) {
list.appendChild(buildInsertDivider(index));
}
list.appendChild(buildFieldRow(field, index));
});
root.appendChild(list);
}
root.appendChild(buildInsertDivider(fields.length));
}
function buildInsertDivider(insertIndex) {
const divider = document.createElement('div');
divider.className = 'handover-schema-insert-divider';
const button = document.createElement('button');
button.type = 'button';
button.className = 'secondary outline small handover-schema-insert-button';
button.setAttribute('data-tooltip', t.add_field || 'Add field');
button.setAttribute('aria-label', t.add_field || 'Add field');
button.appendChild(createIcon('bi-plus'));
button.addEventListener('click', () => {
fields.splice(insertIndex, 0, { type: 'text', key: '', label: '', required: false, options: [] });
render();
notifyChange();
});
divider.appendChild(button);
return divider;
}
function buildFieldRow(field, index) {
const isDisplayOnly = DISPLAY_ONLY_TYPES.includes(field.type);
const isSelect = field.type === 'select';
const row = document.createElement('div');
row.className = 'handover-schema-row';
row.dataset.fieldIndex = index;
row.addEventListener('mouseenter', () => highlightPreview(index, true));
row.addEventListener('mouseleave', () => highlightPreview(index, false));
// --- Header: "Feld #N" title + action icons ---
const header = document.createElement('div');
header.className = 'handover-schema-row-header';
const title = document.createElement('span');
title.className = 'handover-schema-row-title';
title.textContent = (t.field || 'Field') + ' #' + (index + 1);
header.appendChild(title);
const actions = document.createElement('div');
actions.className = 'handover-schema-row-actions';
if (index > 0) {
actions.appendChild(createIconButton(
'bi-arrow-up', 'secondary outline small',
() => { [fields[index - 1], fields[index]] = [fields[index], fields[index - 1]]; render(); notifyChange(); },
(t.move_up || 'Move up') + ' ' + (t.field_label || 'field') + ' ' + (index + 1),
));
}
if (index < fields.length - 1) {
actions.appendChild(createIconButton(
'bi-arrow-down', 'secondary outline small',
() => { [fields[index], fields[index + 1]] = [fields[index + 1], fields[index]]; render(); notifyChange(); },
(t.move_down || 'Move down') + ' ' + (t.field_label || 'field') + ' ' + (index + 1),
));
}
actions.appendChild(createIconButton(
'bi-trash', 'danger outline small',
() => { fields.splice(index, 1); render(); notifyChange(); },
(t.remove_field || 'Remove field') + ' ' + (index + 1),
));
header.appendChild(actions);
row.appendChild(header);
// --- Type select (full width) ---
const typeGroup = createGroup(t.field_type || 'Field type');
const typeSelect = document.createElement('select');
typeSelect.setAttribute('aria-label', (t.field_type || 'Field type') + ' ' + (index + 1));
FIELD_TYPES.forEach((ft) => {
const opt = document.createElement('option');
opt.value = ft;
opt.textContent = t['type_' + ft] || ft;
if (ft === field.type) opt.selected = true;
typeSelect.appendChild(opt);
});
typeSelect.addEventListener('change', () => {
field.type = typeSelect.value;
if (DISPLAY_ONLY_TYPES.includes(field.type)) {
delete field.key;
delete field.required;
} else {
if (!field.key) field.key = '';
if (field.required === undefined) field.required = false;
}
if (field.type === 'select' && (!field.options || field.options.length === 0)) {
field.options = [{ value: '', label: '' }];
}
render();
notifyChange();
});
typeGroup.appendChild(typeSelect);
row.appendChild(typeGroup);
// --- Body: label input ---
const body = document.createElement('div');
body.className = 'handover-schema-row-body';
const labelKey = field.type === 'paragraph' ? (t.text_content || 'Text content') : (t.field_label || 'Label');
const labelGroup = createGroup(labelKey);
const labelInput = document.createElement(field.type === 'paragraph' ? 'textarea' : 'input');
if (field.type !== 'paragraph') {
labelInput.type = 'text';
} else {
labelInput.rows = 2;
}
labelInput.value = field.label || '';
labelInput.setAttribute('aria-label', labelKey + ' ' + (index + 1));
labelInput.addEventListener('input', () => { field.label = labelInput.value; notifyChange(); });
labelGroup.appendChild(labelInput);
body.appendChild(labelGroup);
row.appendChild(body);
// --- Required toggle — own row below key/label ---
if (!isDisplayOnly) {
const reqRow = document.createElement('div');
reqRow.className = 'handover-schema-row-required';
const reqLabel = document.createElement('label');
reqLabel.className = 'handover-schema-checkbox-label';
const reqInput = document.createElement('input');
reqInput.type = 'checkbox';
reqInput.checked = !!field.required;
reqInput.addEventListener('change', () => { field.required = reqInput.checked; notifyChange(); });
reqLabel.appendChild(reqInput);
reqLabel.appendChild(document.createTextNode(' ' + (t.required || 'Required')));
reqRow.appendChild(reqLabel);
row.appendChild(reqRow);
}
// --- Select options ---
if (isSelect) {
const optionsSection = document.createElement('div');
optionsSection.className = 'handover-schema-options';
const optionsHeader = document.createElement('div');
optionsHeader.className = 'handover-schema-options-header';
const optionsTitle = document.createElement('strong');
optionsTitle.textContent = t.options || 'Options';
optionsHeader.appendChild(optionsTitle);
const addOptButton = document.createElement('button');
addOptButton.type = 'button';
addOptButton.className = 'secondary outline small';
addOptButton.setAttribute('data-tooltip', t.add_option || 'Add option');
addOptButton.setAttribute('aria-label', t.add_option || 'Add option');
addOptButton.appendChild(createIcon('bi-plus-lg'));
addOptButton.addEventListener('click', () => {
field.options.push({ value: '', label: '' });
render();
notifyChange();
});
optionsHeader.appendChild(addOptButton);
optionsSection.appendChild(optionsHeader);
if (!field.options) field.options = [];
field.options.forEach((opt, optIndex) => {
const optRow = document.createElement('div');
optRow.className = 'handover-schema-option-row';
const lblInput = document.createElement('input');
lblInput.type = 'text';
lblInput.value = opt.label || '';
lblInput.placeholder = (t.option_label || 'Label') + ' ' + (optIndex + 1);
lblInput.setAttribute('aria-label', (t.option_label || 'Label') + ' ' + (optIndex + 1));
lblInput.addEventListener('input', () => { opt.label = lblInput.value; notifyChange(); });
optRow.appendChild(lblInput);
optRow.appendChild(createIconButton(
'bi-x-lg', 'danger outline small',
() => { field.options.splice(optIndex, 1); render(); notifyChange(); },
(t.remove_option || 'Remove option') + ' ' + (optIndex + 1),
));
optionsSection.appendChild(optRow);
});
row.appendChild(optionsSection);
}
return row;
}
function createGroup(labelText) {
const group = document.createElement('div');
group.className = 'handover-schema-field-group';
if (labelText) {
const label = document.createElement('span');
label.className = 'handover-schema-field-label';
label.textContent = labelText;
group.appendChild(label);
}
return group;
}
function createIcon(iconClass) {
const icon = document.createElement('i');
icon.className = 'bi ' + iconClass;
return icon;
}
function createIconButton(iconClass, buttonClass, onClick, ariaLabel) {
const button = document.createElement('button');
button.type = 'button';
button.className = buttonClass;
button.setAttribute('aria-label', ariaLabel);
button.appendChild(createIcon(iconClass));
button.addEventListener('click', onClick);
return button;
}
function renderPreview() {
if (!previewContainer) return;
clearElement(previewContainer);
if (fields.length === 0) {
const empty = document.createElement('p');
empty.className = 'handover-schema-preview-empty';
empty.textContent = t.preview_empty || 'Add fields to see a live preview.';
previewContainer.appendChild(empty);
return;
}
const formEl = document.createElement('div');
formEl.className = 'handover-schema-preview-form';
fields.forEach((field, index) => {
if (field.type === 'heading') {
const h = document.createElement('h3');
h.dataset.fieldIndex = index;
h.textContent = field.label || '';
formEl.appendChild(h);
return;
}
if (field.type === 'paragraph') {
const p = document.createElement('p');
p.dataset.fieldIndex = index;
p.textContent = field.label || '';
formEl.appendChild(p);
return;
}
const group = document.createElement('div');
group.className = 'handover-schema-preview-field';
group.dataset.fieldIndex = index;
if (field.type === 'checkbox') {
const input = document.createElement('input');
input.type = 'checkbox';
input.disabled = true;
group.appendChild(input);
const label = document.createElement('label');
label.textContent = field.label || '';
if (field.required) {
const star = document.createElement('span');
star.className = 'handover-schema-preview-required';
star.textContent = ' *';
label.appendChild(star);
}
group.appendChild(label);
} else {
const label = document.createElement('label');
label.textContent = field.label || '';
if (field.required) {
const star = document.createElement('span');
star.className = 'handover-schema-preview-required';
star.textContent = ' *';
label.appendChild(star);
}
group.appendChild(label);
let input;
if (field.type === 'textarea') {
input = document.createElement('textarea');
input.rows = 3;
} else if (field.type === 'select') {
input = document.createElement('select');
const placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = '';
input.appendChild(placeholder);
(field.options || []).forEach((opt) => {
const option = document.createElement('option');
option.textContent = opt.label || '';
input.appendChild(option);
});
} else {
input = document.createElement('input');
input.type = field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text';
}
input.disabled = true;
group.appendChild(input);
}
formEl.appendChild(group);
});
previewContainer.appendChild(formEl);
}
function highlightPreview(index, active) {
if (!previewContainer) return;
const el = previewContainer.querySelector('[data-field-index="' + index + '"]');
if (el) {
el.classList.toggle('handover-schema-preview-highlight', active);
}
}
function syncHiddenInput() {
if (!hiddenInput) return;
hiddenInput.value = fields.length > 0 ? JSON.stringify(fields) : '';
}
function notifyChange() {
syncHiddenInput();
renderPreview();
if (hiddenInput) {
hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
}
}
}