1847 lines
68 KiB
JavaScript
1847 lines
68 KiB
JavaScript
/*
|
||
===========================================
|
||
ACCESSIBILITY WIDGET
|
||
A comprehensive web accessibility tool
|
||
Updated: Enhanced Text Spacing, Line Height, and Pause Animations
|
||
===========================================
|
||
*/
|
||
|
||
// ===========================================
|
||
// 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: '55px',
|
||
padding: '0 10px 10px 10px',
|
||
optionPadding: '20px 10px',
|
||
optionMargin: '10px',
|
||
borderRadius: '8px',
|
||
fontSize: '16px',
|
||
titleFontSize: '22px',
|
||
closeButtonSize: '44px'
|
||
},
|
||
|
||
// Typography
|
||
typography: {
|
||
fontFamily: 'Arial, sans-serif',
|
||
fontSize: '16px',
|
||
titleFontSize: '22px',
|
||
titleFontWeight: '500',
|
||
lineHeight: '1'
|
||
},
|
||
|
||
// Animation
|
||
animation: {
|
||
transition: '0.2s',
|
||
hoverScale: '1.05'
|
||
},
|
||
|
||
// 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'],
|
||
highContrast: ['high contrast'],
|
||
biggerText: ['bigger text', 'large text'],
|
||
textSpacing: ['text spacing'],
|
||
pauseAnimations: ['pause animations', 'stop animations'],
|
||
hideImages: ['hide images'],
|
||
dyslexiaFont: ['dyslexia friendly', 'dyslexia font'],
|
||
biggerCursor: ['bigger cursor', 'large cursor'],
|
||
lineHeight: ['line height'],
|
||
textAlign: ['align text', 'text align'],
|
||
screenReader: ['screen reader'],
|
||
voiceControl: ['voice command', 'voice control'],
|
||
resetAll: ['reset all', 'reset everything']
|
||
},
|
||
|
||
// 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;
|
||
}
|
||
|
||
#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;
|
||
}
|
||
|
||
#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;
|
||
width: ${WIDGET_CONFIG.widgetWidth};
|
||
height: 100vh;
|
||
overflow-y: auto;
|
||
background-color: #e2e2e2;
|
||
padding: 0;
|
||
display: none;
|
||
font-family: ${WIDGET_CONFIG.typography.fontFamily};
|
||
z-index: 999999;
|
||
scrollbar-width: thin;
|
||
}
|
||
|
||
.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: 2px 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: 110px;
|
||
}
|
||
|
||
.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;
|
||
margin-bottom: 20px;
|
||
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: 0 10px 10px 10px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
`;
|
||
|
||
// 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;
|
||
}
|
||
|
||
/* 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: `<svg xmlns="http://www.w3.org/2000/svg" style="fill:white;" viewBox="0 0 24 24" width="30px" height="30px"><path d="M0 0h24v24H0V0z" fill="none"></path><path d="M20.5 6c-2.61.7-5.67 1-8.5 1s-5.89-.3-8.5-1L3 8c1.86.5 4 .83 6 1v13h2v-6h2v6h2V9c2-.17 4.14-.5 6-1l-.5-2zM12 6c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"></path></svg>`,
|
||
highContrast: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" version="1.2" viewBox="0 0 35 35"><path fill="currentColor" fill-rule="evenodd" d="M1.89998 15.6285c0-7.58203 6.14649-13.72852 13.72852-13.72852 7.5821 0 13.7286 6.14649 13.7286 13.72852 0 .6081-.0395 1.2069-.1161 1.794.5933.2913 1.1478.6497 1.6534 1.0654.1725-.9268.2627-1.8825.2627-2.8594 0-8.57615-6.9524-15.5285244-15.5286-15.5285244C7.05235.0999756.0999756 7.05235.0999756 15.6285c0 8.5762 6.9523744 15.5286 15.5285244 15.5286 1.2241 0 2.415-.1416 3.5574-.4093-.4388-.4866-.8222-1.0242-1.1402-1.6028-.7847.1394-1.5924.2121-2.4172.2121-7.58203 0-13.72852-6.1465-13.72852-13.7286Z" clip-rule="evenodd"/><path fill="currentColor" fill-rule="evenodd" d="M2.35 15.6286C2.35 8.29502 8.29502 2.35 15.6286 2.35c7.3335 0 13.2785 5.94502 13.2785 13.2786 0 .5408-.0323 1.0741-.0951 1.5979.444.1881.8687.4128 1.2703.6703.1151-.7392.1748-1.4967.1748-2.2682C30.2571 7.54943 23.7077 1 15.6286 1 7.54943 1 1 7.54943 1 15.6286c0 8.0791 6.54943 14.6285 14.6286 14.6285 1.0033 0 1.9831-.101 2.9297-.2934-.276-.3898-.52-.8038-.7282-1.2382-.716.1195-1.4515.1816-2.2015.1816-7.33358 0-13.2786-5.945-13.2786-13.2785Z" clip-rule="evenodd"/><path fill="currentColor" fill-rule="evenodd" d="M15.6286 1C7.54943 1 1 7.54943 1 15.6286c0 8.0791 6.54943 14.6285 14.6286 14.6285" clip-rule="evenodd"/><path stroke="currentColor" stroke-width="1.8" d="M15.6286 1C7.54943 1 1 7.54943 1 15.6286c0 8.0791 6.54943 14.6285 14.6286 14.6285"/><path fill="currentColor" fill-rule="evenodd" d="M22.8729 25.114c0-1.3811 1.0901-2.5007 2.4359-2.5007 1.3459 0 2.436 1.1196 2.436 2.5007 0 1.38-1.0901 2.4997-2.436 2.4997-1.3458 0-2.4359-1.1197-2.4359-2.4997Zm7.2258-2.0373c-.0899-.2248-.071-.4785.0512-.6875l.912-1.5598c.0898-.1532.0668-.3504-.0574-.4779l-1.0556-1.0832c-.1232-.1264-.3153-.1511-.4657-.0589l-1.5225.9374c-.201.1237-.4495.1427-.667.051-.2181-.092-.3797-.2819-.4358-.5118l-.4329-1.7763c-.0428-.1735-.1953-.2957-.3696-.2957h-1.4931c-.1744 0-.3268.1222-.3696.2957l-.433 1.7763c-.056.2299-.2177.4198-.4357.5118-.2176.0917-.466.0727-.6671-.051l-1.5225-.9374c-.1503-.0922-.3424-.0675-.4656.0589l-1.0556 1.0832c-.1243.1275-.1473.3247-.0575.4779l.9121 1.5598c.1222.209.1411.4627.0511.6875-.0895.2239-.2806.3916-.5142.4514l-1.7165.4395c-.1692.0439-.2882.2003-.2882.3803v1.5311c0 .18.119.3364.2882.3804l1.7165.4394c.2336.0599.4247.2276.5142.4515.09.2247.0711.4785-.0511.6874l-.9121 1.5599c-.0898.1532-.0668.3503.0575.4778l1.0556 1.0833c.1232.1264.3153.151.4656.0589l1.5225-.9374c.2011-.1238.4495-.1428.6671-.051.218.092.3797.2818.4357.5118l.433 1.7762c.0428.1736.1952.2968.3696.2968h1.4931c.1743 0 .3268-.1232.3696-.2968l.4329-1.7762c.0561-.23.2177-.4198.4358-.5118.2175-.0918.466-.0728.667.051l1.5225.9374c.1504.0921.3425.0675.4657-.0589l1.0556-1.0833c.1242-.1275.1472-.3246.0574-.4778l-.912-1.5599c-.1222-.2089-.1411-.4627-.0512-.6874.0896-.2239.2806-.3916.5142-.4515l1.7166-.4394c.1691-.044.2881-.2004.2881-.3804v-1.5311c0-.18-.119-.3364-.2881-.3803l-1.7166-.4395c-.2336-.0598-.4246-.2275-.5142-.4514Z" clip-rule="evenodd"/></svg>`,
|
||
biggerText: `<svg xmlns="http://www.w3.org/2000/svg" version="1.2" viewBox="0 0 36 23"><g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-width="2"><path stroke-linejoin="round" d="M26.58 21.3225806V1m-7.92 4.06451613V1H34.5v4.06451613"/><path d="M22.62 21.3225806h7.92"/><path stroke-linejoin="round" d="M6.78 18.6129032V5.06451613M1.5 7.77419355V5.06451613h10.56v2.70967742"/><path d="M4.14 18.6129032h5.28"/></g></svg>`,
|
||
textSpacing: `<svg xmlns="http://www.w3.org/2000/svg" width="800px" height="800px" viewBox="0 0 15 15" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.55293 0.999969C4.75295 0.999969 4.93372 1.11917 5.0125 1.30301L8.01106 8.29982C8.11984 8.55363 8.00226 8.84757 7.74844 8.95635C7.49463 9.06512 7.20069 8.94754 7.09191 8.69373L6.11613 6.41685H2.98973L2.01395 8.69373C1.90517 8.94754 1.61123 9.06512 1.35742 8.95635C1.1036 8.84757 0.986023 8.55363 1.0948 8.29982L4.09336 1.30301C4.17214 1.11917 4.35291 0.999969 4.55293 0.999969ZM4.55293 2.76929L5.75186 5.56685H3.354L4.55293 2.76929ZM11.0562 9.00214C11.2617 9.00214 11.4463 8.87633 11.5215 8.68502L14.2733 1.68299C14.3743 1.42598 14.2478 1.13575 13.9908 1.03475C13.7338 0.933747 13.4436 1.06021 13.3426 1.31722L11.0562 7.13514L8.76973 1.31722C8.66873 1.06021 8.3785 0.933747 8.1215 1.03475C7.86449 1.13575 7.73802 1.42598 7.83902 1.68299L10.5908 8.68502C10.666 8.87633 10.8506 9.00214 11.0562 9.00214ZM14.9537 12.4999C14.9537 12.606 14.9115 12.7077 14.8365 12.7828L12.8365 14.7828C12.6803 14.939 12.4271 14.939 12.2708 14.7828C12.1146 14.6265 12.1146 14.3733 12.2708 14.2171L13.588 12.8999H1.51937L2.83653 14.2171C2.99274 14.3733 2.99274 14.6265 2.83653 14.7828C2.68032 14.939 2.42705 14.939 2.27084 14.7828L0.270843 12.7828C0.195828 12.7077 0.153687 12.606 0.153687 12.4999C0.153687 12.3938 0.195828 12.2921 0.270843 12.2171L2.27084 10.2171C2.42705 10.0609 2.68032 10.0609 2.83653 10.2171C2.99274 10.3733 2.99274 10.6265 2.83653 10.7828L1.51937 12.0999L13.588 12.0999L12.2708 10.7828C12.1146 10.6265 12.1146 10.3733 12.2708 10.2171C12.4271 10.0609 12.6803 10.0609 12.8365 10.2171L14.8365 12.2171C14.9115 12.2921 14.9537 12.3938 14.9537 12.4999Z" fill="#000000"/></svg>`,
|
||
pauseAnimations: `<svg xmlns="http://www.w3.org/2000/svg" version="1.2" viewBox="0 0 37 36"><g fill="none" fill-rule="evenodd"><path fill="currentColor" d="M15.8087111 23.6666667h-1.2702778c-.4429444 0-.8018333-.3598334-.8018333-.8027778v-9.7277778c0-.4429444.3588889-.8027778.8018333-.8027778h1.2702778c.4429445 0 .8027778.3598334.8027778.8027778v9.7277778c0 .4429444-.3598333.8027778-.8027778.8027778m6.6525722 0h-1.2702777c-.442 0-.8018334-.3598334-.8018334-.8027778v-9.7277778c0-.4429444.3598334-.8027778.8018334-.8027778h1.2702777c.4438889 0 .8027778.3598334.8027778.8027778v9.7277778c0 .4429444-.3588889.8027778-.8027778.8027778"/><path stroke="currentColor" stroke-linecap="round" stroke-width="1.88888889" d="M18.5 4.77777778V1m0 34v-3.7777778M31.7222222 18H35.5m-34 0h3.77777778m3.87278889-9.34943333L6.47873333 5.97967778M30.5204167 30.0204167l-2.6708889-2.6708889m-.0000945-18.69896113 2.6708889-2.67088889M6.47911111 30.0204167l2.67183333-2.6708889M23.5542889 5.78219444l1.4440555-3.49066666M12.0013722 33.7087556l1.4440556-3.4906667m17.2723778-7.1638 3.4906666 1.4440555M2.79124444 11.5013722l3.49066667 1.4440556m7.15274999-7.15860558L11.9877722 2.2971m13.0246445 31.4061778-1.4468889-3.4897222m7.14765-17.2788945L34.2029 11.4877722M2.79672222 24.5124167l3.48972222-1.4468889"/></g></svg>`,
|
||
hideImages: `<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M32 12C16 12 4 32 4 32s12 20 28 20 28-20 28-20S48 12 32 12zm0 32a12 12 0 1112-12 12 12 0 01-12 12z"/><circle cx="32" cy="32" r="8"/></svg>`,
|
||
dyslexiaFont: `<svg xmlns="http://www.w3.org/2000/svg" version="1.2" viewBox="0 0 31 22"><path fill="currentColor" fill-rule="evenodd" d="M.5 22V1.0083333h7.2421899c6.8051611 0 11.6124768 4.3388889 11.6124768 10.4805556C19.3546667 17.6305556 14.547351 22 7.7421899 22H.5Zm2.4348742-4.31h4.8073157c5.3692097 0 9.1463863-2.8616703 9.1463863-7.27 0-4.3807776-3.7771766-7.2422222-9.1463863-7.2422222H2.9348742V17.69ZM26.2735913 4.0333333l.0114609 2.1694445h4.0126191V8.25h-4.001719L26.77 22h-3.535416L23.78 8.25h-2.4238344V6.2027778h2.55923l.0751088-2.1694445C24.0706908 1.6805556 25.6007488 0 27.697782 0 28.6896221 0 29.677687.3666667 30.5 1.0083333l-.9627285 1.6805556c-.3479788-.3666667-.9515992-.6416667-1.627768-.6416667-.8819593 0-1.6420082.825-1.6359122 1.9861111Z"/></svg>`,
|
||
biggerCursor: `<svg xmlns="http://www.w3.org/2000/svg" version="1.2" viewBox="0 0 27 27"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15.9983464 11.5517813 9.5269972 9.52699721-4.4465655 4.44656549-9.5269972-9.52699717-4.05145413 9.06403815L1 1.0000004l24.0623846 6.5003268z"/></svg>`,
|
||
lineHeight: `<svg xmlns="http://www.w3.org/2000/svg" version="1.2" viewBox="0 0 47 25"><g fill="none" fill-rule="evenodd"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M3.99999962 2.71042226V22.7104223"/><path fill="currentColor" d="m.16814235 20.5270412 3.44487862 4.2104072c.17486379.2137224.48987514.2452235.70359754.0703597a.4999988.4999988 0 0 0 .07035976-.0703597l3.44487862-4.2104072c.17486378-.2137225.14336265-.5287338-.07035976-.7035976-.08933106-.073089-.20119771-.1130213-.31661889-.1130213H.555121c-.27614238 0-.5.2238576-.5.5 0 .1154211.0399323.2272878.11302135.3166189Zm0-16.1332381L3.61302097.18339592c.17486379-.21372241.48987514-.24522355.70359754-.07035976a.49999975.49999975 0 0 1 .07035976.07035976l3.44487862 4.2104072c.17486378.2137224.14336265.52873375-.07035976.70359754-.08933106.07308905-.20119771.11302135-.31661889.11302135H.555121c-.27614237 0-.5-.22385762-.5-.5 0-.11542118.0399323-.22728783.11302135-.3166189Z"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.4999996 1.71042226h30m-30 7h30m-30 7.00000004h30m-30 7h24"/></g></svg>`,
|
||
textAlign: `<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M10 16h44v4H10zm0 12h44v4H10zm0 12h44v4H10zm0 12h44v4H10z"/></svg>`,
|
||
screenReader: `<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M16 24 L24 24 L32 16 L32 48 L24 40 L16 40 Z" fill="#333" stroke="#555" stroke-width="2"/><path d="M36 20 C42 24, 42 40, 36 44" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"/><path d="M36 12 C48 24, 48 40, 36 52" fill="none" stroke="#555" stroke-width="2" stroke-linecap="round"/><rect x="28" y="48" width="8" height="8" fill="#ccc"/></svg>`,
|
||
resetAll: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 17" width="100%" height="100%"><g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-width="1.84"><path d="M16.20106 8c0 .9667-.189683 1.8872-.5324673 2.7251-.3427843.8372-.8386698 1.5911-1.4517524 2.2246-.6130825.6335-1.3426846 1.1459-2.152902 1.5001-.8108948.3542-1.70172746.5502-2.6372711.5502-.93554365 0-1.8263763-.196-2.63727112-.5502-.81021738-.3542-1.53981948-.8666-2.15290203-1.5001M2.6522744 8c0-.9667.189683-1.8872.53246728-2.7251.34278427-.8372.83866982-1.5911 1.45175237-2.2246.61308255-.6335 1.34268465-1.1459 2.15290203-1.5001C7.6002909 1.196 8.49112355 1 9.4266672 1c.93554364 0 1.8263763.196 2.6372711.5502.8102174.3542 1.5398195.8666 2.152902 1.5001"></path><path stroke-linejoin="round" d="m4.92576062 6.96092-2.48958935 1.484L1 5.87242m13.0125924 2.93832 2.3886509-1.652L18 9.62694"></path></g></svg>`,
|
||
voiceControl: `<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M32 44a12 12 0 0012-12V20a12 12 0 10-24 0v12a12 12 0 0012 12z" fill="#333"/><path d="M20 32h24v4H20z" fill="#555"/><path d="M32 48v8" stroke="#555" stroke-width="4" stroke-linecap="round"/></svg>`,
|
||
fontSelection: `<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><text x="32" y="40" font-family="serif" font-size="24" text-anchor="middle" fill="#333">Aa</text><path d="M8 48h48v2H8z"/></svg>`,
|
||
colorFilter: `<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="24" fill="none" stroke="#333" stroke-width="2"/><path d="M32 8a24 24 0 000 48V8z" fill="#f00" opacity="0.3"/><path d="M32 8a24 24 0 000 48" fill="none" stroke="#333" stroke-width="2" stroke-dasharray="4,2"/></svg>`,
|
||
reducedMotion: `<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect x="16" y="24" width="8" height="16" fill="#333"/><rect x="28" y="24" width="8" height="16" fill="#333"/><rect x="40" y="24" width="8" height="16" fill="#333"/></svg>`,
|
||
};
|
||
|
||
// ===========================================
|
||
// 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 = `
|
||
<defs>
|
||
<filter id="protanopia-filter">
|
||
<feColorMatrix type="matrix" values="0.567,0.433,0,0,0 0.558,0.442,0,0,0 0,0.242,0.758,0,0 0,0,0,1,0"/>
|
||
</filter>
|
||
<filter id="deuteranopia-filter">
|
||
<feColorMatrix type="matrix" values="0.625,0.375,0,0,0 0.7,0.3,0,0,0 0,0.3,0.7,0,0 0,0,0,1,0"/>
|
||
</filter>
|
||
<filter id="tritanopia-filter">
|
||
<feColorMatrix type="matrix" values="0.95,0.05,0,0,0 0,0.433,0.567,0,0 0,0.475,0.525,0,0 0,0,0,1,0"/>
|
||
</filter>
|
||
</defs>
|
||
`;
|
||
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 = {
|
||
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' },
|
||
{ 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
|
||
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 text alignment
|
||
const alignClasses = ['snn-text-align-left', 'snn-text-align-center', 'snn-text-align-right'];
|
||
domCache.body.classList.remove(...alignClasses);
|
||
const selectedAlign = localStorage.getItem('textAlign');
|
||
if (selectedAlign) {
|
||
domCache.body.classList.add(`snn-text-align-${selectedAlign}`);
|
||
}
|
||
|
||
// Handle bigger text
|
||
const textClasses = ['snn-bigger-text-medium', 'snn-bigger-text-large', 'snn-bigger-text-xlarge'];
|
||
domCache.body.classList.remove(...textClasses);
|
||
const selectedTextSize = localStorage.getItem('biggerText');
|
||
if (selectedTextSize) {
|
||
domCache.body.classList.add(`snn-bigger-text-${selectedTextSize}`);
|
||
}
|
||
|
||
// Handle high contrast
|
||
const contrastClasses = ['snn-high-contrast-medium', 'snn-high-contrast-high', 'snn-high-contrast-ultra'];
|
||
domCache.body.classList.remove(...contrastClasses);
|
||
const selectedContrast = localStorage.getItem('highContrast');
|
||
if (selectedContrast) {
|
||
domCache.body.classList.add(`snn-high-contrast-${selectedContrast}`);
|
||
}
|
||
|
||
// Handle Text Spacing (3 Levels)
|
||
const spacingClasses = ['snn-text-spacing-light', 'snn-text-spacing-medium', 'snn-text-spacing-heavy'];
|
||
domCache.body.classList.remove(...spacingClasses);
|
||
const selectedSpacing = localStorage.getItem('textSpacing');
|
||
if (selectedSpacing) {
|
||
domCache.body.classList.add(`snn-text-spacing-${selectedSpacing}`);
|
||
}
|
||
|
||
// Handle Line Height (3 Levels)
|
||
const lineHeightClasses = ['snn-line-height-2em', 'snn-line-height-3em', 'snn-line-height-4em'];
|
||
domCache.body.classList.remove(...lineHeightClasses);
|
||
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', WIDGET_CONFIG.lang.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',
|
||
];
|
||
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-align',
|
||
'snn-font-arial',
|
||
'snn-font-times',
|
||
'snn-font-verdana'
|
||
];
|
||
cssClasses.forEach(cls => document.body.classList.remove(cls));
|
||
|
||
const bodyClasses2 = [
|
||
'snn-high-contrast-medium',
|
||
'snn-high-contrast-high',
|
||
'snn-high-contrast-ultra'
|
||
];
|
||
bodyClasses2.forEach(cls => document.body.classList.remove(cls));
|
||
|
||
const documentClasses = [
|
||
'snn-filter-protanopia',
|
||
'snn-filter-deuteranopia',
|
||
'snn-filter-tritanopia',
|
||
'snn-filter-grayscale'
|
||
];
|
||
documentClasses.forEach(cls => document.documentElement.classList.remove(cls));
|
||
|
||
const textSizeClasses = [
|
||
'snn-bigger-text-medium',
|
||
'snn-bigger-text-large',
|
||
'snn-bigger-text-xlarge'
|
||
];
|
||
textSizeClasses.forEach(cls => document.body.classList.remove(cls));
|
||
|
||
// Clear Multi-level classes
|
||
const spacingClasses = ['snn-text-spacing-light', 'snn-text-spacing-medium', 'snn-text-spacing-heavy'];
|
||
spacingClasses.forEach(cls => document.body.classList.remove(cls));
|
||
|
||
const lineHeightClasses = ['snn-line-height-2em', 'snn-line-height-3em', 'snn-line-height-4em'];
|
||
lineHeightClasses.forEach(cls => document.body.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
|
||
) {
|
||
const button = document.createElement('button');
|
||
button.innerHTML = `
|
||
<span class="snn-icon">${iconSVG}</span>
|
||
<span class="snn-button-text">${buttonText}</span>
|
||
`;
|
||
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} ${WIDGET_CONFIG.lang.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) {
|
||
const button = document.createElement('button');
|
||
|
||
let buttonHTML = `
|
||
<span class="snn-icon">${iconSVG}</span>
|
||
<span class="snn-button-text">${buttonText}</span>
|
||
`;
|
||
|
||
// Add option steps if configured
|
||
if (optionsConfig) {
|
||
buttonHTML += '<div class="snn-option-steps">';
|
||
for (let i = 0; i < optionsConfig.count; i++) {
|
||
buttonHTML += '<div class="snn-option-step"></div>';
|
||
}
|
||
buttonHTML += '</div>';
|
||
}
|
||
|
||
button.innerHTML = buttonHTML;
|
||
button.setAttribute('aria-label', buttonText);
|
||
button.classList.add('snn-accessibility-option');
|
||
button.setAttribute('data-options-config', optionsConfig ? JSON.stringify(optionsConfig) : '');
|
||
|
||
// Update initial status
|
||
updateActionButtonStatus(button, buttonText, optionsConfig);
|
||
|
||
button.addEventListener('click', function () {
|
||
const result = actionFunction();
|
||
if (result) {
|
||
updateActionButtonStatus(button, buttonText, optionsConfig);
|
||
}
|
||
});
|
||
|
||
button.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
const result = actionFunction();
|
||
if (result) {
|
||
updateActionButtonStatus(button, buttonText, optionsConfig);
|
||
}
|
||
}
|
||
});
|
||
|
||
return button;
|
||
}
|
||
|
||
// Update action button status on page load
|
||
function updateActionButtonStatus(button, buttonText, optionsConfig) {
|
||
if (!optionsConfig) return;
|
||
|
||
const steps = button.querySelectorAll('.snn-option-step');
|
||
let currentIndex = -1;
|
||
|
||
if (buttonText.includes('Font')) {
|
||
const currentFont = localStorage.getItem('fontSelection');
|
||
const fonts = ['arial', 'times', 'verdana'];
|
||
currentIndex = currentFont ? fonts.indexOf(currentFont) : -1;
|
||
} else if (buttonText.includes('Color')) {
|
||
const currentFilter = localStorage.getItem('colorFilter');
|
||
const filters = ['protanopia', 'deuteranopia', 'tritanopia', 'grayscale'];
|
||
currentIndex = currentFilter ? filters.indexOf(currentFilter) : -1;
|
||
} else if (buttonText.includes('Text Align')) {
|
||
const currentAlign = localStorage.getItem('textAlign');
|
||
const alignments = ['left', 'center', 'right'];
|
||
currentIndex = currentAlign ? alignments.indexOf(currentAlign) : -1;
|
||
} else if (buttonText.includes('Text Size')) {
|
||
const currentSize = localStorage.getItem('biggerText');
|
||
const sizes = ['medium', 'large', 'xlarge'];
|
||
currentIndex = currentSize ? sizes.indexOf(currentSize) : -1;
|
||
} else if (buttonText.includes('High Contrast')) {
|
||
const currentContrast = localStorage.getItem('highContrast');
|
||
const contrasts = ['medium', 'high', 'ultra'];
|
||
currentIndex = currentContrast ? contrasts.indexOf(currentContrast) : -1;
|
||
} else if (buttonText.includes('Text Spacing')) {
|
||
const currentSpacing = localStorage.getItem('textSpacing');
|
||
const spacings = ['light', 'medium', 'heavy'];
|
||
currentIndex = currentSpacing ? spacings.indexOf(currentSpacing) : -1;
|
||
} else if (buttonText.includes('Line Height')) {
|
||
const currentLineHeight = localStorage.getItem('lineHeight');
|
||
const heights = ['2em', '3em', '4em'];
|
||
currentIndex = currentLineHeight ? heights.indexOf(currentLineHeight) : -1;
|
||
}
|
||
|
||
// Update step indicators
|
||
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 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 WIDGET_CONFIG.lang.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);
|
||
}
|
||
}
|
||
|
||
// 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 WIDGET_CONFIG.lang.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 all alignment classes
|
||
const alignClasses = ['snn-text-align-left', 'snn-text-align-center', 'snn-text-align-right'];
|
||
domCache.body.classList.remove(...alignClasses);
|
||
|
||
if (nextIndex === alignments.length) {
|
||
// Default alignment
|
||
localStorage.removeItem('textAlign');
|
||
return WIDGET_CONFIG.lang.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 all text size classes
|
||
const textClasses = ['snn-bigger-text-medium', 'snn-bigger-text-large', 'snn-bigger-text-xlarge'];
|
||
domCache.body.classList.remove(...textClasses);
|
||
|
||
if (nextIndex === textSizes.length) {
|
||
// Default text size
|
||
localStorage.removeItem('biggerText');
|
||
return WIDGET_CONFIG.lang.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 all contrast classes
|
||
const contrastClasses = ['snn-high-contrast-medium', 'snn-high-contrast-high', 'snn-high-contrast-ultra'];
|
||
domCache.body.classList.remove(...contrastClasses);
|
||
|
||
if (nextIndex === contrastLevels.length) {
|
||
// Default contrast
|
||
localStorage.removeItem('highContrast');
|
||
return WIDGET_CONFIG.lang.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 all spacing classes
|
||
const spacingClasses = ['snn-text-spacing-light', 'snn-text-spacing-medium', 'snn-text-spacing-heavy'];
|
||
domCache.body.classList.remove(...spacingClasses);
|
||
|
||
if (nextIndex === spacings.length) {
|
||
// Default
|
||
localStorage.removeItem('textSpacing');
|
||
return WIDGET_CONFIG.lang.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 all line height classes
|
||
const heightClasses = ['snn-line-height-2em', 'snn-line-height-3em', 'snn-line-height-4em'];
|
||
domCache.body.classList.remove(...heightClasses);
|
||
|
||
if (nextIndex === heights.length) {
|
||
// Default
|
||
localStorage.removeItem('lineHeight');
|
||
return WIDGET_CONFIG.lang.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 ${WIDGET_CONFIG.lang.notSupportedBrowser}`);
|
||
return false;
|
||
}
|
||
|
||
screenReader.active = isActive;
|
||
localStorage.setItem('screenReader', isActive);
|
||
|
||
try {
|
||
if (isActive) {
|
||
document.addEventListener('focusin', screenReader.handleFocus);
|
||
const feedbackSpeech = new SpeechSynthesisUtterance(WIDGET_CONFIG.lang.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(WIDGET_CONFIG.lang.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 ${WIDGET_CONFIG.lang.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(WIDGET_CONFIG.lang.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 {
|
||
// Check for show menu commands
|
||
if (WIDGET_CONFIG.voiceCommands.showMenu.some(cmd => command.includes(cmd))) {
|
||
if (!menuCache.button) menuCache.init();
|
||
if (menuCache.button) {
|
||
menuCache.button.click();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Check for reset all commands
|
||
if (WIDGET_CONFIG.voiceCommands.resetAll.some(cmd => command.includes(cmd))) {
|
||
resetAccessibilitySettings();
|
||
return;
|
||
}
|
||
|
||
// Build dynamic command map based on configuration
|
||
let localStorageKey = null;
|
||
|
||
// Check each command group
|
||
if (WIDGET_CONFIG.voiceCommands.highContrast.some(cmd => command.includes(cmd))) {
|
||
localStorageKey = 'highContrast';
|
||
} else if (WIDGET_CONFIG.voiceCommands.biggerText.some(cmd => command.includes(cmd))) {
|
||
localStorageKey = 'biggerText';
|
||
} else if (WIDGET_CONFIG.voiceCommands.textSpacing.some(cmd => command.includes(cmd))) {
|
||
localStorageKey = 'textSpacing';
|
||
} else if (WIDGET_CONFIG.voiceCommands.pauseAnimations.some(cmd => command.includes(cmd))) {
|
||
localStorageKey = 'pauseAnimations';
|
||
} else if (WIDGET_CONFIG.voiceCommands.hideImages.some(cmd => command.includes(cmd))) {
|
||
localStorageKey = 'hideImages';
|
||
} else if (WIDGET_CONFIG.voiceCommands.dyslexiaFont.some(cmd => command.includes(cmd))) {
|
||
localStorageKey = 'dyslexiaFont';
|
||
} else if (WIDGET_CONFIG.voiceCommands.biggerCursor.some(cmd => command.includes(cmd))) {
|
||
localStorageKey = 'biggerCursor';
|
||
} else if (WIDGET_CONFIG.voiceCommands.lineHeight.some(cmd => command.includes(cmd))) {
|
||
localStorageKey = 'lineHeight';
|
||
} else if (WIDGET_CONFIG.voiceCommands.textAlign.some(cmd => command.includes(cmd))) {
|
||
localStorageKey = 'textAlign';
|
||
} else if (WIDGET_CONFIG.voiceCommands.screenReader.some(cmd => command.includes(cmd))) {
|
||
localStorageKey = 'screenReader';
|
||
} else if (WIDGET_CONFIG.voiceCommands.voiceControl.some(cmd => command.includes(cmd))) {
|
||
localStorageKey = 'voiceControl';
|
||
}
|
||
|
||
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('div');
|
||
title.classList.add('snn-title');
|
||
title.id = 'snn-accessibility-title';
|
||
title.textContent = WIDGET_CONFIG.lang.accessibilityTools;
|
||
|
||
// Create reset button
|
||
const resetButton = document.createElement('button');
|
||
resetButton.classList.add('snn-reset-button');
|
||
resetButton.innerHTML = `${icons.resetAll}<span class="snn-tooltip">${WIDGET_CONFIG.lang.reset}</span>`;
|
||
resetButton.setAttribute('aria-label', WIDGET_CONFIG.lang.resetAllSettings);
|
||
resetButton.addEventListener('click', resetAccessibilitySettings);
|
||
|
||
// Create close button
|
||
const closeButton = document.createElement('button');
|
||
closeButton.className = 'snn-close';
|
||
closeButton.innerHTML = `<span class="snn-tooltip">${WIDGET_CONFIG.lang.close}</span>`;
|
||
closeButton.setAttribute('aria-label', WIDGET_CONFIG.lang.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 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: WIDGET_CONFIG.lang.textSize,
|
||
actionFunction: handleBiggerText,
|
||
icon: icons.biggerText,
|
||
enabled: WIDGET_CONFIG.enableBiggerText,
|
||
optionsConfig: { count: 3 }
|
||
},
|
||
{
|
||
order: 2,
|
||
type: 'action',
|
||
text: WIDGET_CONFIG.lang.highContrast,
|
||
actionFunction: handleHighContrast,
|
||
icon: icons.highContrast,
|
||
enabled: WIDGET_CONFIG.enableHighContrast,
|
||
optionsConfig: { count: 3 }
|
||
},
|
||
{
|
||
order: 3,
|
||
type: 'action',
|
||
text: WIDGET_CONFIG.lang.textAlign,
|
||
actionFunction: handleTextAlign,
|
||
icon: icons.textAlign,
|
||
enabled: WIDGET_CONFIG.enableTextAlign,
|
||
optionsConfig: { count: 3 }
|
||
},
|
||
{
|
||
order: 4,
|
||
type: 'action',
|
||
text: WIDGET_CONFIG.lang.colorFilter,
|
||
actionFunction: handleColorFilter,
|
||
icon: icons.colorFilter,
|
||
enabled: WIDGET_CONFIG.enableColorFilter,
|
||
optionsConfig: { count: 4 }
|
||
},
|
||
|
||
// Order 5-11: Other visual/text features
|
||
{
|
||
order: 5,
|
||
type: 'action', // Changed from toggle to action
|
||
text: WIDGET_CONFIG.lang.textSpacing,
|
||
actionFunction: handleTextSpacing,
|
||
icon: icons.textSpacing,
|
||
enabled: WIDGET_CONFIG.enableTextSpacing,
|
||
optionsConfig: { count: 3 }
|
||
},
|
||
{
|
||
order: 6,
|
||
type: 'action', // Changed from toggle to action
|
||
text: WIDGET_CONFIG.lang.lineHeight,
|
||
actionFunction: handleLineHeight,
|
||
icon: icons.lineHeight,
|
||
enabled: WIDGET_CONFIG.enableLineHeight,
|
||
optionsConfig: { count: 3 }
|
||
},
|
||
{
|
||
order: 7,
|
||
type: 'action',
|
||
text: WIDGET_CONFIG.lang.fontSelection,
|
||
actionFunction: handleFontSelection,
|
||
icon: icons.fontSelection,
|
||
enabled: WIDGET_CONFIG.enableFontSelection,
|
||
optionsConfig: { count: 3 }
|
||
},
|
||
{
|
||
order: 8,
|
||
type: 'toggle',
|
||
text: WIDGET_CONFIG.lang.dyslexiaFriendly,
|
||
key: 'dyslexiaFont',
|
||
className: 'snn-dyslexia-font',
|
||
icon: icons.dyslexiaFont,
|
||
enabled: WIDGET_CONFIG.enableDyslexiaFont,
|
||
},
|
||
{
|
||
order: 9,
|
||
type: 'toggle',
|
||
text: WIDGET_CONFIG.lang.biggerCursor,
|
||
key: 'biggerCursor',
|
||
className: 'snn-bigger-cursor',
|
||
icon: icons.biggerCursor,
|
||
enabled: WIDGET_CONFIG.enableBiggerCursor,
|
||
},
|
||
{
|
||
order: 10,
|
||
type: 'toggle',
|
||
text: WIDGET_CONFIG.lang.hideImages,
|
||
key: 'hideImages',
|
||
icon: icons.hideImages,
|
||
customToggleFunction: toggleHideImages,
|
||
enabled: WIDGET_CONFIG.enableHideImages,
|
||
},
|
||
|
||
// Order 11: Animation controls (Reduced Motion merged here)
|
||
{
|
||
order: 11,
|
||
type: 'toggle',
|
||
text: WIDGET_CONFIG.lang.pauseAnimations,
|
||
key: 'pauseAnimations',
|
||
className: 'snn-pause-animations',
|
||
icon: icons.pauseAnimations,
|
||
enabled: WIDGET_CONFIG.enablePauseAnimations,
|
||
},
|
||
|
||
// Order 98-99: Screen Reader and Voice Control (always last)
|
||
{
|
||
order: 98,
|
||
type: 'toggle',
|
||
text: WIDGET_CONFIG.lang.screenReader,
|
||
key: 'screenReader',
|
||
customToggleFunction: screenReader.toggle,
|
||
icon: icons.screenReader,
|
||
requiresFeature: screenReader,
|
||
enabled: WIDGET_CONFIG.enableScreenReader,
|
||
},
|
||
{
|
||
order: 99,
|
||
type: 'toggle',
|
||
text: WIDGET_CONFIG.lang.voiceCommand,
|
||
key: 'voiceControl',
|
||
customToggleFunction: voiceControl.toggle,
|
||
icon: icons.voiceControl,
|
||
requiresFeature: voiceControl,
|
||
enabled: WIDGET_CONFIG.enableVoiceControl,
|
||
},
|
||
];
|
||
|
||
// 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);
|
||
} else if (config.type === 'toggle') {
|
||
button = createToggleButton(
|
||
config.text,
|
||
config.key,
|
||
config.className,
|
||
config.target || document.body,
|
||
config.customToggleFunction,
|
||
config.icon,
|
||
config.requiresFeature
|
||
);
|
||
}
|
||
|
||
if (button) {
|
||
optionsGrid.appendChild(button);
|
||
}
|
||
});
|
||
|
||
// Add grid to content
|
||
content.appendChild(optionsGrid);
|
||
|
||
// Add content to menu
|
||
menu.appendChild(content);
|
||
|
||
shadowRoot.appendChild(menu);
|
||
}
|
||
|
||
// ===========================================
|
||
// 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');
|
||
|
||
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();
|
||
} |