recreate project
This commit is contained in:
@@ -0,0 +1,612 @@
|
||||
// /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
|
||||
});
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user