/*
===========================================
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 20px 10px 20px;
}
.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;
}
/* 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: ``,
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 = {
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 saturation filters
const saturationClasses = ['snn-saturation-low', 'snn-saturation-high', 'snn-saturation-none'];
domCache.documentElement.classList.remove(...saturationClasses);
const selectedSaturation = localStorage.getItem('saturation');
if (selectedSaturation) {
domCache.documentElement.classList.add(`snn-saturation-${selectedSaturation}`);
}
// 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',
'saturation',
];
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',
'snn-saturation-low',
'snn-saturation-high',
'snn-saturation-none'
];
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 = `
${iconSVG}
${buttonText}
`;
button.setAttribute('data-key', localStorageKey);
button.setAttribute('aria-label', buttonText);
button.classList.add('snn-accessibility-option');
// Check if feature is supported
if (requiresFeature && !requiresFeature.isSupported) {
button.disabled = true;
button.setAttribute('title', `${buttonText} ${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 = `
${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) : '');
// 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;
} else if (buttonText.includes('Saturation')) {
const currentSaturation = localStorage.getItem('saturation');
const saturations = ['low', 'high', 'none'];
currentIndex = currentSaturation ? saturations.indexOf(currentSaturation) : -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);
}
}
// 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 all saturation classes in one operation
const saturationClasses = ['snn-saturation-low', 'snn-saturation-high', 'snn-saturation-none'];
domCache.documentElement.classList.remove(...saturationClasses);
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 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}${WIDGET_CONFIG.lang.reset}`;
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 = `${WIDGET_CONFIG.lang.close}`;
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: 7.5,
type: 'action',
text: 'Saturation',
actionFunction: handleSaturation,
icon: icons.saturation,
enabled: true,
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();
}