feat(js): add app-http contracts and migrate helpdesk runtime layer
This commit is contained in:
@@ -4,15 +4,19 @@
|
||||
* Renders a structured field list from JSON and serializes changes
|
||||
* back to a hidden input on form submit.
|
||||
*/
|
||||
import { resolveHost } from '/js/core/app-dom.js';
|
||||
|
||||
const FIELD_TYPES = ['heading', 'paragraph', 'text', 'textarea', 'number', 'date', 'checkbox', 'select'];
|
||||
const DISPLAY_ONLY_TYPES = ['heading', 'paragraph'];
|
||||
const EMPTY_API = Object.freeze({ destroy: () => {} });
|
||||
|
||||
/** @type {HTMLElement|null} */
|
||||
const container = document.getElementById('handover-schema-editor');
|
||||
if (container) {
|
||||
init(container);
|
||||
}
|
||||
const resolveEditor = (root) => {
|
||||
const host = resolveHost(root);
|
||||
if (host instanceof HTMLElement && host.matches('#handover-schema-editor[data-app-component="helpdesk-handover-schema-editor"]')) {
|
||||
return host;
|
||||
}
|
||||
return host.querySelector('#handover-schema-editor[data-app-component="helpdesk-handover-schema-editor"]');
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove all child nodes from an element (safe alternative to setting markup).
|
||||
@@ -25,10 +29,19 @@ function clearElement(el) {
|
||||
}
|
||||
|
||||
function init(root) {
|
||||
const listenerController = new AbortController();
|
||||
const on = (target, type, handler, options = {}) => {
|
||||
if (!target || typeof target.addEventListener !== 'function') {
|
||||
return;
|
||||
}
|
||||
target.addEventListener(type, handler, { ...options, signal: listenerController.signal });
|
||||
};
|
||||
|
||||
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 scope = root.closest('.app-details-container') || document;
|
||||
const hiddenInput = scope.querySelector('#handover-schema-json');
|
||||
const previewContainer = scope.querySelector('#handover-schema-preview');
|
||||
const form = hiddenInput?.closest('form');
|
||||
|
||||
/** @type {Array<object>} */
|
||||
@@ -38,9 +51,7 @@ function init(root) {
|
||||
renderPreview();
|
||||
syncHiddenInput();
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', () => syncHiddenInput());
|
||||
}
|
||||
on(form, 'submit', () => syncHiddenInput());
|
||||
|
||||
function render() {
|
||||
clearElement(root);
|
||||
@@ -56,7 +67,7 @@ function init(root) {
|
||||
addButton.className = 'secondary outline';
|
||||
addButton.appendChild(createIcon('bi-plus-lg'));
|
||||
addButton.appendChild(document.createTextNode(' ' + (t.add_field || 'Add field')));
|
||||
addButton.addEventListener('click', () => {
|
||||
on(addButton, 'click', () => {
|
||||
fields.push({ type: 'text', key: '', label: '', required: false, options: [] });
|
||||
render();
|
||||
notifyChange();
|
||||
@@ -86,7 +97,7 @@ function init(root) {
|
||||
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', () => {
|
||||
on(button, 'click', () => {
|
||||
fields.splice(insertIndex, 0, { type: 'text', key: '', label: '', required: false, options: [] });
|
||||
render();
|
||||
notifyChange();
|
||||
@@ -102,8 +113,8 @@ function init(root) {
|
||||
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));
|
||||
on(row, 'mouseenter', () => highlightPreview(index, true));
|
||||
on(row, 'mouseleave', () => highlightPreview(index, false));
|
||||
|
||||
// --- Header: "Feld #N" title + action icons ---
|
||||
const header = document.createElement('div');
|
||||
@@ -162,7 +173,7 @@ function init(root) {
|
||||
if (ft === field.type) opt.selected = true;
|
||||
typeSelect.appendChild(opt);
|
||||
});
|
||||
typeSelect.addEventListener('change', () => {
|
||||
on(typeSelect, 'change', () => {
|
||||
field.type = typeSelect.value;
|
||||
if (DISPLAY_ONLY_TYPES.includes(field.type)) {
|
||||
delete field.key;
|
||||
@@ -194,7 +205,7 @@ function init(root) {
|
||||
}
|
||||
labelInput.value = field.label || '';
|
||||
labelInput.setAttribute('aria-label', labelKey + ' ' + (index + 1));
|
||||
labelInput.addEventListener('input', () => { field.label = labelInput.value; notifyChange(); });
|
||||
on(labelInput, 'input', () => { field.label = labelInput.value; notifyChange(); });
|
||||
labelGroup.appendChild(labelInput);
|
||||
body.appendChild(labelGroup);
|
||||
|
||||
@@ -209,7 +220,7 @@ function init(root) {
|
||||
const reqInput = document.createElement('input');
|
||||
reqInput.type = 'checkbox';
|
||||
reqInput.checked = !!field.required;
|
||||
reqInput.addEventListener('change', () => { field.required = reqInput.checked; notifyChange(); });
|
||||
on(reqInput, 'change', () => { field.required = reqInput.checked; notifyChange(); });
|
||||
reqLabel.appendChild(reqInput);
|
||||
reqLabel.appendChild(document.createTextNode(' ' + (t.required || 'Required')));
|
||||
reqRow.appendChild(reqLabel);
|
||||
@@ -233,7 +244,7 @@ function init(root) {
|
||||
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', () => {
|
||||
on(addOptButton, 'click', () => {
|
||||
field.options.push({ value: '', label: '' });
|
||||
render();
|
||||
notifyChange();
|
||||
@@ -252,7 +263,7 @@ function init(root) {
|
||||
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(); });
|
||||
on(lblInput, 'input', () => { opt.label = lblInput.value; notifyChange(); });
|
||||
optRow.appendChild(lblInput);
|
||||
|
||||
optRow.appendChild(createIconButton(
|
||||
@@ -294,7 +305,7 @@ function init(root) {
|
||||
button.className = buttonClass;
|
||||
button.setAttribute('aria-label', ariaLabel);
|
||||
button.appendChild(createIcon(iconClass));
|
||||
button.addEventListener('click', onClick);
|
||||
on(button, 'click', onClick);
|
||||
return button;
|
||||
}
|
||||
|
||||
@@ -337,7 +348,7 @@ function init(root) {
|
||||
if (field.type === 'checkbox') {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.addEventListener('click', (e) => e.preventDefault());
|
||||
on(input, 'click', (e) => e.preventDefault());
|
||||
group.appendChild(input);
|
||||
const label = document.createElement('label');
|
||||
label.textContent = field.label || '';
|
||||
@@ -408,4 +419,19 @@ function init(root) {
|
||||
hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
destroy: () => {
|
||||
listenerController.abort();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function initHandoverSchemaEditor(root = document) {
|
||||
const container = resolveEditor(root);
|
||||
if (!container) {
|
||||
return EMPTY_API;
|
||||
}
|
||||
|
||||
return init(container);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user