commit 645a5be4be234550526dced7d28da209f78a86f2 Author: sinanisler Date: Sun Jun 22 01:50:22 2025 +0300 repo diff --git a/index.html b/index.html new file mode 100644 index 0000000..cc268d9 --- /dev/null +++ b/index.html @@ -0,0 +1,32 @@ + + + + +TEST PAGE + + + + +

test

+

Lorem ipsum dolor sinan amet Lorem ipsum dolor sinan Lorem ipsum dolor sinan amet +Lorem ipsum dolor sinan amet Lorem ipsum dolor sinan Lorem ipsum dolor sinan amet

+

Lorem ipsum dolor sinan amet Lorem ipsum dolor sinan Lorem ipsum dolor sinan amet +Lorem ipsum dolor sinan amet Lorem ipsum dolor sinan Lorem ipsum dolor sinan amet

+

Lorem ipsum dolor sinan amet Lorem ipsum dolor sinan Lorem ipsum dolor sinan amet +Lorem ipsum dolor sinan amet Lorem ipsum dolor sinan Lorem ipsum dolor sinan amet

+ + +

test

+

Lorem ipsum dolor sinan amet Lorem ipsum dolor sinan Lorem ipsum dolor sinan amet +Lorem ipsum dolor sinan amet Lorem ipsum dolor sinan Lorem ipsum dolor sinan amet

+

Lorem ipsum dolor sinan amet Lorem ipsum dolor sinan Lorem ipsum dolor sinan amet +Lorem ipsum dolor sinan amet Lorem ipsum dolor sinan Lorem ipsum dolor sinan amet

+

Lorem ipsum dolor sinan amet Lorem ipsum dolor sinan Lorem ipsum dolor sinan amet +Lorem ipsum dolor sinan amet Lorem ipsum dolor sinan Lorem ipsum dolor sinan amet

+

Lorem ipsum dolor sinan amet Lorem ipsum dolor sinan Lorem ipsum dolor sinan amet + + + + + + \ No newline at end of file diff --git a/widget.js b/widget.js new file mode 100644 index 0000000..e458d27 --- /dev/null +++ b/widget.js @@ -0,0 +1,1240 @@ +/* +=========================================== + ACCESSIBILITY WIDGET + A comprehensive web accessibility tool +=========================================== +*/ + +// =========================================== +// STYLES & VISUAL ASSETS +// =========================================== + +// Styles for the accessibility widget +const styles = ` + #snn-accessibility-fixed-button { + position: fixed !important; + right: 20px !important; + bottom: 20px !important; + z-index: 9999; + } + #snn-accessibility-button { + background: linear-gradient(135deg, #1e90ff, #00bfff); + border: none; + border-radius: 1000px; + cursor: pointer; + width: 55px; + height: 55px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + transition: 0.2s !important; + } + #snn-accessibility-button:hover { + transform: scale(1.05); + } + #snn-accessibility-button:focus { + outline: 2px solid #fff; + outline-offset: 2px; + } + #snn-accessibility-button svg { + width: 28px; + height: 28px; + fill: #fff; + pointer-events: none; + } + #snn-accessibility-menu { + position: fixed; + top: 0; + right: 0; + width: 330px; + height: 100vh; + overflow-y: auto; + background-color: #f9f9f9; + padding: 0 10px 10px 10px; + display: none; + font-family: Arial, sans-serif; + z-index: 9999; + scrollbar-width: thin; + } + .snn-accessibility-option { + font-size: 16px; + display: flex; + align-items: center; + margin-bottom: 10px; + padding: 14px 10px; + width: calc(100% - 20px); + margin-left:10px; + margin-right:10px; + background-color: #e6e6e6; + color: #333; + border: none; + cursor: pointer; + border-radius: 8px; + transition: background-color 0.2s; + line-height:1 !important; + } + .snn-accessibility-option:hover { + background-color: #d4d4d4; + } + .snn-accessibility-option.active { + background-color: #1e90ff; + color: #fff; + } + .snn-icon { + margin-right: 12px; + width: 28px; + height: 28px; + } + .snn-icon svg { + width: 100%; + height: 100%; + fill: currentColor; + } + .snn-close { + background: none; + border: none; + font-size: 44px; + color: #fff; + cursor: pointer; + margin-left: auto; + line-height:1; + border-radius:1000px; + width:44px; + height:44px; + } + .snn-close:focus { + outline:solid 2px #fff; + } + .snn-close:hover { + color: #333; + } + .snn-header { + display: flex; + align-items: center; + margin-bottom: 20px; + padding:10px; + background:#1e90ff; + height:55px; + } + .snn-title { + margin: 0; + font-size: 22px; + color: #fff; + line-height:1 !important; + margin-left:5px; + font-weight:500; + } + /* 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 #ff6b35 !important; + outline-offset: 2px !important; + box-shadow: 0 0 0 5px rgba(255, 107, 53, 0.3) !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 + const options = [ + { + text: 'Screen Reader', + key: 'screenReader', + customToggleFunction: screenReader.toggle, + icon: icons.screenReader, + requiresFeature: screenReader, + }, + { + text: 'Voice Command', + key: 'voiceControl', + customToggleFunction: voiceControl.toggle, + icon: icons.voiceControl, + requiresFeature: voiceControl, + }, + { + text: 'High Contrast', + key: 'highContrast', + className: 'snn-high-contrast', + icon: icons.highContrast, + target: document.documentElement, + }, + { + text: 'Bigger Text', + key: 'biggerText', + className: 'snn-bigger-text', + icon: icons.biggerText, + }, + { + text: 'Text Spacing', + key: 'textSpacing', + className: 'snn-text-spacing', + icon: icons.textSpacing, + }, + { + text: 'Pause Animations', + key: 'pauseAnimations', + className: 'snn-pause-animations', + icon: icons.pauseAnimations, + }, + { + text: 'Hide Images', + key: 'hideImages', + icon: icons.hideImages, + customToggleFunction: toggleHideImages, + }, + { + text: 'Dyslexia Friendly', + key: 'dyslexiaFont', + className: 'snn-dyslexia-font', + icon: icons.dyslexiaFont, + }, + { + text: 'Bigger Cursor', + key: 'biggerCursor', + className: 'snn-bigger-cursor', + icon: icons.biggerCursor, + }, + { + text: 'Line Height', + key: 'lineHeight', + className: 'snn-line-height', + icon: icons.lineHeight, + }, + { + text: 'Text Align', + key: 'textAlign', + className: 'snn-text-align', + icon: icons.textAlign, + }, + ]; + + // Add regular toggle options + options.forEach((option) => { + const button = createToggleButton( + option.text, + option.key, + option.className, + option.target, + option.customToggleFunction, + option.icon, + option.requiresFeature + ); + menu.appendChild(button); + }); + + // Add new accessibility features + const newFeatures = [ + { + text: 'Reading Mode', + key: 'readingMode', + className: 'snn-reading-mode', + icon: icons.readingMode, + }, + { + text: 'Enhanced Focus', + key: 'enhancedFocus', + className: 'snn-enhanced-focus', + icon: icons.enhancedFocus, + }, + { + text: 'Reduced Motion', + key: 'reducedMotion', + className: 'snn-reduced-motion', + icon: icons.reducedMotion, + }, + ]; + + newFeatures.forEach((feature) => { + const button = createToggleButton( + feature.text, + feature.key, + feature.className, + feature.target, + feature.customToggleFunction, + feature.icon, + feature.requiresFeature + ); + menu.appendChild(button); + }); + + // Add action buttons (font selection and color filters) + const fontButton = createActionButton('Font Selection', handleFontSelection, icons.fontSelection); + menu.appendChild(fontButton); + + 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 + +=========================================== +*/ \ No newline at end of file