// /assets/js/validate_student.js
import { validateNameLive } from './name_validation.js';
import { validateEmailLive } from './email_validation.js';
import { validatePhoneLive } from './phone_validation.js';
import { validateAgeLive } from './age_validation.js';
(function () {
document.addEventListener('DOMContentLoaded', function () {
var form = document.getElementById('studentRegistrationForm');
if (!form) return;
// ---- Polyfills / helpers ----
var CSS_ESCAPE = (window.CSS && typeof window.CSS.escape === 'function')
? window.CSS.escape
: function (s) { return String(s).replace(/[^a-zA-Z0-9_\-]/g, '\\$&'); };
function log() { try { console.log.apply(console, arguments); } catch (_) { } }
function warn() { try { console.warn.apply(console, arguments); } catch (_) { } }
function err() { try { console.error.apply(console, arguments); } catch (_) { } }
// ---- Config ----
var MIN_AGE = 5;
var MAX_AGE = 18;
var maxStudents = parseInt(form.dataset.maxStudents, 10) || 6;
var existingStudents = parseInt(form.dataset.existingStudents, 10) || 0;
var maxEmergency = parseInt(form.dataset.maxEmergency, 10) || 1;
var existingEmergencies = parseInt(form.dataset.existingEmergencies, 10) || 0;
// Only allow emergency forms if there are NO registered students in DB
var allowEmergency = (existingStudents === 0);
// ---- DOM ----
var container = document.getElementById('studentFormsContainer');
var emergencyContainer = document.getElementById('emergencyFormsContainer');
var addStudentBtn = document.getElementById('addStudentBtn');
var addAnotherStudentBtn = document.getElementById('addAnotherStudentBtn');
var doneWithStudentBtn = document.getElementById('doneWithStudentBtn');
var saveFormBtn = document.getElementById('saveFormBtn'); // should be type="submit"
var initialButtons = document.getElementById('initialButtons');
var formActionButtons = document.getElementById('formActionButtons');
var studentTmplWrap = document.getElementById('studentFormTemplate');
var contactTmplWrap = document.getElementById('emergencyFormTemplate');
var addedStudents = 0;
var addedEmergencies = 0;
var isSubmitting = false; // re-entrancy guard
// ---- Group validators (at least one required; 'Other' requires text) ----
function ensureFeedback(container, idBase, message) {
var fb = container.querySelector('.invalid-feedback[data-for="' + idBase + '"]');
if (!fb) {
fb = document.createElement('div');
fb.className = 'invalid-feedback';
fb.setAttribute('data-for', idBase);
container.appendChild(fb);
}
fb.textContent = message || '';
fb.style.display = message ? 'block' : 'none';
return fb;
}
function validateChecklistRequired(block, baseName, otherInputName, labelText) {
var groupWrap = block.querySelector('[data-group="' + baseName + '"]') || block; // optional wrapper
var boxes = block.querySelectorAll('input[type="checkbox"][data-base-name="' + baseName + '"]');
if (!boxes.length) return true;
var idBase = baseName + '-' + (block.dataset.index || '');
var anyChecked = Array.prototype.some.call(boxes, function (b) { return b.checked; });
var firstBox = boxes[0];
var valid = true;
var msg = '';
if (!anyChecked) {
valid = false;
msg = 'Please select at least one ' + (labelText || baseName.replace('_', ' ')) + '.';
} else {
// If "Other" is checked, ensure text provided
var otherChecked = Array.prototype.some.call(boxes, function (b) { return /Other$/i.test(b.value) && b.checked; });
if (otherChecked) {
var otherInput = block.querySelector(
'input[name="' + otherInputName + '"], input[data-other-for="' + baseName + '"]'
);
if (otherInput) {
var val = (otherInput.value || '').trim();
if (!val) {
valid = false;
msg = "Please specify the 'Other' " + (labelText || baseName.replace('_', ' ')) + '.';
// mark 'other' input invalid too
otherInput.classList.add('is-invalid');
otherInput.setCustomValidity(msg);
} else {
otherInput.classList.remove('is-invalid');
otherInput.setCustomValidity('');
}
}
}
}
// apply validity to first checkbox (so browser can focus it)
if (!valid) {
firstBox.classList.add('is-invalid');
firstBox.setCustomValidity(msg);
try { firstBox.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (_) { }
} else {
boxes.forEach(function (b) { b.classList.remove('is-invalid'); b.setCustomValidity(''); });
var otherInput = block.querySelector(
'input[name="' + otherInputName + '"], input[data-other-for="' + baseName + '"]'
);
if (otherInput) { otherInput.classList.remove('is-invalid'); otherInput.setCustomValidity(''); }
}
// show/hide feedback message
ensureFeedback(groupWrap, idBase, msg);
return valid;
}
// ---- Utilities ----
function cloneTemplateContent(wrapper) {
var temp = document.createElement('div');
temp.innerHTML = (wrapper && wrapper.innerHTML ? wrapper.innerHTML : '').trim();
return temp.firstElementChild || null;
}
function showToast(message, type) {
type = type || 'success';
var toast = document.createElement('div');
toast.className = 'alert alert-' + type + ' position-fixed top-0 end-0 m-3';
toast.style.zIndex = '9999';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(function () { if (toast && toast.parentNode) toast.parentNode.removeChild(toast); }, 3000);
}
function reindexForms() {
var forms = container.querySelectorAll('.student-form');
forms.forEach(function (formEl, index) {
formEl.querySelectorAll('[data-base-name]').forEach(function (input) {
var base = input.dataset.baseName;
if (input.type === 'radio') {
input.name = base + '_' + index;
input.id = base + '_' + index + '_' + input.value;
} else if (input.type === 'checkbox') {
input.name = base + '[' + index + '][]';
} else {
input.name = base + '[' + index + ']';
}
});
});
}
function updateButtonStates() {
var totalStudents = existingStudents + addedStudents;
var remainingStudents = maxStudents - totalStudents;
var hasStudentForms = container.querySelectorAll('.student-form').length > 0;
// Reset
if (initialButtons) initialButtons.classList.add('d-none');
if (formActionButtons) formActionButtons.classList.add('d-none');
if (saveFormBtn) saveFormBtn.classList.add('d-none');
if (doneWithStudentBtn) doneWithStudentBtn.classList.add('d-none');
// 1) no existing + no draft -> show initial Add
if (existingStudents === 0 && !hasStudentForms) {
if (initialButtons) initialButtons.classList.remove('d-none');
if (addStudentBtn) addStudentBtn.classList.remove('d-none');
return;
}
// 2) existing students present + no draft -> show "Add Another"
if (existingStudents > 0 && !hasStudentForms) {
if (formActionButtons) formActionButtons.classList.remove('d-none');
if (addAnotherStudentBtn) {
addAnotherStudentBtn.classList.remove('d-none');
var atMax = (totalStudents >= maxStudents);
addAnotherStudentBtn.disabled = atMax;
addAnotherStudentBtn.title = atMax
? ('Maximum of ' + maxStudents + ' students reached')
: ('Add student (' + remainingStudents + ' remaining)');
}
return;
}
// 3) at least one draft student form
if (formActionButtons) formActionButtons.classList.remove('d-none');
if (doneWithStudentBtn) doneWithStudentBtn.classList.remove('d-none');
var atMaxStudents = totalStudents >= maxStudents;
if (addAnotherStudentBtn) {
addAnotherStudentBtn.disabled = atMaxStudents;
addAnotherStudentBtn.title = atMaxStudents
? ('Maximum of ' + maxStudents + ' students reached')
: ('Add student (' + remainingStudents + ' remaining)');
if (atMaxStudents && !hasStudentForms) {
addAnotherStudentBtn.classList.add('d-none');
}
}
}
function initializeLiveValidation(scope) {
scope.querySelectorAll('input[data-base-name="studentFirstName"], input[data-base-name="studentLastName"], input[data-base-name="emergency_firstname"], input[data-base-name="emergency_lastname"]').forEach(function (el) {
el.addEventListener('input', function () { validateNameLive(el); });
el.title = 'Only letters, spaces, or dashes (2–30 chars)';
});
scope.querySelectorAll('input[data-base-name="emergency_email"]').forEach(function (el) {
el.addEventListener('input', function () { validateEmailLive(el); });
el.title = 'Email must be in the format: user@example.com';
});
scope.querySelectorAll('input[data-base-name="emergency_phone"]').forEach(function (el) {
el.addEventListener('input', function () { validatePhoneLive(el); });
el.title = 'Phone number must be 10 digits, e.g., 123-456-7890';
});
scope.querySelectorAll('input[data-base-name="dob"]').forEach(function (el) {
el.addEventListener('input', function () { validateAgeLive(el, MIN_AGE, MAX_AGE); });
el.title = 'Age must be between ' + MIN_AGE + ' and ' + MAX_AGE;
});
wireChecklistLiveValidation(scope);
}
function validateRadioGroup(scope, baseName) {
var radios = scope.querySelectorAll('input[data-base-name="' + baseName + '"][type="radio"]');
if (!radios.length) return true;
var names = Array.from(new Set(Array.from(radios).map(function (r) { return r.name; })));
var ok = true;
names.forEach(function (n) {
var group = scope.querySelectorAll('input[name="' + CSS_ESCAPE(n) + '"]');
var oneChecked = Array.from(group).some(function (r) { return r.checked; });
group.forEach(function (r) { r.setCustomValidity(oneChecked ? '' : 'Please select an option.'); });
ok = ok && oneChecked;
});
return ok;
}
function findFirstInvalid(formEl) {
var candidates = formEl.querySelectorAll('input, select, textarea');
for (var i = 0; i < candidates.length; i++) {
var el = candidates[i];
if (!el.checkValidity()) return el;
}
return null;
}
function setDefaultsForNewStudentForm(formEl) {
// Do NOT preselect any radio
formEl.querySelectorAll('input[type="radio"][data-base-name="last_year"]').forEach(function (r) {
r.checked = false;
});
// Keep grade select disabled until a choice is made
var gradeSelect = formEl.querySelector('.grade-select');
var gradeLabel = formEl.querySelector('.grade-label');
if (gradeSelect && gradeLabel) {
gradeSelect.innerHTML = '';
gradeSelect.disabled = true;
gradeLabel.innerHTML = 'Last Year Grade *';
}
}
function addStudentForm() {
if (existingStudents + addedStudents >= maxStudents) {
showToast('Maximum of ' + maxStudents + ' students reached', 'info');
updateButtonStates();
return false;
}
var clone = cloneTemplateContent(studentTmplWrap);
if (!clone) return false;
// ✅ ensure marker class exists
if (!clone.classList.contains('student-form')) clone.classList.add('student-form');
container.classList.remove('d-none');
container.appendChild(clone);
addedStudents++;
reindexForms();
setDefaultsForNewStudentForm(clone);
initializeLiveValidation(clone);
updateButtonStates();
return true;
}
function addEmergencyForm() {
if (!allowEmergency) return false;
if (existingEmergencies + addedEmergencies >= maxEmergency) return false;
var clone = cloneTemplateContent(contactTmplWrap);
if (!clone) return false;
// ✅ ensure marker class exists
if (!clone.classList.contains('emergency-contact-form')) clone.classList.add('emergency-contact-form');
emergencyContainer.classList.remove('d-none');
emergencyContainer.appendChild(clone);
addedEmergencies++;
initializeLiveValidation(clone);
updateButtonStates();
return true;
}
// Disable emergency inputs that shouldn’t participate in THIS submit
function neutralizeEmergencyBlocksForSubmit(formEl, allow) {
var blocks = formEl.querySelectorAll('.emergency-contact-form');
var disabledInputs = [];
if (!blocks.length) return { disabledInputs: disabledInputs };
if (!allow) {
blocks.forEach(function (block) {
block.querySelectorAll('input, select, textarea').forEach(function (el) {
if (!el.disabled) {
el.disabled = true;
disabledInputs.push(el);
}
el.setCustomValidity('');
});
});
return { disabledInputs: disabledInputs };
}
// allow === true: disable completely empty blocks so 'required' won't trigger
blocks.forEach(function (block) {
var fields = block.querySelectorAll('input, select, textarea');
var anyFilled = Array.from(fields).some(function (el) { return (el.value || '').trim().length > 0; });
if (!anyFilled) {
fields.forEach(function (el) {
if (!el.disabled) {
el.disabled = true;
disabledInputs.push(el);
}
el.setCustomValidity('');
});
}
});
return { disabledInputs: disabledInputs };
}
// ---- Listeners ----
if (addStudentBtn && studentTmplWrap) {
addStudentBtn.addEventListener('click', function () {
if (addStudentForm()) updateButtonStates();
});
}
if (addAnotherStudentBtn && studentTmplWrap) {
addAnotherStudentBtn.addEventListener('click', function () {
if (addStudentForm()) updateButtonStates();
});
}
if (doneWithStudentBtn) {
doneWithStudentBtn.addEventListener('click', function () {
var hasEmergencyForms = emergencyContainer.querySelectorAll('.emergency-contact-form').length > 0;
if (allowEmergency && !hasEmergencyForms) addEmergencyForm(); // first-time family
if (saveFormBtn) saveFormBtn.classList.remove('d-none');
doneWithStudentBtn.classList.add('d-none');
});
}
// --- Delete student (SAFE: only intercept draft .student-form) ---
function handleRemoveStudentClick(e) {
const btn = e.target.closest('.js-remove-student');
if (!btn) return;
// If the button is NOT inside a draft student form, let native behavior proceed
const draftBlock = btn.closest('.student-form');
if (!draftBlock) return;
// Draft delete: prevent submit and remove the block
e.preventDefault();
e.stopPropagation();
if (draftBlock.parentNode) draftBlock.parentNode.removeChild(draftBlock);
addedStudents = Math.max(0, addedStudents - 1);
reindexForms();
updateButtonStates();
if (!container.querySelectorAll('.student-form').length) {
if (emergencyContainer) {
emergencyContainer.innerHTML = '';
addedEmergencies = 0;
updateButtonStates();
}
}
}
// Attach listeners (capture = true to beat other handlers)
if (container) container.addEventListener('click', handleRemoveStudentClick, true);
document.addEventListener('click', handleRemoveStudentClick, true);
if (emergencyContainer) {
emergencyContainer.addEventListener('click', function (e) {
var btn = e.target.closest('.js-remove-contact');
if (!btn) return;
var block = btn.closest('.emergency-contact-form');
if (!block) return;
block.remove();
addedEmergencies = Math.max(0, addedEmergencies - 1);
updateButtonStates();
});
}
// Enable grade select only after the last_year radio is chosen
container.addEventListener('change', function (e) {
var target = e.target;
if (target && target.matches('input[type="radio"][data-base-name="last_year"]') && target.checked) {
var studentForm = target.closest('.student-form');
if (!studentForm) return;
if (typeof window.updateGradeOptions === 'function') {
try { window.updateGradeOptions(studentForm, target.value); } catch (ex) { warn('updateGradeOptions error', ex); }
} else {
var gradeSelect = studentForm.querySelector('.grade-select');
if (gradeSelect) gradeSelect.disabled = false;
}
}
});
// ---- Submit & Validation ----
form.addEventListener('submit', function (e) {
try {
console.log('submit handler: start', { action: form.action, method: form.method });
if (isSubmitting) {
console.warn('submit handler: already submitting — ignored');
e.preventDefault();
return;
}
// Disable empty/blocked emergency inputs for THIS attempt
const { disabledInputs } = neutralizeEmergencyBlocksForSubmit(form, allowEmergency);
// ✅ Robust presence detection based on enabled fields inside containers
const studentInputs = form.querySelectorAll(
'#studentFormsContainer input, #studentFormsContainer select, #studentFormsContainer textarea'
);
const emergencyInputs = form.querySelectorAll(
'#emergencyFormsContainer input, #emergencyFormsContainer select, #emergencyFormsContainer textarea'
);
const hasStudentForms = Array.from(studentInputs).some(el => !el.disabled);
const hasEmergencyForms = allowEmergency && Array.from(emergencyInputs).some(el => !el.disabled);
if (!hasStudentForms && !hasEmergencyForms) {
const proceed = confirm('No student or emergency forms detected. Submit anyway?');
if (!proceed) {
e.preventDefault();
// re-enable so user can edit
disabledInputs.forEach(el => el.disabled = false);
console.warn('submit handler: user canceled — no forms present', {
studentInputs: studentInputs.length,
studentEnabled: Array.from(studentInputs).filter(el => !el.disabled).length,
emergencyInputs: emergencyInputs.length,
emergencyEnabled: Array.from(emergencyInputs).filter(el => !el.disabled).length,
allowEmergency
});
return;
}
}
let allValid = true;
// Validate non-emergency inputs (per your existing rules)
form.querySelectorAll('input[data-base-name]:not([data-base-name^="emergency_"])').forEach((input) => {
const base = input.dataset.baseName;
let valid = true;
if (base === 'studentFirstName' || base === 'studentLastName') valid = validateNameLive(input);
else if (base === 'dob') valid = validateAgeLive(input, MIN_AGE, MAX_AGE);
input.setCustomValidity(valid ? '' : 'Please fix this field.');
allValid = allValid && valid;
});
// Required selects (skip disabled)
form.querySelectorAll('select[required]').forEach((sel) => {
if (sel.disabled) { sel.setCustomValidity(''); return; }
const ok = sel.value !== '';
sel.setCustomValidity(ok ? '' : 'Please choose an option.');
allValid = allValid && ok;
});
// Radio groups per student (e.g., last_year)
form.querySelectorAll('.student-form').forEach((sf) => {
const radiosOk = validateRadioGroup(sf, 'last_year');
allValid = allValid && radiosOk;
});
// ---- Validate allergies & medical conditions for each student ----
let groupOk = true;
form.querySelectorAll('.student-form').forEach(function (block, idx) {
block.dataset.index = String(idx); // used for feedback id base
const ok1 = validateChecklistRequired(block, 'allergies', 'allergy_other[]', 'allergy');
const ok2 = validateChecklistRequired(block, 'medical_conditions', 'medical_condition_other[]', 'medical condition');
if (!ok1 || !ok2) groupOk = false;
});
allValid = allValid && groupOk;
// Emergency (allowed + non-empty blocks only)
if (allowEmergency) {
form.querySelectorAll('.emergency-contact-form').forEach(block => {
const anyEnabled = block.querySelector('input:not(:disabled), select:not(:disabled), textarea:not(:disabled)');
if (!anyEnabled) return; // empty block was disabled above
block.querySelectorAll('input[data-base-name]').forEach(input => {
const base = input.dataset.baseName;
let valid = true;
if (base === 'emergency_firstname' || base === 'emergency_lastname') valid = validateNameLive(input);
else if (base === 'emergency_email') valid = validateEmailLive(input);
else if (base === 'emergency_phone') valid = validatePhoneLive(input);
input.setCustomValidity(valid ? '' : 'Please fix this field.');
allValid = allValid && valid;
});
});
}
// Final check
if (!form.checkValidity() || !allValid) {
e.preventDefault();
const firstBad = (function findFirstInvalid(formEl) {
const nodes = formEl.querySelectorAll('input, select, textarea');
for (const el of nodes) { if (!el.checkValidity()) return el; }
return null;
})(form);
if (firstBad) {
console.warn('First invalid:', firstBad, firstBad.name, firstBad.dataset && firstBad.dataset.baseName, firstBad.validationMessage);
try { firstBad.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (_) { }
}
form.reportValidity();
disabledInputs.forEach(el => el.disabled = false); // keep editable
console.warn('submit handler: blocked — invalid');
return;
}
// ✅ SUCCESS: post once, outside this tick (bypass submit handlers)
e.preventDefault();
isSubmitting = true;
console.log('submit handler: valid — posting via form.submit()');
setTimeout(function () { form.submit(); }, 0);
} catch (ex) {
console.error('submit handler: exception', ex);
try { e.preventDefault(); } catch (_) { }
}
});
function revalidateChecklistGroup(el) {
const block = el.closest('.student-form');
if (!block) return;
// Ensure stable id base
if (!block.dataset.index) {
const list = Array.from(block.parentNode.querySelectorAll('.student-form'));
block.dataset.index = String(list.indexOf(block));
}
// Re-validate both groups (safe + simple)
validateChecklistRequired(block, 'allergies', 'allergy_other[]', 'allergy');
validateChecklistRequired(block, 'medical_conditions', 'medical_condition_other[]', 'medical condition');
}
function wireChecklistLiveValidation(scope) {
// Check/uncheck boxes
scope.addEventListener('change', function (e) {
const t = e.target;
if (!t) return;
if (t.matches('input[type="checkbox"][data-base-name="allergies"]') ||
t.matches('input[type="checkbox"][data-base-name="medical_conditions"]')) {
revalidateChecklistGroup(t);
}
});
// Typing in the "Other" input
scope.addEventListener('input', function (e) {
const t = e.target;
if (!t) return;
if (t.matches('input[name="allergy_other[]"], input[name="medical_condition_other[]"], input[data-other-for="allergies"], input[data-other-for="medical_conditions"]')) {
revalidateChecklistGroup(t);
}
});
}
// ---- Init ----
initializeLiveValidation(form);
wireChecklistLiveValidation(form);
updateButtonStates();
// If server repopulated radios, reflect in grade select
container.querySelectorAll('.student-form').forEach(function (sf) {
var checked = sf.querySelector('input[type="radio"][data-base-name="last_year"]:checked');
if (checked && typeof window.updateGradeOptions === 'function') {
try { window.updateGradeOptions(sf, checked.value); } catch (ex) { warn('updateGradeOptions init error', ex); }
}
});
log('validate_student.js loaded', {
maxStudents: maxStudents,
existingStudents: existingStudents,
maxEmergency: maxEmergency,
existingEmergencies: existingEmergencies,
allowEmergency: allowEmergency
});
});
})();