/* 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 }; })();