recreate project

This commit is contained in:
root
2026-02-10 22:11:06 -05:00
commit 663c0cdbda
10149 changed files with 1379710 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
$(document).ready(function(){
$('#birth-date').mask('00/00/0000');
$('#phone-number').mask('0000-0000');
})
+73
View File
@@ -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()})`
);
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4450
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4497
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+21
View File
@@ -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;
}
+107
View File
@@ -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 ✅');
});
+31
View File
@@ -0,0 +1,31 @@
// name_validation.js
export function validateNameLive(inputEl) {
// Allow only letters, spaces, and dashes, 230 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 (230 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;
}
+74
View File
@@ -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();
});
+189
View File
@@ -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);
});
})();
+118
View File
@@ -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"));
}
});
});
+47
View File
@@ -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();
}
});
+247
View File
@@ -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;
+119
View File
@@ -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'));
}
}
});
});
+612
View File
@@ -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 (230 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 shouldnt 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
});
});
})();
+171
View File
@@ -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: "410 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 });
}
});
});