1(function () { 2 "use strict"; 3 4 // Configuration for the accessibility widget 5 const WIDGET_CONFIG = { 6 storageKey: "a11y-widget-settings", 7 defaultLang: "hr", 8 translations: { 9 de: { 10 title: "Barrierefreiheit-Einstellungen", 11 readingMask: "Lesemaske", 12 highContrast: "Hoher Kontrast", 13 fontSize: "SchriftgröÃe", 14 lineHeight: "Zeilenhöhe", 15 letterSpacing: "Buchstabenabstand", 16 contentScale: "Inhaltsskalierung", 17 dyslexiaFont: "Lesbare Schriftart", 18 textAlign: "Rechts Ausrichten", 19 textAlignCenter: "Im Zentrum", 20 textAlignLeft: "Linksbündig", 21 highlightLinks: "Links unterstreichen", 22 hideImages: "Bilder ausblenden", 23 highlightHeadings: "Titel hervorheben", 24 textMagnifier: "Cursor Lesehilfe", 25 reset: "Zurücksetzen", 26 poweredBy: "Powered by", 27 darkMode: "Dunkler Modus", 28 focusOutline: "Fokus-Rahmen anzeigen", 29 reduceMotion: "Animationen reduzieren", 30 cursorSize: "Cursor vergröÃern", 31 stopAutoplay: "Autoplay stoppen", 32 invertColors: "Farben invertieren", 33 columnWidth: "Spaltenbreite", 34 }, 35 en: { 36 title: "Accessibility Settings", 37 readingMask: "Reading Mask", 38 highContrast: "High Contrast", 39 fontSize: "Font Size", 40 lineHeight: "Line Height", 41 letterSpacing: "Letter Spacing", 42 contentScale: "Content Scale", 43 dyslexiaFont: "Readable Font", 44 textAlign: "Align Right", 45 textAlignCenter: "Center", 46 textAlignLeft: "Align Left", 47 highlightLinks: "Underline Links", 48 hideImages: "Hide Images", 49 highlightHeadings: "Highlight Titles", 50 textMagnifier: "Cursor Reading Aid", 51 reset: "Reset", 52 poweredBy: "Powered by", 53 darkMode: "Dark Mode", 54 focusOutline: "Show Focus Outline", 55 reduceMotion: "Reduce Motion", 56 cursorSize: "Large Cursor", 57 stopAutoplay: "Stop Autoplay", 58 invertColors: "Invert Colors", 59 columnWidth: "Column Width", 60 }, 61 hr: { 62 title: "Postavke pristupaÄnosti", 63 readingMask: "Maska za Äitanje", 64 highContrast: "Visoki kontrast", 65 fontSize: "VeliÄina fonta", 66 lineHeight: "Visina retka", 67 letterSpacing: "Razmak slova", 68 contentScale: "Skaliranje sadržaja", 69 dyslexiaFont: "Äitljiv font", 70 textAlign: "Poravnaj desno", 71 textAlignCenter: "Centriraj", 72 textAlignLeft: "Poravnaj lijevo", 73 highlightLinks: "Podcrtaj poveznice", 74 hideImages: "Sakrij slike", 75 highlightHeadings: "Istakni naslove", 76 textMagnifier: "PomoÄ za Äitanje pokazivaÄem", 77 reset: "Resetiraj", 78 poweredBy: "PokreÄe", 79 darkMode: "Tamni naÄin", 80 focusOutline: "Prikaži obrub fokusa", 81 reduceMotion: "Smanji animacije", 82 cursorSize: "Veliki kursor", 83 stopAutoplay: "Zaustavi automatsku reprodukciju", 84 invertColors: "Invertiraj boje", 85 columnWidth: "Å irina stupca" 86 } 87 }, 88 }; 89 90 // Check if FontAwesome is already available 91 function hasFontAwesome() { 92 // First check if FontAwesome CSS is already loaded 93 if (document.querySelector('link[rel="stylesheet"][href*="fontawesome"]')) { 94 return true; 95 } 96 97 // Fallback check by testing the font family 98 const probe = document.createElement("i"); 99 probe.className = "fas"; 100 probe.style.display = "none"; 101 document.body.appendChild(probe); 102 103 const fontFamily = getComputedStyle(probe).fontFamily || ""; 104 probe.remove(); 105 106 return /Font Awesome/i.test(fontFamily); 107 } 108 109 class AccessibilityWidget { 110 constructor() { 111 this.settings = this.loadSettings(); 112 this.currentLang = this.settings.language || WIDGET_CONFIG.defaultLang; 113 this.translations = WIDGET_CONFIG.translations[this.currentLang]; 114 this.isOpen = false; 115 this.observers = []; 116 this.init(); 117 } 118 119 init() { 120 this.loadExternalResources(); 121 this.injectStyles(); 122 this.createWidget(); 123 this.attachEventListeners(); 124 this.setupSPAListeners(); 125 this.applySettings();
126 } 127 128 // Set up listeners for Single Page Application behavior 129 setupSPAListeners() { 130 // Watch for DOM changes using MutationObserver 131 const observer = new MutationObserver((mutations) => { 132 let shouldReapply = false; 133 134 mutations.forEach((mutation) => { 135 // Check for new nodes being added 136 if (mutation.type === "childList" && mutation.addedNodes.length > 0) { 137 for (let node of mutation.addedNodes) { 138 if (node.nodeType === Node.ELEMENT_NODE) { 139 // Look for Flarum-specific elements 140 if ( 141 node.matches && 142 (node.matches(".Post-body, .DiscussionListItem, .UserCard, .CommentPost") || 143 node.querySelector(".Post-body, .DiscussionListItem, .UserCard, .CommentPost")) 144 ) { 145 shouldReapply = true; 146 break; 147 } 148 } 149 } 150 } 151 }); 152 153 if (shouldReapply) { 154 // Give Flarum time to finish rendering before applying styles 155 setTimeout(() => { 156 this.applySettings(); 157 }, 100); 158 } 159 }); 160 161 // Monitor the entire document for changes 162 observer.observe(document.body, { 163 childList: true, 164 subtree: true, 165 }); 166 167 this.observers.push(observer); 168 169 // Intercept browser navigation for SPA 170 const originalPushState = history.pushState; 171 const originalReplaceState = history.replaceState; 172 173 history.pushState = function (...args) { 174 originalPushState.apply(history, args); 175 setTimeout(() => { 176 if (window.accessibilityWidget) { 177 window.accessibilityWidget.applySettings(); 178 } 179 }, 200); 180 }; 181 182 history.replaceState = function (...args) { 183 originalReplaceState.apply(history, args); 184 setTimeout(() => { 185 if (window.accessibilityWidget) { 186 window.accessibilityWidget.applySettings(); 187 } 188 }, 200); 189 }; 190 191 // Handle back/forward navigation 192 window.addEventListener("popstate", () => { 193 setTimeout(() => { 194 this.applySettings(); 195 }, 200); 196 }); 197 198 // Handle hash changes 199 window.addEventListener("hashchange", () => { 200 setTimeout(() => { 201 this.applySettings(); 202 }, 200); 203 }); 204 205 // Fallback timer for very dynamic content 206 this.reapplyInterval = setInterval(() => { 207 this.applySettings(); 208 }, 2000); // Every 2 seconds 209 } 210 211 loadExternalResources() { 212 const basePath = (document.currentScript?.src || "").split("/").slice(0, -1).join("/") + "/"; 213 214 // Load FontAwesome only if it's not already available 215 if (!hasFontAwesome()) { 216 const link = document.createElement("link"); 217 link.id = "fontawesome-local"; 218 link.rel = "stylesheet"; 219 link.href = basePath + "css/all.min.css"; 220 document.head.appendChild(link); 221 } 222 223 // Load Lexend font for dyslexia support 224 if (!document.getElementById("lexend-embed")) { 225 const css = ` 226 @font-face{ 227 font-family:'Lexend'; 228 font-style:normal; 229 font-weight:400; 230 font-display:swap; 231 src:url('${basePath}webfonts/lexend-400.woff2') format('woff2'); 232 } 233 @font-face{ 234 font-family:'Lexend'; 235 font-style:normal; 236 font-weight:700; 237 font-display:swap; 238 src:url('${basePath}webfonts/lexend-700.woff2') format('woff2'); 239 }`; 240 const style = document.createElement("style"); 241 style.id = "lexend-embed"; 242 style.textContent = css; 243 document.head.appendChild(style); 244 } 245 } 246 247 injectStyles() { 248 if (document.getElementById("a11y-widget-styles")) return; 249 250 const style = document.createElement("style"); 251 style.id = "a11y-widget-styles"; 252 style.textContent = ` 253 /* Widget Container */ 254 #a11y-widget-container { 255 position: fixed; 256 bottom: 20px; 257 right: 20px; 258 z-index: 999999; 259 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; 260 } 261 262 /* Toggle Button */ 263 #a11y-toggle-btn { 264 width: 56px; 265 height: 56px; 266 border-radius: 50%; 267 background: #2563eb; 268 color: white; 269 border: none; 270 cursor: pointer; 271 box-shadow: 0 4px 12px rgba(0,0,0,0.15); 272 display: flex; 273 align-items: center; 274 justify-content: center; 275 transition: all 0.3s ease; 276 font-size: 24px; 277 } 278 279 #a11y-toggle-btn:hover { 280 transform: translateY(-2px); 281 box-shadow: 0 6px 20px rgba(0,0,0,0.2); 282 } 283 284 /* Panel */ 285 #a11y-panel { 286 position: absolute; 287 bottom: 76px; 288 right: 0; 289 width: 380px; 290 max-width: calc(100vw - 40px); 291 max-height: calc(100vh - 120px); 292 background: white; 293 border-radius: 16px; 294 box-shadow: 0 10px 40px rgba(0,0,0,0.1); 295 display: none; 296 overflow: hidden; 297 } 298 299 @media (max-height: 700px) { 300 #a11y-panel { 301 bottom: 10px; 302 max-height: calc(100vh - 80px); 303 } 304 } 305 306 @media (max-width: 420px) { 307 #a11y-panel { 308 width: calc(100vw - 40px); 309 right: 50%; 310 transform: translateX(50%); 311 } 312 } 313 314 #a11y-panel.active { 315 display: flex; 316 flex-direction: column; 317 animation: slideIn 0.3s ease; 318 } 319 320 @keyframes slideIn { 321 from { opacity: 0; transform: translateY(20px); } 322 to { opacity: 1; transform: translateY(0); } 323 } 324 325 /* Header */ 326 .a11y-header { 327 background: #f8fafc; 328 padding: 20px; 329 border-bottom: 1px solid #e5e7eb; 330 display: flex; 331 justify-content: space-between; 332 align-items: center; 333 } 334 335 .a11y-header h3 { 336 margin: 0; 337 font-size: 18px; 338 font-weight: 600; 339 color: #1f2937; 340 } 341 342 .a11y-header-actions { 343 display: flex; 344 gap: 8px; 345 align-items: center; 346 } 347 348 .a11y-lang-toggle { 349 display: flex; 350 background: #e5e7eb; 351 border-radius: 8px; 352 padding: 2px; 353 } 354 355 .a11y-lang-btn { 356 padding: 6px 12px; 357 border: none; 358 background: transparent; 359 cursor: pointer; 360 border-radius: 6px; 361 font-size: 12px; 362 font-weight: 500; 363 color: #6b7280; 364 transition: all 0.2s; 365 } 366 367 .a11y-lang-btn.active { 368 background: white; 369 color: #1f2937; 370 box-shadow: 0 1px 3px rgba(0,0,0,0.1); 371 } 372 373 .a11y-close-btn { 374 background: none; 375 border: none; 376 color: #6b7280; 377 font-size: 20px; 378 cursor: pointer; 379 padding: 4px; 380 width: 32px; 381 height: 32px; 382 border-radius: 6px; 383 transition: all 0.2s; 384 } 385 386 .a11y-close-btn:hover { 387 background: #e5e7eb; 388 color: #1f2937; 389 } 390 391 /* Content */ 392 .a11y-content { 393 padding: 20px; 394 overflow-y: auto; 395 max-height: calc(100vh - 380px); 396 } 397 398 @media (max-height: 700px) { 399 .a11y-content { 400 padding: 15px; 401 max-height: calc(100vh - 340px); 402 } 403 } 404 405 /* Grid */ 406 .a11y-grid { 407 display: grid; 408 grid-template-columns: repeat(2, 1fr); 409 gap: 12px; 410 margin-bottom: 20px; 411 } 412 413 /* Cards */ 414 .a11y-card { 415 background: #f8fafc; 416 border: 1px solid #e5e7eb; 417 border-radius: 12px; 418 padding: 16px; 419 text-align: center; 420 cursor: pointer; 421 transition: all 0.2s; 422 } 423 424 .a11y-card:hover { 425 border-color: #2563eb; 426 box-shadow: 0 2px 8px rgba(37, 99, 235, 0.1); 427 } 428 429 .a11y-card.active { 430 background: #dbeafe; 431 border-color: #2563eb; 432 } 433 434 .a11y-card-icon { 435 font-size: 24px; 436 margin-bottom: 8px; 437 color: #1f2937; 438 } 439 440 .a11y-card.active .a11y-card-icon { 441 color: #2563eb; 442 } 443 444 .a11y-card-label { 445 font-size: 13px; 446 color: #1f2937; 447 font-weight: 500; 448 } 449 450 /* Sliders */ 451 .a11y-slider-group { 452 background: #f8fafc; 453 border-radius: 12px; 454 padding: 16px; 455 margin-bottom: 12px; 456 } 457 458 .a11y-slider-header { 459 display: flex; 460 justify-content: space-between; 461 align-items: center; 462 margin-bottom: 12px; 463 } 464 465 .a11y-slider-label { 466 font-size: 14px; 467 font-weight: 500; 468 color: #1f2937; 469 } 470 471 .a11y-slider-value { 472 font-size: 14px; 473 color: #6b7280; 474 font-weight: 500; 475 } 476
477 .a11y-slider-controls { 478 display: flex; 479 align-items: center; 480 gap: 12px; 481 } 482 483 .a11y-slider-btn { 484 width: 32px; 485 height: 32px; 486 border-radius: 50%; 487 background: white; 488 border: 1px solid #e5e7eb; 489 display: flex; 490 align-items: center; 491 justify-content: center; 492 cursor: pointer; 493 transition: all 0.2s; 494 color: #1f2937; 495 } 496 497 .a11y-slider-btn:hover { 498 background: #f3f4f6; 499 border-color: #d1d5db; 500 } 501 502 .a11y-slider { 503 flex: 1; 504 height: 4px; 505 -webkit-appearance: none; 506 appearance: none; 507 background: #e5e7eb; 508 border-radius: 2px; 509 outline: none; 510 cursor: pointer; 511 } 512 513 .a11y-slider::-webkit-slider-thumb { 514 -webkit-appearance: none; 515 appearance: none; 516 width: 20px; 517 height: 20px; 518 background: #2563eb; 519 border-radius: 50%; 520 cursor: pointer; 521 box-shadow: 0 1px 4px rgba(0,0,0,0.2); 522 transition: all 0.2s; 523 } 524 525 .a11y-slider::-webkit-slider-thumb:hover { 526 transform: scale(1.2); 527 } 528 529 .a11y-slider::-moz-range-thumb { 530 width: 20px; 531 height: 20px; 532 background: #2563eb; 533 border-radius: 50%; 534 cursor: pointer; 535 border: none; 536 box-shadow: 0 1px 4px rgba(0,0,0,0.2); 537 } 538 539 /* Footer */ 540 .a11y-footer { 541 padding: 16px 20px; 542 border-top: 1px solid #e5e7eb; 543 background: #f8fafc; 544 } 545 546 .a11y-btn { 547 width: 100%; 548 padding: 12px; 549 border: none; 550 border-radius: 8px; 551 cursor: pointer; 552 font-size: 14px; 553 font-weight: 500; 554 transition: all 0.2s; 555 display: flex; 556 align-items: center; 557 justify-content: center; 558 gap: 8px; 559 margin-bottom: 20px; 560 } 561 562 .a11y-btn-reset { 563 background: #ef4444; 564 color: white; 565 } 566 567 .a11y-btn-reset:hover { 568 background: #dc2626; 569 } 570 571 .a11y-reset-all { 572 padding: 0 20px 20px 20px; 573 background: #f8fafc; 574 } 575 576 .a11y-reset-all button { 577 width: 100%; 578 padding: 10px; 579 background: #f3f4f6; 580 border: 1px solid #e5e7eb; 581 border-radius: 8px; 582 color: #6b7280; 583 font-size: 13px; 584 cursor: pointer; 585 transition: all 0.2s; 586 display: flex; 587 align-items: center; 588 justify-content: center; 589 gap: 6px; 590 } 591 592 .a11y-reset-all button:hover { 593 background: #fee2e2; 594 border-color: #fca5a5;
595 color: #dc2626; 596 } 597 598 .a11y-powered-by { 599 padding: 15px 20px; 600 background: #f1f5f9; 601 text-align: center; 602 border-top: 1px solid #e5e7eb; 603 font-size: 12px; 604 color: #64748b; 605 border-radius: 0 0 16px 16px; 606 } 607 608 .a11y-powered-by a { 609 color: #2563eb; 610 text-decoration: none; 611 font-weight: 500; 612 } 613 614 .a11y-powered-by a:hover { 615 text-decoration: underline; 616 } 617 618 /* Enhanced Flarum selectors for better coverage */ 619 .a11y-flarum-text { 620 font-size: inherit !important; 621 } 622 623 /* Accessibility Features */ 624 body.a11y-dyslexia *:not([class*="fa"]) { 625 font-family: 'Lexend', Arial, sans-serif !important; 626 } 627 628 body.a11y-highlight-links a { 629 text-decoration: underline !important; 630 text-decoration-thickness: 2px !important; 631 text-underline-offset: 2px !important; 632 } 633 634 body.a11y-hide-images img { 635 opacity: 0 !important; 636 visibility: hidden !important; 637 } 638 639 body.a11y-highlight-headings h1::before, 640 body.a11y-highlight-headings h2::before, 641 body.a11y-highlight-headings h3::before, 642 body.a11y-highlight-headings h4::before, 643 body.a11y-highlight-headings h5::before, 644 body.a11y-highlight-headings h6::before { 645 content: '' !important; 646 position: absolute !important; 647 left: 0 !important; 648 top: 0 !important; 649 bottom: 0 !important; 650 width: 4px !important; 651 background: #2563eb !important; 652 } 653 654 body.a11y-highlight-headings h1, 655 body.a11y-highlight-headings h2, 656 body.a11y-highlight-headings h3, 657 body.a11y-highlight-headings h4, 658 body.a11y-highlight-headings h5, 659 body.a11y-highlight-headings h6 { 660 position: relative !important; 661 padding-left: 20px !important; 662 } 663 664 /* High Contrast */ 665 body.a11y-high-contrast { 666 background: #000 !important; 667 color: #fff !important; 668 } 669 670 body.a11y-high-contrast * { 671 background-color: #000 !important; 672 color: #fff !important; 673 border-color: #fff !important; 674 } 675 676 body.a11y-high-contrast a { 677 color: #ffff00 !important; 678 } 679 680 body.a11y-high-contrast button { 681 background: #fff !important; 682 color: #000 !important; 683 } 684 685 /* Dark Mode */ 686 body.a11y-dark-mode { 687 background: #1a1a1a !important; 688 color: #e0e0e0 !important; 689 } 690 691 body.a11y-dark-mode * { 692 background-color: #1a1a1a !important; 693 color: #e0e0e0 !important; 694 border-color: #444 !important; 695 } 696 697 body.a11y-dark-mode a { 698 color: #66b3ff !important; 699 } 700 701 /* Focus Outline */ 702 body.a11y-focus-outline *:focus { 703 outline: 6px solid #ff0000 !important; 704 outline-offset: 6px !important; 705 } 706 707 /* Reduce Motion */ 708 body.a11y-reduce-motion * { 709 animation: none !important; 710 transition: none !important; 711 } 712 713 /* Large Cursor */ 714 body.a11y-cursor-size * { 715 cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path fill="black" stroke="white" stroke-width="2" d="M8 8 L28 20 L20 24 L24 36 L20 38 L16 26 L8 30 Z"/></svg>') 0 0, auto !important; 716 } 717 718 /* Stop Autoplay */ 719 body.a11y-stop-autoplay video, 720 body.a11y-stop-autoplay audio { 721 autoplay: false !important; 722 pause: true !important; 723 } 724 725 /* Invert Colors */ 726 html.a11y-invert::before{ 727 content:""; 728 position:fixed; 729 inset:0; 730 background:#fff; 731 mix-blend-mode:difference; 732 pointer-events:none; 733 z-index:2147483647; 734 } 735 736 html.a11y-invert img, 737 html.a11y-invert video{ 738 mix-blend-mode:normal; 739 } 740 741 /* Reading Mask */ 742 #a11y-reading-mask { 743 position: fixed; 744 left: 0; 745 right: 0; 746 height: 120px; 747 pointer-events: none; 748 z-index: 999998; 749 display: none; 750 } 751 752 #a11y-reading-mask::before, 753 #a11y-reading-mask::after { 754 content: ''; 755 position: absolute; 756 left: 0; 757 right: 0; 758 background: rgba(0,0,0,0.3); 759 } 760 761 #a11y-reading-mask::before { 762 top: -100vh; 763 height: 100vh; 764 } 765 766 #a11y-reading-mask::after { 767 bottom: -100vh; 768 height: 100vh; 769 } 770 771 body.a11y-reading-mask #a11y-reading-mask { 772 display: block; 773 } 774 775 /* Text Magnifier */ 776 .a11y-magnifier { 777 position: fixed; 778 width: 400px; 779 max-width: 90vw; 780 padding: 20px; 781 background: rgba(255, 255, 255, 0.98); 782 border: 2px solid #2563eb; 783 border-radius: 8px; 784 box-shadow: 0 8px 32px rgba(0,0,0,0.2); 785 pointer-events: none; 786 z-index: 999997; 787 display: none; 788 font-size: 150%; 789 line-height: 1.6; 790 word-wrap: break-word; 791 color: #000; 792 } 793 794 body.a11y-text-magnifier .a11y-magnifier { 795 display: block; 796 } 797 798 .a11y-magnifier::before { 799 content: ''; 800 position: absolute; 801 top: 50%; 802 left: 50%; 803 transform: translate(-50%, -50%); 804 width: 2px; 805 height: 20px; 806 background: rgba(37, 99, 235, 0.3); 807 pointer-events: none; 808 } 809 810 /* Responsive styles */ 811 @media (max-width: 420px) { 812 .a11y-grid { 813 grid-template-columns: 1fr; 814 gap: 8px; 815 } 816 817 .a11y-slider-group { 818 padding: 12px; 819 } 820 821 .a11y-content { 822 padding: 15px; 823 } 824 } 825 `; 826 document.head.appendChild(style); 827 } 828 829 createWidget() { 830 // Prevent creating duplicate widgets 831 if (document.getElementById("a11y-widget-container")) return; 832 833 const container = document.createElement("div"); 834 container.id = "a11y-widget-container"; 835 836 // Create the main toggle button 837 const toggleBtn = document.createElement("button"); 838 toggleBtn.id = "a11y-toggle-btn"; 839 toggleBtn.setAttribute("aria-label", this.translations.title); 840 toggleBtn.innerHTML = '<i class="fas fa-universal-access"></i>'; 841 842 // Create the settings panel 843 const panel = document.createElement("div"); 844 panel.id = "a11y-panel"; 845 panel.innerHTML = this.getPanelHTML(); 846 847 container.appendChild(toggleBtn); 848 container.appendChild(panel); 849 document.body.appendChild(container); 850 851 // Create additional accessibility elements 852 this.createReadingMask(); 853 this.createMagnifier(); 854 } 855 856 getPanelHTML() { 857 return ` 858 <div class="a11y-header"> 859 <h3>${this.translations.title}</h3> 860 <div class="a11y-header-actions"> 861 <div class="a11y-lang-toggle"> 862 <button class="a11y-lang-btn ${this.currentLang === "hr" ? "active" : ""}" data-lang="hr">HR</button> 863 <button class="a11y-lang-btn ${this.currentLang === "en" ? "active" : ""}" data-lang="en">EN</button> 864 </div> 865 <button class="a11y-close-btn" aria-label="Close"> 866 <i class="fas fa-times"></i> 867 </button> 868 </div> 869 </div> 870 <div class="a11y-content"> 871 <div class="a11y-grid"> 872 ${this.createFeatureCards()} 873 </div> 874 ${this.createSliders()} 875 </div> 876 <div class="a11y-footer"> 877 <button class="a11y-btn a11y-btn-reset"> 878 <i class="fas fa-undo"></i>
879 <span>${this.translations.reset}</span> 880 </button> 881 </div> 882 <div class="a11y-powered-by"> 883 ${this.translations.poweredBy} <a href="https://www.accessibility-widget.net" target="_blank" rel="noopener">Accessibility Widget</a> 884 </div> 885 `; 886 } 887 888 createFeatureCards() { 889 const features = [ 890 { id: "readingMask", icon: "fa-eye", label: this.translations.readingMask }, 891 { id: "highContrast", icon: "fa-adjust", label: this.translations.highContrast }, 892 { id: "darkMode", icon: "fa-moon", label: this.translations.darkMode }, 893 { id: "dyslexiaFont", icon: "fa-font", label: this.translations.dyslexiaFont }, 894 { id: "textAlignRight", icon: "fa-align-right", label: this.translations.textAlign }, 895 { id: "textAlignCenter", icon: "fa-align-center", label: this.translations.textAlignCenter }, 896 { id: "textAlignLeft", icon: "fa-align-left", label: this.translations.textAlignLeft }, 897 { id: "highlightLinks", icon: "fa-link", label: this.translations.highlightLinks }, 898 { id: "hideImages", icon: "fa-image", label: this.translations.hideImages }, 899 { id: "highlightHeadings", icon: "fa-heading", label: this.translations.highlightHeadings }, 900 { id: "textMagnifier", icon: "fa-search-plus", label: this.translations.textMagnifier }, 901 { id: "focusOutline", icon: "fa-square", label: this.translations.focusOutline }, 902 { id: "reduceMotion", icon: "fa-pause", label: this.translations.reduceMotion }, 903 { id: "cursorSize", icon: "fa-mouse-pointer", label: this.translations.cursorSize }, 904 { id: "stopAutoplay", icon: "fa-stop", label: this.translations.stopAutoplay }, 905 { id: "invertColors", icon: "fa-exchange-alt", label: this.translations.invertColors }, 906 ]; 907 908 return features 909 .map((f) => { 910 const isActive = this.isFeatureActive(f.id); 911 return ` 912 <div class="a11y-card ${isActive ? "active" : ""}" data-feature="${f.id}"> 913 <div class="a11y-card-icon"> 914 <i class="fas ${f.icon}"></i> 915 </div> 916 <div class="a11y-card-label">${f.label}</div> 917 </div> 918 `; 919 }) 920 .join(""); 921 } 922 923 createSliders() { 924 const sliders = [ 925 { id: "fontSize", label: this.translations.fontSize, min: 80, max: 150 }, 926 { id: "lineHeight", label: this.translations.lineHeight, min: 100, max: 200 }, 927 { id: "letterSpacing", label: this.translations.letterSpacing, min: 100, max: 150 }, 928 { id: "contentScale", label: this.translations.contentScale, min: 80, max: 130 }, 929 { id: "columnWidth", label: this.translations.columnWidth, min: 400, max: 1200 }, 930 ]; 931 932 return sliders 933 .map( 934 (s) => ` 935 <div class="a11y-slider-group"> 936 <div class="a11y-slider-header"> 937 <span class="a11y-slider-label">${s.label}</span> 938 <span class="a11y-slider-value" id="${s.id}Value">${this.settings[s.id] || (s.id === "columnWidth" ? 600 : 100)}${ 939 s.id === "columnWidth" ? "px" : "%" 940 }</span> 941 </div> 942 <div class="a11y-slider-controls"> 943 <button class="a11y-slider-btn" data-action="decrease" data-target="${s.id}"> 944 <i class="fas fa-minus"></i> 945 </button> 946 <input type="range" class="a11y-slider" id="${s.id}" 947 min="${s.min}" max="${s.max}" value="${this.settings[s.id] || (s.id === "columnWidth" ? 600 : 100)}"> 948 <button class="a11y-slider-btn" data-action="increase" data-target="${s.id}"> 949 <i class="fas fa-plus"></i> 950 </button> 951 </div> 952 </div> 953 ` 954 ) 955 .join(""); 956 } 957 958 createReadingMask() { 959 if (!document.getElementById("a11y-reading-mask")) { 960 const mask = document.createElement("div"); 961 mask.id = "a11y-reading-mask"; 962 document.body.appendChild(mask); 963 } 964 } 965 966 createMagnifier() { 967 if (!document.querySelector(".a11y-magnifier")) { 968 const magnifier = document.createElement("div"); 969 magnifier.className = "a11y-magnifier"; 970 magnifier.setAttribute("aria-hidden", "true"); 971 document.body.appendChild(magnifier); 972 } 973 } 974 975 isFeatureActive(feature) { 976 if (feature.startsWith("textAlign")) { 977 const align = feature.replace("textAlign", "").toLowerCase(); 978 return this.settings.textAlign === align; 979 } 980 return this.settings[feature] || false; 981 } 982 983 attachEventListeners() { 984 // Main toggle button 985 document.getElementById("a11y-toggle-btn").addEventListener("click", () => { 986 this.togglePanel(); 987 }); 988 989 // Close button 990 document.querySelector(".a11y-close-btn").addEventListener("click", () => { 991 this.togglePanel(); 992 }); 993 994 // Language toggle buttons 995 document.querySelectorAll(".a11y-lang-btn").forEach((btn) => { 996 btn.addEventListener("click", (e) => { 997 this.changeLanguage(e.target.dataset.lang); 998 }); 999 }); 1000 1001 // Feature toggle cards 1002 document.querySelectorAll(".a11y-card").forEach((card) => { 1003 card.addEventListener("click", () => {
1004 this.toggleFeature(card.dataset.feature, card); 1005 }); 1006 }); 1007 1008 // Range sliders 1009 document.querySelectorAll(".a11y-slider").forEach((slider) => { 1010 slider.addEventListener("input", (e) => { 1011 const id = e.target.id; 1012 const value = e.target.value; 1013 const suffix = id === "columnWidth" ? "px" : "%"; 1014 document.getElementById(id + "Value").textContent = value + suffix; 1015 this.settings[id] = parseInt(value); 1016 this.applySettings(); 1017 this.saveSettings(); 1018 }); 1019 }); 1020 1021 // Slider increment/decrement buttons 1022 document.querySelectorAll(".a11y-slider-btn").forEach((btn) => { 1023 btn.addEventListener("click", () => { 1024 const action = btn.dataset.action; 1025 const target = btn.dataset.target; 1026 const slider = document.getElementById(target); 1027 const current = parseInt(slider.value); 1028 const step = target === "columnWidth" ? 50 : 1; 1029 1030 if (action === "increase") { 1031 slider.value = Math.min(current + step, parseInt(slider.max)); 1032 } else { 1033 slider.value = Math.max(current - step, parseInt(slider.min)); 1034 } 1035 1036 slider.dispatchEvent(new Event("input", { bubbles: true })); 1037 }); 1038 }); 1039 1040 // Reset button 1041 document.querySelector(".a11y-btn-reset").addEventListener("click", () => { 1042 this.resetSettings(); 1043 }); 1044 1045 // Set up mouse tracking for reading aids 1046 this.initMouseTracking(); 1047 } 1048 1049 initMouseTracking() { 1050 let magnifierTimeout; 1051 1052 document.addEventListener("mousemove", (e) => { 1053 // Update reading mask position 1054 if (this.settings.readingMask) { 1055 const mask = document.getElementById("a11y-reading-mask"); 1056 if (mask) { 1057 mask.style.top = e.clientY - 60 + "px"; 1058 } 1059 } 1060 1061 // Update text magnifier 1062 if (this.settings.textMagnifier) { 1063 clearTimeout(magnifierTimeout); 1064 magnifierTimeout = setTimeout(() => { 1065 this.updateMagnifier(e); 1066 }, 10); 1067 } 1068 }); 1069 } 1070 1071 updateMagnifier(e) { 1072 const magnifier = document.querySelector(".a11y-magnifier"); 1073 if (!magnifier) return; 1074 1075 // Temporarily hide magnifier to get the element underneath 1076 magnifier.style.display = "none"; 1077 const targetElement = document.elementFromPoint(e.clientX, e.clientY); 1078 magnifier.style.display = "block"; 1079 1080 if (!targetElement || targetElement === document.body || targetElement === document.documentElement) { 1081 magnifier.style.display = "none"; 1082 return; 1083 } 1084 1085 // Don't show magnifier over the widget itself 1086 if (targetElement.closest("#a11y-widget-container")) { 1087 magnifier.style.display = "none"; 1088 return; 1089 } 1090 1091 // Find the text container 1092 let textContainer = targetElement; 1093 const blockTags = ["P", "DIV", "ARTICLE", "SECTION", "LI", "TD", "TH", "H1", "H2", "H3", "H4", "H5", "H6", "SPAN", "A"]; 1094 1095 // Look for the nearest element with text content 1096 while (textContainer && textContainer.textContent.trim().length === 0) { 1097 textContainer = textContainer.parentElement; 1098 } 1099 1100 // For inline elements, try to get the parent block 1101 if (textContainer && !blockTags.includes(textContainer.tagName)) { 1102 const parent = textContainer.closest(blockTags.join(",")); 1103 if (parent) textContainer = parent; 1104 } 1105 1106 if (!textContainer || !textContainer.textContent.trim()) { 1107 magnifier.style.display = "none"; 1108 return; 1109 } 1110 1111 // Extract and clean text content 1112 const fullText = textContainer.textContent.trim().replace(/\s+/g, " "); 1113 const words = fullText.split(" "); 1114 1115 if (words.length === 0) { 1116 magnifier.style.display = "none"; 1117 return; 1118 } 1119 1120 // Calculate relative position for word estimation 1121 const rect = textContainer.getBoundingClientRect(); 1122 const relativeX = (e.clientX - rect.left) / rect.width; 1123 const relativeY = (e.clientY - rect.top) / rect.height; 1124 1125 // Estimate which word the cursor is near 1126 const estimatedPosition = Math.floor(words.length * (relativeY * 0.7 + relativeX * 0.3)); 1127 const wordIndex = Math.max(0, Math.min(estimatedPosition, words.length - 1)); 1128 1129 // Show a window of words around the cursor 1130 const wordsToShow = 20; 1131 const wordsBeforeCursor = 8; 1132 1133 let startIndex = Math.max(0, wordIndex - wordsBeforeCursor); 1134 let endIndex = Math.min(words.length, startIndex + wordsToShow); 1135 1136 // Adjust if we're near the end 1137 if (endIndex - startIndex < wordsToShow && startIndex > 0) { 1138 startIndex = Math.max(0, endIndex - wordsToShow); 1139 } 1140 1141 // Build the display text 1142 let displayText = words.slice(startIndex, endIndex).join(" "); 1143 1144 // Add ellipsis for truncated text 1145 if (startIndex > 0) displayText = "... " + displayText; 1146 if (endIndex < words.length) displayText = displayText + " ..."; 1147 1148 // Update magnifier content 1149 magnifier.textContent = displayText; 1150 1151 // Position the magnifier near the cursor 1152 let left = e.clientX + 20; 1153 let top = e.clientY - magnifier.offsetHeight - 10; 1154 1155 // Keep magnifier within viewport 1156 const magnifierWidth = magnifier.offsetWidth; 1157 const magnifierHeight = magnifier.offsetHeight; 1158 1159 if (left + magnifierWidth > window.innerWidth - 20) { 1160 left = e.clientX - magnifierWidth - 20; 1161 } 1162 if (top < 20) { 1163 top = e.clientY + 20; 1164 } 1165 if (top + magnifierHeight > window.innerHeight - 20) { 1166 top = window.innerHeight - magnifierHeight - 20; 1167 } 1168 1169 magnifier.style.left = left + "px"; 1170 magnifier.style.top = top + "px"; 1171 } 1172
1173 toggleFeature(feature, card) { 1174 if (feature.startsWith("textAlign")) { 1175 // Handle text alignment features 1176 document.querySelectorAll('[data-feature^="textAlign"]').forEach((c) => { 1177 c.classList.remove("active"); 1178 }); 1179 1180 const align = feature.replace("textAlign", "").toLowerCase(); 1181 if (this.settings.textAlign === align) { 1182 this.settings.textAlign = "default"; 1183 } else { 1184 this.settings.textAlign = align; 1185 card.classList.add("active"); 1186 } 1187 } else { 1188 // Handle boolean toggle features 1189 this.settings[feature] = !this.settings[feature]; 1190 card.classList.toggle("active"); 1191 } 1192 1193 this.applySettings(); 1194 this.saveSettings(); 1195 } 1196 1197 changeLanguage(lang) { 1198 this.currentLang = lang; 1199 this.translations = WIDGET_CONFIG.translations[lang]; 1200 this.settings.language = lang; 1201 1202 // Update language toggle buttons 1203 document.querySelectorAll(".a11y-lang-btn").forEach((btn) => { 1204 btn.classList.toggle("active", btn.dataset.lang === lang); 1205 }); 1206 1207 // Update all UI text 1208 this.updateUITexts(); 1209 this.saveSettings(); 1210 } 1211 1212 updateUITexts() { 1213 // Update header title 1214 document.querySelector(".a11y-header h3").textContent = this.translations.title; 1215 1216 // Update feature card labels 1217 const cardMappings = { 1218 readingMask: this.translations.readingMask, 1219 highContrast: this.translations.highContrast, 1220 darkMode: this.translations.darkMode, 1221 dyslexiaFont: this.translations.dyslexiaFont, 1222 textAlignRight: this.translations.textAlign, 1223 textAlignCenter: this.translations.textAlignCenter, 1224 textAlignLeft: this.translations.textAlignLeft, 1225 highlightLinks: this.translations.highlightLinks, 1226 hideImages: this.translations.hideImages, 1227 highlightHeadings: this.translations.highlightHeadings, 1228 textMagnifier: this.translations.textMagnifier, 1229 focusOutline: this.translations.focusOutline, 1230 reduceMotion: this.translations.reduceMotion, 1231 cursorSize: this.translations.cursorSize, 1232 stopAutoplay: this.translations.stopAutoplay, 1233 invertColors: this.translations.invertColors, 1234 }; 1235 1236 Object.entries(cardMappings).forEach(([feature, text]) => { 1237 const label = document.querySelector(`[data-feature="${feature}"] .a11y-card-label`); 1238 if (label) label.textContent = text; 1239 }); 1240 1241 // Update slider labels 1242 const sliderMappings = { 1243 fontSize: this.translations.fontSize, 1244 lineHeight: this.translations.lineHeight, 1245 letterSpacing: this.translations.letterSpacing, 1246 contentScale: this.translations.contentScale, 1247 columnWidth: this.translations.columnWidth, 1248 }; 1249 1250 Object.entries(sliderMappings).forEach(([id, text]) => { 1251 const slider = document.getElementById(id); 1252 if (slider) { 1253 const label = slider.closest(".a11y-slider-group").querySelector(".a11y-slider-label"); 1254 if (label) label.textContent = text; 1255 } 1256 }); 1257 1258 // Update reset button text 1259 const resetBtn = document.querySelector(".a11y-btn-reset span"); 1260 if (resetBtn) resetBtn.textContent = this.translations.reset; 1261 } 1262 1263 togglePanel() { 1264 this.isOpen = !this.isOpen; 1265 const panel = document.getElementById("a11y-panel"); 1266 panel.classList.toggle("active", this.isOpen); 1267 } 1268 1269 // Enhanced applySettings method with better Flarum support 1270 applySettings() { 1271 // Remove all feature classes for clean state 1272 document.body.classList.remove( 1273 "a11y-dyslexia", 1274 "a11y-highlight-links", 1275 "a11y-hide-images", 1276 "a11y-highlight-headings", 1277 "a11y-reading-mask", 1278 "a11y-text-magnifier", 1279 "a11y-high-contrast", 1280 "a11y-dark-mode", 1281 "a11y-focus-outline", 1282 "a11y-reduce-motion", 1283 "a11y-cursor-size", 1284 "a11y-stop-autoplay" 1285 ); 1286 1287 // Clear existing inline styles 1288 document.documentElement.style.fontSize = ""; 1289 document.body.style.lineHeight = ""; 1290 document.body.style.letterSpacing = ""; 1291 document.body.style.zoom = ""; 1292 1293 // Enhanced Flarum selectors for comprehensive coverage 1294 const flarumSelectors = [ 1295 // Basic text elements 1296 "p", 1297 'div:not([class*="a11y"])', 1298 "li", 1299 "td", 1300 "th", 1301 "article", 1302 "section", 1303 "span", 1304 "a", 1305 1306 // Core Flarum classes 1307 ".Post-body", 1308 ".PostUser-name", 1309 ".PostUser", 1310 ".UserCard",
1311 ".DiscussionHero-title", 1312 ".TagLabel-name", 1313 ".TagItem", 1314 ".Post-header", 1315 ".CommentPost", 1316 ".PostMeta", 1317 ".Composer", 1318 ".UserCard-name", 1319 ".item-count", 1320 ".Notification-content", 1321 ".DiscussionListItem-title", 1322 ".DiscussionListItem-info", 1323 ".DiscussionListItem-excerpt", 1324 ".Header-title", 1325 ".Alert", 1326 ".container", 1327 ".sp_footer", 1328 ".App-header", 1329 ".App-footer", 1330 ".App-composer", 1331 ".Header-primary", 1332 ".Header-secondary", 1333 ".App-drawer", 1334 ".App-navigation", 1335 ".App-content", 1336 ".item-excerpt", 1337 ".Post-stream", 1338 ".Post", 1339 ".DiscussionListItem", 1340 ".DiscussionList", 1341 1342 // Additional Flarum UI elements 1343 ".Button-label", 1344 ".Dropdown-toggle", 1345 ".FormControl", 1346 ".TextEditor-editor", 1347 ".NotificationList", 1348 ".Badge", 1349 ".Tooltip", 1350 ".Modal-content", 1351 ".Alert-body", 1352 ".Search-input", 1353 ".IndexPage-nav", 1354 ".DiscussionPage-nav", 1355 ".UserPage-content", 1356 ".SettingsPage-content", 1357 ".ExtensionPage-content", 1358 ".TagsPage-content", 1359 1360 // Dynamic content selectors 1361 '[class*="Post"]', 1362 '[class*="Discussion"]', 1363 '[class*="User"]', 1364 '[class*="Tag"]', 1365 '[class*="Comment"]', 1366 '[class*="Stream"]', 1367 '[class*="List"]', 1368 ]; 1369 1370 const textElements = document.querySelectorAll(flarumSelectors.join(", ")); 1371 1372 // Clear existing styles from all elements 1373 textElements.forEach((el) => { 1374 // el.style.fontSize = ""; 1375 // el.style.textAlign = ""; 1376 // el.style.maxWidth = ""; 1377 el.classList.remove("a11y-flarum-text"); 1378 }); 1379 1380 // Apply font size with enhanced Flarum support 1381 if (this.settings.fontSize && this.settings.fontSize !== 100) { 1382 const fontSizePercent = this.settings.fontSize + "%"; 1383 1384 // Set global font size 1385 document.documentElement.style.setProperty("font-size", fontSizePercent, "important"); 1386 document.body.style.setProperty("font-size", fontSizePercent, "important"); 1387 1388 // Apply to all Flarum elements specifically 1389 textElements.forEach((el) => { 1390 el.style.setProperty("font-size", fontSizePercent, "important"); 1391 el.classList.add("a11y-flarum-text"); 1392 }); 1393 1394 // Inject CSS for dynamically loaded content 1395 this.injectDynamicFontSizeCSS(this.settings.fontSize); 1396 } else { 1397 this.removeDynamicFontSizeCSS(); 1398 } 1399 1400 // Apply line height 1401 if (this.settings.lineHeight && this.settings.lineHeight !== 100) { 1402 const value = this.settings.lineHeight / 100; 1403 document.body.style.setProperty("line-height", value, "important"); 1404 textElements.forEach((el) => { 1405 el.style.setProperty("line-height", value, "important"); 1406 }); 1407 } 1408 1409 // Apply letter spacing 1410 if (this.settings.letterSpacing && this.settings.letterSpacing !== 100) { 1411 const value = (this.settings.letterSpacing - 100) * 0.05; 1412 document.body.style.setProperty("letter-spacing", value + "em", "important"); 1413 textElements.forEach((el) => { 1414 el.style.setProperty("letter-spacing", value + "em", "important"); 1415 }); 1416 } 1417 1418 // Apply content scaling 1419 if (this.settings.contentScale && this.settings.contentScale !== 100) { 1420 document.body.style.zoom = this.settings.contentScale + "%"; 1421 } 1422 1423 // Apply column width 1424 if (this.settings.columnWidth && this.settings.columnWidth !== 600) { 1425 textElements.forEach((el) => { 1426 el.style.maxWidth = this.settings.columnWidth + "px"; 1427 }); 1428 } 1429 1430 // Apply text alignment 1431 if (this.settings.textAlign && this.settings.textAlign !== "default") { 1432 textElements.forEach((el) => { 1433 el.style.setProperty("text-align", this.settings.textAlign, "important"); 1434 }); 1435 } 1436 1437 // Apply feature classes 1438 if (this.settings.dyslexiaFont) document.body.classList.add("a11y-dyslexia"); 1439 if (this.settings.highlightLinks) document.body.classList.add("a11y-highlight-links"); 1440 if (this.settings.hideImages) document.body.classList.add("a11y-hide-images"); 1441 if (this.settings.highlightHeadings) document.body.classList.add("a11y-highlight-headings"); 1442 if (this.settings.readingMask) document.body.classList.add("a11y-reading-mask"); 1443 if (this.settings.textMagnifier) document.body.classList.add("a11y-text-magnifier"); 1444 if (this.settings.highContrast) document.body.classList.add("a11y-high-contrast"); 1445 if (this.settings.darkMode) document.body.classList.add("a11y-dark-mode"); 1446 if (this.settings.focusOutline) document.body.classList.add("a11y-focus-outline"); 1447 if (this.settings.reduceMotion) document.body.classList.add("a11y-reduce-motion"); 1448 if (this.settings.cursorSize) document.body.classList.add("a11y-cursor-size"); 1449 if (this.settings.stopAutoplay) { 1450 document.body.classList.add("a11y-stop-autoplay"); 1451 document.querySelectorAll("video[autoplay], audio[autoplay]").forEach((media) => { 1452 media.pause(); 1453 media.removeAttribute("autoplay"); 1454 }); 1455 } 1456 1457 // Handle color inversion 1458 if (this.settings.invertColors) { 1459 document.documentElement.classList.add("a11y-invert"); 1460 } else { 1461 document.documentElement.classList.remove("a11y-invert"); 1462 } 1463 } 1464 1465 // Helper methods for dynamic CSS rules 1466 injectDynamicFontSizeCSS(fontSize) { 1467 // Remove any existing dynamic CSS 1468 this.removeDynamicFontSizeCSS(); 1469 1470 const style = document.createElement("style"); 1471 style.id = "a11y-dynamic-fontsize"; 1472 style.textContent = ` 1473 /* Dynamic font size for all Flarum elements */ 1474 .Post-body *, .DiscussionListItem *, .UserCard *, .CommentPost *, 1475 .Header-title *, .App-content *, .Post-stream *, .DiscussionList *, 1476 [class*="Post"] *, [class*="Discussion"] *, [class*="User"] *, 1477 [class*="Comment"] *, [class*="Stream"] *, [class*="List"] * { 1478 font-size: ${fontSize}% !important; 1479 } 1480 1481 /* Specific Flarum classes */ 1482 .Post-body, .PostUser-name, .PostUser, .UserCard, .DiscussionHero-title, 1483 .TagLabel-name, .TagItem, .Post-header, .CommentPost, .PostMeta, 1484 .UserCard-name, .item-count, .Notification-content, .DiscussionListItem-title, 1485 .DiscussionListItem-info, .DiscussionListItem-excerpt, .Header-title, 1486 .Button-label, .Dropdown-toggle, .FormControl, .TextEditor-editor { 1487 font-size: ${fontSize}% !important; 1488 } 1489 `; 1490 document.head.appendChild(style); 1491 } 1492 1493 removeDynamicFontSizeCSS() { 1494 const existingStyle = document.getElementById("a11y-dynamic-fontsize"); 1495 if (existingStyle) { 1496 existingStyle.remove(); 1497 } 1498 } 1499 1500 resetSettings() { 1501 // Reset all numeric settings to defaults 1502 this.settings.fontSize = 100; 1503 this.settings.lineHeight = 100; 1504 this.settings.letterSpacing = 100; 1505 this.settings.contentScale = 100; 1506 this.settings.columnWidth = 600; 1507 this.settings.textAlign = "default"; 1508 1509 // Reset all boolean features 1510 this.settings.readingMask = false; 1511 this.settings.highContrast = false; 1512 this.settings.darkMode = false; 1513 this.settings.dyslexiaFont = false; 1514 this.settings.highlightLinks = false; 1515 this.settings.hideImages = false; 1516 this.settings.highlightHeadings = false; 1517 this.settings.textMagnifier = false; 1518 this.settings.focusOutline = false; 1519 this.settings.reduceMotion = false; 1520 this.settings.cursorSize = false; 1521 this.settings.stopAutoplay = false; 1522 this.settings.invertColors = false; 1523 1524 // Update slider values in UI 1525 ["fontSize", "lineHeight", "letterSpacing", "contentScale"].forEach((id) => { 1526 const slider = document.getElementById(id); 1527 if (slider) { 1528 slider.value = 100; 1529 document.getElementById(id + "Value").textContent = "100%"; 1530 } 1531 }); 1532 1533 // Update column width slider 1534 const columnSlider = document.getElementById("columnWidth"); 1535 if (columnSlider) { 1536 columnSlider.value = 600; 1537 document.getElementById("columnWidthValue").textContent = "600px"; 1538 } 1539 1540 // Remove active state from all feature cards 1541 document.querySelectorAll(".a11y-card").forEach((card) => { 1542 card.classList.remove("active"); 1543 }); 1544 1545 // Clean up dynamic CSS 1546 this.removeDynamicFontSizeCSS(); 1547 1548 this.applySettings();
1549 this.saveSettings(); 1550 } 1551 1552 getDefaultSettings() { 1553 return { 1554 fontSize: 100, 1555 lineHeight: 100, 1556 letterSpacing: 100, 1557 contentScale: 100, 1558 columnWidth: 600, 1559 textAlign: "default", 1560 language: this.currentLang, 1561 readingMask: false, 1562 highContrast: false, 1563 darkMode: false, 1564 dyslexiaFont: false, 1565 highlightLinks: false, 1566 hideImages: false, 1567 highlightHeadings: false, 1568 textMagnifier: false, 1569 focusOutline: false, 1570 reduceMotion: false, 1571 cursorSize: false, 1572 stopAutoplay: false, 1573 invertColors: false, 1574 }; 1575 } 1576 1577 saveSettings() { 1578 try { 1579 localStorage.setItem(WIDGET_CONFIG.storageKey, JSON.stringify(this.settings)); 1580 } catch (e) { 1581 console.warn("Could not save accessibility settings:", e); 1582 } 1583 } 1584 1585 loadSettings() { 1586 try { 1587 const saved = localStorage.getItem(WIDGET_CONFIG.storageKey); 1588 if (saved) { 1589 return { ...this.getDefaultSettings(), ...JSON.parse(saved) }; 1590 } 1591 } catch (e) { 1592 console.warn("Could not load accessibility settings:", e); 1593 } 1594 return this.getDefaultSettings(); 1595 } 1596 1597 // Cleanup method for observers and intervals 1598 destroy() { 1599 // Stop all observers 1600 this.observers.forEach((observer) => observer.disconnect()); 1601 this.observers = []; 1602 1603 // Stop the reapply interval 1604 if (this.reapplyInterval) { 1605 clearInterval(this.reapplyInterval); 1606 } 1607 1608 // Remove dynamic CSS rules 1609 this.removeDynamicFontSizeCSS(); 1610 } 1611 } 1612 1613 // Initialize the accessibility widget 1614 function initAccessibilityWidget() { 1615 if (window.accessibilityWidget) return; 1616 window.accessibilityWidget = new AccessibilityWidget(); 1617 } 1618 1619 // Wait for DOM to be ready 1620 if (document.readyState === "loading") { 1621 document.addEventListener("DOMContentLoaded", initAccessibilityWidget); 1622 } else { 1623 initAccessibilityWidget(); 1624 } 1625})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.