accessibility-widgets 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +350 -0
- package/package.json +72 -0
- package/widget.js +1526 -0
package/widget.js
ADDED
|
@@ -0,0 +1,1526 @@
|
|
|
1
|
+
/*
|
|
2
|
+
===========================================
|
|
3
|
+
ACCESSIBILITY WIDGET
|
|
4
|
+
A comprehensive web accessibility tool
|
|
5
|
+
===========================================
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ===========================================
|
|
9
|
+
// CONFIGURATION VARIABLES
|
|
10
|
+
// ===========================================
|
|
11
|
+
|
|
12
|
+
// Default configuration - can be overridden by user
|
|
13
|
+
const DEFAULT_WIDGET_CONFIG = {
|
|
14
|
+
// Core Features
|
|
15
|
+
enableHighContrast: true,
|
|
16
|
+
enableBiggerText: true,
|
|
17
|
+
enableTextSpacing: true,
|
|
18
|
+
enablePauseAnimations: true,
|
|
19
|
+
enableHideImages: true,
|
|
20
|
+
enableDyslexiaFont: true,
|
|
21
|
+
enableBiggerCursor: true,
|
|
22
|
+
enableLineHeight: true,
|
|
23
|
+
enableTextAlign: true,
|
|
24
|
+
|
|
25
|
+
// Advanced Features
|
|
26
|
+
enableScreenReader: true,
|
|
27
|
+
enableVoiceControl: true,
|
|
28
|
+
enableReducedMotion: true,
|
|
29
|
+
enableFontSelection: true,
|
|
30
|
+
enableColorFilter: true,
|
|
31
|
+
|
|
32
|
+
// Widget Styling
|
|
33
|
+
widgetWidth: '440px',
|
|
34
|
+
widgetPosition: {
|
|
35
|
+
side: 'right', // 'left' or 'right'
|
|
36
|
+
right: '20px',
|
|
37
|
+
left: '20px',
|
|
38
|
+
bottom: '20px'
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Colors
|
|
42
|
+
colors: {
|
|
43
|
+
primary: '#000000',
|
|
44
|
+
primaryHover: '#00bfff',
|
|
45
|
+
secondary: '#f9f9f9',
|
|
46
|
+
text: '#333',
|
|
47
|
+
textLight: '#fff',
|
|
48
|
+
border: '#e6e6e6',
|
|
49
|
+
borderHover: '#d4d4d4',
|
|
50
|
+
shadow: 'rgba(0, 0, 0, 0.2)',
|
|
51
|
+
focus: '#ff6b35',
|
|
52
|
+
focusGlow: 'rgba(255, 107, 53, 0.3)'
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// Button styling
|
|
56
|
+
button: {
|
|
57
|
+
size: '55px',
|
|
58
|
+
borderRadius: '100px',
|
|
59
|
+
iconSize: '40px',
|
|
60
|
+
shadow: '0 4px 8px rgba(0, 0, 0, 0.2)'
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Menu styling
|
|
64
|
+
menu: {
|
|
65
|
+
headerHeight: '55px',
|
|
66
|
+
padding: '0 10px 10px 10px',
|
|
67
|
+
optionPadding: '20px 10px',
|
|
68
|
+
optionMargin: '10px',
|
|
69
|
+
borderRadius: '8px',
|
|
70
|
+
fontSize: '16px',
|
|
71
|
+
titleFontSize: '22px',
|
|
72
|
+
closeButtonSize: '44px'
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// Typography
|
|
76
|
+
typography: {
|
|
77
|
+
fontFamily: 'Arial, sans-serif',
|
|
78
|
+
fontSize: '16px',
|
|
79
|
+
titleFontSize: '22px',
|
|
80
|
+
titleFontWeight: '500',
|
|
81
|
+
lineHeight: '1'
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// Animation
|
|
85
|
+
animation: {
|
|
86
|
+
transition: '0.2s',
|
|
87
|
+
hoverScale: '1.05'
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// Language/Text Configuration
|
|
91
|
+
lang: {
|
|
92
|
+
accessibilityMenu: 'Accessibility Menu',
|
|
93
|
+
closeAccessibilityMenu: 'Close Accessibility Menu',
|
|
94
|
+
accessibilityTools: 'Accessibility Tools',
|
|
95
|
+
resetAllSettings: 'Reset All Settings',
|
|
96
|
+
screenReader: 'Screen Reader',
|
|
97
|
+
voiceCommand: 'Voice Command',
|
|
98
|
+
textSpacing: 'Text Spacing',
|
|
99
|
+
pauseAnimations: 'Pause Animations',
|
|
100
|
+
hideImages: 'Hide Images',
|
|
101
|
+
dyslexiaFriendly: 'Dyslexia Friendly',
|
|
102
|
+
biggerCursor: 'Bigger Cursor',
|
|
103
|
+
lineHeight: 'Line Height',
|
|
104
|
+
reducedMotion: 'Reduced Motion',
|
|
105
|
+
fontSelection: 'Font Selection',
|
|
106
|
+
colorFilter: 'Color Filter',
|
|
107
|
+
textAlign: 'Text Align',
|
|
108
|
+
textSize: 'Text Size',
|
|
109
|
+
highContrast: 'High Contrast',
|
|
110
|
+
defaultFont: 'Default Font',
|
|
111
|
+
noFilter: 'No Filter',
|
|
112
|
+
default: 'Default',
|
|
113
|
+
screenReaderOn: 'Screen reader on',
|
|
114
|
+
screenReaderOff: 'Screen reader off',
|
|
115
|
+
voiceControlActivated: 'Voice control activated',
|
|
116
|
+
notSupportedBrowser: 'is not supported in this browser',
|
|
117
|
+
close: 'Close'
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Function to deep merge user configuration with defaults
|
|
122
|
+
function mergeConfigs(defaultConfig, userConfig) {
|
|
123
|
+
const result = { ...defaultConfig };
|
|
124
|
+
|
|
125
|
+
if (!userConfig) return result;
|
|
126
|
+
|
|
127
|
+
for (const key in userConfig) {
|
|
128
|
+
if (userConfig.hasOwnProperty(key)) {
|
|
129
|
+
if (typeof userConfig[key] === 'object' && userConfig[key] !== null && !Array.isArray(userConfig[key])) {
|
|
130
|
+
result[key] = mergeConfigs(defaultConfig[key] || {}, userConfig[key]);
|
|
131
|
+
} else {
|
|
132
|
+
result[key] = userConfig[key];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Merge user configuration with defaults
|
|
141
|
+
// Users can define window.ACCESSIBILITY_WIDGET_CONFIG before loading this script
|
|
142
|
+
const WIDGET_CONFIG = mergeConfigs(DEFAULT_WIDGET_CONFIG, window.ACCESSIBILITY_WIDGET_CONFIG || {});
|
|
143
|
+
|
|
144
|
+
// ===========================================
|
|
145
|
+
// STYLES & VISUAL ASSETS
|
|
146
|
+
// ===========================================
|
|
147
|
+
|
|
148
|
+
// Generate styles using configuration variables
|
|
149
|
+
const styles = `
|
|
150
|
+
#snn-accessibility-fixed-button {
|
|
151
|
+
position: fixed !important;
|
|
152
|
+
${WIDGET_CONFIG.widgetPosition.side}: ${WIDGET_CONFIG.widgetPosition[WIDGET_CONFIG.widgetPosition.side]} !important;
|
|
153
|
+
bottom: ${WIDGET_CONFIG.widgetPosition.bottom} !important;
|
|
154
|
+
z-index: 9999;
|
|
155
|
+
}
|
|
156
|
+
#snn-accessibility-button {
|
|
157
|
+
background: ${WIDGET_CONFIG.colors.primary};
|
|
158
|
+
border: none;
|
|
159
|
+
border-radius: ${WIDGET_CONFIG.button.borderRadius};
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
width: ${WIDGET_CONFIG.button.size};
|
|
162
|
+
height: ${WIDGET_CONFIG.button.size};
|
|
163
|
+
box-shadow: ${WIDGET_CONFIG.button.shadow};
|
|
164
|
+
transition: ${WIDGET_CONFIG.animation.transition} !important;
|
|
165
|
+
display: flex;
|
|
166
|
+
justify-content: center;
|
|
167
|
+
align-items: center;
|
|
168
|
+
}
|
|
169
|
+
#snn-accessibility-button:hover {
|
|
170
|
+
transform: scale(${WIDGET_CONFIG.animation.hoverScale});
|
|
171
|
+
}
|
|
172
|
+
#snn-accessibility-button:focus {
|
|
173
|
+
outline: 2px solid ${WIDGET_CONFIG.colors.textLight};
|
|
174
|
+
outline-offset: 2px;
|
|
175
|
+
}
|
|
176
|
+
#snn-accessibility-button svg {
|
|
177
|
+
width: ${WIDGET_CONFIG.button.iconSize};
|
|
178
|
+
height: ${WIDGET_CONFIG.button.iconSize};
|
|
179
|
+
fill: ${WIDGET_CONFIG.colors.textLight};
|
|
180
|
+
pointer-events: none;
|
|
181
|
+
}
|
|
182
|
+
#snn-accessibility-menu {
|
|
183
|
+
position: fixed;
|
|
184
|
+
top: 0;
|
|
185
|
+
${WIDGET_CONFIG.widgetPosition.side}: 0;
|
|
186
|
+
width: ${WIDGET_CONFIG.widgetWidth};
|
|
187
|
+
height: 100vh;
|
|
188
|
+
overflow-y: auto;
|
|
189
|
+
background-color: ${WIDGET_CONFIG.colors.secondary};
|
|
190
|
+
padding: 0;
|
|
191
|
+
display: none;
|
|
192
|
+
font-family: ${WIDGET_CONFIG.typography.fontFamily};
|
|
193
|
+
z-index: 9999;
|
|
194
|
+
scrollbar-width: thin;
|
|
195
|
+
}
|
|
196
|
+
.snn-accessibility-option {
|
|
197
|
+
font-size: ${WIDGET_CONFIG.menu.fontSize};
|
|
198
|
+
display: flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
padding: ${WIDGET_CONFIG.menu.optionPadding};
|
|
201
|
+
width: 100%;
|
|
202
|
+
background-color: ${WIDGET_CONFIG.colors.border};
|
|
203
|
+
color: ${WIDGET_CONFIG.colors.text};
|
|
204
|
+
border: none;
|
|
205
|
+
cursor: pointer;
|
|
206
|
+
border-radius: ${WIDGET_CONFIG.menu.borderRadius};
|
|
207
|
+
transition: background-color ${WIDGET_CONFIG.animation.transition};
|
|
208
|
+
line-height: ${WIDGET_CONFIG.typography.lineHeight} !important;
|
|
209
|
+
}
|
|
210
|
+
.snn-accessibility-option:hover {
|
|
211
|
+
background-color: ${WIDGET_CONFIG.colors.borderHover};
|
|
212
|
+
}
|
|
213
|
+
.snn-accessibility-option.active {
|
|
214
|
+
background-color: ${WIDGET_CONFIG.colors.primary};
|
|
215
|
+
color: ${WIDGET_CONFIG.colors.textLight};
|
|
216
|
+
}
|
|
217
|
+
.snn-icon {
|
|
218
|
+
margin-right: 12px;
|
|
219
|
+
width: ${WIDGET_CONFIG.button.iconSize};
|
|
220
|
+
height: ${WIDGET_CONFIG.button.iconSize};
|
|
221
|
+
}
|
|
222
|
+
.snn-icon svg {
|
|
223
|
+
width: 100%;
|
|
224
|
+
height: 100%;
|
|
225
|
+
fill: currentColor;
|
|
226
|
+
}
|
|
227
|
+
.snn-close {
|
|
228
|
+
background: none;
|
|
229
|
+
border: none;
|
|
230
|
+
font-size: ${WIDGET_CONFIG.menu.closeButtonSize};
|
|
231
|
+
color: ${WIDGET_CONFIG.colors.textLight};
|
|
232
|
+
cursor: pointer;
|
|
233
|
+
margin-left: auto;
|
|
234
|
+
line-height: ${WIDGET_CONFIG.typography.lineHeight};
|
|
235
|
+
border-radius: ${WIDGET_CONFIG.button.borderRadius};
|
|
236
|
+
width: ${WIDGET_CONFIG.menu.closeButtonSize};
|
|
237
|
+
height: ${WIDGET_CONFIG.menu.closeButtonSize};
|
|
238
|
+
position: relative;
|
|
239
|
+
}
|
|
240
|
+
.snn-close::before {
|
|
241
|
+
content: '×';
|
|
242
|
+
position: absolute;
|
|
243
|
+
top: 50%;
|
|
244
|
+
left: 50%;
|
|
245
|
+
transform: translate(-50%, -50%);
|
|
246
|
+
font-size: ${WIDGET_CONFIG.menu.closeButtonSize};
|
|
247
|
+
line-height: 1;
|
|
248
|
+
}
|
|
249
|
+
.snn-close:focus {
|
|
250
|
+
outline: solid 2px ${WIDGET_CONFIG.colors.textLight};
|
|
251
|
+
}
|
|
252
|
+
.snn-close:hover {
|
|
253
|
+
color: ${WIDGET_CONFIG.colors.text};
|
|
254
|
+
}
|
|
255
|
+
.snn-header {
|
|
256
|
+
display: flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
margin-bottom: 20px;
|
|
259
|
+
padding: 10px;
|
|
260
|
+
background: #000000;
|
|
261
|
+
height: ${WIDGET_CONFIG.menu.headerHeight};
|
|
262
|
+
position: sticky;
|
|
263
|
+
top: 0;
|
|
264
|
+
z-index: 10;
|
|
265
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.snn-content {
|
|
269
|
+
padding: 0 10px 10px 10px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.snn-reset-button {
|
|
273
|
+
font-size: ${WIDGET_CONFIG.menu.fontSize};
|
|
274
|
+
display: flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
justify-content: center;
|
|
277
|
+
margin-bottom: 10px;
|
|
278
|
+
padding: ${WIDGET_CONFIG.menu.optionPadding};
|
|
279
|
+
width: 100%;
|
|
280
|
+
background-color: #343434;
|
|
281
|
+
color: ${WIDGET_CONFIG.colors.textLight};
|
|
282
|
+
border: none;
|
|
283
|
+
cursor: pointer;
|
|
284
|
+
border-radius: ${WIDGET_CONFIG.menu.borderRadius};
|
|
285
|
+
transition: background-color ${WIDGET_CONFIG.animation.transition};
|
|
286
|
+
line-height: ${WIDGET_CONFIG.typography.lineHeight} !important;
|
|
287
|
+
font-weight: 500;
|
|
288
|
+
gap: 8px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.snn-reset-button:hover {
|
|
292
|
+
background-color: #cc3333;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.snn-options-grid {
|
|
296
|
+
display: grid;
|
|
297
|
+
grid-template-columns: 1fr 1fr;
|
|
298
|
+
gap: ${WIDGET_CONFIG.menu.optionMargin};
|
|
299
|
+
margin-bottom: 20px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
.snn-title {
|
|
304
|
+
margin: 0;
|
|
305
|
+
font-size: ${WIDGET_CONFIG.menu.titleFontSize};
|
|
306
|
+
color: ${WIDGET_CONFIG.colors.textLight};
|
|
307
|
+
line-height: ${WIDGET_CONFIG.typography.lineHeight} !important;
|
|
308
|
+
margin-left: 5px;
|
|
309
|
+
font-weight: ${WIDGET_CONFIG.typography.titleFontWeight};
|
|
310
|
+
}
|
|
311
|
+
/* Accessibility feature styles */
|
|
312
|
+
.snn-high-contrast-medium {
|
|
313
|
+
filter: contrast(1.3) !important;
|
|
314
|
+
}
|
|
315
|
+
.snn-high-contrast-medium *{
|
|
316
|
+
filter: contrast(1.3) !important;
|
|
317
|
+
}
|
|
318
|
+
.snn-high-contrast-medium #snn-accessibility-menu{
|
|
319
|
+
filter: contrast(0.8) !important;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.snn-high-contrast-high {
|
|
323
|
+
background-color: #000 !important;
|
|
324
|
+
color: #fff !important;
|
|
325
|
+
filter: contrast(1.5) !important;
|
|
326
|
+
}
|
|
327
|
+
.snn-high-contrast-high *{
|
|
328
|
+
background-color: #000 !important;
|
|
329
|
+
color: #fff !important;
|
|
330
|
+
filter: contrast(1.5) !important;
|
|
331
|
+
}
|
|
332
|
+
.snn-high-contrast-high #snn-accessibility-menu{
|
|
333
|
+
filter: contrast(0.7) !important;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.snn-high-contrast-ultra {
|
|
337
|
+
background-color: #000 !important;
|
|
338
|
+
color: #ffff00 !important;
|
|
339
|
+
filter: contrast(2.0) !important;
|
|
340
|
+
}
|
|
341
|
+
.snn-high-contrast-ultra *{
|
|
342
|
+
background-color: #000 !important;
|
|
343
|
+
color: #ffff00 !important;
|
|
344
|
+
filter: contrast(2.0) !important;
|
|
345
|
+
}
|
|
346
|
+
.snn-high-contrast-ultra #snn-accessibility-menu{
|
|
347
|
+
filter: contrast(0.6) !important;
|
|
348
|
+
}
|
|
349
|
+
.snn-bigger-text-medium * {
|
|
350
|
+
font-size: 20px !important;
|
|
351
|
+
}
|
|
352
|
+
.snn-bigger-text-large * {
|
|
353
|
+
font-size: 24px !important;
|
|
354
|
+
}
|
|
355
|
+
.snn-bigger-text-xlarge * {
|
|
356
|
+
font-size: 28px !important;
|
|
357
|
+
}
|
|
358
|
+
.snn-text-spacing *:not(#snn-accessibility-menu *, #snn-accessibility-fixed-button *, #snn-accessibility-button *, .snn-accessibility-option *) {
|
|
359
|
+
letter-spacing: 0.2em !important;
|
|
360
|
+
word-spacing: 0.3em !important;
|
|
361
|
+
}
|
|
362
|
+
.snn-pause-animations * {
|
|
363
|
+
animation: none !important;
|
|
364
|
+
transition: none !important;
|
|
365
|
+
}
|
|
366
|
+
.snn-dyslexia-font {
|
|
367
|
+
font-family: 'Comic Sans MS', 'Chalkboard SE', 'Bradley Hand', Brush Script MT, fantasy !important;
|
|
368
|
+
}
|
|
369
|
+
.snn-dyslexia-font * {
|
|
370
|
+
font-family: 'Comic Sans MS', 'Chalkboard SE', 'Bradley Hand', Brush Script MT, fantasy !important;
|
|
371
|
+
}
|
|
372
|
+
.snn-line-height *:not(#snn-accessibility-menu *, #snn-accessibility-fixed-button *, #snn-accessibility-button *, .snn-accessibility-option *) {
|
|
373
|
+
line-height: 2.5 !important;
|
|
374
|
+
}
|
|
375
|
+
.snn-text-align-left *:not(#snn-accessibility-menu *, #snn-accessibility-fixed-button *, #snn-accessibility-button *, .snn-accessibility-option *) {
|
|
376
|
+
text-align: left !important;
|
|
377
|
+
}
|
|
378
|
+
.snn-text-align-center *:not(#snn-accessibility-menu *, #snn-accessibility-fixed-button *, #snn-accessibility-button *, .snn-accessibility-option *) {
|
|
379
|
+
text-align: center !important;
|
|
380
|
+
}
|
|
381
|
+
.snn-text-align-right *:not(#snn-accessibility-menu *, #snn-accessibility-fixed-button *, #snn-accessibility-button *, .snn-accessibility-option *) {
|
|
382
|
+
text-align: right !important;
|
|
383
|
+
}
|
|
384
|
+
.snn-bigger-cursor {
|
|
385
|
+
cursor: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNzIiIHZpZXdCb3g9IjAgMCA0OCA3MiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNCAyVjcwTDIwIDU0SDM2TDQgMloiIGZpbGw9IiMwMDAiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSI0Ii8+PC9zdmc+'), auto !important;
|
|
386
|
+
}
|
|
387
|
+
.snn-bigger-cursor * {
|
|
388
|
+
cursor: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNzIiIHZpZXdCb3g9IjAgMCA0OCA3MiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNCAyVjcwTDIwIDU0SDM2TDQgMloiIGZpbGw9IiMwMDAiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSI0Ii8+PC9zdmc+'), auto !important;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* Font Selection */
|
|
392
|
+
.snn-font-arial {
|
|
393
|
+
font-family: Arial, sans-serif !important;
|
|
394
|
+
}
|
|
395
|
+
.snn-font-arial * {
|
|
396
|
+
font-family: Arial, sans-serif !important;
|
|
397
|
+
}
|
|
398
|
+
.snn-font-times {
|
|
399
|
+
font-family: 'Times New Roman', serif !important;
|
|
400
|
+
}
|
|
401
|
+
.snn-font-times * {
|
|
402
|
+
font-family: 'Times New Roman', serif !important;
|
|
403
|
+
}
|
|
404
|
+
.snn-font-verdana {
|
|
405
|
+
font-family: Verdana, sans-serif !important;
|
|
406
|
+
}
|
|
407
|
+
.snn-font-verdana * {
|
|
408
|
+
font-family: Verdana, sans-serif !important;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* Color Filters */
|
|
412
|
+
.snn-filter-protanopia {
|
|
413
|
+
filter: url('#protanopia-filter') !important;
|
|
414
|
+
}
|
|
415
|
+
.snn-filter-deuteranopia {
|
|
416
|
+
filter: url('#deuteranopia-filter') !important;
|
|
417
|
+
}
|
|
418
|
+
.snn-filter-tritanopia {
|
|
419
|
+
filter: url('#tritanopia-filter') !important;
|
|
420
|
+
}
|
|
421
|
+
.snn-filter-grayscale {
|
|
422
|
+
filter: grayscale(100%) !important;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/* Reduced Motion */
|
|
426
|
+
.snn-reduced-motion * {
|
|
427
|
+
animation: none !important;
|
|
428
|
+
transition: none !important;
|
|
429
|
+
}
|
|
430
|
+
.snn-reduced-motion *::before,
|
|
431
|
+
.snn-reduced-motion *::after {
|
|
432
|
+
animation: none !important;
|
|
433
|
+
transition: none !important;
|
|
434
|
+
}
|
|
435
|
+
`;
|
|
436
|
+
|
|
437
|
+
// ===========================================
|
|
438
|
+
// SVG ICONS
|
|
439
|
+
// ===========================================
|
|
440
|
+
|
|
441
|
+
// SVG icons
|
|
442
|
+
const icons = {
|
|
443
|
+
buttonsvg: `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="" height="" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" viewBox="0 0 2713 2713" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"><![CDATA[.fil1 {fill:${WIDGET_CONFIG.colors.primary}} .fil0 {fill:white}]]></style></defs><g id="Layer_x0020_1"><metadata id="CorelCorpID_0Corel-Layer"/><g id="_275060008"><circle class="fil0" cx="1356" cy="1356" r="1356"/><path class="fil1" d="M1305 315c-143,32 -237,147 -205,319 25,141 143,240 312,213 131,-21 237,-160 206,-324 -23,-125 -156,-243 -313,-208zm-150 1699l0 -340c1,-75 5,-367 1,-417 -9,-113 -93,-177 -174,-250 -19,-17 -33,-31 -53,-50 -19,-18 -35,-30 -54,-49 -19,-18 -34,-29 -53,-50 -38,-40 -162,-118 -98,-188 60,-65 124,34 188,86l111 99c11,10 17,13 27,25 9,12 16,18 28,28 35,30 72,64 125,85 122,50 214,44 334,-14 71,-34 103,-68 150,-113 9,-9 17,-15 27,-24 20,-18 39,-34 56,-51l108 -103c19,-18 29,-36 65,-39 33,-3 58,10 67,36 11,30 3,63 -13,83l-273 254c-40,31 -76,64 -109,98 -38,41 -54,80 -55,153 -3,243 -1,489 0,733 0,3 0,5 0,8 0,0 0,0 0,0 0,184 149,333 333,333 61,0 118,-17 167,-45 24,-18 48,-36 67,-51 39,-32 140,-145 171,-186 11,-16 19,-26 30,-42 104,-151 178,-317 209,-505 39,-242 -12,-506 -119,-712 -36,-69 -69,-123 -108,-178 -12,-15 -20,-24 -32,-39 -28,-36 -67,-84 -99,-115 -69,-66 -76,-68 -158,-129 -53,-39 -113,-70 -182,-103 -140,-67 -297,-100 -472,-102 -180,-2 -322,37 -472,97 -55,22 -93,42 -143,72 -55,33 -73,43 -127,87 -47,38 -70,60 -111,104 -6,6 -12,10 -18,17 -7,7 -9,13 -16,20 -8,9 -10,8 -17,18 -80,101 -91,116 -158,235 -64,113 -121,286 -136,435 -18,190 1,329 58,498 46,134 132,283 204,367 13,15 21,26 32,40 34,43 103,105 146,139 7,6 14,11 22,17 54,38 120,61 192,61 183,0 332,-149 332,-333l0 0z"/></g></g></svg>`,
|
|
444
|
+
highContrast: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#000"/><path d="M32 2a30 30 0 000 60V2z" fill="#fff"/></svg>`,
|
|
445
|
+
biggerText: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M32 8L8 56h12l6-12h24l6 12h12L32 8zm-6 36L32 20l6 24H26z"/></svg>`,
|
|
446
|
+
textSpacing: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M10 16h44v4H10zm0 12h44v4H10zm0 12h44v4H10zm0 12h44v4H10z"/></svg>`,
|
|
447
|
+
pauseAnimations: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect x="16" y="12" width="10" height="40"/><rect x="38" y="12" width="10" height="40"/></svg>`,
|
|
448
|
+
hideImages: `<svg 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>`,
|
|
449
|
+
dyslexiaFont: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M12 8v48h12a16 16 0 000-32h-8v-8h16V8H12zm12 24a8 8 0 010 16h-4V32h4zM40 8v48h12V8H40z"/></svg>`,
|
|
450
|
+
biggerCursor: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M12 4v56l16-16h24L12 4z"/></svg>`,
|
|
451
|
+
lineHeight: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M16 16h32v4H16zm0 12h32v4H16zm0 12h32v4H16zm0 12h32v4H16zM8 8l8 8-8 8V8zm0 32l8 8-8 8V40z"/></svg>`,
|
|
452
|
+
textAlign: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M10 16h44v4H10zm0 12h44v4H10zm0 12h44v4H10zm0 12h44v4H10z"/></svg>`,
|
|
453
|
+
screenReader: `<svg 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>`,
|
|
454
|
+
resetAll: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/><path d="M8 12l4-4 4 4-1.41 1.41L12 10.83l-2.59 2.58z" transform="rotate(45 12 12)"/></svg>`,
|
|
455
|
+
voiceControl: `<svg 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>`,
|
|
456
|
+
fontSelection: `<svg 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>`,
|
|
457
|
+
colorFilter: `<svg 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>`,
|
|
458
|
+
reducedMotion: `<svg 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>`,
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// ===========================================
|
|
462
|
+
// CORE UTILITY FUNCTIONS
|
|
463
|
+
// ===========================================
|
|
464
|
+
|
|
465
|
+
// Inject styles and SVG filters into the document
|
|
466
|
+
function injectStyles() {
|
|
467
|
+
const styleSheet = document.createElement('style');
|
|
468
|
+
styleSheet.innerText = styles;
|
|
469
|
+
document.head.appendChild(styleSheet);
|
|
470
|
+
|
|
471
|
+
// Add SVG color blindness filters
|
|
472
|
+
const svgFilters = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
473
|
+
svgFilters.style.position = 'absolute';
|
|
474
|
+
svgFilters.style.width = '0';
|
|
475
|
+
svgFilters.style.height = '0';
|
|
476
|
+
svgFilters.innerHTML = `
|
|
477
|
+
<defs>
|
|
478
|
+
<filter id="protanopia-filter">
|
|
479
|
+
<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"/>
|
|
480
|
+
</filter>
|
|
481
|
+
<filter id="deuteranopia-filter">
|
|
482
|
+
<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"/>
|
|
483
|
+
</filter>
|
|
484
|
+
<filter id="tritanopia-filter">
|
|
485
|
+
<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"/>
|
|
486
|
+
</filter>
|
|
487
|
+
</defs>
|
|
488
|
+
`;
|
|
489
|
+
document.body.appendChild(svgFilters);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ===========================================
|
|
493
|
+
// PERFORMANCE OPTIMIZATION
|
|
494
|
+
// ===========================================
|
|
495
|
+
|
|
496
|
+
// Cache for DOM elements to improve performance
|
|
497
|
+
const domCache = {
|
|
498
|
+
body: document.body,
|
|
499
|
+
documentElement: document.documentElement,
|
|
500
|
+
images: null,
|
|
501
|
+
lastImageUpdate: 0,
|
|
502
|
+
getImages: function() {
|
|
503
|
+
const now = Date.now();
|
|
504
|
+
if (!this.images || now - this.lastImageUpdate > 5000) {
|
|
505
|
+
this.images = document.querySelectorAll('img');
|
|
506
|
+
this.lastImageUpdate = now;
|
|
507
|
+
}
|
|
508
|
+
return this.images;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// Apply saved settings from localStorage (optimized)
|
|
513
|
+
function applySettings() {
|
|
514
|
+
const settings = [
|
|
515
|
+
{ key: 'biggerCursor', className: 'snn-bigger-cursor' },
|
|
516
|
+
{ key: 'biggerText', className: 'snn-bigger-text' },
|
|
517
|
+
{ key: 'highContrast', className: 'snn-high-contrast', target: domCache.documentElement },
|
|
518
|
+
{ key: 'dyslexiaFont', className: 'snn-dyslexia-font' },
|
|
519
|
+
{ key: 'lineHeight', className: 'snn-line-height' },
|
|
520
|
+
{ key: 'textAlign', className: 'snn-text-align' },
|
|
521
|
+
{ key: 'pauseAnimations', className: 'snn-pause-animations' },
|
|
522
|
+
{ key: 'textSpacing', className: 'snn-text-spacing' },
|
|
523
|
+
{ key: 'reducedMotion', className: 'snn-reduced-motion' },
|
|
524
|
+
];
|
|
525
|
+
|
|
526
|
+
// Batch DOM operations for better performance
|
|
527
|
+
const bodyClassesToAdd = [];
|
|
528
|
+
const bodyClassesToRemove = [];
|
|
529
|
+
const docClassesToAdd = [];
|
|
530
|
+
const docClassesToRemove = [];
|
|
531
|
+
|
|
532
|
+
settings.forEach(({ key, className, target = domCache.body }) => {
|
|
533
|
+
const isActive = localStorage.getItem(key) === 'true';
|
|
534
|
+
if (className) {
|
|
535
|
+
if (target === domCache.documentElement) {
|
|
536
|
+
if (isActive) {
|
|
537
|
+
docClassesToAdd.push(className);
|
|
538
|
+
} else {
|
|
539
|
+
docClassesToRemove.push(className);
|
|
540
|
+
}
|
|
541
|
+
} else {
|
|
542
|
+
if (isActive) {
|
|
543
|
+
bodyClassesToAdd.push(className);
|
|
544
|
+
} else {
|
|
545
|
+
bodyClassesToRemove.push(className);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Apply all class changes at once
|
|
552
|
+
if (bodyClassesToAdd.length > 0) {
|
|
553
|
+
domCache.body.classList.add(...bodyClassesToAdd);
|
|
554
|
+
}
|
|
555
|
+
if (bodyClassesToRemove.length > 0) {
|
|
556
|
+
domCache.body.classList.remove(...bodyClassesToRemove);
|
|
557
|
+
}
|
|
558
|
+
if (docClassesToAdd.length > 0) {
|
|
559
|
+
domCache.documentElement.classList.add(...docClassesToAdd);
|
|
560
|
+
}
|
|
561
|
+
if (docClassesToRemove.length > 0) {
|
|
562
|
+
domCache.documentElement.classList.remove(...docClassesToRemove);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Handle font selection
|
|
566
|
+
const fontClasses = ['snn-font-arial', 'snn-font-times', 'snn-font-verdana'];
|
|
567
|
+
domCache.body.classList.remove(...fontClasses);
|
|
568
|
+
const selectedFont = localStorage.getItem('fontSelection');
|
|
569
|
+
if (selectedFont) {
|
|
570
|
+
domCache.body.classList.add(`snn-font-${selectedFont}`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Handle color filters
|
|
574
|
+
const filterClasses = ['snn-filter-protanopia', 'snn-filter-deuteranopia', 'snn-filter-tritanopia', 'snn-filter-grayscale'];
|
|
575
|
+
domCache.documentElement.classList.remove(...filterClasses);
|
|
576
|
+
const selectedFilter = localStorage.getItem('colorFilter');
|
|
577
|
+
if (selectedFilter) {
|
|
578
|
+
domCache.documentElement.classList.add(`snn-filter-${selectedFilter}`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Handle text alignment
|
|
582
|
+
const alignClasses = ['snn-text-align-left', 'snn-text-align-center', 'snn-text-align-right'];
|
|
583
|
+
domCache.body.classList.remove(...alignClasses);
|
|
584
|
+
const selectedAlign = localStorage.getItem('textAlign');
|
|
585
|
+
if (selectedAlign) {
|
|
586
|
+
domCache.body.classList.add(`snn-text-align-${selectedAlign}`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Handle bigger text
|
|
590
|
+
const textClasses = ['snn-bigger-text-medium', 'snn-bigger-text-large', 'snn-bigger-text-xlarge'];
|
|
591
|
+
domCache.body.classList.remove(...textClasses);
|
|
592
|
+
const selectedTextSize = localStorage.getItem('biggerText');
|
|
593
|
+
if (selectedTextSize) {
|
|
594
|
+
domCache.body.classList.add(`snn-bigger-text-${selectedTextSize}`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Handle high contrast
|
|
598
|
+
const contrastClasses = ['snn-high-contrast-medium', 'snn-high-contrast-high', 'snn-high-contrast-ultra'];
|
|
599
|
+
domCache.documentElement.classList.remove(...contrastClasses);
|
|
600
|
+
const selectedContrast = localStorage.getItem('highContrast');
|
|
601
|
+
if (selectedContrast) {
|
|
602
|
+
domCache.documentElement.classList.add(`snn-high-contrast-${selectedContrast}`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Handle images with cached query
|
|
606
|
+
const hideImages = localStorage.getItem('hideImages') === 'true';
|
|
607
|
+
const displayStyle = hideImages ? 'none' : '';
|
|
608
|
+
domCache.getImages().forEach((img) => {
|
|
609
|
+
img.style.display = displayStyle;
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
if (screenReader.active && screenReader.isSupported) {
|
|
613
|
+
document.addEventListener('focusin', screenReader.handleFocus);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (voiceControl.isActive && voiceControl.isSupported) {
|
|
617
|
+
voiceControl.startListening();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ===========================================
|
|
622
|
+
// UI COMPONENTS
|
|
623
|
+
// ===========================================
|
|
624
|
+
|
|
625
|
+
// Create the accessibility button
|
|
626
|
+
function createAccessibilityButton() {
|
|
627
|
+
const buttonContainer = document.createElement('div');
|
|
628
|
+
buttonContainer.id = 'snn-accessibility-fixed-button';
|
|
629
|
+
|
|
630
|
+
const button = document.createElement('button');
|
|
631
|
+
button.id = 'snn-accessibility-button';
|
|
632
|
+
button.innerHTML = icons.buttonsvg;
|
|
633
|
+
button.setAttribute('aria-label', WIDGET_CONFIG.lang.accessibilityMenu);
|
|
634
|
+
|
|
635
|
+
button.addEventListener('click', function () {
|
|
636
|
+
toggleMenu();
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
button.addEventListener('keydown', function (e) {
|
|
640
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
641
|
+
e.preventDefault();
|
|
642
|
+
toggleMenu();
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
buttonContainer.appendChild(button);
|
|
647
|
+
document.body.appendChild(buttonContainer);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Reset all accessibility settings
|
|
651
|
+
function resetAccessibilitySettings() {
|
|
652
|
+
const keys = [
|
|
653
|
+
'biggerCursor',
|
|
654
|
+
'biggerText',
|
|
655
|
+
'dyslexiaFont',
|
|
656
|
+
'hideImages',
|
|
657
|
+
'lineHeight',
|
|
658
|
+
'pauseAnimations',
|
|
659
|
+
'screenReader',
|
|
660
|
+
'textAlign',
|
|
661
|
+
'textSpacing',
|
|
662
|
+
'highContrast',
|
|
663
|
+
'voiceControl',
|
|
664
|
+
'reducedMotion',
|
|
665
|
+
'fontSelection',
|
|
666
|
+
'colorFilter',
|
|
667
|
+
];
|
|
668
|
+
keys.forEach((key) => localStorage.removeItem(key));
|
|
669
|
+
|
|
670
|
+
// Remove all CSS classes
|
|
671
|
+
const cssClasses = [
|
|
672
|
+
'snn-bigger-cursor',
|
|
673
|
+
'snn-bigger-text',
|
|
674
|
+
'snn-dyslexia-font',
|
|
675
|
+
'snn-pause-animations',
|
|
676
|
+
'snn-text-spacing',
|
|
677
|
+
'snn-line-height',
|
|
678
|
+
'snn-text-align',
|
|
679
|
+
'snn-reduced-motion',
|
|
680
|
+
'snn-font-arial',
|
|
681
|
+
'snn-font-times',
|
|
682
|
+
'snn-font-verdana'
|
|
683
|
+
];
|
|
684
|
+
cssClasses.forEach(cls => document.body.classList.remove(cls));
|
|
685
|
+
|
|
686
|
+
const documentClasses = [
|
|
687
|
+
'snn-high-contrast',
|
|
688
|
+
'snn-filter-protanopia',
|
|
689
|
+
'snn-filter-deuteranopia',
|
|
690
|
+
'snn-filter-tritanopia',
|
|
691
|
+
'snn-filter-grayscale'
|
|
692
|
+
];
|
|
693
|
+
documentClasses.forEach(cls => document.documentElement.classList.remove(cls));
|
|
694
|
+
|
|
695
|
+
domCache.getImages().forEach((img) => (img.style.display = ''));
|
|
696
|
+
|
|
697
|
+
if (screenReader.active) {
|
|
698
|
+
screenReader.toggle(false);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (voiceControl.isActive) {
|
|
702
|
+
voiceControl.toggle(false);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
applySettings();
|
|
706
|
+
|
|
707
|
+
const buttons = document.querySelectorAll('#snn-accessibility-menu .snn-accessibility-option');
|
|
708
|
+
buttons.forEach((button) => {
|
|
709
|
+
button.classList.remove('active');
|
|
710
|
+
button.setAttribute('aria-pressed', 'false');
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Create toggle buttons for accessibility options
|
|
715
|
+
function createToggleButton(
|
|
716
|
+
buttonText,
|
|
717
|
+
localStorageKey,
|
|
718
|
+
className,
|
|
719
|
+
targetElement = document.body,
|
|
720
|
+
customToggleFunction = null,
|
|
721
|
+
iconSVG = '',
|
|
722
|
+
requiresFeature = null
|
|
723
|
+
) {
|
|
724
|
+
const button = document.createElement('button');
|
|
725
|
+
button.innerHTML = `<span class="snn-icon">${iconSVG}</span><span class="snn-button-text">${buttonText}</span>`;
|
|
726
|
+
button.setAttribute('data-key', localStorageKey);
|
|
727
|
+
button.setAttribute('aria-label', buttonText);
|
|
728
|
+
button.classList.add('snn-accessibility-option');
|
|
729
|
+
|
|
730
|
+
// Check if feature is supported
|
|
731
|
+
if (requiresFeature && !requiresFeature.isSupported) {
|
|
732
|
+
button.disabled = true;
|
|
733
|
+
button.setAttribute('title', `${buttonText} ${WIDGET_CONFIG.lang.notSupportedBrowser}`);
|
|
734
|
+
button.style.opacity = '0.5';
|
|
735
|
+
return button;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const isActive = localStorage.getItem(localStorageKey) === 'true';
|
|
739
|
+
button.setAttribute('aria-pressed', isActive);
|
|
740
|
+
button.setAttribute('role', 'switch');
|
|
741
|
+
if (isActive) {
|
|
742
|
+
button.classList.add('active');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
button.addEventListener('click', function () {
|
|
746
|
+
handleToggle();
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
button.addEventListener('keydown', function (e) {
|
|
750
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
751
|
+
e.preventDefault();
|
|
752
|
+
handleToggle();
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
function handleToggle() {
|
|
757
|
+
const newIsActive = localStorage.getItem(localStorageKey) !== 'true';
|
|
758
|
+
|
|
759
|
+
// If there's a custom toggle function, call it and check if it succeeded
|
|
760
|
+
if (customToggleFunction) {
|
|
761
|
+
const success = customToggleFunction(newIsActive);
|
|
762
|
+
if (success === false) {
|
|
763
|
+
// Feature not supported or failed
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
localStorage.setItem(localStorageKey, newIsActive);
|
|
769
|
+
button.setAttribute('aria-pressed', newIsActive);
|
|
770
|
+
|
|
771
|
+
if (newIsActive) {
|
|
772
|
+
button.classList.add('active');
|
|
773
|
+
if (className) {
|
|
774
|
+
targetElement.classList.add(className);
|
|
775
|
+
}
|
|
776
|
+
} else {
|
|
777
|
+
button.classList.remove('active');
|
|
778
|
+
if (className) {
|
|
779
|
+
targetElement.classList.remove(className);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return button;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Create special action buttons (for cycling through options)
|
|
788
|
+
function createActionButton(buttonText, actionFunction, iconSVG) {
|
|
789
|
+
const button = document.createElement('button');
|
|
790
|
+
button.innerHTML = `<span class="snn-icon">${iconSVG}</span><span class="snn-button-text">${buttonText}: <span class="snn-status">${WIDGET_CONFIG.lang.default}</span></span>`;
|
|
791
|
+
button.setAttribute('aria-label', buttonText);
|
|
792
|
+
button.classList.add('snn-accessibility-option');
|
|
793
|
+
|
|
794
|
+
// Update initial status
|
|
795
|
+
updateActionButtonStatus(button, buttonText, actionFunction);
|
|
796
|
+
|
|
797
|
+
button.addEventListener('click', function () {
|
|
798
|
+
const result = actionFunction();
|
|
799
|
+
if (result) {
|
|
800
|
+
const statusSpan = button.querySelector('.snn-status');
|
|
801
|
+
statusSpan.textContent = result;
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
button.addEventListener('keydown', function (e) {
|
|
806
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
807
|
+
e.preventDefault();
|
|
808
|
+
const result = actionFunction();
|
|
809
|
+
if (result) {
|
|
810
|
+
const statusSpan = button.querySelector('.snn-status');
|
|
811
|
+
statusSpan.textContent = result;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
return button;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Update action button status on page load
|
|
820
|
+
function updateActionButtonStatus(button, buttonText, actionFunction) {
|
|
821
|
+
const statusSpan = button.querySelector('.snn-status');
|
|
822
|
+
|
|
823
|
+
if (buttonText.includes('Font')) {
|
|
824
|
+
const currentFont = localStorage.getItem('fontSelection');
|
|
825
|
+
statusSpan.textContent = currentFont ? currentFont.charAt(0).toUpperCase() + currentFont.slice(1) : WIDGET_CONFIG.lang.default;
|
|
826
|
+
} else if (buttonText.includes('Color')) {
|
|
827
|
+
const currentFilter = localStorage.getItem('colorFilter');
|
|
828
|
+
statusSpan.textContent = currentFilter ? currentFilter.charAt(0).toUpperCase() + currentFilter.slice(1) : WIDGET_CONFIG.lang.noFilter;
|
|
829
|
+
} else if (buttonText.includes('Text Align')) {
|
|
830
|
+
const currentAlign = localStorage.getItem('textAlign');
|
|
831
|
+
statusSpan.textContent = currentAlign ? currentAlign.charAt(0).toUpperCase() + currentAlign.slice(1) : WIDGET_CONFIG.lang.default;
|
|
832
|
+
} else if (buttonText.includes('Text Size')) {
|
|
833
|
+
const currentSize = localStorage.getItem('biggerText');
|
|
834
|
+
statusSpan.textContent = currentSize ? (currentSize === 'xlarge' ? 'X-Large' : currentSize.charAt(0).toUpperCase() + currentSize.slice(1)) : WIDGET_CONFIG.lang.default;
|
|
835
|
+
} else if (buttonText.includes('High Contrast')) {
|
|
836
|
+
const currentContrast = localStorage.getItem('highContrast');
|
|
837
|
+
statusSpan.textContent = currentContrast ? currentContrast.charAt(0).toUpperCase() + currentContrast.slice(1) : WIDGET_CONFIG.lang.default;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ===========================================
|
|
842
|
+
// FEATURE TOGGLE FUNCTIONS
|
|
843
|
+
// ===========================================
|
|
844
|
+
|
|
845
|
+
// Function to hide or show images (optimized)
|
|
846
|
+
function toggleHideImages(isActive) {
|
|
847
|
+
const displayStyle = isActive ? 'none' : '';
|
|
848
|
+
domCache.getImages().forEach((img) => {
|
|
849
|
+
img.style.display = displayStyle;
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Font selection handler (optimized)
|
|
854
|
+
function handleFontSelection() {
|
|
855
|
+
const fonts = ['arial', 'times', 'verdana'];
|
|
856
|
+
const currentFont = localStorage.getItem('fontSelection') || 'default';
|
|
857
|
+
const currentIndex = fonts.indexOf(currentFont);
|
|
858
|
+
const nextIndex = (currentIndex + 1) % (fonts.length + 1); // +1 for default
|
|
859
|
+
|
|
860
|
+
// Remove all font classes in one operation
|
|
861
|
+
const fontClasses = ['snn-font-arial', 'snn-font-times', 'snn-font-verdana'];
|
|
862
|
+
domCache.body.classList.remove(...fontClasses);
|
|
863
|
+
|
|
864
|
+
if (nextIndex === fonts.length) {
|
|
865
|
+
// Default font
|
|
866
|
+
localStorage.removeItem('fontSelection');
|
|
867
|
+
return WIDGET_CONFIG.lang.defaultFont;
|
|
868
|
+
} else {
|
|
869
|
+
const selectedFont = fonts[nextIndex];
|
|
870
|
+
localStorage.setItem('fontSelection', selectedFont);
|
|
871
|
+
domCache.body.classList.add(`snn-font-${selectedFont}`);
|
|
872
|
+
return selectedFont.charAt(0).toUpperCase() + selectedFont.slice(1);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Color filter handler (optimized)
|
|
877
|
+
function handleColorFilter() {
|
|
878
|
+
const filters = ['protanopia', 'deuteranopia', 'tritanopia', 'grayscale'];
|
|
879
|
+
const currentFilter = localStorage.getItem('colorFilter') || 'none';
|
|
880
|
+
const currentIndex = filters.indexOf(currentFilter);
|
|
881
|
+
const nextIndex = (currentIndex + 1) % (filters.length + 1); // +1 for none
|
|
882
|
+
|
|
883
|
+
// Remove all filter classes in one operation
|
|
884
|
+
const filterClasses = ['snn-filter-protanopia', 'snn-filter-deuteranopia', 'snn-filter-tritanopia', 'snn-filter-grayscale'];
|
|
885
|
+
domCache.documentElement.classList.remove(...filterClasses);
|
|
886
|
+
|
|
887
|
+
if (nextIndex === filters.length) {
|
|
888
|
+
// No filter
|
|
889
|
+
localStorage.removeItem('colorFilter');
|
|
890
|
+
return WIDGET_CONFIG.lang.noFilter;
|
|
891
|
+
} else {
|
|
892
|
+
const selectedFilter = filters[nextIndex];
|
|
893
|
+
localStorage.setItem('colorFilter', selectedFilter);
|
|
894
|
+
domCache.documentElement.classList.add(`snn-filter-${selectedFilter}`);
|
|
895
|
+
return selectedFilter.charAt(0).toUpperCase() + selectedFilter.slice(1);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Text align handler with 3 states
|
|
900
|
+
function handleTextAlign() {
|
|
901
|
+
const alignments = ['left', 'center', 'right'];
|
|
902
|
+
const currentAlign = localStorage.getItem('textAlign') || 'none';
|
|
903
|
+
const currentIndex = alignments.indexOf(currentAlign);
|
|
904
|
+
const nextIndex = (currentIndex + 1) % (alignments.length + 1); // +1 for none
|
|
905
|
+
|
|
906
|
+
// Remove all alignment classes
|
|
907
|
+
const alignClasses = ['snn-text-align-left', 'snn-text-align-center', 'snn-text-align-right'];
|
|
908
|
+
domCache.body.classList.remove(...alignClasses);
|
|
909
|
+
|
|
910
|
+
if (nextIndex === alignments.length) {
|
|
911
|
+
// Default alignment
|
|
912
|
+
localStorage.removeItem('textAlign');
|
|
913
|
+
return WIDGET_CONFIG.lang.default;
|
|
914
|
+
} else {
|
|
915
|
+
const selectedAlign = alignments[nextIndex];
|
|
916
|
+
localStorage.setItem('textAlign', selectedAlign);
|
|
917
|
+
domCache.body.classList.add(`snn-text-align-${selectedAlign}`);
|
|
918
|
+
return selectedAlign.charAt(0).toUpperCase() + selectedAlign.slice(1);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Bigger text handler with 3 states
|
|
923
|
+
function handleBiggerText() {
|
|
924
|
+
const textSizes = ['medium', 'large', 'xlarge'];
|
|
925
|
+
const currentSize = localStorage.getItem('biggerText') || 'none';
|
|
926
|
+
const currentIndex = textSizes.indexOf(currentSize);
|
|
927
|
+
const nextIndex = (currentIndex + 1) % (textSizes.length + 1); // +1 for none
|
|
928
|
+
|
|
929
|
+
// Remove all text size classes
|
|
930
|
+
const textClasses = ['snn-bigger-text-medium', 'snn-bigger-text-large', 'snn-bigger-text-xlarge'];
|
|
931
|
+
domCache.body.classList.remove(...textClasses);
|
|
932
|
+
|
|
933
|
+
if (nextIndex === textSizes.length) {
|
|
934
|
+
// Default text size
|
|
935
|
+
localStorage.removeItem('biggerText');
|
|
936
|
+
return WIDGET_CONFIG.lang.default;
|
|
937
|
+
} else {
|
|
938
|
+
const selectedSize = textSizes[nextIndex];
|
|
939
|
+
localStorage.setItem('biggerText', selectedSize);
|
|
940
|
+
domCache.body.classList.add(`snn-bigger-text-${selectedSize}`);
|
|
941
|
+
return selectedSize === 'xlarge' ? 'X-Large' : selectedSize.charAt(0).toUpperCase() + selectedSize.slice(1);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// High contrast handler with 3 states
|
|
946
|
+
function handleHighContrast() {
|
|
947
|
+
const contrastLevels = ['medium', 'high', 'ultra'];
|
|
948
|
+
const currentContrast = localStorage.getItem('highContrast') || 'none';
|
|
949
|
+
const currentIndex = contrastLevels.indexOf(currentContrast);
|
|
950
|
+
const nextIndex = (currentIndex + 1) % (contrastLevels.length + 1); // +1 for none
|
|
951
|
+
|
|
952
|
+
// Remove all contrast classes
|
|
953
|
+
const contrastClasses = ['snn-high-contrast-medium', 'snn-high-contrast-high', 'snn-high-contrast-ultra'];
|
|
954
|
+
domCache.documentElement.classList.remove(...contrastClasses);
|
|
955
|
+
|
|
956
|
+
if (nextIndex === contrastLevels.length) {
|
|
957
|
+
// Default contrast
|
|
958
|
+
localStorage.removeItem('highContrast');
|
|
959
|
+
return WIDGET_CONFIG.lang.default;
|
|
960
|
+
} else {
|
|
961
|
+
const selectedContrast = contrastLevels[nextIndex];
|
|
962
|
+
localStorage.setItem('highContrast', selectedContrast);
|
|
963
|
+
domCache.documentElement.classList.add(`snn-high-contrast-${selectedContrast}`);
|
|
964
|
+
return selectedContrast.charAt(0).toUpperCase() + selectedContrast.slice(1);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// ===========================================
|
|
969
|
+
// ACCESSIBILITY FEATURES
|
|
970
|
+
// ===========================================
|
|
971
|
+
|
|
972
|
+
// Screen reader functionality
|
|
973
|
+
const screenReader = {
|
|
974
|
+
active: localStorage.getItem('screenReader') === 'true',
|
|
975
|
+
isSupported: 'speechSynthesis' in window,
|
|
976
|
+
handleFocus: function (event) {
|
|
977
|
+
if (screenReader.active && screenReader.isSupported) {
|
|
978
|
+
try {
|
|
979
|
+
const content = event.target.innerText || event.target.alt || event.target.title || '';
|
|
980
|
+
if (content.trim() !== '') {
|
|
981
|
+
window.speechSynthesis.cancel();
|
|
982
|
+
const speech = new SpeechSynthesisUtterance(content);
|
|
983
|
+
speech.lang = 'en-US';
|
|
984
|
+
speech.onerror = function(event) {
|
|
985
|
+
console.warn('Speech synthesis error:', event.error);
|
|
986
|
+
};
|
|
987
|
+
window.speechSynthesis.speak(speech);
|
|
988
|
+
}
|
|
989
|
+
} catch (error) {
|
|
990
|
+
console.warn('Screen reader error:', error);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
},
|
|
994
|
+
toggle: function (isActive) {
|
|
995
|
+
if (!screenReader.isSupported) {
|
|
996
|
+
console.warn(`Speech synthesis ${WIDGET_CONFIG.lang.notSupportedBrowser}`);
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
screenReader.active = isActive;
|
|
1001
|
+
localStorage.setItem('screenReader', isActive);
|
|
1002
|
+
|
|
1003
|
+
try {
|
|
1004
|
+
if (isActive) {
|
|
1005
|
+
document.addEventListener('focusin', screenReader.handleFocus);
|
|
1006
|
+
const feedbackSpeech = new SpeechSynthesisUtterance(WIDGET_CONFIG.lang.screenReaderOn);
|
|
1007
|
+
feedbackSpeech.lang = 'en-US';
|
|
1008
|
+
feedbackSpeech.onerror = function(event) {
|
|
1009
|
+
console.warn('Speech synthesis feedback error:', event.error);
|
|
1010
|
+
};
|
|
1011
|
+
window.speechSynthesis.speak(feedbackSpeech);
|
|
1012
|
+
} else {
|
|
1013
|
+
document.removeEventListener('focusin', screenReader.handleFocus);
|
|
1014
|
+
window.speechSynthesis.cancel();
|
|
1015
|
+
const feedbackSpeech = new SpeechSynthesisUtterance(WIDGET_CONFIG.lang.screenReaderOff);
|
|
1016
|
+
feedbackSpeech.lang = 'en-US';
|
|
1017
|
+
feedbackSpeech.onerror = function(event) {
|
|
1018
|
+
console.warn('Speech synthesis feedback error:', event.error);
|
|
1019
|
+
};
|
|
1020
|
+
window.speechSynthesis.speak(feedbackSpeech);
|
|
1021
|
+
}
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
console.warn('Screen reader toggle error:', error);
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return true;
|
|
1028
|
+
},
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
// Voice control functionality
|
|
1032
|
+
const voiceControl = {
|
|
1033
|
+
isActive: localStorage.getItem('voiceControl') === 'true',
|
|
1034
|
+
recognition: null,
|
|
1035
|
+
isSupported: 'SpeechRecognition' in window || 'webkitSpeechRecognition' in window,
|
|
1036
|
+
retryCount: 0,
|
|
1037
|
+
maxRetries: 3,
|
|
1038
|
+
toggle: function (isActive) {
|
|
1039
|
+
if (!voiceControl.isSupported) {
|
|
1040
|
+
console.warn(`Speech Recognition API ${WIDGET_CONFIG.lang.notSupportedBrowser}`);
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
voiceControl.isActive = isActive;
|
|
1045
|
+
localStorage.setItem('voiceControl', isActive);
|
|
1046
|
+
|
|
1047
|
+
try {
|
|
1048
|
+
if (isActive) {
|
|
1049
|
+
voiceControl.startListening();
|
|
1050
|
+
} else {
|
|
1051
|
+
if (voiceControl.recognition) {
|
|
1052
|
+
voiceControl.recognition.stop();
|
|
1053
|
+
voiceControl.recognition = null;
|
|
1054
|
+
}
|
|
1055
|
+
voiceControl.retryCount = 0;
|
|
1056
|
+
}
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
console.warn('Voice control toggle error:', error);
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
return true;
|
|
1063
|
+
},
|
|
1064
|
+
startListening: function () {
|
|
1065
|
+
if (!voiceControl.isSupported) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
try {
|
|
1070
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
1071
|
+
voiceControl.recognition = new SpeechRecognition();
|
|
1072
|
+
voiceControl.recognition.interimResults = false;
|
|
1073
|
+
voiceControl.recognition.lang = 'en-US';
|
|
1074
|
+
voiceControl.recognition.continuous = false;
|
|
1075
|
+
|
|
1076
|
+
voiceControl.recognition.onstart = function () {
|
|
1077
|
+
console.log(WIDGET_CONFIG.lang.voiceControlActivated);
|
|
1078
|
+
voiceControl.retryCount = 0;
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
voiceControl.recognition.onresult = function (event) {
|
|
1082
|
+
try {
|
|
1083
|
+
const command = event.results[0][0].transcript.toLowerCase();
|
|
1084
|
+
voiceControl.handleVoiceCommand(command);
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
console.warn('Voice command processing error:', error);
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
voiceControl.recognition.onerror = function (event) {
|
|
1091
|
+
console.warn('Speech recognition error:', event.error);
|
|
1092
|
+
if (event.error === 'no-speech' && voiceControl.retryCount < voiceControl.maxRetries) {
|
|
1093
|
+
voiceControl.retryCount++;
|
|
1094
|
+
setTimeout(() => {
|
|
1095
|
+
if (voiceControl.isActive) {
|
|
1096
|
+
voiceControl.startListening();
|
|
1097
|
+
}
|
|
1098
|
+
}, 1000);
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
voiceControl.recognition.onend = function () {
|
|
1103
|
+
if (voiceControl.isActive && voiceControl.retryCount < voiceControl.maxRetries) {
|
|
1104
|
+
setTimeout(() => {
|
|
1105
|
+
if (voiceControl.isActive) {
|
|
1106
|
+
voiceControl.startListening();
|
|
1107
|
+
}
|
|
1108
|
+
}, 100);
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
voiceControl.recognition.start();
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
console.warn('Voice control initialization error:', error);
|
|
1115
|
+
}
|
|
1116
|
+
},
|
|
1117
|
+
handleVoiceCommand: function (command) {
|
|
1118
|
+
console.log(`Received command: ${command}`);
|
|
1119
|
+
|
|
1120
|
+
try {
|
|
1121
|
+
const commandMap = {
|
|
1122
|
+
'show menu': 'snn-accessibility-button',
|
|
1123
|
+
'open menu': 'snn-accessibility-button',
|
|
1124
|
+
'accessibility menu': 'snn-accessibility-button',
|
|
1125
|
+
'high contrast': 'highContrast',
|
|
1126
|
+
'bigger text': 'biggerText',
|
|
1127
|
+
'large text': 'biggerText',
|
|
1128
|
+
'text spacing': 'textSpacing',
|
|
1129
|
+
'pause animations': 'pauseAnimations',
|
|
1130
|
+
'stop animations': 'pauseAnimations',
|
|
1131
|
+
'hide images': 'hideImages',
|
|
1132
|
+
'dyslexia friendly': 'dyslexiaFont',
|
|
1133
|
+
'dyslexia font': 'dyslexiaFont',
|
|
1134
|
+
'bigger cursor': 'biggerCursor',
|
|
1135
|
+
'large cursor': 'biggerCursor',
|
|
1136
|
+
'line height': 'lineHeight',
|
|
1137
|
+
'align text': 'textAlign',
|
|
1138
|
+
'text align': 'textAlign',
|
|
1139
|
+
'screen reader': 'screenReader',
|
|
1140
|
+
'voice command': 'voiceControl',
|
|
1141
|
+
'voice control': 'voiceControl',
|
|
1142
|
+
'reset all': 'resetAll',
|
|
1143
|
+
'reset everything': 'resetAll',
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
if (command === 'show menu' || command === 'open menu' || command === 'accessibility menu') {
|
|
1147
|
+
if (!menuCache.button) menuCache.init();
|
|
1148
|
+
if (menuCache.button) {
|
|
1149
|
+
menuCache.button.click();
|
|
1150
|
+
}
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (command === 'reset all' || command === 'reset everything') {
|
|
1155
|
+
resetAccessibilitySettings();
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const localStorageKey = commandMap[command];
|
|
1160
|
+
if (localStorageKey) {
|
|
1161
|
+
// Use cached menu reference if available
|
|
1162
|
+
if (!menuCache.menu) menuCache.init();
|
|
1163
|
+
const button = menuCache.menu?.querySelector(
|
|
1164
|
+
`.snn-accessibility-option[data-key='${localStorageKey}']`
|
|
1165
|
+
);
|
|
1166
|
+
if (button) {
|
|
1167
|
+
button.click();
|
|
1168
|
+
} else {
|
|
1169
|
+
console.log('Button not found for command:', command);
|
|
1170
|
+
}
|
|
1171
|
+
} else {
|
|
1172
|
+
console.log('Command not recognized:', command);
|
|
1173
|
+
}
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
console.warn('Voice command handling error:', error);
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
// Create the accessibility menu
|
|
1181
|
+
function createAccessibilityMenu() {
|
|
1182
|
+
const menu = document.createElement('div');
|
|
1183
|
+
menu.id = 'snn-accessibility-menu';
|
|
1184
|
+
menu.style.display = 'none';
|
|
1185
|
+
menu.setAttribute('role', 'dialog');
|
|
1186
|
+
menu.setAttribute('aria-labelledby', 'snn-accessibility-title');
|
|
1187
|
+
menu.setAttribute('aria-hidden', 'true');
|
|
1188
|
+
|
|
1189
|
+
const header = document.createElement('div');
|
|
1190
|
+
header.classList.add('snn-header');
|
|
1191
|
+
|
|
1192
|
+
const title = document.createElement('h2');
|
|
1193
|
+
title.classList.add('snn-title');
|
|
1194
|
+
title.id = 'snn-accessibility-title';
|
|
1195
|
+
title.textContent = WIDGET_CONFIG.lang.accessibilityTools;
|
|
1196
|
+
|
|
1197
|
+
const closeButton = document.createElement('button');
|
|
1198
|
+
closeButton.className = 'snn-close';
|
|
1199
|
+
closeButton.innerHTML = '';
|
|
1200
|
+
closeButton.setAttribute('title', WIDGET_CONFIG.lang.closeAccessibilityMenu);
|
|
1201
|
+
|
|
1202
|
+
closeButton.addEventListener('click', function () {
|
|
1203
|
+
closeMenu();
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
closeButton.addEventListener('keydown', function (e) {
|
|
1207
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
1208
|
+
e.preventDefault();
|
|
1209
|
+
closeMenu();
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
header.appendChild(title);
|
|
1214
|
+
header.appendChild(closeButton);
|
|
1215
|
+
menu.appendChild(header);
|
|
1216
|
+
|
|
1217
|
+
// Create content wrapper
|
|
1218
|
+
const content = document.createElement('div');
|
|
1219
|
+
content.classList.add('snn-content');
|
|
1220
|
+
|
|
1221
|
+
// Create reset button (outside grid, full width)
|
|
1222
|
+
const resetButton = document.createElement('button');
|
|
1223
|
+
resetButton.innerHTML = `<span class="snn-icon">${icons.resetAll}</span><span class="snn-button-text">${WIDGET_CONFIG.lang.resetAllSettings}</span>`;
|
|
1224
|
+
resetButton.setAttribute('aria-label', WIDGET_CONFIG.lang.resetAllSettings);
|
|
1225
|
+
resetButton.classList.add('snn-reset-button');
|
|
1226
|
+
resetButton.addEventListener('click', resetAccessibilitySettings);
|
|
1227
|
+
content.appendChild(resetButton);
|
|
1228
|
+
|
|
1229
|
+
// Create grid wrapper for accessibility options
|
|
1230
|
+
const optionsGrid = document.createElement('div');
|
|
1231
|
+
optionsGrid.classList.add('snn-options-grid');
|
|
1232
|
+
|
|
1233
|
+
// Add accessibility options based on configuration
|
|
1234
|
+
const options = [
|
|
1235
|
+
{
|
|
1236
|
+
text: WIDGET_CONFIG.lang.screenReader,
|
|
1237
|
+
key: 'screenReader',
|
|
1238
|
+
customToggleFunction: screenReader.toggle,
|
|
1239
|
+
icon: icons.screenReader,
|
|
1240
|
+
requiresFeature: screenReader,
|
|
1241
|
+
enabled: WIDGET_CONFIG.enableScreenReader,
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
text: WIDGET_CONFIG.lang.voiceCommand,
|
|
1245
|
+
key: 'voiceControl',
|
|
1246
|
+
customToggleFunction: voiceControl.toggle,
|
|
1247
|
+
icon: icons.voiceControl,
|
|
1248
|
+
requiresFeature: voiceControl,
|
|
1249
|
+
enabled: WIDGET_CONFIG.enableVoiceControl,
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
text: WIDGET_CONFIG.lang.textSpacing,
|
|
1253
|
+
key: 'textSpacing',
|
|
1254
|
+
className: 'snn-text-spacing',
|
|
1255
|
+
icon: icons.textSpacing,
|
|
1256
|
+
enabled: WIDGET_CONFIG.enableTextSpacing,
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
text: WIDGET_CONFIG.lang.pauseAnimations,
|
|
1260
|
+
key: 'pauseAnimations',
|
|
1261
|
+
className: 'snn-pause-animations',
|
|
1262
|
+
icon: icons.pauseAnimations,
|
|
1263
|
+
enabled: WIDGET_CONFIG.enablePauseAnimations,
|
|
1264
|
+
},
|
|
1265
|
+
{
|
|
1266
|
+
text: WIDGET_CONFIG.lang.hideImages,
|
|
1267
|
+
key: 'hideImages',
|
|
1268
|
+
icon: icons.hideImages,
|
|
1269
|
+
customToggleFunction: toggleHideImages,
|
|
1270
|
+
enabled: WIDGET_CONFIG.enableHideImages,
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
text: WIDGET_CONFIG.lang.dyslexiaFriendly,
|
|
1274
|
+
key: 'dyslexiaFont',
|
|
1275
|
+
className: 'snn-dyslexia-font',
|
|
1276
|
+
icon: icons.dyslexiaFont,
|
|
1277
|
+
enabled: WIDGET_CONFIG.enableDyslexiaFont,
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
text: WIDGET_CONFIG.lang.biggerCursor,
|
|
1281
|
+
key: 'biggerCursor',
|
|
1282
|
+
className: 'snn-bigger-cursor',
|
|
1283
|
+
icon: icons.biggerCursor,
|
|
1284
|
+
enabled: WIDGET_CONFIG.enableBiggerCursor,
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
text: WIDGET_CONFIG.lang.lineHeight,
|
|
1288
|
+
key: 'lineHeight',
|
|
1289
|
+
className: 'snn-line-height',
|
|
1290
|
+
icon: icons.lineHeight,
|
|
1291
|
+
enabled: WIDGET_CONFIG.enableLineHeight,
|
|
1292
|
+
},
|
|
1293
|
+
{
|
|
1294
|
+
text: WIDGET_CONFIG.lang.reducedMotion,
|
|
1295
|
+
key: 'reducedMotion',
|
|
1296
|
+
className: 'snn-reduced-motion',
|
|
1297
|
+
icon: icons.reducedMotion,
|
|
1298
|
+
enabled: WIDGET_CONFIG.enableReducedMotion,
|
|
1299
|
+
},
|
|
1300
|
+
];
|
|
1301
|
+
|
|
1302
|
+
// Add enabled toggle options to grid
|
|
1303
|
+
options.forEach((option) => {
|
|
1304
|
+
if (option.enabled) {
|
|
1305
|
+
const button = createToggleButton(
|
|
1306
|
+
option.text,
|
|
1307
|
+
option.key,
|
|
1308
|
+
option.className,
|
|
1309
|
+
option.target,
|
|
1310
|
+
option.customToggleFunction,
|
|
1311
|
+
option.icon,
|
|
1312
|
+
option.requiresFeature
|
|
1313
|
+
);
|
|
1314
|
+
optionsGrid.appendChild(button);
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
// Add action buttons (font selection and color filters) to grid if enabled
|
|
1319
|
+
if (WIDGET_CONFIG.enableFontSelection) {
|
|
1320
|
+
const fontButton = createActionButton(WIDGET_CONFIG.lang.fontSelection, handleFontSelection, icons.fontSelection);
|
|
1321
|
+
optionsGrid.appendChild(fontButton);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
if (WIDGET_CONFIG.enableColorFilter) {
|
|
1325
|
+
const colorButton = createActionButton(WIDGET_CONFIG.lang.colorFilter, handleColorFilter, icons.colorFilter);
|
|
1326
|
+
optionsGrid.appendChild(colorButton);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (WIDGET_CONFIG.enableTextAlign) {
|
|
1330
|
+
const textAlignButton = createActionButton(WIDGET_CONFIG.lang.textAlign, handleTextAlign, icons.textAlign);
|
|
1331
|
+
optionsGrid.appendChild(textAlignButton);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (WIDGET_CONFIG.enableBiggerText) {
|
|
1335
|
+
const biggerTextButton = createActionButton(WIDGET_CONFIG.lang.textSize, handleBiggerText, icons.biggerText);
|
|
1336
|
+
optionsGrid.appendChild(biggerTextButton);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (WIDGET_CONFIG.enableHighContrast) {
|
|
1340
|
+
const highContrastButton = createActionButton(WIDGET_CONFIG.lang.highContrast, handleHighContrast, icons.highContrast);
|
|
1341
|
+
optionsGrid.appendChild(highContrastButton);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// Add grid to content
|
|
1345
|
+
content.appendChild(optionsGrid);
|
|
1346
|
+
|
|
1347
|
+
// Add content to menu
|
|
1348
|
+
menu.appendChild(content);
|
|
1349
|
+
|
|
1350
|
+
document.body.appendChild(menu);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// ===========================================
|
|
1354
|
+
// MENU MANAGEMENT
|
|
1355
|
+
// ===========================================
|
|
1356
|
+
|
|
1357
|
+
// Cache for menu elements
|
|
1358
|
+
const menuCache = {
|
|
1359
|
+
menu: null,
|
|
1360
|
+
button: null,
|
|
1361
|
+
closeButton: null,
|
|
1362
|
+
init: function() {
|
|
1363
|
+
this.menu = document.getElementById('snn-accessibility-menu');
|
|
1364
|
+
this.button = document.getElementById('snn-accessibility-button');
|
|
1365
|
+
this.closeButton = this.menu?.querySelector('.snn-close');
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
// Menu control functions (optimized)
|
|
1370
|
+
function toggleMenu() {
|
|
1371
|
+
if (!menuCache.menu) menuCache.init();
|
|
1372
|
+
const isOpen = menuCache.menu.style.display === 'block';
|
|
1373
|
+
|
|
1374
|
+
if (isOpen) {
|
|
1375
|
+
closeMenu();
|
|
1376
|
+
} else {
|
|
1377
|
+
openMenu();
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function openMenu() {
|
|
1382
|
+
if (!menuCache.menu) menuCache.init();
|
|
1383
|
+
menuCache.menu.style.display = 'block';
|
|
1384
|
+
menuCache.menu.setAttribute('aria-hidden', 'false');
|
|
1385
|
+
|
|
1386
|
+
if (menuCache.closeButton) {
|
|
1387
|
+
menuCache.closeButton.focus();
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// Add keyboard navigation
|
|
1391
|
+
document.addEventListener('keydown', handleMenuKeyboard);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function closeMenu() {
|
|
1395
|
+
if (!menuCache.menu) menuCache.init();
|
|
1396
|
+
menuCache.menu.style.display = 'none';
|
|
1397
|
+
menuCache.menu.setAttribute('aria-hidden', 'true');
|
|
1398
|
+
|
|
1399
|
+
if (menuCache.button) {
|
|
1400
|
+
menuCache.button.focus();
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Remove keyboard navigation
|
|
1404
|
+
document.removeEventListener('keydown', handleMenuKeyboard);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Cache for keyboard navigation elements
|
|
1408
|
+
let keyboardCache = {
|
|
1409
|
+
focusableElements: null,
|
|
1410
|
+
lastUpdate: 0,
|
|
1411
|
+
getFocusableElements: function() {
|
|
1412
|
+
const now = Date.now();
|
|
1413
|
+
if (!this.focusableElements || now - this.lastUpdate > 1000) {
|
|
1414
|
+
if (menuCache.menu) {
|
|
1415
|
+
this.focusableElements = {
|
|
1416
|
+
all: menuCache.menu.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'),
|
|
1417
|
+
options: Array.from(menuCache.menu.querySelectorAll('.snn-accessibility-option, .snn-close'))
|
|
1418
|
+
};
|
|
1419
|
+
this.lastUpdate = now;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return this.focusableElements;
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
function handleMenuKeyboard(e) {
|
|
1427
|
+
if (!menuCache.menu || menuCache.menu.style.display !== 'block') return;
|
|
1428
|
+
|
|
1429
|
+
if (e.key === 'Escape') {
|
|
1430
|
+
e.preventDefault();
|
|
1431
|
+
closeMenu();
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const elements = keyboardCache.getFocusableElements();
|
|
1436
|
+
if (!elements) return;
|
|
1437
|
+
|
|
1438
|
+
if (e.key === 'Tab') {
|
|
1439
|
+
const firstElement = elements.all[0];
|
|
1440
|
+
const lastElement = elements.all[elements.all.length - 1];
|
|
1441
|
+
|
|
1442
|
+
if (e.shiftKey) {
|
|
1443
|
+
if (document.activeElement === firstElement) {
|
|
1444
|
+
e.preventDefault();
|
|
1445
|
+
lastElement.focus();
|
|
1446
|
+
}
|
|
1447
|
+
} else {
|
|
1448
|
+
if (document.activeElement === lastElement) {
|
|
1449
|
+
e.preventDefault();
|
|
1450
|
+
firstElement.focus();
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
1456
|
+
e.preventDefault();
|
|
1457
|
+
const currentIndex = elements.options.indexOf(document.activeElement);
|
|
1458
|
+
let nextIndex;
|
|
1459
|
+
|
|
1460
|
+
if (e.key === 'ArrowDown') {
|
|
1461
|
+
nextIndex = currentIndex === elements.options.length - 1 ? 0 : currentIndex + 1;
|
|
1462
|
+
} else {
|
|
1463
|
+
nextIndex = currentIndex === 0 ? elements.options.length - 1 : currentIndex - 1;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
elements.options[nextIndex].focus();
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// ===========================================
|
|
1471
|
+
// INITIALIZATION
|
|
1472
|
+
// ===========================================
|
|
1473
|
+
|
|
1474
|
+
// Initialize the widget
|
|
1475
|
+
function initAccessibilityWidget() {
|
|
1476
|
+
injectStyles();
|
|
1477
|
+
applySettings();
|
|
1478
|
+
createAccessibilityButton();
|
|
1479
|
+
createAccessibilityMenu();
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// ===========================================
|
|
1483
|
+
// WIDGET BOOTSTRAP
|
|
1484
|
+
// ===========================================
|
|
1485
|
+
|
|
1486
|
+
// Load the widget when the DOM is fully loaded
|
|
1487
|
+
if (document.readyState === 'loading') {
|
|
1488
|
+
document.addEventListener('DOMContentLoaded', initAccessibilityWidget);
|
|
1489
|
+
} else {
|
|
1490
|
+
initAccessibilityWidget();
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/*
|
|
1494
|
+
===========================================
|
|
1495
|
+
WIDGET FEATURES SUMMARY:
|
|
1496
|
+
|
|
1497
|
+
Core Features:
|
|
1498
|
+
- High contrast mode
|
|
1499
|
+
- Text size adjustment
|
|
1500
|
+
- Text spacing modification
|
|
1501
|
+
- Animation pausing
|
|
1502
|
+
- Image hiding
|
|
1503
|
+
- Dyslexia-friendly font
|
|
1504
|
+
- Cursor size adjustment
|
|
1505
|
+
- Line height adjustment
|
|
1506
|
+
- Text alignment
|
|
1507
|
+
|
|
1508
|
+
Advanced Features:
|
|
1509
|
+
- Screen reader with speech synthesis
|
|
1510
|
+
- Voice control with speech recognition
|
|
1511
|
+
- Reading mode
|
|
1512
|
+
- Enhanced focus indicators
|
|
1513
|
+
- Reduced motion mode
|
|
1514
|
+
- Font selection (Arial, Times, Verdana)
|
|
1515
|
+
- Color blindness filters (Protanopia, Deuteranopia, Tritanopia, Grayscale)
|
|
1516
|
+
|
|
1517
|
+
Technical Features:
|
|
1518
|
+
- Persistent settings via localStorage
|
|
1519
|
+
- Full keyboard navigation
|
|
1520
|
+
- ARIA compliance
|
|
1521
|
+
- Error handling for browser compatibility
|
|
1522
|
+
- Performance optimization with DOM caching
|
|
1523
|
+
- Single file deployment
|
|
1524
|
+
|
|
1525
|
+
===========================================
|
|
1526
|
+
*/
|