// Main application JavaScript for CodePress CMS // Enhanced with PWA support and accessibility features // Initialize application when DOM is ready document.addEventListener('DOMContentLoaded', function() { console.log('CodePress CMS v1.5.0 initialized'); // Register Service Worker for PWA if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then(function(registration) { console.log('Service Worker registered:', registration.scope); }) .catch(function(error) { console.log('Service Worker registration failed:', error); }); } // Handle nested dropdowns for touch devices initializeDropdowns(); // Initialize accessibility features initializeAccessibility(); // Initialize form validation initializeFormValidation(); // Initialize PWA features initializePWA(); // Initialize search enhancements initializeSearch(); // Run accessibility tests in development if (window.location.hostname === 'localhost') { setTimeout(runAccessibilityTests, 1000); } }); // Dropdown menu handling function initializeDropdowns() { const dropdownSubmenus = document.querySelectorAll('.dropdown-submenu'); dropdownSubmenus.forEach(function(submenu) { const toggle = submenu.querySelector('.dropdown-toggle'); const dropdown = submenu.querySelector('.dropdown-menu'); if (toggle && dropdown) { // Prevent default link behavior toggle.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); // Close other submenus at the same level const parent = submenu.parentElement; parent.querySelectorAll('.dropdown-submenu').forEach(function(sibling) { if (sibling !== submenu) { sibling.querySelector('.dropdown-menu').classList.remove('show'); } }); // Toggle current submenu dropdown.classList.toggle('show'); }); // Close submenu when clicking outside document.addEventListener('click', function(e) { if (!submenu.contains(e.target)) { dropdown.classList.remove('show'); } }); // Keyboard navigation for dropdowns toggle.addEventListener('keydown', function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); dropdown.classList.toggle('show'); } else if (e.key === 'Escape') { dropdown.classList.remove('show'); toggle.focus(); } }); } }); } // Accessibility enhancements function initializeAccessibility() { // High contrast mode detection if (window.matchMedia && window.matchMedia('(prefers-contrast: high)').matches) { document.documentElement.classList.add('high-contrast'); } // Reduced motion preference if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) { document.documentElement.classList.add('reduced-motion'); } // Focus management document.addEventListener('keydown', function(e) { // Close modals with Escape if (e.key === 'Escape') { const openModals = document.querySelectorAll('.modal.show'); openModals.forEach(modal => { const bsModal = bootstrap.Modal.getInstance(modal); if (bsModal) bsModal.hide(); }); // Close dropdowns const openDropdowns = document.querySelectorAll('.dropdown-menu.show'); openDropdowns.forEach(dropdown => { dropdown.classList.remove('show'); }); } }); // Announce dynamic content changes to screen readers const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { // Announce new content announceToScreenReader('Content updated', 'polite'); } }); }); observer.observe(document.body, { childList: true, subtree: true }); // Focus trap for modals document.addEventListener('shown.bs.modal', function(e) { const modal = e.target; trapFocus(modal); }); document.addEventListener('hidden.bs.modal', function(e) { const modal = e.target; releaseFocusTrap(modal); }); // Enhanced keyboard navigation document.addEventListener('keydown', function(e) { // Skip to content with Ctrl+Home if (e.ctrlKey && e.key === 'Home') { e.preventDefault(); const mainContent = document.getElementById('main-content'); if (mainContent) { mainContent.focus(); mainContent.scrollIntoView(); } } }); } // PWA functionality function initializePWA() { // Install prompt handling let deferredPrompt; window.addEventListener('beforeinstallprompt', function(e) { e.preventDefault(); deferredPrompt = e; // Show install button if desired const installButton = document.createElement('button'); installButton.textContent = 'Install App'; installButton.className = 'btn btn-primary position-fixed bottom-0 end-0 m-3 d-none d-md-block'; installButton.style.zIndex = '1050'; installButton.addEventListener('click', function() { deferredPrompt.prompt(); deferredPrompt.userChoice.then(function(choiceResult) { if (choiceResult.outcome === 'accepted') { console.log('User accepted the install prompt'); } deferredPrompt = null; document.body.removeChild(installButton); }); }); document.body.appendChild(installButton); }); // Online/offline status window.addEventListener('online', function() { console.log('Connection restored'); showToast('Connection restored', 'success'); }); window.addEventListener('offline', function() { console.log('Connection lost'); showToast('You are offline', 'warning'); }); } // Form validation and error handling function initializeFormValidation() { const forms = document.querySelectorAll('form'); forms.forEach(function(form) { form.addEventListener('submit', function(e) { if (!validateForm(form)) { e.preventDefault(); // Focus first invalid field const firstInvalid = form.querySelector('[aria-invalid="true"]'); if (firstInvalid) { firstInvalid.focus(); } } }); // Real-time validation const inputs = form.querySelectorAll('input, select, textarea'); inputs.forEach(function(input) { input.addEventListener('blur', function() { validateField(input); }); input.addEventListener('input', function() { // Clear errors on input clearFieldError(input); }); }); }); } // Validate entire form function validateForm(form) { let isValid = true; const inputs = form.querySelectorAll('input, select, textarea'); inputs.forEach(function(input) { if (!validateField(input)) { isValid = false; } }); return isValid; } // Validate individual field function validateField(field) { const value = field.value.trim(); let isValid = true; let errorMessage = ''; // Required field validation if (field.hasAttribute('required') && !value) { isValid = false; errorMessage = 'This field is required'; } // Email validation if (field.type === 'email' && value) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { isValid = false; errorMessage = 'Please enter a valid email address'; } } // Search field validation (minimum length) if (field.type === 'search' && value && value.length < 2) { isValid = false; errorMessage = 'Please enter at least 2 characters'; } // Update field state field.setAttribute('aria-invalid', isValid ? 'false' : 'true'); if (!isValid) { showFieldError(field, errorMessage); } else { clearFieldError(field); } return isValid; } // Show field error function showFieldError(field, message) { // Remove existing error clearFieldError(field); // Create error message const errorDiv = document.createElement('div'); errorDiv.className = 'invalid-feedback d-block'; errorDiv.setAttribute('role', 'alert'); errorDiv.setAttribute('aria-live', 'polite'); errorDiv.textContent = message; // Add error class to field field.classList.add('is-invalid'); // Insert error after field field.parentNode.insertBefore(errorDiv, field.nextSibling); } // Clear field error function clearFieldError(field) { field.classList.remove('is-invalid'); const errorDiv = field.parentNode.querySelector('.invalid-feedback'); if (errorDiv) { errorDiv.remove(); } } // Enhanced search functionality function initializeSearch() { const searchInputs = document.querySelectorAll('input[type="search"]'); searchInputs.forEach(function(input) { // Clear search on Escape input.addEventListener('keydown', function(e) { if (e.key === 'Escape') { input.value = ''; input.blur(); announceToScreenReader('Search cleared', 'polite'); } }); // Auto-focus search on '/' key document.addEventListener('keydown', function(e) { if (e.key === '/' && !['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) { e.preventDefault(); input.focus(); announceToScreenReader('Search input focused', 'polite'); } }); // Announce search results input.addEventListener('input', debounce(function() { if (input.value.length > 0) { announceToScreenReader(`Searching for: ${input.value}`, 'polite'); } }, 500)); }); } // Toast notification system function showToast(message, type = 'info') { // Create toast container if it doesn't exist let toastContainer = document.querySelector('.toast-container'); if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3'; toastContainer.style.zIndex = '1060'; document.body.appendChild(toastContainer); } // Create toast const toast = document.createElement('div'); toast.className = `toast align-items-center text-white bg-${type} border-0`; toast.setAttribute('role', 'alert'); toast.setAttribute('aria-live', 'assertive'); toast.setAttribute('aria-atomic', 'true'); toast.innerHTML = `
${message}
`; toastContainer.appendChild(toast); // Initialize and show toast const bsToast = new bootstrap.Toast(toast); bsToast.show(); // Remove toast after it's hidden toast.addEventListener('hidden.bs.toast', function() { toast.remove(); }); } // Utility functions for accessibility function announceToScreenReader(message, priority = 'polite') { // Remove existing announcements const existing = document.querySelectorAll('[aria-live]'); existing.forEach(el => { if (el !== document.querySelector('.sr-only[aria-live]')) el.remove(); }); const announcement = document.createElement('div'); announcement.setAttribute('aria-live', priority); announcement.setAttribute('aria-atomic', 'true'); announcement.className = 'sr-only'; announcement.textContent = message; document.body.appendChild(announcement); // Remove after announcement setTimeout(() => { if (announcement.parentNode) { document.body.removeChild(announcement); } }, 1000); } function trapFocus(element) { const focusableElements = element.querySelectorAll( 'a[href], button, textarea, input[type="text"], input[type="search"], ' + 'input[type="email"], select, [tabindex]:not([tabindex="-1"])' ); if (focusableElements.length === 0) return null; const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; function handleTab(e) { if (e.key !== 'Tab') return; if (e.shiftKey) { if (document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } } else { if (document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } } element.addEventListener('keydown', handleTab); // Focus first element firstElement.focus(); // Return cleanup function return function() { element.removeEventListener('keydown', handleTab); }; } function releaseFocusTrap(element) { // Focus trap is automatically released when event listener is removed // This function can be extended for additional cleanup } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Accessibility testing function function runAccessibilityTests() { console.log('๐Ÿงช Running Accessibility Tests...'); const results = { passed: 0, failed: 0, warnings: 0, total: 0 }; // Test 1: Check for alt text on images const images = document.querySelectorAll('img'); images.forEach(img => { results.total++; if (!img.hasAttribute('alt') && !img.hasAttribute('role') && img.getAttribute('role') !== 'presentation') { console.warn('โš ๏ธ Image missing alt text:', img.src); results.warnings++; } else { results.passed++; } }); // Test 2: Check for form labels const inputs = document.querySelectorAll('input:not([type="hidden"]), select, textarea'); inputs.forEach(input => { results.total++; const label = document.querySelector(`label[for="${input.id}"]`); const ariaLabel = input.getAttribute('aria-label'); const ariaLabelledBy = input.getAttribute('aria-labelledby'); if (!label && !ariaLabel && !ariaLabelledBy) { console.error('โŒ Form control missing label:', input.name || input.id); results.failed++; } else { results.passed++; } }); // Test 3: Check heading hierarchy const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); let lastLevel = 0; headings.forEach(heading => { results.total++; const level = parseInt(heading.tagName.charAt(1)); if (level - lastLevel > 1 && lastLevel !== 0) { console.warn('โš ๏ธ Skipped heading level:', heading.textContent.trim().substring(0, 50)); results.warnings++; } else { results.passed++; } lastLevel = level; }); // Test 4: Check ARIA landmarks results.total++; const landmarks = document.querySelectorAll('[role="banner"], [role="main"], [role="complementary"], [role="contentinfo"], header, main, aside, footer'); const uniqueRoles = new Set(); landmarks.forEach(element => { const role = element.getAttribute('role') || element.tagName.toLowerCase(); uniqueRoles.add(role); }); const requiredRoles = ['banner', 'main', 'contentinfo']; let hasRequired = true; requiredRoles.forEach(role => { if (!uniqueRoles.has(role)) { console.error(`โŒ Missing ARIA landmark: ${role}`); hasRequired = false; } }); if (hasRequired) { results.passed++; } else { results.failed++; } // Test 5: Check focus indicators results.total++; const focusableElements = document.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'); if (focusableElements.length === 0) { results.passed++; } else { // Check if focus styles are defined in CSS const computedStyle = getComputedStyle(focusableElements[0]); const outline = computedStyle.outline; const boxShadow = computedStyle.boxShadow; if (outline !== 'none' && outline !== '' && outline !== '0px none rgb(0, 0, 0)') { results.passed++; } else if (boxShadow && boxShadow !== 'none') { results.passed++; } else { console.warn('โš ๏ธ Focus indicators may not be visible'); results.warnings++; } } // Summary console.log(`\n๐Ÿ“Š Accessibility Test Results:`); console.log(`โœ… Passed: ${results.passed}`); console.log(`โŒ Failed: ${results.failed}`); console.log(`โš ๏ธ Warnings: ${results.warnings}`); console.log(`๐Ÿ“ˆ Success Rate: ${Math.round((results.passed / results.total) * 100)}%`); if (results.failed === 0 && results.warnings === 0) { console.log('๐ŸŽ‰ All accessibility tests passed!'); } else if (results.failed === 0) { console.log('๐Ÿ‘ Accessibility compliant with minor warnings'); } else { console.log('โš ๏ธ Accessibility issues found - review and fix'); } return results; } // Utility functions for accessibility function announceToScreenReader(message, priority = 'polite') { // Remove existing announcements const existing = document.querySelectorAll('[aria-live]'); existing.forEach(el => { if (el !== document.querySelector('.sr-only[aria-live]')) el.remove(); }); const announcement = document.createElement('div'); announcement.setAttribute('aria-live', priority); announcement.setAttribute('aria-atomic', 'true'); announcement.className = 'sr-only'; announcement.textContent = message; document.body.appendChild(announcement); // Remove after announcement setTimeout(() => { if (announcement.parentNode) { document.body.removeChild(announcement); } }, 1000); } function releaseFocusTrap(element) { // Focus trap is automatically released when event listener is removed // This function can be extended for additional cleanup } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Accessibility testing function function runAccessibilityTests() { console.log('๐Ÿงช Running Accessibility Tests...'); const results = { passed: 0, failed: 0, warnings: 0, total: 0 }; // Test 1: Check for alt text on images const images = document.querySelectorAll('img'); images.forEach(img => { results.total++; if (!img.hasAttribute('alt') && !img.hasAttribute('role') && img.getAttribute('role') !== 'presentation') { console.warn('โš ๏ธ Image missing alt text:', img.src); results.warnings++; } else { results.passed++; } }); // Test 2: Check for form labels const inputs = document.querySelectorAll('input:not([type="hidden"]), select, textarea'); inputs.forEach(input => { results.total++; const label = document.querySelector(`label[for="${input.id}"]`); const ariaLabel = input.getAttribute('aria-label'); const ariaLabelledBy = input.getAttribute('aria-labelledby'); if (!label && !ariaLabel && !ariaLabelledBy) { console.error('โŒ Form control missing label:', input.name || input.id); results.failed++; } else { results.passed++; } }); // Test 3: Check heading hierarchy const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); let lastLevel = 0; headings.forEach(heading => { results.total++; const level = parseInt(heading.tagName.charAt(1)); if (level - lastLevel > 1 && lastLevel !== 0) { console.warn('โš ๏ธ Skipped heading level:', heading.textContent.trim().substring(0, 50)); results.warnings++; } else { results.passed++; } lastLevel = level; }); // Test 4: Check ARIA landmarks results.total++; const landmarks = document.querySelectorAll('[role="banner"], [role="main"], [role="complementary"], [role="contentinfo"], header, main, aside, footer'); const uniqueRoles = new Set(); landmarks.forEach(element => { const role = element.getAttribute('role') || element.tagName.toLowerCase(); uniqueRoles.add(role); }); const requiredRoles = ['banner', 'main', 'contentinfo']; let hasRequired = true; requiredRoles.forEach(role => { if (!uniqueRoles.has(role)) { console.error(`โŒ Missing ARIA landmark: ${role}`); hasRequired = false; } }); if (hasRequired) { results.passed++; } else { results.failed++; } // Test 5: Check focus indicators results.total++; const focusableElements = document.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'); if (focusableElements.length === 0) { results.passed++; } else { // Check if focus styles are defined in CSS const computedStyle = getComputedStyle(focusableElements[0]); const outline = computedStyle.outline; const boxShadow = computedStyle.boxShadow; if (outline !== 'none' && outline !== '' && outline !== '0px none rgb(0, 0, 0)') { results.passed++; } else if (boxShadow && boxShadow !== 'none') { results.passed++; } else { console.warn('โš ๏ธ Focus indicators may not be visible'); results.warnings++; } } // Summary console.log(`\n๐Ÿ“Š Accessibility Test Results:`); console.log(`โœ… Passed: ${results.passed}`); console.log(`โŒ Failed: ${results.failed}`); console.log(`โš ๏ธ Warnings: ${results.warnings}`); console.log(`๐Ÿ“ˆ Success Rate: ${Math.round((results.passed / results.total) * 100)}%`); if (results.failed === 0 && results.warnings === 0) { console.log('๐ŸŽ‰ All accessibility tests passed!'); } else if (results.failed === 0) { console.log('๐Ÿ‘ Accessibility compliant with minor warnings'); } else { console.log('โš ๏ธ Accessibility issues found - review and fix'); } return results; }