document.addEventListener('DOMContentLoaded', () => { /* ---------- helpers ---------- */ const form = document.getElementById('registrationForm'); // <— pick the specific form if (!form) { // <— bail out if not on this page console.debug('validation.js: no #registrationForm — skipping init'); return; } const el = name => form.querySelector(`[name="${name}"]`); const allInputs = form.querySelectorAll('input, select'); /* ---------- regex rules ---------- */ const nameRE = /^[A-Za-z\s-]{2,30}$/; const phoneRE = /^\(?\d{3}\)?[-\s]?\d{3}[-\s]?\d{4}$/; const emailRE = /^(?=.{2,50}$)[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; const streetRE = /^[A-Za-z0-9.\s-]{2,30}$/; const aptRE = /^[A-Za-z0-9.\s-]{0,15}$/; const cityRE = /^[A-Za-z0-9\s.'-]{2,15}$/; const zipRE = /^\d{5}$/; const captchaRE= /^[A-Za-z0-9]{4,10}$/; const formatHints = { firstname: "Only letters, spaces, or dashes", lastname: "Only letters, spaces, or dashes", email: "Format: user@example.com", confirm_email: "Must match the email exactly", cellphone: "Exactly 10 digits (e.g., 1234567890 , (123) 456-4789)", address_street: "Letters, numbers, dots, dashes", apt: "Optional - Only letters, spaces, or dashes", city: "Letters and spaces", zip: "Exactly 5 digits (e.g., 12345)", captcha: "4–10 letters or digits" }; const validateField = (input) => { const v = input.value.trim(); let ok = true; switch (input.name) { case 'firstname': case 'lastname': case 'second_firstname': case 'second_lastname': ok = nameRE.test(v); break; case 'cellphone': case 'second_cellphone': ok = phoneRE.test(v); break; case 'email': case 'second_email': ok = emailRE.test(v); break; case 'confirm_email': ok = v === (el('email')?.value ?? ''); break; // <— guard case 'address_street': ok = streetRE.test(v); break; case 'apt': ok = !v || aptRE.test(v); break; case 'city': ok = cityRE.test(v); break; case 'zip': ok = zipRE.test(v); break; case 'captcha': ok = captchaRE.test(v); break; } input.classList.toggle('is-invalid', !ok); return ok; }; /* ---------- live validation & re-check confirm_email ---------- */ allInputs.forEach(inp => { const hint = document.getElementById(`${inp.name}-hint`); inp.addEventListener('input', () => { if (inp.disabled) return; validateField(inp); // Show hint while typing if (hint) hint.classList.remove('d-none'); // Special revalidation for confirm_email if (inp.name === 'email') { const conf = el('confirm_email'); if (conf && !conf.disabled) validateField(conf); } }); inp.addEventListener('blur', () => { const hint = document.getElementById(`${inp.name}-hint`); if (hint) hint.classList.add('d-none'); }); }); /* ---------- copy / paste blocking with inline warnings ---------- */ const blockPasteConfig = [ { id: 'confirm_email', warnId: 'copy-warning-email', msg: 'Copy/paste is disabled for this field. Please type the answer.' }, { id: 'captcha', warnId: 'captcha-warning', msg: 'Copy/paste is disabled for this field. Please type the answer.' } ]; blockPasteConfig.forEach(({ id, warnId, msg }) => { const input = document.getElementById(id); if (!input) return; let warnBox = document.getElementById(warnId); if (!warnBox) { warnBox = document.createElement('div'); warnBox.id = warnId; warnBox.className = 'invalid-feedback d-none'; warnBox.textContent = msg; input.parentElement.appendChild(warnBox); } const showWarn = (e) => { e.preventDefault(); warnBox.classList.remove('d-none'); input.classList.add('is-invalid'); setTimeout(() => { warnBox.classList.add('d-none'); input.classList.remove('is-invalid'); }, 3000); }; ['paste', 'copy', 'cut'].forEach(evt => input.addEventListener(evt, showWarn)); }); /* ---------- second-parent toggle logic ---------- */ const isParentCheckbox = document.getElementById('is_parent'); const secondSection = document.getElementById('second-parent-section'); const noSecondToggle = document.getElementById('no_second_parent_info'); const secondParentFields = document.getElementById('second-parent-fields'); const setSecondDisabled = (dis) => { if (!secondParentFields) return; // <— guard secondParentFields.querySelectorAll('input, select').forEach(f => { f.disabled = dis; if (dis) f.classList.remove('is-invalid'); }); }; const refreshSecondUI = () => { if (!secondSection) return; // <— guard const parent = !!isParentCheckbox?.checked; const noInfo = !!noSecondToggle?.checked; secondSection.style.display = parent ? 'block' : 'none'; setSecondDisabled(!parent || noInfo); }; isParentCheckbox?.addEventListener('change', refreshSecondUI); noSecondToggle?.addEventListener('change', refreshSecondUI); refreshSecondUI(); /* ---------- submit event ---------- */ form.addEventListener('submit', e => { let ok = true; allInputs.forEach(inp => { if (inp.disabled) return; const inSecond = secondParentFields && inp.closest('#second-parent-fields'); if (inSecond && (!isParentCheckbox?.checked || noSecondToggle?.checked)) return; if (!validateField(inp)) ok = false; }); if (!ok) { e.preventDefault(); form.querySelector('.is-invalid')?.focus({ preventScroll: true }); } }); });