2025-06-17 19:03:48 -06:00

463 lines
14 KiB
JavaScript

/* Linda Lindsay for Ward B School Trustee - Modern Website JavaScript */
(function() {
'use strict';
// DOM Content Loaded Event
document.addEventListener('DOMContentLoaded', function() {
initMobileNavigation();
initSmoothScrolling();
initFormValidation();
initImageLazyLoading();
initAccessibility();
initAnalytics();
});
/**
* Mobile Navigation
*/
function initMobileNavigation() {
const mobileToggle = document.querySelector('.mobile-nav-toggle');
const nav = document.querySelector('.nav');
if (mobileToggle && nav) {
mobileToggle.addEventListener('click', function() {
nav.classList.toggle('active');
mobileToggle.setAttribute('aria-expanded',
nav.classList.contains('active') ? 'true' : 'false'
);
});
// Close mobile nav when clicking outside
document.addEventListener('click', function(e) {
if (!mobileToggle.contains(e.target) && !nav.contains(e.target)) {
nav.classList.remove('active');
mobileToggle.setAttribute('aria-expanded', 'false');
}
});
// Close mobile nav on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && nav.classList.contains('active')) {
nav.classList.remove('active');
mobileToggle.setAttribute('aria-expanded', 'false');
}
});
}
}
/**
* Smooth Scrolling for Anchor Links
*/
function initSmoothScrolling() {
const anchorLinks = document.querySelectorAll('a[href^="#"]');
anchorLinks.forEach(link => {
link.addEventListener('click', function(e) {
const href = this.getAttribute('href');
if (href === '#') return;
const target = document.querySelector(href);
if (target) {
e.preventDefault();
const headerHeight = document.querySelector('.header')?.offsetHeight || 0;
const targetPosition = target.offsetTop - headerHeight - 20;
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
});
}
});
});
}
/**
* Form Validation
*/
function initFormValidation() {
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function(e) {
if (!validateForm(this)) {
e.preventDefault();
}
});
// Real-time validation
const inputs = form.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
input.addEventListener('blur', function() {
validateField(this);
});
});
});
}
/**
* Validate Form
*/
function validateForm(form) {
let isValid = true;
const inputs = form.querySelectorAll('input[required], textarea[required], select[required]');
inputs.forEach(input => {
if (!validateField(input)) {
isValid = false;
}
});
return isValid;
}
/**
* Validate Individual Field
*/
function validateField(field) {
const value = field.value.trim();
const type = field.type;
let isValid = true;
let message = '';
// Clear previous errors
clearFieldError(field);
// Required field validation
if (field.hasAttribute('required') && !value) {
isValid = false;
message = 'This field is required.';
}
// Email validation
else if (type === 'email' && value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
isValid = false;
message = 'Please enter a valid email address.';
}
}
// Phone validation
else if (type === 'tel' && value) {
const phoneRegex = /^[\+]?[1-9]?[\d\s\-\(\)]{7,15}$/;
if (!phoneRegex.test(value)) {
isValid = false;
message = 'Please enter a valid phone number.';
}
}
if (!isValid) {
showFieldError(field, message);
}
return isValid;
}
/**
* Show Field Error
*/
function showFieldError(field, message) {
field.classList.add('error');
let errorElement = field.parentNode.querySelector('.field-error');
if (!errorElement) {
errorElement = document.createElement('div');
errorElement.className = 'field-error';
errorElement.style.color = '#dc3545';
errorElement.style.fontSize = '0.875rem';
errorElement.style.marginTop = '0.25rem';
field.parentNode.appendChild(errorElement);
}
errorElement.textContent = message;
}
/**
* Clear Field Error
*/
function clearFieldError(field) {
field.classList.remove('error');
const errorElement = field.parentNode.querySelector('.field-error');
if (errorElement) {
errorElement.remove();
}
}
/**
* Image Lazy Loading
*/
function initImageLazyLoading() {
const images = document.querySelectorAll('img[data-src]');
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
} else {
// Fallback for browsers without IntersectionObserver
images.forEach(img => {
img.src = img.dataset.src;
img.classList.remove('lazy');
});
}
}
/**
* Accessibility Enhancements
*/
function initAccessibility() {
// Skip to main content link
const skipLink = document.querySelector('.skip-link');
if (skipLink) {
skipLink.addEventListener('click', function(e) {
e.preventDefault();
const main = document.querySelector('main') || document.querySelector('#main');
if (main) {
main.focus();
main.scrollIntoView();
}
});
}
// Keyboard navigation for buttons
const buttons = document.querySelectorAll('button, .btn');
buttons.forEach(button => {
button.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.click();
}
});
});
// Focus management for modals
document.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
trapFocus(e);
}
});
}
/**
* Trap Focus for Modals
*/
function trapFocus(e) {
const modal = document.querySelector('.modal.active');
if (!modal) return;
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
}
/**
* Analytics and Tracking
*/
function initAnalytics() {
// Track page views
trackPageView();
// Track button clicks
const trackingButtons = document.querySelectorAll('[data-track]');
trackingButtons.forEach(button => {
button.addEventListener('click', function() {
const action = this.dataset.track;
trackEvent('button_click', action);
});
});
// Track form submissions
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function() {
const formName = this.name || this.id || 'unknown_form';
trackEvent('form_submit', formName);
});
});
// Track scroll depth
let scrollDepth = 0;
window.addEventListener('scroll', throttle(function() {
const currentScroll = window.scrollY + window.innerHeight;
const totalHeight = document.documentElement.scrollHeight;
const currentDepth = Math.round((currentScroll / totalHeight) * 100);
if (currentDepth > scrollDepth && currentDepth % 25 === 0) {
scrollDepth = currentDepth;
trackEvent('scroll_depth', `${currentDepth}%`);
}
}, 1000));
}
/**
* Track Page View
*/
function trackPageView() {
const page = window.location.pathname + window.location.search;
trackEvent('page_view', page);
}
/**
* Track Event
*/
function trackEvent(action, label) {
// Google Analytics 4 tracking
if (typeof gtag !== 'undefined') {
gtag('event', action, {
event_label: label,
event_category: 'website_interaction'
});
}
// Console log for development
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
console.log('Track Event:', action, label);
}
}
/**
* Throttle Function
*/
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
/**
* Debounce Function
*/
function debounce(func, wait, immediate) {
let timeout;
return function() {
const context = this;
const args = arguments;
const later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
/**
* Utility: Get URL Parameters
*/
function getURLParam(param) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(param);
}
/**
* Utility: Format Phone Number
*/
function formatPhoneNumber(phoneNumberString) {
const cleaned = ('' + phoneNumberString).replace(/\D/g, '');
const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/);
if (match) {
return '(' + match[1] + ') ' + match[2] + '-' + match[3];
}
return null;
}
/**
* Utility: Copy to Clipboard
*/
function copyToClipboard(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function() {
showNotification('Copied to clipboard!');
});
} else {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification('Copied to clipboard!');
}
}
/**
* Show Notification
*/
function showNotification(message, type = 'success') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: var(--primary-green);
color: white;
padding: 1rem 2rem;
border-radius: var(--border-radius);
box-shadow: var(--shadow-medium);
z-index: 10000;
opacity: 0;
transition: opacity 0.3s ease;
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '1';
}, 10);
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
// Export utility functions to global scope for external use
window.LindaLindsayUtils = {
trackEvent,
getURLParam,
formatPhoneNumber,
copyToClipboard,
showNotification
};
})();