/* =========================================== ACCESSIBILITY WIDGET github.com/sinanisler/accessibility-widgets github.com/sponsors/sinanisler =========================================== */ // =========================================== // TRANSLATIONS // =========================================== const TRANSLATIONS = { en: { accessibilityMenu: 'Accessibility Menu', closeAccessibilityMenu: 'Close Accessibility Menu', accessibilityTools: 'Accessibility Tools', resetAllSettings: 'Reset All Settings', screenReader: 'Screen Reader', voiceCommand: 'Voice Command', textSpacing: 'Text Spacing', pauseAnimations: 'Pause Animations', hideImages: 'Hide Images', dyslexiaFriendly: 'Dyslexia Friendly', biggerCursor: 'Bigger Cursor', lineHeight: 'Line Height', fontSelection: 'Font Selection', colorFilter: 'Color Filter', textAlign: 'Text Align', textSize: 'Text Size', highContrast: 'High Contrast', defaultFont: 'Default Font', noFilter: 'No Filter', default: 'Default', screenReaderOn: 'Screen reader on', screenReaderOff: 'Screen reader off', voiceControlActivated: 'Voice control activated', notSupportedBrowser: 'is not supported in this browser', close: 'Close', reset: 'Reset', saturation: 'Saturation', selectLanguage: 'Select Language' }, de: { accessibilityMenu: 'Barrierefreiheitsmenü', closeAccessibilityMenu: 'Barrierefreiheitsmenü schließen', accessibilityTools: 'Barrierefreiheitswerkzeuge', resetAllSettings: 'Alle Einstellungen zurücksetzen', screenReader: 'Screenreader', voiceCommand: 'Sprachbefehl', textSpacing: 'Textabstand', pauseAnimations: 'Animationen pausieren', hideImages: 'Bilder ausblenden', dyslexiaFriendly: 'Legasthenie-freundlich', biggerCursor: 'Größerer Cursor', lineHeight: 'Zeilenhöhe', fontSelection: 'Schriftauswahl', colorFilter: 'Farbfilter', textAlign: 'Textausrichtung', textSize: 'Textgröße', highContrast: 'Hoher Kontrast', defaultFont: 'Standardschrift', noFilter: 'Kein Filter', default: 'Standard', screenReaderOn: 'Screenreader ein', screenReaderOff: 'Screenreader aus', voiceControlActivated: 'Sprachsteuerung aktiviert', notSupportedBrowser: 'wird in diesem Browser nicht unterstützt', close: 'Schließen', reset: 'Zurücksetzen', saturation: 'Sättigung', selectLanguage: 'Sprache wählen' }, es: { accessibilityMenu: 'Menú de Accesibilidad', closeAccessibilityMenu: 'Cerrar Menú de Accesibilidad', accessibilityTools: 'Herramientas de Accesibilidad', resetAllSettings: 'Restablecer Todas las Configuraciones', screenReader: 'Lector de Pantalla', voiceCommand: 'Comando de Voz', textSpacing: 'Espaciado de Texto', pauseAnimations: 'Pausar Animaciones', hideImages: 'Ocultar Imágenes', dyslexiaFriendly: 'Amigable para Dislexia', biggerCursor: 'Cursor Más Grande', lineHeight: 'Altura de Línea', fontSelection: 'Selección de Fuente', colorFilter: 'Filtro de Color', textAlign: 'Alineación de Texto', textSize: 'Tamaño de Texto', highContrast: 'Alto Contraste', defaultFont: 'Fuente Predeterminada', noFilter: 'Sin Filtro', default: 'Predeterminado', screenReaderOn: 'Lector de pantalla activado', screenReaderOff: 'Lector de pantalla desactivado', voiceControlActivated: 'Control de voz activado', notSupportedBrowser: 'no es compatible con este navegador', close: 'Cerrar', reset: 'Restablecer', saturation: 'Saturación', selectLanguage: 'Seleccionar Idioma' }, it: { accessibilityMenu: 'Menu Accessibilità', closeAccessibilityMenu: 'Chiudi Menu Accessibilità', accessibilityTools: 'Strumenti di Accessibilità', resetAllSettings: 'Ripristina Tutte le Impostazioni', screenReader: 'Lettore Schermo', voiceCommand: 'Comando Vocale', textSpacing: 'Spaziatura Testo', pauseAnimations: 'Pausa Animazioni', hideImages: 'Nascondi Immagini', dyslexiaFriendly: 'Adatto alla Dislessia', biggerCursor: 'Cursore Più Grande', lineHeight: 'Altezza Linea', fontSelection: 'Selezione Font', colorFilter: 'Filtro Colore', textAlign: 'Allineamento Testo', textSize: 'Dimensione Testo', highContrast: 'Alto Contrasto', defaultFont: 'Font Predefinito', noFilter: 'Nessun Filtro', default: 'Predefinito', screenReaderOn: 'Lettore schermo attivo', screenReaderOff: 'Lettore schermo disattivo', voiceControlActivated: 'Controllo vocale attivato', notSupportedBrowser: 'non è supportato in questo browser', close: 'Chiudi', reset: 'Ripristina', saturation: 'Saturazione', selectLanguage: 'Seleziona Lingua' }, fr: { accessibilityMenu: 'Menu Accessibilité', closeAccessibilityMenu: 'Fermer le Menu Accessibilité', accessibilityTools: 'Outils d\'Accessibilité', resetAllSettings: 'Réinitialiser Tous les Paramètres', screenReader: 'Lecteur d\'Écran', voiceCommand: 'Commande Vocale', textSpacing: 'Espacement du Texte', pauseAnimations: 'Mettre en Pause les Animations', hideImages: 'Masquer les Images', dyslexiaFriendly: 'Convivial pour la Dyslexie', biggerCursor: 'Curseur Plus Grand', lineHeight: 'Hauteur de Ligne', fontSelection: 'Sélection de Police', colorFilter: 'Filtre de Couleur', textAlign: 'Alignement du Texte', textSize: 'Taille du Texte', highContrast: 'Contraste Élevé', defaultFont: 'Police par Défaut', noFilter: 'Aucun Filtre', default: 'Par Défaut', screenReaderOn: 'Lecteur d\'écran activé', screenReaderOff: 'Lecteur d\'écran désactivé', voiceControlActivated: 'Contrôle vocal activé', notSupportedBrowser: 'n\'est pas pris en charge dans ce navigateur', close: 'Fermer', reset: 'Réinitialiser', saturation: 'Saturation', selectLanguage: 'Sélectionner la Langue' }, ru: { accessibilityMenu: 'Меню Доступности', closeAccessibilityMenu: 'Закрыть Меню Доступности', accessibilityTools: 'Инструменты Доступности', resetAllSettings: 'Сбросить Все Настройки', screenReader: 'Программа Чтения с Экрана', voiceCommand: 'Голосовая Команда', textSpacing: 'Межбуквенный Интервал', pauseAnimations: 'Приостановить Анимацию', hideImages: 'Скрыть Изображения', dyslexiaFriendly: 'Для Дислексии', biggerCursor: 'Увеличенный Курсор', lineHeight: 'Высота Строки', fontSelection: 'Выбор Шрифта', colorFilter: 'Цветовой Фильтр', textAlign: 'Выравнивание Текста', textSize: 'Размер Текста', highContrast: 'Высокая Контрастность', defaultFont: 'Шрифт по Умолчанию', noFilter: 'Без Фильтра', default: 'По Умолчанию', screenReaderOn: 'Программа чтения включена', screenReaderOff: 'Программа чтения выключена', voiceControlActivated: 'Голосовое управление активировано', notSupportedBrowser: 'не поддерживается в этом браузере', close: 'Закрыть', reset: 'Сбросить', saturation: 'Насыщенность', selectLanguage: 'Выберите Язык' }, tr: { accessibilityMenu: 'Erişilebilirlik Menüsü', closeAccessibilityMenu: 'Erişilebilirlik Menüsünü Kapat', accessibilityTools: 'Erişilebilirlik Araçları', resetAllSettings: 'Tüm Ayarları Sıfırla', screenReader: 'Ekran Okuyucu', voiceCommand: 'Sesli Komut', textSpacing: 'Metin Aralığı', pauseAnimations: 'Animasyonları Duraklat', hideImages: 'Resimleri Gizle', dyslexiaFriendly: 'Disleksi Dostu', biggerCursor: 'Daha Büyük İmleç', lineHeight: 'Satır Yüksekliği', fontSelection: 'Yazı Tipi Seçimi', colorFilter: 'Renk Filtresi', textAlign: 'Metin Hizalama', textSize: 'Metin Boyutu', highContrast: 'Yüksek Kontrast', defaultFont: 'Varsayılan Yazı Tipi', noFilter: 'Filtre Yok', default: 'Varsayılan', screenReaderOn: 'Ekran okuyucu açık', screenReaderOff: 'Ekran okuyucu kapalı', voiceControlActivated: 'Sesli kontrol etkinleştirildi', notSupportedBrowser: 'bu tarayıcıda desteklenmiyor', close: 'Kapat', reset: 'Sıfırla', saturation: 'Doygunluk', selectLanguage: 'Dil Seçin' }, ar: { accessibilityMenu: 'قائمة إمكانية الوصول', closeAccessibilityMenu: 'إغلاق قائمة إمكانية الوصول', accessibilityTools: 'أدوات إمكانية الوصول', resetAllSettings: 'إعادة تعيين جميع الإعدادات', screenReader: 'قارئ الشاشة', voiceCommand: 'الأمر الصوتي', textSpacing: 'تباعد النص', pauseAnimations: 'إيقاف الرسوم المتحركة مؤقتًا', hideImages: 'إخفاء الصور', dyslexiaFriendly: 'صديق لعسر القراءة', biggerCursor: 'مؤشر أكبر', lineHeight: 'ارتفاع الخط', fontSelection: 'اختيار الخط', colorFilter: 'مرشح الألوان', textAlign: 'محاذاة النص', textSize: 'حجم النص', highContrast: 'تباين عالي', defaultFont: 'الخط الافتراضي', noFilter: 'بدون مرشح', default: 'افتراضي', screenReaderOn: 'قارئ الشاشة مفعّل', screenReaderOff: 'قارئ الشاشة معطل', voiceControlActivated: 'تم تفعيل التحكم الصوتي', notSupportedBrowser: 'غير مدعوم في هذا المتصفح', close: 'إغلاق', reset: 'إعادة تعيين', saturation: 'التشبع', selectLanguage: 'اختر اللغة' }, hi: { accessibilityMenu: 'पहुँच मेनू', closeAccessibilityMenu: 'पहुँच मेनू बंद करें', accessibilityTools: 'पहुँच उपकरण', resetAllSettings: 'सभी सेटिंग्स रीसेट करें', screenReader: 'स्क्रीन रीडर', voiceCommand: 'वॉयस कमांड', textSpacing: 'टेक्स्ट स्पेसिंग', pauseAnimations: 'एनिमेशन रोकें', hideImages: 'चित्र छिपाएँ', dyslexiaFriendly: 'डिस्लेक्सिया के अनुकूल', biggerCursor: 'बड़ा कर्सर', lineHeight: 'लाइन की ऊँचाई', fontSelection: 'फ़ॉन्ट चयन', colorFilter: 'रंग फ़िल्टर', textAlign: 'टेक्स्ट संरेखण', textSize: 'टेक्स्ट का आकार', highContrast: 'उच्च कंट्रास्ट', defaultFont: 'डिफ़ॉल्ट फ़ॉन्ट', noFilter: 'कोई फ़िल्टर नहीं', default: 'डिफ़ॉल्ट', screenReaderOn: 'स्क्रीन रीडर चालू', screenReaderOff: 'स्क्रीन रीडर बंद', voiceControlActivated: 'वॉयस नियंत्रण सक्रिय', notSupportedBrowser: 'इस ब्राउज़र में समर्थित नहीं है', close: 'बंद करें', reset: 'रीसेट करें', saturation: 'संतृप्ति', selectLanguage: 'भाषा चुनें' }, 'zh-cn': { accessibilityMenu: '辅助功能菜单', closeAccessibilityMenu: '关闭辅助功能菜单', accessibilityTools: '辅助功能工具', resetAllSettings: '重置所有设置', screenReader: '屏幕阅读器', voiceCommand: '语音命令', textSpacing: '文本间距', pauseAnimations: '暂停动画', hideImages: '隐藏图片', dyslexiaFriendly: '阅读障碍友好', biggerCursor: '更大的光标', lineHeight: '行高', fontSelection: '字体选择', colorFilter: '颜色滤镜', textAlign: '文本对齐', textSize: '文本大小', highContrast: '高对比度', defaultFont: '默认字体', noFilter: '无滤镜', default: '默认', screenReaderOn: '屏幕阅读器已开启', screenReaderOff: '屏幕阅读器已关闭', voiceControlActivated: '语音控制已激活', notSupportedBrowser: '此浏览器不支持', close: '关闭', reset: '重置', saturation: '饱和度', selectLanguage: '选择语言' }, jp: { accessibilityMenu: 'アクセシビリティメニュー', closeAccessibilityMenu: 'アクセシビリティメニューを閉じる', accessibilityTools: 'アクセシビリティツール', resetAllSettings: 'すべての設定をリセット', screenReader: 'スクリーンリーダー', voiceCommand: '音声コマンド', textSpacing: 'テキスト間隔', pauseAnimations: 'アニメーション一時停止', hideImages: '画像を非表示', dyslexiaFriendly: 'ディスレクシア対応', biggerCursor: '大きいカーソル', lineHeight: '行の高さ', fontSelection: 'フォント選択', colorFilter: 'カラーフィルター', textAlign: 'テキスト配置', textSize: 'テキストサイズ', highContrast: 'ハイコントラスト', defaultFont: 'デフォルトフォント', noFilter: 'フィルターなし', default: 'デフォルト', screenReaderOn: 'スクリーンリーダーがオン', screenReaderOff: 'スクリーンリーダーがオフ', voiceControlActivated: '音声制御が有効', notSupportedBrowser: 'このブラウザではサポートされていません', close: '閉じる', reset: 'リセット', saturation: '彩度', selectLanguage: '言語を選択' } }; // Language detection and management let currentLanguage = 'en'; function detectBrowserLanguage() { const browserLang = (navigator.language || navigator.userLanguage).toLowerCase(); // Direct match if (TRANSLATIONS[browserLang]) { return browserLang; } // Try language code only (e.g., 'en' from 'en-US') const langCode = browserLang.split('-')[0]; if (TRANSLATIONS[langCode]) { return langCode; } // Special case for Chinese if (browserLang.includes('zh')) { if (browserLang.includes('cn') || browserLang.includes('hans')) { return 'zh-cn'; } } // Default to English return 'en'; } function setLanguage(lang) { if (TRANSLATIONS[lang]) { currentLanguage = lang; localStorage.setItem('accessibilityWidgetLanguage', lang); return true; } return false; } function getTranslation(key) { return TRANSLATIONS[currentLanguage][key] || TRANSLATIONS['en'][key] || key; } // Initialize language from localStorage or detect from browser const savedLanguage = localStorage.getItem('accessibilityWidgetLanguage'); if (savedLanguage && TRANSLATIONS[savedLanguage]) { currentLanguage = savedLanguage; } else { currentLanguage = detectBrowserLanguage(); localStorage.setItem('accessibilityWidgetLanguage', currentLanguage); } // =========================================== // CONFIGURATION VARIABLES // =========================================== // Default configuration - can be overridden by user const DEFAULT_WIDGET_CONFIG = { // Core Features enableHighContrast: true, enableBiggerText: true, enableTextSpacing: true, // Now has 3 levels enablePauseAnimations: true, // Enhanced to include reduced motion features enableHideImages: true, enableDyslexiaFont: true, enableBiggerCursor: true, enableLineHeight: true, // Now has 3 levels (2em, 3em, 4em) enableTextAlign: true, // Advanced Features enableScreenReader: true, enableVoiceControl: true, enableFontSelection: true, enableColorFilter: true, // Widget Styling widgetWidth: '440px', widgetPosition: { side: 'right', // 'left' or 'right' right: '20px', left: '20px', bottom: '20px' }, // Colors colors: { primary: '#1663d7', // Header bg, main button bg, active border, close hover bg secondary: '#ffffff', // Main button icon color optionBg: '#ffffff', // Option button background optionText: '#333333', // Option button text color optionIcon: '#000000' // Option button icon color }, // Button styling button: { size: '55px', borderRadius: '100px', iconSize: '40px', shadow: '0 4px 8px rgba(0, 0, 0, 0.2)' }, // Menu styling menu: { headerHeight: '70px', padding: '0 10px 10px 10px', optionPadding: '20px 10px', optionMargin: '10px', borderRadius: '8px', fontSize: '16px', titleFontSize: '16px', closeButtonSize: '44px' }, // Typography typography: { fontFamily: 'Arial, sans-serif', fontSize: '17px', titleFontSize: '22px', titleFontWeight: '700', lineHeight: '1' }, // Animation animation: { transition: '0.2s', hoverScale: '1.05' }, // Language/Text Configuration lang: { accessibilityMenu: 'Accessibility Menu', closeAccessibilityMenu: 'Close Accessibility Menu', accessibilityTools: 'Accessibility Tools', resetAllSettings: 'Reset All Settings', screenReader: 'Screen Reader', voiceCommand: 'Voice Command', textSpacing: 'Text Spacing', pauseAnimations: 'Pause Animations', hideImages: 'Hide Images', dyslexiaFriendly: 'Dyslexia Friendly', biggerCursor: 'Bigger Cursor', lineHeight: 'Line Height', fontSelection: 'Font Selection', colorFilter: 'Color Filter', textAlign: 'Text Align', textSize: 'Text Size', highContrast: 'High Contrast', defaultFont: 'Default Font', noFilter: 'No Filter', default: 'Default', screenReaderOn: 'Screen reader on', screenReaderOff: 'Screen reader off', voiceControlActivated: 'Voice control activated', notSupportedBrowser: 'is not supported in this browser', close: 'Close', reset: 'Reset' }, // Voice Command Configuration - Developers can customize commands for different languages voiceCommands: { showMenu: ['show menu', 'open menu', 'accessibility menu', 'access menu'], highContrast: ['high contrast', 'contrast', 'dark mode', 'increase contrast'], biggerText: ['bigger text', 'large text', 'text size', 'increase text', 'bigger', 'larger text', 'text bigger', 'make text bigger', 'enlarge text'], textSpacing: ['text spacing', 'spacing', 'letter spacing', 'text space'], pauseAnimations: ['pause animations', 'stop animations', 'disable animations', 'no animations'], hideImages: ['hide images', 'remove images', 'no images'], dyslexiaFont: ['dyslexia friendly', 'dyslexia font', 'readable font', 'easy font'], biggerCursor: ['bigger cursor', 'large cursor', 'cursor size', 'big cursor'], lineHeight: ['line height', 'line spacing', 'space between lines', 'line space'], textAlign: ['align text', 'text align', 'center text', 'alignment'], screenReader: ['screen reader', 'read aloud', 'voice reader'], voiceControl: ['voice command', 'voice control', 'voice commands'], resetAll: ['reset all', 'reset everything', 'clear all', 'reset settings' , 'reset'] }, // Grid Layout Configuration gridLayout: { columns: '1fr 1fr', // Default 2-column layout gap: '10px' // Gap between grid items } }; // 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 const WIDGET_CONFIG = mergeConfigs(DEFAULT_WIDGET_CONFIG, window.ACCESSIBILITY_WIDGET_CONFIG || {}); // =========================================== // STYLES & VISUAL ASSETS // =========================================== // Widget styles (will go inside Shadow DOM - NOT affected by page styles or accessibility features) const widgetStyles = ` :host { all: initial; font-family: ${WIDGET_CONFIG.typography.fontFamily}; } * { box-sizing: border-box; } #snn-accessibility-fixed-button { position: fixed !important; ${WIDGET_CONFIG.widgetPosition.side}: ${WIDGET_CONFIG.widgetPosition[WIDGET_CONFIG.widgetPosition.side]} !important; bottom: ${WIDGET_CONFIG.widgetPosition.bottom} !important; z-index: 9999; background:${WIDGET_CONFIG.colors.primary}; padding:5px; border-radius:100%; } #snn-accessibility-button { background: ${WIDGET_CONFIG.colors.primary}; 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; display: flex; justify-content: center; align-items: center; border:solid 4px white; } #snn-accessibility-button:hover { transform: scale(${WIDGET_CONFIG.animation.hoverScale}); } #snn-accessibility-button:focus { outline: 2px solid ${WIDGET_CONFIG.colors.secondary}; outline-offset: 2px; } #snn-accessibility-button svg { width: ${WIDGET_CONFIG.button.iconSize}; height: ${WIDGET_CONFIG.button.iconSize}; fill: ${WIDGET_CONFIG.colors.secondary}; pointer-events: none; } #snn-accessibility-menu { position: fixed; top: 0; ${WIDGET_CONFIG.widgetPosition.side}: 0; max-width: ${WIDGET_CONFIG.widgetWidth}; width:100%; height: 100vh; overflow-y: auto; background-color: #e2e2e2; padding: 0; display: none; font-family: ${WIDGET_CONFIG.typography.fontFamily}; z-index: 999999; scrollbar-width: thin; line-height:1 !important; } .snn-accessibility-option { font-size: ${WIDGET_CONFIG.menu.fontSize}; display: flex; flex-direction: column; align-items: center; justify-content: space-around; padding: 5px; width: 100%; background-color: ${WIDGET_CONFIG.colors.optionBg}; color: ${WIDGET_CONFIG.colors.optionText}; border: 3px solid ${WIDGET_CONFIG.colors.optionBg}; cursor: pointer; border-radius: ${WIDGET_CONFIG.menu.borderRadius}; transition: background-color ${WIDGET_CONFIG.animation.transition}, border-color ${WIDGET_CONFIG.animation.transition}; line-height: ${WIDGET_CONFIG.typography.lineHeight} !important; gap: 5px; min-height: 105px; } .snn-accessibility-option:hover { border-color: ${WIDGET_CONFIG.colors.primary}; } .snn-accessibility-option.active { border-color: ${WIDGET_CONFIG.colors.primary}; } .snn-accessibility-option:disabled { opacity: 0.5; cursor: not-allowed; } .snn-icon { width: ${WIDGET_CONFIG.button.iconSize}; height: ${WIDGET_CONFIG.button.iconSize}; fill: ${WIDGET_CONFIG.colors.optionIcon}; flex-shrink: 0; } .snn-icon svg { width: 100%; height: 100%; fill: currentColor; } .snn-button-text { text-align: center; line-height: 1.2; font-size:16px; font-weight: 600; } .snn-option-steps { display: flex; gap: 5px; align-items: center; justify-content: center; margin-top: 5px; } .snn-option-step { width: 30px; height: 6px; border-radius: 3px; background-color: #d0d0d0; transition: background-color ${WIDGET_CONFIG.animation.transition}; } .snn-option-step.active { background-color: ${WIDGET_CONFIG.colors.primary}; } .snn-close, .snn-reset-button { background: none; border: none; font-size: ${WIDGET_CONFIG.menu.closeButtonSize}; color: ${WIDGET_CONFIG.colors.secondary}; cursor: pointer; line-height: ${WIDGET_CONFIG.typography.lineHeight}; border-radius: ${WIDGET_CONFIG.button.borderRadius}; width: ${WIDGET_CONFIG.menu.closeButtonSize}; height: ${WIDGET_CONFIG.menu.closeButtonSize}; position: relative; display: flex; align-items: center; justify-content: center; } .snn-close::before { content: '×'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: ${WIDGET_CONFIG.menu.closeButtonSize}; line-height: 1; } .snn-reset-button svg { width: 22px; height: 22px; fill: ${WIDGET_CONFIG.colors.secondary}; } .snn-close:focus, .snn-reset-button:focus { outline: solid 2px ${WIDGET_CONFIG.colors.secondary}; } .snn-close:hover, .snn-reset-button:hover { color: ${WIDGET_CONFIG.colors.secondary}; background: rgba(255, 255, 255, 0.2); } /* Tooltip styles */ .snn-tooltip { position: absolute; bottom: -35px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.8); color: white; padding: 6px 10px; border-radius: 4px; font-size: 12px; white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity 0.2s; z-index: 1000; } .snn-tooltip::before { content: ''; position: absolute; top: -4px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 5px solid rgba(0, 0, 0, 0.8); } .snn-close:hover .snn-tooltip, .snn-close:focus .snn-tooltip, .snn-reset-button:hover .snn-tooltip, .snn-reset-button:focus .snn-tooltip { opacity: 1; } .snn-header { display: flex; align-items: center; padding: 10px; background: ${WIDGET_CONFIG.colors.primary}; height: ${WIDGET_CONFIG.menu.headerHeight}; position: sticky; top: 0; z-index: 10; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); gap: 8px; } .snn-content { padding: 20px 20px 0px 20px; } .snn-language-selector { width: 100%; background: white; color: black; border: none; padding: 14px; font-size: 16px; font-family: ${WIDGET_CONFIG.typography.fontFamily}; border-radius: 5px; margin-bottom: 20px; cursor: pointer; outline: none; } .snn-language-selector:focus { outline: 2px solid ${WIDGET_CONFIG.colors.primary}; outline-offset: 2px; } .snn-options-grid { display: grid; grid-template-columns: ${WIDGET_CONFIG.gridLayout.columns}; gap: ${WIDGET_CONFIG.gridLayout.gap}; margin-bottom: 20px; } .snn-title { margin: 0; font-size: ${WIDGET_CONFIG.menu.titleFontSize}; color: ${WIDGET_CONFIG.colors.secondary}; line-height: ${WIDGET_CONFIG.typography.lineHeight} !important; margin-left: 5px; font-weight: ${WIDGET_CONFIG.typography.titleFontWeight}; flex: 1; letter-spacing: 1px !important; word-spacing: 2px !important; text-align: left; } `; // Page accessibility styles (will go in main document - these affect the page, NOT the widget) const pageStyles = ` /* High Contrast Modes */ .snn-high-contrast-medium { filter: none !important; } .snn-high-contrast-medium *:not(#snn-accessibility-widget-container):not(#snn-accessibility-widget-container *) { filter: contrast(1.3) !important; } .snn-high-contrast-high { background-color: #000 !important; color: #fff !important; filter: none !important; } .snn-high-contrast-high *:not(#snn-accessibility-widget-container):not(#snn-accessibility-widget-container *) { background-color: #000 !important; color: #fff !important; filter: contrast(1.5) !important; } .snn-high-contrast-ultra { background-color: #000 !important; color: #ffff00 !important; filter: none !important; } .snn-high-contrast-ultra *:not(#snn-accessibility-widget-container):not(#snn-accessibility-widget-container *) { background-color: #000 !important; color: #ffff00 !important; filter: contrast(2.0) !important; } /* Text Size */ .snn-bigger-text-medium * { font-size: 20px !important; } .snn-bigger-text-large * { font-size: 24px !important; } .snn-bigger-text-xlarge * { font-size: 28px !important; } /* Text Spacing - 3 Options */ .snn-text-spacing-light * { letter-spacing: 0.1em !important; word-spacing: 0.5em !important; } .snn-text-spacing-medium * { letter-spacing: 0.15em !important; word-spacing: 1em !important; } .snn-text-spacing-heavy * { letter-spacing: 0.25em !important; word-spacing: 2em !important; } /* Pause Animations (Enhanced to include Reduced Motion features) */ .snn-pause-animations * { animation: none !important; transition: none !important; } .snn-pause-animations *::before { animation: none !important; transition: none !important; } .snn-pause-animations *::after { animation: none !important; transition: none !important; } /* Dyslexia Font */ .snn-dyslexia-font { font-family: 'Comic Sans MS', 'Chalkboard SE', 'Bradley Hand', 'Brush Script MT', fantasy !important; } .snn-dyslexia-font * { font-family: 'Comic Sans MS', 'Chalkboard SE', 'Bradley Hand', 'Brush Script MT', fantasy !important; } /* Line Height - 3 Options */ .snn-line-height-2em * { line-height: 2 !important; } .snn-line-height-3em * { line-height: 3 !important; } .snn-line-height-4em * { line-height: 4 !important; } /* Text Alignment */ .snn-text-align-left * { text-align: left !important; } .snn-text-align-center * { text-align: center !important; } .snn-text-align-right * { text-align: right !important; } /* Bigger Cursor */ .snn-bigger-cursor { cursor: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNzIiIHZpZXdCb3g9IjAgMCA0OCA3MiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNCAyVjcwTDIwIDU0SDM2TDQgMloiIGZpbGw9IiMwMDAiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSI0Ii8+PC9zdmc+'), auto !important; } .snn-bigger-cursor * { cursor: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNzIiIHZpZXdCb3g9IjAgMCA0OCA3MiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNCAyVjcwTDIwIDU0SDM2TDQgMloiIGZpbGw9IiMwMDAiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSI0Ii8+PC9zdmc+'), auto !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: none !important; } .snn-filter-protanopia body > *:not(#snn-accessibility-widget-container) { filter: url('#protanopia-filter') !important; } .snn-filter-deuteranopia { filter: none !important; } .snn-filter-deuteranopia body > *:not(#snn-accessibility-widget-container) { filter: url('#deuteranopia-filter') !important; } .snn-filter-tritanopia { filter: none !important; } .snn-filter-tritanopia body > *:not(#snn-accessibility-widget-container) { filter: url('#tritanopia-filter') !important; } .snn-filter-grayscale { filter: none !important; } .snn-filter-grayscale body > *:not(#snn-accessibility-widget-container) { filter: grayscale(100%) !important; } /* Saturation Filters */ .snn-saturation-low { filter: none !important; } .snn-saturation-low body > *:not(#snn-accessibility-widget-container) { filter: saturate(0.5) !important; } .snn-saturation-high { filter: none !important; } .snn-saturation-high body > *:not(#snn-accessibility-widget-container) { filter: saturate(10) !important; } .snn-saturation-none { filter: none !important; } .snn-saturation-none body > *:not(#snn-accessibility-widget-container) { filter: grayscale(100%) saturate(0) !important; } /* Protect widget container from page styles */ #snn-accessibility-widget-container, #snn-accessibility-widget-container * { filter: none !important; background-color: initial !important; color: initial !important; } `; // =========================================== // SVG ICONS // =========================================== const icons = { buttonsvg: ``, highContrast: ``, biggerText: ``, textSpacing: ``, pauseAnimations: ``, hideImages: ``, dyslexiaFont: ``, biggerCursor: ``, lineHeight: ``, textAlign: ``, screenReader: ``, resetAll: ``, voiceControl: ``, fontSelection: `Aa`, colorFilter: ``, saturation: ``, reducedMotion: ``, }; // =========================================== // SHADOW DOM SETUP // =========================================== let shadowRoot = null; // Inject styles into the page (NOT the widget) function injectPageStyles() { const styleSheet = document.createElement('style'); styleSheet.innerText = pageStyles; styleSheet.id = 'snn-accessibility-page-styles'; document.head.appendChild(styleSheet); // Add SVG color blindness filters to main document const svgFilters = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svgFilters.style.position = 'absolute'; svgFilters.style.width = '0'; svgFilters.setAttribute('class', 'snn-accessibility-filters'); svgFilters.style.height = '0'; svgFilters.innerHTML = ` `; document.body.appendChild(svgFilters); } // Create shadow DOM container function createShadowContainer() { const container = document.createElement('div'); container.id = 'snn-accessibility-widget-container'; document.body.appendChild(container); // Create shadow root shadowRoot = container.attachShadow({ mode: 'open' }); // Add widget styles to shadow DOM const styleElement = document.createElement('style'); styleElement.textContent = widgetStyles; shadowRoot.appendChild(styleElement); return shadowRoot; } // =========================================== // CORE UTILITY FUNCTIONS // =========================================== // Cache for DOM elements to improve performance const domCache = { get body() { return document.body; }, get documentElement() { return 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() { // Check if body element exists if (!domCache.body || !domCache.documentElement) { console.warn('Body or document element not ready yet'); return; } const settings = [ { key: 'biggerCursor', className: 'snn-bigger-cursor' }, { key: 'biggerText', className: 'snn-bigger-text' }, { key: 'highContrast', className: 'snn-high-contrast' }, { key: 'dyslexiaFont', className: 'snn-dyslexia-font' }, { key: 'textAlign', className: 'snn-text-align' }, { key: 'pauseAnimations', className: 'snn-pause-animations' }, ]; // 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 - ONLY remove classes that start with 'snn-' if (bodyClassesToAdd.length > 0) { domCache.body.classList.add(...bodyClassesToAdd); } if (bodyClassesToRemove.length > 0) { // Only remove our own classes, never remove classes that don't start with 'snn-' bodyClassesToRemove.forEach(className => { if (className.startsWith('snn-')) { domCache.body.classList.remove(className); } }); } if (docClassesToAdd.length > 0) { domCache.documentElement.classList.add(...docClassesToAdd); } if (docClassesToRemove.length > 0) { // Only remove our own classes, never remove classes that don't start with 'snn-' docClassesToRemove.forEach(className => { if (className.startsWith('snn-')) { domCache.documentElement.classList.remove(className); } }); } // Handle font selection - only remove widget's own font classes const fontClasses = ['snn-font-arial', 'snn-font-times', 'snn-font-verdana']; fontClasses.forEach(className => { if (domCache.body.classList.contains(className)) { domCache.body.classList.remove(className); } }); const selectedFont = localStorage.getItem('fontSelection'); if (selectedFont) { domCache.body.classList.add(`snn-font-${selectedFont}`); } // Handle color filters - only remove widget's own filter classes const filterClasses = ['snn-filter-protanopia', 'snn-filter-deuteranopia', 'snn-filter-tritanopia', 'snn-filter-grayscale']; filterClasses.forEach(className => { if (domCache.documentElement.classList.contains(className)) { domCache.documentElement.classList.remove(className); } }); const selectedFilter = localStorage.getItem('colorFilter'); if (selectedFilter) { domCache.documentElement.classList.add(`snn-filter-${selectedFilter}`); } // Handle saturation filters - only remove widget's own saturation classes const saturationClasses = ['snn-saturation-low', 'snn-saturation-high', 'snn-saturation-none']; saturationClasses.forEach(className => { if (domCache.documentElement.classList.contains(className)) { domCache.documentElement.classList.remove(className); } }); const selectedSaturation = localStorage.getItem('saturation'); if (selectedSaturation) { domCache.documentElement.classList.add(`snn-saturation-${selectedSaturation}`); } // Handle text alignment - only remove widget's own alignment classes const alignClasses = ['snn-text-align-left', 'snn-text-align-center', 'snn-text-align-right']; alignClasses.forEach(className => { if (domCache.body.classList.contains(className)) { domCache.body.classList.remove(className); } }); const selectedAlign = localStorage.getItem('textAlign'); if (selectedAlign) { domCache.body.classList.add(`snn-text-align-${selectedAlign}`); } // Handle bigger text - only remove widget's own text size classes const textClasses = ['snn-bigger-text-medium', 'snn-bigger-text-large', 'snn-bigger-text-xlarge']; textClasses.forEach(className => { if (domCache.body.classList.contains(className)) { domCache.body.classList.remove(className); } }); const selectedTextSize = localStorage.getItem('biggerText'); if (selectedTextSize) { domCache.body.classList.add(`snn-bigger-text-${selectedTextSize}`); } // Handle high contrast - only remove widget's own contrast classes const contrastClasses = ['snn-high-contrast-medium', 'snn-high-contrast-high', 'snn-high-contrast-ultra']; contrastClasses.forEach(className => { if (domCache.body.classList.contains(className)) { domCache.body.classList.remove(className); } }); const selectedContrast = localStorage.getItem('highContrast'); if (selectedContrast) { domCache.body.classList.add(`snn-high-contrast-${selectedContrast}`); } // Handle Text Spacing (3 Levels) - only remove widget's own spacing classes const spacingClasses = ['snn-text-spacing-light', 'snn-text-spacing-medium', 'snn-text-spacing-heavy']; spacingClasses.forEach(className => { if (domCache.body.classList.contains(className)) { domCache.body.classList.remove(className); } }); const selectedSpacing = localStorage.getItem('textSpacing'); if (selectedSpacing) { domCache.body.classList.add(`snn-text-spacing-${selectedSpacing}`); } // Handle Line Height (3 Levels) - only remove widget's own line height classes const lineHeightClasses = ['snn-line-height-2em', 'snn-line-height-3em', 'snn-line-height-4em']; lineHeightClasses.forEach(className => { if (domCache.body.classList.contains(className)) { domCache.body.classList.remove(className); } }); const selectedLineHeight = localStorage.getItem('lineHeight'); if (selectedLineHeight) { domCache.body.classList.add(`snn-line-height-${selectedLineHeight}`); } // 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', getTranslation('accessibilityMenu')); button.addEventListener('click', function () { toggleMenu(); }); button.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleMenu(); } }); buttonContainer.appendChild(button); shadowRoot.appendChild(buttonContainer); } // Reset all accessibility settings function resetAccessibilitySettings() { const keys = [ 'biggerCursor', 'biggerText', 'dyslexiaFont', 'hideImages', 'lineHeight', 'pauseAnimations', 'screenReader', 'textAlign', 'textSpacing', 'highContrast', 'voiceControl', 'fontSelection', 'colorFilter', 'saturation', ]; keys.forEach((key) => localStorage.removeItem(key)); // Remove only widget's own CSS classes - never touch existing body/document classes // Check if body and documentElement exist first if (!document.body || !document.documentElement) { console.warn('Body or document element not ready during reset'); return; } // Remove body classes only if they exist and start with 'snn-' const cssClasses = [ 'snn-bigger-cursor', 'snn-bigger-text', 'snn-dyslexia-font', 'snn-pause-animations', 'snn-text-align', 'snn-font-arial', 'snn-font-times', 'snn-font-verdana', 'snn-high-contrast-medium', 'snn-high-contrast-high', 'snn-high-contrast-ultra', 'snn-bigger-text-medium', 'snn-bigger-text-large', 'snn-bigger-text-xlarge', 'snn-text-spacing-light', 'snn-text-spacing-medium', 'snn-text-spacing-heavy', 'snn-line-height-2em', 'snn-line-height-3em', 'snn-line-height-4em', 'snn-text-align-left', 'snn-text-align-center', 'snn-text-align-right' ]; cssClasses.forEach(cls => { if (document.body.classList.contains(cls)) { document.body.classList.remove(cls); } }); // Remove document element classes only if they exist and start with 'snn-' const documentClasses = [ 'snn-filter-protanopia', 'snn-filter-deuteranopia', 'snn-filter-tritanopia', 'snn-filter-grayscale', 'snn-saturation-low', 'snn-saturation-high', 'snn-saturation-none' ]; documentClasses.forEach(cls => { if (document.documentElement.classList.contains(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 = shadowRoot.querySelectorAll('#snn-accessibility-menu .snn-accessibility-option'); buttons.forEach((button) => { button.classList.remove('active'); button.setAttribute('aria-pressed', 'false'); // Reset step indicators const steps = button.querySelectorAll('.snn-option-step'); steps.forEach(step => step.classList.remove('active')); }); } // Create toggle buttons for accessibility options function createToggleButton( buttonText, localStorageKey, className, targetElement = document.body, customToggleFunction = null, iconSVG = '', requiresFeature = null, optionId = 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'); if (optionId) { button.setAttribute('data-accessibility-option-id', optionId); } // Check if feature is supported if (requiresFeature && !requiresFeature.isSupported) { button.disabled = true; button.setAttribute('title', `${buttonText} ${getTranslation('notSupportedBrowser')}`); 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, optionsConfig = null, optionId = null) { const button = document.createElement('button'); let buttonHTML = ` ${iconSVG} ${buttonText} `; // Add option steps if configured if (optionsConfig) { buttonHTML += '
'; for (let i = 0; i < optionsConfig.count; i++) { buttonHTML += '
'; } buttonHTML += '
'; } button.innerHTML = buttonHTML; button.setAttribute('aria-label', buttonText); button.classList.add('snn-accessibility-option'); button.setAttribute('data-options-config', optionsConfig ? JSON.stringify(optionsConfig) : ''); if (optionId) { button.setAttribute('data-accessibility-option-id', optionId); button.setAttribute('data-key', optionId); // Add data-key for voice commands } // Update initial status updateActionButtonStatus(button, optionId, optionsConfig); button.addEventListener('click', function () { const result = actionFunction(); if (result) { updateActionButtonStatus(button, optionId, optionsConfig); } }); button.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const result = actionFunction(); if (result) { updateActionButtonStatus(button, optionId, optionsConfig); } } }); return button; } // Update action button status on page load function updateActionButtonStatus(button, optionId, optionsConfig) { if (!optionsConfig) return; const steps = button.querySelectorAll('.snn-option-step'); let currentIndex = -1; if (optionId === 'fontSelection') { const currentFont = localStorage.getItem('fontSelection'); const fonts = ['arial', 'times', 'verdana']; currentIndex = currentFont ? fonts.indexOf(currentFont) : -1; } else if (optionId === 'colorFilter') { const currentFilter = localStorage.getItem('colorFilter'); const filters = ['protanopia', 'deuteranopia', 'tritanopia', 'grayscale']; currentIndex = currentFilter ? filters.indexOf(currentFilter) : -1; } else if (optionId === 'textAlign') { const currentAlign = localStorage.getItem('textAlign'); const alignments = ['left', 'center', 'right']; currentIndex = currentAlign ? alignments.indexOf(currentAlign) : -1; } else if (optionId === 'biggerText') { const currentSize = localStorage.getItem('biggerText'); const sizes = ['medium', 'large', 'xlarge']; currentIndex = currentSize ? sizes.indexOf(currentSize) : -1; } else if (optionId === 'highContrast') { const currentContrast = localStorage.getItem('highContrast'); const contrasts = ['medium', 'high', 'ultra']; currentIndex = currentContrast ? contrasts.indexOf(currentContrast) : -1; } else if (optionId === 'textSpacing') { const currentSpacing = localStorage.getItem('textSpacing'); const spacings = ['light', 'medium', 'heavy']; currentIndex = currentSpacing ? spacings.indexOf(currentSpacing) : -1; } else if (optionId === 'lineHeight') { const currentLineHeight = localStorage.getItem('lineHeight'); const heights = ['2em', '3em', '4em']; currentIndex = currentLineHeight ? heights.indexOf(currentLineHeight) : -1; } else if (optionId === 'saturation') { const currentSaturation = localStorage.getItem('saturation'); const saturations = ['low', 'high', 'none']; currentIndex = currentSaturation ? saturations.indexOf(currentSaturation) : -1; } // Update step indicators - show all previous steps as active steps.forEach((step, index) => { if (index <= currentIndex) { step.classList.add('active'); } else { step.classList.remove('active'); } }); // Toggle active class on button itself if any option is selected if (currentIndex !== -1) { button.classList.add('active'); button.setAttribute('aria-pressed', 'true'); } else { button.classList.remove('active'); button.setAttribute('aria-pressed', 'false'); } } // =========================================== // 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 only widget's own font classes const fontClasses = ['snn-font-arial', 'snn-font-times', 'snn-font-verdana']; fontClasses.forEach(className => { if (domCache.body.classList.contains(className)) { domCache.body.classList.remove(className); } }); if (nextIndex === fonts.length) { // Default font localStorage.removeItem('fontSelection'); return getTranslation('defaultFont'); } else { const selectedFont = fonts[nextIndex]; localStorage.setItem('fontSelection', selectedFont); domCache.body.classList.add(`snn-font-${selectedFont}`); return selectedFont.charAt(0).toUpperCase() + selectedFont.slice(1); } } // Saturation handler with 3 states (low, high, none/grayscale) function handleSaturation() { const saturations = ['low', 'high', 'none']; const currentSaturation = localStorage.getItem('saturation') || 'default'; const currentIndex = saturations.indexOf(currentSaturation); const nextIndex = (currentIndex + 1) % (saturations.length + 1); // +1 for default // Remove only widget's own saturation classes const saturationClasses = ['snn-saturation-low', 'snn-saturation-high', 'snn-saturation-none']; saturationClasses.forEach(className => { if (domCache.documentElement.classList.contains(className)) { domCache.documentElement.classList.remove(className); } }); if (nextIndex === saturations.length) { // Default saturation localStorage.removeItem('saturation'); return 'Default'; } else { const selectedSaturation = saturations[nextIndex]; localStorage.setItem('saturation', selectedSaturation); domCache.documentElement.classList.add(`snn-saturation-${selectedSaturation}`); if (selectedSaturation === 'none') { return 'No Saturation'; } return selectedSaturation.charAt(0).toUpperCase() + selectedSaturation.slice(1) + ' Saturation'; } } // 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 only widget's own filter classes const filterClasses = ['snn-filter-protanopia', 'snn-filter-deuteranopia', 'snn-filter-tritanopia', 'snn-filter-grayscale']; filterClasses.forEach(className => { if (domCache.documentElement.classList.contains(className)) { domCache.documentElement.classList.remove(className); } }); if (nextIndex === filters.length) { // No filter localStorage.removeItem('colorFilter'); return getTranslation('noFilter'); } else { const selectedFilter = filters[nextIndex]; localStorage.setItem('colorFilter', selectedFilter); domCache.documentElement.classList.add(`snn-filter-${selectedFilter}`); return selectedFilter.charAt(0).toUpperCase() + selectedFilter.slice(1); } } // Text align handler with 3 states function handleTextAlign() { const alignments = ['left', 'center', 'right']; const currentAlign = localStorage.getItem('textAlign') || 'none'; const currentIndex = alignments.indexOf(currentAlign); const nextIndex = (currentIndex + 1) % (alignments.length + 1); // +1 for none // Remove only widget's own alignment classes const alignClasses = ['snn-text-align-left', 'snn-text-align-center', 'snn-text-align-right']; alignClasses.forEach(className => { if (domCache.body.classList.contains(className)) { domCache.body.classList.remove(className); } }); if (nextIndex === alignments.length) { // Default alignment localStorage.removeItem('textAlign'); return getTranslation('default'); } else { const selectedAlign = alignments[nextIndex]; localStorage.setItem('textAlign', selectedAlign); domCache.body.classList.add(`snn-text-align-${selectedAlign}`); return selectedAlign.charAt(0).toUpperCase() + selectedAlign.slice(1); } } // Bigger text handler with 3 states function handleBiggerText() { const textSizes = ['medium', 'large', 'xlarge']; const currentSize = localStorage.getItem('biggerText') || 'none'; const currentIndex = textSizes.indexOf(currentSize); const nextIndex = (currentIndex + 1) % (textSizes.length + 1); // +1 for none // Remove only widget's own text size classes const textClasses = ['snn-bigger-text-medium', 'snn-bigger-text-large', 'snn-bigger-text-xlarge']; textClasses.forEach(className => { if (domCache.body.classList.contains(className)) { domCache.body.classList.remove(className); } }); if (nextIndex === textSizes.length) { // Default text size localStorage.removeItem('biggerText'); return getTranslation('default'); } else { const selectedSize = textSizes[nextIndex]; localStorage.setItem('biggerText', selectedSize); domCache.body.classList.add(`snn-bigger-text-${selectedSize}`); return selectedSize === 'xlarge' ? 'X-Large' : selectedSize.charAt(0).toUpperCase() + selectedSize.slice(1); } } // High contrast handler with 3 states function handleHighContrast() { const contrastLevels = ['medium', 'high', 'ultra']; const currentContrast = localStorage.getItem('highContrast') || 'none'; const currentIndex = contrastLevels.indexOf(currentContrast); const nextIndex = (currentIndex + 1) % (contrastLevels.length + 1); // +1 for none // Remove only widget's own contrast classes const contrastClasses = ['snn-high-contrast-medium', 'snn-high-contrast-high', 'snn-high-contrast-ultra']; contrastClasses.forEach(className => { if (domCache.body.classList.contains(className)) { domCache.body.classList.remove(className); } }); if (nextIndex === contrastLevels.length) { // Default contrast localStorage.removeItem('highContrast'); return getTranslation('default'); } else { const selectedContrast = contrastLevels[nextIndex]; localStorage.setItem('highContrast', selectedContrast); domCache.body.classList.add(`snn-high-contrast-${selectedContrast}`); return selectedContrast.charAt(0).toUpperCase() + selectedContrast.slice(1); } } // Text Spacing Handler with 3 states (1em, 2em, 4em equivalents) function handleTextSpacing() { const spacings = ['light', 'medium', 'heavy']; // Maps to 1, 2, 4 approx const currentSpacing = localStorage.getItem('textSpacing') || 'none'; const currentIndex = spacings.indexOf(currentSpacing); const nextIndex = (currentIndex + 1) % (spacings.length + 1); // +1 for none // Remove only widget's own spacing classes const spacingClasses = ['snn-text-spacing-light', 'snn-text-spacing-medium', 'snn-text-spacing-heavy']; spacingClasses.forEach(className => { if (domCache.body.classList.contains(className)) { domCache.body.classList.remove(className); } }); if (nextIndex === spacings.length) { // Default localStorage.removeItem('textSpacing'); return getTranslation('default'); } else { const selectedSpacing = spacings[nextIndex]; localStorage.setItem('textSpacing', selectedSpacing); domCache.body.classList.add(`snn-text-spacing-${selectedSpacing}`); return selectedSpacing.charAt(0).toUpperCase() + selectedSpacing.slice(1); } } // Line Height Handler with 3 states (2em, 3em, 4em) function handleLineHeight() { const heights = ['2em', '3em', '4em']; const currentHeight = localStorage.getItem('lineHeight') || 'none'; const currentIndex = heights.indexOf(currentHeight); const nextIndex = (currentIndex + 1) % (heights.length + 1); // +1 for none // Remove only widget's own line height classes const heightClasses = ['snn-line-height-2em', 'snn-line-height-3em', 'snn-line-height-4em']; heightClasses.forEach(className => { if (domCache.body.classList.contains(className)) { domCache.body.classList.remove(className); } }); if (nextIndex === heights.length) { // Default localStorage.removeItem('lineHeight'); return getTranslation('default'); } else{ const selectedHeight = heights[nextIndex]; localStorage.setItem('lineHeight', selectedHeight); domCache.body.classList.add(`snn-line-height-${selectedHeight}`); return selectedHeight; } } // =========================================== // 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 ${getTranslation('notSupportedBrowser')}`); return false; } screenReader.active = isActive; localStorage.setItem('screenReader', isActive); try { if (isActive) { document.addEventListener('focusin', screenReader.handleFocus); const feedbackSpeech = new SpeechSynthesisUtterance(getTranslation('screenReaderOn')); 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(getTranslation('screenReaderOff')); 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 ${getTranslation('notSupportedBrowser')}`); 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(getTranslation('voiceControlActivated')); 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 { // Normalize the command by removing extra spaces and making it lowercase const normalizedCommand = command.toLowerCase().trim().replace(/\s+/g, ' '); // Check for show menu commands if (WIDGET_CONFIG.voiceCommands.showMenu.some(cmd => normalizedCommand.includes(cmd))) { if (!menuCache.button) menuCache.init(); if (menuCache.button) { menuCache.button.click(); console.log('Successfully opened menu'); } return; } // Check for reset all commands if (WIDGET_CONFIG.voiceCommands.resetAll.some(cmd => normalizedCommand.includes(cmd))) { resetAccessibilitySettings(); console.log('Successfully reset all settings'); return; } // Build dynamic command map based on configuration let localStorageKey = null; let matchedCommand = null; // Check each command group with better matching for (const [key, commands] of Object.entries(WIDGET_CONFIG.voiceCommands)) { if (key === 'showMenu' || key === 'resetAll') continue; // Already handled above const isMatch = commands.some(cmd => { // Check for exact matches first if (normalizedCommand.includes(cmd.toLowerCase())) { matchedCommand = cmd; return true; } // Check for partial word matches (at least 3 characters) const cmdWords = cmd.toLowerCase().split(' '); const inputWords = normalizedCommand.split(' '); return cmdWords.some(cmdWord => cmdWord.length >= 3 && inputWords.some(inputWord => inputWord.includes(cmdWord) || cmdWord.includes(inputWord) ) ); }); if (isMatch) { localStorageKey = key; break; } } if (localStorageKey) { // Use cached menu reference if available if (!menuCache.menu) menuCache.init(); // Try to find button by data-key first (toggle buttons) let button = menuCache.menu?.querySelector( `.snn-accessibility-option[data-key='${localStorageKey}']` ); // If not found, try to find by data-accessibility-option-id (action buttons) if (!button) { button = menuCache.menu?.querySelector( `.snn-accessibility-option[data-accessibility-option-id='${localStorageKey}']` ); } if (button) { button.click(); console.log(`Successfully executed command: ${command} (matched: ${matchedCommand || localStorageKey})`); } else { console.log('Button not found for command:', command, '(key:', localStorageKey, ')'); } } else { console.log('Command not recognized:', command); // Provide helpful suggestions const availableCommands = Object.values(WIDGET_CONFIG.voiceCommands).flat(); const suggestions = availableCommands.filter(cmd => cmd.toLowerCase().includes(normalizedCommand.split(' ')[0]) || normalizedCommand.split(' ')[0].includes(cmd.toLowerCase().split(' ')[0]) ); if (suggestions.length > 0) { console.log('Did you mean one of these?', suggestions.slice(0, 3)); } } } 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('div'); title.classList.add('snn-title'); title.id = 'snn-accessibility-title'; title.textContent = getTranslation('accessibilityTools'); // Create reset button const resetButton = document.createElement('button'); resetButton.classList.add('snn-reset-button'); resetButton.innerHTML = `${icons.resetAll}${getTranslation('reset')}`; resetButton.setAttribute('aria-label', getTranslation('resetAllSettings')); resetButton.addEventListener('click', resetAccessibilitySettings); // Create close button const closeButton = document.createElement('button'); closeButton.className = 'snn-close'; closeButton.innerHTML = `${getTranslation('close')}`; closeButton.setAttribute('aria-label', getTranslation('closeAccessibilityMenu')); closeButton.addEventListener('click', function () { closeMenu(); }); closeButton.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); closeMenu(); } }); header.appendChild(title); header.appendChild(resetButton); header.appendChild(closeButton); menu.appendChild(header); // Create content wrapper const content = document.createElement('div'); content.classList.add('snn-content'); // Create language selector dropdown const languageSelector = document.createElement('select'); languageSelector.classList.add('snn-language-selector'); languageSelector.setAttribute('aria-label', getTranslation('selectLanguage')); const languages = [ { code: 'en', name: 'English' }, { code: 'de', name: 'Deutsch' }, { code: 'es', name: 'Español' }, { code: 'it', name: 'Italiano' }, { code: 'fr', name: 'Français' }, { code: 'ru', name: 'Русский' }, { code: 'tr', name: 'Türkçe' }, { code: 'ar', name: 'العربية' }, { code: 'hi', name: 'हिन्दी' }, { code: 'zh-cn', name: '简体中文' }, { code: 'jp', name: '日本語' } ]; languages.forEach(lang => { const option = document.createElement('option'); option.value = lang.code; option.textContent = lang.name; if (lang.code === currentLanguage) { option.selected = true; } languageSelector.appendChild(option); }); languageSelector.addEventListener('change', function(e) { const newLang = e.target.value; if (setLanguage(newLang)) { // Recreate the menu with new language updateMenuLanguage(); } }); content.appendChild(languageSelector); // Create grid wrapper for accessibility options const optionsGrid = document.createElement('div'); optionsGrid.classList.add('snn-options-grid'); // =================================================================== // UNIFIED BUTTON CONFIGURATION WITH EXPLICIT ORDERING // Add/remove/reorder buttons by changing the 'order' property // Lower order numbers appear first, higher numbers appear last // =================================================================== const allButtonConfigs = [ // Order 1-4: Primary accessibility features { order: 1, type: 'action', text: getTranslation('textSize'), actionFunction: handleBiggerText, icon: icons.biggerText, enabled: WIDGET_CONFIG.enableBiggerText, optionsConfig: { count: 3 }, optionId: 'biggerText' }, { order: 2, type: 'action', text: getTranslation('highContrast'), actionFunction: handleHighContrast, icon: icons.highContrast, enabled: WIDGET_CONFIG.enableHighContrast, optionsConfig: { count: 3 }, optionId: 'highContrast' }, { order: 3, type: 'action', text: getTranslation('textAlign'), actionFunction: handleTextAlign, icon: icons.textAlign, enabled: WIDGET_CONFIG.enableTextAlign, optionsConfig: { count: 3 }, optionId: 'textAlign' }, { order: 4, type: 'action', text: getTranslation('colorFilter'), actionFunction: handleColorFilter, icon: icons.colorFilter, enabled: WIDGET_CONFIG.enableColorFilter, optionsConfig: { count: 4 }, optionId: 'colorFilter' }, // Order 5-11: Other visual/text features { order: 5, type: 'action', // Changed from toggle to action text: getTranslation('textSpacing'), actionFunction: handleTextSpacing, icon: icons.textSpacing, enabled: WIDGET_CONFIG.enableTextSpacing, optionsConfig: { count: 3 }, optionId: 'textSpacing' }, { order: 6, type: 'action', // Changed from toggle to action text: getTranslation('lineHeight'), actionFunction: handleLineHeight, icon: icons.lineHeight, enabled: WIDGET_CONFIG.enableLineHeight, optionsConfig: { count: 3 }, optionId: 'lineHeight' }, { order: 7, type: 'action', text: getTranslation('fontSelection'), actionFunction: handleFontSelection, icon: icons.fontSelection, enabled: WIDGET_CONFIG.enableFontSelection, optionsConfig: { count: 3 }, optionId: 'fontSelection' }, { order: 7.5, type: 'action', text: getTranslation('saturation'), actionFunction: handleSaturation, icon: icons.saturation, enabled: true, optionsConfig: { count: 3 }, optionId: 'saturation' }, { order: 8, type: 'toggle', text: getTranslation('dyslexiaFriendly'), key: 'dyslexiaFont', className: 'snn-dyslexia-font', icon: icons.dyslexiaFont, enabled: WIDGET_CONFIG.enableDyslexiaFont, optionId: 'dyslexiaFont' }, { order: 9, type: 'toggle', text: getTranslation('biggerCursor'), key: 'biggerCursor', className: 'snn-bigger-cursor', icon: icons.biggerCursor, enabled: WIDGET_CONFIG.enableBiggerCursor, optionId: 'biggerCursor' }, { order: 10, type: 'toggle', text: getTranslation('hideImages'), key: 'hideImages', icon: icons.hideImages, customToggleFunction: toggleHideImages, enabled: WIDGET_CONFIG.enableHideImages, optionId: 'hideImages' }, // Order 11: Animation controls (Reduced Motion merged here) { order: 11, type: 'toggle', text: getTranslation('pauseAnimations'), key: 'pauseAnimations', className: 'snn-pause-animations', icon: icons.pauseAnimations, enabled: WIDGET_CONFIG.enablePauseAnimations, optionId: 'pauseAnimations' }, // Order 98-99: Screen Reader and Voice Control (always last) { order: 98, type: 'toggle', text: getTranslation('screenReader'), key: 'screenReader', customToggleFunction: screenReader.toggle, icon: icons.screenReader, requiresFeature: screenReader, enabled: WIDGET_CONFIG.enableScreenReader, optionId: 'screenReader' }, { order: 99, type: 'toggle', text: getTranslation('voiceCommand'), key: 'voiceControl', customToggleFunction: voiceControl.toggle, icon: icons.voiceControl, requiresFeature: voiceControl, enabled: WIDGET_CONFIG.enableVoiceControl, optionId: 'voiceControl' }, ]; // Sort buttons by order and add only enabled ones to the grid allButtonConfigs .filter(config => config.enabled) .sort((a, b) => a.order - b.order) .forEach((config) => { let button; if (config.type === 'action') { button = createActionButton(config.text, config.actionFunction, config.icon, config.optionsConfig, config.optionId); } else if (config.type === 'toggle') { button = createToggleButton( config.text, config.key, config.className, config.target || document.body, config.customToggleFunction, config.icon, config.requiresFeature, config.optionId ); } if (button) { optionsGrid.appendChild(button); } }); // Add grid to content content.appendChild(optionsGrid); // Add content to menu menu.appendChild(content); shadowRoot.appendChild(menu); } // Update menu language without recreating everything function updateMenuLanguage() { const menu = shadowRoot.getElementById('snn-accessibility-menu'); if (!menu) return; const wasOpen = menu.style.display === 'block'; // Remove old menu menu.remove(); // Clear cache menuCache.menu = null; menuCache.closeButton = null; keyboardCache.focusableElements = null; // Recreate menu createAccessibilityMenu(); // Update button aria-label const mainButton = shadowRoot.getElementById('snn-accessibility-button'); if (mainButton) { mainButton.setAttribute('aria-label', getTranslation('accessibilityMenu')); } // Reopen if it was open if (wasOpen) { menuCache.init(); openMenu(); } } // =========================================== // MENU MANAGEMENT // =========================================== // Cache for menu elements const menuCache = { menu: null, button: null, closeButton: null, init: function () { this.menu = shadowRoot.getElementById('snn-accessibility-menu'); this.button = shadowRoot.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'); // UPDATED: Now focuses on the first tool button instead of the Close button const firstOption = menuCache.menu.querySelector('.snn-accessibility-option'); if (firstOption) { firstOption.focus(); } else 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, .snn-reset-button')) }; 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(shadowRoot.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() { // Create shadow DOM first createShadowContainer(); // Inject page styles (for accessibility features) injectPageStyles(); // Apply saved settings applySettings(); // Create widget UI inside shadow DOM createAccessibilityButton(); createAccessibilityMenu(); } // =========================================== // WIDGET BOOTSTRAP // =========================================== // Load the widget when the DOM is fully loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAccessibilityWidget); } else { initAccessibilityWidget(); }