/** * 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 innerHTML = ''). * @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 form = hiddenInput?.closest('form'); /** @type {Array} */ let fields = initialFields.map((f) => ({ ...f, options: f.options ? f.options.map((o) => ({ ...o })) : [] })); render(); 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) => { list.appendChild(buildFieldRow(field, index)); }); root.appendChild(list); } const addButton = document.createElement('button'); addButton.type = 'button'; addButton.className = 'handover-schema-add-button'; addButton.textContent = t.add_field || 'Add field'; addButton.addEventListener('click', () => { fields.push({ type: 'text', key: '', label: '', required: false, options: [] }); render(); notifyChange(); }); root.appendChild(addButton); } 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'; // --- Header with type + actions --- const header = document.createElement('div'); header.className = 'handover-schema-row-header'; // Type select const typeGroup = createGroup(t.field_type || 'Type'); const typeSelect = document.createElement('select'); typeSelect.setAttribute('aria-label', (t.field_type || '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); header.appendChild(typeGroup); // Actions const actions = document.createElement('div'); actions.className = 'handover-schema-row-actions'; if (index > 0) { actions.appendChild(createActionButton(t.move_up || 'Up', 'handover-schema-button-move', () => { [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(createActionButton(t.move_down || 'Down', 'handover-schema-button-move', () => { [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(createActionButton(t.remove_field || 'Remove', 'handover-schema-button-remove', () => { fields.splice(index, 1); render(); notifyChange(); }, (t.remove_field || 'Remove field') + ' ' + (index + 1))); header.appendChild(actions); row.appendChild(header); // --- Body fields --- const body = document.createElement('div'); body.className = 'handover-schema-row-body'; // Key (hidden for display-only types) if (!isDisplayOnly) { const keyGroup = createGroup(t.field_key || 'Key'); const keyInput = document.createElement('input'); keyInput.type = 'text'; keyInput.value = field.key || ''; keyInput.placeholder = 'e.g. handover_date'; keyInput.pattern = '[a-z][a-z0-9_]*'; keyInput.setAttribute('aria-label', (t.field_key || 'Key') + ' ' + (index + 1)); keyInput.addEventListener('input', () => { field.key = keyInput.value; notifyChange(); }); keyGroup.appendChild(keyInput); body.appendChild(keyGroup); } // Label (or text content for paragraph) 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); // Required toggle (hidden for display-only types) if (!isDisplayOnly) { const reqGroup = createGroup(''); 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'))); reqGroup.appendChild(reqLabel); body.appendChild(reqGroup); } row.appendChild(body); // --- Select options --- if (isSelect) { const optionsSection = document.createElement('div'); optionsSection.className = 'handover-schema-options'; const optionsTitle = document.createElement('strong'); optionsTitle.textContent = t.options || 'Options'; optionsSection.appendChild(optionsTitle); if (!field.options) field.options = []; field.options.forEach((opt, optIndex) => { const optRow = document.createElement('div'); optRow.className = 'handover-schema-option-row'; const valInput = document.createElement('input'); valInput.type = 'text'; valInput.value = opt.value || ''; valInput.placeholder = t.option_value || 'Value'; valInput.setAttribute('aria-label', (t.option_value || 'Value') + ' ' + (optIndex + 1)); valInput.addEventListener('input', () => { opt.value = valInput.value; notifyChange(); }); optRow.appendChild(valInput); const lblInput = document.createElement('input'); lblInput.type = 'text'; lblInput.value = opt.label || ''; lblInput.placeholder = t.option_label || 'Label'; lblInput.setAttribute('aria-label', (t.option_label || 'Label') + ' ' + (optIndex + 1)); lblInput.addEventListener('input', () => { opt.label = lblInput.value; notifyChange(); }); optRow.appendChild(lblInput); optRow.appendChild(createActionButton(t.remove_option || 'Remove', 'handover-schema-button-remove-option', () => { field.options.splice(optIndex, 1); render(); notifyChange(); }, (t.remove_option || 'Remove option') + ' ' + (optIndex + 1))); optionsSection.appendChild(optRow); }); const addOptButton = document.createElement('button'); addOptButton.type = 'button'; addOptButton.className = 'handover-schema-add-option-button'; addOptButton.textContent = t.add_option || 'Add option'; addOptButton.addEventListener('click', () => { field.options.push({ value: '', label: '' }); render(); notifyChange(); }); optionsSection.appendChild(addOptButton); 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 createActionButton(text, className, onClick, ariaLabel) { const button = document.createElement('button'); button.type = 'button'; button.className = className; button.textContent = text; if (ariaLabel) button.setAttribute('aria-label', ariaLabel); button.addEventListener('click', onClick); return button; } function syncHiddenInput() { if (!hiddenInput) return; hiddenInput.value = fields.length > 0 ? JSON.stringify(fields) : ''; } function notifyChange() { syncHiddenInput(); if (hiddenInput) { hiddenInput.dispatchEvent(new Event('input', { bubbles: true })); } } }