ally-widget 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/README.md +527 -0
- package/dist/ally-widget.cjs.js +4468 -0
- package/dist/ally-widget.esm.js +4466 -0
- package/dist/ally-widget.js +4471 -0
- package/dist/ally-widget.min.js +93 -0
- package/dist/ally-widget.min.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,4468 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Ally Widget v1.0.0
|
|
3
|
+
* https://github.com/SunilCz/ally
|
|
4
|
+
*
|
|
5
|
+
* Released under the MIT License
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const WIDGET_THEME = {
|
|
10
|
+
primaryColor: '#1976d2',
|
|
11
|
+
primaryColorLight: '#42a5f5',
|
|
12
|
+
primaryColorDark: '#0d47a1',
|
|
13
|
+
backgroundColor: '#f5f7fa',
|
|
14
|
+
textColor: '#222222',
|
|
15
|
+
textColorInverted: '#ffffff',
|
|
16
|
+
buttonSize: '52px',
|
|
17
|
+
cardBackground: '#ffffff',
|
|
18
|
+
borderColor: '#d1d5db',
|
|
19
|
+
focusRingColor: '#1976d2',
|
|
20
|
+
hoverColor: '#42a5f5',
|
|
21
|
+
activeColor: '#0d47a1',
|
|
22
|
+
successColor: '#2e7d32',
|
|
23
|
+
errorColor: '#c62828',
|
|
24
|
+
warningColor: '#f57c00',
|
|
25
|
+
headerHeight: '54px',
|
|
26
|
+
borderRadius: '8px',
|
|
27
|
+
buttonBorderRadius: '0.4rem',
|
|
28
|
+
menuPosition: 'right',
|
|
29
|
+
zIndex: 100000,
|
|
30
|
+
focusBorderWidth: '3px',
|
|
31
|
+
focusOutlineOffset: '2px'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const WIDGET_ICONS = {
|
|
35
|
+
accessibility: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M423.5-743.5Q400-767 400-800t23.5-56.5Q447-880 480-880t56.5 23.5Q560-833 560-800t-23.5 56.5Q513-720 480-720t-56.5-23.5ZM360-80v-520q-60-5-122-15t-118-25l20-80q78 21 166 30.5t174 9.5q86 0 174-9.5T820-720l20 80q-56 15-118 25t-122 15v520h-80v-240h-80v240h-80Z"/></svg>',
|
|
36
|
+
accessibilityVariants: {
|
|
37
|
+
default: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M423.5-743.5Q400-767 400-800t23.5-56.5Q447-880 480-880t56.5 23.5Q560-833 560-800t-23.5 56.5Q513-720 480-720t-56.5-23.5ZM360-80v-520q-60-5-122-15t-118-25l20-80q78 21 166 30.5t174 9.5q86 0 174-9.5T820-720l20 80q-56 15-118 25t-122 15v520h-80v-240h-80v240h-80Z"/></svg>',
|
|
38
|
+
'icon-2': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M423.5-743.5Q400-767 400-800t23.5-56.5Q447-880 480-880t56.5 23.5Q560-833 560-800t-23.5 56.5Q513-720 480-720t-56.5-23.5ZM360-80v-520H120v-80h720v80H600v520h-80v-240h-80v240h-80Z"/></svg>',
|
|
39
|
+
'icon-3': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M423.5-743.5Q400-767 400-800t23.5-56.5Q447-880 480-880t56.5 23.5Q560-833 560-800t-23.5 56.5Q513-720 480-720t-56.5-23.5ZM680-80v-200H480q-33 0-56.5-23.5T400-360v-240q0-33 23.5-56.5T480-680q24 0 41.5 10.5T559-636q55 66 99.5 90.5T760-520v80q-53 0-107-23t-93-55v138h120q33 0 56.5 23.5T760-300v220h-80Zm-280 0q-83 0-141.5-58.5T200-280q0-72 45.5-127T360-476v82q-35 14-57.5 44.5T280-280q0 50 35 85t85 35q39 0 69.5-22.5T514-240h82q-14 69-69 114.5T400-80Z"/></svg>',
|
|
40
|
+
'icon-4': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M320-80q-83 0-141.5-58.5T120-280q0-83 58.5-141.5T320-480v80q-50 0-85 35t-35 85q0 50 35 85t85 35q50 0 85-35t35-85h80q0 83-58.5 141.5T320-80Zm360-40v-200H440q-44 0-68-37.5t-6-78.5l74-164h-91l-24 62-77-22 28-72q9-23 29.5-35.5T350-680h208q45 0 68.5 36.5T632-566l-66 146h114q33 0 56.5 23.5T760-340v220h-80Zm-96.5-603.5Q560-747 560-780t23.5-56.5Q607-860 640-860t56.5 23.5Q720-813 720-780t-23.5 56.5Q673-700 640-700t-56.5-23.5Z"/></svg>'
|
|
41
|
+
},
|
|
42
|
+
largePointer: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="m320-410 79-110h170L320-716v306ZM551-80 406-392 240-160v-720l560 440H516l144 309-109 51ZM399-520Z"/></svg>',
|
|
43
|
+
pauseMotion: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="m791-55-91-91q-49 32-104.5 49T480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 17-115.5T146-700l-91-91 57-57 736 736-57 57ZM480-160q43 0 83.5-11t78.5-33L204-642q-22 38-33 78.5T160-480q0 133 93.5 226.5T480-160Zm334-100-58-58q22-38 33-78.5t11-83.5q0-133-93.5-226.5T480-800q-43 0-83.5 11T318-756l-58-58q49-32 104.5-49T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 60-17 115.5T814-260ZM537-537ZM423-423Z"/></svg>',
|
|
44
|
+
readingAid: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M320-80v-440l-80-120v-240h480v240l-80 120v440H320Zm160-260q-25 0-42.5-17.5T420-400q0-25 17.5-42.5T480-460q25 0 42.5 17.5T540-400q0 25-17.5 42.5T480-340ZM320-760h320v-40H320v40Zm320 80H320v16l80 120v384h160v-384l80-120v-16ZM480-480Z"/></svg>',
|
|
45
|
+
boldText: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M272-200v-560h221q65 0 120 40t55 111q0 51-23 78.5T602-491q25 11 55.5 41t30.5 90q0 89-65 124.5T501-200H272Zm121-112h104q48 0 58.5-24.5T566-372q0-11-10.5-35.5T494-432H393v120Zm0-228h93q33 0 48-17t15-38q0-24-17-39t-44-15h-95v109Z"/></svg>',
|
|
46
|
+
lineSpacing: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M240-160 80-320l56-56 64 62v-332l-64 62-56-56 160-160 160 160-56 56-64-62v332l64-62 56 56-160 160Zm240-40v-80h400v80H480Zm0-240v-80h400v80H480Zm0-240v-80h400v80H480Z"/></svg>',
|
|
47
|
+
letterSpacing: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M320-80 160-240l160-160 57 56-64 64h334l-63-64 56-56 160 160L640-80l-57-56 64-64H313l63 64-56 56ZM200-480v-400h80v400h-80Zm240 0v-400h80v400h-80Zm240 0v-400h80v400h-80Z"/></svg>',
|
|
48
|
+
dyslexiaFont: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="m131-252 165-440h79l165 440h-76l-39-112H247l-40 112h-76Zm139-176h131l-64-182h-4l-63 182Zm395 186q-51 0-81-27.5T554-342q0-44 34.5-72.5T677-443q23 0 45 4t38 11v-12q0-29-20.5-47T685-505q-23 0-42 9.5T610-468l-47-35q24-29 54.5-43t68.5-14q69 0 103 32.5t34 97.5v178h-63v-37h-4q-14 23-38 35t-53 12Zm12-54q35 0 59.5-24t24.5-56q-14-8-33.5-12.5T689-393q-32 0-50 14t-18 37q0 20 16 33t40 13Z"/></svg>',
|
|
49
|
+
highlightLinks: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm200 160v-80h160q50 0 85-35t35-85q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H520Z"/></svg>',
|
|
50
|
+
highlightTitle: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M320-280q17 0 28.5-11.5T360-320q0-17-11.5-28.5T320-360q-17 0-28.5 11.5T280-320q0 17 11.5 28.5T320-280Zm0-160q17 0 28.5-11.5T360-480q0-17-11.5-28.5T320-520q-17 0-28.5 11.5T280-480q0 17 11.5 28.5T320-440Zm0-160q17 0 28.5-11.5T360-640q0-17-11.5-28.5T320-680q-17 0-28.5 11.5T280-640q0 17 11.5 28.5T320-600Zm120 320h240v-80H440v80Zm0-160h240v-80H440v80Zm0-160h240v-80H440v80ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm0-560v560-560Z"/></svg>',
|
|
51
|
+
contrast: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="m480-260 60-60h100v-100l60-60-60-60v-100H540l-60-60-60 60H320v100l-60 60 60 60v100h100l60 60Zm0-100v-240q50 0 85 35t35 85q0 50-35 85t-85 35ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Z"/></svg>',
|
|
52
|
+
darkContrast: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M484-80q-84 0-157.5-32t-128-86.5Q144-253 112-326.5T80-484q0-146 93-257.5T410-880q-18 99 11 193.5T521-521q71 71 165.5 100T880-410q-26 144-138 237T484-80Zm0-80q88 0 163-44t118-121q-86-8-163-43.5T464-465q-61-61-97-138t-43-163q-77 43-120.5 118.5T160-484q0 135 94.5 229.5T484-160Zm-20-305Z"/></svg>',
|
|
53
|
+
lightContrast: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M440-800v-120h80v120h-80Zm0 760v-120h80v120h-80Zm360-400v-80h120v80H800Zm-760 0v-80h120v80H40Zm708-252-56-56 70-72 58 58-72 70ZM198-140l-58-58 72-70 56 56-70 72Zm564 0-70-72 56-56 72 70-58 58ZM212-692l-72-70 58-58 70 72-56 56Zm268 452q-100 0-170-70t-70-170q0-100 70-170t170-70q100 0 170 70t70 170q0 100-70 170t-170 70Zm0-80q67 0 113.5-46.5T640-480q0-67-46.5-113.5T480-640q-67 0-113.5 46.5T320-480q0 67 46.5 113.5T480-320Zm0-160Z"/></svg>',
|
|
54
|
+
invertColors: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-64-24.5-122.5T706-706L254-254q45 45 103.5 69.5T480-160Zm0-160v-60h200v60H480ZM320-500h60v-80h80v-60h-80v-80h-60v80h-80v60h80v80Z"/></svg>',
|
|
55
|
+
saturation: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-120q-133 0-226.5-92.5T160-436q0-66 25-122t69-100l226-222 226 222q44 44 69 100t25 122q0 131-93.5 223.5T480-120Zm0-80v-568L310-600q-35 33-52.5 74.5T240-436q0 97 70 166.5T480-200Z"/></svg>',
|
|
56
|
+
lowSaturation: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-120q-133 0-226.5-92T160-436q0-65 25-121.5T254-658l226-222 226 222q44 44 69 100.5T800-436q0 132-93.5 224T480-120ZM242-400h474q12-72-13.5-123T650-600L480-768 310-600q-27 26-53 77t-15 123Z"/></svg>',
|
|
57
|
+
highSaturation: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 32.5-156t88-127Q256-817 330-848.5T488-880q80 0 151 27.5t124.5 76q53.5 48.5 85 115T880-518q0 115-70 176.5T640-280h-74q-9 0-12.5 5t-3.5 11q0 12 15 34.5t15 51.5q0 50-27.5 74T480-80Zm0-400Zm-220 40q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm120-160q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm200 0q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm120 160q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17ZM480-160q9 0 14.5-5t5.5-13q0-14-15-33t-15-57q0-42 29-67t71-25h70q66 0 113-38.5T800-518q0-121-92.5-201.5T488-800q-136 0-232 93t-96 227q0 133 93.5 226.5T480-160Z"/></svg>',
|
|
58
|
+
reset: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M440-122q-121-15-200.5-105.5T160-440q0-66 26-126.5T260-672l57 57q-38 34-57.5 79T240-440q0 88 56 155.5T440-202v80Zm80 0v-80q87-16 143.5-83T720-440q0-100-70-170t-170-70h-3l44 44-56 56-140-140 140-140 56 56-44 44h3q134 0 227 93t93 227q0 121-79.5 211.5T520-122Z"/></svg>',
|
|
59
|
+
close: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41Z"/></svg>',
|
|
60
|
+
increase: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2Z"/></svg>',
|
|
61
|
+
decrease: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 13H5v-2h14v2Z"/></svg>',
|
|
62
|
+
arrowBack: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="M400-80 0-480l400-400 71 71-329 329 329 329-71 71Z"/></svg>',
|
|
63
|
+
adjustFontSize: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 4v3h5v12h3V7h5V4H2m19 5h-9v3h3v7h3v-7h3V9Z"/></svg>',
|
|
64
|
+
language: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-155.5t86-127Q252-817 325-848.5T480-880q83 0 155.5 31.5t127 86q54.5 54.5 86 127T880-480q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480-80Zm0-82q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/></svg>',
|
|
65
|
+
hideImages: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="m840-234-80-80v-446H314l-80-80h526q33 0 56.5 23.5T840-760v526ZM792-56l-64-64H200q-33 0-56.5-23.5T120-200v-528l-64-64 56-56 736 736-56 56ZM240-280l120-160 90 120 33-44-283-283v447h447l-80-80H240Zm297-257ZM424-424Z"/></svg>',
|
|
66
|
+
accessibilityReport: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h168q13-36 43.5-58t68.5-22q38 0 68.5 22t43.5 58h168q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm80-80h280v-80H280v80Zm0-160h400v-80H280v80Zm0-160h400v-80H280v80Zm221.5-198.5Q510-807 510-820t-8.5-21.5Q493-850 480-850t-21.5 8.5Q450-833 450-820t8.5 21.5Q467-790 480-790t21.5-8.5ZM200-200v-560 560Z"/></svg>',
|
|
67
|
+
annotations: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v268q-19-9-39-15.5t-41-9.5v-243H200v560h242q3 22 9.5 42t15.5 38H200Zm0-120v40-560 243-3 280Zm80-40h163q3-21 9.5-41t14.5-39H280v80Zm0-160h244q32-30 71.5-50t84.5-27v-3H280v80Zm0-160h400v-80H280v80ZM720-40q-83 0-141.5-58.5T520-240q0-83 58.5-141.5T720-440q83 0 141.5 58.5T920-240q0 83-58.5 141.5T720-40Zm-20-80h40v-100h100v-40H740v-100h-40v100H600v40h100v100Z"/></svg>',
|
|
68
|
+
textToSpeech: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320ZM400-606l-86 86H200v80h114l86 86v-252ZM300-480Z"/></svg>',
|
|
69
|
+
highContrast: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93v640Z"/></svg>',
|
|
70
|
+
simplifyLayout: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M760-360v-80H200v80h560Zm0-160v-80H200v80h560Zm0-160v-80H200v80h560ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm560-80v-80H200v80h560Z"/></svg>'
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const TARGET_SELECTORS = {
|
|
74
|
+
ALL: ['', '*:not(.material-icons,.acc-menu,.acc-menu *)'],
|
|
75
|
+
LINKS: ['a[href]'],
|
|
76
|
+
HEADERS: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', '.wsite-headline', '.wsite-content-title'],
|
|
77
|
+
TEXT: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', '.wsite-headline', '.wsite-content-title', 'img', 'p', 'i', 'svg', 'a', 'button:not(.acc-btn)', 'label', 'li', 'ol']
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const PAGE_CONTENT_SELECTOR = 'body > *:not(.acc-container):not(.acc-rg-container):not(#acc-skip-link)';
|
|
81
|
+
|
|
82
|
+
const TRANSLATIONS = {
|
|
83
|
+
en: {
|
|
84
|
+
"Accessibility Menu": "Accessibility Menu",
|
|
85
|
+
"Reset settings": "Reset settings",
|
|
86
|
+
"Reset All Settings": "Reset All Settings",
|
|
87
|
+
"Close": "Close",
|
|
88
|
+
"Content Adjustments": "Content Adjustments",
|
|
89
|
+
"Adjust Font Size": "Adjust Font Size",
|
|
90
|
+
"Highlight Title": "Highlight Title",
|
|
91
|
+
"Highlight Links": "Highlight Links",
|
|
92
|
+
"Readable Font": "Readable Font",
|
|
93
|
+
"Color Adjustments": "Color Adjustments",
|
|
94
|
+
"Invert Colors": "Invert Colors",
|
|
95
|
+
"Light Contrast": "Light Contrast",
|
|
96
|
+
"Dark Contrast": "Dark Contrast",
|
|
97
|
+
"High Contrast": "High Contrast",
|
|
98
|
+
"High Saturation": "High Saturation",
|
|
99
|
+
"Low Saturation": "Low Saturation",
|
|
100
|
+
"Monochrome": "Monochrome",
|
|
101
|
+
"Tools": "Tools",
|
|
102
|
+
"Reading Guide": "Reading Guide",
|
|
103
|
+
"Stop Animations": "Stop Animations",
|
|
104
|
+
"Big Cursor": "Big Cursor",
|
|
105
|
+
"Increase Font Size": "Increase Font Size",
|
|
106
|
+
"Decrease Font Size": "Decrease Font Size",
|
|
107
|
+
"Letter Spacing": "Letter Spacing",
|
|
108
|
+
"Line Height": "Line Height",
|
|
109
|
+
"Font Weight": "Font Weight",
|
|
110
|
+
"Dyslexia Font": "Dyslexia Font",
|
|
111
|
+
"Language": "Language",
|
|
112
|
+
"Open Accessibility Menu": "Open Accessibility Menu",
|
|
113
|
+
"Hide Images": "Hide Images",
|
|
114
|
+
"Skip to accessibility menu": "Skip to accessibility menu",
|
|
115
|
+
"Accessibility Report": "Accessibility Report",
|
|
116
|
+
"Run Accessibility Check": "Run Accessibility Check",
|
|
117
|
+
"Loading...": "Loading...",
|
|
118
|
+
"Analyzing page...": "Analyzing page...",
|
|
119
|
+
"Critical": "Critical",
|
|
120
|
+
"Serious": "Serious",
|
|
121
|
+
"Moderate": "Moderate",
|
|
122
|
+
"Minor": "Minor",
|
|
123
|
+
"Violations Found": "Violations Found",
|
|
124
|
+
"No Issues Found": "No Issues Found",
|
|
125
|
+
"Element": "Element",
|
|
126
|
+
"Issue": "Issue",
|
|
127
|
+
"How to Fix": "How to Fix",
|
|
128
|
+
"Close Report": "Close Report",
|
|
129
|
+
"Passed Tests": "Passed Tests",
|
|
130
|
+
"Items Need Review": "Items Need Review",
|
|
131
|
+
"Annotations": "Annotations",
|
|
132
|
+
"Text to Speech": "Text to Speech",
|
|
133
|
+
"Text to Speech On": "Text to Speech On",
|
|
134
|
+
"Text to Speech Off": "Text to Speech Off",
|
|
135
|
+
"Simplify Layout": "Simplify Layout",
|
|
136
|
+
"Play": "Play",
|
|
137
|
+
"Pause": "Pause",
|
|
138
|
+
"Stop": "Stop",
|
|
139
|
+
"Loading voice...": "Loading voice...",
|
|
140
|
+
"Reading...": "Reading...",
|
|
141
|
+
"All Languages": "All Languages",
|
|
142
|
+
"Search language": "Search language",
|
|
143
|
+
"Font Size": "Font Size",
|
|
144
|
+
"Contrast": "Contrast",
|
|
145
|
+
"Saturation": "Saturation",
|
|
146
|
+
"Accessibility": "Accessibility"
|
|
147
|
+
},
|
|
148
|
+
ne: {
|
|
149
|
+
"Accessibility Menu": "पहुँच मेनु",
|
|
150
|
+
"Reset settings": "सेटिङ रिसेट गर्नुहोस्",
|
|
151
|
+
"Reset All Settings": "सबै सेटिङ रिसेट गर्नुहोस्",
|
|
152
|
+
"Close": "बन्द गर्नुहोस्",
|
|
153
|
+
"Content Adjustments": "सामग्री समायोजन",
|
|
154
|
+
"Adjust Font Size": "फन्ट आकार समायोजन",
|
|
155
|
+
"Highlight Title": "शीर्षक हाइलाइट",
|
|
156
|
+
"Highlight Links": "लिङ्क हाइलाइट",
|
|
157
|
+
"Readable Font": "पठनीय फन्ट",
|
|
158
|
+
"Color Adjustments": "रङ समायोजन",
|
|
159
|
+
"Invert Colors": "रङ उल्टाउनुहोस्",
|
|
160
|
+
"Light Contrast": "हल्का कन्ट्रास्ट",
|
|
161
|
+
"Dark Contrast": "गाढा कन्ट्रास्ट",
|
|
162
|
+
"High Contrast": "उच्च कन्ट्रास्ट",
|
|
163
|
+
"High Saturation": "उच्च संतृप्तता",
|
|
164
|
+
"Low Saturation": "कम संतृप्तता",
|
|
165
|
+
"Monochrome": "एकरङी",
|
|
166
|
+
"Tools": "उपकरणहरू",
|
|
167
|
+
"Reading Guide": "पठन सहायक",
|
|
168
|
+
"Stop Animations": "एनिमेसन रोक्नुहोस्",
|
|
169
|
+
"Big Cursor": "ठूलो कर्सर",
|
|
170
|
+
"Increase Font Size": "फन्ट आकार बढाउनुहोस्",
|
|
171
|
+
"Decrease Font Size": "फन्ट आकार घटाउनुहोस्",
|
|
172
|
+
"Letter Spacing": "अक्षर दूरी",
|
|
173
|
+
"Line Height": "लाइन उचाइ",
|
|
174
|
+
"Font Weight": "फन्ट वजन",
|
|
175
|
+
"Dyslexia Font": "डिस्लेक्सिया फन्ट",
|
|
176
|
+
"Language": "भाषा",
|
|
177
|
+
"Open Accessibility Menu": "पहुँच मेनु खोल्नुहोस्",
|
|
178
|
+
"Hide Images": "तस्बिर लुकाउनुहोस्",
|
|
179
|
+
"Skip to accessibility menu": "पहुँच मेनुमा जानुहोस्",
|
|
180
|
+
"Accessibility Report": "पहुँच प्रतिवेदन",
|
|
181
|
+
"Run Accessibility Check": "पहुँच जाँच चलाउनुहोस्",
|
|
182
|
+
"Loading...": "लोड हुँदैछ...",
|
|
183
|
+
"Analyzing page...": "पृष्ठ विश्लेषण हुँदैछ...",
|
|
184
|
+
"Critical": "गम्भीर",
|
|
185
|
+
"Serious": "साँघुरो",
|
|
186
|
+
"Moderate": "मध्यम",
|
|
187
|
+
"Minor": "सामान्य",
|
|
188
|
+
"Violations Found": "उल्लंघनहरू फेला परे",
|
|
189
|
+
"No Issues Found": "कुनै समस्या फेला परेन",
|
|
190
|
+
"Element": "तत्व",
|
|
191
|
+
"Issue": "समस्या",
|
|
192
|
+
"How to Fix": "कसरी ठीक गर्ने",
|
|
193
|
+
"Close Report": "प्रतिवेदन बन्द गर्नुहोस्",
|
|
194
|
+
"Passed Tests": "पास भएका परीक्षणहरू",
|
|
195
|
+
"Items Need Review": "समीक्षा आवश्यक वस्तुहरू",
|
|
196
|
+
"Annotations": "टिप्पणीहरू",
|
|
197
|
+
"Text to Speech": "पाठ वाचन",
|
|
198
|
+
"Text to Speech On": "पाठ वाचन सक्रिय",
|
|
199
|
+
"Text to Speech Off": "पाठ वाचन निष्क्रिय",
|
|
200
|
+
"Simplify Layout": "लेआउट सरल बनाउनुहोस्",
|
|
201
|
+
"Play": "प्ले",
|
|
202
|
+
"Pause": "पज",
|
|
203
|
+
"Stop": "रोक्नुहोस्",
|
|
204
|
+
"Loading voice...": "आवाज लोड हुँदैछ...",
|
|
205
|
+
"Reading...": "पढिँदैछ...",
|
|
206
|
+
"All Languages": "सबै भाषाहरू",
|
|
207
|
+
"Search language": "भाषा खोज्नुहोस्",
|
|
208
|
+
"Font Size": "फन्ट आकार",
|
|
209
|
+
"Contrast": "कन्ट्रास्ट",
|
|
210
|
+
"Saturation": "संतृप्तता",
|
|
211
|
+
"Accessibility": "पहुँचयोग्यता"
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const SUPPORTED_LANGUAGES = [
|
|
216
|
+
{ code: 'en', label: 'English (English)' },
|
|
217
|
+
{ code: 'ne', label: 'नेपाली (Nepali)' }
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
/** @typedef {import('./index.js').default} AllyWidget */
|
|
221
|
+
|
|
222
|
+
/** @type {{ [methodName: string]: (this: AllyWidget, ...args: any[]) => any }} */
|
|
223
|
+
const stateMethods = {
|
|
224
|
+
|
|
225
|
+
storageAvailable() {
|
|
226
|
+
try {
|
|
227
|
+
const test = '__ally_test__';
|
|
228
|
+
localStorage.setItem(test, test);
|
|
229
|
+
localStorage.removeItem(test);
|
|
230
|
+
return true;
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
fetchCookie(name) {
|
|
237
|
+
const cookieName = name + '=';
|
|
238
|
+
try {
|
|
239
|
+
const decodedCookie = decodeURIComponent(document.cookie);
|
|
240
|
+
return decodedCookie.split(';')
|
|
241
|
+
.map(c => c.trim())
|
|
242
|
+
.find(c => c.startsWith(cookieName))
|
|
243
|
+
?.substring(cookieName.length) || '{}';
|
|
244
|
+
} catch (e) {
|
|
245
|
+
console.warn('[AllyWidget] Error reading cookie:', e);
|
|
246
|
+
return '{}';
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
storeCookie(name, value, days) {
|
|
251
|
+
try {
|
|
252
|
+
const d = new Date();
|
|
253
|
+
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
254
|
+
const expires = 'expires=' + d.toUTCString();
|
|
255
|
+
const isSecure = window.location.protocol === 'https:';
|
|
256
|
+
document.cookie = name + '=' + value + ';' + expires + ';path=/;SameSite=Strict' + (isSecure ? ';Secure' : '');
|
|
257
|
+
} catch (e) {
|
|
258
|
+
console.warn('[AllyWidget] Error setting cookie:', e);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
getSavedLanguage() {
|
|
263
|
+
try {
|
|
264
|
+
if (this.storageAvailable()) {
|
|
265
|
+
const stored = localStorage.getItem(this.storageKey);
|
|
266
|
+
if (stored) {
|
|
267
|
+
const config = JSON.parse(stored);
|
|
268
|
+
if (config.lang) return config.lang;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const cookieVal = this.fetchCookie(this.storageKey);
|
|
272
|
+
if (cookieVal && cookieVal !== '') {
|
|
273
|
+
const config = JSON.parse(cookieVal);
|
|
274
|
+
if (config.lang) return config.lang;
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
// ignore
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
getBrowserLanguage() {
|
|
283
|
+
if (typeof navigator === 'undefined') return 'en';
|
|
284
|
+
const supportedCodes = this.supportedLanguages.map(lang => lang.code);
|
|
285
|
+
const browserLanguages = navigator.languages
|
|
286
|
+
? [...navigator.languages]
|
|
287
|
+
: [navigator.language || 'en'];
|
|
288
|
+
|
|
289
|
+
for (const browserLang of browserLanguages) {
|
|
290
|
+
const primaryCode = browserLang.split('-')[0].toLowerCase();
|
|
291
|
+
if (supportedCodes.includes(primaryCode)) return primaryCode;
|
|
292
|
+
}
|
|
293
|
+
return 'en';
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
getDefaultLanguage() {
|
|
297
|
+
const savedLang = this.getSavedLanguage();
|
|
298
|
+
if (savedLang) {
|
|
299
|
+
const supportedCodes = this.supportedLanguages.map(lang => lang.code);
|
|
300
|
+
if (supportedCodes.includes(savedLang)) return savedLang;
|
|
301
|
+
}
|
|
302
|
+
return this.getBrowserLanguage();
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
isDevMode() {
|
|
306
|
+
if (typeof window === 'undefined') return false;
|
|
307
|
+
try {
|
|
308
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
309
|
+
return urlParams.get('ally-dev') === 'true' || urlParams.get('acc-dev') === 'true';
|
|
310
|
+
} catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
getDataAttributeOptions() {
|
|
316
|
+
const options = {};
|
|
317
|
+
if (typeof document === 'undefined') return options;
|
|
318
|
+
const attributes = ['lang', 'position', 'offset', 'size', 'icon'];
|
|
319
|
+
|
|
320
|
+
const assignValue = (key, value) => {
|
|
321
|
+
if (value === null || typeof value === 'undefined' || value === '') return;
|
|
322
|
+
switch (key) {
|
|
323
|
+
case 'lang': {
|
|
324
|
+
const lang = String(value).trim();
|
|
325
|
+
if (lang) options.lang = lang;
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
case 'position': {
|
|
329
|
+
const position = String(value).trim();
|
|
330
|
+
if (position) options.position = position;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
case 'offset': {
|
|
334
|
+
const normalized = this.normalizeOffset(value);
|
|
335
|
+
if (normalized) options.offset = normalized;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case 'size': {
|
|
339
|
+
const normalizedSize = this.normalizeButtonSize(value);
|
|
340
|
+
if (normalizedSize) options.size = normalizedSize;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case 'icon': {
|
|
344
|
+
const icon = String(value).trim();
|
|
345
|
+
if (icon) options.icon = icon;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const inspectElement = (el) => {
|
|
352
|
+
if (!el) return;
|
|
353
|
+
attributes.forEach(key => {
|
|
354
|
+
const attrName = `data-ally-${key}`;
|
|
355
|
+
const fallback = `data-acc-${key}`;
|
|
356
|
+
const attrValue = el.getAttribute ? (el.getAttribute(attrName) || el.getAttribute(fallback)) : null;
|
|
357
|
+
assignValue(key, attrValue);
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const scriptCandidates = [];
|
|
362
|
+
if (document.currentScript) scriptCandidates.push(document.currentScript);
|
|
363
|
+
document.querySelectorAll('script[data-ally-lang],script[data-ally-position],script[data-ally-offset],script[data-ally-size],script[data-ally-icon]').forEach(script => {
|
|
364
|
+
if (!scriptCandidates.includes(script)) scriptCandidates.push(script);
|
|
365
|
+
});
|
|
366
|
+
scriptCandidates.forEach(inspectElement);
|
|
367
|
+
|
|
368
|
+
attributes.forEach(key => {
|
|
369
|
+
if (options[key] !== undefined) return;
|
|
370
|
+
const el = document.querySelector(`[data-ally-${key}]`) || document.querySelector(`[data-acc-${key}]`);
|
|
371
|
+
if (el) assignValue(key, el.getAttribute(`data-ally-${key}`) || el.getAttribute(`data-acc-${key}`));
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
return options;
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
normalizeOffset(value) {
|
|
378
|
+
if (!value && value !== 0) return undefined;
|
|
379
|
+
let parts = [];
|
|
380
|
+
if (Array.isArray(value)) {
|
|
381
|
+
parts = value;
|
|
382
|
+
} else if (typeof value === 'string') {
|
|
383
|
+
parts = value.split(/[, ]+/);
|
|
384
|
+
} else {
|
|
385
|
+
parts = [value];
|
|
386
|
+
}
|
|
387
|
+
const parsed = parts
|
|
388
|
+
.map(part => {
|
|
389
|
+
const number = Number(part);
|
|
390
|
+
return Number.isFinite(number) ? Math.round(number) : null;
|
|
391
|
+
})
|
|
392
|
+
.filter(number => number !== null);
|
|
393
|
+
|
|
394
|
+
if (!parsed.length) return undefined;
|
|
395
|
+
if (parsed.length === 1) parsed.push(parsed[0]);
|
|
396
|
+
return [parsed[0], parsed[1] !== undefined ? parsed[1] : parsed[0]];
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
normalizeButtonSize(value) {
|
|
400
|
+
const fallback = this.widgetTheme?.buttonSize || '52px';
|
|
401
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
402
|
+
return `${Math.max(24, Math.round(value))}px`;
|
|
403
|
+
}
|
|
404
|
+
if (typeof value === 'string') {
|
|
405
|
+
const trimmed = value.trim();
|
|
406
|
+
if (!trimmed) return fallback;
|
|
407
|
+
if (/^\d+(\.\d+)?(px|em|rem|%)$/i.test(trimmed)) return trimmed;
|
|
408
|
+
const numeric = Number(trimmed);
|
|
409
|
+
if (Number.isFinite(numeric)) return `${Math.max(24, Math.round(numeric))}px`;
|
|
410
|
+
return trimmed;
|
|
411
|
+
}
|
|
412
|
+
return fallback;
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
toggleDisplay(el, state) {
|
|
416
|
+
if (!el) return;
|
|
417
|
+
try {
|
|
418
|
+
el.style.display = (typeof state === 'undefined')
|
|
419
|
+
? (el.style.display === 'none' ? 'block' : 'none')
|
|
420
|
+
: (state ? 'block' : 'none');
|
|
421
|
+
} catch (e) {
|
|
422
|
+
console.warn('[AllyWidget] Error toggling element:', e);
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
isSystemControlledPreference(key) {
|
|
427
|
+
const systemDefaults = this.widgetConfig?.systemDefaults || {};
|
|
428
|
+
return Object.prototype.hasOwnProperty.call(systemDefaults, key);
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
hasExplicitStatePreference(key) {
|
|
432
|
+
const states = this.widgetConfig?.states || {};
|
|
433
|
+
if (!Object.prototype.hasOwnProperty.call(states, key)) return false;
|
|
434
|
+
return !this.isSystemControlledPreference(key);
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
hasExplicitColorFilterPreference() {
|
|
438
|
+
const states = this.widgetConfig?.states || {};
|
|
439
|
+
const systemDefaults = this.widgetConfig?.systemDefaults || {};
|
|
440
|
+
const keys = Array.isArray(this.colorFilterKeys) ? this.colorFilterKeys : [];
|
|
441
|
+
return keys.some((key) =>
|
|
442
|
+
Object.prototype.hasOwnProperty.call(states, key) &&
|
|
443
|
+
!Object.prototype.hasOwnProperty.call(systemDefaults, key)
|
|
444
|
+
);
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
updateState(payload, options = {}) {
|
|
448
|
+
const source = options.source || 'user';
|
|
449
|
+
const previousStates = this.widgetConfig.states || {};
|
|
450
|
+
const previousSystemDefaults = this.widgetConfig.systemDefaults || {};
|
|
451
|
+
const nextStates = { ...previousStates, ...payload };
|
|
452
|
+
const nextSystemDefaults = { ...previousSystemDefaults };
|
|
453
|
+
const keys = Object.keys(payload || {});
|
|
454
|
+
|
|
455
|
+
if (source === 'system') {
|
|
456
|
+
keys.forEach((key) => { nextSystemDefaults[key] = payload[key]; });
|
|
457
|
+
} else {
|
|
458
|
+
keys.forEach((key) => {
|
|
459
|
+
if (Object.prototype.hasOwnProperty.call(nextSystemDefaults, key)) {
|
|
460
|
+
delete nextSystemDefaults[key];
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const updatedConfig = { ...this.widgetConfig, states: nextStates, systemDefaults: nextSystemDefaults };
|
|
466
|
+
this.saveConfig(updatedConfig);
|
|
467
|
+
return updatedConfig;
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
saveConfig(newConfig) {
|
|
471
|
+
this.widgetConfig = { ...this.widgetConfig, ...newConfig };
|
|
472
|
+
const data = JSON.stringify(this.widgetConfig);
|
|
473
|
+
if (this.storageAvailable()) {
|
|
474
|
+
try {
|
|
475
|
+
localStorage.setItem(this.storageKey, data);
|
|
476
|
+
} catch {
|
|
477
|
+
this.storeCookie(this.storageKey, data, 365);
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
this.storeCookie(this.storageKey, data, 365);
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
retrieveState(key) {
|
|
485
|
+
return this.widgetConfig.states ? this.widgetConfig.states[key] : undefined;
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
loadConfig(cache = true) {
|
|
489
|
+
if (cache) return this.widgetConfig;
|
|
490
|
+
const savedConfig = this.fetchSavedConfig();
|
|
491
|
+
if (savedConfig) {
|
|
492
|
+
try {
|
|
493
|
+
this.widgetConfig = JSON.parse(savedConfig);
|
|
494
|
+
} catch (e) {
|
|
495
|
+
console.warn('[AllyWidget] Error parsing config:', e);
|
|
496
|
+
this.widgetConfig = {};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return this.widgetConfig;
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
fetchSavedConfig() {
|
|
503
|
+
if (this.storageAvailable()) {
|
|
504
|
+
try {
|
|
505
|
+
const stored = localStorage.getItem(this.storageKey);
|
|
506
|
+
if (stored) return stored;
|
|
507
|
+
} catch (e) {
|
|
508
|
+
console.warn('[AllyWidget] localStorage failed, falling back to cookies:', e);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const cookieVal = this.fetchCookie(this.storageKey);
|
|
512
|
+
return cookieVal && cookieVal !== '' ? cookieVal : '{}';
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
var menuCSS = "/* Base styles */\n.acc-menu {\n position: fixed;\n left: var(--acc-menu-inline-gap, 12px);\n top: var(--acc-menu-block-gap, 12px);\n bottom: var(--acc-menu-block-gap, 12px);\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);\n opacity: 1;\n transition: 0.3s;\n z-index: var(--acc-widget-z-index, 100000);\n overflow: hidden;\n background: var(--acc-bg-color);\n width: min(430px, calc(100vw - (var(--acc-menu-inline-gap, 12px) * 2)));\n line-height: 1.5;\n font-size: 16px;\n height: auto;\n letter-spacing: 0.015em;\n color: var(--acc-text-color);\n --acc-content-inline-padding: 14px;\n --acc-menu-inline-gap: clamp(8px, 2.3vw, 18px);\n --acc-menu-block-gap: clamp(10px, 2.2vh, 20px);\n border-radius: 16px;\n box-shadow: 0 18px 42px rgba(0, 0, 0, 0.2);\n}\n\n/* Ensure all elements inherit proper colors for accessibility */\n.acc-menu * {\n color: var(--acc-text-color);\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\n padding: 0;\n margin: 0;\n line-height: 1.5 !important;\n letter-spacing: normal !important;\n}\n\n/* Header section */\n.acc-menu-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0 var(--acc-content-inline-padding);\n height: var(--acc-header-height);\n font-weight: 700 !important;\n background-color: var(--acc-primary-color) !important;\n}\n\n.acc-menu-title {\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 18px !important;\n color: var(--acc-text-color-inverted) !important;\n font-weight: bold;\n}\n\n.acc-menu-title .acc-label {\n color: var(--acc-text-color-inverted) !important;\n}\n\n.acc-menu-title-icon {\n width: 30px;\n height: 30px;\n border-radius: 8px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.acc-menu-title-icon svg {\n fill: var(--acc-text-color-inverted) !important;\n width: 18px !important;\n height: 18px !important;\n min-width: 18px !important;\n min-height: 18px !important;\n max-width: 18px !important;\n max-height: 18px !important;\n}\n\n.acc-header-back {\n display: flex;\n align-items: center;\n}\n\n.acc-back-btn {\n display: flex;\n align-items: center;\n gap: 8px;\n background: transparent;\n border: none;\n padding: 8px;\n cursor: pointer;\n font-size: 16px;\n font-weight: 600;\n color: var(--acc-text-color-inverted) !important;\n transition: background-color 0.2s ease;\n border-radius: 4px;\n visibility: hidden;\n}\n\n.acc-back-btn > span {\n color: var(--acc-text-color-inverted) !important;\n}\n\n.acc-back-btn.visible {\n visibility: visible;\n}\n\n.acc-back-btn:hover {\n background-color: rgba(255, 255, 255, 0.18);\n}\n\n.acc-back-btn:focus {\n outline: 2px solid var(--acc-text-color-inverted);\n outline-offset: 1px;\n}\n\n.acc-back-btn svg {\n fill: var(--acc-text-color-inverted) !important;\n width: 24px !important;\n height: 24px !important;\n}\n\n.acc-menu-title-dynamic {\n display: none !important;\n}\n\n.acc-menu-title-dynamic.visible {\n display: block !important;\n}\n\n.acc-menu-title-default {\n display: block !important;\n}\n\n.acc-menu-title-default.hidden {\n display: none !important;\n}\n\n.acc-menu-header svg {\n fill: var(--acc-text-color-inverted) !important;\n width: 28px !important;\n height: 28px !important;\n min-width: 28px !important;\n min-height: 28px !important;\n max-width: 28px !important;\n max-height: 28px !important;\n}\n\n.acc-menu-header > div {\n display: flex;\n align-items: center;\n}\n\n/* Interactive elements */\n.acc-menu-header div[role=\"button\"] {\n cursor: pointer;\n padding: 8px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n transition: background-color 0.2s ease;\n}\n\n.acc-menu-header div[role=\"button\"]:hover {\n background-color: rgba(255, 255, 255, 0.18);\n}\n\n.acc-menu-header div[role=\"button\"]:focus {\n outline: 2px solid var(--acc-text-color-inverted);\n outline-offset: 1px;\n}\n\n.acc-menu-header .acc-header-actions {\n display: flex;\n align-items: center;\n}\n\n.acc-language-container {\n margin: 0 var(--acc-content-inline-padding) 24px;\n}\n\n.acc-lang-details {\n border: 1px solid rgba(0, 0, 0, 0.1);\n border-radius: var(--acc-button-border-radius);\n background: var(--acc-card-bg);\n}\n\n.acc-lang-summary {\n list-style: none;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 12px 14px;\n cursor: pointer;\n border-radius: var(--acc-button-border-radius);\n}\n\n.acc-lang-summary::-webkit-details-marker {\n display: none;\n}\n\n.acc-lang-summary::marker {\n content: '';\n}\n\n.acc-lang-summary-main {\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: 0;\n}\n\n.acc-lang-current-label {\n font-size: 16px !important;\n font-weight: 600 !important;\n}\n\n.acc-lang-summary:hover {\n background-color: rgba(25, 118, 210, 0.06);\n}\n\n.acc-lang-summary:focus {\n outline: var(--acc-focus-outline-width) solid var(--acc-focus-ring-color);\n outline-offset: var(--acc-focus-outline-offset);\n}\n\n.acc-lang-summary-arrow {\n width: 10px;\n height: 10px;\n border-right: 2px solid var(--acc-text-color);\n border-bottom: 2px solid var(--acc-text-color);\n transform: rotate(-45deg);\n transition: transform 0.2s ease;\n}\n\n.acc-lang-details[open] .acc-lang-summary-arrow {\n transform: rotate(45deg);\n}\n\n.acc-lang-details[open] .acc-lang-summary {\n border-bottom: 1px solid var(--acc-border-color);\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.acc-lang-details-panel {\n padding: 12px 0 8px;\n}\n\n.acc-lang-details-panel .acc-section-title {\n font-size: 16px !important;\n padding: 0 16px 10px;\n}\n\n.acc-lang-flag {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 22px;\n font-size: 18px !important;\n line-height: 1;\n}\n\n.acc-lang-search-wrapper {\n padding: 0 16px 8px;\n}\n\n.acc-lang-search {\n width: 100%;\n padding: 10px 16px;\n border: 1.5px solid var(--acc-border-color);\n border-radius: var(--acc-button-border-radius);\n font-size: 16px;\n background-color: var(--acc-card-bg);\n transition: border-color 0.2s ease;\n}\n\n.acc-lang-search:focus {\n outline: var(--acc-focus-outline-width) solid var(--acc-focus-ring-color);\n outline-offset: var(--acc-focus-outline-offset);\n border-color: var(--acc-primary-color) !important;\n}\n\n.acc-lang-list {\n padding: 6px 8px 12px;\n max-height: 280px;\n overflow-y: auto;\n}\n\n.acc-lang-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n text-align: left;\n padding: 11px 10px;\n margin-bottom: 4px;\n background-color: transparent;\n border: none;\n border-radius: var(--acc-button-border-radius);\n cursor: pointer;\n font-size: 16px;\n color: var(--acc-text-color);\n transition: background-color 0.12s ease;\n}\n\n.acc-lang-item:hover {\n background-color: rgba(25, 118, 210, 0.06);\n}\n\n.acc-lang-item:focus {\n outline: var(--acc-focus-outline-width) solid var(--acc-focus-ring-color);\n outline-offset: var(--acc-focus-outline-offset);\n}\n\n.acc-lang-item.selected {\n background-color: rgba(25, 118, 210, 0.08);\n font-weight: 600;\n}\n\n.acc-lang-item-main {\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: 0;\n}\n\n.acc-lang-item-label {\n font-size: 16px !important;\n line-height: 1.4 !important;\n}\n\n.acc-icon-check {\n display: inline-block;\n width: 18px;\n height: 18px;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23886f60' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: center;\n opacity: 0;\n transition: opacity 0.15s ease;\n}\n\n.acc-lang-item.selected .acc-icon-check {\n opacity: 1;\n}\n\n.acc-menu .acc-lang-select {\n width: 100% !important;\n padding: 0 16px !important;\n font-size: 16px !important;\n font-family: inherit !important;\n font-weight: 600 !important;\n border-radius: var(--acc-button-border-radius) !important;\n background: var(--acc-card-bg) !important;\n border: 1.5px solid var(--acc-border-color) !important;\n min-height: 48px !important;\n max-height: 48px !important;\n height: 48px !important;\n color: var(--acc-text-color) !important;\n -webkit-appearance: none !important;\n -moz-appearance: none !important;\n appearance: none !important;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24px' viewBox='0%20-960%20960%20960' width='24px' fill='%231f1f1f'%3E%3Cpath d='M480-344%20240-584l56-56%20184 184%20184-184%2056 56-240 240Z'/%3E%3C/svg%3E\") !important;\n background-repeat: no-repeat !important;\n background-position: right 12px center !important;\n background-size: 20px !important;\n padding-right: 44px !important;\n}\n\n/* Hide default arrows in Firefox and IE */\n.acc-menu .acc-lang-select::-ms-expand {\n display: none !important;\n}\n\n.acc-menu .acc-lang-select:focus {\n outline: var(--acc-focus-outline-width) solid var(--acc-focus-ring-color);\n outline-offset: var(--acc-focus-outline-offset);\n border-color: var(--acc-primary-color) !important;\n}\n\n/* Option grid layout */\n.acc-options-all {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 0 var(--acc-content-inline-padding) 12px;\n}\n\n.acc-option-category {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.acc-option-category-interaction {\n padding-bottom: 12px;\n}\n\n.acc-option-category .acc-section-title {\n font-size: 14px !important;\n font-weight: 700 !important;\n letter-spacing: 0.01em !important;\n text-transform: none;\n color: #6b7280 !important;\n padding: 0 2px;\n}\n\n.acc-options {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 12px;\n margin: 0;\n}\n\n.acc-options-text {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n}\n\n.acc-options-text-inline {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n}\n\n.acc-btn.acc-text-inline {\n min-height: 54px;\n aspect-ratio: auto;\n flex-direction: row;\n align-items: center;\n justify-content: flex-start;\n text-align: left;\n gap: 10px;\n padding: 12px 14px;\n}\n\n.acc-btn.acc-text-inline .acc-label {\n font-size: 14px !important;\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.acc-btn.acc-text-inline .acc-progress-indicator {\n margin-top: 0;\n margin-left: auto;\n justify-content: flex-end;\n min-width: 24px;\n}\n\n@media only screen and (max-width: 430px) {\n .acc-options-text-inline {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n\n .acc-btn.acc-text-inline {\n min-height: auto;\n aspect-ratio: 11 / 8;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n gap: 6px;\n padding: 10px;\n }\n\n .acc-btn.acc-text-inline .acc-label {\n font-size: 13px !important;\n flex: 0 1 auto;\n }\n\n .acc-btn.acc-text-inline .acc-progress-indicator {\n margin-top: 8px;\n margin-left: 0;\n justify-content: center;\n }\n}\n\n.acc-tts-toggle-container {\n margin: 0;\n}\n\n.acc-text-scale-control {\n width: 100%;\n background: var(--acc-card-bg) !important;\n border: 1px solid rgba(0, 0, 0, 0.1) !important;\n border-radius: var(--acc-border-radius);\n padding: 12px 14px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.acc-text-scale-meta {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.acc-text-scale-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.acc-text-scale-icon svg {\n width: 22px !important;\n height: 22px !important;\n min-width: 22px !important;\n min-height: 22px !important;\n max-width: 22px !important;\n max-height: 22px !important;\n fill: var(--acc-text-color);\n}\n\n.acc-text-scale-meta .acc-label {\n font-size: 15px !important;\n font-weight: 600 !important;\n}\n\n.acc-text-scale-percent {\n margin-left: auto;\n font-size: 15px !important;\n font-weight: 700 !important;\n color: var(--acc-primary-color) !important;\n}\n\n.acc-text-scale-range {\n width: 100%;\n appearance: none;\n -webkit-appearance: none;\n background: transparent;\n height: 20px;\n cursor: pointer;\n margin: 0;\n}\n\n.acc-text-scale-range:focus {\n outline: none;\n}\n\n.acc-text-scale-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 4px;\n border-radius: 999px;\n background: linear-gradient(\n to right,\n var(--acc-primary-color) 0%,\n var(--acc-primary-color) var(--acc-text-scale-progress, 0%),\n #d8d8d8 var(--acc-text-scale-progress, 0%),\n #d8d8d8 100%\n );\n}\n\n.acc-text-scale-range::-webkit-slider-thumb {\n appearance: none;\n -webkit-appearance: none;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: #fff;\n border: 2px solid var(--acc-primary-color);\n margin-top: -7px;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n}\n\n.acc-text-scale-range::-moz-range-track {\n width: 100%;\n height: 4px;\n border-radius: 999px;\n background: #d8d8d8;\n}\n\n.acc-text-scale-range::-moz-range-progress {\n height: 4px;\n border-radius: 999px;\n background: var(--acc-primary-color);\n}\n\n.acc-text-scale-range::-moz-range-thumb {\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: #fff;\n border: 2px solid var(--acc-primary-color);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n}\n\n.acc-btn.acc-tts-toggle {\n width: 100%;\n min-height: 54px;\n aspect-ratio: auto;\n flex-direction: row;\n align-items: center;\n justify-content: flex-start;\n gap: 10px;\n text-align: left;\n padding: 12px 76px 12px 14px;\n}\n\n.acc-btn.acc-tts-toggle .acc-label {\n font-size: 15px !important;\n}\n\n.acc-btn.acc-tts-toggle svg {\n width: 22px !important;\n height: 22px !important;\n min-width: 22px !important;\n min-height: 22px !important;\n max-width: 22px !important;\n max-height: 22px !important;\n}\n\n.acc-btn.acc-tts-toggle::before {\n content: \"\";\n position: absolute;\n right: 14px;\n top: 50%;\n width: 44px;\n height: 24px;\n border-radius: 999px;\n transform: translateY(-50%);\n background: #dbd7d2;\n border: 1px solid rgba(0, 0, 0, 0.08);\n}\n\n.acc-btn.acc-tts-toggle::after {\n content: \"\";\n position: absolute;\n right: 38px;\n top: 50%;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n transform: translateY(-50%);\n background: #fff;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);\n}\n\n.acc-btn.acc-tts-toggle.acc-selected::before {\n background: var(--acc-primary-color);\n border-color: var(--acc-primary-color);\n}\n\n.acc-btn.acc-tts-toggle.acc-selected::after {\n right: 16px;\n}\n\n/* Button styling */\n.acc-btn {\n aspect-ratio: 11 / 8;\n border-radius: var(--acc-border-radius);\n padding: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-direction: column;\n text-align: center;\n font-size: 15px !important;\n background: var(--acc-card-bg) !important;\n border: 1px solid rgba(0, 0, 0, 0.1) !important;\n transition: background-color 0.2s ease;\n cursor: pointer;\n word-break: break-word;\n gap: 6px;\n position: relative;\n}\n\n.acc-btn:hover {\n border-color: var(--acc-hover-color) !important;\n border-width: 1px !important;\n box-shadow: inset 0 0 0 1px var(--acc-hover-color);\n}\n\n.acc-btn:focus {\n outline: var(--acc-focus-outline-width) solid var(--acc-focus-ring-color);\n outline-offset: var(--acc-focus-outline-offset);\n border-color: var(--acc-primary-color) !important;\n}\n\n.acc-btn .acc-label, .acc-text-adjust .acc-label div {\n font-size: 13px !important;\n font-weight: 600 !important;\n}\n\n/* SVG icons */\n.acc-text-adjust svg {\n width: 20px !important;\n height: 20px !important;\n min-width: 20px !important;\n min-height: 20px !important;\n max-width: 20px !important;\n max-height: 20px !important;\n}\n\n.acc-btn svg {\n width: 24px !important;\n height: 24px !important;\n min-width: 24px !important;\n min-height: 24px !important;\n max-width: 24px !important;\n max-height: 24px !important;\n fill: var(--acc-text-color);\n}\n\n/* Selected state */\n.acc-btn.acc-selected {\n background-color: var(--acc-primary-color) !important;\n border-color: var(--acc-primary-color) !important;\n}\n\n.acc-btn.acc-selected .acc-progress-dot {\n background-color: rgba(255, 255, 255, 0.5);\n}\n\n.acc-btn.acc-selected .acc-progress-dot.active {\n background-color: var(--acc-text-color-inverted) !important;\n}\n\n.acc-btn.acc-selected svg,\n.acc-btn.acc-selected span,\n.acc-btn.acc-selected .acc-label {\n fill: var(--acc-text-color-inverted) !important;\n color: var(--acc-text-color-inverted) !important;\n}\n\n.acc-btn.acc-tts-toggle.acc-selected {\n background-color: var(--acc-card-bg) !important;\n border-color: rgba(0, 0, 0, 0.1) !important;\n}\n\n.acc-btn.acc-tts-toggle.acc-selected svg,\n.acc-btn.acc-tts-toggle.acc-selected span,\n.acc-btn.acc-tts-toggle.acc-selected .acc-label {\n fill: var(--acc-text-color) !important;\n color: var(--acc-text-color) !important;\n}\n\n/* Footer section */\n.acc-footer {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n background: var(--acc-card-bg);\n padding: 12px 16px;\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 10px;\n border-top: 1px solid var(--acc-border-color);\n z-index: 100;\n overflow: visible;\n}\n\n.acc-footer-meta {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n width: 100%;\n flex-wrap: nowrap;\n}\n\n.acc-footer-reset {\n display: flex;\n width: 100%;\n align-items: center;\n justify-content: center;\n gap: 8px;\n padding: 12px 16px;\n border: none;\n border-radius: var(--acc-button-border-radius);\n background-color: var(--acc-primary-color) !important;\n cursor: pointer;\n transition: background-color 0.2s ease;\n white-space: nowrap;\n}\n\n.acc-footer-reset svg {\n width: 24px !important;\n height: 24px !important;\n fill: var(--acc-text-color-inverted) !important;\n}\n\n.acc-footer-reset .acc-label {\n font-size: 16px !important;\n font-weight: 600 !important;\n color: var(--acc-text-color-inverted) !important;\n line-height: 1.2 !important;\n}\n\n.acc-footer-reset:hover {\n background-color: var(--acc-primary-color-dark) !important;\n}\n\n.acc-footer-reset:focus {\n outline: var(--acc-focus-outline-width) solid var(--acc-focus-ring-color);\n outline-offset: var(--acc-focus-outline-offset);\n}\n\n.acc-footer a {\n font-size: 12px !important;\n text-decoration: none !important;\n color: #6b7280 !important;\n background: transparent !important;\n font-weight: 500 !important;\n padding: 4px 0;\n border-radius: 4px;\n transition: color 0.15s ease;\n letter-spacing: 0.01em !important;\n align-self: center;\n display: inline-flex;\n align-items: center;\n white-space: nowrap;\n line-height: 1.2 !important;\n}\n\n.acc-footer a:hover {\n text-decoration: none !important;\n color: var(--acc-primary-color) !important;\n}\n\n.acc-footer a:focus {\n outline: var(--acc-focus-outline-width) solid var(--acc-focus-ring-color);\n outline-offset: var(--acc-focus-outline-offset);\n}\n\n.acc-footer-lang-toggle {\n display: inline-flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n min-width: 84px;\n padding: 7px 10px 7px 12px;\n border: 1px solid var(--acc-border-color);\n border-radius: var(--acc-button-border-radius);\n background: var(--acc-card-bg);\n cursor: pointer;\n transition: border-color 0.2s ease, background-color 0.2s ease;\n}\n\n.acc-footer-lang-toggle:hover {\n border-color: var(--acc-hover-color);\n}\n\n.acc-footer-lang-toggle:focus {\n outline: var(--acc-focus-outline-width) solid var(--acc-focus-ring-color);\n outline-offset: var(--acc-focus-outline-offset);\n}\n\n.acc-footer-lang-current {\n display: inline-block;\n min-width: 2ch;\n text-align: left;\n font-size: 14px !important;\n font-weight: 600 !important;\n line-height: 1 !important;\n}\n\n.acc-footer-lang-arrow {\n width: 8px;\n height: 8px;\n border-right: 1.5px solid var(--acc-text-color);\n border-bottom: 1.5px solid var(--acc-text-color);\n transform: rotate(45deg) translateY(-1px);\n transition: transform 0.2s ease;\n}\n\n.acc-footer-lang-toggle[aria-expanded=\"true\"] .acc-footer-lang-arrow {\n transform: rotate(-135deg) translateY(-1px);\n}\n\n.acc-lang-modal {\n position: absolute;\n right: 16px;\n bottom: calc(100% + 10px);\n width: min(320px, calc(100% - 32px));\n border: 1px solid var(--acc-border-color);\n border-radius: 14px;\n background: var(--acc-card-bg);\n box-shadow: 0 16px 32px rgba(0, 0, 0, 0.2);\n z-index: 120;\n padding: 10px 0 8px;\n}\n\n.acc-lang-modal[hidden] {\n display: none;\n}\n\n.acc-lang-modal-header {\n padding: 0 14px 8px;\n}\n\n.acc-lang-modal .acc-section-title {\n font-size: 14px !important;\n font-weight: 700 !important;\n color: #6b7280 !important;\n}\n\n.acc-lang-modal .acc-lang-search-wrapper {\n padding: 0 12px 8px;\n}\n\n.acc-lang-modal .acc-lang-list {\n padding: 6px 8px 8px;\n max-height: 240px;\n}\n\n/* Content area */\n.acc-menu-content {\n overflow: auto;\n max-height: calc(100% - 122px);\n padding: 24px 0 36px;\n}\n\n/* Text adjustments */\n.acc-text-adjust {\n background: var(--acc-card-bg);\n padding: 18px 20px;\n margin-bottom: 20px;\n border-radius: var(--acc-border-radius);\n border: 1px solid rgba(0, 0, 0, 0.1);\n}\n\n.acc-text-adjust .acc-label {\n display: flex;\n justify-content: flex-start;\n}\n\n.acc-text-adjust > div {\n display: flex;\n justify-content: space-between;\n margin-top: 20px;\n align-items: center;\n font-size: 16px;\n}\n\n.acc-text-adjust .acc-label div {\n font-size: 16px !important;\n}\n\n.acc-text-adjust div[role=\"button\"] {\n background: var(--acc-bg-color) !important;\n border-radius: 50%;\n width: 40px;\n height: 40px;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n border: 1px solid rgba(0, 0, 0, 0.1);\n transition: border-color 0.2s ease;\n}\n\n.acc-text-adjust div[role=\"button\"]:hover {\n border-color: var(--acc-primary-color);\n}\n\n.acc-text-adjust div[role=\"button\"]:focus {\n outline: var(--acc-focus-outline-width) solid var(--acc-focus-ring-color);\n outline-offset: var(--acc-focus-outline-offset);\n}\n\n/* Overlay */\n.acc-overlay {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n z-index: calc(var(--acc-widget-z-index, 100000) - 1);\n}\n\n/* Progress indicator */\n.acc-progress-indicator {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 4px;\n margin-top: 8px;\n height: 8px;\n}\n\n.acc-progress-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background-color: var(--acc-border-color);\n transition: background-color 0.2s ease;\n}\n\n.acc-progress-dot.active {\n background-color: var(--acc-primary-color);\n}\n\n/* Selected state updates indicator colors */\n.acc-btn.acc-selected .acc-progress-dot.active {\n background-color: var(--acc-bg-color);\n}\n\n/* Responsive adjustments */\n@media only screen and (max-width: 560px) {\n .acc-menu {\n width: calc(100vw - (var(--acc-menu-inline-gap, 8px) * 2));\n }\n}\n\n@media only screen and (max-width: 420px) {\n .acc-options {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 12px;\n }\n .acc-btn {\n padding: 8px;\n }\n}\n\n/* Ensure proper focus visibility for assistive technology */\n@media (prefers-reduced-motion: reduce) {\n .acc-menu,\n .acc-btn,\n .acc-lang-select,\n .acc-progress-dot,\n .acc-menu-header div[role=\"button\"],\n .acc-lang-toggle,\n .acc-back-btn,\n .acc-footer-reset,\n .acc-footer-lang-toggle,\n .acc-footer-lang-arrow,\n .acc-text-adjust div[role=\"button\"] {\n transition: none;\n }\n}\n";
|
|
518
|
+
|
|
519
|
+
var widgetCSS = " /* Base styles for the widget */\n .acc-widget, .acc-menu {\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n font-weight: 400;\n -webkit-font-smoothing: antialiased;\n }\n \n .acc-widget *, .acc-menu * { \n box-sizing: border-box !important; \n }\n \n /* Accessibility toggle button */\n .acc-toggle-btn {\n position: fixed;\n z-index: var(--acc-widget-z-index, 100000);\n left: 30px;\n bottom: 30px;\n border-radius: 50%;\n align-items: center;\n justify-content: center;\n width: var(--acc-button-size, 48px);\n height: var(--acc-button-size, 48px);\n display: flex;\n cursor: pointer;\n outline: none !important;\n border: none !important;\n box-shadow: inset 0 0 0 4px var(--acc-primary-color, #1976d2), inset 0 0 0 6px white, 0 2px 5px rgba(0,0,0,0.2) !important;\n background: var(--acc-primary-color, #1976d2) !important;\n transition: transform 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;\n overflow: visible;\n }\n \n .acc-toggle-btn .acc-toggle-icon {\n width: 60%;\n height: 60%;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n }\n\n .acc-toggle-btn .acc-toggle-icon svg {\n width: 100%;\n height: 100%;\n fill: white;\n transition: none;\n }\n \n .acc-toggle-btn:hover {\n transform: scale(1.12);\n }\n\n .acc-toggle-btn::before,\n .acc-toggle-btn::after {\n content: \"\";\n position: absolute;\n top: 50%;\n left: 50%;\n width: 44%;\n height: 2.6px;\n border-radius: 999px;\n background: var(--acc-text-color-inverted, #fff);\n opacity: 0;\n pointer-events: none;\n transform: translate(-50%, -50%) rotate(0deg);\n transition: none;\n }\n\n .acc-toggle-btn[aria-expanded=\"true\"] {\n background: var(--acc-primary-color, #1976d2) !important;\n box-shadow: inset 0 0 0 4px var(--acc-primary-color, #1976d2), inset 0 0 0 6px white, 0 6px 14px rgba(0, 0, 0, 0.24) !important;\n }\n\n .acc-toggle-btn[aria-expanded=\"true\"] .acc-toggle-icon svg {\n opacity: 0;\n }\n\n .acc-toggle-btn[aria-expanded=\"true\"]::before,\n .acc-toggle-btn[aria-expanded=\"true\"]::after {\n opacity: 1;\n }\n\n .acc-toggle-btn[aria-expanded=\"true\"]::before {\n transform: translate(-50%, -50%) rotate(45deg);\n }\n\n .acc-toggle-btn[aria-expanded=\"true\"]::after {\n transform: translate(-50%, -50%) rotate(-45deg);\n }\n\n .acc-violation-bubble {\n position: absolute;\n top: -8px;\n right: -8px;\n min-width: 24px;\n height: 24px;\n border-radius: 12px;\n font-size: 11px;\n font-weight: 700;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 4px;\n pointer-events: none;\n z-index: 1;\n color: #fff;\n border: none;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n }\n\n .acc-violation-bubble[data-severity=\"critical\"] {\n background: #d32f2f;\n color: #fff;\n }\n\n .acc-violation-bubble[data-severity=\"serious\"] {\n background: #f57c00;\n color: #fff;\n }\n\n .acc-violation-bubble[data-severity=\"moderate\"] {\n background: #fbc02d;\n color: #333;\n }\n\n .acc-violation-bubble[hidden] {\n display: none;\n }\n\n .acc-toggle-btn:focus {\n outline: none !important;\n }\n\n .acc-toggle-btn:focus-visible {\n outline: 3px solid var(--acc-primary-color, #1976d2) !important;\n outline-offset: 2px;\n }\n\n body.acc-tts-click-mode :is(h1, h2, h3, h4, h5, h6, p, li, dt, dd, blockquote, figcaption, caption, th, td, div, section):not(.acc-container *):hover {\n cursor: pointer;\n }\n\n .acc-tts-active-block {\n outline: 2px solid var(--acc-primary-color, #1976d2) !important;\n outline-offset: 3px !important;\n border-radius: 4px;\n }\n \n @media (prefers-reduced-motion: reduce) {\n .acc-toggle-btn {\n transition: none;\n }\n\n .acc-toggle-btn .acc-toggle-icon svg,\n .acc-toggle-btn::before,\n .acc-toggle-btn::after {\n transition: none;\n }\n\n }\n";
|
|
520
|
+
|
|
521
|
+
var reportCSS = ".acc-report-panel {\n display: none;\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: calc(var(--acc-widget-z-index, 100000) + 10);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n}\n.acc-report-panel.acc-report-visible {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.acc-report-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.6);\n}\n.acc-report-dialog {\n position: relative;\n background: #fff;\n border-radius: 12px;\n width: 90%;\n max-width: 800px;\n max-height: 85vh;\n display: flex;\n flex-direction: column;\n box-shadow: 0 20px 60px rgba(0,0,0,0.3);\n}\n.acc-report-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 20px;\n border-bottom: 1px solid #e0e0e0;\n}\n.acc-report-title {\n margin: 0;\n font-size: 18px;\n font-weight: 600;\n color: #1a1a1a;\n}\n.acc-report-close {\n background: none;\n border: none;\n padding: 8px;\n cursor: pointer;\n border-radius: 6px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.acc-report-close:hover {\n background: #f0f0f0;\n}\n.acc-report-close svg {\n width: 20px;\n height: 20px;\n fill: #666;\n}\n.acc-report-status {\n padding: 8px 20px;\n font-size: 14px;\n color: #666;\n background: #f8f9fa;\n}\n.acc-report-content {\n flex: 1;\n overflow-y: auto;\n padding: 16px 20px;\n}\n.acc-report-loading {\n text-align: center;\n padding: 40px;\n color: #666;\n}\n.acc-report-error {\n color: #d32f2f;\n padding: 20px;\n text-align: center;\n}\n.acc-report-summary {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n gap: 12px;\n margin-bottom: 20px;\n}\n.acc-report-stat {\n background: #f8f9fa;\n border-radius: 8px;\n padding: 16px;\n text-align: center;\n}\n.acc-report-stat-value {\n font-size: 28px;\n font-weight: 700;\n display: block;\n}\n.acc-report-stat-label {\n font-size: 12px;\n color: #666;\n text-transform: uppercase;\n margin-top: 4px;\n}\n.acc-report-stat.critical .acc-report-stat-value { color: #d32f2f; }\n.acc-report-stat.serious .acc-report-stat-value { color: #f57c00; }\n.acc-report-stat.moderate .acc-report-stat-value { color: #fbc02d; }\n.acc-report-stat.minor .acc-report-stat-value { color: #7cb342; }\n.acc-report-stat.passed .acc-report-stat-value { color: #43a047; }\n.acc-report-section {\n margin-bottom: 20px;\n}\n.acc-report-section-title {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n margin-bottom: 12px;\n padding-bottom: 8px;\n border-bottom: 2px solid #e0e0e0;\n}\n.acc-report-violation {\n background: #fff;\n border: 1px solid #e0e0e0;\n border-radius: 8px;\n margin-bottom: 12px;\n overflow: hidden;\n}\n.acc-report-violation-header {\n padding: 12px 16px;\n background: #f8f9fa;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.acc-report-violation-header:hover {\n background: #f0f0f0;\n}\n.acc-report-violation-impact {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n padding: 4px 8px;\n border-radius: 4px;\n color: #fff;\n}\n.acc-report-violation-impact.critical { background: #d32f2f; }\n.acc-report-violation-impact.serious { background: #f57c00; }\n.acc-report-violation-impact.moderate { background: #fbc02d; color: #333; }\n.acc-report-violation-impact.minor { background: #7cb342; }\n.acc-report-violation-title {\n flex: 1;\n font-weight: 500;\n color: #333;\n}\n.acc-report-violation-count {\n font-size: 12px;\n color: #666;\n background: #e0e0e0;\n padding: 2px 8px;\n border-radius: 12px;\n}\n.acc-report-violation-details {\n display: none;\n padding: 16px;\n border-top: 1px solid #e0e0e0;\n}\n.acc-report-violation.expanded .acc-report-violation-details {\n display: block;\n}\n.acc-report-violation-desc {\n color: #666;\n font-size: 14px;\n margin-bottom: 12px;\n}\n.acc-report-violation-help {\n font-size: 13px;\n margin-bottom: 12px;\n}\n.acc-report-violation-help a {\n color: #1976d2;\n}\n.acc-report-node {\n background: #f8f9fa;\n border-radius: 6px;\n padding: 12px;\n margin-top: 8px;\n}\n.acc-report-node-html {\n font-family: monospace;\n font-size: 12px;\n background: #263238;\n color: #80cbc4;\n padding: 8px 12px;\n border-radius: 4px;\n overflow-x: auto;\n white-space: pre-wrap;\n word-break: break-all;\n}\n.acc-report-node-fix {\n margin-top: 8px;\n font-size: 13px;\n color: #333;\n}\n.acc-report-node-fix strong {\n color: #1976d2;\n}\n.acc-report-success {\n text-align: center;\n padding: 40px;\n}\n.acc-report-success-icon {\n width: 64px;\n height: 64px;\n background: #43a047;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 16px;\n}\n.acc-report-success-icon svg {\n width: 32px;\n height: 32px;\n fill: #fff;\n}\n.acc-report-footer {\n padding: 12px 20px;\n border-top: 1px solid #e0e0e0;\n text-align: center;\n}\n.acc-report-powered {\n font-size: 12px;\n}\n@media (max-width: 600px) {\n .acc-report-dialog {\n width: 95%;\n max-height: 90vh;\n }\n .acc-report-summary {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n";
|
|
522
|
+
|
|
523
|
+
var readingGuideCSS = ".acc-rg {\n position: fixed;\n left: 0;\n right: 0;\n width: 100%;\n pointer-events: none;\n background-color: rgba(0, 0, 0, 0.4);\n z-index: calc(var(--acc-widget-z-index, 100000) + 1);\n}\n.acc-rg-top {\n top: 0;\n}\n.acc-rg-bottom {\n bottom: 0;\n}\n/* Softer overlay when high contrast is active */\nbody.acc-high-contrast-mode .acc-rg {\n background-color: rgba(0, 0, 0, 0.25);\n}\n";
|
|
524
|
+
|
|
525
|
+
var skipLinkCSS = ".acc-skip-link {\n font-family: inherit;\n position: fixed;\n top: 16px;\n left: 16px;\n background: var(--acc-card-bg, #ffffff);\n color: var(--acc-text-color, #222222);\n border: 3px solid var(--acc-primary-color, #1976d2);\n border-radius: var(--acc-button-border-radius, 0.4rem);\n padding: 8px 16px;\n z-index: calc(var(--acc-widget-z-index, 100000) + 2);\n transform: translateY(-140%);\n opacity: 0;\n pointer-events: none;\n transition: transform 0.2s ease, opacity 0.2s ease;\n font-size: 16px;\n line-height: 1.2;\n font-weight: 600;\n background-clip: padding-box;\n}\n.acc-skip-link:focus,\n.acc-skip-link:active {\n transform: translateY(0);\n opacity: 1;\n pointer-events: auto;\n outline: var(--acc-focus-outline-width, 3px) solid var(--acc-focus-ring-color, #1976d2);\n outline-offset: var(--acc-focus-outline-offset, 2px);\n}\n";
|
|
526
|
+
|
|
527
|
+
var annotationsCSS = ".acc-annotation-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 0;\n height: 0;\n z-index: calc(var(--acc-widget-z-index, 100000) + 5);\n pointer-events: none;\n}\n\n.acc-annotation-marker {\n position: absolute;\n width: 20px;\n height: 20px;\n border: none;\n border-radius: 999px;\n color: #fff;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);\n cursor: pointer;\n pointer-events: auto;\n transform: translate(-40%, -50%);\n}\n\n.acc-annotation-marker svg {\n width: 12px;\n height: 12px;\n fill: currentColor;\n}\n\n.acc-annotation-marker[data-impact=\"critical\"] {\n background: #b71c1c;\n}\n\n.acc-annotation-marker[data-impact=\"serious\"] {\n background: #d84315;\n}\n\n.acc-annotation-marker[data-impact=\"moderate\"] {\n background: #ef6c00;\n}\n\n.acc-annotation-marker[data-impact=\"minor\"] {\n background: #1565c0;\n}\n\n.acc-annotation-popup {\n position: absolute;\n width: min(320px, 92vw);\n background: #fff;\n color: #1a1a1a;\n border: 1px solid #d7d7d7;\n border-radius: 10px;\n box-shadow: 0 10px 24px rgba(0, 0, 0, 0.24);\n padding: 12px;\n pointer-events: auto;\n}\n\n.acc-annotation-popup-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n}\n\n.acc-annotation-popup-title {\n margin: 0;\n font-size: 14px;\n line-height: 1.3;\n}\n\n.acc-annotation-popup-close {\n border: 0;\n background: transparent;\n padding: 2px;\n width: 24px;\n height: 24px;\n color: #444;\n cursor: pointer;\n}\n\n.acc-annotation-popup-close svg {\n width: 20px;\n height: 20px;\n fill: currentColor;\n}\n\n.acc-annotation-popup p {\n margin: 8px 0;\n font-size: 13px;\n line-height: 1.45;\n}\n\n.acc-annotation-popup a {\n color: #0d47a1;\n font-weight: 600;\n text-decoration: underline;\n}\n";
|
|
528
|
+
|
|
529
|
+
const STATIC_STYLE_ID = 'ally-static-styles';
|
|
530
|
+
const STATIC_STYLES = [
|
|
531
|
+
menuCSS,
|
|
532
|
+
widgetCSS,
|
|
533
|
+
reportCSS,
|
|
534
|
+
readingGuideCSS,
|
|
535
|
+
skipLinkCSS,
|
|
536
|
+
annotationsCSS
|
|
537
|
+
].join('\n');
|
|
538
|
+
|
|
539
|
+
/** @typedef {import('./index.js').default} AllyWidget */
|
|
540
|
+
|
|
541
|
+
/** @type {{ [methodName: string]: (this: AllyWidget, ...args: any[]) => any }} */
|
|
542
|
+
const styleMethods = {
|
|
543
|
+
|
|
544
|
+
findElement(selector, parent = document) {
|
|
545
|
+
try {
|
|
546
|
+
return parent.querySelector(selector);
|
|
547
|
+
} catch (e) {
|
|
548
|
+
console.warn(`[AllyWidget] Failed to query selector: ${selector}`, e);
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
injectStyle(id, css) {
|
|
554
|
+
if (!css || typeof document === 'undefined') return;
|
|
555
|
+
try {
|
|
556
|
+
let style = document.getElementById(id) || document.createElement('style');
|
|
557
|
+
style.innerHTML = css;
|
|
558
|
+
if (!style.id) {
|
|
559
|
+
style.id = id;
|
|
560
|
+
document.head.appendChild(style);
|
|
561
|
+
}
|
|
562
|
+
} catch (e) {
|
|
563
|
+
console.warn('[AllyWidget] Error adding stylesheet:', e);
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
createCSS(styles) {
|
|
568
|
+
let css = '';
|
|
569
|
+
if (!styles) return css;
|
|
570
|
+
const browserPrefixes = ['-o-', '-ms-', '-moz-', '-webkit-', ''];
|
|
571
|
+
const prefixedProperties = ['filter'];
|
|
572
|
+
for (let key in styles) {
|
|
573
|
+
if (!Object.prototype.hasOwnProperty.call(styles, key)) continue;
|
|
574
|
+
let prefixes = prefixedProperties.includes(key) ? browserPrefixes : [''];
|
|
575
|
+
prefixes.forEach(prefix => {
|
|
576
|
+
css += `${prefix}${key}:${styles[key]} !important;`;
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
return css;
|
|
580
|
+
},
|
|
581
|
+
|
|
582
|
+
wrapCSS(selector, childrenSelector, css) {
|
|
583
|
+
let output = '';
|
|
584
|
+
childrenSelector.forEach(child => {
|
|
585
|
+
output += `${selector} ${child}{${css}}`;
|
|
586
|
+
});
|
|
587
|
+
return output;
|
|
588
|
+
},
|
|
589
|
+
|
|
590
|
+
buildCSS(config) {
|
|
591
|
+
if (!config) return '';
|
|
592
|
+
let output = '';
|
|
593
|
+
output += this.createCSS(config.styles || {});
|
|
594
|
+
if (output.length && config.selector) {
|
|
595
|
+
output = this.wrapCSS(config.selector, config.childrenSelector || [''], output);
|
|
596
|
+
}
|
|
597
|
+
output += config.css || '';
|
|
598
|
+
return output;
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
applyToolStyle(config) {
|
|
602
|
+
let { id = '', enable = false } = config;
|
|
603
|
+
let styleId = `acc-${id}`;
|
|
604
|
+
if (enable) {
|
|
605
|
+
let css = this.buildCSS(config);
|
|
606
|
+
this.injectStyle(styleId, css);
|
|
607
|
+
} else {
|
|
608
|
+
let style = document.getElementById(styleId);
|
|
609
|
+
if (style) style.remove();
|
|
610
|
+
}
|
|
611
|
+
document.documentElement.classList.toggle(styleId, enable);
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
applyThemeVariables() {
|
|
615
|
+
if (typeof document === 'undefined') return;
|
|
616
|
+
const vars = {
|
|
617
|
+
'--acc-primary-color': this.widgetTheme.primaryColor,
|
|
618
|
+
'--acc-primary-color-light': this.widgetTheme.primaryColorLight,
|
|
619
|
+
'--acc-primary-color-dark': this.widgetTheme.primaryColorDark,
|
|
620
|
+
'--acc-bg-color': this.widgetTheme.backgroundColor,
|
|
621
|
+
'--acc-text-color': this.widgetTheme.textColor,
|
|
622
|
+
'--acc-text-color-inverted': this.widgetTheme.textColorInverted,
|
|
623
|
+
'--acc-card-bg': this.widgetTheme.cardBackground,
|
|
624
|
+
'--acc-border-color': this.widgetTheme.borderColor,
|
|
625
|
+
'--acc-focus-ring-color': this.widgetTheme.focusRingColor,
|
|
626
|
+
'--acc-hover-color': this.widgetTheme.hoverColor,
|
|
627
|
+
'--acc-active-color': this.widgetTheme.activeColor,
|
|
628
|
+
'--acc-border-radius': this.widgetTheme.borderRadius,
|
|
629
|
+
'--acc-button-border-radius': this.widgetTheme.buttonBorderRadius,
|
|
630
|
+
'--acc-header-height': this.widgetTheme.headerHeight,
|
|
631
|
+
'--acc-focus-outline-width': this.widgetTheme.focusBorderWidth,
|
|
632
|
+
'--acc-focus-outline-offset': this.widgetTheme.focusOutlineOffset,
|
|
633
|
+
'--acc-widget-z-index': String(this.widgetTheme.zIndex),
|
|
634
|
+
'--acc-button-size': this.widgetTheme.buttonSize
|
|
635
|
+
};
|
|
636
|
+
Object.entries(vars).forEach(([key, value]) => {
|
|
637
|
+
document.documentElement.style.setProperty(key, value);
|
|
638
|
+
});
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
registerStaticStyles() {
|
|
642
|
+
if (this.staticStylesRegistered) return;
|
|
643
|
+
this.injectStyle(STATIC_STYLE_ID, STATIC_STYLES);
|
|
644
|
+
this.staticStylesRegistered = true;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
/** @typedef {import('./index.js').default} AllyWidget */
|
|
650
|
+
|
|
651
|
+
const AXE_CORE_VERSION = '4.11.1';
|
|
652
|
+
const AXE_CORE_SRC = `https://cdn.jsdelivr.net/npm/axe-core@${AXE_CORE_VERSION}/axe.min.js`;
|
|
653
|
+
const AXE_CORE_INTEGRITY = 'sha384-wb3zgvLcZeMFSec08dk7g8K8yDFFAX2uNKVwOUuowwc/wIfE2t6XVUjTEgPrOJCS';
|
|
654
|
+
const AXE_RUN_OPTIONS = {
|
|
655
|
+
runOnly: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice']
|
|
656
|
+
};
|
|
657
|
+
const MAX_ANNOTATIONS = 50;
|
|
658
|
+
const SYSTEM_PREFERS_REDUCED_MOTION = '(prefers-reduced-motion: reduce)';
|
|
659
|
+
|
|
660
|
+
/** @type {{ [methodName: string]: (this: AllyWidget, ...args: any[]) => any }} */
|
|
661
|
+
const featureMethods = {
|
|
662
|
+
|
|
663
|
+
getContrastToggleDisplay(index) {
|
|
664
|
+
if (index === 0) {
|
|
665
|
+
return { key: 'light-contrast', label: 'Light', icon: this.widgetIcons.lightContrast };
|
|
666
|
+
}
|
|
667
|
+
if (index === 1) {
|
|
668
|
+
return { key: 'dark-contrast', label: 'Dark', icon: this.widgetIcons.darkContrast };
|
|
669
|
+
}
|
|
670
|
+
return { key: null, label: 'Contrast', icon: this.widgetIcons.contrast };
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
updateContrastToggleButton(button, index) {
|
|
674
|
+
if (!button) return;
|
|
675
|
+
const display = this.getContrastToggleDisplay(index);
|
|
676
|
+
const iconNode = button.querySelector('svg');
|
|
677
|
+
if (iconNode) {
|
|
678
|
+
iconNode.outerHTML = display.icon;
|
|
679
|
+
} else {
|
|
680
|
+
button.insertAdjacentHTML('afterbegin', display.icon);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const translatedLabel = this.translate(display.label);
|
|
684
|
+
const labelNode = button.querySelector('.acc-label');
|
|
685
|
+
if (labelNode) {
|
|
686
|
+
labelNode.setAttribute('data-acc-text', display.label);
|
|
687
|
+
labelNode.innerText = translatedLabel;
|
|
688
|
+
}
|
|
689
|
+
button.setAttribute('title', translatedLabel);
|
|
690
|
+
button.setAttribute('aria-label', translatedLabel);
|
|
691
|
+
button.setAttribute('data-contrast-mode', display.key || 'off');
|
|
692
|
+
},
|
|
693
|
+
|
|
694
|
+
getSaturationToggleDisplay(index) {
|
|
695
|
+
if (index === 0) {
|
|
696
|
+
return { key: 'low-saturation', label: 'Low', icon: this.widgetIcons.lowSaturation };
|
|
697
|
+
}
|
|
698
|
+
if (index === 1) {
|
|
699
|
+
return { key: 'high-saturation', label: 'High', icon: this.widgetIcons.highSaturation };
|
|
700
|
+
}
|
|
701
|
+
return { key: null, label: 'Saturation', icon: this.widgetIcons.saturation };
|
|
702
|
+
},
|
|
703
|
+
|
|
704
|
+
updateSaturationToggleButton(button, index) {
|
|
705
|
+
if (!button) return;
|
|
706
|
+
const display = this.getSaturationToggleDisplay(index);
|
|
707
|
+
const iconNode = button.querySelector('svg');
|
|
708
|
+
if (iconNode) {
|
|
709
|
+
iconNode.outerHTML = display.icon;
|
|
710
|
+
} else {
|
|
711
|
+
button.insertAdjacentHTML('afterbegin', display.icon);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const translatedLabel = this.translate(display.label);
|
|
715
|
+
const labelNode = button.querySelector('.acc-label');
|
|
716
|
+
if (labelNode) {
|
|
717
|
+
labelNode.setAttribute('data-acc-text', display.label);
|
|
718
|
+
labelNode.innerText = translatedLabel;
|
|
719
|
+
}
|
|
720
|
+
button.setAttribute('title', translatedLabel);
|
|
721
|
+
button.setAttribute('aria-label', translatedLabel);
|
|
722
|
+
button.setAttribute('data-saturation-mode', display.key || 'off');
|
|
723
|
+
},
|
|
724
|
+
|
|
725
|
+
ensureSkipLink() {
|
|
726
|
+
if (typeof document === 'undefined') return null;
|
|
727
|
+
if (this.skipLinkElement && document.body.contains(this.skipLinkElement)) {
|
|
728
|
+
this.updateSkipLinkLabel();
|
|
729
|
+
return this.skipLinkElement;
|
|
730
|
+
}
|
|
731
|
+
const existing = document.getElementById('acc-skip-link');
|
|
732
|
+
if (existing) {
|
|
733
|
+
this.skipLinkElement = existing;
|
|
734
|
+
if (!existing.getAttribute('data-acc-text')) {
|
|
735
|
+
existing.setAttribute('data-acc-text', 'Skip to accessibility menu');
|
|
736
|
+
}
|
|
737
|
+
this.updateSkipLinkLabel();
|
|
738
|
+
return existing;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const button = document.createElement('button');
|
|
742
|
+
button.type = 'button';
|
|
743
|
+
button.id = 'acc-skip-link';
|
|
744
|
+
button.className = 'acc-skip-link';
|
|
745
|
+
button.setAttribute('tabindex', '0');
|
|
746
|
+
button.setAttribute('data-acc-text', 'Skip to accessibility menu');
|
|
747
|
+
button.setAttribute('aria-label', this.translate('Skip to accessibility menu'));
|
|
748
|
+
button.textContent = this.translate('Skip to accessibility menu');
|
|
749
|
+
|
|
750
|
+
button.addEventListener('click', (event) => {
|
|
751
|
+
event.preventDefault();
|
|
752
|
+
const toggle = this.widgetToggleButton;
|
|
753
|
+
if (!toggle) return;
|
|
754
|
+
|
|
755
|
+
const currentMenu = this.activeMenuContainer || this.menuContainer;
|
|
756
|
+
const menuIsVisible = currentMenu && currentMenu.style.display !== 'none';
|
|
757
|
+
|
|
758
|
+
const focusMenu = () => {
|
|
759
|
+
const targetMenu = this.activeMenuContainer || this.menuContainer;
|
|
760
|
+
if (!targetMenu) return;
|
|
761
|
+
const focusables = this.getFocusableElements(targetMenu);
|
|
762
|
+
if (focusables.length) {
|
|
763
|
+
focusables[0].focus();
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
if (!menuIsVisible) {
|
|
768
|
+
toggle.click();
|
|
769
|
+
requestAnimationFrame(focusMenu);
|
|
770
|
+
} else {
|
|
771
|
+
focusMenu();
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
document.body.insertBefore(button, document.body.firstChild);
|
|
776
|
+
this.skipLinkElement = button;
|
|
777
|
+
return button;
|
|
778
|
+
},
|
|
779
|
+
|
|
780
|
+
updateSkipLinkLabel() {
|
|
781
|
+
if (!this.skipLinkElement) return;
|
|
782
|
+
const key = this.skipLinkElement.getAttribute('data-acc-text') || 'Skip to accessibility menu';
|
|
783
|
+
const label = this.translate(key);
|
|
784
|
+
this.skipLinkElement.textContent = label;
|
|
785
|
+
this.skipLinkElement.setAttribute('aria-label', label);
|
|
786
|
+
},
|
|
787
|
+
|
|
788
|
+
shouldSkipScaling(element) {
|
|
789
|
+
return element.closest('.acc-menu, .acc-container, .acc-widget');
|
|
790
|
+
},
|
|
791
|
+
|
|
792
|
+
collectDirectTextParents(rootElement = document.body) {
|
|
793
|
+
const directTextParents = new Set();
|
|
794
|
+
if (typeof document === 'undefined' || typeof NodeFilter === 'undefined') {
|
|
795
|
+
return directTextParents;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const root = rootElement instanceof Element ? rootElement : document.body;
|
|
799
|
+
if (!root) return directTextParents;
|
|
800
|
+
|
|
801
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
802
|
+
acceptNode: (node) => {
|
|
803
|
+
if (!node || !node.textContent || !node.textContent.trim()) {
|
|
804
|
+
return NodeFilter.FILTER_REJECT;
|
|
805
|
+
}
|
|
806
|
+
const parent = node.parentElement;
|
|
807
|
+
if (!parent) return NodeFilter.FILTER_REJECT;
|
|
808
|
+
if (this.shouldSkipScaling(parent)) return NodeFilter.FILTER_REJECT;
|
|
809
|
+
if (parent.closest('script,style,noscript,textarea,pre,code,svg')) {
|
|
810
|
+
return NodeFilter.FILTER_REJECT;
|
|
811
|
+
}
|
|
812
|
+
if (parent.matches(this.textScaleSelectors)) {
|
|
813
|
+
return NodeFilter.FILTER_REJECT;
|
|
814
|
+
}
|
|
815
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
let currentNode = walker.nextNode();
|
|
820
|
+
while (currentNode) {
|
|
821
|
+
const parent = currentNode.parentElement;
|
|
822
|
+
if (parent) {
|
|
823
|
+
directTextParents.add(parent);
|
|
824
|
+
}
|
|
825
|
+
currentNode = walker.nextNode();
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return directTextParents;
|
|
829
|
+
},
|
|
830
|
+
|
|
831
|
+
applyScaleToElement(element, multiplier) {
|
|
832
|
+
if (
|
|
833
|
+
!element ||
|
|
834
|
+
!(element instanceof Element) ||
|
|
835
|
+
this.shouldSkipScaling(element) ||
|
|
836
|
+
element.classList.contains('material-icons') ||
|
|
837
|
+
element.classList.contains('fa')
|
|
838
|
+
) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
const baseAttr = 'data-acc-baseSize';
|
|
842
|
+
if (!element.hasAttribute(baseAttr)) {
|
|
843
|
+
const computedSize = parseFloat(window.getComputedStyle(element).fontSize);
|
|
844
|
+
if (Number.isNaN(computedSize) || computedSize <= 0) {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
element.setAttribute(baseAttr, String(computedSize));
|
|
848
|
+
}
|
|
849
|
+
const baseSize = parseFloat(element.getAttribute(baseAttr));
|
|
850
|
+
if (Number.isNaN(baseSize) || baseSize <= 0) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
element.style.fontSize = `${baseSize * multiplier}px`;
|
|
854
|
+
},
|
|
855
|
+
|
|
856
|
+
ensureTextScaleObserver() {
|
|
857
|
+
if (this.textScaleObserver || !document.body) return;
|
|
858
|
+
this.textScaleObserver = new MutationObserver((mutations) => {
|
|
859
|
+
if (Math.abs(this.currentTextScaleMultiplier - 1) < 0.001) {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const pending = new Set();
|
|
863
|
+
mutations.forEach(mutation => {
|
|
864
|
+
mutation.addedNodes.forEach(node => {
|
|
865
|
+
if (!(node instanceof Element)) return;
|
|
866
|
+
if (node.matches && node.matches(this.textScaleSelectors)) {
|
|
867
|
+
pending.add(node);
|
|
868
|
+
}
|
|
869
|
+
node.querySelectorAll?.(this.textScaleSelectors).forEach(el => pending.add(el));
|
|
870
|
+
this.collectDirectTextParents(node).forEach(el => pending.add(el));
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
if (!pending.size) return;
|
|
874
|
+
pending.forEach(el => this.applyScaleToElement(el, this.currentTextScaleMultiplier));
|
|
875
|
+
});
|
|
876
|
+
this.textScaleObserver.observe(document.body, { childList: true, subtree: true });
|
|
877
|
+
},
|
|
878
|
+
|
|
879
|
+
disconnectTextScaleObserver() {
|
|
880
|
+
if (!this.textScaleObserver) return;
|
|
881
|
+
this.textScaleObserver.disconnect();
|
|
882
|
+
this.textScaleObserver = null;
|
|
883
|
+
},
|
|
884
|
+
|
|
885
|
+
scaleText(multiply = 1) {
|
|
886
|
+
try {
|
|
887
|
+
const numericMultiply = Number(multiply);
|
|
888
|
+
const resolvedMultiply = Number.isFinite(numericMultiply) && numericMultiply > 0 ? numericMultiply : 1;
|
|
889
|
+
const isDefaultScale = Math.abs(resolvedMultiply - 1) < 0.001;
|
|
890
|
+
this.currentTextScaleMultiplier = resolvedMultiply;
|
|
891
|
+
if (!isDefaultScale) {
|
|
892
|
+
this.ensureTextScaleObserver();
|
|
893
|
+
const elements = document.querySelectorAll(this.textScaleSelectors);
|
|
894
|
+
elements.forEach(el => this.applyScaleToElement(el, resolvedMultiply));
|
|
895
|
+
this.collectDirectTextParents(document.body).forEach(el => this.applyScaleToElement(el, resolvedMultiply));
|
|
896
|
+
} else {
|
|
897
|
+
this.disconnectTextScaleObserver();
|
|
898
|
+
const scaledElements = document.querySelectorAll('[data-acc-baseSize]');
|
|
899
|
+
scaledElements.forEach(el => {
|
|
900
|
+
if (this.shouldSkipScaling(el)) return;
|
|
901
|
+
el.style.fontSize = '';
|
|
902
|
+
el.removeAttribute('data-acc-baseSize');
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
} catch (e) {
|
|
906
|
+
console.warn('Error scaling text:', e);
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
|
|
910
|
+
clampTextScalePercent(percent) {
|
|
911
|
+
const min = Number(this.textScaleMinPercent) || 80;
|
|
912
|
+
const max = Number(this.textScaleMaxPercent) || 150;
|
|
913
|
+
const step = Number(this.textScaleStepPercent) || 5;
|
|
914
|
+
const numeric = Number(percent);
|
|
915
|
+
if (!Number.isFinite(numeric)) return 100;
|
|
916
|
+
const snapped = Math.round((numeric - min) / step) * step + min;
|
|
917
|
+
return Math.min(max, Math.max(min, snapped));
|
|
918
|
+
},
|
|
919
|
+
|
|
920
|
+
getTextScalePercent(scaleValue = 1) {
|
|
921
|
+
if (scaleValue === false || scaleValue === null || typeof scaleValue === 'undefined') {
|
|
922
|
+
return this.clampTextScalePercent(100);
|
|
923
|
+
}
|
|
924
|
+
const numeric = Number(scaleValue);
|
|
925
|
+
if (!Number.isFinite(numeric)) {
|
|
926
|
+
return this.clampTextScalePercent(100);
|
|
927
|
+
}
|
|
928
|
+
if (numeric <= 0) {
|
|
929
|
+
return this.clampTextScalePercent(100);
|
|
930
|
+
}
|
|
931
|
+
const percent = numeric > 10 ? numeric : numeric * 100;
|
|
932
|
+
return this.clampTextScalePercent(percent);
|
|
933
|
+
},
|
|
934
|
+
|
|
935
|
+
syncTextScaleControlUI(menu, scaleValue = 1) {
|
|
936
|
+
if (!menu || !menu.querySelector) return;
|
|
937
|
+
const range = menu.querySelector('.acc-text-scale-range');
|
|
938
|
+
const label = menu.querySelector('.acc-text-scale-percent');
|
|
939
|
+
const percent = this.getTextScalePercent(scaleValue);
|
|
940
|
+
if (range) {
|
|
941
|
+
const min = Number(this.textScaleMinPercent) || 80;
|
|
942
|
+
const max = Number(this.textScaleMaxPercent) || 150;
|
|
943
|
+
const progress = ((percent - min) / (max - min)) * 100;
|
|
944
|
+
range.value = String(percent);
|
|
945
|
+
range.style.setProperty('--acc-text-scale-progress', `${Math.max(0, Math.min(100, progress))}%`);
|
|
946
|
+
}
|
|
947
|
+
if (label) {
|
|
948
|
+
label.textContent = `${percent}%`;
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
|
|
952
|
+
setTextScaleFromPercent(percent, options = {}) {
|
|
953
|
+
const shouldPersist = options.persist !== false;
|
|
954
|
+
const clampedPercent = this.getTextScalePercent(percent);
|
|
955
|
+
const multiplier = Number((clampedPercent / 100).toFixed(2));
|
|
956
|
+
const exactIndex = this.textScaleValues.indexOf(multiplier);
|
|
957
|
+
|
|
958
|
+
if (this.multiLevelFeatures['text-scale']) {
|
|
959
|
+
this.multiLevelFeatures['text-scale'].currentIndex = exactIndex;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (exactIndex > -1) {
|
|
963
|
+
this.textScaleIndex = exactIndex;
|
|
964
|
+
} else {
|
|
965
|
+
let nearestIndex = 0;
|
|
966
|
+
let minDistance = Infinity;
|
|
967
|
+
this.textScaleValues.forEach((value, index) => {
|
|
968
|
+
const distance = Math.abs(value - multiplier);
|
|
969
|
+
if (distance < minDistance) {
|
|
970
|
+
minDistance = distance;
|
|
971
|
+
nearestIndex = index;
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
this.textScaleIndex = nearestIndex;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
this.scaleText(multiplier);
|
|
978
|
+
|
|
979
|
+
if (shouldPersist) {
|
|
980
|
+
this.updateState({ 'text-scale': multiplier });
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
return multiplier;
|
|
984
|
+
},
|
|
985
|
+
|
|
986
|
+
enableBoldText(enable = false) {
|
|
987
|
+
const contentSelector = [
|
|
988
|
+
'*',
|
|
989
|
+
':not(.material-icons)',
|
|
990
|
+
':not(.material-icons-outlined)',
|
|
991
|
+
':not(.material-icons-round)',
|
|
992
|
+
':not(.material-symbols-outlined)',
|
|
993
|
+
':not(.material-symbols-rounded)',
|
|
994
|
+
':not(.fa)',
|
|
995
|
+
':not(.fas)',
|
|
996
|
+
':not(.far)',
|
|
997
|
+
':not(.fab)',
|
|
998
|
+
':not(.fa-solid)',
|
|
999
|
+
':not(.fa-regular)',
|
|
1000
|
+
':not(.fa-brands)',
|
|
1001
|
+
':not(.glyphicon)',
|
|
1002
|
+
':not(.icon)',
|
|
1003
|
+
':not(.icons)',
|
|
1004
|
+
':not([class*="icon-"])',
|
|
1005
|
+
':not([data-icon])'
|
|
1006
|
+
].join('');
|
|
1007
|
+
const config = {
|
|
1008
|
+
id: "bold-text",
|
|
1009
|
+
selector: "body",
|
|
1010
|
+
childrenSelector: [contentSelector],
|
|
1011
|
+
styles: { 'font-weight': '700' },
|
|
1012
|
+
css: `
|
|
1013
|
+
.acc-container, .acc-container *, .acc-menu, .acc-menu * {
|
|
1014
|
+
font-weight: inherit !important;
|
|
1015
|
+
}
|
|
1016
|
+
input::placeholder, textarea::placeholder {
|
|
1017
|
+
font-weight: 700 !important;
|
|
1018
|
+
}
|
|
1019
|
+
`
|
|
1020
|
+
};
|
|
1021
|
+
this.applyToolStyle({ ...config, enable });
|
|
1022
|
+
},
|
|
1023
|
+
|
|
1024
|
+
adjustLetterSpacing(enable = false) {
|
|
1025
|
+
const config = {
|
|
1026
|
+
id: "letter-spacing",
|
|
1027
|
+
selector: "html",
|
|
1028
|
+
childrenSelector: ['', '*:not(.material-icons,.acc-menu,.acc-menu *, .acc-widget, .acc-widget *)'],
|
|
1029
|
+
styles: { 'letter-spacing': '2px' }
|
|
1030
|
+
};
|
|
1031
|
+
this.applyToolStyle({ ...config, enable });
|
|
1032
|
+
},
|
|
1033
|
+
|
|
1034
|
+
adjustLineSpacing(enable = false) {
|
|
1035
|
+
const config = {
|
|
1036
|
+
id: "line-spacing",
|
|
1037
|
+
selector: "html",
|
|
1038
|
+
childrenSelector: ['', '*:not(.material-icons,.acc-menu,.acc-menu *, .acc-widget, .acc-widget *)'],
|
|
1039
|
+
styles: { 'line-height': '3' }
|
|
1040
|
+
};
|
|
1041
|
+
this.applyToolStyle({ ...config, enable });
|
|
1042
|
+
},
|
|
1043
|
+
|
|
1044
|
+
enableLargePointer(enable = false) {
|
|
1045
|
+
const config = {
|
|
1046
|
+
id: "large-pointer",
|
|
1047
|
+
selector: "body",
|
|
1048
|
+
childrenSelector: ['*'],
|
|
1049
|
+
styles: {
|
|
1050
|
+
'cursor': `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='98px' height='98px' viewBox='0 0 48 48'%3E%3Cpath fill='%23FFFFFF' d='M27.8 39.7c-.1 0-.2 0-.4-.1s-.4-.3-.6-.5l-3.7-8.6-4.5 4.2c-.1.2-.3.3-.6.3-.1 0-.3 0-.4-.1-.3-.1-.6-.5-.6-.9V12c0-.4.2-.8.6-.9.1-.1.3-.1.4-.1.2 0 .5.1.7.3l16 15c.3.3.4.7.3 1.1-.1.4-.5.6-.9.7l-6.3.6 3.9 8.5c.1.2.1.5 0 .8-.1.2-.3.5-.5.6l-2.9 1.3c-.2-.2-.4-.2-.5-.2z'/%3E%3Cpath fill='%23212121' d='m18 12 16 15-7.7.7 4.5 9.8-2.9 1.3-4.3-9.9L18 34V12m0-2c-.3 0-.5.1-.8.2-.7.3-1.2 1-1.2 1.8v22c0 .8.5 1.5 1.2 1.8.3.2.6.2.8.2.5 0 1-.2 1.4-.5l3.4-3.2 3.1 7.3c.2.5.6.9 1.1 1.1.2.1.5.1.7.1.3 0 .5-.1.8-.2l2.9-1.3c.5-.2.9-.6 1.1-1.1.2-.5.2-1.1 0-1.5l-3.3-7.2 4.9-.4c.8-.1 1.5-.6 1.7-1.3.3-.7.1-1.6-.5-2.1l-16-15c-.3-.5-.8-.7-1.3-.7z'/%3E%3C/svg%3E") 40 15, auto`
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
this.applyToolStyle({ ...config, enable });
|
|
1054
|
+
},
|
|
1055
|
+
|
|
1056
|
+
highlightLinks(enable = false) {
|
|
1057
|
+
const config = {
|
|
1058
|
+
id: "highlight-links",
|
|
1059
|
+
selector: "html",
|
|
1060
|
+
childrenSelector: [
|
|
1061
|
+
"a[href]:not(.acc-menu a, .acc-widget a)",
|
|
1062
|
+
"summary.header__menu-item:not(.acc-menu summary, .acc-widget summary)",
|
|
1063
|
+
"summary.link:not(.acc-menu summary, .acc-widget summary)",
|
|
1064
|
+
"summary[role='button']:not(.acc-menu summary, .acc-widget summary)",
|
|
1065
|
+
"details > summary.list-menu__item:not(.acc-menu summary, .acc-widget summary)",
|
|
1066
|
+
".header__menu-item[role='button']:not(.acc-menu *, .acc-widget *)"
|
|
1067
|
+
],
|
|
1068
|
+
styles: { 'outline': `3px solid ${this.widgetTheme.primaryColor}`, 'outline-offset': '2px' }
|
|
1069
|
+
};
|
|
1070
|
+
this.applyToolStyle({ ...config, enable });
|
|
1071
|
+
},
|
|
1072
|
+
|
|
1073
|
+
highlightTitles(enable = false) {
|
|
1074
|
+
const config = {
|
|
1075
|
+
id: "highlight-title",
|
|
1076
|
+
selector: "html",
|
|
1077
|
+
childrenSelector: this.targetSelectors.HEADERS,
|
|
1078
|
+
styles: { 'outline': `3px solid ${this.widgetTheme.primaryColor}`, 'outline-offset': '2px' }
|
|
1079
|
+
};
|
|
1080
|
+
this.applyToolStyle({ ...config, enable });
|
|
1081
|
+
},
|
|
1082
|
+
|
|
1083
|
+
ensureReadableFontLoaded() {
|
|
1084
|
+
if (this.readableFontLoaded) return;
|
|
1085
|
+
const existing = document.getElementById('acc-readable-text-font');
|
|
1086
|
+
if (existing) {
|
|
1087
|
+
this.readableFontLoaded = true;
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
const style = document.createElement('style');
|
|
1091
|
+
style.id = 'acc-readable-text-font';
|
|
1092
|
+
style.textContent = `
|
|
1093
|
+
@font-face {
|
|
1094
|
+
font-family: "OpenDyslexic3";
|
|
1095
|
+
src: url("https://website-widgets.pages.dev/fonts/OpenDyslexic3-Regular.woff") format("woff"),
|
|
1096
|
+
url("https://website-widgets.pages.dev/fonts/OpenDyslexic3-Regular.ttf") format("truetype");
|
|
1097
|
+
font-display: swap;
|
|
1098
|
+
}
|
|
1099
|
+
`;
|
|
1100
|
+
document.head.appendChild(style);
|
|
1101
|
+
this.readableFontLoaded = true;
|
|
1102
|
+
},
|
|
1103
|
+
|
|
1104
|
+
enableReadableText(enable = false) {
|
|
1105
|
+
const shouldEnable = !!enable;
|
|
1106
|
+
if (shouldEnable) {
|
|
1107
|
+
this.ensureReadableFontLoaded();
|
|
1108
|
+
}
|
|
1109
|
+
const contentSelector = [
|
|
1110
|
+
'*',
|
|
1111
|
+
':not(.material-icons)',
|
|
1112
|
+
':not(.material-icons-outlined)',
|
|
1113
|
+
':not(.material-icons-round)',
|
|
1114
|
+
':not(.material-symbols-outlined)',
|
|
1115
|
+
':not(.material-symbols-rounded)',
|
|
1116
|
+
':not(.fa)',
|
|
1117
|
+
':not(.fas)',
|
|
1118
|
+
':not(.far)',
|
|
1119
|
+
':not(.fab)',
|
|
1120
|
+
':not(.fa-solid)',
|
|
1121
|
+
':not(.fa-regular)',
|
|
1122
|
+
':not(.fa-brands)',
|
|
1123
|
+
':not(.glyphicon)',
|
|
1124
|
+
':not(.icon)',
|
|
1125
|
+
':not(.icons)',
|
|
1126
|
+
':not([class*="icon-"])',
|
|
1127
|
+
':not([data-icon])'
|
|
1128
|
+
].join('');
|
|
1129
|
+
const config = {
|
|
1130
|
+
id: "readable-text",
|
|
1131
|
+
selector: "body",
|
|
1132
|
+
childrenSelector: [contentSelector],
|
|
1133
|
+
styles: {
|
|
1134
|
+
'font-family': '"OpenDyslexic3", "Comic Sans MS", Arial, Helvetica, sans-serif'
|
|
1135
|
+
},
|
|
1136
|
+
css: `
|
|
1137
|
+
.acc-container, .acc-container *, .acc-menu, .acc-menu * {
|
|
1138
|
+
font-family: inherit !important;
|
|
1139
|
+
}
|
|
1140
|
+
input::placeholder, textarea::placeholder {
|
|
1141
|
+
font-family: "OpenDyslexic3", "Comic Sans MS", Arial, Helvetica, sans-serif !important;
|
|
1142
|
+
}
|
|
1143
|
+
`
|
|
1144
|
+
};
|
|
1145
|
+
this.applyToolStyle({ ...config, enable: shouldEnable });
|
|
1146
|
+
},
|
|
1147
|
+
|
|
1148
|
+
pauseMotion(enable = false) {
|
|
1149
|
+
const config = {
|
|
1150
|
+
id: "pause-motion",
|
|
1151
|
+
selector: "html",
|
|
1152
|
+
childrenSelector: ['*'],
|
|
1153
|
+
styles: { 'transition': 'none', 'animation-fill-mode': 'forwards', 'animation-iteration-count': '1', 'animation-duration': '.01s' }
|
|
1154
|
+
};
|
|
1155
|
+
this.applyToolStyle({ ...config, enable });
|
|
1156
|
+
},
|
|
1157
|
+
|
|
1158
|
+
enableHighContrastMode(enable = false) {
|
|
1159
|
+
const X = ':not(.acc-container):not(.acc-container *):not(.acc-rg-container):not(.acc-rg-container *)';
|
|
1160
|
+
const config = {
|
|
1161
|
+
id: 'high-contrast-mode',
|
|
1162
|
+
css: `
|
|
1163
|
+
/* ── Base: force black on white ── */
|
|
1164
|
+
body.acc-high-contrast-mode *${X} {
|
|
1165
|
+
color: #000 !important;
|
|
1166
|
+
background-color: #fff !important;
|
|
1167
|
+
background-image: none !important;
|
|
1168
|
+
text-shadow: none !important;
|
|
1169
|
+
box-shadow: none !important;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
body.acc-high-contrast-mode {
|
|
1173
|
+
background: #fff !important;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/* ── Borders: solid and visible ── */
|
|
1177
|
+
body.acc-high-contrast-mode *${X} {
|
|
1178
|
+
border-color: #000 !important;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/* ── Links: underlined, distinct color ── */
|
|
1182
|
+
body.acc-high-contrast-mode a${X} {
|
|
1183
|
+
color: #00e !important;
|
|
1184
|
+
text-decoration: underline !important;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
body.acc-high-contrast-mode a:visited${X} {
|
|
1188
|
+
color: #551a8b !important;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/* ── Headings: bold black ── */
|
|
1192
|
+
body.acc-high-contrast-mode :where(h1,h2,h3,h4,h5,h6)${X} {
|
|
1193
|
+
color: #000 !important;
|
|
1194
|
+
font-weight: 700 !important;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/* ── Images: keep visible, add border ── */
|
|
1198
|
+
body.acc-high-contrast-mode img${X} {
|
|
1199
|
+
border: 1px solid #000 !important;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/* ── Inputs & buttons: high-contrast borders ── */
|
|
1203
|
+
body.acc-high-contrast-mode :where(input, textarea, select, button)${X} {
|
|
1204
|
+
border: 2px solid #000 !important;
|
|
1205
|
+
background: #fff !important;
|
|
1206
|
+
color: #000 !important;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/* ── Focus rings: thick, high-visibility ── */
|
|
1210
|
+
body.acc-high-contrast-mode :focus-visible${X} {
|
|
1211
|
+
outline: 3px solid #000 !important;
|
|
1212
|
+
outline-offset: 2px !important;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/* ── Tables: visible cell borders ── */
|
|
1216
|
+
body.acc-high-contrast-mode :where(table, th, td)${X} {
|
|
1217
|
+
border: 1px solid #000 !important;
|
|
1218
|
+
}
|
|
1219
|
+
`
|
|
1220
|
+
};
|
|
1221
|
+
this.applyToolStyle({ ...config, enable });
|
|
1222
|
+
document.body?.classList.toggle('acc-high-contrast-mode', !!enable);
|
|
1223
|
+
},
|
|
1224
|
+
|
|
1225
|
+
enableReadingAid(enable = false) {
|
|
1226
|
+
try {
|
|
1227
|
+
let container = this.findElement('.acc-rg-container');
|
|
1228
|
+
const guideHeight = 100; // Height of the clear reading window
|
|
1229
|
+
|
|
1230
|
+
if (enable) {
|
|
1231
|
+
// Always clean up existing listener first to prevent duplicates
|
|
1232
|
+
if (window.__accweb__scrollGuide) {
|
|
1233
|
+
document.removeEventListener('mousemove', window.__accweb__scrollGuide);
|
|
1234
|
+
delete window.__accweb__scrollGuide;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (!container) {
|
|
1238
|
+
// Create container but don't make it visible yet
|
|
1239
|
+
container = document.createElement('div');
|
|
1240
|
+
container.setAttribute('class', 'acc-rg-container');
|
|
1241
|
+
container.setAttribute('aria-hidden', 'true');
|
|
1242
|
+
container.innerHTML = this.readingAidTemplate;
|
|
1243
|
+
document.body.prepend(container);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const rgTop = container.querySelector('.acc-rg-top');
|
|
1247
|
+
const rgBottom = container.querySelector('.acc-rg-bottom');
|
|
1248
|
+
|
|
1249
|
+
// Initially hide the guide completely
|
|
1250
|
+
rgTop.style.display = 'none';
|
|
1251
|
+
rgBottom.style.display = 'none';
|
|
1252
|
+
this.readingAidVisible = false;
|
|
1253
|
+
|
|
1254
|
+
const updateGuidePosition = this.throttle((event) => {
|
|
1255
|
+
// If first mouse movement after enabling, make the guide visible
|
|
1256
|
+
if (!this.readingAidVisible) {
|
|
1257
|
+
rgTop.style.display = 'block';
|
|
1258
|
+
rgBottom.style.display = 'block';
|
|
1259
|
+
this.readingAidVisible = true;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Position the guide at the mouse Y coordinate
|
|
1263
|
+
const mouseY = event.clientY;
|
|
1264
|
+
const topHeight = Math.max(0, mouseY - guideHeight / 2);
|
|
1265
|
+
const bottomHeight = Math.max(0, window.innerHeight - (mouseY + guideHeight / 2));
|
|
1266
|
+
rgTop.style.height = `${topHeight}px`;
|
|
1267
|
+
rgBottom.style.height = `${bottomHeight}px`;
|
|
1268
|
+
}, 16);
|
|
1269
|
+
|
|
1270
|
+
window.__accweb__scrollGuide = updateGuidePosition;
|
|
1271
|
+
document.addEventListener('mousemove', updateGuidePosition, { passive: true });
|
|
1272
|
+
} else {
|
|
1273
|
+
// Reset visibility flag when disabled
|
|
1274
|
+
this.readingAidVisible = false;
|
|
1275
|
+
|
|
1276
|
+
// Clean up event listener
|
|
1277
|
+
if (window.__accweb__scrollGuide) {
|
|
1278
|
+
document.removeEventListener('mousemove', window.__accweb__scrollGuide);
|
|
1279
|
+
delete window.__accweb__scrollGuide;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Remove container
|
|
1283
|
+
if (container) {
|
|
1284
|
+
container.remove();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
} catch (e) {
|
|
1288
|
+
console.warn('Error with reading aid:', e);
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
|
|
1292
|
+
loadAxeCore() {
|
|
1293
|
+
if (this.axeCoreLoaded && window.axe) {
|
|
1294
|
+
return Promise.resolve(window.axe);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (this.axeCorePromise) {
|
|
1298
|
+
return this.axeCorePromise;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
this.axeCoreLoading = true;
|
|
1302
|
+
this.axeCorePromise = new Promise((resolve, reject) => {
|
|
1303
|
+
let script = document.querySelector('script[data-acc-axe-core="true"]');
|
|
1304
|
+
let settled = false;
|
|
1305
|
+
let timeoutId = null;
|
|
1306
|
+
|
|
1307
|
+
const settleSuccess = () => {
|
|
1308
|
+
if (settled) return;
|
|
1309
|
+
settled = true;
|
|
1310
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1311
|
+
if (!window.axe) {
|
|
1312
|
+
this.axeCoreLoading = false;
|
|
1313
|
+
this.axeCoreLoaded = false;
|
|
1314
|
+
this.axeCorePromise = null;
|
|
1315
|
+
reject(new Error('axe-core loaded but window.axe is unavailable'));
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
this.axeCoreLoading = false;
|
|
1319
|
+
this.axeCoreLoaded = true;
|
|
1320
|
+
if (script) {
|
|
1321
|
+
script.setAttribute('data-acc-axe-core-loaded', 'true');
|
|
1322
|
+
}
|
|
1323
|
+
resolve(window.axe);
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
const settleError = (error) => {
|
|
1327
|
+
if (settled) return;
|
|
1328
|
+
settled = true;
|
|
1329
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1330
|
+
this.axeCoreLoading = false;
|
|
1331
|
+
this.axeCoreLoaded = false;
|
|
1332
|
+
this.axeCorePromise = null;
|
|
1333
|
+
reject(error);
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
if (window.axe) {
|
|
1337
|
+
settleSuccess();
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
if (script && !script.src.includes(`/axe-core@${AXE_CORE_VERSION}/`)) {
|
|
1342
|
+
script.remove();
|
|
1343
|
+
script = null;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (!script) {
|
|
1347
|
+
script = document.createElement('script');
|
|
1348
|
+
script.src = AXE_CORE_SRC;
|
|
1349
|
+
script.async = true;
|
|
1350
|
+
script.integrity = AXE_CORE_INTEGRITY;
|
|
1351
|
+
script.crossOrigin = 'anonymous';
|
|
1352
|
+
script.setAttribute('data-acc-axe-core', 'true');
|
|
1353
|
+
document.head.appendChild(script);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
script.addEventListener('load', settleSuccess, { once: true });
|
|
1357
|
+
script.addEventListener('error', () => settleError(new Error('Failed to load axe-core')), { once: true });
|
|
1358
|
+
timeoutId = setTimeout(() => {
|
|
1359
|
+
settleError(new Error('Timed out loading axe-core'));
|
|
1360
|
+
}, 15000);
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
return this.axeCorePromise;
|
|
1364
|
+
},
|
|
1365
|
+
|
|
1366
|
+
getAxeRunOptions() {
|
|
1367
|
+
return { ...AXE_RUN_OPTIONS };
|
|
1368
|
+
},
|
|
1369
|
+
|
|
1370
|
+
getViolationCounts(results = {}) {
|
|
1371
|
+
const counts = { critical: 0, serious: 0, moderate: 0, minor: 0 };
|
|
1372
|
+
const violations = Array.isArray(results.violations) ? results.violations : [];
|
|
1373
|
+
violations.forEach((violation) => {
|
|
1374
|
+
const impact = violation?.impact;
|
|
1375
|
+
if (impact && counts[impact] !== undefined) {
|
|
1376
|
+
counts[impact] += Array.isArray(violation.nodes) ? violation.nodes.length : 0;
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
return counts;
|
|
1380
|
+
},
|
|
1381
|
+
|
|
1382
|
+
updateViolationBubble(results = null) {
|
|
1383
|
+
const bubble = this.violationBubble || this.findElement('.acc-violation-bubble');
|
|
1384
|
+
if (!bubble) return;
|
|
1385
|
+
|
|
1386
|
+
const counts = this.getViolationCounts(results || this.axeScanResults || {});
|
|
1387
|
+
const devMode = this.isDevMode();
|
|
1388
|
+
|
|
1389
|
+
let displayCount = 0;
|
|
1390
|
+
let severity = '';
|
|
1391
|
+
|
|
1392
|
+
if (counts.critical > 0) {
|
|
1393
|
+
displayCount = counts.critical;
|
|
1394
|
+
severity = 'critical';
|
|
1395
|
+
} else if (devMode && counts.serious > 0) {
|
|
1396
|
+
displayCount = counts.serious;
|
|
1397
|
+
severity = 'serious';
|
|
1398
|
+
} else if (devMode && counts.moderate > 0) {
|
|
1399
|
+
displayCount = counts.moderate;
|
|
1400
|
+
severity = 'moderate';
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (displayCount <= 0) {
|
|
1404
|
+
bubble.textContent = '';
|
|
1405
|
+
bubble.hidden = true;
|
|
1406
|
+
bubble.removeAttribute('data-severity');
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
bubble.hidden = false;
|
|
1411
|
+
bubble.dataset.severity = severity;
|
|
1412
|
+
bubble.textContent = displayCount > 99 ? '99+' : String(displayCount);
|
|
1413
|
+
},
|
|
1414
|
+
|
|
1415
|
+
async runBackgroundAxeScan({ force = false } = {}) {
|
|
1416
|
+
if (!force && this.axeScanResults) {
|
|
1417
|
+
this.updateViolationBubble(this.axeScanResults);
|
|
1418
|
+
return this.axeScanResults;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (!force && this.axeScanPromise) {
|
|
1422
|
+
return this.axeScanPromise;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
this.axeScanPromise = (async () => {
|
|
1426
|
+
try {
|
|
1427
|
+
const axe = await this.loadAxeCore();
|
|
1428
|
+
const results = await axe.run(document, this.getAxeRunOptions());
|
|
1429
|
+
this.axeScanResults = results;
|
|
1430
|
+
this.updateViolationBubble(results);
|
|
1431
|
+
return results;
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
this.updateViolationBubble({ violations: [] });
|
|
1434
|
+
throw error;
|
|
1435
|
+
} finally {
|
|
1436
|
+
this.axeScanPromise = null;
|
|
1437
|
+
}
|
|
1438
|
+
})();
|
|
1439
|
+
|
|
1440
|
+
return this.axeScanPromise;
|
|
1441
|
+
},
|
|
1442
|
+
|
|
1443
|
+
ensureMediaQuery(query) {
|
|
1444
|
+
if (!query || typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
if (!this.systemPreferenceMediaQueries) {
|
|
1448
|
+
this.systemPreferenceMediaQueries = {};
|
|
1449
|
+
}
|
|
1450
|
+
if (!this.systemPreferenceMediaQueries[query]) {
|
|
1451
|
+
this.systemPreferenceMediaQueries[query] = window.matchMedia(query);
|
|
1452
|
+
}
|
|
1453
|
+
return this.systemPreferenceMediaQueries[query];
|
|
1454
|
+
},
|
|
1455
|
+
|
|
1456
|
+
applySystemMotionPreference(shouldEnable) {
|
|
1457
|
+
this.loadConfig();
|
|
1458
|
+
if (this.hasExplicitStatePreference('pause-motion')) {
|
|
1459
|
+
return false;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const currentValue = !!this.retrieveState('pause-motion');
|
|
1463
|
+
if (currentValue === shouldEnable && this.isSystemControlledPreference('pause-motion')) {
|
|
1464
|
+
return false;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
this.updateState({ 'pause-motion': shouldEnable }, { source: 'system' });
|
|
1468
|
+
return true;
|
|
1469
|
+
},
|
|
1470
|
+
|
|
1471
|
+
detectSystemPreferences() {
|
|
1472
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
|
1473
|
+
|
|
1474
|
+
const reducedMotionQuery = this.ensureMediaQuery(SYSTEM_PREFERS_REDUCED_MOTION);
|
|
1475
|
+
const shouldPauseMotion = !!reducedMotionQuery?.matches;
|
|
1476
|
+
const motionChanged = this.applySystemMotionPreference(shouldPauseMotion);
|
|
1477
|
+
|
|
1478
|
+
if (motionChanged) {
|
|
1479
|
+
this.applyEnhancements();
|
|
1480
|
+
}
|
|
1481
|
+
},
|
|
1482
|
+
|
|
1483
|
+
clearSystemPreferenceListeners() {
|
|
1484
|
+
const listeners = Array.isArray(this.systemPreferenceListeners) ? this.systemPreferenceListeners : [];
|
|
1485
|
+
listeners.forEach((remove) => {
|
|
1486
|
+
if (typeof remove === 'function') {
|
|
1487
|
+
remove();
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
this.systemPreferenceListeners = [];
|
|
1491
|
+
},
|
|
1492
|
+
|
|
1493
|
+
setupMediaQueryListeners() {
|
|
1494
|
+
this.clearSystemPreferenceListeners();
|
|
1495
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
|
1496
|
+
|
|
1497
|
+
const reducedMotionQuery = this.ensureMediaQuery(SYSTEM_PREFERS_REDUCED_MOTION);
|
|
1498
|
+
|
|
1499
|
+
const listen = (mediaQuery, handler) => {
|
|
1500
|
+
if (!mediaQuery || typeof handler !== 'function') return;
|
|
1501
|
+
if (typeof mediaQuery.addEventListener === 'function') {
|
|
1502
|
+
mediaQuery.addEventListener('change', handler);
|
|
1503
|
+
this.systemPreferenceListeners.push(() => mediaQuery.removeEventListener('change', handler));
|
|
1504
|
+
} else if (typeof mediaQuery.addListener === 'function') {
|
|
1505
|
+
mediaQuery.addListener(handler);
|
|
1506
|
+
this.systemPreferenceListeners.push(() => mediaQuery.removeListener(handler));
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
|
|
1510
|
+
const onReducedMotionChange = (event) => {
|
|
1511
|
+
const changed = this.applySystemMotionPreference(!!event.matches);
|
|
1512
|
+
if (changed) {
|
|
1513
|
+
this.applyEnhancements();
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
|
|
1517
|
+
listen(reducedMotionQuery, onReducedMotionChange);
|
|
1518
|
+
},
|
|
1519
|
+
|
|
1520
|
+
async runAccessibilityReport() {
|
|
1521
|
+
// Create or get report panel
|
|
1522
|
+
let panel = this.findElement('.acc-report-panel');
|
|
1523
|
+
|
|
1524
|
+
if (!panel) {
|
|
1525
|
+
panel = this.createReportPanel();
|
|
1526
|
+
document.body.appendChild(panel);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
this.openReportPanel(panel);
|
|
1530
|
+
|
|
1531
|
+
const contentArea = this.findElement('.acc-report-content', panel);
|
|
1532
|
+
const statusArea = this.findElement('.acc-report-status', panel);
|
|
1533
|
+
|
|
1534
|
+
// Show loading state
|
|
1535
|
+
statusArea.textContent = this.translate('Loading...');
|
|
1536
|
+
contentArea.innerHTML = `<div class="acc-report-loading">${this.translate('Analyzing page...')}</div>`;
|
|
1537
|
+
|
|
1538
|
+
try {
|
|
1539
|
+
const results = await this.runBackgroundAxeScan({ force: true });
|
|
1540
|
+
|
|
1541
|
+
this.displayReportResults(panel, results);
|
|
1542
|
+
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
contentArea.innerHTML = `<div class="acc-report-error">Error: ${error.message}</div>`;
|
|
1545
|
+
statusArea.textContent = '';
|
|
1546
|
+
}
|
|
1547
|
+
},
|
|
1548
|
+
|
|
1549
|
+
getReportFocusableElements(panel) {
|
|
1550
|
+
if (!panel) return [];
|
|
1551
|
+
const dialog = this.findElement('.acc-report-dialog', panel);
|
|
1552
|
+
return this.getFocusableElements(dialog || panel);
|
|
1553
|
+
},
|
|
1554
|
+
|
|
1555
|
+
openReportPanel(panel) {
|
|
1556
|
+
if (!panel) return;
|
|
1557
|
+
|
|
1558
|
+
this.reportPreviousFocus = document.activeElement && typeof document.activeElement.focus === 'function'
|
|
1559
|
+
? document.activeElement
|
|
1560
|
+
: null;
|
|
1561
|
+
|
|
1562
|
+
panel.classList.add('acc-report-visible');
|
|
1563
|
+
panel.setAttribute('aria-hidden', 'false');
|
|
1564
|
+
|
|
1565
|
+
const dialog = this.findElement('.acc-report-dialog', panel);
|
|
1566
|
+
const focusTarget = dialog || panel;
|
|
1567
|
+
if (!focusTarget.hasAttribute('tabindex')) {
|
|
1568
|
+
focusTarget.setAttribute('tabindex', '-1');
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (this.reportKeyListener) {
|
|
1572
|
+
document.removeEventListener('keydown', this.reportKeyListener, true);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
this.reportKeyListener = (event) => {
|
|
1576
|
+
if (!panel.classList.contains('acc-report-visible')) return;
|
|
1577
|
+
|
|
1578
|
+
if (event.key === 'Escape' || event.key === 'Esc') {
|
|
1579
|
+
event.preventDefault();
|
|
1580
|
+
event.stopPropagation();
|
|
1581
|
+
this.closeReportPanel();
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if (event.key !== 'Tab') return;
|
|
1586
|
+
|
|
1587
|
+
const focusables = this.getReportFocusableElements(panel);
|
|
1588
|
+
if (!focusables.length) {
|
|
1589
|
+
event.preventDefault();
|
|
1590
|
+
focusTarget.focus();
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
const first = focusables[0];
|
|
1595
|
+
const last = focusables[focusables.length - 1];
|
|
1596
|
+
const active = document.activeElement;
|
|
1597
|
+
const outsidePanel = !panel.contains(active);
|
|
1598
|
+
|
|
1599
|
+
if (event.shiftKey) {
|
|
1600
|
+
if (active === first || outsidePanel) {
|
|
1601
|
+
event.preventDefault();
|
|
1602
|
+
last.focus();
|
|
1603
|
+
}
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
if (active === last || outsidePanel) {
|
|
1608
|
+
event.preventDefault();
|
|
1609
|
+
first.focus();
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
|
|
1613
|
+
document.addEventListener('keydown', this.reportKeyListener, true);
|
|
1614
|
+
|
|
1615
|
+
requestAnimationFrame(() => {
|
|
1616
|
+
const focusables = this.getReportFocusableElements(panel);
|
|
1617
|
+
if (focusables.length) {
|
|
1618
|
+
focusables[0].focus();
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
focusTarget.focus();
|
|
1622
|
+
});
|
|
1623
|
+
},
|
|
1624
|
+
|
|
1625
|
+
createReportPanel() {
|
|
1626
|
+
const panel = document.createElement('div');
|
|
1627
|
+
panel.className = 'acc-report-panel acc-container';
|
|
1628
|
+
panel.setAttribute('role', 'dialog');
|
|
1629
|
+
panel.setAttribute('aria-modal', 'true');
|
|
1630
|
+
panel.setAttribute('aria-hidden', 'true');
|
|
1631
|
+
panel.setAttribute('aria-labelledby', 'acc-report-title');
|
|
1632
|
+
|
|
1633
|
+
panel.innerHTML = `
|
|
1634
|
+
<div class="acc-report-overlay"></div>
|
|
1635
|
+
<div class="acc-report-dialog">
|
|
1636
|
+
<div class="acc-report-header">
|
|
1637
|
+
<h2 id="acc-report-title" class="acc-report-title">${this.translate('Accessibility Report')}</h2>
|
|
1638
|
+
<button type="button" class="acc-report-close" aria-label="${this.translate('Close')}">${this.widgetIcons.close}</button>
|
|
1639
|
+
</div>
|
|
1640
|
+
<div class="acc-report-status"></div>
|
|
1641
|
+
<div class="acc-report-content"></div>
|
|
1642
|
+
<div class="acc-report-footer">
|
|
1643
|
+
<span class="acc-report-powered">Powered by axe-core</span>
|
|
1644
|
+
</div>
|
|
1645
|
+
</div>
|
|
1646
|
+
`;
|
|
1647
|
+
|
|
1648
|
+
// Close handlers
|
|
1649
|
+
const closeBtn = panel.querySelector('.acc-report-close');
|
|
1650
|
+
const overlay = panel.querySelector('.acc-report-overlay');
|
|
1651
|
+
|
|
1652
|
+
closeBtn.addEventListener('click', () => this.closeReportPanel());
|
|
1653
|
+
overlay.addEventListener('click', () => this.closeReportPanel());
|
|
1654
|
+
|
|
1655
|
+
return panel;
|
|
1656
|
+
},
|
|
1657
|
+
|
|
1658
|
+
displayReportResults(panel, results) {
|
|
1659
|
+
const contentArea = this.findElement('.acc-report-content', panel);
|
|
1660
|
+
const statusArea = this.findElement('.acc-report-status', panel);
|
|
1661
|
+
|
|
1662
|
+
const violations = results.violations || [];
|
|
1663
|
+
const passes = results.passes || [];
|
|
1664
|
+
const incomplete = results.incomplete || [];
|
|
1665
|
+
|
|
1666
|
+
// Count by severity
|
|
1667
|
+
const counts = { critical: 0, serious: 0, moderate: 0, minor: 0 };
|
|
1668
|
+
violations.forEach(v => {
|
|
1669
|
+
if (counts[v.impact] !== undefined) {
|
|
1670
|
+
counts[v.impact] += v.nodes.length;
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
const totalViolations = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
1675
|
+
|
|
1676
|
+
statusArea.textContent = totalViolations > 0
|
|
1677
|
+
? `${totalViolations} ${this.translate('Violations Found')}`
|
|
1678
|
+
: this.translate('No Issues Found');
|
|
1679
|
+
|
|
1680
|
+
let html = `
|
|
1681
|
+
<div class="acc-report-summary">
|
|
1682
|
+
<div class="acc-report-stat critical">
|
|
1683
|
+
<span class="acc-report-stat-value">${counts.critical}</span>
|
|
1684
|
+
<span class="acc-report-stat-label">${this.translate('Critical')}</span>
|
|
1685
|
+
</div>
|
|
1686
|
+
<div class="acc-report-stat serious">
|
|
1687
|
+
<span class="acc-report-stat-value">${counts.serious}</span>
|
|
1688
|
+
<span class="acc-report-stat-label">${this.translate('Serious')}</span>
|
|
1689
|
+
</div>
|
|
1690
|
+
<div class="acc-report-stat moderate">
|
|
1691
|
+
<span class="acc-report-stat-value">${counts.moderate}</span>
|
|
1692
|
+
<span class="acc-report-stat-label">${this.translate('Moderate')}</span>
|
|
1693
|
+
</div>
|
|
1694
|
+
<div class="acc-report-stat minor">
|
|
1695
|
+
<span class="acc-report-stat-value">${counts.minor}</span>
|
|
1696
|
+
<span class="acc-report-stat-label">${this.translate('Minor')}</span>
|
|
1697
|
+
</div>
|
|
1698
|
+
<div class="acc-report-stat passed">
|
|
1699
|
+
<span class="acc-report-stat-value">${passes.length}</span>
|
|
1700
|
+
<span class="acc-report-stat-label">${this.translate('Passed Tests')}</span>
|
|
1701
|
+
</div>
|
|
1702
|
+
</div>
|
|
1703
|
+
`;
|
|
1704
|
+
|
|
1705
|
+
if (violations.length === 0) {
|
|
1706
|
+
html += `
|
|
1707
|
+
<div class="acc-report-success">
|
|
1708
|
+
<div class="acc-report-success-icon">
|
|
1709
|
+
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>
|
|
1710
|
+
</div>
|
|
1711
|
+
<h3>${this.translate('No Issues Found')}</h3>
|
|
1712
|
+
</div>
|
|
1713
|
+
`;
|
|
1714
|
+
} else {
|
|
1715
|
+
html += `<div class="acc-report-section">
|
|
1716
|
+
<div class="acc-report-section-title">${this.translate('Violations Found')} (${violations.length})</div>
|
|
1717
|
+
`;
|
|
1718
|
+
|
|
1719
|
+
// Sort by severity
|
|
1720
|
+
const severityOrder = { critical: 0, serious: 1, moderate: 2, minor: 3 };
|
|
1721
|
+
violations.sort((a, b) => (severityOrder[a.impact] || 4) - (severityOrder[b.impact] || 4));
|
|
1722
|
+
|
|
1723
|
+
violations.forEach((violation, index) => {
|
|
1724
|
+
html += `
|
|
1725
|
+
<div class="acc-report-violation" data-index="${index}">
|
|
1726
|
+
<div class="acc-report-violation-header">
|
|
1727
|
+
<span class="acc-report-violation-impact ${violation.impact}">${this.translate(this.capitalizeFirst(violation.impact))}</span>
|
|
1728
|
+
<span class="acc-report-violation-title">${this.escapeHtml(violation.help)}</span>
|
|
1729
|
+
<span class="acc-report-violation-count">${violation.nodes.length} ${this.translate('Element')}${violation.nodes.length > 1 ? 's' : ''}</span>
|
|
1730
|
+
</div>
|
|
1731
|
+
<div class="acc-report-violation-details">
|
|
1732
|
+
<p class="acc-report-violation-desc">${this.escapeHtml(violation.description)}</p>
|
|
1733
|
+
<p class="acc-report-violation-help">
|
|
1734
|
+
<a href="${violation.helpUrl}" target="_blank" rel="noopener">${this.translate('How to Fix')} →</a>
|
|
1735
|
+
</p>
|
|
1736
|
+
${violation.nodes.slice(0, 5).map(node => `
|
|
1737
|
+
<div class="acc-report-node">
|
|
1738
|
+
<div class="acc-report-node-html">${this.escapeHtml(node.html)}</div>
|
|
1739
|
+
${node.failureSummary ? `<div class="acc-report-node-fix"><strong>${this.translate('Issue')}:</strong> ${this.escapeHtml(node.failureSummary)}</div>` : ''}
|
|
1740
|
+
</div>
|
|
1741
|
+
`).join('')}
|
|
1742
|
+
${violation.nodes.length > 5 ? `<p style="color:#666;font-size:13px;margin-top:12px;">... and ${violation.nodes.length - 5} more elements</p>` : ''}
|
|
1743
|
+
</div>
|
|
1744
|
+
</div>
|
|
1745
|
+
`;
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
html += `</div>`;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
if (incomplete.length > 0) {
|
|
1752
|
+
html += `
|
|
1753
|
+
<div class="acc-report-section">
|
|
1754
|
+
<div class="acc-report-section-title">${this.translate('Items Need Review')} (${incomplete.length})</div>
|
|
1755
|
+
<p style="color:#666;font-size:14px;">These items require manual verification.</p>
|
|
1756
|
+
</div>
|
|
1757
|
+
`;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
contentArea.innerHTML = html;
|
|
1761
|
+
|
|
1762
|
+
// Add click handlers for expandable violations
|
|
1763
|
+
contentArea.querySelectorAll('.acc-report-violation-header').forEach(header => {
|
|
1764
|
+
header.addEventListener('click', () => {
|
|
1765
|
+
header.parentElement.classList.toggle('expanded');
|
|
1766
|
+
});
|
|
1767
|
+
});
|
|
1768
|
+
},
|
|
1769
|
+
|
|
1770
|
+
capitalizeFirst(str) {
|
|
1771
|
+
if (!str) return '';
|
|
1772
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1773
|
+
},
|
|
1774
|
+
|
|
1775
|
+
escapeHtml(str) {
|
|
1776
|
+
if (!str) return '';
|
|
1777
|
+
const div = document.createElement('div');
|
|
1778
|
+
div.textContent = str;
|
|
1779
|
+
return div.innerHTML;
|
|
1780
|
+
},
|
|
1781
|
+
|
|
1782
|
+
closeReportPanel() {
|
|
1783
|
+
const panel = this.findElement('.acc-report-panel');
|
|
1784
|
+
if (panel) {
|
|
1785
|
+
panel.classList.remove('acc-report-visible');
|
|
1786
|
+
panel.setAttribute('aria-hidden', 'true');
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
if (this.reportKeyListener) {
|
|
1790
|
+
document.removeEventListener('keydown', this.reportKeyListener, true);
|
|
1791
|
+
this.reportKeyListener = null;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
const focusTarget = this.reportPreviousFocus;
|
|
1795
|
+
this.reportPreviousFocus = null;
|
|
1796
|
+
if (focusTarget && typeof focusTarget.focus === 'function') {
|
|
1797
|
+
focusTarget.focus();
|
|
1798
|
+
}
|
|
1799
|
+
},
|
|
1800
|
+
|
|
1801
|
+
concealImages(enable = false) {
|
|
1802
|
+
const styleId = `acc-hide-images`;
|
|
1803
|
+
const existingStyle = document.getElementById(styleId);
|
|
1804
|
+
if (existingStyle) existingStyle.remove();
|
|
1805
|
+
document.documentElement.classList.toggle(styleId, enable);
|
|
1806
|
+
if (enable) {
|
|
1807
|
+
const css = `
|
|
1808
|
+
body > *:not(.acc-container) img,
|
|
1809
|
+
body > *:not(.acc-container) picture,
|
|
1810
|
+
body > *:not(.acc-container) svg:not(.acc-container svg),
|
|
1811
|
+
body > *:not(.acc-container) video,
|
|
1812
|
+
body > *:not(.acc-container) iframe,
|
|
1813
|
+
body > *:not(.acc-container) canvas,
|
|
1814
|
+
body > *:not(.acc-container) .video,
|
|
1815
|
+
body > *:not(.acc-container) .image {
|
|
1816
|
+
display: none !important;
|
|
1817
|
+
}
|
|
1818
|
+
`;
|
|
1819
|
+
this.injectStyle(styleId, css);
|
|
1820
|
+
}
|
|
1821
|
+
},
|
|
1822
|
+
|
|
1823
|
+
supportsSpeechSynthesis() {
|
|
1824
|
+
if (typeof window === 'undefined') return false;
|
|
1825
|
+
return (
|
|
1826
|
+
'speechSynthesis' in window &&
|
|
1827
|
+
typeof window.SpeechSynthesisUtterance !== 'undefined'
|
|
1828
|
+
);
|
|
1829
|
+
},
|
|
1830
|
+
|
|
1831
|
+
supportsTextToSpeech() {
|
|
1832
|
+
return this.supportsSpeechSynthesis();
|
|
1833
|
+
},
|
|
1834
|
+
|
|
1835
|
+
normalizeSpeechLanguage(languageCode = 'en') {
|
|
1836
|
+
const code = String(languageCode || 'en').toLowerCase();
|
|
1837
|
+
const languageMap = {
|
|
1838
|
+
en: 'en-US',
|
|
1839
|
+
ne: 'ne-NP'
|
|
1840
|
+
};
|
|
1841
|
+
return languageMap[code] || code;
|
|
1842
|
+
},
|
|
1843
|
+
|
|
1844
|
+
getNativeTtsRate() {
|
|
1845
|
+
const configured = Number(this.nativeTtsConfig?.rate);
|
|
1846
|
+
if (!Number.isFinite(configured)) return 1;
|
|
1847
|
+
return Math.min(2, Math.max(0.5, configured));
|
|
1848
|
+
},
|
|
1849
|
+
|
|
1850
|
+
getNativeTtsPitch() {
|
|
1851
|
+
const configured = Number(this.nativeTtsConfig?.pitch);
|
|
1852
|
+
if (!Number.isFinite(configured)) return 1;
|
|
1853
|
+
return Math.min(2, Math.max(0, configured));
|
|
1854
|
+
},
|
|
1855
|
+
|
|
1856
|
+
isElementVisibleForTts(element) {
|
|
1857
|
+
if (!(element instanceof Element)) return false;
|
|
1858
|
+
const style = window.getComputedStyle(element);
|
|
1859
|
+
if (style.display === 'none' || style.visibility === 'hidden') {
|
|
1860
|
+
return false;
|
|
1861
|
+
}
|
|
1862
|
+
const rect = element.getBoundingClientRect();
|
|
1863
|
+
return rect.width > 0 && rect.height > 0;
|
|
1864
|
+
},
|
|
1865
|
+
|
|
1866
|
+
isTtsExcludedElement(element, { allowLandmarkRegions = false } = {}) {
|
|
1867
|
+
if (!(element instanceof Element)) return true;
|
|
1868
|
+
if (element.closest('.acc-container')) return true;
|
|
1869
|
+
if (element.closest('script,style,noscript,template')) return true;
|
|
1870
|
+
if (element.closest('[aria-hidden="true"]')) return true;
|
|
1871
|
+
if (
|
|
1872
|
+
!allowLandmarkRegions &&
|
|
1873
|
+
element.closest(
|
|
1874
|
+
'nav,header,footer,aside,form,dialog,[role="navigation"],[role="complementary"],[role="search"],[role="menu"],[role="dialog"],[role="alert"],[aria-live]'
|
|
1875
|
+
)
|
|
1876
|
+
) {
|
|
1877
|
+
return true;
|
|
1878
|
+
}
|
|
1879
|
+
return false;
|
|
1880
|
+
},
|
|
1881
|
+
|
|
1882
|
+
normalizeReadableText(text = '') {
|
|
1883
|
+
return String(text)
|
|
1884
|
+
.replace(/\s+/g, ' ')
|
|
1885
|
+
.replace(/[ \t]+([,.;!?])/g, '$1')
|
|
1886
|
+
.trim();
|
|
1887
|
+
},
|
|
1888
|
+
|
|
1889
|
+
getTtsCandidateRoots() {
|
|
1890
|
+
const selectors = [
|
|
1891
|
+
'article',
|
|
1892
|
+
'main',
|
|
1893
|
+
'[role="main"]',
|
|
1894
|
+
'.content',
|
|
1895
|
+
'.post',
|
|
1896
|
+
'.entry-content',
|
|
1897
|
+
'#content'
|
|
1898
|
+
];
|
|
1899
|
+
const roots = [];
|
|
1900
|
+
const seen = new Set();
|
|
1901
|
+
|
|
1902
|
+
selectors.forEach((selector) => {
|
|
1903
|
+
document.querySelectorAll(selector).forEach((element) => {
|
|
1904
|
+
if (!(element instanceof Element)) return;
|
|
1905
|
+
if (this.isTtsExcludedElement(element) || !this.isElementVisibleForTts(element)) return;
|
|
1906
|
+
if (seen.has(element)) return;
|
|
1907
|
+
seen.add(element);
|
|
1908
|
+
roots.push(element);
|
|
1909
|
+
});
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
return roots;
|
|
1913
|
+
},
|
|
1914
|
+
|
|
1915
|
+
getPrimaryContentRoot() {
|
|
1916
|
+
if (typeof document === 'undefined') return null;
|
|
1917
|
+
const candidates = this.getTtsCandidateRoots();
|
|
1918
|
+
if (!candidates.length) {
|
|
1919
|
+
const explicitCandidates = Array.from(
|
|
1920
|
+
document.querySelectorAll('main,article,[role="main"],#content,.content,.post,.entry-content')
|
|
1921
|
+
).filter((element) =>
|
|
1922
|
+
element instanceof Element &&
|
|
1923
|
+
!this.isTtsExcludedElement(element) &&
|
|
1924
|
+
this.isElementVisibleForTts(element)
|
|
1925
|
+
);
|
|
1926
|
+
|
|
1927
|
+
if (explicitCandidates.length) {
|
|
1928
|
+
let explicitBest = explicitCandidates[0];
|
|
1929
|
+
let explicitBestScore = -1;
|
|
1930
|
+
explicitCandidates.forEach((candidate) => {
|
|
1931
|
+
const textLength = this.normalizeReadableText(candidate.innerText || candidate.textContent || '').length;
|
|
1932
|
+
const rect = candidate.getBoundingClientRect();
|
|
1933
|
+
const score = textLength + (rect.width * rect.height * 0.0025);
|
|
1934
|
+
if (score > explicitBestScore) {
|
|
1935
|
+
explicitBestScore = score;
|
|
1936
|
+
explicitBest = candidate;
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
return explicitBest;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
if (document.body) {
|
|
1943
|
+
const topLevelCandidates = Array.from(document.body.children).filter((element) =>
|
|
1944
|
+
element instanceof Element &&
|
|
1945
|
+
!element.classList.contains('acc-container') &&
|
|
1946
|
+
this.isElementVisibleForTts(element)
|
|
1947
|
+
);
|
|
1948
|
+
|
|
1949
|
+
if (topLevelCandidates.length) {
|
|
1950
|
+
let best = topLevelCandidates[0];
|
|
1951
|
+
let bestScore = -1;
|
|
1952
|
+
topLevelCandidates.forEach((candidate) => {
|
|
1953
|
+
const textLength = this.normalizeReadableText(candidate.innerText || candidate.textContent || '').length;
|
|
1954
|
+
const rect = candidate.getBoundingClientRect();
|
|
1955
|
+
const score = textLength + (rect.width * rect.height * 0.0025);
|
|
1956
|
+
if (score > bestScore) {
|
|
1957
|
+
bestScore = score;
|
|
1958
|
+
best = candidate;
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
return best;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
return document.body;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
let bestRoot = candidates[0];
|
|
1969
|
+
let bestScore = -1;
|
|
1970
|
+
candidates.forEach((candidate) => {
|
|
1971
|
+
const blocks = this.extractReadableBlocks(candidate);
|
|
1972
|
+
const score = blocks.join(' ').length;
|
|
1973
|
+
if (score > bestScore) {
|
|
1974
|
+
bestScore = score;
|
|
1975
|
+
bestRoot = candidate;
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
return bestRoot;
|
|
1979
|
+
},
|
|
1980
|
+
|
|
1981
|
+
extractReadableBlocks(root) {
|
|
1982
|
+
if (!(root instanceof Element)) return [];
|
|
1983
|
+
const blockSelector = 'h1,h2,h3,h4,h5,h6,p,li,dt,dd,blockquote,figcaption,caption,th,td';
|
|
1984
|
+
const blocks = [];
|
|
1985
|
+
root.querySelectorAll(blockSelector).forEach((element) => {
|
|
1986
|
+
if (!(element instanceof Element)) return;
|
|
1987
|
+
if (this.isTtsExcludedElement(element) || !this.isElementVisibleForTts(element)) return;
|
|
1988
|
+
const text = this.normalizeReadableText(element.innerText || element.textContent || '');
|
|
1989
|
+
if (text.length < 15) return;
|
|
1990
|
+
blocks.push(text);
|
|
1991
|
+
});
|
|
1992
|
+
return blocks;
|
|
1993
|
+
},
|
|
1994
|
+
|
|
1995
|
+
getReadableContent() {
|
|
1996
|
+
if (typeof document === 'undefined') return '';
|
|
1997
|
+
const candidateRoots = this.getTtsCandidateRoots();
|
|
1998
|
+
let bestBlocks = [];
|
|
1999
|
+
let bestScore = -1;
|
|
2000
|
+
|
|
2001
|
+
candidateRoots.forEach((root) => {
|
|
2002
|
+
const blocks = this.extractReadableBlocks(root);
|
|
2003
|
+
if (!blocks.length) return;
|
|
2004
|
+
const characterCount = blocks.join(' ').length;
|
|
2005
|
+
const score = (blocks.length * 60) + characterCount;
|
|
2006
|
+
if (score > bestScore) {
|
|
2007
|
+
bestScore = score;
|
|
2008
|
+
bestBlocks = blocks;
|
|
2009
|
+
}
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2012
|
+
if (!bestBlocks.length && document.body) {
|
|
2013
|
+
bestBlocks = this.extractReadableBlocks(document.body);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
if (!bestBlocks.length && document.body) {
|
|
2017
|
+
const fallback = this.normalizeReadableText(document.body.innerText || document.body.textContent || '');
|
|
2018
|
+
return fallback.slice(0, 30000);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
return bestBlocks.join('\n\n').slice(0, 30000);
|
|
2022
|
+
},
|
|
2023
|
+
|
|
2024
|
+
splitLongSpeechSegment(segment, maxLength = 240) {
|
|
2025
|
+
if (!segment) return [];
|
|
2026
|
+
if (segment.length <= maxLength) return [segment];
|
|
2027
|
+
|
|
2028
|
+
const parts = [];
|
|
2029
|
+
let remaining = segment;
|
|
2030
|
+
while (remaining.length > maxLength) {
|
|
2031
|
+
let splitIndex = remaining.lastIndexOf(',', maxLength);
|
|
2032
|
+
if (splitIndex < Math.floor(maxLength * 0.5)) {
|
|
2033
|
+
splitIndex = remaining.lastIndexOf(' ', maxLength);
|
|
2034
|
+
}
|
|
2035
|
+
if (splitIndex < Math.floor(maxLength * 0.5)) {
|
|
2036
|
+
splitIndex = maxLength;
|
|
2037
|
+
}
|
|
2038
|
+
parts.push(remaining.slice(0, splitIndex).trim());
|
|
2039
|
+
remaining = remaining.slice(splitIndex).trim();
|
|
2040
|
+
}
|
|
2041
|
+
if (remaining.length) {
|
|
2042
|
+
parts.push(remaining);
|
|
2043
|
+
}
|
|
2044
|
+
return parts.filter(Boolean);
|
|
2045
|
+
},
|
|
2046
|
+
|
|
2047
|
+
buildSpeechQueue(text = '') {
|
|
2048
|
+
const normalized = this.normalizeReadableText(text);
|
|
2049
|
+
if (!normalized) return [];
|
|
2050
|
+
|
|
2051
|
+
const paragraphCandidates = text
|
|
2052
|
+
.split(/\n{2,}/)
|
|
2053
|
+
.map((paragraph) => this.normalizeReadableText(paragraph))
|
|
2054
|
+
.filter(Boolean);
|
|
2055
|
+
|
|
2056
|
+
const paragraphs = paragraphCandidates.length ? paragraphCandidates : [normalized];
|
|
2057
|
+
const queue = [];
|
|
2058
|
+
|
|
2059
|
+
paragraphs.forEach((paragraph) => {
|
|
2060
|
+
const sentences = paragraph.split(/(?<=[.!?])\s+/).filter(Boolean);
|
|
2061
|
+
const sentenceList = sentences.length ? sentences : [paragraph];
|
|
2062
|
+
let chunk = '';
|
|
2063
|
+
|
|
2064
|
+
sentenceList.forEach((sentence) => {
|
|
2065
|
+
const next = chunk ? `${chunk} ${sentence}` : sentence;
|
|
2066
|
+
if (next.length <= 240) {
|
|
2067
|
+
chunk = next;
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
if (chunk) {
|
|
2071
|
+
queue.push(...this.splitLongSpeechSegment(chunk, 240));
|
|
2072
|
+
}
|
|
2073
|
+
chunk = sentence;
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
if (chunk) {
|
|
2077
|
+
queue.push(...this.splitLongSpeechSegment(chunk, 240));
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
|
|
2081
|
+
return queue.slice(0, 300);
|
|
2082
|
+
},
|
|
2083
|
+
|
|
2084
|
+
resolveSpeechVoice(languageCode) {
|
|
2085
|
+
if (!this.supportsSpeechSynthesis()) return null;
|
|
2086
|
+
const synth = window.speechSynthesis;
|
|
2087
|
+
const voices = synth.getVoices?.() || [];
|
|
2088
|
+
if (!voices.length) return null;
|
|
2089
|
+
|
|
2090
|
+
const preferredVoiceName = String(this.nativeTtsConfig?.preferredVoiceName || '').trim().toLowerCase();
|
|
2091
|
+
if (preferredVoiceName) {
|
|
2092
|
+
const byExactName = voices.find((voice) => String(voice.name || '').trim().toLowerCase() === preferredVoiceName);
|
|
2093
|
+
if (byExactName) return byExactName;
|
|
2094
|
+
const byNameContains = voices.find((voice) => String(voice.name || '').toLowerCase().includes(preferredVoiceName));
|
|
2095
|
+
if (byNameContains) return byNameContains;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
const configuredVoiceLang = String(this.nativeTtsConfig?.preferredVoiceLang || '').trim();
|
|
2099
|
+
const targetLanguage = configuredVoiceLang || languageCode;
|
|
2100
|
+
const normalized = this.normalizeSpeechLanguage(targetLanguage).toLowerCase();
|
|
2101
|
+
const primary = normalized.split('-')[0];
|
|
2102
|
+
const localExact = voices.find((voice) => (
|
|
2103
|
+
voice.localService &&
|
|
2104
|
+
String(voice.lang || '').toLowerCase() === normalized
|
|
2105
|
+
));
|
|
2106
|
+
const exact = voices.find((voice) => String(voice.lang || '').toLowerCase() === normalized);
|
|
2107
|
+
const localByPrimary = voices.find((voice) => (
|
|
2108
|
+
voice.localService &&
|
|
2109
|
+
String(voice.lang || '').toLowerCase().startsWith(primary)
|
|
2110
|
+
));
|
|
2111
|
+
const byPrimary = voices.find((voice) => String(voice.lang || '').toLowerCase().startsWith(primary));
|
|
2112
|
+
const defaultVoice = voices.find((voice) => voice.default);
|
|
2113
|
+
return localExact || exact || localByPrimary || byPrimary || defaultVoice || voices[0];
|
|
2114
|
+
},
|
|
2115
|
+
|
|
2116
|
+
getSpeechTargetFromEvent(event) {
|
|
2117
|
+
const target = event?.target;
|
|
2118
|
+
if (!(target instanceof Element)) return null;
|
|
2119
|
+
if (target.closest('.acc-container')) return null;
|
|
2120
|
+
|
|
2121
|
+
const block = target.closest('h1,h2,h3,h4,h5,h6,p,li,dt,dd,blockquote,figcaption,caption,th,td,div,section');
|
|
2122
|
+
if (!(block instanceof Element)) return null;
|
|
2123
|
+
if (this.isTtsExcludedElement(block, { allowLandmarkRegions: true }) || !this.isElementVisibleForTts(block)) return null;
|
|
2124
|
+
|
|
2125
|
+
const text = this.normalizeReadableText(block.innerText || block.textContent || '');
|
|
2126
|
+
if (text.length < 2) return null;
|
|
2127
|
+
|
|
2128
|
+
return {
|
|
2129
|
+
element: block,
|
|
2130
|
+
text: text.slice(0, 30000)
|
|
2131
|
+
};
|
|
2132
|
+
},
|
|
2133
|
+
|
|
2134
|
+
setActiveSpeechTarget(element = null) {
|
|
2135
|
+
if (this.ttsActiveTarget && this.ttsActiveTarget !== element) {
|
|
2136
|
+
this.ttsActiveTarget.classList.remove('acc-tts-active-block');
|
|
2137
|
+
}
|
|
2138
|
+
this.ttsActiveTarget = element;
|
|
2139
|
+
if (this.ttsActiveTarget) {
|
|
2140
|
+
this.ttsActiveTarget.classList.add('acc-tts-active-block');
|
|
2141
|
+
}
|
|
2142
|
+
},
|
|
2143
|
+
|
|
2144
|
+
startTtsClickMode() {
|
|
2145
|
+
if (typeof document === 'undefined' || this.ttsClickListener) return;
|
|
2146
|
+
this.ttsClickListener = (event) => {
|
|
2147
|
+
this.handleTtsClick(event);
|
|
2148
|
+
};
|
|
2149
|
+
document.addEventListener('click', this.ttsClickListener, true);
|
|
2150
|
+
document.body?.classList.add('acc-tts-click-mode');
|
|
2151
|
+
},
|
|
2152
|
+
|
|
2153
|
+
stopTtsClickMode() {
|
|
2154
|
+
if (typeof document === 'undefined') return;
|
|
2155
|
+
if (this.ttsClickListener) {
|
|
2156
|
+
document.removeEventListener('click', this.ttsClickListener, true);
|
|
2157
|
+
this.ttsClickListener = null;
|
|
2158
|
+
}
|
|
2159
|
+
document.body?.classList.remove('acc-tts-click-mode');
|
|
2160
|
+
this.setActiveSpeechTarget(null);
|
|
2161
|
+
},
|
|
2162
|
+
|
|
2163
|
+
handleTtsClick(event) {
|
|
2164
|
+
if (!this.retrieveState('text-to-speech')) return;
|
|
2165
|
+
const target = this.getSpeechTargetFromEvent(event);
|
|
2166
|
+
if (!target) return;
|
|
2167
|
+
|
|
2168
|
+
this.setActiveSpeechTarget(target.element);
|
|
2169
|
+
this.ttsTextCache = target.text;
|
|
2170
|
+
this.startSpeechPlayback({ restart: true });
|
|
2171
|
+
},
|
|
2172
|
+
|
|
2173
|
+
ensureTtsQueue() {
|
|
2174
|
+
const text = this.ttsTextCache || this.getReadableContent();
|
|
2175
|
+
if (!text) {
|
|
2176
|
+
this.ttsQueue = [];
|
|
2177
|
+
this.ttsQueueIndex = 0;
|
|
2178
|
+
return false;
|
|
2179
|
+
}
|
|
2180
|
+
this.ttsTextCache = text;
|
|
2181
|
+
this.ttsQueue = this.buildSpeechQueue(text);
|
|
2182
|
+
this.ttsQueueIndex = 0;
|
|
2183
|
+
return this.ttsQueue.length > 0;
|
|
2184
|
+
},
|
|
2185
|
+
|
|
2186
|
+
speakNextTtsChunk(sessionId) {
|
|
2187
|
+
if (!this.supportsSpeechSynthesis()) return;
|
|
2188
|
+
if (sessionId !== this.ttsSessionId) return;
|
|
2189
|
+
|
|
2190
|
+
if (this.ttsQueueIndex >= this.ttsQueue.length) {
|
|
2191
|
+
this.ttsStatus = 'stopped';
|
|
2192
|
+
this.setActiveSpeechTarget(null);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
const synth = window.speechSynthesis;
|
|
2197
|
+
const chunk = this.ttsQueue[this.ttsQueueIndex];
|
|
2198
|
+
if (!chunk) {
|
|
2199
|
+
this.ttsQueueIndex += 1;
|
|
2200
|
+
this.speakNextTtsChunk(sessionId);
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
const utterance = new window.SpeechSynthesisUtterance(chunk);
|
|
2205
|
+
utterance.lang = this.normalizeSpeechLanguage(this.loadConfig().lang || 'en');
|
|
2206
|
+
if (!this.ttsVoice || this.ttsVoice.lang?.toLowerCase() !== utterance.lang.toLowerCase()) {
|
|
2207
|
+
this.ttsVoice = this.resolveSpeechVoice(utterance.lang);
|
|
2208
|
+
}
|
|
2209
|
+
if (this.ttsVoice) {
|
|
2210
|
+
utterance.voice = this.ttsVoice;
|
|
2211
|
+
}
|
|
2212
|
+
utterance.rate = this.getNativeTtsRate();
|
|
2213
|
+
utterance.pitch = this.getNativeTtsPitch();
|
|
2214
|
+
|
|
2215
|
+
utterance.onstart = () => {
|
|
2216
|
+
if (sessionId !== this.ttsSessionId) return;
|
|
2217
|
+
this.ttsStatus = 'reading';
|
|
2218
|
+
};
|
|
2219
|
+
utterance.onpause = () => {
|
|
2220
|
+
if (sessionId !== this.ttsSessionId) return;
|
|
2221
|
+
this.ttsStatus = 'paused';
|
|
2222
|
+
};
|
|
2223
|
+
utterance.onresume = () => {
|
|
2224
|
+
if (sessionId !== this.ttsSessionId) return;
|
|
2225
|
+
this.ttsStatus = 'reading';
|
|
2226
|
+
};
|
|
2227
|
+
utterance.onend = () => {
|
|
2228
|
+
if (sessionId !== this.ttsSessionId) return;
|
|
2229
|
+
this.ttsQueueIndex += 1;
|
|
2230
|
+
this.speakNextTtsChunk(sessionId);
|
|
2231
|
+
};
|
|
2232
|
+
utterance.onerror = () => {
|
|
2233
|
+
if (sessionId !== this.ttsSessionId) return;
|
|
2234
|
+
this.ttsStatus = 'stopped';
|
|
2235
|
+
this.setActiveSpeechTarget(null);
|
|
2236
|
+
};
|
|
2237
|
+
|
|
2238
|
+
this.ttsUtterance = utterance;
|
|
2239
|
+
synth.speak(utterance);
|
|
2240
|
+
},
|
|
2241
|
+
|
|
2242
|
+
startNativeSpeechPlayback({ restart = false } = {}) {
|
|
2243
|
+
if (!this.supportsSpeechSynthesis()) return;
|
|
2244
|
+
const synth = window.speechSynthesis;
|
|
2245
|
+
|
|
2246
|
+
if (!restart && synth.paused) {
|
|
2247
|
+
synth.resume();
|
|
2248
|
+
this.ttsStatus = 'reading';
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
const hasQueue = restart || !this.ttsQueue.length || this.ttsQueueIndex >= this.ttsQueue.length
|
|
2253
|
+
? this.ensureTtsQueue()
|
|
2254
|
+
: true;
|
|
2255
|
+
if (!hasQueue) {
|
|
2256
|
+
this.stopSpeech();
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
const sessionId = this.ttsSessionId + 1;
|
|
2261
|
+
this.ttsSessionId = sessionId;
|
|
2262
|
+
synth.cancel();
|
|
2263
|
+
this.ttsStatus = 'reading';
|
|
2264
|
+
this.speakNextTtsChunk(sessionId);
|
|
2265
|
+
},
|
|
2266
|
+
|
|
2267
|
+
startSpeechPlayback({ restart = false } = {}) {
|
|
2268
|
+
this.startNativeSpeechPlayback({ restart });
|
|
2269
|
+
},
|
|
2270
|
+
|
|
2271
|
+
pauseSpeech() {
|
|
2272
|
+
if (!this.supportsSpeechSynthesis()) return;
|
|
2273
|
+
const synth = window.speechSynthesis;
|
|
2274
|
+
if (synth.speaking && !synth.paused) {
|
|
2275
|
+
synth.pause();
|
|
2276
|
+
this.ttsStatus = 'paused';
|
|
2277
|
+
}
|
|
2278
|
+
},
|
|
2279
|
+
|
|
2280
|
+
resumeSpeech() {
|
|
2281
|
+
if (!this.supportsSpeechSynthesis()) return;
|
|
2282
|
+
const synth = window.speechSynthesis;
|
|
2283
|
+
if (synth.paused) {
|
|
2284
|
+
synth.resume();
|
|
2285
|
+
this.ttsStatus = 'reading';
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
this.startSpeechPlayback({ restart: false });
|
|
2289
|
+
},
|
|
2290
|
+
|
|
2291
|
+
stopSpeech() {
|
|
2292
|
+
const synth = this.supportsSpeechSynthesis() ? window.speechSynthesis : null;
|
|
2293
|
+
this.ttsSessionId += 1;
|
|
2294
|
+
if (synth && (synth.speaking || synth.paused)) {
|
|
2295
|
+
synth.cancel();
|
|
2296
|
+
}
|
|
2297
|
+
this.ttsUtterance = null;
|
|
2298
|
+
this.ttsQueueIndex = 0;
|
|
2299
|
+
this.ttsStatus = 'stopped';
|
|
2300
|
+
this.setActiveSpeechTarget(null);
|
|
2301
|
+
},
|
|
2302
|
+
|
|
2303
|
+
enableTextToSpeech(enable = false) {
|
|
2304
|
+
if (!this.supportsTextToSpeech()) return;
|
|
2305
|
+
const isUserToggle = this.userInitiatedToggleKey === 'text-to-speech';
|
|
2306
|
+
|
|
2307
|
+
if (!enable) {
|
|
2308
|
+
if (isUserToggle) this.announceTtsState(false);
|
|
2309
|
+
this.stopSpeech();
|
|
2310
|
+
this.stopTtsClickMode();
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
const synth = window.speechSynthesis;
|
|
2315
|
+
if (synth?.getVoices) {
|
|
2316
|
+
synth.getVoices();
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
this.startTtsClickMode();
|
|
2320
|
+
this.ttsStatus = 'idle';
|
|
2321
|
+
if (isUserToggle) this.announceTtsState(true);
|
|
2322
|
+
},
|
|
2323
|
+
|
|
2324
|
+
announceTtsState(active) {
|
|
2325
|
+
if (!this.supportsTextToSpeech()) return;
|
|
2326
|
+
const synth = window.speechSynthesis;
|
|
2327
|
+
if (!synth) return;
|
|
2328
|
+
const msg = active
|
|
2329
|
+
? this.translate('Text to Speech On')
|
|
2330
|
+
: this.translate('Text to Speech Off');
|
|
2331
|
+
synth.cancel();
|
|
2332
|
+
const utterance = new SpeechSynthesisUtterance(msg);
|
|
2333
|
+
const lang = this.normalizeSpeechLanguage(this.loadConfig().lang || 'en');
|
|
2334
|
+
utterance.lang = lang;
|
|
2335
|
+
const voice = this.resolveSpeechVoice(lang);
|
|
2336
|
+
if (voice) {
|
|
2337
|
+
utterance.voice = voice;
|
|
2338
|
+
}
|
|
2339
|
+
utterance.rate = this.getNativeTtsRate();
|
|
2340
|
+
utterance.pitch = this.getNativeTtsPitch();
|
|
2341
|
+
synth.speak(utterance);
|
|
2342
|
+
},
|
|
2343
|
+
|
|
2344
|
+
ensureAnnotationLayer() {
|
|
2345
|
+
if (this.annotationLayer && document.body.contains(this.annotationLayer)) {
|
|
2346
|
+
return this.annotationLayer;
|
|
2347
|
+
}
|
|
2348
|
+
const layer = document.createElement('div');
|
|
2349
|
+
layer.className = 'acc-annotation-layer acc-container';
|
|
2350
|
+
layer.setAttribute('aria-hidden', 'true');
|
|
2351
|
+
document.body.appendChild(layer);
|
|
2352
|
+
this.annotationLayer = layer;
|
|
2353
|
+
return layer;
|
|
2354
|
+
},
|
|
2355
|
+
|
|
2356
|
+
resolveAnnotationTarget(selector) {
|
|
2357
|
+
if (!selector || typeof selector !== 'string') return null;
|
|
2358
|
+
try {
|
|
2359
|
+
const element = document.querySelector(selector);
|
|
2360
|
+
if (!element || element.closest('.acc-container')) return null;
|
|
2361
|
+
return element;
|
|
2362
|
+
} catch {
|
|
2363
|
+
return null;
|
|
2364
|
+
}
|
|
2365
|
+
},
|
|
2366
|
+
|
|
2367
|
+
buildAnnotationEntries(results = {}) {
|
|
2368
|
+
const violations = Array.isArray(results.violations) ? results.violations : [];
|
|
2369
|
+
const entries = [];
|
|
2370
|
+
const seen = new Set();
|
|
2371
|
+
|
|
2372
|
+
violations.forEach((violation) => {
|
|
2373
|
+
const nodes = Array.isArray(violation.nodes) ? violation.nodes : [];
|
|
2374
|
+
nodes.forEach((node) => {
|
|
2375
|
+
const targets = Array.isArray(node.target) ? node.target : [];
|
|
2376
|
+
const selector = targets.find((target) => typeof target === 'string' && target.trim().length);
|
|
2377
|
+
if (!selector) return;
|
|
2378
|
+
const key = `${selector}::${violation.id || violation.help || ''}`;
|
|
2379
|
+
if (seen.has(key)) return;
|
|
2380
|
+
|
|
2381
|
+
const element = this.resolveAnnotationTarget(selector);
|
|
2382
|
+
if (!element) return;
|
|
2383
|
+
seen.add(key);
|
|
2384
|
+
entries.push({
|
|
2385
|
+
selector,
|
|
2386
|
+
element,
|
|
2387
|
+
impact: violation.impact || 'minor',
|
|
2388
|
+
title: violation.help || this.translate('Issue'),
|
|
2389
|
+
description: violation.description || '',
|
|
2390
|
+
helpUrl: violation.helpUrl || '',
|
|
2391
|
+
failureSummary: node.failureSummary || ''
|
|
2392
|
+
});
|
|
2393
|
+
});
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
return entries.slice(0, MAX_ANNOTATIONS);
|
|
2397
|
+
},
|
|
2398
|
+
|
|
2399
|
+
createAnnotationMarker(annotation) {
|
|
2400
|
+
const marker = document.createElement('button');
|
|
2401
|
+
marker.type = 'button';
|
|
2402
|
+
marker.className = 'acc-annotation-marker';
|
|
2403
|
+
marker.dataset.impact = annotation.impact || 'minor';
|
|
2404
|
+
marker.setAttribute('aria-label', annotation.title);
|
|
2405
|
+
marker.title = annotation.title;
|
|
2406
|
+
marker.innerHTML = '';
|
|
2407
|
+
|
|
2408
|
+
marker.addEventListener('click', (event) => {
|
|
2409
|
+
event.preventDefault();
|
|
2410
|
+
event.stopPropagation();
|
|
2411
|
+
this.showAnnotationPopup(annotation, marker);
|
|
2412
|
+
});
|
|
2413
|
+
|
|
2414
|
+
return marker;
|
|
2415
|
+
},
|
|
2416
|
+
|
|
2417
|
+
positionAnnotationPopup(popup, marker) {
|
|
2418
|
+
if (!popup || !marker) return;
|
|
2419
|
+
const markerRect = marker.getBoundingClientRect();
|
|
2420
|
+
const popupWidth = popup.offsetWidth || 300;
|
|
2421
|
+
const popupHeight = popup.offsetHeight || 180;
|
|
2422
|
+
const viewportWidth = window.innerWidth;
|
|
2423
|
+
const viewportHeight = window.innerHeight;
|
|
2424
|
+
const gap = 10;
|
|
2425
|
+
|
|
2426
|
+
let left = window.scrollX + markerRect.left + gap;
|
|
2427
|
+
let top = window.scrollY + markerRect.bottom + gap;
|
|
2428
|
+
|
|
2429
|
+
if (left + popupWidth > window.scrollX + viewportWidth - gap) {
|
|
2430
|
+
left = window.scrollX + viewportWidth - popupWidth - gap;
|
|
2431
|
+
}
|
|
2432
|
+
if (left < window.scrollX + gap) {
|
|
2433
|
+
left = window.scrollX + gap;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
if (top + popupHeight > window.scrollY + viewportHeight - gap) {
|
|
2437
|
+
top = window.scrollY + markerRect.top - popupHeight - gap;
|
|
2438
|
+
}
|
|
2439
|
+
if (top < window.scrollY + gap) {
|
|
2440
|
+
top = window.scrollY + gap;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
popup.style.left = `${left}px`;
|
|
2444
|
+
popup.style.top = `${top}px`;
|
|
2445
|
+
},
|
|
2446
|
+
|
|
2447
|
+
clearAnnotationPopup() {
|
|
2448
|
+
if (this.annotationPopup) {
|
|
2449
|
+
this.annotationPopup.remove();
|
|
2450
|
+
this.annotationPopup = null;
|
|
2451
|
+
}
|
|
2452
|
+
},
|
|
2453
|
+
|
|
2454
|
+
showAnnotationPopup(annotation, marker) {
|
|
2455
|
+
if (!annotation || !marker || !this.annotationLayer) return;
|
|
2456
|
+
this.clearAnnotationPopup();
|
|
2457
|
+
|
|
2458
|
+
const popup = document.createElement('div');
|
|
2459
|
+
popup.className = 'acc-annotation-popup';
|
|
2460
|
+
popup.innerHTML = `
|
|
2461
|
+
<div class="acc-annotation-popup-header">
|
|
2462
|
+
<h3 class="acc-annotation-popup-title">${this.escapeHtml(annotation.title)}</h3>
|
|
2463
|
+
<button type="button" class="acc-annotation-popup-close" aria-label="${this.translate('Close')}">${this.widgetIcons.close}</button>
|
|
2464
|
+
</div>
|
|
2465
|
+
<p><strong>${this.translate(this.capitalizeFirst(annotation.impact))}</strong></p>
|
|
2466
|
+
<p>${this.escapeHtml(annotation.description)}</p>
|
|
2467
|
+
${annotation.failureSummary ? `<p><strong>${this.translate('Issue')}:</strong> ${this.escapeHtml(annotation.failureSummary)}</p>` : ''}
|
|
2468
|
+
${annotation.helpUrl ? `<p><a href="${annotation.helpUrl}" target="_blank" rel="noopener">${this.translate('How to Fix')} →</a></p>` : ''}
|
|
2469
|
+
`;
|
|
2470
|
+
popup.__accMarker = marker;
|
|
2471
|
+
|
|
2472
|
+
const closeButton = popup.querySelector('.acc-annotation-popup-close');
|
|
2473
|
+
closeButton?.addEventListener('click', () => this.clearAnnotationPopup());
|
|
2474
|
+
|
|
2475
|
+
this.annotationLayer.appendChild(popup);
|
|
2476
|
+
this.annotationPopup = popup;
|
|
2477
|
+
this.positionAnnotationPopup(popup, marker);
|
|
2478
|
+
},
|
|
2479
|
+
|
|
2480
|
+
positionAnnotations() {
|
|
2481
|
+
if (!Array.isArray(this.annotationItems) || !this.annotationItems.length) return;
|
|
2482
|
+
|
|
2483
|
+
this.annotationItems.forEach((item) => {
|
|
2484
|
+
if (!item?.target || !item?.marker) return;
|
|
2485
|
+
if (!document.contains(item.target)) {
|
|
2486
|
+
item.marker.hidden = true;
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
const rect = item.target.getBoundingClientRect();
|
|
2491
|
+
const outOfView = (
|
|
2492
|
+
rect.width <= 0 ||
|
|
2493
|
+
rect.height <= 0 ||
|
|
2494
|
+
rect.bottom < 0 ||
|
|
2495
|
+
rect.top > window.innerHeight ||
|
|
2496
|
+
rect.right < 0 ||
|
|
2497
|
+
rect.left > window.innerWidth
|
|
2498
|
+
);
|
|
2499
|
+
|
|
2500
|
+
if (outOfView) {
|
|
2501
|
+
item.marker.hidden = true;
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
item.marker.hidden = false;
|
|
2506
|
+
item.marker.style.top = `${window.scrollY + rect.top + Math.min(16, rect.height / 2)}px`;
|
|
2507
|
+
item.marker.style.left = `${window.scrollX + rect.right}px`;
|
|
2508
|
+
});
|
|
2509
|
+
|
|
2510
|
+
if (this.annotationPopup?.__accMarker && !this.annotationPopup.__accMarker.hidden) {
|
|
2511
|
+
this.positionAnnotationPopup(this.annotationPopup, this.annotationPopup.__accMarker);
|
|
2512
|
+
}
|
|
2513
|
+
},
|
|
2514
|
+
|
|
2515
|
+
enableAnnotations(enable = false) {
|
|
2516
|
+
if (!enable) {
|
|
2517
|
+
this.disableAnnotations();
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
const requestId = ++this.annotationRequestId;
|
|
2522
|
+
this.disableAnnotations({ cancelPending: false });
|
|
2523
|
+
const layer = this.ensureAnnotationLayer();
|
|
2524
|
+
|
|
2525
|
+
this.runBackgroundAxeScan()
|
|
2526
|
+
.then((results) => {
|
|
2527
|
+
if (requestId !== this.annotationRequestId || !this.retrieveState('annotations')) {
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
const annotations = this.buildAnnotationEntries(results);
|
|
2531
|
+
this.annotationItems = annotations.map((annotation) => {
|
|
2532
|
+
const marker = this.createAnnotationMarker(annotation);
|
|
2533
|
+
layer.appendChild(marker);
|
|
2534
|
+
return {
|
|
2535
|
+
marker,
|
|
2536
|
+
target: annotation.element,
|
|
2537
|
+
data: annotation
|
|
2538
|
+
};
|
|
2539
|
+
});
|
|
2540
|
+
|
|
2541
|
+
if (!this.annotationRepositionHandler) {
|
|
2542
|
+
this.annotationRepositionHandler = this.throttle(() => this.positionAnnotations(), 80);
|
|
2543
|
+
}
|
|
2544
|
+
window.addEventListener('scroll', this.annotationRepositionHandler, { passive: true });
|
|
2545
|
+
window.addEventListener('resize', this.annotationRepositionHandler, { passive: true });
|
|
2546
|
+
|
|
2547
|
+
if (!this.annotationOutsideHandler) {
|
|
2548
|
+
this.annotationOutsideHandler = (event) => {
|
|
2549
|
+
if (!this.annotationPopup) return;
|
|
2550
|
+
const clickedPopup = this.annotationPopup.contains(event.target);
|
|
2551
|
+
const clickedMarker = event.target?.closest?.('.acc-annotation-marker');
|
|
2552
|
+
if (!clickedPopup && !clickedMarker) {
|
|
2553
|
+
this.clearAnnotationPopup();
|
|
2554
|
+
}
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
document.addEventListener('click', this.annotationOutsideHandler, true);
|
|
2558
|
+
|
|
2559
|
+
this.positionAnnotations();
|
|
2560
|
+
})
|
|
2561
|
+
.catch((error) => {
|
|
2562
|
+
console.warn('Failed to render annotations:', error);
|
|
2563
|
+
});
|
|
2564
|
+
},
|
|
2565
|
+
|
|
2566
|
+
disableAnnotations({ cancelPending = true } = {}) {
|
|
2567
|
+
if (cancelPending) {
|
|
2568
|
+
this.annotationRequestId += 1;
|
|
2569
|
+
}
|
|
2570
|
+
this.clearAnnotationPopup();
|
|
2571
|
+
|
|
2572
|
+
if (this.annotationOutsideHandler) {
|
|
2573
|
+
document.removeEventListener('click', this.annotationOutsideHandler, true);
|
|
2574
|
+
}
|
|
2575
|
+
if (this.annotationRepositionHandler) {
|
|
2576
|
+
window.removeEventListener('scroll', this.annotationRepositionHandler);
|
|
2577
|
+
window.removeEventListener('resize', this.annotationRepositionHandler);
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
if (this.annotationLayer) {
|
|
2581
|
+
this.annotationLayer.remove();
|
|
2582
|
+
this.annotationLayer = null;
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
this.annotationItems = [];
|
|
2586
|
+
},
|
|
2587
|
+
|
|
2588
|
+
|
|
2589
|
+
clearSimpleLayoutDomMutations() {
|
|
2590
|
+
if (this.simpleLayoutRoot) {
|
|
2591
|
+
this.simpleLayoutRoot.classList.remove('acc-simple-layout-root');
|
|
2592
|
+
this.simpleLayoutRoot = null;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
if (Array.isArray(this.simpleLayoutHiddenElements)) {
|
|
2596
|
+
this.simpleLayoutHiddenElements.forEach((element) => {
|
|
2597
|
+
if (element && element.classList) {
|
|
2598
|
+
element.classList.remove('acc-simple-layout-hidden');
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
this.simpleLayoutHiddenElements = [];
|
|
2603
|
+
},
|
|
2604
|
+
|
|
2605
|
+
applySimpleLayoutDomMutations() {
|
|
2606
|
+
const root = this.getPrimaryContentRoot();
|
|
2607
|
+
if (!root || !document.body) return;
|
|
2608
|
+
|
|
2609
|
+
this.simpleLayoutRoot = root;
|
|
2610
|
+
root.classList.add('acc-simple-layout-root');
|
|
2611
|
+
|
|
2612
|
+
const hiddenElements = [];
|
|
2613
|
+
Array.from(document.body.children).forEach((child) => {
|
|
2614
|
+
if (!(child instanceof Element)) return;
|
|
2615
|
+
if (child.classList.contains('acc-container')) return;
|
|
2616
|
+
if (child === root || child.contains(root)) return;
|
|
2617
|
+
child.classList.add('acc-simple-layout-hidden');
|
|
2618
|
+
hiddenElements.push(child);
|
|
2619
|
+
});
|
|
2620
|
+
|
|
2621
|
+
const clutterSelectors = [
|
|
2622
|
+
'aside',
|
|
2623
|
+
'nav',
|
|
2624
|
+
'form',
|
|
2625
|
+
'footer',
|
|
2626
|
+
'[role="complementary"]',
|
|
2627
|
+
'[role="search"]',
|
|
2628
|
+
'[role="contentinfo"]',
|
|
2629
|
+
'[aria-hidden="true"]',
|
|
2630
|
+
'[class*="cookie"]',
|
|
2631
|
+
'[id*="cookie"]',
|
|
2632
|
+
'[class*="banner"]',
|
|
2633
|
+
'[id*="banner"]',
|
|
2634
|
+
'[class*="popup"]',
|
|
2635
|
+
'[id*="popup"]',
|
|
2636
|
+
'[class*="modal"]',
|
|
2637
|
+
'[id*="modal"]',
|
|
2638
|
+
'[class*="advert"]',
|
|
2639
|
+
'[id*="advert"]',
|
|
2640
|
+
'[class*="ads"]',
|
|
2641
|
+
'[id*="ads"]',
|
|
2642
|
+
'[class*="sidebar"]',
|
|
2643
|
+
'[id*="sidebar"]',
|
|
2644
|
+
'[class*="social"]',
|
|
2645
|
+
'[id*="social"]',
|
|
2646
|
+
'[class*="share"]',
|
|
2647
|
+
'[id*="share"]',
|
|
2648
|
+
'[class*="newsletter"]',
|
|
2649
|
+
'[id*="newsletter"]',
|
|
2650
|
+
'[class*="related"]',
|
|
2651
|
+
'[id*="related"]',
|
|
2652
|
+
'[class*="comment"]',
|
|
2653
|
+
'[id*="comment"]',
|
|
2654
|
+
'[class*="footer"]',
|
|
2655
|
+
'[id*="footer"]',
|
|
2656
|
+
'[class*="promo"]',
|
|
2657
|
+
'[id*="promo"]'
|
|
2658
|
+
].join(',');
|
|
2659
|
+
|
|
2660
|
+
root.querySelectorAll(clutterSelectors).forEach((element) => {
|
|
2661
|
+
if (!(element instanceof Element)) return;
|
|
2662
|
+
if (element.closest('.acc-container')) return;
|
|
2663
|
+
if (element === root) return;
|
|
2664
|
+
element.classList.add('acc-simple-layout-hidden');
|
|
2665
|
+
hiddenElements.push(element);
|
|
2666
|
+
});
|
|
2667
|
+
|
|
2668
|
+
this.simpleLayoutHiddenElements = hiddenElements;
|
|
2669
|
+
},
|
|
2670
|
+
|
|
2671
|
+
enableSimpleLayout(enable = false) {
|
|
2672
|
+
const S = 'body.acc-simple-layout-enabled';
|
|
2673
|
+
const R = `${S} .acc-simple-layout-root`;
|
|
2674
|
+
const X = ':not(.acc-container):not(.acc-container *)';
|
|
2675
|
+
const config = {
|
|
2676
|
+
id: 'simple-layout',
|
|
2677
|
+
css: `
|
|
2678
|
+
/* ── Body & root container ── */
|
|
2679
|
+
${S} {
|
|
2680
|
+
background: #fff !important;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
${S} .acc-simple-layout-hidden {
|
|
2684
|
+
display: none !important;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
${R} {
|
|
2688
|
+
max-width: 72ch !important;
|
|
2689
|
+
margin: 0 auto !important;
|
|
2690
|
+
padding: clamp(20px, 4vw, 40px) 20px !important;
|
|
2691
|
+
position: relative !important;
|
|
2692
|
+
border-radius: 0 !important;
|
|
2693
|
+
box-shadow: none !important;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
/* ── Universal decoration strip ── */
|
|
2697
|
+
${R} :where(*)${X} {
|
|
2698
|
+
background-color: transparent !important;
|
|
2699
|
+
background-image: none !important;
|
|
2700
|
+
box-shadow: none !important;
|
|
2701
|
+
text-shadow: none !important;
|
|
2702
|
+
border-color: transparent !important;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
/* ── Layout linearization ── */
|
|
2706
|
+
${R} :where(div, section, article, header, main, footer, figure, figcaption, details, summary, hgroup, search)${X} {
|
|
2707
|
+
display: block !important;
|
|
2708
|
+
position: static !important;
|
|
2709
|
+
float: none !important;
|
|
2710
|
+
transform: none !important;
|
|
2711
|
+
columns: auto !important;
|
|
2712
|
+
column-count: auto !important;
|
|
2713
|
+
width: auto !important;
|
|
2714
|
+
min-width: 0 !important;
|
|
2715
|
+
max-width: 100% !important;
|
|
2716
|
+
margin-left: 0 !important;
|
|
2717
|
+
margin-right: 0 !important;
|
|
2718
|
+
padding-left: 0 !important;
|
|
2719
|
+
padding-right: 0 !important;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
/* ── Color reset ── */
|
|
2723
|
+
${R} :where(h1, h2, h3, h4, h5, h6)${X} {
|
|
2724
|
+
color: #111 !important;
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
${R} :where(p, li, dt, dd, td, th, span, blockquote, figcaption, label, summary, details)${X} {
|
|
2728
|
+
color: #222 !important;
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
${R} :where(a)${X} {
|
|
2732
|
+
color: #1a0dab !important;
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
${R} :where(a:visited)${X} {
|
|
2736
|
+
color: #681da8 !important;
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
/* ── Typography ── */
|
|
2740
|
+
${R} :where(*)${X} {
|
|
2741
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
${R} :where(p, li, dt, dd, blockquote, figcaption, td, th, label, summary)${X} {
|
|
2745
|
+
font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem) !important;
|
|
2746
|
+
line-height: 1.8 !important;
|
|
2747
|
+
letter-spacing: 0.01em !important;
|
|
2748
|
+
max-width: 70ch !important;
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
${R} :where(h1)${X} {
|
|
2752
|
+
font-size: 2em !important;
|
|
2753
|
+
line-height: 1.2 !important;
|
|
2754
|
+
margin: 0.67em 0 !important;
|
|
2755
|
+
font-weight: 700 !important;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
${R} :where(h2)${X} {
|
|
2759
|
+
font-size: 1.5em !important;
|
|
2760
|
+
line-height: 1.25 !important;
|
|
2761
|
+
margin: 0.83em 0 !important;
|
|
2762
|
+
font-weight: 700 !important;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
${R} :where(h3)${X} {
|
|
2766
|
+
font-size: 1.25em !important;
|
|
2767
|
+
line-height: 1.3 !important;
|
|
2768
|
+
margin: 1em 0 !important;
|
|
2769
|
+
font-weight: 600 !important;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
${R} :where(h4, h5, h6)${X} {
|
|
2773
|
+
font-size: 1.1em !important;
|
|
2774
|
+
line-height: 1.35 !important;
|
|
2775
|
+
margin: 1em 0 !important;
|
|
2776
|
+
font-weight: 600 !important;
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
/* ── Decorative images hidden ── */
|
|
2780
|
+
${R} :where(img[role="presentation"], img[alt=""], img:not([alt]), svg[aria-hidden="true"])${X} {
|
|
2781
|
+
display: none !important;
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
/* ── Meaningful borders restored ── */
|
|
2785
|
+
${R} :where(hr)${X} {
|
|
2786
|
+
border: none !important;
|
|
2787
|
+
border-top: 1px solid #d1d5db !important;
|
|
2788
|
+
margin: 1.5em 0 !important;
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
${R} :where(blockquote)${X} {
|
|
2792
|
+
border-left: 4px solid #d1d5db !important;
|
|
2793
|
+
padding-left: 1em !important;
|
|
2794
|
+
margin-left: 0 !important;
|
|
2795
|
+
font-style: italic !important;
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
${R} :where(table)${X} {
|
|
2799
|
+
border-collapse: collapse !important;
|
|
2800
|
+
max-width: 100% !important;
|
|
2801
|
+
overflow-x: auto !important;
|
|
2802
|
+
display: table !important;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
${R} :where(th, td)${X} {
|
|
2806
|
+
border: 1px solid #d1d5db !important;
|
|
2807
|
+
padding: 8px 12px !important;
|
|
2808
|
+
text-align: left !important;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
${R} :where(th)${X} {
|
|
2812
|
+
font-weight: 600 !important;
|
|
2813
|
+
background: #f8f9fa !important;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
/* ── Lists ── */
|
|
2817
|
+
${R} :where(ul, ol)${X} {
|
|
2818
|
+
padding-left: 1.5em !important;
|
|
2819
|
+
margin: 0.75em 0 !important;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
${R} :where(li)${X} {
|
|
2823
|
+
display: list-item !important;
|
|
2824
|
+
margin: 0.25em 0 !important;
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
/* ── Code blocks ── */
|
|
2828
|
+
${R} :where(pre)${X} {
|
|
2829
|
+
background: #f6f8fa !important;
|
|
2830
|
+
border-radius: 6px !important;
|
|
2831
|
+
padding: 1em !important;
|
|
2832
|
+
overflow-x: auto !important;
|
|
2833
|
+
max-width: 100% !important;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
${R} :where(code, kbd, samp)${X} {
|
|
2837
|
+
font-family: ui-monospace, "SFMono-Regular", "SF Mono", Menlo, Consolas, "Liberation Mono", monospace !important;
|
|
2838
|
+
font-size: 0.9em !important;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
${R} :where(code):not(pre code)${X} {
|
|
2842
|
+
background: #f0f2f5 !important;
|
|
2843
|
+
padding: 0.15em 0.4em !important;
|
|
2844
|
+
border-radius: 3px !important;
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
/* ── Empty wrapper collapse ── */
|
|
2848
|
+
${R} :where(div:empty)${X} {
|
|
2849
|
+
display: none !important;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
/* ── Media ── */
|
|
2853
|
+
${R} :where(img, video, iframe)${X} {
|
|
2854
|
+
max-width: 100% !important;
|
|
2855
|
+
height: auto !important;
|
|
2856
|
+
border-radius: 4px !important;
|
|
2857
|
+
}
|
|
2858
|
+
`
|
|
2859
|
+
};
|
|
2860
|
+
|
|
2861
|
+
this.applyToolStyle({ ...config, enable });
|
|
2862
|
+
document.body?.classList.toggle('acc-simple-layout-enabled', !!enable);
|
|
2863
|
+
|
|
2864
|
+
this.clearSimpleLayoutDomMutations();
|
|
2865
|
+
if (enable) {
|
|
2866
|
+
this.applySimpleLayoutDomMutations();
|
|
2867
|
+
}
|
|
2868
|
+
},
|
|
2869
|
+
|
|
2870
|
+
applyEnhancements() {
|
|
2871
|
+
const { states } = this.loadConfig();
|
|
2872
|
+
// Handle font size scaling
|
|
2873
|
+
const hasTextScaleState = !!(states && Object.prototype.hasOwnProperty.call(states, 'text-scale'));
|
|
2874
|
+
if (hasTextScaleState) {
|
|
2875
|
+
const storedScale = states['text-scale'] === false ? 1 : states['text-scale'];
|
|
2876
|
+
const appliedScale = this.setTextScaleFromPercent(storedScale, { persist: false });
|
|
2877
|
+
this.syncTextScaleControlUI(document.querySelector('.acc-menu'), appliedScale);
|
|
2878
|
+
} else {
|
|
2879
|
+
this.textScaleIndex = 0;
|
|
2880
|
+
if (this.multiLevelFeatures['text-scale']) {
|
|
2881
|
+
this.multiLevelFeatures['text-scale'].currentIndex = -1;
|
|
2882
|
+
}
|
|
2883
|
+
this.scaleText(1);
|
|
2884
|
+
this.syncTextScaleControlUI(document.querySelector('.acc-menu'), 1);
|
|
2885
|
+
}
|
|
2886
|
+
// Apply other enhancements
|
|
2887
|
+
this.concealImages(states && states['hide-images']);
|
|
2888
|
+
this.highlightTitles(states && states['highlight-title']);
|
|
2889
|
+
this.highlightLinks(states && states['highlight-links']);
|
|
2890
|
+
this.adjustLetterSpacing(states && states['letter-spacing']);
|
|
2891
|
+
this.adjustLineSpacing(states && states['line-spacing']);
|
|
2892
|
+
this.enableBoldText(states && states['bold-text']);
|
|
2893
|
+
this.enableReadableText(states && states['readable-text']);
|
|
2894
|
+
this.enableReadingAid(states && states['reading-aid']);
|
|
2895
|
+
this.pauseMotion(states && states['pause-motion']);
|
|
2896
|
+
this.enableLargePointer(states && states['large-pointer']);
|
|
2897
|
+
this.enableHighContrastMode(states && states['high-contrast-mode']);
|
|
2898
|
+
this.enableAnnotations(states && states['annotations']);
|
|
2899
|
+
this.enableTextToSpeech(states && states['text-to-speech']);
|
|
2900
|
+
this.enableSimpleLayout(states && states['simple-layout']);
|
|
2901
|
+
},
|
|
2902
|
+
|
|
2903
|
+
isColorFilterKey(key) {
|
|
2904
|
+
return Array.isArray(this.colorFilterKeys) && this.colorFilterKeys.includes(key);
|
|
2905
|
+
},
|
|
2906
|
+
|
|
2907
|
+
getActiveColorFilterKey(states = this.widgetConfig.states) {
|
|
2908
|
+
if (!this.colorFilterKeys || !this.colorFilterKeys.length) return null;
|
|
2909
|
+
if (!states) return this.activeColorFilterKey || null;
|
|
2910
|
+
for (const key of this.colorFilterKeys) {
|
|
2911
|
+
if (states[key]) {
|
|
2912
|
+
return key;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
return null;
|
|
2916
|
+
},
|
|
2917
|
+
|
|
2918
|
+
setColorFilterUI(menu, activeKey = null) {
|
|
2919
|
+
if (!menu || !menu.querySelectorAll) return;
|
|
2920
|
+
const contrastFeature = this.multiLevelFeatures?.['contrast-toggle'];
|
|
2921
|
+
const contrastButton = menu.querySelector('.acc-btn[data-key="contrast-toggle"]');
|
|
2922
|
+
if (contrastFeature) {
|
|
2923
|
+
const contrastIndex = contrastFeature.values.indexOf(activeKey);
|
|
2924
|
+
contrastFeature.currentIndex = contrastIndex;
|
|
2925
|
+
this.updateContrastToggleButton(contrastButton, contrastIndex);
|
|
2926
|
+
if (contrastButton) {
|
|
2927
|
+
const isContrastActive = contrastIndex >= 0;
|
|
2928
|
+
contrastButton.classList.toggle('acc-selected', isContrastActive);
|
|
2929
|
+
contrastButton.setAttribute('aria-pressed', isContrastActive ? 'true' : 'false');
|
|
2930
|
+
const indicator = contrastButton.querySelector('.acc-progress-indicator[data-feature="contrast-toggle"]');
|
|
2931
|
+
if (indicator) {
|
|
2932
|
+
const dots = indicator.querySelectorAll('.acc-progress-dot');
|
|
2933
|
+
dots.forEach((dot, index) => {
|
|
2934
|
+
dot.classList.toggle('active', index === contrastIndex);
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
const saturationFeature = this.multiLevelFeatures?.['saturation-toggle'];
|
|
2940
|
+
const saturationButton = menu.querySelector('.acc-btn[data-key="saturation-toggle"]');
|
|
2941
|
+
if (saturationFeature) {
|
|
2942
|
+
const saturationIndex = saturationFeature.values.indexOf(activeKey);
|
|
2943
|
+
saturationFeature.currentIndex = saturationIndex;
|
|
2944
|
+
this.updateSaturationToggleButton(saturationButton, saturationIndex);
|
|
2945
|
+
if (saturationButton) {
|
|
2946
|
+
const isSaturationActive = saturationIndex >= 0;
|
|
2947
|
+
saturationButton.classList.toggle('acc-selected', isSaturationActive);
|
|
2948
|
+
saturationButton.setAttribute('aria-pressed', isSaturationActive ? 'true' : 'false');
|
|
2949
|
+
const indicator = saturationButton.querySelector('.acc-progress-indicator[data-feature="saturation-toggle"]');
|
|
2950
|
+
if (indicator) {
|
|
2951
|
+
const dots = indicator.querySelectorAll('.acc-progress-dot');
|
|
2952
|
+
dots.forEach((dot, index) => {
|
|
2953
|
+
dot.classList.toggle('active', index === saturationIndex);
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
this.colorFilterKeys.forEach(filterKey => {
|
|
2959
|
+
const button = menu.querySelector(`.acc-btn[data-key="${filterKey}"]`);
|
|
2960
|
+
if (!button) return;
|
|
2961
|
+
const isActive = filterKey === activeKey;
|
|
2962
|
+
button.classList.toggle('acc-selected', isActive);
|
|
2963
|
+
button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
|
2964
|
+
});
|
|
2965
|
+
},
|
|
2966
|
+
|
|
2967
|
+
updateColorFilterState(activeKey = null, source = 'user') {
|
|
2968
|
+
if (!this.colorFilterKeys || !this.colorFilterKeys.length) {
|
|
2969
|
+
this.activeColorFilterKey = null;
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
const states = this.widgetConfig.states || {};
|
|
2973
|
+
const payload = {};
|
|
2974
|
+
let requiresUpdate = false;
|
|
2975
|
+
this.colorFilterKeys.forEach(filterKey => {
|
|
2976
|
+
const rawCurrent = states[filterKey];
|
|
2977
|
+
const shouldBeActive = activeKey === filterKey;
|
|
2978
|
+
if (shouldBeActive) {
|
|
2979
|
+
if (rawCurrent !== true) {
|
|
2980
|
+
payload[filterKey] = true;
|
|
2981
|
+
requiresUpdate = true;
|
|
2982
|
+
}
|
|
2983
|
+
} else if (rawCurrent) {
|
|
2984
|
+
payload[filterKey] = false;
|
|
2985
|
+
requiresUpdate = true;
|
|
2986
|
+
}
|
|
2987
|
+
});
|
|
2988
|
+
if (requiresUpdate) {
|
|
2989
|
+
this.updateState(payload, { source });
|
|
2990
|
+
}
|
|
2991
|
+
this.activeColorFilterKey = activeKey;
|
|
2992
|
+
},
|
|
2993
|
+
|
|
2994
|
+
computeFilteredBodyColor(filterKey) {
|
|
2995
|
+
const bg = window.getComputedStyle(document.body).backgroundColor;
|
|
2996
|
+
const m = bg.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);
|
|
2997
|
+
if (!m) return null;
|
|
2998
|
+
let r = +m[1], g = +m[2], b = +m[3];
|
|
2999
|
+
const clamp = (v) => Math.min(255, Math.max(0, Math.round(v)));
|
|
3000
|
+
|
|
3001
|
+
switch (filterKey) {
|
|
3002
|
+
case 'invert-colors':
|
|
3003
|
+
r = 255 - r; g = 255 - g; b = 255 - b;
|
|
3004
|
+
break;
|
|
3005
|
+
case 'dark-contrast': {
|
|
3006
|
+
// filter: contrast(150%) brightness(0.8) — applied left to right
|
|
3007
|
+
r = 128 + (r - 128) * 1.5; g = 128 + (g - 128) * 1.5; b = 128 + (b - 128) * 1.5;
|
|
3008
|
+
r *= 0.8; g *= 0.8; b *= 0.8;
|
|
3009
|
+
break;
|
|
3010
|
+
}
|
|
3011
|
+
case 'light-contrast': {
|
|
3012
|
+
// filter: contrast(125%) brightness(1.2)
|
|
3013
|
+
r = 128 + (r - 128) * 1.25; g = 128 + (g - 128) * 1.25; b = 128 + (b - 128) * 1.25;
|
|
3014
|
+
r *= 1.2; g *= 1.2; b *= 1.2;
|
|
3015
|
+
break;
|
|
3016
|
+
}
|
|
3017
|
+
case 'low-saturation': {
|
|
3018
|
+
// filter: saturate(50%) — use standard saturate matrix with s=0.5
|
|
3019
|
+
const s = 0.5;
|
|
3020
|
+
const nr = (0.213 + 0.787 * s) * r + (0.715 - 0.715 * s) * g + (0.072 - 0.072 * s) * b;
|
|
3021
|
+
const ng = (0.213 - 0.213 * s) * r + (0.715 + 0.285 * s) * g + (0.072 - 0.072 * s) * b;
|
|
3022
|
+
const nb = (0.213 - 0.213 * s) * r + (0.715 - 0.715 * s) * g + (0.072 + 0.928 * s) * b;
|
|
3023
|
+
r = nr; g = ng; b = nb;
|
|
3024
|
+
break;
|
|
3025
|
+
}
|
|
3026
|
+
case 'high-saturation': {
|
|
3027
|
+
// filter: saturate(200%) — use standard saturate matrix with s=2
|
|
3028
|
+
const s2 = 2;
|
|
3029
|
+
const hr = (0.213 + 0.787 * s2) * r + (0.715 - 0.715 * s2) * g + (0.072 - 0.072 * s2) * b;
|
|
3030
|
+
const hg = (0.213 - 0.213 * s2) * r + (0.715 + 0.285 * s2) * g + (0.072 - 0.072 * s2) * b;
|
|
3031
|
+
const hb = (0.213 - 0.213 * s2) * r + (0.715 - 0.715 * s2) * g + (0.072 + 0.928 * s2) * b;
|
|
3032
|
+
r = hr; g = hg; b = hb;
|
|
3033
|
+
break;
|
|
3034
|
+
}
|
|
3035
|
+
default:
|
|
3036
|
+
return null;
|
|
3037
|
+
}
|
|
3038
|
+
r = clamp(r); g = clamp(g); b = clamp(b);
|
|
3039
|
+
// Skip if the result is the same as the original (no visible change)
|
|
3040
|
+
if (r === +m[1] && g === +m[2] && b === +m[3]) return null;
|
|
3041
|
+
return `rgb(${r},${g},${b})`;
|
|
3042
|
+
},
|
|
3043
|
+
|
|
3044
|
+
applyVisualFilters() {
|
|
3045
|
+
const { states } = this.loadConfig();
|
|
3046
|
+
const activeKey = this.getActiveColorFilterKey(states);
|
|
3047
|
+
this.activeColorFilterKey = activeKey;
|
|
3048
|
+
|
|
3049
|
+
if (!activeKey) {
|
|
3050
|
+
const style = document.getElementById('acc-filter-style');
|
|
3051
|
+
if (style) style.remove();
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
const filter = this.visualFilters[activeKey];
|
|
3055
|
+
if (!filter) {
|
|
3056
|
+
const style = document.getElementById('acc-filter-style');
|
|
3057
|
+
if (style) style.remove();
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
const adjustedFilter = {
|
|
3061
|
+
...filter,
|
|
3062
|
+
selector: filter.selector || 'body > *:not(.acc-container):not(.acc-rg-container):not(#acc-skip-link)'
|
|
3063
|
+
};
|
|
3064
|
+
let css = this.buildCSS(adjustedFilter);
|
|
3065
|
+
|
|
3066
|
+
// Compute a filtered body background so the body itself matches
|
|
3067
|
+
// the visual filter applied to its children.
|
|
3068
|
+
const filteredBg = this.computeFilteredBodyColor(activeKey);
|
|
3069
|
+
if (filteredBg) {
|
|
3070
|
+
css += `body{background-color:${filteredBg}!important;}`;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
this.injectStyle('acc-filter-style', css);
|
|
3074
|
+
},
|
|
3075
|
+
|
|
3076
|
+
cycleTextScale(enable = false) {
|
|
3077
|
+
if (enable) {
|
|
3078
|
+
this.textScaleIndex = (this.textScaleIndex + 1) % this.textScaleValues.length;
|
|
3079
|
+
if (this.multiLevelFeatures['text-scale']) {
|
|
3080
|
+
this.multiLevelFeatures['text-scale'].currentIndex = this.textScaleIndex;
|
|
3081
|
+
}
|
|
3082
|
+
} else {
|
|
3083
|
+
this.textScaleIndex = 0;
|
|
3084
|
+
if (this.multiLevelFeatures['text-scale']) {
|
|
3085
|
+
this.multiLevelFeatures['text-scale'].currentIndex = -1;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
const progressIndicator = document.querySelector(`.acc-progress-indicator[data-feature="text-scale"]`);
|
|
3089
|
+
if (progressIndicator) {
|
|
3090
|
+
const dots = progressIndicator.querySelectorAll('.acc-progress-dot');
|
|
3091
|
+
dots.forEach(dot => dot.classList.remove('active'));
|
|
3092
|
+
if (enable && this.textScaleIndex < dots.length) {
|
|
3093
|
+
dots[this.textScaleIndex].classList.add('active');
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
const multiply = enable ? this.textScaleValues[this.textScaleIndex] : 1;
|
|
3097
|
+
this.scaleText(multiply);
|
|
3098
|
+
this.updateState({ 'text-scale': multiply });
|
|
3099
|
+
return this.textScaleIndex;
|
|
3100
|
+
},
|
|
3101
|
+
|
|
3102
|
+
cycleMultiLevelFeature(featureKey, button) {
|
|
3103
|
+
const feature = this.multiLevelFeatures[featureKey];
|
|
3104
|
+
if (!feature || !button) return;
|
|
3105
|
+
|
|
3106
|
+
if (featureKey === 'contrast-toggle' || featureKey === 'saturation-toggle') {
|
|
3107
|
+
const newIndex = feature.currentIndex + 1;
|
|
3108
|
+
const newActiveKey = newIndex >= feature.levels ? null : feature.values[newIndex];
|
|
3109
|
+
this.updateColorFilterState(newActiveKey);
|
|
3110
|
+
this.setColorFilterUI(button.closest('.acc-menu'), newActiveKey);
|
|
3111
|
+
this.applyVisualFilters();
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
const newIndex = feature.currentIndex + 1;
|
|
3116
|
+
if (newIndex >= feature.levels) {
|
|
3117
|
+
feature.currentIndex = -1;
|
|
3118
|
+
button.classList.remove('acc-selected');
|
|
3119
|
+
button.setAttribute('aria-pressed', 'false');
|
|
3120
|
+
this.updateState({ [featureKey]: featureKey === 'text-scale' ? 1 : false });
|
|
3121
|
+
if (featureKey === 'text-scale') {
|
|
3122
|
+
this.textScaleIndex = 0;
|
|
3123
|
+
}
|
|
3124
|
+
} else {
|
|
3125
|
+
feature.currentIndex = newIndex;
|
|
3126
|
+
button.classList.add('acc-selected');
|
|
3127
|
+
button.setAttribute('aria-pressed', 'true');
|
|
3128
|
+
const newValue = feature.values[newIndex];
|
|
3129
|
+
this.updateState({ [featureKey]: newValue });
|
|
3130
|
+
if (featureKey === 'text-scale') {
|
|
3131
|
+
this.textScaleIndex = newIndex;
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
const indicator = button.querySelector(`.acc-progress-indicator[data-feature="${featureKey}"]`);
|
|
3135
|
+
if (indicator) {
|
|
3136
|
+
const dots = indicator.querySelectorAll('.acc-progress-dot');
|
|
3137
|
+
dots.forEach((dot, i) => {
|
|
3138
|
+
dot.classList.toggle('active', i === feature.currentIndex);
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
if (featureKey === 'text-scale') {
|
|
3142
|
+
const multiply = feature.currentIndex >= 0 ? feature.values[feature.currentIndex] : 1;
|
|
3143
|
+
this.scaleText(multiply);
|
|
3144
|
+
} else {
|
|
3145
|
+
this.applyVisualFilters();
|
|
3146
|
+
}
|
|
3147
|
+
},
|
|
3148
|
+
|
|
3149
|
+
resetEnhancements() {
|
|
3150
|
+
this.saveConfig({ states: {}, systemDefaults: {} });
|
|
3151
|
+
this.textScaleIndex = 0;
|
|
3152
|
+
this.activeColorFilterKey = null;
|
|
3153
|
+
Object.keys(this.multiLevelFeatures).forEach(key => {
|
|
3154
|
+
this.multiLevelFeatures[key].currentIndex = -1;
|
|
3155
|
+
});
|
|
3156
|
+
const selected = document.querySelectorAll(".acc-selected");
|
|
3157
|
+
selected.forEach(el => {
|
|
3158
|
+
el.classList.remove("acc-selected");
|
|
3159
|
+
el.setAttribute('aria-pressed', 'false');
|
|
3160
|
+
});
|
|
3161
|
+
const indicators = document.querySelectorAll('.acc-progress-indicator');
|
|
3162
|
+
indicators.forEach(indicator => {
|
|
3163
|
+
const dots = indicator.querySelectorAll('.acc-progress-dot');
|
|
3164
|
+
dots.forEach(dot => dot.classList.remove('active'));
|
|
3165
|
+
});
|
|
3166
|
+
const menu = document.querySelector('.acc-menu');
|
|
3167
|
+
if (menu) {
|
|
3168
|
+
this.setColorFilterUI(menu, null);
|
|
3169
|
+
this.syncTextScaleControlUI(menu, 1);
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// Remove focus from active element to fix the persistent focus ring bug
|
|
3173
|
+
if (document.activeElement) {
|
|
3174
|
+
document.activeElement.blur();
|
|
3175
|
+
}
|
|
3176
|
+
const styleIds = [
|
|
3177
|
+
'acc-bold-text',
|
|
3178
|
+
'acc-letter-spacing',
|
|
3179
|
+
'acc-line-spacing',
|
|
3180
|
+
'acc-large-pointer',
|
|
3181
|
+
'acc-highlight-links',
|
|
3182
|
+
'acc-highlight-title',
|
|
3183
|
+
'acc-readable-text',
|
|
3184
|
+
'acc-pause-motion',
|
|
3185
|
+
'acc-hide-images',
|
|
3186
|
+
'acc-filter-style',
|
|
3187
|
+
'acc-simple-layout'
|
|
3188
|
+
];
|
|
3189
|
+
styleIds.forEach(id => {
|
|
3190
|
+
const style = document.getElementById(id);
|
|
3191
|
+
if (style) style.remove();
|
|
3192
|
+
});
|
|
3193
|
+
this.clearSimpleLayoutDomMutations();
|
|
3194
|
+
document.documentElement.classList.remove(
|
|
3195
|
+
'acc-filter',
|
|
3196
|
+
'acc-saturation',
|
|
3197
|
+
'acc-bold-text',
|
|
3198
|
+
'acc-letter-spacing',
|
|
3199
|
+
'acc-line-spacing',
|
|
3200
|
+
'acc-large-pointer',
|
|
3201
|
+
'acc-highlight-links',
|
|
3202
|
+
'acc-highlight-title',
|
|
3203
|
+
'acc-readable-text',
|
|
3204
|
+
'acc-pause-motion',
|
|
3205
|
+
'acc-hide-images',
|
|
3206
|
+
'acc-high-contrast-mode',
|
|
3207
|
+
'acc-simple-layout'
|
|
3208
|
+
);
|
|
3209
|
+
document.body?.classList.remove('acc-simple-layout-enabled');
|
|
3210
|
+
document.body?.classList.remove('acc-high-contrast-mode');
|
|
3211
|
+
this.disconnectTextScaleObserver();
|
|
3212
|
+
this.currentTextScaleMultiplier = 1;
|
|
3213
|
+
const scaledElements = document.querySelectorAll('[data-acc-baseSize]');
|
|
3214
|
+
scaledElements.forEach(el => {
|
|
3215
|
+
if (!(el instanceof Element) || this.shouldSkipScaling(el)) return;
|
|
3216
|
+
el.style.fontSize = '';
|
|
3217
|
+
el.removeAttribute('data-acc-baseSize');
|
|
3218
|
+
});
|
|
3219
|
+
let guide = this.findElement('.acc-rg-container');
|
|
3220
|
+
if (guide) {
|
|
3221
|
+
guide.remove();
|
|
3222
|
+
if (window.__accweb__scrollGuide) {
|
|
3223
|
+
document.removeEventListener('mousemove', window.__accweb__scrollGuide);
|
|
3224
|
+
delete window.__accweb__scrollGuide;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
this.disableAnnotations();
|
|
3228
|
+
this.stopSpeech();
|
|
3229
|
+
this.stopTtsClickMode();
|
|
3230
|
+
this.clearSystemPreferenceListeners();
|
|
3231
|
+
this.detectSystemPreferences();
|
|
3232
|
+
this.setupMediaQueryListeners();
|
|
3233
|
+
this.updateViolationBubble(this.axeScanResults);
|
|
3234
|
+
this._fireCallback('onReset');
|
|
3235
|
+
},
|
|
3236
|
+
|
|
3237
|
+
};
|
|
3238
|
+
|
|
3239
|
+
/** @typedef {import('./index.js').default} AllyWidget */
|
|
3240
|
+
|
|
3241
|
+
/** @type {{ [methodName: string]: (this: AllyWidget, ...args: any[]) => any }} */
|
|
3242
|
+
const uiMethods = {
|
|
3243
|
+
|
|
3244
|
+
_fireCallback(name, ...args) {
|
|
3245
|
+
const fn = this.options && this.options[name];
|
|
3246
|
+
if (typeof fn === 'function') {
|
|
3247
|
+
try { fn(...args); } catch (e) { console.warn(`[AllyWidget] Callback "${name}" threw:`, e); }
|
|
3248
|
+
}
|
|
3249
|
+
},
|
|
3250
|
+
|
|
3251
|
+
translate(label) {
|
|
3252
|
+
const { lang } = this.loadConfig();
|
|
3253
|
+
const dictionary = this.translations[lang] || this.translations['en'];
|
|
3254
|
+
return dictionary[label] || label;
|
|
3255
|
+
},
|
|
3256
|
+
|
|
3257
|
+
getLanguageCountryLabel(languageCode) {
|
|
3258
|
+
const countryByLanguage = {
|
|
3259
|
+
en: 'USA',
|
|
3260
|
+
ne: 'Nepal'
|
|
3261
|
+
};
|
|
3262
|
+
return countryByLanguage[languageCode] || String(languageCode || '').toUpperCase();
|
|
3263
|
+
},
|
|
3264
|
+
|
|
3265
|
+
getLanguageFlag(languageCode) {
|
|
3266
|
+
const countryCodeByLanguage = {
|
|
3267
|
+
en: 'US',
|
|
3268
|
+
ne: 'NP'
|
|
3269
|
+
};
|
|
3270
|
+
const countryCode = (countryCodeByLanguage[languageCode] || String(languageCode || '').slice(0, 2)).toUpperCase();
|
|
3271
|
+
if (!/^[A-Z]{2}$/.test(countryCode)) return '';
|
|
3272
|
+
return String.fromCodePoint(...countryCode.split('').map(char => 127397 + char.charCodeAt(0)));
|
|
3273
|
+
},
|
|
3274
|
+
|
|
3275
|
+
formatLanguageLabel(language) {
|
|
3276
|
+
if (!language) return 'English (USA)';
|
|
3277
|
+
const baseName = String(language.label || language.code || '')
|
|
3278
|
+
.split('(')[0].trim() || String(language.code || 'en').toUpperCase();
|
|
3279
|
+
return `${baseName} (${this.getLanguageCountryLabel(language.code)})`;
|
|
3280
|
+
},
|
|
3281
|
+
|
|
3282
|
+
getWidgetIconMarkup(iconKey) {
|
|
3283
|
+
const normalized = typeof iconKey === 'string' ? iconKey.trim().toLowerCase() : '';
|
|
3284
|
+
const variants = this.widgetIcons?.accessibilityVariants || {};
|
|
3285
|
+
return variants[normalized] || variants.default || this.widgetIcons.accessibility;
|
|
3286
|
+
},
|
|
3287
|
+
|
|
3288
|
+
throttle(func, limit) {
|
|
3289
|
+
let inThrottle;
|
|
3290
|
+
return function (...args) {
|
|
3291
|
+
if (!inThrottle) {
|
|
3292
|
+
func.apply(this, args);
|
|
3293
|
+
inThrottle = true;
|
|
3294
|
+
setTimeout(() => (inThrottle = false), limit);
|
|
3295
|
+
}
|
|
3296
|
+
};
|
|
3297
|
+
},
|
|
3298
|
+
|
|
3299
|
+
getFocusableElements(root) {
|
|
3300
|
+
if (!root) return [];
|
|
3301
|
+
const selectors = [
|
|
3302
|
+
'a[href]',
|
|
3303
|
+
'button:not([disabled])',
|
|
3304
|
+
'input:not([disabled])',
|
|
3305
|
+
'textarea:not([disabled])',
|
|
3306
|
+
'select:not([disabled])',
|
|
3307
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
3308
|
+
].join(',');
|
|
3309
|
+
const hasDocument = typeof document !== 'undefined';
|
|
3310
|
+
const activeElement = hasDocument ? document.activeElement : null;
|
|
3311
|
+
return Array.from(root.querySelectorAll(selectors)).filter(el => {
|
|
3312
|
+
if (el.hasAttribute('disabled')) return false;
|
|
3313
|
+
if (el.getAttribute('aria-hidden') === 'true') return false;
|
|
3314
|
+
if (el.closest('[aria-hidden="true"]')) return false;
|
|
3315
|
+
const rect = el.getBoundingClientRect();
|
|
3316
|
+
const style = typeof window !== 'undefined' && window.getComputedStyle ? window.getComputedStyle(el) : { visibility: 'visible', display: 'block' };
|
|
3317
|
+
const hasSize = rect.width > 0 || rect.height > 0;
|
|
3318
|
+
const isVisible = (hasSize || (hasDocument && el === activeElement)) && style.visibility !== 'hidden' && style.display !== 'none';
|
|
3319
|
+
return isVisible;
|
|
3320
|
+
});
|
|
3321
|
+
},
|
|
3322
|
+
|
|
3323
|
+
openMenu(menuContainer, toggleBtn) {
|
|
3324
|
+
if (!menuContainer) return;
|
|
3325
|
+
const menu = this.findElement('.acc-menu', menuContainer);
|
|
3326
|
+
this.activeMenuContainer = menuContainer;
|
|
3327
|
+
this.activeMenuToggle = toggleBtn || this.activeMenuToggle;
|
|
3328
|
+
this.previousFocus = document.activeElement && typeof document.activeElement.focus === 'function'
|
|
3329
|
+
? document.activeElement : null;
|
|
3330
|
+
|
|
3331
|
+
menuContainer.style.display = 'block';
|
|
3332
|
+
if (menu) {
|
|
3333
|
+
menu.setAttribute('aria-hidden', 'false');
|
|
3334
|
+
menu.setAttribute('aria-modal', 'true');
|
|
3335
|
+
if (!menu.hasAttribute('tabindex')) menu.setAttribute('tabindex', '-1');
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
if (this.activeMenuToggle) this.activeMenuToggle.setAttribute('aria-expanded', 'true');
|
|
3339
|
+
|
|
3340
|
+
const focusable = this.getFocusableElements(menuContainer);
|
|
3341
|
+
if (focusable.length) {
|
|
3342
|
+
focusable[0].focus();
|
|
3343
|
+
} else if (menu) {
|
|
3344
|
+
menu.focus();
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
if (this.menuKeyListener) document.removeEventListener('keydown', this.menuKeyListener, true);
|
|
3348
|
+
|
|
3349
|
+
this.menuKeyListener = (event) => {
|
|
3350
|
+
if (!this.activeMenuContainer) return;
|
|
3351
|
+
if (event.key === 'Escape' || event.key === 'Esc') {
|
|
3352
|
+
event.preventDefault();
|
|
3353
|
+
this.closeMenu(this.activeMenuContainer);
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
3356
|
+
if (event.key === 'Tab') {
|
|
3357
|
+
const focusables = this.getFocusableElements(this.activeMenuContainer);
|
|
3358
|
+
if (!focusables.length) { event.preventDefault(); return; }
|
|
3359
|
+
const first = focusables[0];
|
|
3360
|
+
const last = focusables[focusables.length - 1];
|
|
3361
|
+
if (event.shiftKey) {
|
|
3362
|
+
if (document.activeElement === first) { event.preventDefault(); last.focus(); }
|
|
3363
|
+
} else if (document.activeElement === last) {
|
|
3364
|
+
event.preventDefault(); first.focus();
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
};
|
|
3368
|
+
|
|
3369
|
+
document.addEventListener('keydown', this.menuKeyListener, true);
|
|
3370
|
+
this._fireCallback('onOpen');
|
|
3371
|
+
},
|
|
3372
|
+
|
|
3373
|
+
closeMenu(menuContainer, toggleBtn) {
|
|
3374
|
+
const targetContainer = menuContainer || this.activeMenuContainer;
|
|
3375
|
+
if (!targetContainer) return;
|
|
3376
|
+
const menu = this.findElement('.acc-menu', targetContainer);
|
|
3377
|
+
targetContainer.style.display = 'none';
|
|
3378
|
+
if (menu) {
|
|
3379
|
+
menu.setAttribute('aria-hidden', 'true');
|
|
3380
|
+
menu.setAttribute('aria-modal', 'false');
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
const langModal = this.findElement('#acc-lang-modal', targetContainer);
|
|
3384
|
+
if (langModal) langModal.setAttribute('hidden', '');
|
|
3385
|
+
|
|
3386
|
+
const langToggle = this.findElement('.acc-footer-lang-toggle', targetContainer);
|
|
3387
|
+
if (langToggle) langToggle.setAttribute('aria-expanded', 'false');
|
|
3388
|
+
|
|
3389
|
+
const langSearch = this.findElement('#acc-lang-search', targetContainer);
|
|
3390
|
+
if (langSearch) langSearch.value = '';
|
|
3391
|
+
|
|
3392
|
+
targetContainer.querySelectorAll('.acc-lang-item').forEach(item => {
|
|
3393
|
+
item.style.display = '';
|
|
3394
|
+
});
|
|
3395
|
+
|
|
3396
|
+
if (this.menuKeyListener) {
|
|
3397
|
+
document.removeEventListener('keydown', this.menuKeyListener, true);
|
|
3398
|
+
this.menuKeyListener = null;
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
const toggle = toggleBtn || this.activeMenuToggle;
|
|
3402
|
+
if (toggle) toggle.setAttribute('aria-expanded', 'false');
|
|
3403
|
+
|
|
3404
|
+
const focusTarget = toggle || this.previousFocus;
|
|
3405
|
+
if (focusTarget && typeof focusTarget.focus === 'function') focusTarget.focus();
|
|
3406
|
+
|
|
3407
|
+
this.stopSpeech();
|
|
3408
|
+
|
|
3409
|
+
this.activeMenuContainer = null;
|
|
3410
|
+
this.activeMenuToggle = null;
|
|
3411
|
+
this.previousFocus = null;
|
|
3412
|
+
this._fireCallback('onClose');
|
|
3413
|
+
},
|
|
3414
|
+
|
|
3415
|
+
renderOptions(options, optionClass) {
|
|
3416
|
+
let html = '';
|
|
3417
|
+
for (let i = 0; i < options.length; i++) {
|
|
3418
|
+
const opt = options[i];
|
|
3419
|
+
if (opt.requiresSpeechSynthesis && !this.supportsTextToSpeech()) continue;
|
|
3420
|
+
const isMultiLevel = opt.multiLevel === true;
|
|
3421
|
+
const mergedOptionClass = [optionClass, opt.optionClass].filter(Boolean).join(' ');
|
|
3422
|
+
let progressIndicator = '';
|
|
3423
|
+
if (isMultiLevel) {
|
|
3424
|
+
const featureData = this.multiLevelFeatures[opt.key];
|
|
3425
|
+
if (featureData) {
|
|
3426
|
+
progressIndicator = `<div class="acc-progress-indicator" data-feature="${opt.key}">`;
|
|
3427
|
+
for (let j = 0; j < featureData.levels; j++) {
|
|
3428
|
+
const isActive = featureData.currentIndex === j;
|
|
3429
|
+
progressIndicator += `<div class="acc-progress-dot${isActive ? ' active' : ''}" data-level="${j}"> </div>`;
|
|
3430
|
+
}
|
|
3431
|
+
progressIndicator += ` </div>`;
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
html += `<button
|
|
3435
|
+
class="acc-btn ${mergedOptionClass} ${this.multiLevelFeatures[opt.key]?.currentIndex >= 0 ? 'acc-selected' : ''}"
|
|
3436
|
+
type="button"
|
|
3437
|
+
data-key="${opt.key}"
|
|
3438
|
+
data-multi-level="${isMultiLevel}"
|
|
3439
|
+
title="${opt.label}"
|
|
3440
|
+
aria-label="${opt.label}"
|
|
3441
|
+
aria-pressed="${this.multiLevelFeatures[opt.key]?.currentIndex >= 0 || false}"
|
|
3442
|
+
aria-controls="acc-menu-content">
|
|
3443
|
+
${opt.icon}
|
|
3444
|
+
<span class="acc-label">${opt.label}</span>
|
|
3445
|
+
${progressIndicator}
|
|
3446
|
+
</button>`;
|
|
3447
|
+
}
|
|
3448
|
+
return html;
|
|
3449
|
+
},
|
|
3450
|
+
|
|
3451
|
+
renderTextScaleControl(scaleValue = 1) {
|
|
3452
|
+
const percent = this.getTextScalePercent(scaleValue);
|
|
3453
|
+
const min = Number(this.textScaleMinPercent) || 80;
|
|
3454
|
+
const max = Number(this.textScaleMaxPercent) || 150;
|
|
3455
|
+
const step = Number(this.textScaleStepPercent) || 5;
|
|
3456
|
+
const progress = ((percent - min) / (max - min)) * 100;
|
|
3457
|
+
|
|
3458
|
+
return `
|
|
3459
|
+
<div class="acc-text-scale-control" data-key="text-scale">
|
|
3460
|
+
<div class="acc-text-scale-meta">
|
|
3461
|
+
<span class="acc-text-scale-icon" aria-hidden="true">${this.widgetIcons.adjustFontSize}</span>
|
|
3462
|
+
<span class="acc-label" data-acc-text="Font Size">Font Size</span>
|
|
3463
|
+
<span class="acc-text-scale-percent">${percent}%</span>
|
|
3464
|
+
</div>
|
|
3465
|
+
<input
|
|
3466
|
+
type="range"
|
|
3467
|
+
class="acc-text-scale-range"
|
|
3468
|
+
min="${min}"
|
|
3469
|
+
max="${max}"
|
|
3470
|
+
step="${step}"
|
|
3471
|
+
value="${percent}"
|
|
3472
|
+
data-acc-text="Font Size"
|
|
3473
|
+
aria-label="Font Size"
|
|
3474
|
+
style="--acc-text-scale-progress: ${Math.max(0, Math.min(100, progress))}%">
|
|
3475
|
+
</div>
|
|
3476
|
+
`;
|
|
3477
|
+
},
|
|
3478
|
+
|
|
3479
|
+
getTranslatedText(el, defaultValue) {
|
|
3480
|
+
let text = el.getAttribute('data-acc-text');
|
|
3481
|
+
if (!text && defaultValue) {
|
|
3482
|
+
text = defaultValue;
|
|
3483
|
+
el.setAttribute('data-acc-text', text);
|
|
3484
|
+
}
|
|
3485
|
+
return this.translate(text);
|
|
3486
|
+
},
|
|
3487
|
+
|
|
3488
|
+
translateMenuUI(menu) {
|
|
3489
|
+
menu.querySelectorAll('.acc-section-title, .acc-label').forEach(el => {
|
|
3490
|
+
el.innerText = this.getTranslatedText(el, el.innerText.trim());
|
|
3491
|
+
});
|
|
3492
|
+
menu.querySelectorAll('[title]').forEach(el => {
|
|
3493
|
+
el.setAttribute('title', this.getTranslatedText(el, el.getAttribute('title')));
|
|
3494
|
+
});
|
|
3495
|
+
menu.querySelectorAll('[aria-label]').forEach(el => {
|
|
3496
|
+
el.setAttribute('aria-label', this.getTranslatedText(el, el.getAttribute('aria-label')));
|
|
3497
|
+
});
|
|
3498
|
+
this.updateSkipLinkLabel();
|
|
3499
|
+
},
|
|
3500
|
+
|
|
3501
|
+
displayMenu({ container, lang, position = 'bottom-right', offset = [20, 20], size, icon }) {
|
|
3502
|
+
try {
|
|
3503
|
+
this.applyThemeVariables();
|
|
3504
|
+
this.registerStaticStyles();
|
|
3505
|
+
|
|
3506
|
+
const activeLanguageCode = String(lang || 'en').split(/[_-]/)[0].toLowerCase();
|
|
3507
|
+
|
|
3508
|
+
// Configurable branding
|
|
3509
|
+
const poweredByText = this.options?.poweredByText || 'Ally Widget';
|
|
3510
|
+
const poweredByUrl = this.options?.poweredByUrl || 'https://github.com/SunilCz/ally';
|
|
3511
|
+
|
|
3512
|
+
const menuTemplate = `
|
|
3513
|
+
<div class="acc-menu" role="dialog" aria-labelledby="accessibility-title">
|
|
3514
|
+
<div class="acc-menu-header">
|
|
3515
|
+
<div id="accessibility-title" class="acc-menu-title">
|
|
3516
|
+
<span class="acc-menu-title-icon" aria-hidden="true">${this.getWidgetIconMarkup(icon)}</span>
|
|
3517
|
+
<span class="acc-label">Accessibility</span>
|
|
3518
|
+
</div>
|
|
3519
|
+
<div class="acc-header-actions"></div>
|
|
3520
|
+
</div>
|
|
3521
|
+
<div id="acc-menu-content" class="acc-menu-content">
|
|
3522
|
+
<div class="acc-options-all"> </div>
|
|
3523
|
+
</div>
|
|
3524
|
+
<div class="acc-footer">
|
|
3525
|
+
<button type="button" class="acc-footer-reset" title="Reset settings" aria-label="Reset settings">
|
|
3526
|
+
${this.widgetIcons.reset}
|
|
3527
|
+
<span class="acc-label">Reset settings</span>
|
|
3528
|
+
</button>
|
|
3529
|
+
<div class="acc-footer-meta">
|
|
3530
|
+
<a href="${poweredByUrl}" target="_blank" rel="noopener noreferrer">${poweredByText}</a>
|
|
3531
|
+
<button type="button" class="acc-footer-lang-toggle" title="Language" aria-label="Language" aria-expanded="false" aria-controls="acc-lang-modal">
|
|
3532
|
+
<span id="acc-current-language" class="acc-footer-lang-current">${String(activeLanguageCode || 'en').toUpperCase()}</span>
|
|
3533
|
+
<span class="acc-footer-lang-arrow" aria-hidden="true"> </span>
|
|
3534
|
+
</button>
|
|
3535
|
+
</div>
|
|
3536
|
+
<div id="acc-lang-modal" class="acc-lang-modal" hidden>
|
|
3537
|
+
<div class="acc-lang-modal-header">
|
|
3538
|
+
<div class="acc-section-title acc-label">All Languages</div>
|
|
3539
|
+
</div>
|
|
3540
|
+
<div class="acc-lang-search-wrapper">
|
|
3541
|
+
<input type="text" id="acc-lang-search" class="acc-lang-search" placeholder="Search language" aria-label="Search language">
|
|
3542
|
+
</div>
|
|
3543
|
+
<div class="acc-lang-list">
|
|
3544
|
+
${this.supportedLanguages.map(language => {
|
|
3545
|
+
const languageLabel = this.formatLanguageLabel(language);
|
|
3546
|
+
const languageFlag = this.getLanguageFlag(language.code);
|
|
3547
|
+
return `<button type="button" class="acc-lang-item${language.code === activeLanguageCode ? ' selected' : ''}" data-lang="${language.code}" aria-label="${languageLabel}">
|
|
3548
|
+
<span class="acc-lang-item-main">
|
|
3549
|
+
<span class="acc-lang-flag" aria-hidden="true">${languageFlag}</span>
|
|
3550
|
+
<span class="acc-lang-item-label">${languageLabel}</span>
|
|
3551
|
+
</span>
|
|
3552
|
+
<span class="acc-icon-check" aria-hidden="true"> </span>
|
|
3553
|
+
</button>`;
|
|
3554
|
+
}).join('')}
|
|
3555
|
+
</div>
|
|
3556
|
+
</div>
|
|
3557
|
+
</div>
|
|
3558
|
+
</div>
|
|
3559
|
+
<div class="acc-overlay"> </div>
|
|
3560
|
+
`;
|
|
3561
|
+
|
|
3562
|
+
const menuContainer = document.createElement('div');
|
|
3563
|
+
menuContainer.innerHTML = menuTemplate;
|
|
3564
|
+
menuContainer.style.display = 'none';
|
|
3565
|
+
const menu = this.findElement('.acc-menu', menuContainer);
|
|
3566
|
+
if (menu) {
|
|
3567
|
+
menu.setAttribute('aria-hidden', 'true');
|
|
3568
|
+
if (!menu.hasAttribute('tabindex')) menu.setAttribute('tabindex', '-1');
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
const isRightAligned = position === 'bottom-right' || position === 'top-right' || this.widgetTheme.menuPosition === 'right';
|
|
3572
|
+
if (isRightAligned) {
|
|
3573
|
+
menu.style.right = 'var(--acc-menu-inline-gap, 12px)';
|
|
3574
|
+
menu.style.left = 'auto';
|
|
3575
|
+
} else {
|
|
3576
|
+
menu.style.left = 'var(--acc-menu-inline-gap, 12px)';
|
|
3577
|
+
menu.style.right = 'auto';
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
const normalizedOffset = this.normalizeOffset(offset) || [20, 20];
|
|
3581
|
+
const offsetY = normalizedOffset[1] ?? 25;
|
|
3582
|
+
const buttonSize = size !== undefined && size !== null && String(size).trim() !== ''
|
|
3583
|
+
? this.normalizeButtonSize(size)
|
|
3584
|
+
: (this.widgetTheme?.buttonSize || '52px');
|
|
3585
|
+
|
|
3586
|
+
if (position === 'bottom-right' || position === 'bottom-left') {
|
|
3587
|
+
menu.style.bottom = `calc(${offsetY}px + ${buttonSize} + 16px)`;
|
|
3588
|
+
} else if (position === 'top-right' || position === 'top-left') {
|
|
3589
|
+
menu.style.top = `calc(${offsetY}px + ${buttonSize} + 16px)`;
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
const config = this.loadConfig();
|
|
3593
|
+
|
|
3594
|
+
const textKeys = new Set(['text-scale', 'bold-text', 'line-spacing', 'letter-spacing', 'readable-text']);
|
|
3595
|
+
const colorKeys = new Set(['contrast-toggle', 'invert-colors', 'saturation-toggle', 'high-contrast-mode']);
|
|
3596
|
+
const readingAidsKeys = new Set(['reading-aid', 'highlight-links', 'highlight-title', 'simple-layout']);
|
|
3597
|
+
|
|
3598
|
+
const sourceOptions = [
|
|
3599
|
+
...this.contentOptions.map(option => ({ ...option })),
|
|
3600
|
+
...this.colorOptions.map(option => ({ ...option, optionClass: 'acc-filter' })),
|
|
3601
|
+
...this.accessTools.map(option => ({ ...option, optionClass: 'acc-tools' }))
|
|
3602
|
+
];
|
|
3603
|
+
|
|
3604
|
+
const groupedOptions = { speech: [], text: [], color: [], reading: [], interaction: [] };
|
|
3605
|
+
const seenKeys = new Set();
|
|
3606
|
+
sourceOptions.forEach(option => {
|
|
3607
|
+
if (!option?.key || seenKeys.has(option.key)) return;
|
|
3608
|
+
seenKeys.add(option.key);
|
|
3609
|
+
if (option.key === 'text-to-speech') { groupedOptions.speech.push(option); return; }
|
|
3610
|
+
if (textKeys.has(option.key)) { groupedOptions.text.push(option); return; }
|
|
3611
|
+
if (colorKeys.has(option.key)) { groupedOptions.color.push(option); return; }
|
|
3612
|
+
if (readingAidsKeys.has(option.key)) { groupedOptions.reading.push(option); return; }
|
|
3613
|
+
groupedOptions.interaction.push(option);
|
|
3614
|
+
});
|
|
3615
|
+
|
|
3616
|
+
const sectionConfig = [
|
|
3617
|
+
{ key: 'speech', label: 'Speech', containerClass: 'acc-tts-toggle-container', optionClass: 'acc-tts-toggle' },
|
|
3618
|
+
{ key: 'text', label: 'Text', containerClass: 'acc-options acc-options-text' },
|
|
3619
|
+
{ key: 'color', label: 'Color & Contrast', containerClass: 'acc-options' },
|
|
3620
|
+
{ key: 'reading', label: 'Reading Aids', containerClass: 'acc-options' },
|
|
3621
|
+
{ key: 'interaction', label: 'Interaction', containerClass: 'acc-options' }
|
|
3622
|
+
];
|
|
3623
|
+
|
|
3624
|
+
const sectionMarkup = sectionConfig.map(section => {
|
|
3625
|
+
let sectionOptions = groupedOptions[section.key];
|
|
3626
|
+
let specialContent = '';
|
|
3627
|
+
|
|
3628
|
+
if (section.key === 'text') {
|
|
3629
|
+
const textScaleOption = sectionOptions.find(option => option.key === 'text-scale');
|
|
3630
|
+
sectionOptions = sectionOptions.filter(option => option.key !== 'text-scale');
|
|
3631
|
+
const textOrder = ['line-spacing', 'letter-spacing', 'bold-text', 'readable-text'];
|
|
3632
|
+
sectionOptions.sort((a, b) => {
|
|
3633
|
+
const rankA = textOrder.indexOf(a.key); const rankB = textOrder.indexOf(b.key);
|
|
3634
|
+
return (rankA === -1 ? Number.MAX_SAFE_INTEGER : rankA) - (rankB === -1 ? Number.MAX_SAFE_INTEGER : rankB);
|
|
3635
|
+
});
|
|
3636
|
+
if (textScaleOption) specialContent = this.renderTextScaleControl(config.states?.['text-scale'] || 1);
|
|
3637
|
+
|
|
3638
|
+
const firstThinRowKeys = new Set(['line-spacing', 'letter-spacing']);
|
|
3639
|
+
const secondThinRowKeys = new Set(['bold-text', 'readable-text']);
|
|
3640
|
+
const firstThinRowOptions = sectionOptions.filter(o => firstThinRowKeys.has(o.key));
|
|
3641
|
+
const secondThinRowOptions = sectionOptions.filter(o => secondThinRowKeys.has(o.key));
|
|
3642
|
+
const remainingTextOptions = sectionOptions.filter(o => !firstThinRowKeys.has(o.key) && !secondThinRowKeys.has(o.key));
|
|
3643
|
+
const firstThinRowHtml = firstThinRowOptions.length ? `<div class="acc-options acc-options-text-inline">${this.renderOptions(firstThinRowOptions, 'acc-text-inline')}</div>` : '';
|
|
3644
|
+
const secondThinRowHtml = secondThinRowOptions.length ? `<div class="acc-options acc-options-text-inline">${this.renderOptions(secondThinRowOptions, 'acc-text-inline')}</div>` : '';
|
|
3645
|
+
const remainingTextHtml = remainingTextOptions.length ? `<div class="${section.containerClass}">${this.renderOptions(remainingTextOptions, section.optionClass || '')}</div>` : '';
|
|
3646
|
+
if (!specialContent && !firstThinRowHtml && !secondThinRowHtml && !remainingTextHtml) return '';
|
|
3647
|
+
return `<section class="acc-option-category acc-option-category-${section.key}"><div class="acc-section-title acc-label">${section.label}</div>${specialContent}${firstThinRowHtml}${secondThinRowHtml}${remainingTextHtml}</section>`;
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
if (section.key === 'color') {
|
|
3651
|
+
const colorOrder = ['contrast-toggle', 'saturation-toggle', 'invert-colors', 'high-contrast-mode'];
|
|
3652
|
+
sectionOptions.sort((a, b) => {
|
|
3653
|
+
const rankA = colorOrder.indexOf(a.key); const rankB = colorOrder.indexOf(b.key);
|
|
3654
|
+
return (rankA === -1 ? Number.MAX_SAFE_INTEGER : rankA) - (rankB === -1 ? Number.MAX_SAFE_INTEGER : rankB);
|
|
3655
|
+
});
|
|
3656
|
+
const firstThinRowKeys = new Set(['contrast-toggle', 'saturation-toggle']);
|
|
3657
|
+
const secondThinRowKeys = new Set(['invert-colors', 'high-contrast-mode']);
|
|
3658
|
+
const firstThinRowOptions = sectionOptions.filter(o => firstThinRowKeys.has(o.key));
|
|
3659
|
+
const secondThinRowOptions = sectionOptions.filter(o => secondThinRowKeys.has(o.key));
|
|
3660
|
+
const remainingColorOptions = sectionOptions.filter(o => !firstThinRowKeys.has(o.key) && !secondThinRowKeys.has(o.key));
|
|
3661
|
+
const firstThinRowHtml = firstThinRowOptions.length ? `<div class="acc-options acc-options-text-inline">${this.renderOptions(firstThinRowOptions, 'acc-text-inline')}</div>` : '';
|
|
3662
|
+
const secondThinRowHtml = secondThinRowOptions.length ? `<div class="acc-options acc-options-text-inline">${this.renderOptions(secondThinRowOptions, 'acc-text-inline')}</div>` : '';
|
|
3663
|
+
const remainingColorHtml = remainingColorOptions.length ? `<div class="${section.containerClass}">${this.renderOptions(remainingColorOptions, section.optionClass || '')}</div>` : '';
|
|
3664
|
+
if (!firstThinRowHtml && !secondThinRowHtml && !remainingColorHtml) return '';
|
|
3665
|
+
return `<section class="acc-option-category acc-option-category-${section.key}"><div class="acc-section-title acc-label">Color & Contrast</div>${firstThinRowHtml}${secondThinRowHtml}${remainingColorHtml}</section>`;
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
if (section.key === 'reading') {
|
|
3669
|
+
const readingOrder = ['highlight-links', 'highlight-title', 'reading-aid', 'simple-layout'];
|
|
3670
|
+
sectionOptions.sort((a, b) => {
|
|
3671
|
+
const rankA = readingOrder.indexOf(a.key); const rankB = readingOrder.indexOf(b.key);
|
|
3672
|
+
return (rankA === -1 ? Number.MAX_SAFE_INTEGER : rankA) - (rankB === -1 ? Number.MAX_SAFE_INTEGER : rankB);
|
|
3673
|
+
});
|
|
3674
|
+
const firstThinRowKeys = new Set(['highlight-links', 'highlight-title']);
|
|
3675
|
+
const secondThinRowKeys = new Set(['reading-aid', 'simple-layout']);
|
|
3676
|
+
const firstThinRowOptions = sectionOptions.filter(o => firstThinRowKeys.has(o.key));
|
|
3677
|
+
const secondThinRowOptions = sectionOptions.filter(o => secondThinRowKeys.has(o.key));
|
|
3678
|
+
const remainingReadingOptions = sectionOptions.filter(o => !firstThinRowKeys.has(o.key) && !secondThinRowKeys.has(o.key));
|
|
3679
|
+
const firstThinRowHtml = firstThinRowOptions.length ? `<div class="acc-options acc-options-text-inline">${this.renderOptions(firstThinRowOptions, 'acc-text-inline')}</div>` : '';
|
|
3680
|
+
const secondThinRowHtml = secondThinRowOptions.length ? `<div class="acc-options acc-options-text-inline">${this.renderOptions(secondThinRowOptions, 'acc-text-inline')}</div>` : '';
|
|
3681
|
+
const remainingReadingHtml = remainingReadingOptions.length ? `<div class="${section.containerClass}">${this.renderOptions(remainingReadingOptions, section.optionClass || '')}</div>` : '';
|
|
3682
|
+
if (!firstThinRowHtml && !secondThinRowHtml && !remainingReadingHtml) return '';
|
|
3683
|
+
return `<section class="acc-option-category acc-option-category-${section.key}"><div class="acc-section-title acc-label">Reading Aids</div>${firstThinRowHtml}${secondThinRowHtml}${remainingReadingHtml}</section>`;
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
const sectionOptionsHtml = sectionOptions.length ? this.renderOptions(sectionOptions, section.optionClass || '') : '';
|
|
3687
|
+
if (!specialContent && !sectionOptionsHtml.trim()) return '';
|
|
3688
|
+
const optionsContainer = sectionOptionsHtml.trim() ? `<div class="${section.containerClass}">${sectionOptionsHtml}</div>` : '';
|
|
3689
|
+
return `<section class="acc-option-category acc-option-category-${section.key}"><div class="acc-section-title acc-label">${section.label}</div>${specialContent}${optionsContainer}</section>`;
|
|
3690
|
+
}).join('');
|
|
3691
|
+
|
|
3692
|
+
menu.querySelector('.acc-options-all').innerHTML = sectionMarkup;
|
|
3693
|
+
|
|
3694
|
+
const langModal = this.findElement('#acc-lang-modal', menu);
|
|
3695
|
+
const langToggle = this.findElement('.acc-footer-lang-toggle', menu);
|
|
3696
|
+
const langSearch = this.findElement('#acc-lang-search', menu);
|
|
3697
|
+
const langItems = menu.querySelectorAll('.acc-lang-item');
|
|
3698
|
+
|
|
3699
|
+
const closeLanguageModal = () => {
|
|
3700
|
+
if (langModal) langModal.setAttribute('hidden', '');
|
|
3701
|
+
if (langToggle) langToggle.setAttribute('aria-expanded', 'false');
|
|
3702
|
+
if (langSearch) langSearch.value = '';
|
|
3703
|
+
langItems.forEach(item => { item.style.display = ''; });
|
|
3704
|
+
};
|
|
3705
|
+
|
|
3706
|
+
const openLanguageModal = () => {
|
|
3707
|
+
if (langModal) langModal.removeAttribute('hidden');
|
|
3708
|
+
if (langToggle) langToggle.setAttribute('aria-expanded', 'true');
|
|
3709
|
+
if (langSearch) langSearch.focus();
|
|
3710
|
+
};
|
|
3711
|
+
|
|
3712
|
+
if (langToggle && langModal) {
|
|
3713
|
+
langToggle.addEventListener('click', (event) => {
|
|
3714
|
+
event.stopPropagation();
|
|
3715
|
+
langModal.hasAttribute('hidden') ? openLanguageModal() : closeLanguageModal();
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
if (langSearch) {
|
|
3720
|
+
langSearch.addEventListener('input', () => {
|
|
3721
|
+
const searchValue = langSearch.value.toLowerCase();
|
|
3722
|
+
langItems.forEach(item => {
|
|
3723
|
+
const labelElement = item.querySelector('.acc-lang-item-label');
|
|
3724
|
+
const text = (labelElement?.textContent || item.textContent).toLowerCase();
|
|
3725
|
+
item.style.display = text.includes(searchValue) ? '' : 'none';
|
|
3726
|
+
});
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
langItems.forEach(item => {
|
|
3731
|
+
item.addEventListener('click', () => {
|
|
3732
|
+
const langCode = item.getAttribute('data-lang');
|
|
3733
|
+
if (!langCode) return;
|
|
3734
|
+
langItems.forEach(i => i.classList.remove('selected'));
|
|
3735
|
+
item.classList.add('selected');
|
|
3736
|
+
const currentLang = this.findElement('#acc-current-language', menu);
|
|
3737
|
+
if (currentLang) currentLang.textContent = String(langCode).toUpperCase();
|
|
3738
|
+
closeLanguageModal();
|
|
3739
|
+
this.saveConfig({ lang: langCode });
|
|
3740
|
+
this.translateMenuUI(menuContainer);
|
|
3741
|
+
});
|
|
3742
|
+
});
|
|
3743
|
+
|
|
3744
|
+
const textScaleRange = this.findElement('.acc-text-scale-range', menu);
|
|
3745
|
+
if (textScaleRange) {
|
|
3746
|
+
textScaleRange.addEventListener('input', () => {
|
|
3747
|
+
const multiplier = this.setTextScaleFromPercent(textScaleRange.value, { persist: false });
|
|
3748
|
+
this.syncTextScaleControlUI(menu, multiplier);
|
|
3749
|
+
});
|
|
3750
|
+
textScaleRange.addEventListener('change', () => {
|
|
3751
|
+
const multiplier = this.setTextScaleFromPercent(textScaleRange.value, { persist: true });
|
|
3752
|
+
this.syncTextScaleControlUI(menu, multiplier);
|
|
3753
|
+
this._fireCallback('onFeatureToggle', 'text-scale', multiplier);
|
|
3754
|
+
});
|
|
3755
|
+
this.syncTextScaleControlUI(menu, config.states?.['text-scale'] || 1);
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
menu.addEventListener('click', (e) => {
|
|
3759
|
+
if (langModal && !langModal.hasAttribute('hidden')) {
|
|
3760
|
+
const clickedInsideLanguageModal = Boolean(e.target.closest('.acc-lang-modal'));
|
|
3761
|
+
const clickedLanguageToggle = Boolean(e.target.closest('.acc-footer-lang-toggle'));
|
|
3762
|
+
if (!clickedInsideLanguageModal && !clickedLanguageToggle) closeLanguageModal();
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
const target = e.target.closest('[role="button"], button, .acc-overlay');
|
|
3766
|
+
if (!target) return;
|
|
3767
|
+
if (target.classList.contains('acc-overlay')) { this.closeMenu(menuContainer); return; }
|
|
3768
|
+
if (target.classList.contains('acc-footer-reset')) { this.resetEnhancements(); return; }
|
|
3769
|
+
|
|
3770
|
+
const btn = target.classList.contains('acc-btn') ? target : null;
|
|
3771
|
+
if (btn) {
|
|
3772
|
+
const key = btn.dataset.key;
|
|
3773
|
+
if (key === 'accessibility-report') {
|
|
3774
|
+
this.runAccessibilityReport();
|
|
3775
|
+
} else if (this.multiLevelFeatures[key]) {
|
|
3776
|
+
this.cycleMultiLevelFeature(key, btn);
|
|
3777
|
+
this._fireCallback('onFeatureToggle', key, this.multiLevelFeatures[key].currentIndex >= 0);
|
|
3778
|
+
} else if (this.isColorFilterKey(key)) {
|
|
3779
|
+
const isCurrentlyActive = btn.classList.contains('acc-selected');
|
|
3780
|
+
const newActiveKey = isCurrentlyActive ? null : key;
|
|
3781
|
+
this.setColorFilterUI(menu, newActiveKey);
|
|
3782
|
+
this.updateColorFilterState(newActiveKey);
|
|
3783
|
+
this.applyVisualFilters();
|
|
3784
|
+
this._fireCallback('onFeatureToggle', key, !!newActiveKey);
|
|
3785
|
+
} else {
|
|
3786
|
+
const isSelected = !btn.classList.contains('acc-selected');
|
|
3787
|
+
btn.classList.toggle('acc-selected', isSelected);
|
|
3788
|
+
btn.setAttribute('aria-pressed', isSelected);
|
|
3789
|
+
this.userInitiatedToggleKey = key;
|
|
3790
|
+
try {
|
|
3791
|
+
this.updateState({ [key]: isSelected });
|
|
3792
|
+
this.applyEnhancements();
|
|
3793
|
+
this._fireCallback('onFeatureToggle', key, isSelected);
|
|
3794
|
+
} finally {
|
|
3795
|
+
this.userInitiatedToggleKey = null;
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
});
|
|
3800
|
+
|
|
3801
|
+
menu.querySelectorAll('[role="button"], button').forEach(el => {
|
|
3802
|
+
el.addEventListener('keydown', (e) => {
|
|
3803
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); el.click(); }
|
|
3804
|
+
});
|
|
3805
|
+
});
|
|
3806
|
+
|
|
3807
|
+
this.translateMenuUI(menuContainer);
|
|
3808
|
+
const activeColorFilter = this.getActiveColorFilterKey(config.states);
|
|
3809
|
+
this.setColorFilterUI(menu, activeColorFilter);
|
|
3810
|
+
this.updateColorFilterState(activeColorFilter);
|
|
3811
|
+
|
|
3812
|
+
if (config.states) {
|
|
3813
|
+
for (let key in config.states) {
|
|
3814
|
+
if (this.isColorFilterKey(key)) continue;
|
|
3815
|
+
if (config.states[key] && key !== 'text-scale') {
|
|
3816
|
+
const btn = this.findElement(`.acc-btn[data-key="${key}"]`, menu);
|
|
3817
|
+
if (btn) { btn.classList.add('acc-selected'); btn.setAttribute('aria-pressed', 'true'); }
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
container.appendChild(menuContainer);
|
|
3823
|
+
return menuContainer;
|
|
3824
|
+
} catch (e) {
|
|
3825
|
+
console.error('[AllyWidget] Error displaying menu:', e);
|
|
3826
|
+
return null;
|
|
3827
|
+
}
|
|
3828
|
+
},
|
|
3829
|
+
|
|
3830
|
+
displayWidget(options) {
|
|
3831
|
+
try {
|
|
3832
|
+
this.applyThemeVariables();
|
|
3833
|
+
this.registerStaticStyles();
|
|
3834
|
+
|
|
3835
|
+
const widgetTemplate = `
|
|
3836
|
+
<div class="acc-widget">
|
|
3837
|
+
<a href="#" id="allyWidget" class="acc-toggle-btn" title="Open Accessibility Menu" role="button" aria-label="Open accessibility menu" aria-expanded="false">
|
|
3838
|
+
<span class="acc-toggle-icon" aria-hidden="true">${this.getWidgetIconMarkup(options?.icon)}</span>
|
|
3839
|
+
<span class="acc-violation-bubble" data-severity="warning" hidden> </span>
|
|
3840
|
+
</a>
|
|
3841
|
+
</div>
|
|
3842
|
+
`;
|
|
3843
|
+
|
|
3844
|
+
const widget = document.createElement('div');
|
|
3845
|
+
widget.innerHTML = widgetTemplate;
|
|
3846
|
+
widget.classList.add('acc-container');
|
|
3847
|
+
const btn = this.findElement('.acc-toggle-btn', widget);
|
|
3848
|
+
this.violationBubble = this.findElement('.acc-violation-bubble', widget);
|
|
3849
|
+
this.widgetToggleButton = btn;
|
|
3850
|
+
|
|
3851
|
+
const { position = 'bottom-right', offset = [20, 20], size } = options;
|
|
3852
|
+
const normalizedOffset = this.normalizeOffset(offset) || [20, 20];
|
|
3853
|
+
const offsetX = normalizedOffset[0] ?? 20;
|
|
3854
|
+
const offsetY = normalizedOffset[1] ?? 25;
|
|
3855
|
+
let buttonStyle = { left: `${offsetX}px`, bottom: `${offsetY}px` };
|
|
3856
|
+
if (position === 'bottom-right') {
|
|
3857
|
+
buttonStyle = { right: `${offsetX}px`, left: 'auto', bottom: `${offsetY}px` };
|
|
3858
|
+
} else if (position === 'top-left') {
|
|
3859
|
+
buttonStyle = { top: `${offsetY}px`, bottom: 'auto', left: `${offsetX}px` };
|
|
3860
|
+
} else if (position === 'top-right') {
|
|
3861
|
+
buttonStyle = { top: `${offsetY}px`, right: `${offsetX}px`, bottom: 'auto', left: 'auto' };
|
|
3862
|
+
}
|
|
3863
|
+
Object.assign(btn.style, buttonStyle);
|
|
3864
|
+
|
|
3865
|
+
if (size !== undefined && size !== null && String(size).trim() !== '') {
|
|
3866
|
+
const buttonSize = this.normalizeButtonSize(size);
|
|
3867
|
+
btn.style.setProperty('--acc-button-size', buttonSize);
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
let menu;
|
|
3871
|
+
btn.addEventListener('click', (event) => {
|
|
3872
|
+
event.preventDefault();
|
|
3873
|
+
if (!menu) {
|
|
3874
|
+
menu = this.displayMenu({ ...options, container: widget });
|
|
3875
|
+
if (!menu) return;
|
|
3876
|
+
this.menuContainer = menu;
|
|
3877
|
+
const overlay = menu.querySelector('.acc-overlay');
|
|
3878
|
+
if (overlay) {
|
|
3879
|
+
overlay.addEventListener('click', (overlayEvent) => {
|
|
3880
|
+
overlayEvent.stopPropagation();
|
|
3881
|
+
this.closeMenu(menu, btn);
|
|
3882
|
+
});
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
const isHidden = menu.style.display === 'none' || window.getComputedStyle(menu).display === 'none';
|
|
3886
|
+
if (isHidden) { this.openMenu(menu, btn); } else { this.closeMenu(menu, btn); }
|
|
3887
|
+
});
|
|
3888
|
+
|
|
3889
|
+
btn.addEventListener('keydown', (e) => {
|
|
3890
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); btn.click(); }
|
|
3891
|
+
});
|
|
3892
|
+
|
|
3893
|
+
document.body.appendChild(widget);
|
|
3894
|
+
this.translateMenuUI(widget);
|
|
3895
|
+
this.ensureSkipLink();
|
|
3896
|
+
|
|
3897
|
+
// Optional keyboard shortcut (Alt+A) to toggle the widget
|
|
3898
|
+
if (this.options?.keyboardShortcut !== false) {
|
|
3899
|
+
document.addEventListener('keydown', (e) => {
|
|
3900
|
+
if (e.altKey && e.key === 'a') {
|
|
3901
|
+
e.preventDefault();
|
|
3902
|
+
btn.click();
|
|
3903
|
+
}
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
this.runBackgroundAxeScan().catch(() => {
|
|
3908
|
+
this.updateViolationBubble({ violations: [] });
|
|
3909
|
+
});
|
|
3910
|
+
|
|
3911
|
+
if (this.isDevMode()) {
|
|
3912
|
+
const rerunDevScan = () => {
|
|
3913
|
+
this.runBackgroundAxeScan({ force: true }).catch(() => {
|
|
3914
|
+
this.updateViolationBubble(this.axeScanResults || { violations: [] });
|
|
3915
|
+
});
|
|
3916
|
+
};
|
|
3917
|
+
if (document.readyState === 'complete') {
|
|
3918
|
+
setTimeout(rerunDevScan, 250);
|
|
3919
|
+
} else {
|
|
3920
|
+
window.addEventListener('load', () => { setTimeout(rerunDevScan, 150); }, { once: true });
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3924
|
+
document.addEventListener('click', (e) => {
|
|
3925
|
+
if (!btn) return;
|
|
3926
|
+
const clickedToggle = e.target === btn || btn.contains(e.target);
|
|
3927
|
+
const clickedInsideWidget = e.target.closest('.acc-container');
|
|
3928
|
+
if (menu && this.activeMenuContainer === menu && menu.style.display !== 'none') {
|
|
3929
|
+
const clickedInsideMenu = menu.contains(e.target);
|
|
3930
|
+
if (!clickedToggle && !clickedInsideMenu && !clickedInsideWidget) {
|
|
3931
|
+
this.closeMenu(menu, btn);
|
|
3932
|
+
}
|
|
3933
|
+
} else if (!clickedToggle) {
|
|
3934
|
+
btn.blur();
|
|
3935
|
+
}
|
|
3936
|
+
});
|
|
3937
|
+
|
|
3938
|
+
return widget;
|
|
3939
|
+
} catch (e) {
|
|
3940
|
+
console.error('[AllyWidget] Error displaying widget:', e);
|
|
3941
|
+
return null;
|
|
3942
|
+
}
|
|
3943
|
+
},
|
|
3944
|
+
|
|
3945
|
+
startAccessibleWebWidget() {
|
|
3946
|
+
try {
|
|
3947
|
+
if (document.querySelector('.acc-widget .acc-toggle-btn')) return;
|
|
3948
|
+
|
|
3949
|
+
const dataAttributeOptions = this.getDataAttributeOptions();
|
|
3950
|
+
if (dataAttributeOptions && Object.keys(dataAttributeOptions).length) {
|
|
3951
|
+
this.dataOptions = dataAttributeOptions;
|
|
3952
|
+
this.options = { ...this.options, ...dataAttributeOptions };
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
const baseOptions = { ...this.options };
|
|
3956
|
+
const lang = baseOptions.lang ||
|
|
3957
|
+
document.querySelector('html')?.getAttribute('lang')?.replace(/[_-].*/, '') ||
|
|
3958
|
+
navigator.language ||
|
|
3959
|
+
'en';
|
|
3960
|
+
baseOptions.lang = lang;
|
|
3961
|
+
baseOptions.position = baseOptions.position || 'bottom-right';
|
|
3962
|
+
|
|
3963
|
+
if (baseOptions.offset) baseOptions.offset = this.normalizeOffset(baseOptions.offset);
|
|
3964
|
+
if (baseOptions.size) {
|
|
3965
|
+
baseOptions.size = this.normalizeButtonSize(baseOptions.size);
|
|
3966
|
+
this.widgetTheme.buttonSize = baseOptions.size;
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
this.options = { ...baseOptions };
|
|
3970
|
+
this.applyThemeOverrides(baseOptions);
|
|
3971
|
+
this.applyThemeVariables();
|
|
3972
|
+
this.registerStaticStyles();
|
|
3973
|
+
|
|
3974
|
+
this.loadConfig(false);
|
|
3975
|
+
this.detectSystemPreferences();
|
|
3976
|
+
this.setupMediaQueryListeners();
|
|
3977
|
+
const initialColorFilter = this.getActiveColorFilterKey(this.widgetConfig.states);
|
|
3978
|
+
this.updateColorFilterState(initialColorFilter);
|
|
3979
|
+
|
|
3980
|
+
this.applyEnhancements();
|
|
3981
|
+
this.applyVisualFilters();
|
|
3982
|
+
this.launchWidget(baseOptions);
|
|
3983
|
+
} catch (e) {
|
|
3984
|
+
console.error('[AllyWidget] Error starting widget:', e);
|
|
3985
|
+
}
|
|
3986
|
+
},
|
|
3987
|
+
|
|
3988
|
+
launchWidget(args = {}) {
|
|
3989
|
+
try {
|
|
3990
|
+
let options = { lang: this.getDefaultLanguage(), position: 'bottom-right', offset: [20, 20] };
|
|
3991
|
+
try {
|
|
3992
|
+
const savedConfig = this.fetchSavedConfig();
|
|
3993
|
+
if (savedConfig) {
|
|
3994
|
+
options = { ...options, ...JSON.parse(savedConfig) };
|
|
3995
|
+
}
|
|
3996
|
+
} catch (e) {
|
|
3997
|
+
console.warn('[AllyWidget] Error loading saved config:', e);
|
|
3998
|
+
}
|
|
3999
|
+
options = { ...options, ...args };
|
|
4000
|
+
this.saveConfig(options);
|
|
4001
|
+
this.displayWidget(options);
|
|
4002
|
+
} catch (e) {
|
|
4003
|
+
console.error('[AllyWidget] Error in widget launch:', e);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
};
|
|
4008
|
+
|
|
4009
|
+
/*!
|
|
4010
|
+
* Ally Widget v1.0.0
|
|
4011
|
+
* https://github.com/SunilCz/ally
|
|
4012
|
+
*
|
|
4013
|
+
* Copyright (c) 2025 SunilCz
|
|
4014
|
+
* Released under the MIT License
|
|
4015
|
+
*
|
|
4016
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
|
4017
|
+
* This widget does not guarantee accessibility compliance.
|
|
4018
|
+
*
|
|
4019
|
+
* ─── Developer Options Reference ─────────────────────────────────────────────
|
|
4020
|
+
*
|
|
4021
|
+
* window.AllyWidgetOptions = {
|
|
4022
|
+
* // ── Position & Size ──────────────────────────────────────────────────────
|
|
4023
|
+
* position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
|
|
4024
|
+
* offset: [20, 20] // [horizontal, vertical] px from edge
|
|
4025
|
+
* size: '52px' // toggle button size (px / em / rem / %)
|
|
4026
|
+
* keyboardShortcut: true // Alt+A to toggle (set false to disable)
|
|
4027
|
+
*
|
|
4028
|
+
* // ── Language ─────────────────────────────────────────────────────────────
|
|
4029
|
+
* lang: 'en' | 'ne'
|
|
4030
|
+
*
|
|
4031
|
+
* // ── Storage ──────────────────────────────────────────────────────────────
|
|
4032
|
+
* storageKey: 'ally-wgt' // localStorage key (change to avoid collisions)
|
|
4033
|
+
*
|
|
4034
|
+
* // ── Branding / Footer ────────────────────────────────────────────────────
|
|
4035
|
+
* poweredByText: 'Ally Widget' // footer link label
|
|
4036
|
+
* poweredByUrl: 'https://...' // footer link href
|
|
4037
|
+
*
|
|
4038
|
+
* // ── Toggle Button Icon ───────────────────────────────────────────────────
|
|
4039
|
+
* icon: 'default' | 'person' | 'eye' | 'universal' (built-in variants)
|
|
4040
|
+
* OR any SVG string for a fully custom icon
|
|
4041
|
+
*
|
|
4042
|
+
* // ── Color Theme ──────────────────────────────────────────────────────────
|
|
4043
|
+
* primaryColor: '#4f46e5'
|
|
4044
|
+
* primaryColorLight: '#818cf8'
|
|
4045
|
+
* primaryColorDark: '#3730a3'
|
|
4046
|
+
* backgroundColor: '#ffffff'
|
|
4047
|
+
* textColor: '#1f2937'
|
|
4048
|
+
* textColorInverted: '#ffffff'
|
|
4049
|
+
* cardBackground: '#f9fafb'
|
|
4050
|
+
* borderColor: '#e5e7eb'
|
|
4051
|
+
* focusRingColor: '#4f46e5'
|
|
4052
|
+
* hoverColor: '#f3f4f6'
|
|
4053
|
+
* activeColor: '#ede9fe'
|
|
4054
|
+
* borderRadius: '12px'
|
|
4055
|
+
* buttonBorderRadius: '50%'
|
|
4056
|
+
* headerHeight: '56px'
|
|
4057
|
+
* focusBorderWidth: '2px'
|
|
4058
|
+
* focusOutlineOffset: '2px'
|
|
4059
|
+
* zIndex: 9999
|
|
4060
|
+
* buttonSize: '52px' // alias for size
|
|
4061
|
+
*
|
|
4062
|
+
* // ── OR pass all theme keys nested under `theme: {}` ──────────────────────
|
|
4063
|
+
* theme: { primaryColor: '#e11d48', borderRadius: '8px', ... }
|
|
4064
|
+
*
|
|
4065
|
+
* // ── Features: disable specific features ──────────────────────────────────
|
|
4066
|
+
* disableFeatures: ['hide-images', 'dyslexia-font', 'annotations']
|
|
4067
|
+
* // All feature keys: 'bold-text' | 'line-spacing' | 'letter-spacing' |
|
|
4068
|
+
* // 'hide-images' | 'readable-text' | 'highlight-links' | 'highlight-title' |
|
|
4069
|
+
* // 'text-scale' | 'contrast-toggle' | 'invert-colors' | 'saturation-toggle' |
|
|
4070
|
+
* // 'large-pointer' | 'pause-motion' | 'reading-aid' | 'text-to-speech' |
|
|
4071
|
+
* // 'high-contrast-mode' | 'simple-layout' | 'annotations' | 'accessibility-report'
|
|
4072
|
+
*
|
|
4073
|
+
* // ── Features: override label or icon for any feature ─────────────────────
|
|
4074
|
+
* featureOverrides: {
|
|
4075
|
+
* 'bold-text': { label: 'Bold', icon: '<svg>...</svg>' },
|
|
4076
|
+
* 'text-to-speech': { label: 'Read Aloud' }
|
|
4077
|
+
* }
|
|
4078
|
+
*
|
|
4079
|
+
* // ── TTS voice configuration ───────────────────────────────────────────────
|
|
4080
|
+
* ttsNativeVoiceName: '' // exact voice name to prefer (e.g. 'Google हिन्दी')
|
|
4081
|
+
* ttsNativeVoiceLang: '' // BCP-47 lang tag override (e.g. 'ne-NP')
|
|
4082
|
+
* ttsRate: 1 // speech rate 0.5–2
|
|
4083
|
+
* ttsPitch: 1 // speech pitch 0–2
|
|
4084
|
+
*
|
|
4085
|
+
* // ── Event Callbacks ───────────────────────────────────────────────────────
|
|
4086
|
+
* onOpen() // widget menu opened
|
|
4087
|
+
* onClose() // widget menu closed
|
|
4088
|
+
* onFeatureToggle(key, enabled) // a feature was toggled
|
|
4089
|
+
* onReset() // all settings reset
|
|
4090
|
+
* };
|
|
4091
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
4092
|
+
*/
|
|
4093
|
+
|
|
4094
|
+
|
|
4095
|
+
class AllyWidget {
|
|
4096
|
+
constructor(options = {}) {
|
|
4097
|
+
this.widgetTheme = { ...WIDGET_THEME };
|
|
4098
|
+
|
|
4099
|
+
// Allow full icon override via options.icons
|
|
4100
|
+
this.widgetIcons = options.icons && typeof options.icons === 'object'
|
|
4101
|
+
? { ...WIDGET_ICONS, ...options.icons }
|
|
4102
|
+
: { ...WIDGET_ICONS };
|
|
4103
|
+
|
|
4104
|
+
this.targetSelectors = TARGET_SELECTORS;
|
|
4105
|
+
|
|
4106
|
+
this.visualFilters = {
|
|
4107
|
+
'dark-contrast': {
|
|
4108
|
+
styles: { 'filter': 'contrast(150%) brightness(0.8)' },
|
|
4109
|
+
selector: PAGE_CONTENT_SELECTOR
|
|
4110
|
+
},
|
|
4111
|
+
'light-contrast': {
|
|
4112
|
+
styles: { 'filter': 'contrast(125%) brightness(1.2)' },
|
|
4113
|
+
selector: PAGE_CONTENT_SELECTOR
|
|
4114
|
+
},
|
|
4115
|
+
'invert-colors': {
|
|
4116
|
+
styles: { 'filter': 'invert(100%)' },
|
|
4117
|
+
selector: PAGE_CONTENT_SELECTOR
|
|
4118
|
+
},
|
|
4119
|
+
'low-saturation': {
|
|
4120
|
+
styles: { 'filter': 'saturate(50%)' },
|
|
4121
|
+
selector: PAGE_CONTENT_SELECTOR
|
|
4122
|
+
},
|
|
4123
|
+
'high-saturation': {
|
|
4124
|
+
styles: { 'filter': 'saturate(200%)' },
|
|
4125
|
+
selector: PAGE_CONTENT_SELECTOR
|
|
4126
|
+
}
|
|
4127
|
+
};
|
|
4128
|
+
|
|
4129
|
+
this.translations = TRANSLATIONS;
|
|
4130
|
+
this.supportedLanguages = SUPPORTED_LANGUAGES.map((language) => ({ ...language }));
|
|
4131
|
+
|
|
4132
|
+
// Build the disabled-features set for quick lookup
|
|
4133
|
+
const disabledSet = new Set(
|
|
4134
|
+
Array.isArray(options.disableFeatures) ? options.disableFeatures : []
|
|
4135
|
+
);
|
|
4136
|
+
|
|
4137
|
+
// Per-feature label/icon overrides: { 'bold-text': { label: '...', icon: '...' } }
|
|
4138
|
+
const featureOverrides = (options.featureOverrides && typeof options.featureOverrides === 'object')
|
|
4139
|
+
? options.featureOverrides
|
|
4140
|
+
: {};
|
|
4141
|
+
|
|
4142
|
+
const applyOverride = (item) => {
|
|
4143
|
+
const override = featureOverrides[item.key];
|
|
4144
|
+
if (!override) return item;
|
|
4145
|
+
return {
|
|
4146
|
+
...item,
|
|
4147
|
+
...(override.label ? { label: override.label } : {}),
|
|
4148
|
+
...(override.icon ? { icon: override.icon } : {})
|
|
4149
|
+
};
|
|
4150
|
+
};
|
|
4151
|
+
|
|
4152
|
+
const filterAndOverride = (items) =>
|
|
4153
|
+
items
|
|
4154
|
+
.filter(item => !disabledSet.has(item.key))
|
|
4155
|
+
.map(applyOverride);
|
|
4156
|
+
|
|
4157
|
+
this.accessTools = filterAndOverride([
|
|
4158
|
+
{ label: 'Big Cursor', key: 'large-pointer', icon: this.widgetIcons.largePointer },
|
|
4159
|
+
{ label: 'Stop Animations', key: 'pause-motion', icon: this.widgetIcons.pauseMotion },
|
|
4160
|
+
{ label: 'Reading Guide', key: 'reading-aid', icon: this.widgetIcons.readingAid },
|
|
4161
|
+
{
|
|
4162
|
+
label: 'Text to Speech',
|
|
4163
|
+
key: 'text-to-speech',
|
|
4164
|
+
icon: this.widgetIcons.textToSpeech,
|
|
4165
|
+
requiresSpeechSynthesis: true
|
|
4166
|
+
},
|
|
4167
|
+
{ label: 'High Contrast', key: 'high-contrast-mode', icon: this.widgetIcons.highContrast },
|
|
4168
|
+
{ label: 'Simplify Layout', key: 'simple-layout', icon: this.widgetIcons.simplifyLayout }
|
|
4169
|
+
]);
|
|
4170
|
+
|
|
4171
|
+
if (this.isDevMode()) {
|
|
4172
|
+
if (!disabledSet.has('annotations')) {
|
|
4173
|
+
this.accessTools.push(applyOverride(
|
|
4174
|
+
{ label: 'Annotations', key: 'annotations', icon: this.widgetIcons.annotations }
|
|
4175
|
+
));
|
|
4176
|
+
}
|
|
4177
|
+
if (!disabledSet.has('accessibility-report')) {
|
|
4178
|
+
this.accessTools.push(applyOverride(
|
|
4179
|
+
{ label: 'Accessibility Report', key: 'accessibility-report', icon: this.widgetIcons.accessibilityReport, isAction: true }
|
|
4180
|
+
));
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
// axe-core state
|
|
4185
|
+
this.axeCoreLoaded = false;
|
|
4186
|
+
this.axeCoreLoading = false;
|
|
4187
|
+
this.axeCorePromise = null;
|
|
4188
|
+
this.axeScanResults = null;
|
|
4189
|
+
this.axeScanPromise = null;
|
|
4190
|
+
this.violationBubble = null;
|
|
4191
|
+
|
|
4192
|
+
// Accessibility report modal state
|
|
4193
|
+
this.reportPreviousFocus = null;
|
|
4194
|
+
this.reportKeyListener = null;
|
|
4195
|
+
|
|
4196
|
+
// System preference state
|
|
4197
|
+
this.systemPreferenceListeners = [];
|
|
4198
|
+
this.systemPreferenceMediaQueries = {};
|
|
4199
|
+
|
|
4200
|
+
// Annotation state
|
|
4201
|
+
this.annotationLayer = null;
|
|
4202
|
+
this.annotationItems = [];
|
|
4203
|
+
this.annotationPopup = null;
|
|
4204
|
+
this.annotationRepositionHandler = null;
|
|
4205
|
+
this.annotationOutsideHandler = null;
|
|
4206
|
+
this.annotationRequestId = 0;
|
|
4207
|
+
|
|
4208
|
+
// Text-to-speech state
|
|
4209
|
+
this.ttsUtterance = null;
|
|
4210
|
+
this.ttsClickListener = null;
|
|
4211
|
+
this.ttsActiveTarget = null;
|
|
4212
|
+
this.ttsTextCache = '';
|
|
4213
|
+
this.ttsStatus = 'stopped';
|
|
4214
|
+
this.ttsQueue = [];
|
|
4215
|
+
this.ttsQueueIndex = 0;
|
|
4216
|
+
this.ttsSessionId = 0;
|
|
4217
|
+
this.ttsVoice = null;
|
|
4218
|
+
|
|
4219
|
+
// Simple layout state
|
|
4220
|
+
this.simpleLayoutRoot = null;
|
|
4221
|
+
this.simpleLayoutHiddenElements = [];
|
|
4222
|
+
|
|
4223
|
+
this.userInitiatedToggleKey = null;
|
|
4224
|
+
|
|
4225
|
+
// Font size / multi-level state
|
|
4226
|
+
this.textScaleIndex = 0;
|
|
4227
|
+
this.textScaleValues = [1.2, 1.4, 1.6];
|
|
4228
|
+
this.textScaleMinPercent = 80;
|
|
4229
|
+
this.textScaleMaxPercent = 150;
|
|
4230
|
+
this.textScaleStepPercent = 5;
|
|
4231
|
+
this.contrastFilterValues = ['light-contrast', 'dark-contrast'];
|
|
4232
|
+
this.saturationFilterValues = ['low-saturation', 'high-saturation'];
|
|
4233
|
+
|
|
4234
|
+
this.contentOptions = filterAndOverride([
|
|
4235
|
+
{ label: 'Font Weight', key: 'bold-text', icon: this.widgetIcons.boldText },
|
|
4236
|
+
{ label: 'Line Height', key: 'line-spacing', icon: this.widgetIcons.lineSpacing },
|
|
4237
|
+
{ label: 'Letter Spacing', key: 'letter-spacing', icon: this.widgetIcons.letterSpacing },
|
|
4238
|
+
{ label: 'Hide Images', key: 'hide-images', icon: this.widgetIcons.hideImages },
|
|
4239
|
+
{ label: 'Dyslexia Font', key: 'readable-text', icon: this.widgetIcons.dyslexiaFont },
|
|
4240
|
+
{ label: 'Highlight Links', key: 'highlight-links', icon: this.widgetIcons.highlightLinks },
|
|
4241
|
+
{ label: 'Highlight Title', key: 'highlight-title', icon: this.widgetIcons.highlightTitle },
|
|
4242
|
+
{
|
|
4243
|
+
label: 'Font Size',
|
|
4244
|
+
key: 'text-scale',
|
|
4245
|
+
icon: this.widgetIcons.adjustFontSize,
|
|
4246
|
+
multiLevel: true,
|
|
4247
|
+
levels: this.textScaleValues.length
|
|
4248
|
+
}
|
|
4249
|
+
]);
|
|
4250
|
+
|
|
4251
|
+
this.multiLevelFeatures = {
|
|
4252
|
+
'text-scale': {
|
|
4253
|
+
levels: this.textScaleValues.length,
|
|
4254
|
+
currentIndex: -1,
|
|
4255
|
+
values: this.textScaleValues
|
|
4256
|
+
},
|
|
4257
|
+
'contrast-toggle': {
|
|
4258
|
+
levels: this.contrastFilterValues.length,
|
|
4259
|
+
currentIndex: -1,
|
|
4260
|
+
values: this.contrastFilterValues
|
|
4261
|
+
},
|
|
4262
|
+
'saturation-toggle': {
|
|
4263
|
+
levels: this.saturationFilterValues.length,
|
|
4264
|
+
currentIndex: -1,
|
|
4265
|
+
values: this.saturationFilterValues
|
|
4266
|
+
}
|
|
4267
|
+
};
|
|
4268
|
+
|
|
4269
|
+
this.readingAidTemplate = `
|
|
4270
|
+
<div class="acc-rg acc-rg-top" role="presentation"> </div>
|
|
4271
|
+
<div class="acc-rg acc-rg-bottom" role="presentation"> </div>
|
|
4272
|
+
`;
|
|
4273
|
+
|
|
4274
|
+
this.colorOptions = filterAndOverride([
|
|
4275
|
+
{
|
|
4276
|
+
label: 'Contrast',
|
|
4277
|
+
key: 'contrast-toggle',
|
|
4278
|
+
icon: this.widgetIcons.contrast,
|
|
4279
|
+
multiLevel: true,
|
|
4280
|
+
levels: this.contrastFilterValues.length
|
|
4281
|
+
},
|
|
4282
|
+
{ label: 'Invert Colors', key: 'invert-colors', icon: this.widgetIcons.invertColors },
|
|
4283
|
+
{
|
|
4284
|
+
label: 'Saturation',
|
|
4285
|
+
key: 'saturation-toggle',
|
|
4286
|
+
icon: this.widgetIcons.saturation,
|
|
4287
|
+
multiLevel: true,
|
|
4288
|
+
levels: this.saturationFilterValues.length
|
|
4289
|
+
}
|
|
4290
|
+
]);
|
|
4291
|
+
|
|
4292
|
+
this.colorFilterKeys = Object.keys(this.visualFilters);
|
|
4293
|
+
this.activeColorFilterKey = null;
|
|
4294
|
+
|
|
4295
|
+
this.textScaleSelectors = 'h1,h2,h3,h4,h5,h6,p,a,dl,dt,li,ol,th,td,span,blockquote,.acc-text';
|
|
4296
|
+
this.textScaleObserver = null;
|
|
4297
|
+
this.currentTextScaleMultiplier = 1;
|
|
4298
|
+
|
|
4299
|
+
this.widgetConfig = {};
|
|
4300
|
+
|
|
4301
|
+
// Configurable storage key
|
|
4302
|
+
this.storageKey = (typeof options.storageKey === 'string' && options.storageKey.trim())
|
|
4303
|
+
? options.storageKey.trim()
|
|
4304
|
+
: 'ally-wgt';
|
|
4305
|
+
|
|
4306
|
+
this.readingAidVisible = false;
|
|
4307
|
+
this.readableFontLoaded = false;
|
|
4308
|
+
|
|
4309
|
+
// Menu state
|
|
4310
|
+
this.activeMenuContainer = null;
|
|
4311
|
+
this.activeMenuToggle = null;
|
|
4312
|
+
this.menuKeyListener = null;
|
|
4313
|
+
this.previousFocus = null;
|
|
4314
|
+
this.widgetToggleButton = null;
|
|
4315
|
+
this.skipLinkElement = null;
|
|
4316
|
+
this.menuContainer = null;
|
|
4317
|
+
|
|
4318
|
+
this.staticStylesRegistered = false;
|
|
4319
|
+
|
|
4320
|
+
this.dataOptions = this.getDataAttributeOptions();
|
|
4321
|
+
|
|
4322
|
+
this.options = {
|
|
4323
|
+
lang: this.getDefaultLanguage(),
|
|
4324
|
+
position: 'bottom-right',
|
|
4325
|
+
...this.dataOptions,
|
|
4326
|
+
...options
|
|
4327
|
+
};
|
|
4328
|
+
|
|
4329
|
+
this.applyThemeOverrides(this.options);
|
|
4330
|
+
|
|
4331
|
+
const normalizeTtsRate = (value) => {
|
|
4332
|
+
const numeric = Number(value);
|
|
4333
|
+
if (!Number.isFinite(numeric)) return 1;
|
|
4334
|
+
return Math.min(2, Math.max(0.5, numeric));
|
|
4335
|
+
};
|
|
4336
|
+
|
|
4337
|
+
const normalizeTtsPitch = (value) => {
|
|
4338
|
+
const numeric = Number(value);
|
|
4339
|
+
if (!Number.isFinite(numeric)) return 1;
|
|
4340
|
+
return Math.min(2, Math.max(0, numeric));
|
|
4341
|
+
};
|
|
4342
|
+
|
|
4343
|
+
this.nativeTtsConfig = {
|
|
4344
|
+
preferredVoiceName: (
|
|
4345
|
+
typeof options.ttsNativeVoiceName === 'string' &&
|
|
4346
|
+
options.ttsNativeVoiceName.trim()
|
|
4347
|
+
) ? options.ttsNativeVoiceName.trim() : '',
|
|
4348
|
+
preferredVoiceLang: (
|
|
4349
|
+
typeof options.ttsNativeVoiceLang === 'string' &&
|
|
4350
|
+
options.ttsNativeVoiceLang.trim()
|
|
4351
|
+
) ? options.ttsNativeVoiceLang.trim() : '',
|
|
4352
|
+
rate: normalizeTtsRate(options.ttsRate),
|
|
4353
|
+
pitch: normalizeTtsPitch(options.ttsPitch)
|
|
4354
|
+
};
|
|
4355
|
+
|
|
4356
|
+
if (this.options.offset) {
|
|
4357
|
+
this.options.offset = this.normalizeOffset(this.options.offset);
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
if (this.options.size) {
|
|
4361
|
+
this.widgetTheme.buttonSize = this.normalizeButtonSize(this.options.size);
|
|
4362
|
+
this.options.size = this.widgetTheme.buttonSize;
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
this.applyThemeVariables();
|
|
4366
|
+
this.registerStaticStyles();
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
applyThemeOverrides(options = {}) {
|
|
4370
|
+
if (!options || typeof options !== 'object') return;
|
|
4371
|
+
|
|
4372
|
+
const mergedOptions = {
|
|
4373
|
+
...(options.theme && typeof options.theme === 'object' ? options.theme : {}),
|
|
4374
|
+
...options
|
|
4375
|
+
};
|
|
4376
|
+
|
|
4377
|
+
const themeKeys = [
|
|
4378
|
+
'primaryColor', 'primaryColorLight', 'primaryColorDark',
|
|
4379
|
+
'backgroundColor', 'textColor', 'textColorInverted',
|
|
4380
|
+
'cardBackground', 'borderColor', 'focusRingColor',
|
|
4381
|
+
'hoverColor', 'activeColor', 'borderRadius', 'buttonBorderRadius',
|
|
4382
|
+
'headerHeight', 'focusBorderWidth', 'focusOutlineOffset', 'zIndex'
|
|
4383
|
+
];
|
|
4384
|
+
|
|
4385
|
+
themeKeys.forEach((key) => {
|
|
4386
|
+
const value = mergedOptions[key];
|
|
4387
|
+
if (value === undefined || value === null) return;
|
|
4388
|
+
if (typeof value === 'string') {
|
|
4389
|
+
const trimmed = value.trim();
|
|
4390
|
+
if (!trimmed) return;
|
|
4391
|
+
this.widgetTheme[key] = trimmed;
|
|
4392
|
+
return;
|
|
4393
|
+
}
|
|
4394
|
+
this.widgetTheme[key] = value;
|
|
4395
|
+
});
|
|
4396
|
+
|
|
4397
|
+
if (mergedOptions.buttonSize !== undefined && mergedOptions.buttonSize !== null && mergedOptions.buttonSize !== '') {
|
|
4398
|
+
this.widgetTheme.buttonSize = this.normalizeButtonSize(mergedOptions.buttonSize);
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
/** @typedef {typeof stateMethods & typeof styleMethods & typeof featureMethods & typeof uiMethods} AllyWidgetMixedMethods */
|
|
4404
|
+
/** @typedef {AllyWidget & AllyWidgetMixedMethods} AllyWidgetInstance */
|
|
4405
|
+
|
|
4406
|
+
/** @type {AllyWidget['prototype'] & AllyWidgetMixedMethods} */
|
|
4407
|
+
const widgetPrototype = AllyWidget.prototype;
|
|
4408
|
+
|
|
4409
|
+
Object.assign(
|
|
4410
|
+
widgetPrototype,
|
|
4411
|
+
stateMethods,
|
|
4412
|
+
styleMethods,
|
|
4413
|
+
featureMethods,
|
|
4414
|
+
uiMethods
|
|
4415
|
+
);
|
|
4416
|
+
|
|
4417
|
+
/**
|
|
4418
|
+
* AllyWidget.init(options) — programmatic initialisation.
|
|
4419
|
+
*
|
|
4420
|
+
* Use this instead of the auto-init in frameworks (Next.js, Nuxt, SvelteKit…)
|
|
4421
|
+
* where you control when the widget mounts:
|
|
4422
|
+
*
|
|
4423
|
+
* // Next.js app router component (use client)
|
|
4424
|
+
* useEffect(() => { AllyWidget.init({ primaryColor: '#6366f1' }); }, []);
|
|
4425
|
+
*
|
|
4426
|
+
* // CDN
|
|
4427
|
+
* AllyWidget.init({ lang: 'ne' });
|
|
4428
|
+
*
|
|
4429
|
+
* Returns the widget instance, or null when called server-side.
|
|
4430
|
+
*/
|
|
4431
|
+
AllyWidget.init = function init(options = {}) {
|
|
4432
|
+
if (typeof document === 'undefined') return null;
|
|
4433
|
+
const instance = new AllyWidget(options);
|
|
4434
|
+
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
|
4435
|
+
instance.startAccessibleWebWidget();
|
|
4436
|
+
} else {
|
|
4437
|
+
document.addEventListener('DOMContentLoaded', () => instance.startAccessibleWebWidget(), { once: true });
|
|
4438
|
+
}
|
|
4439
|
+
return instance;
|
|
4440
|
+
};
|
|
4441
|
+
|
|
4442
|
+
if (typeof window !== 'undefined') {
|
|
4443
|
+
window.AllyWidget = AllyWidget;
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
// Auto-init for CDN / script-tag usage.
|
|
4447
|
+
// Skipped when:
|
|
4448
|
+
// • running server-side (no document)
|
|
4449
|
+
// • window.AllyWidgetOptions.autoInit === false (opt-out for manual control)
|
|
4450
|
+
if (typeof document !== 'undefined') {
|
|
4451
|
+
const globalAutoInitOptions = (
|
|
4452
|
+
typeof window !== 'undefined' &&
|
|
4453
|
+
window.AllyWidgetOptions &&
|
|
4454
|
+
typeof window.AllyWidgetOptions === 'object'
|
|
4455
|
+
) ? window.AllyWidgetOptions : {};
|
|
4456
|
+
|
|
4457
|
+
if (globalAutoInitOptions.autoInit !== false) {
|
|
4458
|
+
/** @type {AllyWidgetInstance} */
|
|
4459
|
+
const widgetInstance = new AllyWidget(globalAutoInitOptions);
|
|
4460
|
+
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
|
4461
|
+
widgetInstance.startAccessibleWebWidget();
|
|
4462
|
+
} else {
|
|
4463
|
+
document.addEventListener('DOMContentLoaded', () => widgetInstance.startAccessibleWebWidget(), { once: true });
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4468
|
+
module.exports = AllyWidget;
|