172 lines
5.9 KiB
JavaScript
172 lines
5.9 KiB
JavaScript
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 });
|
||
}
|
||
});
|
||
});
|