1/** 2 * Joomla BarrierFree Plugin JavaScript v2.2.1 - Security Hardened 3 * @copyright Copyright (C) 2025 Mertsch-Web. All rights reserved. 4 */ 5 6;(() => { 7 function debugLog(message, data) { 8 if (window.accessibilityConfig && window.accessibilityConfig.debug) { 9 console.log("[BarrierFree Debug]", message, data || "") 10 } 11 } 12 13 14 /** 15 * SAFE: Update button content without innerHTML. 16 * Creates <span class="accessibility-icon">icon</span> text 17 */ 18 function setButtonContent(btn, icon, text) { 19 if (!btn) return 20 while (btn.firstChild) btn.removeChild(btn.firstChild) 21 var iconSpan = document.createElement("span") 22 iconSpan.className = "accessibility-icon" 23 iconSpan.textContent = icon 24 btn.appendChild(iconSpan) 25 btn.appendChild(document.createTextNode(" " + text)) 26 } 27 28 /** 29 * SAFE: Update TTS button content (icon + separate text span). 30 */ 31 function setTtsButtonContent(btn, icon, text) { 32 if (!btn) return 33 while (btn.firstChild) btn.removeChild(btn.firstChild) 34 var iconSpan = document.createElement("span") 35 iconSpan.className = "accessibility-icon" 36 iconSpan.textContent = icon 37 btn.appendChild(iconSpan) 38 var textSpan = document.createElement("span") 39 textSpan.className = "accessibility-button-text" 40 textSpan.textContent = text 41 btn.appendChild(textSpan) 42 } 43 44 /** 45 * Security-hardened: skip cookie consent banners, CSRF overlays 46 */ 47 var securityCriticalSelectors = [ 48 ".cc-banner", ".cc-window", ".cookie-consent", ".cookie-banner", 49 ".gdpr-banner", ".consent-banner", "#cookie-notice", "#cookieConsent", 50 ".modal-backdrop" 51 ] 52 53 function isSecurityCritical(el) { 54 for (var i = 0; i < securityCriticalSelectors.length; i++) { 55 try { if (el.matches(securityCriticalSelectors[i])) return true } catch(e) {} 56 } 57 return false 58 } 59 60 function domReady(callback) { 61 if (document.readyState === "loading") { 62 document.addEventListener("DOMContentLoaded", callback) 63 } else { 64 callback() 65 } 66 } 67 68 let initAttempts = 0 69 const maxAttempts = 5 70 71 function attemptInit() { 72 initAttempts++ 73 debugLog("Initialization attempt", initAttempts) 74 75 if (initAccessibilityToolbar()) { 76 debugLog("Successfully initialized on attempt", initAttempts) 77 return 78 } 79 80 if (initAttempts < maxAttempts) { 81 setTimeout(attemptInit, 1000 * initAttempts) 82 } else { 83 console.error("[BarrierFree] Failed to initialize after", maxAttempts, "attempts") 84 } 85 } 86 87 domReady(() => { 88 debugLog("DOM ready, starting initialization") 89 setTimeout(attemptInit, 100) 90 // FIX: Voices vorladen (Chrome liefert sie erst nach voiceschanged-Event) 91 if (window.speechSynthesis) { 92 window.speechSynthesis.getVoices() 93 window.speechSynthesis.addEventListener("voiceschanged", function() { 94 debugLog("Voices loaded:", window.speechSynthesis.getVoices().length) 95 }) 96 } 97 }) 98 99 /** 100 * FIX: Prüft ob der Toggle-Button tatsächlich klickbar ist 101 * und entfernt blockierende Overlays 102 */ 103 function ensureClickability(toggle) { 104 if (!toggle) return 105 106 var rect = toggle.getBoundingClientRect() 107 var centerX = rect.left + rect.width / 2 108 var centerY = rect.top + rect.height / 2 109 110 var topElement = document.elementFromPoint(centerX, centerY) 111 112 if (!topElement) { 113 debugLog("elementFromPoint returned null - button may be off-screen") 114 return 115 } 116 117 // Prüfen ob das gefundene Element der Button oder ein Kind davon ist 118 var toolbar = document.getElementById("accessibility-toolbar") 119 if (toolbar && (toolbar.contains(topElement) || topElement === toolbar)) { 120 debugLog("Button is clickable - no overlay detected") 121 return 122 } 123 124 // Ein anderes Element blockiert den Button 125 debugLog("Overlay detected blocking button:", topElement.tagName, topElement.className) 126 127 // FIX: Never modify security-critical elements (cookie banners, CSRF overlays) 128 if (isSecurityCritical(topElement)) { 129 debugLog("Blocked by security-critical element, not modifying:", topElement.className) 130 return 131 } 132 133 // Strategie 1: pointer-events: none auf das blockierende Element setzen 134 // Nur wenn es kein interaktives Element ist 135 var isInteractive = topElement.matches('a, button, input, select, textarea, [role="button"], [tabindex]') 136 if (!isInteractive) { 137 topElement.style.setProperty("pointer-events", "none", "important") 138 debugLog("Set pointer-events:none on blocking element") 139 140 // Erneut prüfen 141 var recheckElement = document.elementFromPoint(centerX, centerY) 142 if (toolbar && (toolbar.contains(recheckElement) || recheckElement === toolbar)) { 143 debugLog("Button is now clickable after fix") 144 return 145 } 146 147 // Rückgängig machen wenn es nicht geholfen hat 148 topElement.style.removeProperty("pointer-events") 149 } 150 151 // Strategie 2: Toolbar z-index nochmal forcieren 152 toolbar.style.setProperty("z-index", "2147483647", "important") 153 toggle.style.setProperty("z-index", "2147483647", "important") 154
155 // Strategie 3: Wenn immer noch blockiert, den Button über das blockierende Element heben 156 // indem wir das blockierende Element tiefer setzen 157 var blockingZIndex = window.getComputedStyle(topElement).zIndex 158 if (blockingZIndex !== "auto") { 159 debugLog("Blocking element has z-index:", blockingZIndex) 160 } 161 162 // Letzte Möglichkeit: Alle fixed/absolute Elemente mit hohem z-index prüfen 163 var allFixed = document.querySelectorAll('[style*="position"], [style*="z-index"]') 164 for (var i = 0; i < allFixed.length; i++) { 165 var el = allFixed[i] 166 if (el === toolbar || toolbar.contains(el)) continue 167 if (isSecurityCritical(el)) continue 168 169 var style = window.getComputedStyle(el) 170 if ((style.position === "fixed" || style.position === "absolute") && 171 parseInt(style.zIndex, 10) >= 2147483647) { 172 // Dieses Element konkurriert mit der Toolbar 173 var elRect = el.getBoundingClientRect() 174 if (rectsOverlap(rect, elRect)) { 175 el.style.setProperty("z-index", "2147483646", "important") 176 debugLog("Lowered z-index of competing element:", el.tagName, el.className) 177 } 178 } 179 } 180 181 // Finale Prüfung 182 setTimeout(() => { 183 var finalCheck = document.elementFromPoint(centerX, centerY) 184 if (toolbar && (toolbar.contains(finalCheck) || finalCheck === toolbar)) { 185 debugLog("Button is clickable after all fixes") 186 } else { 187 console.warn("[BarrierFree] Button may still be blocked by:", finalCheck ? finalCheck.tagName + "." + finalCheck.className : "unknown") 188 } 189 }, 100) 190 } 191 192 function rectsOverlap(a, b) { 193 return !(a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom) 194 } 195 196 /** 197 * Initialize the accessibility toolbar v2.2.1 198 */ 199 function initAccessibilityToolbar() { 200 debugLog("Starting toolbar initialization") 201 202 var toolbar = document.getElementById("accessibility-toolbar") 203 204 if (!toolbar) { 205 debugLog("Toolbar element not found") 206 207 var body = document.body 208 if (!body || !body.innerHTML.includes("accessibility-toolbar")) { 209 debugLog("Plugin HTML not found in body") 210 return false 211 } 212 213 setTimeout(() => { 214 toolbar = document.getElementById("accessibility-toolbar") 215 if (toolbar) { 216 debugLog("Toolbar found on retry") 217 continueInit(toolbar) 218 } 219 }, 500) 220 221 return false 222 } 223 224 return continueInit(toolbar) 225 } 226 227 function continueInit(toolbar) { 228 debugLog("Continuing initialization with toolbar element") 229 230 // Force visibility 231 toolbar.style.display = "block" 232 toolbar.style.visibility = "visible" 233 toolbar.style.opacity = "1" 234 235 // Apply configuration from PHP 236 if (typeof window.accessibilityConfig !== "undefined") { 237 const config = window.accessibilityConfig 238 debugLog("Applying configuration:", config) 239 240 // Apply position 241 toolbar.classList.remove( 242 "accessibility-position-left", 243 "accessibility-position-right", 244 "accessibility-position-bottom-left", 245 "accessibility-position-bottom-right", 246 ) 247 248 if (config.position === "left") { 249 toolbar.classList.add("accessibility-position-left") 250 } else if (config.position === "bottom-left") { 251 toolbar.classList.add("accessibility-position-bottom-left") 252 } else if (config.position === "bottom-right") { 253 toolbar.classList.add("accessibility-position-bottom-right") 254 } else { 255 toolbar.classList.add("accessibility-position-right") 256 } 257 258 // Apply color scheme 259 toolbar.classList.remove( 260 "accessibility-color-blue", 261 "accessibility-color-green", 262 "accessibility-color-red", 263 "accessibility-color-purple", 264 "accessibility-color-orange", 265 "accessibility-color-teal", 266 "accessibility-color-dark", 267 ) 268 toolbar.classList.add("accessibility-color-" + config.color) 269 270 // Apply button size 271 const toggle = toolbar.querySelector(".accessibility-toggle") 272 if (toggle && config.buttonSize) { 273 toggle.style.setProperty("width", config.buttonSize + "px", "important") 274 toggle.style.setProperty("height", config.buttonSize + "px", "important") 275 debugLog("Applied button size:", config.buttonSize + "px") 276 } 277 278 window.accessibilityReadingSpeed = config.readingSpeed || 1 279 debugLog("Reading speed set to:", window.accessibilityReadingSpeed) 280 debugLog("Configuration applied successfully") 281 } 282 283 // Get all necessary elements 284 var toggle = toolbar.querySelector(".accessibility-toggle") 285 var tools = document.getElementById("accessibility-tools") 286 var toolsContent = tools ? tools.querySelector(".accessibility-tools-content") : null 287 var closeButton = toolbar.querySelector(".accessibility-close") 288 289 // Zeilenlineal-Element suchen oder erstellen 290 var readingRuler = document.querySelector(".accessibility-reading-ruler") 291 if (!readingRuler) { 292 debugLog("Creating reading ruler element") 293 readingRuler = document.createElement("div") 294 readingRuler.className = "accessibility-reading-ruler" 295 readingRuler.style.cssText = `
296 position: fixed !important; 297 left: 0 !important; 298 right: 0 !important; 299 width: 100vw !important; 300 height: 3px !important; 301 background-color: #ffcc00 !important; 302 z-index: 2147483645 !important; 303 pointer-events: none !important; 304 box-shadow: 0 0 8px rgba(255, 204, 0, 0.8) !important; 305 transition: top 0.05s ease !important; 306 display: none !important; 307 top: 0 !important; 308 ` 309 document.body.appendChild(readingRuler) 310 } 311 312 if (!toggle) { debugLog("Toggle button not found"); return false } 313 if (!tools) { debugLog("Tools panel not found"); return false } 314 315 debugLog("All critical elements found, setting up event handlers") 316 317 // FIX: Klickbarkeits-Check nach kurzer Verzögerung 318 setTimeout(() => { 319 ensureClickability(toggle) 320 }, 500) 321 322 // Auch nach Scroll und Resize erneut prüfen (Template-Elemente können sich bewegen) 323 var clickCheckDebounce = null 324 function debouncedClickCheck() { 325 clearTimeout(clickCheckDebounce) 326 clickCheckDebounce = setTimeout(() => ensureClickability(toggle), 300) 327 } 328 window.addEventListener("scroll", debouncedClickCheck, { passive: true }) 329 window.addEventListener("resize", debouncedClickCheck, { passive: true }) 330 331 // Language selector 332 var languageSelect = document.getElementById("accessibility-language") 333 334 // Font size buttons 335 var fontIncreaseBtn = document.getElementById("accessibility-font-increase") 336 var fontDecreaseBtn = document.getElementById("accessibility-font-decrease") 337 var fontResetBtn = document.getElementById("accessibility-font-reset") 338 339 // Letter spacing buttons 340 var letterSpacingIncreaseBtn = document.getElementById("accessibility-letter-spacing-increase") 341 var letterSpacingDecreaseBtn = document.getElementById("accessibility-letter-spacing-decrease") 342 var letterSpacingResetBtn = document.getElementById("accessibility-letter-spacing-reset") 343 344 // Feature buttons 345 var grayscaleToggleBtn = document.getElementById("accessibility-grayscale-toggle") 346 var highlightLinksBtn = document.getElementById("accessibility-highlight-links") 347 var underlineLinksBtn = document.getElementById("accessibility-underline-links") 348 var bigCursorBtn = document.getElementById("accessibility-big-cursor") 349 var readingRulerBtn = document.getElementById("accessibility-reading-ruler") 350 var readPageBtn = document.getElementById("accessibility-read-page") 351 var stopReadingBtn = document.getElementById("accessibility-stop-reading") 352 var invertColorsBtn = document.getElementById("accessibility-invert-colors") 353 var readSelectionBtn = document.getElementById("accessibility-read-selection") 354 var readingSpeedSelect = document.getElementById("accessibility-reading-speed") 355 356 // Set initial language 357 var currentLanguage = localStorage.getItem("accessibility-language") || detectBrowserLanguage() 358 if (languageSelect) { 359 languageSelect.value = currentLanguage 360 } 361 362 if (readingSpeedSelect) { 363 var savedSpeed = localStorage.getItem("accessibility-reading-speed") || window.accessibilityReadingSpeed || 1 364 readingSpeedSelect.value = savedSpeed 365 window.accessibilityReadingSpeed = parseFloat(savedSpeed) 366 } 367 368 // Language change handler 369 if (languageSelect) { 370 languageSelect.addEventListener("click", (e) => e.stopPropagation()) 371 languageSelect.addEventListener("change", () => { 372 currentLanguage = languageSelect.value 373 localStorage.setItem("accessibility-language", currentLanguage) 374 updateLanguageTexts() 375 }) 376 } 377 378 // Reading speed change handler 379 if (readingSpeedSelect) { 380 readingSpeedSelect.addEventListener("click", (e) => e.stopPropagation()) 381 readingSpeedSelect.addEventListener("change", () => { 382 var newSpeed = parseFloat(readingSpeedSelect.value) 383 window.accessibilityReadingSpeed = newSpeed 384 localStorage.setItem("accessibility-reading-speed", newSpeed) 385 debugLog("Reading speed changed to:", newSpeed) 386 }) 387 } 388 389 function detectBrowserLanguage() { 390 var lang = navigator.language || navigator.userLanguage 391 return (lang && lang.startsWith("de")) ? "de-DE" : "en-GB" 392 } 393 394 function updateLanguageTexts() { 395 if (typeof window.accessibilityLang === "undefined") return 396 var toolbarHeader = toolbar.querySelector(".accessibility-tools-header h2") 397 if (toolbarHeader) toolbarHeader.textContent = window.accessibilityLang.title 398 updateHighlightLinksButtonText() 399 updateSelectionButtonText() 400 } 401 402 function updateHighlightLinksButtonText() { 403 if (!highlightLinksBtn) return 404 if (document.body.classList.contains("accessibility-highlight-links")) { 405 setButtonContent(highlightLinksBtn, "ð", 406 window.accessibilityLang ? window.accessibilityLang.unhighlight_links : "Links normal anzeigen") 407 } else { 408 setButtonContent(highlightLinksBtn, "ð", 409 window.accessibilityLang ? window.accessibilityLang.highlight_links : "Links hervorheben") 410 } 411 } 412 413 // Panel toggle 414 function togglePanel() { 415 toolbar.classList.toggle("panel-open") 416 var isOpen = toolbar.classList.contains("panel-open") 417 toggle.setAttribute("aria-expanded", isOpen) 418 419 if (isOpen && toolsContent) { 420 setTimeout(() => { toolsContent.style.overflowY = "auto" }, 350) 421 } 422 } 423 424 function closePanel() { 425 toolbar.classList.remove("panel-open") 426 toggle.setAttribute("aria-expanded", "false") 427 } 428 429 if (toggle) { 430 var touchStartTime = 0 431 var touchMoved = false 432 433 toggle.addEventListener("touchstart", (e) => { 434 touchStartTime = Date.now() 435 touchMoved = false 436 }, { passive: true }) 437 438 toggle.addEventListener("touchmove", (e) => { 439 touchMoved = true 440 }, { passive: true }) 441 442 toggle.addEventListener("touchend", (e) => { 443 var touchDuration = Date.now() - touchStartTime 444 if (!touchMoved && touchDuration < 500) { 445 e.preventDefault() 446 e.stopPropagation() 447 togglePanel() 448 } 449 }, { passive: false }) 450 451 toggle.addEventListener("click", (e) => { 452 e.stopPropagation() 453 togglePanel() 454 }) 455 } 456 457 if (closeButton) { 458 closeButton.addEventListener("touchend", (e) => { 459 e.preventDefault() 460 e.stopPropagation() 461 closePanel() 462 }, { passive: false }) 463 464 closeButton.addEventListener("click", (e) => { 465 e.stopPropagation() 466 closePanel() 467 }) 468 } 469 470 // Close panel when clicking outside 471 document.addEventListener("click", (event) => { 472 if (toolbar.classList.contains("panel-open") && !toolbar.contains(event.target)) closePanel() 473 }) 474 475 document.addEventListener("touchend", (event) => { 476 if (toolbar.classList.contains("panel-open") && !toolbar.contains(event.target)) closePanel() 477 }, { passive: true }) 478 479 if (tools) { 480 tools.addEventListener("click", (event) => event.stopPropagation()) 481 } 482 483 // Keyboard: Escape to close 484 document.addEventListener("keydown", (e) => { 485 if (e.key === "Escape" && toolbar.classList.contains("panel-open")) { 486 closePanel() 487 toggle.focus() 488 } 489 }) 490 491 // Font size 492 var currentFontSize = parseInt(localStorage.getItem("accessibility-font-size") || "100") 493 494 function updateFontSize() { 495 for (var s = 70; s <= 200; s += 10) { 496 document.documentElement.classList.remove("accessibility-font-size-" + s) 497 } 498 if (currentFontSize !== 100) { 499 document.documentElement.classList.add("accessibility-font-size-" + currentFontSize) 500 } 501 localStorage.setItem("accessibility-font-size", currentFontSize.toString()) 502 } 503 504 if (fontIncreaseBtn) fontIncreaseBtn.addEventListener("click", (e) => { e.stopPropagation(); if (currentFontSize < 200) { currentFontSize += 10; updateFontSize() } }) 505 if (fontDecreaseBtn) fontDecreaseBtn.addEventListener("click", (e) => { e.stopPropagation(); if (currentFontSize > 70) { currentFontSize -= 10; updateFontSize() } }) 506 if (fontResetBtn) fontResetBtn.addEventListener("click", (e) => { e.stopPropagation(); currentFontSize = 100; updateFontSize() }) 507 508 // Letter spacing 509 var currentLetterSpacing = parseInt(localStorage.getItem("accessibility-letter-spacing") || "0") 510 511 function updateLetterSpacing() { 512 document.body.classList.remove("accessibility-letter-spacing-1", "accessibility-letter-spacing-2", "accessibility-letter-spacing-3") 513 if (currentLetterSpacing > 0) document.body.classList.add("accessibility-letter-spacing-" + currentLetterSpacing) 514 localStorage.setItem("accessibility-letter-spacing", currentLetterSpacing.toString()) 515 } 516 517 if (letterSpacingIncreaseBtn) letterSpacingIncreaseBtn.addEventListener("click", (e) => { e.stopPropagation(); if (currentLetterSpacing < 3) { currentLetterSpacing++; updateLetterSpacing() } }) 518 if (letterSpacingDecreaseBtn) letterSpacingDecreaseBtn.addEventListener("click", (e) => { e.stopPropagation(); if (currentLetterSpacing > 0) { currentLetterSpacing--; updateLetterSpacing() } }) 519 if (letterSpacingResetBtn) letterSpacingResetBtn.addEventListener("click", (e) => { e.stopPropagation(); currentLetterSpacing = 0; updateLetterSpacing() }) 520 521 // Grayscale toggle 522 if (grayscaleToggleBtn) { 523 grayscaleToggleBtn.addEventListener("click", (e) => { 524 e.stopPropagation() 525 var active = document.body.classList.toggle("accessibility-grayscale") 526 localStorage.setItem("accessibility-grayscale", active) 527 setButtonContent(grayscaleToggleBtn, "ð²", active ? "Normale Farben" : "Graue Farbtöne") 528 }) 529 } 530 531 // Highlight links 532 if (highlightLinksBtn) { 533 highlightLinksBtn.addEventListener("click", (e) => { 534 e.stopPropagation() 535 var active = document.body.classList.toggle("accessibility-highlight-links") 536 localStorage.setItem("accessibility-highlight-links", active) 537 updateHighlightLinksButtonText() 538 }) 539 } 540 541 // Underline links 542 if (underlineLinksBtn) { 543 underlineLinksBtn.addEventListener("click", (e) => { 544 e.stopPropagation() 545 var active = document.body.classList.toggle("accessibility-underline-links") 546 localStorage.setItem("accessibility-underline-links", active) 547 setButtonContent(underlineLinksBtn, "ð", active ? "Links normal anzeigen" : "Links unterstreichen") 548 }) 549 } 550 551 // Big cursor 552 if (bigCursorBtn) { 553 bigCursorBtn.addEventListener("click", (e) => { 554 e.stopPropagation() 555 var active = document.body.classList.toggle("accessibility-big-cursor") 556 localStorage.setItem("accessibility-big-cursor", active) 557 setButtonContent(bigCursorBtn, "ð", active ? "Normaler Mauszeiger" : "GroÃer Mauszeiger") 558 }) 559 } 560 561 // Zeilenlineal 562 var rulerActive = false 563 564 function moveRuler(e) { 565 if (rulerActive && readingRuler) { 566 readingRuler.style.top = (e.clientY - 1) + "px" 567 } 568 } 569 570 if (readingRulerBtn && readingRuler) { 571 readingRulerBtn.addEventListener("click", (e) => { 572 e.stopPropagation() 573 if (rulerActive) { 574 readingRuler.style.display = "none" 575 localStorage.setItem("accessibility-reading-ruler", "false") 576 setButtonContent(readingRulerBtn, "ð", "Zeilenlineal") 577 document.removeEventListener("mousemove", moveRuler) 578 rulerActive = false 579 } else { 580 readingRuler.style.display = "block" 581 localStorage.setItem("accessibility-reading-ruler", "true") 582 setButtonContent(readingRulerBtn, "ð", "Zeilenlineal ausblenden") 583 document.addEventListener("mousemove", moveRuler) 584 rulerActive = true 585 } 586 }) 587 } 588 589 // Text-to-speech 590 var speechSynthesis = window.speechSynthesis 591 var currentUtterance = null 592 var readingElements = [] 593 var currentElementIndex = 0 594 var isReading = false 595 596 function stopReading() {
597 if (speechSynthesis) { 598 speechSynthesis.cancel() 599 setTimeout(() => speechSynthesis.cancel(), 100) 600 } 601 currentUtterance = null 602 readingElements = [] 603 currentElementIndex = 0 604 isReading = false 605 isSelectionReadingActive = false 606 clearReadingHighlight() 607 608 if (readPageBtn) { readPageBtn.disabled = false; readPageBtn.style.opacity = "1" } 609 if (stopReadingBtn) { stopReadingBtn.disabled = true; stopReadingBtn.style.opacity = "0.5" } 610 updateSelectionButtonText() 611 } 612 613 if (readPageBtn) { 614 readPageBtn.addEventListener("click", (e) => { 615 e.preventDefault() 616 e.stopPropagation() 617 if (!speechSynthesis) { alert("Text-to-Speech ist in diesem Browser nicht verfügbar."); return } 618 stopReading() 619 setTimeout(() => { 620 readingElements = getReadableElements() 621 currentElementIndex = 0 622 isReading = true 623 if (readingElements.length > 0) { 624 readPageBtn.disabled = true; readPageBtn.style.opacity = "0.5" 625 stopReadingBtn.disabled = false; stopReadingBtn.style.opacity = "1" 626 readNextElement() 627 } else { 628 alert("Keine lesbaren Inhalte gefunden.") 629 stopReading() 630 } 631 }, 200) 632 }) 633 } 634 635 if (stopReadingBtn) { 636 stopReadingBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); stopReading() }) 637 } 638 639 document.addEventListener("keydown", (e) => { 640 if (e.key === "Escape" && isReading) stopReading() 641 }) 642 643 function getReadableElements() { 644 var mainContent = null 645 var contentSelectors = [ 646 "#mw-content", "#content", "main", "#component", ".item-page", ".blog", 647 ".easyblog-content", ".easyblog-entry", ".easyblog-grid", 648 ".com-content-article", ".com-content-category", ".container-component", 649 '[itemprop="articleBody"]', ".content", "#system-content", ".system-content", 650 ".entry-content", ".post-content", ".article-content", 651 "#sp-main-body", "#t3-content", "#jsn-mainbody", ".sppb-section", 652 "[role='main']" 653 ] 654 655 for (var i = 0; i < contentSelectors.length; i++) { 656 var element = document.querySelector(contentSelectors[i]) 657 if (element) { mainContent = element; debugLog("Content area found:", contentSelectors[i]); break } 658 } 659 660 if (!mainContent) { mainContent = document.body; debugLog("Fallback to body") } 661 662 var contentElements = mainContent.querySelectorAll( 663 "p, h1, h2, h3, h4, h5, h6, li, td, th, figcaption, blockquote, article, section, " + 664 ".item-page, .blog-item, .card-body, .card-text, .article-body, " + 665 ".easyblog-entry-text, .easyblog-content-text, " + 666 '[itemprop="articleBody"], [itemprop="description"], ' + 667 "div.eb-text, div.eb-content" 668 ) 669 670 debugLog("Found candidate elements:", contentElements.length) 671 672 var filtered = Array.prototype.filter.call(contentElements, function(el) { 673 var parent = el 674 while (parent && parent !== mainContent) { 675 if (parent.tagName === "NAV") return false 676 if (parent.id && parent.id === "accessibility-toolbar") return false 677 if (parent.className && typeof parent.className === "string") { 678 var cls = " " + parent.className + " " 679 if (cls.indexOf(" accessibility-toolbar ") !== -1) return false 680 if ((parent.getAttribute("role") === "navigation") || 681 (parent.tagName === "HEADER" && parent.parentElement === document.body) || 682 (parent.tagName === "FOOTER" && parent.parentElement === document.body)) { 683 return false 684 } 685 } 686 parent = parent.parentElement 687 } 688 var text = el.textContent.trim() 689 if (text.length < 2) return false 690 var style = window.getComputedStyle(el) 691 return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" 692 }) 693 694 debugLog("Filtered readable elements:", filtered.length) 695 696 if (filtered.length === 0 && contentElements.length > 0) { 697 debugLog("Filter too strict, using all visible text elements") 698 filtered = Array.prototype.filter.call(contentElements, function(el) { 699 if (el.closest && el.closest("#accessibility-toolbar, .accessibility-toolbar")) return false 700 var text = el.textContent.trim() 701 if (text.length < 2) return false 702 var style = window.getComputedStyle(el) 703 return style.display !== "none" && style.visibility !== "hidden" 704 }) 705 debugLog("Fallback elements:", filtered.length) 706 } 707 708 return filtered 709 } 710 711 function getVoice(lang) { 712 var voices = speechSynthesis.getVoices() 713 var langFilter = lang === "de-DE" ? "de" : "en" 714 var filtered = voices.filter(v => v.lang.includes(langFilter)) 715 716 // Premium voices first 717 var premium = filtered.find(v => 718 v.name.includes("Google") || v.name.includes("Premium") || v.name.includes("Enhanced") || 719 v.name.includes("Neural") || v.name.includes("Anna") || v.name.includes("Helena") || 720 v.name.includes("Samantha") || v.name.includes("Alex") || v.name.includes("Karen") 721 ) 722 if (premium) return premium 723 724 // Then local voices 725 var local = filtered.find(v => v.localService === true) 726 if (local) return local 727 728 return filtered[0] || null 729 } 730 731 function readNextElement() { 732 if (!isReading || currentElementIndex >= readingElements.length) { 733 stopReading() 734 return 735 } 736 737 var element = readingElements[currentElementIndex] 738 clearReadingHighlight() 739 element.classList.add("accessibility-reading-indicator") 740 element.scrollIntoView({ behavior: "smooth", block: "center" }) 741 742 currentUtterance = new SpeechSynthesisUtterance(element.textContent) 743 var speechLang = currentLanguage === "de-DE" ? "de-DE" : "en-US" 744 currentUtterance.lang = speechLang 745 746 var voice = getVoice(speechLang) 747 if (voice) currentUtterance.voice = voice 748 749 currentUtterance.rate = window.accessibilityReadingSpeed || 1 750 currentUtterance.pitch = speechLang === "de-DE" ? 1.1 : 1.05 751 currentUtterance.volume = 1.0 752 753 currentUtterance.onend = () => { 754 if (isReading) { currentElementIndex++; setTimeout(readNextElement, 400) } 755 } 756 currentUtterance.onerror = () => { 757 if (isReading) { currentElementIndex++; setTimeout(readNextElement, 500) } 758 } 759
760 try { speechSynthesis.speak(currentUtterance) } 761 catch (error) { stopReading() } 762 } 763 764 function clearReadingHighlight() { 765 var els = document.querySelectorAll(".accessibility-reading-indicator") 766 for (var i = 0; i < els.length; i++) els[i].classList.remove("accessibility-reading-indicator") 767 } 768 769 // Text selection reading 770 var selectedText = "" 771 var isSelectionReadingActive = false 772 773 function updateSelectionButtonText() { 774 if (!readSelectionBtn) return 775 const selection = window.getSelection() 776 const txt = selection.toString().trim() 777 778 if (txt.length > 0) { 779 readSelectionBtn.disabled = false 780 readSelectionBtn.style.opacity = "1" 781 const langText = window.accessibilityLang ? window.accessibilityLang.read_selection : "Auswahl vorlesen" 782 setTtsButtonContent(readSelectionBtn, "ð", langText + " (" + txt.length + ")") 783 selectedText = txt 784 } else { 785 readSelectionBtn.disabled = true 786 readSelectionBtn.style.opacity = "0.5" 787 const langText = window.accessibilityLang ? window.accessibilityLang.read_selection : "Auswahl vorlesen" 788 setTtsButtonContent(readSelectionBtn, "ð", langText) 789 selectedText = "" 790 } 791 } 792 793 function readSelectedText() { 794 const selection = window.getSelection() 795 const textToRead = selection.toString().trim() 796 if (!textToRead) { alert("Bitte markieren Sie zuerst Text zum Vorlesen."); return } 797 798 if (speechSynthesis && speechSynthesis.speaking) speechSynthesis.cancel() 799 if (isReading) stopReading() 800 801 const utterance = new SpeechSynthesisUtterance(textToRead) 802 const speechLang = currentLanguage === "de-DE" ? "de-DE" : "en-US" 803 utterance.lang = speechLang 804 805 var voice = getVoice(speechLang) 806 if (voice) utterance.voice = voice 807 808 utterance.rate = window.accessibilityReadingSpeed || 1 809 utterance.pitch = speechLang === "de-DE" ? 1.1 : 1.05 810 utterance.volume = 1.0 811 812 isSelectionReadingActive = true 813 if (readSelectionBtn) { readSelectionBtn.disabled = true; readSelectionBtn.style.opacity = "0.5" } 814 if (readPageBtn) { readPageBtn.disabled = true; readPageBtn.style.opacity = "0.5" } 815 if (stopReadingBtn) { stopReadingBtn.disabled = false; stopReadingBtn.style.opacity = "1" } 816 817 utterance.onend = () => { 818 isSelectionReadingActive = false 819 if (stopReadingBtn) { stopReadingBtn.disabled = true; stopReadingBtn.style.opacity = "0.5" } 820 if (readPageBtn) { readPageBtn.disabled = false; readPageBtn.style.opacity = "1" } 821 updateSelectionButtonText() 822 window.getSelection().removeAllRanges() 823 } 824 825 utterance.onerror = () => { 826 isSelectionReadingActive = false 827 if (stopReadingBtn) { stopReadingBtn.disabled = true; stopReadingBtn.style.opacity = "0.5" } 828 if (readPageBtn) { readPageBtn.disabled = false; readPageBtn.style.opacity = "1" } 829 updateSelectionButtonText() 830 } 831 832 try { speechSynthesis.speak(utterance) } 833 catch (error) { isSelectionReadingActive = false; updateSelectionButtonText() } 834 } 835 836 function readSelectedTextHandler(e) { 837 e.preventDefault(); e.stopPropagation() 838 readSelectedText() 839 } 840 841 if (readSelectionBtn) { 842 readSelectionBtn.addEventListener("click", readSelectedTextHandler) 843 updateSelectionButtonText() 844 } 845 846 // Selection change detection 847 document.addEventListener("mouseup", () => setTimeout(updateSelectionButtonText, 150)) 848 document.addEventListener("selectionchange", () => setTimeout(updateSelectionButtonText, 100)) 849 document.addEventListener("keyup", (e) => { 850 if (["ArrowLeft","ArrowRight","ArrowUp","ArrowDown","Home","End","PageUp","PageDown"].includes(e.key) || e.shiftKey) { 851 setTimeout(updateSelectionButtonText, 150) 852 } 853 }) 854 855 // Keyboard shortcut Ctrl+R for selection reading 856 document.addEventListener("keydown", (e) => { 857 if ((e.ctrlKey || e.metaKey) && e.key === "r" && !e.shiftKey && !e.altKey) { 858 const sel = window.getSelection().toString().trim() 859 if (sel.length > 0) { e.preventDefault(); e.stopPropagation(); selectedText = sel; readSelectedText(); return false } 860 } 861 if (e.key === "Escape" && isSelectionReadingActive) {
862 if (speechSynthesis && speechSynthesis.speaking) speechSynthesis.cancel() 863 isSelectionReadingActive = false 864 updateSelectionButtonText() 865 } 866 }) 867 868 // Color inversion 869 if (invertColorsBtn) { 870 invertColorsBtn.addEventListener("click", (e) => { 871 e.stopPropagation() 872 var active = document.body.classList.toggle("accessibility-invert-colors") 873 localStorage.setItem("accessibility-invert-colors", active) 874 setButtonContent(invertColorsBtn, "ð", 875 active 876 ? (window.accessibilityLang ? window.accessibilityLang.disable_invert_colors : "Normale Farben") 877 : (window.accessibilityLang ? window.accessibilityLang.enable_invert_colors : "Farben invertieren")) 878 }) 879 } 880 881 // Load saved settings 882 function loadSavedSettings() { 883 var savedFontSize = localStorage.getItem("accessibility-font-size") 884 if (savedFontSize) { currentFontSize = parseInt(savedFontSize, 10); if (currentFontSize !== 100) updateFontSize() } 885 886 var savedLetterSpacing = localStorage.getItem("accessibility-letter-spacing") 887 if (savedLetterSpacing) { currentLetterSpacing = parseInt(savedLetterSpacing, 10); if (currentLetterSpacing > 0) document.body.classList.add("accessibility-letter-spacing-" + currentLetterSpacing) } 888 889 if (localStorage.getItem("accessibility-grayscale") === "true") { 890 document.body.classList.add("accessibility-grayscale") 891 setButtonContent(grayscaleToggleBtn, "ð²", "Normale Farben") 892 } 893 894 if (localStorage.getItem("accessibility-highlight-links") === "true") document.body.classList.add("accessibility-highlight-links") 895 896 if (localStorage.getItem("accessibility-underline-links") === "true") { 897 document.body.classList.add("accessibility-underline-links") 898 setButtonContent(underlineLinksBtn, "ð", "Links normal anzeigen") 899 } 900 901 if (localStorage.getItem("accessibility-big-cursor") === "true") { 902 document.body.classList.add("accessibility-big-cursor") 903 setButtonContent(bigCursorBtn, "ð", "Normaler Mauszeiger") 904 } 905 906 if (localStorage.getItem("accessibility-reading-ruler") === "true" && readingRuler) { 907 readingRuler.style.display = "block" 908 if (readingRulerBtn) setButtonContent(readingRulerBtn, "ð", "Zeilenlineal ausblenden") 909 document.addEventListener("mousemove", moveRuler) 910 rulerActive = true 911 } 912 913 if (localStorage.getItem("accessibility-invert-colors") === "true") { 914 document.body.classList.add("accessibility-invert-colors") 915 setButtonContent(invertColorsBtn, "ð", 916 window.accessibilityLang ? window.accessibilityLang.disable_invert_colors : "Normale Farben") 917 } 918 919 updateHighlightLinksButtonText() 920 updateSelectionButtonText() 921 } 922 923 // Scroll handling for tools panel 924 if (toolsContent) { 925 toolsContent.addEventListener("wheel", (e) => { 926 e.stopPropagation() 927 const { scrollTop, scrollHeight, clientHeight } = toolsContent 928 if ((scrollTop <= 0 && e.deltaY < 0) || (scrollTop + clientHeight >= scrollHeight && e.deltaY > 0)) e.preventDefault() 929 }, { passive: false }) 930 931 toolsContent.addEventListener("touchmove", (e) => e.stopPropagation(), { passive: true }) 932 } 933 934 // Safety measures 935 window.addEventListener("beforeunload", () => { if (isReading) stopReading() }) 936 window.addEventListener("blur", () => { if (isReading && speechSynthesis.speaking) speechSynthesis.pause() }) 937 window.addEventListener("focus", () => { if (isReading && speechSynthesis.paused) speechSynthesis.resume() }) 938 939 // Initialize 940 loadSavedSettings() 941 updateLanguageTexts() 942 943 debugLog("BarrierFree Plugin v2.2.1 initialized successfully") 944 return true 945 } 946 947 window.initAccessibilityToolbar = initAccessibilityToolbar 948})()
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.