612 lines
25 KiB
JavaScript
612 lines
25 KiB
JavaScript
// /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 = '<option value="">Select Grade</option>';
|
||
gradeSelect.disabled = true;
|
||
gradeLabel.innerHTML = 'Last Year Grade <span class="text-danger">*</span>';
|
||
}
|
||
}
|
||
|
||
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
|
||
});
|
||
});
|
||
})(); |