/* =========================================== ACCESSIBILITY WIDGET A comprehensive web accessibility tool =========================================== */ // =========================================== // CONFIGURATION VARIABLES // =========================================== // Default configuration - can be overridden by user const DEFAULT_WIDGET_CONFIG = { // Core Features enableHighContrast: true, enableBiggerText: true, enableTextSpacing: true, enablePauseAnimations: true, enableHideImages: true, enableDyslexiaFont: true, enableBiggerCursor: true, enableLineHeight: true, enableTextAlign: true, // Advanced Features enableScreenReader: true, enableVoiceControl: true, enableReadingMode: true, enableEnhancedFocus: true, enableReducedMotion: true, enableFontSelection: true, enableColorFilter: true, // Widget Styling widgetWidth: '440px', widgetPosition: { right: '20px', bottom: '20px' }, // Colors colors: { primary: '#1e90ff', primaryHover: '#00bfff', secondary: '#f9f9f9', text: '#333', textLight: '#fff', border: '#e6e6e6', borderHover: '#d4d4d4', shadow: 'rgba(0, 0, 0, 0.2)', focus: '#ff6b35', focusGlow: 'rgba(255, 107, 53, 0.3)' }, // Button styling button: { size: '55px', borderRadius: '1000px', iconSize: '28px', shadow: '0 4px 8px rgba(0, 0, 0, 0.2)' }, // Menu styling menu: { headerHeight: '55px', padding: '0 10px 10px 10px', optionPadding: '14px 10px', optionMargin: '10px', borderRadius: '8px', fontSize: '16px', titleFontSize: '22px', closeButtonSize: '44px' }, // Typography typography: { fontFamily: 'Arial, sans-serif', fontSize: '16px', titleFontSize: '22px', titleFontWeight: '500', lineHeight: '1' }, // Animation animation: { transition: '0.2s', hoverScale: '1.05' } }; // Function to deep merge user configuration with defaults function mergeConfigs(defaultConfig, userConfig) { const result = { ...defaultConfig }; if (!userConfig) return result; for (const key in userConfig) { if (userConfig.hasOwnProperty(key)) { if (typeof userConfig[key] === 'object' && userConfig[key] !== null && !Array.isArray(userConfig[key])) { result[key] = mergeConfigs(defaultConfig[key] || {}, userConfig[key]); } else { result[key] = userConfig[key]; } } } return result; } // Merge user configuration with defaults // Users can define window.ACCESSIBILITY_WIDGET_CONFIG before loading this script const WIDGET_CONFIG = mergeConfigs(DEFAULT_WIDGET_CONFIG, window.ACCESSIBILITY_WIDGET_CONFIG || {}); // =========================================== // STYLES & VISUAL ASSETS // =========================================== // Generate styles using configuration variables const styles = ` #snn-accessibility-fixed-button { position: fixed !important; right: ${WIDGET_CONFIG.widgetPosition.right} !important; bottom: ${WIDGET_CONFIG.widgetPosition.bottom} !important; z-index: 9999; } #snn-accessibility-button { background: linear-gradient(135deg, ${WIDGET_CONFIG.colors.primary}, ${WIDGET_CONFIG.colors.primaryHover}); border: none; border-radius: ${WIDGET_CONFIG.button.borderRadius}; cursor: pointer; width: ${WIDGET_CONFIG.button.size}; height: ${WIDGET_CONFIG.button.size}; box-shadow: ${WIDGET_CONFIG.button.shadow}; transition: ${WIDGET_CONFIG.animation.transition} !important; } #snn-accessibility-button:hover { transform: scale(${WIDGET_CONFIG.animation.hoverScale}); } #snn-accessibility-button:focus { outline: 2px solid ${WIDGET_CONFIG.colors.textLight}; outline-offset: 2px; } #snn-accessibility-button svg { width: ${WIDGET_CONFIG.button.iconSize}; height: ${WIDGET_CONFIG.button.iconSize}; fill: ${WIDGET_CONFIG.colors.textLight}; pointer-events: none; } #snn-accessibility-menu { position: fixed; top: 0; right: 0; width: ${WIDGET_CONFIG.widgetWidth}; height: 100vh; overflow-y: auto; background-color: ${WIDGET_CONFIG.colors.secondary}; padding: ${WIDGET_CONFIG.menu.padding}; display: none; font-family: ${WIDGET_CONFIG.typography.fontFamily}; z-index: 9999; scrollbar-width: thin; } .snn-accessibility-option { font-size: ${WIDGET_CONFIG.menu.fontSize}; display: flex; align-items: center; margin-bottom: ${WIDGET_CONFIG.menu.optionMargin}; padding: ${WIDGET_CONFIG.menu.optionPadding}; width: calc(100% - ${parseInt(WIDGET_CONFIG.menu.optionMargin) * 2}px); margin-left: ${WIDGET_CONFIG.menu.optionMargin}; margin-right: ${WIDGET_CONFIG.menu.optionMargin}; background-color: ${WIDGET_CONFIG.colors.border}; color: ${WIDGET_CONFIG.colors.text}; border: none; cursor: pointer; border-radius: ${WIDGET_CONFIG.menu.borderRadius}; transition: background-color ${WIDGET_CONFIG.animation.transition}; line-height: ${WIDGET_CONFIG.typography.lineHeight} !important; } .snn-accessibility-option:hover { background-color: ${WIDGET_CONFIG.colors.borderHover}; } .snn-accessibility-option.active { background-color: ${WIDGET_CONFIG.colors.primary}; color: ${WIDGET_CONFIG.colors.textLight}; } .snn-icon { margin-right: 12px; width: ${WIDGET_CONFIG.button.iconSize}; height: ${WIDGET_CONFIG.button.iconSize}; } .snn-icon svg { width: 100%; height: 100%; fill: currentColor; } .snn-close { background: none; border: none; font-size: ${WIDGET_CONFIG.menu.closeButtonSize}; color: ${WIDGET_CONFIG.colors.textLight}; cursor: pointer; margin-left: auto; line-height: ${WIDGET_CONFIG.typography.lineHeight}; border-radius: ${WIDGET_CONFIG.button.borderRadius}; width: ${WIDGET_CONFIG.menu.closeButtonSize}; height: ${WIDGET_CONFIG.menu.closeButtonSize}; } .snn-close:focus { outline: solid 2px ${WIDGET_CONFIG.colors.textLight}; } .snn-close:hover { color: ${WIDGET_CONFIG.colors.text}; } .snn-header { display: flex; align-items: center; margin-bottom: 20px; padding: 10px; background: ${WIDGET_CONFIG.colors.primary}; height: ${WIDGET_CONFIG.menu.headerHeight}; } .snn-title { margin: 0; font-size: ${WIDGET_CONFIG.menu.titleFontSize}; color: ${WIDGET_CONFIG.colors.textLight}; line-height: ${WIDGET_CONFIG.typography.lineHeight} !important; margin-left: 5px; font-weight: ${WIDGET_CONFIG.typography.titleFontWeight}; } /* Accessibility feature styles */ .snn-high-contrast { background-color: #000 !important; color: #fff !important; filter: contrast(1.5) !important; } .snn-high-contrast body *{ background-color: #000 !important; color: #fff !important; filter: contrast(1.5) !important; } .snn-high-contrast #snn-accessibility-menu{ filter: contrast(0.7) !important; } .snn-bigger-text * { font-size: 24px !important; } .snn-text-spacing *:not(#snn-accessibility-menu *, #snn-accessibility-fixed-button *, #snn-accessibility-button *, .snn-accessibility-option *) { letter-spacing: 0.2em !important; word-spacing: 0.3em !important; } .snn-pause-animations * { animation: none !important; transition: none !important; } .snn-dyslexia-font { font-family: 'OpenDyslexic', Arial, sans-serif !important; } .snn-line-height *:not(#snn-accessibility-menu *, #snn-accessibility-fixed-button *, #snn-accessibility-button *, .snn-accessibility-option *) { line-height: 2.5 !important; } .snn-text-align *:not(#snn-accessibility-menu *, #snn-accessibility-fixed-button *, #snn-accessibility-button *, .snn-accessibility-option *) { text-align: left !important; } .snn-bigger-cursor { cursor: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAyNCAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMiAxVjM1TDEwIDI3SDE4TDIgMVoiIGZpbGw9IiMwMDAiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIzIi8+PC9zdmc+'), auto !important; } /* Reading Mode */ .snn-reading-mode { background: #f9f9f9 !important; color: #333 !important; line-height: 1.6 !important; font-family: Georgia, serif !important; } .snn-reading-mode * { background: transparent !important; color: inherit !important; font-family: inherit !important; line-height: inherit !important; } .snn-reading-mode h1, .snn-reading-mode h2, .snn-reading-mode h3, .snn-reading-mode h4, .snn-reading-mode h5, .snn-reading-mode h6 { color: #222 !important; font-weight: bold !important; } .snn-reading-mode img, .snn-reading-mode video, .snn-reading-mode iframe, .snn-reading-mode aside, .snn-reading-mode nav, .snn-reading-mode footer, .snn-reading-mode header, .snn-reading-mode .sidebar, .snn-reading-mode .menu { display: none !important; } /* Font Selection */ .snn-font-arial { font-family: Arial, sans-serif !important; } .snn-font-arial * { font-family: Arial, sans-serif !important; } .snn-font-times { font-family: 'Times New Roman', serif !important; } .snn-font-times * { font-family: 'Times New Roman', serif !important; } .snn-font-verdana { font-family: Verdana, sans-serif !important; } .snn-font-verdana * { font-family: Verdana, sans-serif !important; } /* Color Filters */ .snn-filter-protanopia { filter: url('#protanopia-filter') !important; } .snn-filter-deuteranopia { filter: url('#deuteranopia-filter') !important; } .snn-filter-tritanopia { filter: url('#tritanopia-filter') !important; } .snn-filter-grayscale { filter: grayscale(100%) !important; } /* Enhanced Focus */ .snn-enhanced-focus *:focus { outline: 3px solid ${WIDGET_CONFIG.colors.focus} !important; outline-offset: 2px !important; box-shadow: 0 0 0 5px ${WIDGET_CONFIG.colors.focusGlow} !important; } /* Reduced Motion */ .snn-reduced-motion * { animation: none !important; transition: none !important; } .snn-reduced-motion *::before, .snn-reduced-motion *::after { animation: none !important; transition: none !important; } `; // =========================================== // SVG ICONS // =========================================== // SVG icons const icons = { buttonsvg: ``, highContrast: ``, biggerText: ``, textSpacing: ``, pauseAnimations: ``, hideImages: ``, dyslexiaFont: ``, biggerCursor: ``, lineHeight: ``, textAlign: ``, screenReader: ``, resetAll: ``, voiceControl: ``, readingMode: ``, fontSelection: `Aa`, colorFilter: ``, enhancedFocus: ``, reducedMotion: ``, }; // =========================================== // CORE UTILITY FUNCTIONS // =========================================== // Inject styles and SVG filters into the document function injectStyles() { const styleSheet = document.createElement('style'); styleSheet.innerText = styles; document.head.appendChild(styleSheet); // Add SVG color blindness filters const svgFilters = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svgFilters.style.position = 'absolute'; svgFilters.style.width = '0'; svgFilters.style.height = '0'; svgFilters.innerHTML = ` `; document.body.appendChild(svgFilters); } // =========================================== // PERFORMANCE OPTIMIZATION // =========================================== // Cache for DOM elements to improve performance const domCache = { body: document.body, documentElement: document.documentElement, images: null, lastImageUpdate: 0, getImages: function() { const now = Date.now(); if (!this.images || now - this.lastImageUpdate > 5000) { this.images = document.querySelectorAll('img'); this.lastImageUpdate = now; } return this.images; } }; // Apply saved settings from localStorage (optimized) function applySettings() { const settings = [ { key: 'biggerCursor', className: 'snn-bigger-cursor' }, { key: 'biggerText', className: 'snn-bigger-text' }, { key: 'highContrast', className: 'snn-high-contrast', target: domCache.documentElement }, { key: 'dyslexiaFont', className: 'snn-dyslexia-font' }, { key: 'lineHeight', className: 'snn-line-height' }, { key: 'textAlign', className: 'snn-text-align' }, { key: 'pauseAnimations', className: 'snn-pause-animations' }, { key: 'textSpacing', className: 'snn-text-spacing' }, { key: 'readingMode', className: 'snn-reading-mode' }, { key: 'enhancedFocus', className: 'snn-enhanced-focus' }, { key: 'reducedMotion', className: 'snn-reduced-motion' }, ]; // Batch DOM operations for better performance const bodyClassesToAdd = []; const bodyClassesToRemove = []; const docClassesToAdd = []; const docClassesToRemove = []; settings.forEach(({ key, className, target = domCache.body }) => { const isActive = localStorage.getItem(key) === 'true'; if (className) { if (target === domCache.documentElement) { if (isActive) { docClassesToAdd.push(className); } else { docClassesToRemove.push(className); } } else { if (isActive) { bodyClassesToAdd.push(className); } else { bodyClassesToRemove.push(className); } } } }); // Apply all class changes at once if (bodyClassesToAdd.length > 0) { domCache.body.classList.add(...bodyClassesToAdd); } if (bodyClassesToRemove.length > 0) { domCache.body.classList.remove(...bodyClassesToRemove); } if (docClassesToAdd.length > 0) { domCache.documentElement.classList.add(...docClassesToAdd); } if (docClassesToRemove.length > 0) { domCache.documentElement.classList.remove(...docClassesToRemove); } // Handle font selection const fontClasses = ['snn-font-arial', 'snn-font-times', 'snn-font-verdana']; domCache.body.classList.remove(...fontClasses); const selectedFont = localStorage.getItem('fontSelection'); if (selectedFont) { domCache.body.classList.add(`snn-font-${selectedFont}`); } // Handle color filters const filterClasses = ['snn-filter-protanopia', 'snn-filter-deuteranopia', 'snn-filter-tritanopia', 'snn-filter-grayscale']; domCache.documentElement.classList.remove(...filterClasses); const selectedFilter = localStorage.getItem('colorFilter'); if (selectedFilter) { domCache.documentElement.classList.add(`snn-filter-${selectedFilter}`); } // Handle images with cached query const hideImages = localStorage.getItem('hideImages') === 'true'; const displayStyle = hideImages ? 'none' : ''; domCache.getImages().forEach((img) => { img.style.display = displayStyle; }); if (screenReader.active && screenReader.isSupported) { document.addEventListener('focusin', screenReader.handleFocus); } if (voiceControl.isActive && voiceControl.isSupported) { voiceControl.startListening(); } } // =========================================== // UI COMPONENTS // =========================================== // Create the accessibility button function createAccessibilityButton() { const buttonContainer = document.createElement('div'); buttonContainer.id = 'snn-accessibility-fixed-button'; const button = document.createElement('button'); button.id = 'snn-accessibility-button'; button.innerHTML = icons.buttonsvg; button.setAttribute('aria-label', 'Accessibility Menu'); button.addEventListener('click', function () { toggleMenu(); }); button.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleMenu(); } }); buttonContainer.appendChild(button); document.body.appendChild(buttonContainer); } // Reset all accessibility settings function resetAccessibilitySettings() { const keys = [ 'biggerCursor', 'biggerText', 'dyslexiaFont', 'hideImages', 'lineHeight', 'pauseAnimations', 'screenReader', 'textAlign', 'textSpacing', 'highContrast', 'voiceControl', 'readingMode', 'enhancedFocus', 'reducedMotion', 'fontSelection', 'colorFilter', ]; keys.forEach((key) => localStorage.removeItem(key)); // Remove all CSS classes const cssClasses = [ 'snn-bigger-cursor', 'snn-bigger-text', 'snn-dyslexia-font', 'snn-pause-animations', 'snn-text-spacing', 'snn-line-height', 'snn-text-align', 'snn-reading-mode', 'snn-enhanced-focus', 'snn-reduced-motion', 'snn-font-arial', 'snn-font-times', 'snn-font-verdana' ]; cssClasses.forEach(cls => document.body.classList.remove(cls)); const documentClasses = [ 'snn-high-contrast', 'snn-filter-protanopia', 'snn-filter-deuteranopia', 'snn-filter-tritanopia', 'snn-filter-grayscale' ]; documentClasses.forEach(cls => document.documentElement.classList.remove(cls)); domCache.getImages().forEach((img) => (img.style.display = '')); if (screenReader.active) { screenReader.toggle(false); } if (voiceControl.isActive) { voiceControl.toggle(false); } applySettings(); const buttons = document.querySelectorAll('#snn-accessibility-menu .snn-accessibility-option'); buttons.forEach((button) => { button.classList.remove('active'); button.setAttribute('aria-pressed', 'false'); }); } // Create toggle buttons for accessibility options function createToggleButton( buttonText, localStorageKey, className, targetElement = document.body, customToggleFunction = null, iconSVG = '', requiresFeature = null ) { const button = document.createElement('button'); button.innerHTML = `${iconSVG}${buttonText}`; button.setAttribute('data-key', localStorageKey); button.setAttribute('aria-label', buttonText); button.classList.add('snn-accessibility-option'); // Check if feature is supported if (requiresFeature && !requiresFeature.isSupported) { button.disabled = true; button.setAttribute('title', `${buttonText} is not supported in this browser`); button.style.opacity = '0.5'; return button; } const isActive = localStorage.getItem(localStorageKey) === 'true'; button.setAttribute('aria-pressed', isActive); button.setAttribute('role', 'switch'); if (isActive) { button.classList.add('active'); } button.addEventListener('click', function () { handleToggle(); }); button.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleToggle(); } }); function handleToggle() { const newIsActive = localStorage.getItem(localStorageKey) !== 'true'; // If there's a custom toggle function, call it and check if it succeeded if (customToggleFunction) { const success = customToggleFunction(newIsActive); if (success === false) { // Feature not supported or failed return; } } localStorage.setItem(localStorageKey, newIsActive); button.setAttribute('aria-pressed', newIsActive); if (newIsActive) { button.classList.add('active'); if (className) { targetElement.classList.add(className); } } else { button.classList.remove('active'); if (className) { targetElement.classList.remove(className); } } } return button; } // Create special action buttons (for cycling through options) function createActionButton(buttonText, actionFunction, iconSVG) { const button = document.createElement('button'); button.innerHTML = `${iconSVG}${buttonText}: Default`; button.setAttribute('aria-label', buttonText); button.classList.add('snn-accessibility-option'); // Update initial status updateActionButtonStatus(button, buttonText, actionFunction); button.addEventListener('click', function () { const result = actionFunction(); if (result) { const statusSpan = button.querySelector('.snn-status'); statusSpan.textContent = result; } }); button.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const result = actionFunction(); if (result) { const statusSpan = button.querySelector('.snn-status'); statusSpan.textContent = result; } } }); return button; } // Update action button status on page load function updateActionButtonStatus(button, buttonText, actionFunction) { const statusSpan = button.querySelector('.snn-status'); if (buttonText.includes('Font')) { const currentFont = localStorage.getItem('fontSelection'); statusSpan.textContent = currentFont ? currentFont.charAt(0).toUpperCase() + currentFont.slice(1) : 'Default'; } else if (buttonText.includes('Color')) { const currentFilter = localStorage.getItem('colorFilter'); statusSpan.textContent = currentFilter ? currentFilter.charAt(0).toUpperCase() + currentFilter.slice(1) : 'None'; } } // =========================================== // FEATURE TOGGLE FUNCTIONS // =========================================== // Function to hide or show images (optimized) function toggleHideImages(isActive) { const displayStyle = isActive ? 'none' : ''; domCache.getImages().forEach((img) => { img.style.display = displayStyle; }); } // Font selection handler (optimized) function handleFontSelection() { const fonts = ['arial', 'times', 'verdana']; const currentFont = localStorage.getItem('fontSelection') || 'default'; const currentIndex = fonts.indexOf(currentFont); const nextIndex = (currentIndex + 1) % (fonts.length + 1); // +1 for default // Remove all font classes in one operation const fontClasses = ['snn-font-arial', 'snn-font-times', 'snn-font-verdana']; domCache.body.classList.remove(...fontClasses); if (nextIndex === fonts.length) { // Default font localStorage.removeItem('fontSelection'); return 'Default Font'; } else { const selectedFont = fonts[nextIndex]; localStorage.setItem('fontSelection', selectedFont); domCache.body.classList.add(`snn-font-${selectedFont}`); return selectedFont.charAt(0).toUpperCase() + selectedFont.slice(1); } } // Color filter handler (optimized) function handleColorFilter() { const filters = ['protanopia', 'deuteranopia', 'tritanopia', 'grayscale']; const currentFilter = localStorage.getItem('colorFilter') || 'none'; const currentIndex = filters.indexOf(currentFilter); const nextIndex = (currentIndex + 1) % (filters.length + 1); // +1 for none // Remove all filter classes in one operation const filterClasses = ['snn-filter-protanopia', 'snn-filter-deuteranopia', 'snn-filter-tritanopia', 'snn-filter-grayscale']; domCache.documentElement.classList.remove(...filterClasses); if (nextIndex === filters.length) { // No filter localStorage.removeItem('colorFilter'); return 'No Filter'; } else { const selectedFilter = filters[nextIndex]; localStorage.setItem('colorFilter', selectedFilter); domCache.documentElement.classList.add(`snn-filter-${selectedFilter}`); return selectedFilter.charAt(0).toUpperCase() + selectedFilter.slice(1); } } // =========================================== // ACCESSIBILITY FEATURES // =========================================== // Screen reader functionality const screenReader = { active: localStorage.getItem('screenReader') === 'true', isSupported: 'speechSynthesis' in window, handleFocus: function (event) { if (screenReader.active && screenReader.isSupported) { try { const content = event.target.innerText || event.target.alt || event.target.title || ''; if (content.trim() !== '') { window.speechSynthesis.cancel(); const speech = new SpeechSynthesisUtterance(content); speech.lang = 'en-US'; speech.onerror = function(event) { console.warn('Speech synthesis error:', event.error); }; window.speechSynthesis.speak(speech); } } catch (error) { console.warn('Screen reader error:', error); } } }, toggle: function (isActive) { if (!screenReader.isSupported) { console.warn('Speech synthesis is not supported in this browser.'); return false; } screenReader.active = isActive; localStorage.setItem('screenReader', isActive); try { if (isActive) { document.addEventListener('focusin', screenReader.handleFocus); const feedbackSpeech = new SpeechSynthesisUtterance('Screen reader on'); feedbackSpeech.lang = 'en-US'; feedbackSpeech.onerror = function(event) { console.warn('Speech synthesis feedback error:', event.error); }; window.speechSynthesis.speak(feedbackSpeech); } else { document.removeEventListener('focusin', screenReader.handleFocus); window.speechSynthesis.cancel(); const feedbackSpeech = new SpeechSynthesisUtterance('Screen reader off'); feedbackSpeech.lang = 'en-US'; feedbackSpeech.onerror = function(event) { console.warn('Speech synthesis feedback error:', event.error); }; window.speechSynthesis.speak(feedbackSpeech); } } catch (error) { console.warn('Screen reader toggle error:', error); return false; } return true; }, }; // Voice control functionality const voiceControl = { isActive: localStorage.getItem('voiceControl') === 'true', recognition: null, isSupported: 'SpeechRecognition' in window || 'webkitSpeechRecognition' in window, retryCount: 0, maxRetries: 3, toggle: function (isActive) { if (!voiceControl.isSupported) { console.warn('Speech Recognition API is not supported in this browser.'); return false; } voiceControl.isActive = isActive; localStorage.setItem('voiceControl', isActive); try { if (isActive) { voiceControl.startListening(); } else { if (voiceControl.recognition) { voiceControl.recognition.stop(); voiceControl.recognition = null; } voiceControl.retryCount = 0; } } catch (error) { console.warn('Voice control toggle error:', error); return false; } return true; }, startListening: function () { if (!voiceControl.isSupported) { return; } try { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; voiceControl.recognition = new SpeechRecognition(); voiceControl.recognition.interimResults = false; voiceControl.recognition.lang = 'en-US'; voiceControl.recognition.continuous = false; voiceControl.recognition.onstart = function () { console.log('Voice control activated'); voiceControl.retryCount = 0; }; voiceControl.recognition.onresult = function (event) { try { const command = event.results[0][0].transcript.toLowerCase(); voiceControl.handleVoiceCommand(command); } catch (error) { console.warn('Voice command processing error:', error); } }; voiceControl.recognition.onerror = function (event) { console.warn('Speech recognition error:', event.error); if (event.error === 'no-speech' && voiceControl.retryCount < voiceControl.maxRetries) { voiceControl.retryCount++; setTimeout(() => { if (voiceControl.isActive) { voiceControl.startListening(); } }, 1000); } }; voiceControl.recognition.onend = function () { if (voiceControl.isActive && voiceControl.retryCount < voiceControl.maxRetries) { setTimeout(() => { if (voiceControl.isActive) { voiceControl.startListening(); } }, 100); } }; voiceControl.recognition.start(); } catch (error) { console.warn('Voice control initialization error:', error); } }, handleVoiceCommand: function (command) { console.log(`Received command: ${command}`); try { const commandMap = { 'show menu': 'snn-accessibility-button', 'open menu': 'snn-accessibility-button', 'accessibility menu': 'snn-accessibility-button', 'high contrast': 'highContrast', 'bigger text': 'biggerText', 'large text': 'biggerText', 'text spacing': 'textSpacing', 'pause animations': 'pauseAnimations', 'stop animations': 'pauseAnimations', 'hide images': 'hideImages', 'dyslexia friendly': 'dyslexiaFont', 'dyslexia font': 'dyslexiaFont', 'bigger cursor': 'biggerCursor', 'large cursor': 'biggerCursor', 'line height': 'lineHeight', 'align text': 'textAlign', 'text align': 'textAlign', 'screen reader': 'screenReader', 'voice command': 'voiceControl', 'voice control': 'voiceControl', 'reset all': 'resetAll', 'reset everything': 'resetAll', }; if (command === 'show menu' || command === 'open menu' || command === 'accessibility menu') { if (!menuCache.button) menuCache.init(); if (menuCache.button) { menuCache.button.click(); } return; } if (command === 'reset all' || command === 'reset everything') { resetAccessibilitySettings(); return; } const localStorageKey = commandMap[command]; if (localStorageKey) { // Use cached menu reference if available if (!menuCache.menu) menuCache.init(); const button = menuCache.menu?.querySelector( `.snn-accessibility-option[data-key='${localStorageKey}']` ); if (button) { button.click(); } else { console.log('Button not found for command:', command); } } else { console.log('Command not recognized:', command); } } catch (error) { console.warn('Voice command handling error:', error); } }, }; // Create the accessibility menu function createAccessibilityMenu() { const menu = document.createElement('div'); menu.id = 'snn-accessibility-menu'; menu.style.display = 'none'; menu.setAttribute('role', 'dialog'); menu.setAttribute('aria-labelledby', 'snn-accessibility-title'); menu.setAttribute('aria-hidden', 'true'); const header = document.createElement('div'); header.classList.add('snn-header'); const title = document.createElement('h2'); title.classList.add('snn-title'); title.id = 'snn-accessibility-title'; title.textContent = 'Accessibility Tools'; const closeButton = document.createElement('button'); closeButton.className = 'snn-close'; closeButton.innerHTML = '×'; closeButton.setAttribute('aria-label', 'Close Accessibility Menu'); closeButton.addEventListener('click', function () { closeMenu(); }); closeButton.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); closeMenu(); } }); header.appendChild(title); header.appendChild(closeButton); menu.appendChild(header); // Add accessibility options based on configuration const options = [ { text: 'Screen Reader', key: 'screenReader', customToggleFunction: screenReader.toggle, icon: icons.screenReader, requiresFeature: screenReader, enabled: WIDGET_CONFIG.enableScreenReader, }, { text: 'Voice Command', key: 'voiceControl', customToggleFunction: voiceControl.toggle, icon: icons.voiceControl, requiresFeature: voiceControl, enabled: WIDGET_CONFIG.enableVoiceControl, }, { text: 'High Contrast', key: 'highContrast', className: 'snn-high-contrast', icon: icons.highContrast, target: document.documentElement, enabled: WIDGET_CONFIG.enableHighContrast, }, { text: 'Bigger Text', key: 'biggerText', className: 'snn-bigger-text', icon: icons.biggerText, enabled: WIDGET_CONFIG.enableBiggerText, }, { text: 'Text Spacing', key: 'textSpacing', className: 'snn-text-spacing', icon: icons.textSpacing, enabled: WIDGET_CONFIG.enableTextSpacing, }, { text: 'Pause Animations', key: 'pauseAnimations', className: 'snn-pause-animations', icon: icons.pauseAnimations, enabled: WIDGET_CONFIG.enablePauseAnimations, }, { text: 'Hide Images', key: 'hideImages', icon: icons.hideImages, customToggleFunction: toggleHideImages, enabled: WIDGET_CONFIG.enableHideImages, }, { text: 'Dyslexia Friendly', key: 'dyslexiaFont', className: 'snn-dyslexia-font', icon: icons.dyslexiaFont, enabled: WIDGET_CONFIG.enableDyslexiaFont, }, { text: 'Bigger Cursor', key: 'biggerCursor', className: 'snn-bigger-cursor', icon: icons.biggerCursor, enabled: WIDGET_CONFIG.enableBiggerCursor, }, { text: 'Line Height', key: 'lineHeight', className: 'snn-line-height', icon: icons.lineHeight, enabled: WIDGET_CONFIG.enableLineHeight, }, { text: 'Text Align', key: 'textAlign', className: 'snn-text-align', icon: icons.textAlign, enabled: WIDGET_CONFIG.enableTextAlign, }, { text: 'Reading Mode', key: 'readingMode', className: 'snn-reading-mode', icon: icons.readingMode, enabled: WIDGET_CONFIG.enableReadingMode, }, { text: 'Enhanced Focus', key: 'enhancedFocus', className: 'snn-enhanced-focus', icon: icons.enhancedFocus, enabled: WIDGET_CONFIG.enableEnhancedFocus, }, { text: 'Reduced Motion', key: 'reducedMotion', className: 'snn-reduced-motion', icon: icons.reducedMotion, enabled: WIDGET_CONFIG.enableReducedMotion, }, ]; // Add enabled toggle options options.forEach((option) => { if (option.enabled) { const button = createToggleButton( option.text, option.key, option.className, option.target, option.customToggleFunction, option.icon, option.requiresFeature ); menu.appendChild(button); } }); // Add action buttons (font selection and color filters) if enabled if (WIDGET_CONFIG.enableFontSelection) { const fontButton = createActionButton('Font Selection', handleFontSelection, icons.fontSelection); menu.appendChild(fontButton); } if (WIDGET_CONFIG.enableColorFilter) { const colorButton = createActionButton('Color Filter', handleColorFilter, icons.colorFilter); menu.appendChild(colorButton); } // Reset All Button const resetButton = document.createElement('button'); resetButton.innerHTML = `${icons.resetAll}Reset All`; resetButton.setAttribute('aria-label', 'Reset All Accessibility Settings'); resetButton.classList.add('snn-accessibility-option'); resetButton.addEventListener('click', resetAccessibilitySettings); menu.appendChild(resetButton); document.body.appendChild(menu); } // =========================================== // MENU MANAGEMENT // =========================================== // Cache for menu elements const menuCache = { menu: null, button: null, closeButton: null, init: function() { this.menu = document.getElementById('snn-accessibility-menu'); this.button = document.getElementById('snn-accessibility-button'); this.closeButton = this.menu?.querySelector('.snn-close'); } }; // Menu control functions (optimized) function toggleMenu() { if (!menuCache.menu) menuCache.init(); const isOpen = menuCache.menu.style.display === 'block'; if (isOpen) { closeMenu(); } else { openMenu(); } } function openMenu() { if (!menuCache.menu) menuCache.init(); menuCache.menu.style.display = 'block'; menuCache.menu.setAttribute('aria-hidden', 'false'); if (menuCache.closeButton) { menuCache.closeButton.focus(); } // Add keyboard navigation document.addEventListener('keydown', handleMenuKeyboard); } function closeMenu() { if (!menuCache.menu) menuCache.init(); menuCache.menu.style.display = 'none'; menuCache.menu.setAttribute('aria-hidden', 'true'); if (menuCache.button) { menuCache.button.focus(); } // Remove keyboard navigation document.removeEventListener('keydown', handleMenuKeyboard); } // Cache for keyboard navigation elements let keyboardCache = { focusableElements: null, lastUpdate: 0, getFocusableElements: function() { const now = Date.now(); if (!this.focusableElements || now - this.lastUpdate > 1000) { if (menuCache.menu) { this.focusableElements = { all: menuCache.menu.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'), options: Array.from(menuCache.menu.querySelectorAll('.snn-accessibility-option, .snn-close')) }; this.lastUpdate = now; } } return this.focusableElements; } }; function handleMenuKeyboard(e) { if (!menuCache.menu || menuCache.menu.style.display !== 'block') return; if (e.key === 'Escape') { e.preventDefault(); closeMenu(); return; } const elements = keyboardCache.getFocusableElements(); if (!elements) return; if (e.key === 'Tab') { const firstElement = elements.all[0]; const lastElement = elements.all[elements.all.length - 1]; if (e.shiftKey) { if (document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } } else { if (document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } } if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); const currentIndex = elements.options.indexOf(document.activeElement); let nextIndex; if (e.key === 'ArrowDown') { nextIndex = currentIndex === elements.options.length - 1 ? 0 : currentIndex + 1; } else { nextIndex = currentIndex === 0 ? elements.options.length - 1 : currentIndex - 1; } elements.options[nextIndex].focus(); } } // =========================================== // INITIALIZATION // =========================================== // Initialize the widget function initAccessibilityWidget() { injectStyles(); applySettings(); createAccessibilityButton(); createAccessibilityMenu(); } // =========================================== // WIDGET BOOTSTRAP // =========================================== // Load the widget when the DOM is fully loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAccessibilityWidget); } else { initAccessibilityWidget(); } /* =========================================== WIDGET FEATURES SUMMARY: Core Features: - High contrast mode - Text size adjustment - Text spacing modification - Animation pausing - Image hiding - Dyslexia-friendly font - Cursor size adjustment - Line height adjustment - Text alignment Advanced Features: - Screen reader with speech synthesis - Voice control with speech recognition - Reading mode - Enhanced focus indicators - Reduced motion mode - Font selection (Arial, Times, Verdana) - Color blindness filters (Protanopia, Deuteranopia, Tritanopia, Grayscale) Technical Features: - Persistent settings via localStorage - Full keyboard navigation - ARIA compliance - Error handling for browser compatibility - Performance optimization with DOM caching - Single file deployment =========================================== */