/** * 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(), }; }