1
0
Files
breadcrumb-the-shire/modules/security/web/js/security-check-workspace.js

109 lines
3.5 KiB
JavaScript
Raw Normal View History

2026-06-22 13:19:05 +02:00
/**
* Security check workspace light client-side enhancement of the checklist form.
*
* Progress is authoritative on the server (recomputed on save); this only gives
* live visual feedback: toggles the done-styling on a row and updates the
* progress bar width as items are checked. No business logic.
*/
import { resolveHost } from '/js/core/app-dom.js';
const SELECTOR = '[data-app-component="security-check-workspace"]';
const EMPTY_API = Object.freeze({ destroy: () => {} });
const resolveForm = (root) => {
const host = resolveHost(root);
if (host instanceof HTMLElement && host.matches(SELECTOR)) {
return host;
}
return host.querySelector(SELECTOR);
};
export function initSecurityCheckWorkspace(root = document) {
const form = resolveForm(root);
if (!form) {
return EMPTY_API;
}
const section = form.closest('section') || document;
const fill = section.querySelector('.security-check-progress-fill');
const doneToggles = () => Array.from(form.querySelectorAll('input[type="checkbox"][name$="[done]"]'));
// Step gating: a step's done-checkbox can only be ticked once all of its
// required fields are filled. Authoritative gate lives on the server; this
// just mirrors it so the box is disabled until the fields are complete.
const requiredFields = Array.from(form.querySelectorAll('[data-security-required="1"]'));
const readonly = requiredFields.length > 0 && requiredFields.every((field) => field.disabled);
const gateGroups = [];
new Set(requiredFields.map((field) => field.closest('.security-step'))).forEach((step) => {
if (!step) {
return;
}
const doneBox = step.querySelector('input[type="checkbox"][name$="[done]"]');
if (!doneBox) {
return;
}
gateGroups.push({
doneBox,
hint: step.querySelector('[data-security-gate-hint]'),
fields: requiredFields.filter((field) => field.closest('.security-step') === step),
});
});
const enforceGates = () => {
if (readonly) {
return;
}
gateGroups.forEach(({ doneBox, hint, fields }) => {
const met = fields.every((field) => field.value.trim() !== '');
doneBox.disabled = !met;
if (!met && doneBox.checked) {
doneBox.checked = false;
}
if (hint) {
hint.hidden = met;
}
});
};
const listenerController = new AbortController();
const refresh = () => {
const boxes = doneToggles();
const total = boxes.length;
const done = boxes.filter((b) => b.checked).length;
boxes.forEach((box) => {
const row = box.closest('.security-step, .security-tech-item');
row?.classList.toggle('is-done', box.checked);
});
if (fill && total > 0) {
fill.style.width = `${Math.floor((done / total) * 100)}%`;
}
};
form.addEventListener('change', (event) => {
const target = event.target;
if (target instanceof HTMLInputElement && target.type === 'checkbox' && target.name.endsWith('[done]')) {
refresh();
}
}, { signal: listenerController.signal });
if (!readonly && gateGroups.length > 0) {
const onFieldChange = (event) => {
if (event.target instanceof HTMLElement && event.target.matches('[data-security-required="1"]')) {
enforceGates();
refresh();
}
};
form.addEventListener('input', onFieldChange, { signal: listenerController.signal });
form.addEventListener('change', onFieldChange, { signal: listenerController.signal });
}
enforceGates();
refresh();
return {
destroy: () => listenerController.abort(),
};
}