First version of the security module
This commit is contained in:
108
modules/security/web/js/security-check-workspace.js
Normal file
108
modules/security/web/js/security-check-workspace.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user