/** * Progressive enhancement for the student registration form. * * Two independent behaviours, both optional — without JS every panel stays * visible and the single submit still works: * * 1. **Two steps.** When account-signup questions are configured the form * renders two panels (`[data-step="1"]` account details, `[data-step="2"]` * the questions) inside a form marked `data-steps="1"`. Step two is hidden * behind a "Next" button that only advances once step one passes native * validation. * 2. **Parent/guardian.** The children section is hidden until the * parent/guardian box is ticked, and "Add another child" clones the child * block. Ticking the box also takes the guardian's *own* question panel out * of play — in guardian mode the questions are asked per child, so the * server ignores those answers and the browser must not demand them. */ (function () { 'use strict'; function enhanceSteps(form) { var step1 = form.querySelector('[data-step="1"]'); var step2 = form.querySelector('[data-step="2"]'); var next = form.querySelector('.us-reg-next'); var back = form.querySelector('.us-reg-back'); if (!step1 || !step2 || !next) { return null; } function show(step) { step1.hidden = step !== 1; step2.hidden = step !== 2; } show(1); next.addEventListener('click', function () { var fields = step1.querySelectorAll('input, select, textarea'); for (var i = 0; i < fields.length; i++) { if (!fields[i].checkValidity()) { fields[i].reportValidity(); return; } } show(2); }); if (back) { back.addEventListener('click', function () { show(1); }); } return { step2: step2, next: next, earlySubmit: form.querySelector('.us-reg-submit-early'), }; } /** * Rewrite a cloned child block's `children[0][…]` names and ids to the new * index, and clear the values carried over from the block it was cloned from. */ function reindex(block, index) { block.setAttribute('data-child-index', String(index)); var fields = block.querySelectorAll('input, select, textarea'); for (var i = 0; i < fields.length; i++) { var field = fields[i]; if (field.name) { field.name = field.name.replace(/^children\[\d+\]/, 'children[' + index + ']'); } var oldId = field.id; if (oldId) { field.id = oldId.replace(/^us-child-\d+-/, 'us-child-' + index + '-'); var label = block.querySelector('label[for="' + oldId + '"]'); if (label) { label.setAttribute('for', field.id); } } if (field.type === 'checkbox' || field.type === 'radio') { field.checked = false; } else { field.value = ''; } } } function enhanceGuardian(form, steps) { var toggle = form.querySelector('#us-is-guardian'); var children = form.querySelector('#us-children'); if (!toggle || !children) { return; } var addButton = children.querySelector('.us-add-child'); var nextIndex = 1; // The guardian's own question panel is only meaningful when they are // registering for themselves. Disabling it (rather than hiding it) is what // stops a `required` question the server will ignore from blocking submit. function sync() { children.hidden = !toggle.checked; // Each student's name and birth year are required, but only once the // block is in play: a `required` field inside a hidden container makes // the form unsubmittable with no way to reach the offending control, so // the attribute goes on and comes off with the block itself. The server // enforces the same rule either way. var required = children.querySelectorAll('[data-us-child-required]'); for (var r = 0; r < required.length; r++) { required[r].required = toggle.checked; } if (!steps) { return; } var fields = steps.step2.querySelectorAll('input, select, textarea'); for (var i = 0; i < fields.length; i++) { fields[i].disabled = toggle.checked; } // With the questions out of play there is no second step to advance to, // so "Next" would be a dead end — swap it for the submit. steps.next.hidden = toggle.checked; if (steps.earlySubmit) { steps.earlySubmit.hidden = !toggle.checked; } } toggle.addEventListener('change', sync); sync(); if (addButton) { addButton.addEventListener('click', function () { var blocks = children.querySelectorAll('.us-child'); var clone = blocks[blocks.length - 1].cloneNode(true); reindex(clone, nextIndex); nextIndex += 1; children.insertBefore(clone, addButton.parentNode); // The clone carries the data attribute but not necessarily the // current required state, so settle it the same way as the rest. sync(); }); } } document.addEventListener('DOMContentLoaded', function () { var forms = document.querySelectorAll('.us-register-form form'); for (var i = 0; i < forms.length; i++) { var steps = forms[i].getAttribute('data-steps') === '1' ? enhanceSteps(forms[i]) : null; enhanceGuardian(forms[i], steps); } }); })();