recreate project
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
$(document).ready(function(){
|
||||
$('#birth-date').mask('00/00/0000');
|
||||
$('#phone-number').mask('0000-0000');
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
export function validateAgeLive(inputEl, minAge = 5, maxAge = 18) {
|
||||
const dob = inputEl.value.trim();
|
||||
if (!dob) return false;
|
||||
|
||||
const birthDate = normalize(new Date(dob));
|
||||
if (isNaN(birthDate.getTime())) {
|
||||
showFeedback(inputEl, false, "Invalid date format (Use YYYY-MM-DD)");
|
||||
return false;
|
||||
}
|
||||
|
||||
const registrationAgeDeadline = window.appConfig?.registrationAgeDeadline;
|
||||
const deadline = normalize(new Date(registrationAgeDeadline || new Date()));
|
||||
|
||||
// Calculate boundaries - add one day to effectively make the age calculation more lenient
|
||||
const adjustedDeadline = new Date(deadline);
|
||||
adjustedDeadline.setDate(adjustedDeadline.getDate() + 1);
|
||||
|
||||
const minBirthDate = new Date(Date.UTC(adjustedDeadline.getFullYear() - maxAge, adjustedDeadline.getMonth(), adjustedDeadline.getDate())); // oldest
|
||||
const maxBirthDate = new Date(Date.UTC(adjustedDeadline.getFullYear() - minAge, adjustedDeadline.getMonth(), adjustedDeadline.getDate())); // youngest
|
||||
|
||||
const isValidAge = birthDate >= minBirthDate && birthDate <= maxBirthDate;
|
||||
|
||||
// Format date as MM-DD-YYYY for error message (use original deadline, not adjusted)
|
||||
const formatDateMMDDYYYY = (dateStr) => {
|
||||
if (!dateStr) return dateStr;
|
||||
// Parse as UTC to avoid timezone issues that could shift the date
|
||||
const date = new Date(dateStr + 'T00:00:00.000Z');
|
||||
if (isNaN(date.getTime())) return dateStr;
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
const year = date.getUTCFullYear();
|
||||
return `${month}-${day}-${year}`;
|
||||
};
|
||||
|
||||
const errorMessage = isValidAge
|
||||
? ""
|
||||
: `Must be ${minAge}-${maxAge} years old by ${formatDateMMDDYYYY(registrationAgeDeadline)}.`;
|
||||
|
||||
showFeedback(inputEl, isValidAge, errorMessage);
|
||||
return isValidAge;
|
||||
}
|
||||
|
||||
function normalize(date) {
|
||||
return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
}
|
||||
|
||||
|
||||
// Helper function (unchanged)
|
||||
function showFeedback(inputEl, isValid, message) {
|
||||
inputEl.classList.toggle("is-invalid", !isValid);
|
||||
const feedbackEl = inputEl.nextElementSibling || document.createElement("div");
|
||||
feedbackEl.textContent = message;
|
||||
feedbackEl.className = `invalid-feedback ${isValid ? "d-none" : ""}`;
|
||||
inputEl.parentNode.appendChild(feedbackEl);
|
||||
}
|
||||
|
||||
// Set max birthdate (2025 - minAge)
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const minAge = 5; // Must match validateAgeLive's default
|
||||
const deadline = window.appConfig?.registrationAgeDeadline
|
||||
? new Date(window.appConfig.registrationAgeDeadline)
|
||||
: new Date();
|
||||
const maxBirthYear = deadline.getFullYear() - minAge;
|
||||
const maxDateStr = `${maxBirthYear}-12-31`; // Latest allowed birthdate
|
||||
|
||||
document.querySelectorAll(".dob-input").forEach((input) => {
|
||||
input.setAttribute("max", maxDateStr);
|
||||
input.setAttribute(
|
||||
"aria-label",
|
||||
`Born before ${maxBirthYear + 1} (${minAge}+ years by ${deadline.getFullYear()})`
|
||||
);
|
||||
});
|
||||
});
|
||||
Vendored
+6315
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+4450
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+4497
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Vendored
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,21 @@
|
||||
// email_validation.js
|
||||
export function validateEmailLive(inputEl) {
|
||||
const emailRE = /^(?=.{2,50}$)[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
|
||||
const val = inputEl.value.trim();
|
||||
const valid = val === '' || emailRE.test(val);
|
||||
const message = valid ? '' : "Email must be in the format: user@example.com.";
|
||||
|
||||
inputEl.classList.toggle('is-invalid', !valid);
|
||||
|
||||
let feedback = inputEl.nextElementSibling;
|
||||
if (!feedback || !feedback.classList.contains('invalid-feedback')) {
|
||||
feedback = document.createElement('div');
|
||||
feedback.className = 'invalid-feedback';
|
||||
inputEl.parentNode.insertBefore(feedback, inputEl.nextSibling);
|
||||
}
|
||||
|
||||
feedback.textContent = message;
|
||||
feedback.style.display = valid ? 'none' : 'block';
|
||||
|
||||
return valid;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/* modal_validation.js
|
||||
live client-side checks for each .student-edit-form
|
||||
─────────────────────────────────────────────── */
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
/* basic regex rules */
|
||||
const nameRE = /^[A-Za-z\s-]{2,30}$/;
|
||||
const gradeRE = /^[A-Za-z0-9\s-]{1,20}$/;
|
||||
const MIN_AGE = 5, MAX_AGE = 18;
|
||||
|
||||
const calcAge = (d) => {
|
||||
const dob = new Date(d);
|
||||
const today = new Date();
|
||||
let a = today.getFullYear() - dob.getFullYear();
|
||||
const m = today.getMonth() - dob.getMonth();
|
||||
if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) a--;
|
||||
return a;
|
||||
};
|
||||
|
||||
/* attach to every modal edit-form */
|
||||
document.querySelectorAll('.student-edit-form').forEach(form => {
|
||||
|
||||
const feedback = (el, msg) => {
|
||||
let f = el.nextElementSibling;
|
||||
if (!f || !f.classList.contains('invalid-feedback')) {
|
||||
f = document.createElement('div');
|
||||
f.className = 'invalid-feedback';
|
||||
el.after(f);
|
||||
}
|
||||
f.textContent = msg;
|
||||
el.classList.add('is-invalid');
|
||||
};
|
||||
|
||||
const clear = el => {
|
||||
el.classList.remove('is-invalid');
|
||||
const f = el.nextElementSibling;
|
||||
if (f && f.classList.contains('invalid-feedback')) f.textContent = '';
|
||||
};
|
||||
|
||||
/* live input validation */
|
||||
form.addEventListener('input', e => {
|
||||
const t = e.target;
|
||||
clear(t); // reset first
|
||||
|
||||
if (t.name === 'firstname' || t.name === 'lastname') {
|
||||
if (!nameRE.test(t.value.trim())) {
|
||||
feedback(t, 'Only letters, spaces or dashes (2-30)');
|
||||
}
|
||||
}
|
||||
|
||||
if (t.name === 'registration_grade') {
|
||||
if (!gradeRE.test(t.value.trim())) {
|
||||
feedback(t, 'Invalid grade value');
|
||||
}
|
||||
}
|
||||
|
||||
if (t.name === 'dob') {
|
||||
const age = calcAge(t.value);
|
||||
if (!t.value || age < MIN_AGE || age > MAX_AGE) {
|
||||
feedback(t, `Age must be ${MIN_AGE}-${MAX_AGE}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* submit validation */
|
||||
form.addEventListener('submit', e => {
|
||||
let firstBad = null;
|
||||
|
||||
form.querySelectorAll('input,select').forEach(el => clear(el)); // reset all
|
||||
|
||||
const fname = form.querySelector('[name="firstname"]');
|
||||
const lname = form.querySelector('[name="lastname"]');
|
||||
const dob = form.querySelector('[name="dob"]');
|
||||
const grade = form.querySelector('[name="registration_grade"]');
|
||||
const gender= form.querySelector('[name="gender"]');
|
||||
const photo = form.querySelector('[name="photo_consent"]');
|
||||
|
||||
/* run checks */
|
||||
if (!nameRE.test(fname.value.trim())) {
|
||||
feedback(fname,'Only letters, spaces or dashes (2-30)'); firstBad ??= fname;
|
||||
}
|
||||
if (!nameRE.test(lname.value.trim())) {
|
||||
feedback(lname,'Only letters, spaces or dashes (2-30)'); firstBad ??= lname;
|
||||
}
|
||||
const age = calcAge(dob.value);
|
||||
if (!dob.value || age < MIN_AGE || age > MAX_AGE) {
|
||||
feedback(dob,`Age must be ${MIN_AGE}-${MAX_AGE}`); firstBad ??= dob;
|
||||
}
|
||||
if (!gender.value) {
|
||||
feedback(gender,'Required'); firstBad ??= gender;
|
||||
}
|
||||
if (!photo.value) {
|
||||
feedback(photo,'Required'); firstBad ??= photo;
|
||||
}
|
||||
if (!gradeRE.test(grade.value.trim())) {
|
||||
feedback(grade,'Invalid grade'); firstBad ??= grade;
|
||||
}
|
||||
|
||||
if (firstBad) {
|
||||
e.preventDefault();
|
||||
firstBad.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log('modal_validation.js initialised ✅');
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
// name_validation.js
|
||||
export function validateNameLive(inputEl) {
|
||||
// Allow only letters, spaces, and dashes, 2–30 chars
|
||||
const nameRE = /^[A-Za-z -]{2,30}$/;
|
||||
const val = inputEl.value.trim();
|
||||
|
||||
const valid = nameRE.test(val);
|
||||
const message = valid ? '' : 'Only letters, spaces, or dashes (2–30 chars).';
|
||||
|
||||
// Toggle invalid class
|
||||
inputEl.classList.toggle('is-invalid', !valid);
|
||||
|
||||
// Find or create feedback element
|
||||
let feedback = inputEl.nextElementSibling;
|
||||
while (feedback && feedback.nodeType === Node.TEXT_NODE) {
|
||||
feedback = feedback.nextSibling;
|
||||
}
|
||||
if (!feedback || !feedback.classList.contains('invalid-feedback')) {
|
||||
feedback = document.createElement('div');
|
||||
feedback.className = 'invalid-feedback';
|
||||
inputEl.parentNode.insertBefore(feedback, inputEl.nextSibling);
|
||||
}
|
||||
|
||||
feedback.textContent = message;
|
||||
feedback.style.display = valid ? 'none' : 'block';
|
||||
|
||||
// Sync with browser constraint validation
|
||||
inputEl.setCustomValidity(valid ? '' : message);
|
||||
|
||||
return valid;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Phone validation and formatting function
|
||||
export function validatePhoneLive(inputEl) {
|
||||
// Get current value and remove any non-digit characters
|
||||
let val = inputEl.value.replace(/\D/g, '');
|
||||
|
||||
// Format the phone number as 123-456-7890
|
||||
let formattedValue = '';
|
||||
if (val.length > 0) {
|
||||
formattedValue = val.substring(0, 3);
|
||||
if (val.length > 3) {
|
||||
formattedValue += '-' + val.substring(3, 6);
|
||||
}
|
||||
if (val.length > 6) {
|
||||
formattedValue += '-' + val.substring(6, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the input value with formatted version
|
||||
inputEl.value = formattedValue;
|
||||
|
||||
// Check if the phone number is valid (exactly 10 digits)
|
||||
const valid = val.length === 10;
|
||||
|
||||
// Update validation UI - add red border for invalid input
|
||||
if (val.length === 0) {
|
||||
inputEl.classList.remove('is-invalid', 'is-valid');
|
||||
} else if (valid) {
|
||||
inputEl.classList.remove('is-invalid');
|
||||
inputEl.classList.add('is-valid');
|
||||
} else {
|
||||
inputEl.classList.remove('is-valid');
|
||||
inputEl.classList.add('is-invalid');
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
// Prevent form submission if phone is invalid
|
||||
function preventInvalidSubmission() {
|
||||
const forms = document.querySelectorAll('form');
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function (e) {
|
||||
const phoneInput = document.getElementById('phoneInput');
|
||||
if (phoneInput && !validatePhoneLive(phoneInput)) {
|
||||
e.preventDefault();
|
||||
phoneInput.focus();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize the event listener when the page loads
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const phoneInput = document.getElementById('phoneInput');
|
||||
|
||||
// Add input event listener for live validation and formatting
|
||||
phoneInput.addEventListener('input', function () {
|
||||
validatePhoneLive(this);
|
||||
});
|
||||
|
||||
// Add blur event listener to finalize formatting
|
||||
phoneInput.addEventListener('blur', function () {
|
||||
validatePhoneLive(this);
|
||||
});
|
||||
|
||||
// Add focus event to select all text when focused
|
||||
phoneInput.addEventListener('focus', function () {
|
||||
this.select();
|
||||
});
|
||||
|
||||
// Prevent form submission for invalid phone numbers
|
||||
preventInvalidSubmission();
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
(() => {
|
||||
const endpoint = window.PROOFREAD_ENDPOINT;
|
||||
if (!endpoint) return;
|
||||
|
||||
const csrfNameEl = document.getElementById('proofreadCsrfName');
|
||||
const csrfHashEl = document.getElementById('proofreadCsrfHash');
|
||||
|
||||
const resize = (el) => {
|
||||
if (!el) return;
|
||||
el.style.height = 'auto';
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
};
|
||||
|
||||
const isSafeMatch = (m) => {
|
||||
const issueType = (m.rule?.issueType || '').toLowerCase();
|
||||
const repl = m.replacements || [];
|
||||
if (repl.length !== 1) return false;
|
||||
|
||||
const len = m.length ?? 0;
|
||||
if (len > 25) return false;
|
||||
|
||||
if (issueType === 'misspelling') return true;
|
||||
|
||||
const msg = (m.message || '').toLowerCase();
|
||||
if (msg.includes('whitespace') || msg.includes('space') || msg.includes('punctuation')) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const applyMatches = (text, matches) => {
|
||||
const sorted = [...matches].sort((a, b) => (b.offset || 0) - (a.offset || 0));
|
||||
let out = text;
|
||||
|
||||
for (const m of sorted) {
|
||||
const offset = m.offset || 0;
|
||||
const length = m.length || 0;
|
||||
const repl = m.replacements?.[0]?.value;
|
||||
if (typeof repl !== 'string') continue;
|
||||
|
||||
out = out.slice(0, offset) + repl + out.slice(offset + length);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const refreshCsrf = (hash) => {
|
||||
if (!hash) return;
|
||||
if (csrfHashEl) csrfHashEl.value = hash;
|
||||
const name = csrfNameEl ? csrfNameEl.value : null;
|
||||
if (!name) return;
|
||||
document.querySelectorAll(`input[name="${name}"]`).forEach((el) => {
|
||||
el.value = hash;
|
||||
});
|
||||
};
|
||||
|
||||
const setStatus = (targetId, message, isError = false) => {
|
||||
const status = document.querySelector(`[data-status-for="${targetId}"]`);
|
||||
if (!status) return;
|
||||
status.textContent = message;
|
||||
status.classList.toggle('text-danger', !!isError);
|
||||
};
|
||||
|
||||
const clearList = (targetId) => {
|
||||
const list = document.querySelector(`[data-list-for="${targetId}"]`);
|
||||
if (list) list.innerHTML = '';
|
||||
};
|
||||
|
||||
const renderRisky = (targetId, risky, textarea) => {
|
||||
const list = document.querySelector(`[data-list-for="${targetId}"]`);
|
||||
if (!list) return;
|
||||
list.innerHTML = '';
|
||||
|
||||
risky.forEach((match) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'mb-1';
|
||||
|
||||
const suggestion = match.replacements?.[0]?.value || '(no suggestion)';
|
||||
const label = document.createElement('span');
|
||||
label.textContent = `${match.message || 'Suggestion'} → ${suggestion}`;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn btn-link btn-sm ps-0';
|
||||
btn.textContent = 'Apply';
|
||||
btn.addEventListener('click', () => {
|
||||
textarea.value = applyMatches(textarea.value, [match]);
|
||||
resize(textarea);
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
li.remove();
|
||||
});
|
||||
|
||||
li.appendChild(label);
|
||||
li.appendChild(document.createTextNode(' '));
|
||||
li.appendChild(btn);
|
||||
list.appendChild(li);
|
||||
});
|
||||
};
|
||||
|
||||
const proofreadField = async (textarea) => {
|
||||
if (!textarea) return { ok: false };
|
||||
const targetId = textarea.id || textarea.name || '';
|
||||
|
||||
const text = textarea.value || '';
|
||||
if (!text.trim()) {
|
||||
setStatus(targetId, 'Enter text to proofread.', true);
|
||||
clearList(targetId);
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
setStatus(targetId, 'Checking...');
|
||||
clearList(targetId);
|
||||
|
||||
const body = new URLSearchParams();
|
||||
body.append('text', text);
|
||||
const name = csrfNameEl ? csrfNameEl.value : null;
|
||||
const hash = csrfHashEl ? csrfHashEl.value : null;
|
||||
if (name && hash) body.append(name, hash);
|
||||
|
||||
let data;
|
||||
try {
|
||||
const resp = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body,
|
||||
});
|
||||
data = await resp.json();
|
||||
} catch (err) {
|
||||
setStatus(targetId, 'Network error.', true);
|
||||
return { ok: false, error: 'Network error.' };
|
||||
}
|
||||
|
||||
refreshCsrf(data?.csrfHash);
|
||||
|
||||
if (!data || !data.ok) {
|
||||
setStatus(targetId, data?.error || 'Proofread failed.', true);
|
||||
return { ok: false, error: data?.error || 'Proofread failed.' };
|
||||
}
|
||||
|
||||
const matches = data.result?.matches || [];
|
||||
if (!matches.length) {
|
||||
setStatus(targetId, 'No issues found.');
|
||||
return { ok: true, safe: 0, risky: 0 };
|
||||
}
|
||||
|
||||
const safe = matches.filter(isSafeMatch);
|
||||
const risky = matches.filter((m) => !isSafeMatch(m));
|
||||
|
||||
const updated = applyMatches(text, safe);
|
||||
if (updated !== text) {
|
||||
textarea.value = updated;
|
||||
resize(textarea);
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
renderRisky(targetId, risky, textarea);
|
||||
setStatus(targetId, `Auto-fixed: ${safe.length}. Needs review: ${risky.length}.`);
|
||||
return { ok: true, safe: safe.length, risky: risky.length };
|
||||
};
|
||||
|
||||
const proofreadAll = async () => {
|
||||
const btn = document.getElementById('proofreadAllBtn');
|
||||
const summary = document.getElementById('proofreadAllStatus');
|
||||
const fields = Array.from(document.querySelectorAll('textarea.review-field'));
|
||||
if (!fields.length) return;
|
||||
|
||||
if (btn) btn.disabled = true;
|
||||
if (summary) summary.textContent = 'Checking all reviews...';
|
||||
|
||||
let totalSafe = 0;
|
||||
let totalRisky = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const field of fields) {
|
||||
const result = await proofreadField(field);
|
||||
if (result?.safe) totalSafe += result.safe;
|
||||
if (result?.risky) totalRisky += result.risky;
|
||||
if (!result?.ok) errors += 1;
|
||||
}
|
||||
|
||||
if (summary) {
|
||||
summary.textContent = `Auto-fixed: ${totalSafe}. Needs review: ${totalRisky}. ${errors ? 'Errors on ' + errors + ' field(s).' : ''}`;
|
||||
}
|
||||
if (btn) btn.disabled = false;
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const btn = document.getElementById('proofreadAllBtn');
|
||||
if (btn) btn.addEventListener('click', proofreadAll);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,118 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const form = document.querySelector("form");
|
||||
const email = document.getElementById("email");
|
||||
const confirmEmail = document.getElementById("confirmEmail");
|
||||
|
||||
// Disable copy/paste for confirm email
|
||||
confirmEmail.addEventListener("paste", function (e) {
|
||||
e.preventDefault();
|
||||
alert("Pasting is disabled in the confirm email field.");
|
||||
});
|
||||
confirmEmail.addEventListener("copy", function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
form.addEventListener("submit", function (e) {
|
||||
let isValid = true;
|
||||
let messages = [];
|
||||
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const alphanumericPattern = /^[A-Za-z0-9 ]+$/;
|
||||
const alphaOnlyPattern = /^[A-Za-z ]+$/;
|
||||
|
||||
// Validate required fields
|
||||
const requiredFields = [
|
||||
"firstName", "lastName", "email", "confirmEmail", "phone",
|
||||
"street", "city", "state", "zip", "captcha", "acceptPolicy"
|
||||
];
|
||||
|
||||
for (const fieldId of requiredFields) {
|
||||
const field = document.getElementById(fieldId);
|
||||
if (field) {
|
||||
if ((field.type === "checkbox" || field.type === "radio") && !field.checked) {
|
||||
const groupName = field.name || field.id;
|
||||
const anyChecked = document.querySelector(`input[name='${groupName}']:checked`);
|
||||
if (!anyChecked) {
|
||||
messages.push(`Please select a value for ${groupName}.`);
|
||||
isValid = false;
|
||||
}
|
||||
} else if (!field.value.trim()) {
|
||||
messages.push(`Please fill out the ${fieldId} field.`);
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gender radio group validation
|
||||
const genderSelected = document.querySelector("input[name='gender']:checked");
|
||||
if (!genderSelected) {
|
||||
messages.push("Please select your gender.");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Email format check
|
||||
if (!emailPattern.test(email.value.trim())) {
|
||||
messages.push("Email format is invalid.");
|
||||
isValid = false;
|
||||
}
|
||||
if (!emailPattern.test(confirmEmail.value.trim())) {
|
||||
messages.push("Confirm Email format is invalid.");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Email match check
|
||||
if (email.value.trim() !== confirmEmail.value.trim()) {
|
||||
messages.push("Email and Confirm Email do not match.");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// ZIP code validation
|
||||
const zip = document.getElementById("zip");
|
||||
if (zip && !/^\d{5}$/.test(zip.value.trim())) {
|
||||
messages.push("ZIP code must be exactly 5 digits.");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Alphanumeric field validation
|
||||
const alphanumericFields = ["firstName", "lastName", "street", "apt"];
|
||||
for (const id of alphanumericFields) {
|
||||
const field = document.getElementById(id);
|
||||
if (field && field.value.trim() !== "" && !alphanumericPattern.test(field.value)) {
|
||||
messages.push(`${id} must contain only letters, numbers, and spaces.`);
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// City must contain only letters and spaces
|
||||
const city = document.getElementById("city");
|
||||
if (city && city.value.trim() !== "" && !alphaOnlyPattern.test(city.value)) {
|
||||
messages.push("City must contain only letters and spaces.");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Character length validation
|
||||
const lengthChecks = [
|
||||
{ id: "firstName", max: 30 },
|
||||
{ id: "lastName", max: 30 },
|
||||
{ id: "email", max: 30 },
|
||||
{ id: "confirmEmail", max: 30 },
|
||||
{ id: "street", max: 30 },
|
||||
{ id: "city", max: 30 },
|
||||
{ id: "phone", max: 14 },
|
||||
{ id: "apt", max: 5 },
|
||||
];
|
||||
for (const check of lengthChecks) {
|
||||
const field = document.getElementById(check.id);
|
||||
if (field && field.value.length > check.max) {
|
||||
messages.push(`${check.id} must not exceed ${check.max} characters.`);
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
alert(messages.join("\n"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const studentSelect = document.getElementById('student_id');
|
||||
const classSelect = document.getElementById('class_section_id');
|
||||
|
||||
const studentFrame = document.getElementById('studentReportFrame');
|
||||
const classFrame = document.getElementById('classReportFrame');
|
||||
|
||||
const downloadStudentBtn = document.getElementById('downloadStudentBtn');
|
||||
const downloadClassBtn = document.getElementById('downloadClassBtn');
|
||||
|
||||
const printStudentBtn = document.getElementById('printStudentBtn');
|
||||
const printClassBtn = document.getElementById('printClassBtn');
|
||||
|
||||
document.getElementById('studentForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const studentId = studentSelect.value;
|
||||
const viewUrl = `/report-card/student/${studentId}`;
|
||||
studentFrame.src = viewUrl;
|
||||
studentFrame.hidden = false;
|
||||
downloadStudentBtn.href = viewUrl + "?download=1";
|
||||
});
|
||||
|
||||
document.getElementById('classForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const classId = classSelect.value;
|
||||
const viewUrl = `/report-card/class/${classId}`;
|
||||
classFrame.src = viewUrl;
|
||||
classFrame.hidden = false;
|
||||
downloadClassBtn.href = viewUrl + "?download=1";
|
||||
});
|
||||
|
||||
printStudentBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
printIframe('studentReportFrame');
|
||||
});
|
||||
|
||||
printClassBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
printIframe('classReportFrame');
|
||||
});
|
||||
|
||||
function printIframe(id) {
|
||||
const frame = document.getElementById(id);
|
||||
frame.contentWindow.focus();
|
||||
frame.contentWindow.print();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,247 @@
|
||||
class SessionTimeoutManager {
|
||||
constructor() {
|
||||
this.config = {
|
||||
timeout: 1800, // default 30 min
|
||||
warning_time: 1500, // default 5 min before timeout
|
||||
check_interval: 60000, // 1 minute
|
||||
logout_url: '/auth/logout',
|
||||
keep_alive_url: '/session/ping-activity',
|
||||
check_url: '/session/check-timeout'
|
||||
};
|
||||
this.timers = {
|
||||
checkTimer: null,
|
||||
logoutTimer: null,
|
||||
warningTimer: null
|
||||
};
|
||||
this.modal = null;
|
||||
this.warningShown = false;
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
await this.fetchConfig();
|
||||
this.setupEventListeners();
|
||||
this.startPeriodicChecks();
|
||||
console.log('Session timeout manager initialized with config:', this.config);
|
||||
} catch (error) {
|
||||
console.warn('Session timeout using default config due to error:', error.message);
|
||||
this.setupEventListeners();
|
||||
this.startPeriodicChecks();
|
||||
}
|
||||
}
|
||||
|
||||
async fetchConfig() {
|
||||
try {
|
||||
const response = await fetch('/session/get-timeout-config', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
this.config = { ...this.config, ...data };
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load timeout config, using defaults:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Reset timers on user activity
|
||||
const events = ['mousedown', 'keydown', 'scroll', 'touchstart', 'click', 'input'];
|
||||
events.forEach(event => {
|
||||
document.addEventListener(event, () => this.resetActivity(), { passive: true });
|
||||
});
|
||||
|
||||
// Handle visibility change
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden) {
|
||||
this.checkSessionStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async resetActivity() {
|
||||
try {
|
||||
await fetch(this.config.keep_alive_url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
this.clearWarning();
|
||||
} catch (error) {
|
||||
console.warn('Activity reset failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
startPeriodicChecks() {
|
||||
this.timers.checkTimer = setInterval(() => {
|
||||
this.checkSessionStatus();
|
||||
}, this.config.check_interval);
|
||||
}
|
||||
|
||||
async checkSessionStatus() {
|
||||
try {
|
||||
const response = await fetch(this.config.check_url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
switch (data.status) {
|
||||
case 'expired':
|
||||
this.handleSessionExpired(data);
|
||||
break;
|
||||
case 'warning':
|
||||
this.handleSessionWarning(data);
|
||||
break;
|
||||
case 'active':
|
||||
this.handleSessionActive(data);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Session status check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
handleSessionExpired(data) {
|
||||
this.clearWarning();
|
||||
clearInterval(this.timers.checkTimer);
|
||||
|
||||
if (data.redirect) {
|
||||
// Show message before redirect
|
||||
alert(data.message || 'Your session has expired. Please log in again.');
|
||||
window.location.href = data.redirect;
|
||||
} else {
|
||||
window.location.href = this.config.logout_url;
|
||||
}
|
||||
}
|
||||
|
||||
handleSessionWarning(data) {
|
||||
if (!this.warningShown) {
|
||||
this.showWarning(data.time_remaining);
|
||||
} else {
|
||||
this.updateWarning(data.time_remaining);
|
||||
}
|
||||
}
|
||||
|
||||
handleSessionActive(data) {
|
||||
this.clearWarning();
|
||||
}
|
||||
|
||||
showWarning(timeRemaining) {
|
||||
this.warningShown = true;
|
||||
this.createWarningModal(timeRemaining);
|
||||
}
|
||||
|
||||
updateWarning(timeRemaining) {
|
||||
if (this.modal) {
|
||||
const countdownElement = this.modal.querySelector('.countdown');
|
||||
if (countdownElement) {
|
||||
countdownElement.textContent = Math.max(0, timeRemaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createWarningModal(timeRemaining) {
|
||||
this.hideWarning();
|
||||
|
||||
this.modal = document.createElement('div');
|
||||
this.modal.className = 'session-timeout-modal';
|
||||
this.modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.8);
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: Arial, sans-serif;
|
||||
`;
|
||||
|
||||
this.modal.innerHTML = `
|
||||
<div style="background: white; padding: 30px; border-radius: 10px; text-align: center; max-width: 400px; box-shadow: 0 5px 25px rgba(0,0,0,0.3);">
|
||||
<h3 style="color: #d35400; margin-bottom: 20px; font-size: 1.5em;">Session About to Expire</h3>
|
||||
<p style="font-size: 1.1em; margin-bottom: 20px;">
|
||||
Your session will expire in <span class="countdown" style="font-weight: bold; color: #e74c3c;">${timeRemaining}</span> seconds due to inactivity.
|
||||
</p>
|
||||
<div style="margin-top: 25px;">
|
||||
<button onclick="sessionTimeout.continueSession()"
|
||||
style="background: #27ae60; color: white; border: none; padding: 12px 24px;
|
||||
border-radius: 5px; cursor: pointer; margin-right: 15px; font-size: 1em;">
|
||||
Continue Session
|
||||
</button>
|
||||
<button onclick="sessionTimeout.logout()"
|
||||
style="background: #e74c3c; color: white; border: none; padding: 12px 24px;
|
||||
border-radius: 5px; cursor: pointer; font-size: 1em;">
|
||||
Logout Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(this.modal);
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
hideWarning() {
|
||||
if (this.modal) {
|
||||
this.modal.remove();
|
||||
this.modal = null;
|
||||
}
|
||||
this.warningShown = false;
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
clearWarning() {
|
||||
this.hideWarning();
|
||||
}
|
||||
|
||||
continueSession() {
|
||||
this.resetActivity();
|
||||
this.hideWarning();
|
||||
}
|
||||
|
||||
logout() {
|
||||
window.location.href = this.config.logout_url;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
clearInterval(this.timers.checkTimer);
|
||||
this.hideWarning();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize globally
|
||||
const sessionTimeout = new SessionTimeoutManager();
|
||||
|
||||
// Start when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => sessionTimeout.init());
|
||||
} else {
|
||||
sessionTimeout.init();
|
||||
}
|
||||
|
||||
// Make available globally
|
||||
window.sessionTimeout = sessionTimeout;
|
||||
@@ -0,0 +1,119 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.getElementById('gradesForm');
|
||||
if (!form) {
|
||||
console.warn("⚠️ Form with ID 'gradesForm' not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const isScoreInRange = function (value) {
|
||||
const num = Number(value);
|
||||
return Number.isFinite(num) && num >= 0 && num <= 100;
|
||||
};
|
||||
|
||||
const toggleCommentCheckbox = function (wrapper) {
|
||||
if (!wrapper) return;
|
||||
const textarea = wrapper.querySelector('textarea');
|
||||
const label = wrapper.querySelector('label.missing-check');
|
||||
if (!textarea || !label) return;
|
||||
const empty = textarea.value.trim() === '';
|
||||
label.classList.toggle('d-none', !empty);
|
||||
if (!empty) {
|
||||
const checkbox = label.querySelector('input[type="checkbox"]');
|
||||
if (checkbox) checkbox.checked = false;
|
||||
}
|
||||
};
|
||||
|
||||
form.querySelectorAll('.comment-check').forEach(function (wrapper) {
|
||||
const textarea = wrapper.querySelector('textarea');
|
||||
if (!textarea) return;
|
||||
textarea.addEventListener('input', function () {
|
||||
toggleCommentCheckbox(wrapper);
|
||||
});
|
||||
toggleCommentCheckbox(wrapper);
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function (event) {
|
||||
const submitter = event.submitter;
|
||||
if (!submitter || submitter.id !== 'submitScoresLockBtn') {
|
||||
return;
|
||||
}
|
||||
|
||||
const missingEntries = [];
|
||||
const invalidEntries = [];
|
||||
|
||||
form.querySelectorAll('#myTable tbody tr').forEach(function (row) {
|
||||
let studentName = row.getAttribute('data-student-name') || '';
|
||||
if (!studentName) {
|
||||
const cells = row.querySelectorAll('td');
|
||||
const first = cells[0] ? cells[0].textContent.trim() : '';
|
||||
const last = cells[1] ? cells[1].textContent.trim() : '';
|
||||
studentName = (first || last) ? (first + ' ' + last).trim() : 'Student';
|
||||
}
|
||||
const rowMissingSet = new Set();
|
||||
const rowInvalid = [];
|
||||
|
||||
const rawMissingDetails = row.getAttribute('data-missing-details') || '[]';
|
||||
let missingDetails = [];
|
||||
try {
|
||||
missingDetails = JSON.parse(rawMissingDetails) || [];
|
||||
} catch (_) {
|
||||
missingDetails = [];
|
||||
}
|
||||
|
||||
const commentLabels = new Set(['PTAP Comment', 'Midterm Comment', 'Final Comment']);
|
||||
missingDetails.forEach(function (label) {
|
||||
if (!commentLabels.has(label)) {
|
||||
rowMissingSet.add(label);
|
||||
}
|
||||
});
|
||||
|
||||
row.querySelectorAll('[data-score-check="1"]').forEach(function (cell) {
|
||||
const label = cell.getAttribute('data-field-label') || 'Score';
|
||||
const value = cell.getAttribute('data-field-value') || '';
|
||||
if (value !== '' && value !== null && !isScoreInRange(value)) {
|
||||
rowInvalid.push(label);
|
||||
}
|
||||
});
|
||||
|
||||
row.querySelectorAll('.comment-check').forEach(function (wrapper) {
|
||||
const label = wrapper.getAttribute('data-field-label') || 'Comment';
|
||||
const textarea = wrapper.querySelector('textarea');
|
||||
const checkbox = wrapper.querySelector('input.missing-comment-checkbox');
|
||||
const value = textarea ? textarea.value.trim() : '';
|
||||
if (value === '' && (!checkbox || !checkbox.checked)) {
|
||||
rowMissingSet.add(label);
|
||||
}
|
||||
});
|
||||
|
||||
const rowMissing = Array.from(rowMissingSet);
|
||||
if (rowMissing.length) {
|
||||
missingEntries.push(studentName + ': ' + rowMissing.join(', '));
|
||||
}
|
||||
if (rowInvalid.length) {
|
||||
invalidEntries.push(studentName + ': ' + rowInvalid.join(', '));
|
||||
}
|
||||
});
|
||||
|
||||
if (invalidEntries.length || missingEntries.length) {
|
||||
event.preventDefault();
|
||||
const lines = [];
|
||||
if (invalidEntries.length) {
|
||||
lines.push('Scores must be between 0 and 100 for:');
|
||||
lines.push('- ' + invalidEntries.join('\n- '));
|
||||
}
|
||||
if (missingEntries.length) {
|
||||
lines.push('Missing entries (fill or check "Missing ok"):');
|
||||
lines.push('- ' + missingEntries.join('\n- '));
|
||||
}
|
||||
const warningBox = document.getElementById('missingWarning');
|
||||
const warningBody = warningBox ? warningBox.querySelector('.missing-warning-body') : null;
|
||||
if (warningBox && warningBody) {
|
||||
warningBody.textContent = lines.join('\n');
|
||||
warningBox.classList.remove('d-none');
|
||||
warningBox.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
} else {
|
||||
alert(lines.join('\n'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,171 @@
|
||||
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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user