PageSourceSearch

https://jacobischule-kalletal.de/media/plg_system_barrierfree/js/accessibility.js?2.2.1

js jacobischule-kalletal.de collected 2026-10-06 12:14:46 UTC 37,158 bytes, 948 lines download raw bytes

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.