/** * 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. **Who are you registering?** The student section is hidden until the * choice is "on behalf of students" or "both", and "Add another student" * clones the student block. "On behalf of students" *alone* also takes the * account holder's own **About you** panel out of play — they are not a * student in that case, so the server ignores their birth year and answers * and the browser must not demand them. Under "both" they are a student and * do fill it in. * 2. **Password strength.** The password is scored with zxcvbn (via WordPress's * own `wp.passwordStrength`) and a weak one is refused. The server applies * its own, coarser rule regardless — see `Auth\PasswordPolicy`. */ (function () { 'use strict'; var PASSWORD = window.usSchedulerPassword || {}; /** * Gate the form on password strength. * * The verdict is attached to the field with `setCustomValidity()` rather than * by disabling the submit button: an invalid field blocks the submit without * the button having to know why. * * It is also re-scored on submit, which is the case the input handler alone * misses. zxcvbn's dictionary arrives after page load, and until it does the * meter has no opinion and the field is left valid — so a password typed in * the first second and submitted straight away would otherwise never be * scored at all, and the first the student heard of it would be the server * rejecting the whole form. */ function enhancePassword(form) { var field = form.querySelector('#us-reg-pass'); var output = form.querySelector('#us-reg-pass-strength'); var strings = PASSWORD.strings || {}; if (!field || !PASSWORD.minScore) { return; } // What the password must not simply repeat back. Mirrors the identity // check PasswordPolicy makes server-side. function identity() { var out = []; var sources = form.querySelectorAll('#us-reg-email, #us-reg-name'); for (var i = 0; i < sources.length; i++) { var value = (sources[i].value || '').trim(); if (value) { out.push(value); if (value.indexOf('@') > 0) { out.push(value.split('@')[0]); } } } return out; } function assess() { var value = field.value || ''; if (!value) { report('', ''); return; } if (value.length < (PASSWORD.minLength || 8)) { report(strings.short, 'short'); return; } // zxcvbn's dictionary is fetched after load, and wp.passwordStrength // reports -1 until it arrives. Say nothing and allow the submit in that // window — the server still checks, and the next keystroke re-runs this // once the dictionary is in. if (!window.wp || !window.wp.passwordStrength || typeof window.zxcvbn === 'undefined') { report('', ''); return; } var score = window.wp.passwordStrength.meter(value, identity(), ''); if (score < 0) { report('', ''); return; } if (score >= 3) { report(strings.strong, 'strong'); } else if (score >= PASSWORD.minScore) { report(strings.medium, 'medium'); } else { report(score <= 0 ? strings.veryWeak : strings.weak, 'weak'); } } /** Show the verdict, and make it the field's validity at the same time. */ function report(message, level) { var acceptable = '' === level || 'medium' === level || 'strong' === level; if (output) { output.textContent = message || ''; output.className = 'us-password-strength' + (level ? ' is-' + level : ''); } field.setCustomValidity(acceptable ? '' : message || ''); } field.addEventListener('input', assess); field.addEventListener('blur', assess); // The identity check depends on these, so a password typed first and an // email typed second is still caught. var sources = form.querySelectorAll('#us-reg-email, #us-reg-name'); for (var i = 0; i < sources.length; i++) { sources[i].addEventListener('change', assess); } // Native validation has already run by the time `submit` fires, so a // verdict reached here has to stop the submit by hand. form.addEventListener('submit', function (event) { assess(); if (!field.checkValidity()) { event.preventDefault(); field.reportValidity(); } }); } /** * 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) { var choices = form.querySelectorAll('.us-registering-for'); var children = form.querySelector('#us-children'); var self = form.querySelector('#us-reg-self'); if (!choices.length || !children) { return; } var addButton = children.querySelector('.us-add-child'); var nextIndex = 1; /** The selected "who are you registering?" value; 'self' if somehow none is. */ function mode() { for (var i = 0; i < choices.length; i++) { if (choices[i].checked) return choices[i].value; } return 'self'; } /** * Keep the form in step with the choice. * * Two independent questions, which is why "both" needs its own answer to * each: * * - Are student blocks in play? For "students" and "both". * - Is the account holder a student themselves? For "self" and "both" — * only then are they asked for their own birth year and answers. A pure * guardian gives those per student instead. * * Each panel is disabled as well as hidden. Disabling is what actually * settles it: a `required` field inside a hidden container makes the form * unsubmittable with no way to reach the offending control, and a disabled * fieldset is neither validated nor submitted. The server enforces the * same rules either way. */ function sync() { var current = mode(); var wantsStudents = current !== 'self'; var asksSelf = current !== 'students'; children.hidden = !wantsStudents; children.disabled = !wantsStudents; // Belt and braces alongside the disabled fieldset, so the required // state is right if a browser ever renders the block on its own. var required = children.querySelectorAll('[data-us-child-required]'); for (var r = 0; r < required.length; r++) { required[r].required = wantsStudents; } if (self) { self.hidden = !asksSelf; self.disabled = !asksSelf; } } for (var c = 0; c < choices.length; c++) { choices[c].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++) { enhanceGuardian(forms[i]); enhancePassword(forms[i]); } }); })();