https://www.lotte.de/typo3temp/assets/compressed/a11y-71c74757fea73b522f63d7fd6613945d.js?1788781776
1/* ========================================================================== 2 A11Y.JS â Einheitliche Barrierefreiheit-Toolbar 3 Dark Mode · Schriftgroessen · Kontrast · Leichte Sprache · Vorlesen 4 EINZIGE A11y-Toolbar - ersetzt den separaten Text-Switcher aus includes.js 5 ========================================================================== */ 6 7(function() { 8 'use strict'; 9 10 var STORAGE_KEYS = { 11 theme: 'lotte-theme', 12 fontSize: 'lotte-font-size', 13 contrast: 'lotte-contrast', 14 easyText: 'lotte-easy-text', 15 layout: 'lotte-layout' 16 }; 17 18 var FONT_SIZES = ['normal', 'large', 'xlarge']; 19 20 var currentTheme = localStorage.getItem(STORAGE_KEYS.theme) || 'light'; 21 var currentFontSize = localStorage.getItem(STORAGE_KEYS.fontSize) || 'normal'; 22 var highContrast = localStorage.getItem(STORAGE_KEYS.contrast) === 'true'; 23 var easyTextActive = localStorage.getItem(STORAGE_KEYS.easyText) === 'true'; 24 // 'full' = no max-width constraint (default), 'boxed' = 1300px centered 25 var currentLayout = localStorage.getItem(STORAGE_KEYS.layout) || 'full'; 26 var VOICE_READER_ENDPOINT = '/lotte-voicereader/synthesize'; 27 var VOICE_READER_WARM_ENDPOINT = '/lotte-voicereader/warm'; 28 var VOICE_READER_MAX_TEXT_LENGTH = 0; 29 var voiceReaderAudio = null; 30 var voiceReaderObjectUrl = null; 31 var voiceReaderLoading = false; 32 var voiceReaderRequestController = null; 33 var voiceReaderWarmTimer = null; 34 var voiceReaderWarmSignature = ''; 35 var voiceReaderSpeechUtterance = null; 36 var voiceReaderSpeechQueue = []; 37 var voiceReaderSpeechIndex = 0; 38 var voiceReaderSpeechActive = false; 39 var voiceReaderExtensionQueue = []; 40 var voiceReaderExtensionIndex = 0; 41 var voiceReaderExtensionActive = false; 42 var voiceReaderPlayback = null; 43 var voiceReaderStatusMessage = ''; 44 var voiceReaderStatusKind = 'idle'; 45 var voiceReaderStatusTimer = null; 46 var voiceReaderGeneration = 0; 47 var voiceReaderPrefetchController = null; 48 var voiceReaderPrefetch = null; 49 var voiceReaderVoiceWaiter = null; 50 var voiceReaderNeedsResume = false; 51 52 // ========================================================================== 53 // APPLY SETTINGS 54 // ========================================================================== 55 56 function applyTheme(theme) { 57 document.documentElement.setAttribute('data-theme', theme); 58 currentTheme = theme; 59 localStorage.setItem(STORAGE_KEYS.theme, theme); 60 updateToolbarState(); 61 } 62 63 function applyFontSize(size) { 64 if (size === 'normal') { 65 document.documentElement.removeAttribute('data-font-size'); 66 } else { 67 document.documentElement.setAttribute('data-font-size', size); 68 } 69 currentFontSize = size; 70 localStorage.setItem(STORAGE_KEYS.fontSize, size); 71 updateToolbarState(); 72 } 73 74 function applyContrast(enabled) { 75 if (enabled) { 76 document.documentElement.setAttribute('data-high-contrast', 'true'); 77 } else { 78 document.documentElement.removeAttribute('data-high-contrast'); 79 } 80 highContrast = enabled; 81 localStorage.setItem(STORAGE_KEYS.contrast, String(enabled)); 82 updateToolbarState(); 83 } 84 85 function applyLayout(layout) { 86 // 'boxed' sets data-layout="boxed" on <html>, 'full' removes the attribute 87 if (layout === 'boxed') { 88 document.documentElement.setAttribute('data-layout', 'boxed'); 89 } else { 90 document.documentElement.removeAttribute('data-layout'); 91 } 92 currentLayout = layout; 93 localStorage.setItem(STORAGE_KEYS.layout, layout); 94 updateToolbarState(); 95 } 96 97 function hasEasytextContent(block) { 98 return Boolean(block && ((block.textContent || '').replace(/\s+/g, ' ').trim() || block.querySelector('img, svg, picture, video, iframe'))); 99 } 100 101 function getEasytextBlock(group, selector) { 102 var block = group.querySelector(selector); 103 return hasEasytextContent(block) ? block : null; 104 } 105 106 /* 107 * An Easy-language variant must not silently remove an editorial call to 108 * action. Compare the actual link targets that belong to this group; empty 109 * a lone hash placeholder and javascript pseudo-links are presentation 110 * controls, not editorial destinations. Real fragment targets such as 111 * #kontakt remain protected. This intentionally fails closed: an editor can 112 * still use Easy Language for all other complete groups on the page. 113 */ 114 function collectSemanticLinkTargets(block, group) { 115 var targets = {}; 116 if (!block) { 117 return targets; 118 } 119 120 block.querySelectorAll('a[href]').forEach(function(link) { 121 if (link.closest('[data-mincore-easytext-group]') !== group) { 122 return; 123 } 124 125 var href = (link.getAttribute('href') || '').trim(); 126 if (!href || href === '#' || /^javascript:/i.test(href)) { 127 return; 128 } 129
130 try { 131 targets[new URL(href, window.location.href).href] = true; 132 } catch (error) { 133 targets[href] = true; 134 } 135 }); 136 137 return targets; 138 } 139 140 function hasCompleteSemanticLinkSet(normal, easy, group) { 141 var originalTargets = collectSemanticLinkTargets(normal, group); 142 var easyTargets = collectSemanticLinkTargets(easy, group); 143 144 return Object.keys(originalTargets).every(function(target) { 145 return easyTargets[target] === true; 146 }); 147 } 148 149 function setEasytextBlockVisible(block, visible) { 150 if (block) { 151 block.hidden = !visible; 152 } 153 } 154 155 function applyEasyTextBlocks(enabled) { 156 document.querySelectorAll('[data-mincore-easytext-group]').forEach(function(group) { 157 var normal = getEasytextBlock(group, '[data-level-6="true"]'); 158 var easy = getEasytextBlock(group, '[data-simple="true"]'); 159 var hasSafeEasyAlternative = group.getAttribute('data-has-easy') === 'true' && normal && easy && hasCompleteSemanticLinkSet(normal, easy, group) && !group.closest('[data-no-simple-language], [data-legal-page], [data-ce-type="min_legal"], [data-easytext-policy="external"], [data-mincore-easytext-policy="external"], [data-external-policy]'); 160 var target = enabled && hasSafeEasyAlternative ? easy : normal; 161 var source = target === easy ? 'simple' : 'original'; 162 163 group.setAttribute('data-easytext-current-source', source); 164 setEasytextBlockVisible(group.querySelector('[data-level-6="true"]'), target === normal); 165 setEasytextBlockVisible(group.querySelector('[data-level-3="true"]'), false); 166 setEasytextBlockVisible(group.querySelector('[data-simple="true"]'), target === easy); 167 }); 168 } 169 170 function isManagedEasytextBlockElement(el) { 171 if (el.closest('[data-mincore-easytext-group]')) { 172 return true; 173 } 174 175 return el.getAttribute('data-level-6') === 'true' || 176 el.getAttribute('data-level-3') === 'true' || 177 el.getAttribute('data-simple') === 'true'; 178 } 179 180 function applyEasyText(enabled) { 181 easyTextActive = enabled; 182 document.documentElement.setAttribute('data-easy-text', String(enabled)); 183 localStorage.setItem(STORAGE_KEYS.easyText, String(enabled)); 184 document.body.classList.toggle('simple-language-active', enabled); 185 186 applyEasyTextBlocks(enabled); 187 188 document.querySelectorAll('[data-text-easy], [data-simple], [data-level-3], [data-level-6]').forEach(function(el) { 189 if (isManagedEasytextBlockElement(el)) { 190 return; 191 } 192 193 var normalText = el.getAttribute('data-text-normal') || el.getAttribute('data-lotte-normal-html'); 194 if (!normalText) { 195 normalText = el.innerHTML; 196 el.setAttribute('data-lotte-normal-html', normalText); 197 } 198 var easyText = el.getAttribute('data-text-easy') || el.getAttribute('data-simple') || el.getAttribute('data-level-3') || el.getAttribute('data-level-6'); 199 if (easyText) { 200 el.innerHTML = enabled ? easyText : normalText; 201 } 202 }); 203 204 updateToolbarState(); 205 } 206 207 function findVoiceReaderControl() { 208 return document.querySelector('.voicereader-pro[data-vr-initialized="1"] [data-action="play"]'); 209 } 210 211 function getVoiceReaderArea() { 212 var areaButton = document.getElementById('voiceReaderArea'); 213 return areaButton && areaButton.getAttribute('aria-pressed') === 'true' ? 'main' : 'whole'; 214 } 215 216 function getVoiceReaderSource(area) { 217 if (area === 'whole') return document.body; 218 var mainContent = document.getElementById('main-content') || document.querySelector('main') || document.querySelector('article'); 219 if (!mainContent) { 220 return null; 221 } 222 return area === 'main' ? mainContent : document.body; 223 } 224 225 function removeUnreadableElements(root, includeSiteChrome) { 226 var selectors = 'script, style, noscript, svg, button, form, [aria-hidden="true"], [hidden], [data-no-readaloud], [data-no-voice-reader], .lotte-a11y-toolbar, .min-style-switcher, .styleswitcher, .voicereader-pro, .visually-hidden, .sr-only'; 227 if (!includeSiteChrome) { selectors += ', header, nav, footer, aside, [role="navigation"], [role="banner"], [role="contentinfo"]'; }
228 root.querySelectorAll(selectors).forEach(function(el) { el.remove(); }); 229 } 230 231 function extractReadableText(node, includeSiteChrome, visibleOnly) { 232 if (!node) { 233 return ''; 234 } 235 var clone = node.cloneNode(true); 236 if (visibleOnly && typeof window.getComputedStyle === 'function') { 237 var originals = [node].concat(Array.prototype.slice.call(node.querySelectorAll('*'))); 238 var copies = [clone].concat(Array.prototype.slice.call(clone.querySelectorAll('*'))); 239 originals.forEach(function(original, index) { 240 var style = window.getComputedStyle(original); 241 if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') { 242 if (index === 0) clone.textContent = ''; 243 else copies[index].remove(); 244 } 245 }); 246 } 247 removeUnreadableElements(clone, includeSiteChrome); 248 return (clone.innerText || clone.textContent || '').replace(/\s+/g, ' ').trim(); 249 } 250 251 function getStoredEasyText(source) { 252 if (!source || isLegalVoiceReaderPage()) return ''; 253 var blocks = []; 254 var exclusions = '[data-no-simple-language], [data-legal-page], [data-ce-type="min_legal"], [data-easytext-policy="external"], [data-mincore-easytext-policy="external"], [data-external-policy]'; 255 source.querySelectorAll('[data-mincore-easytext-group]').forEach(function(group) { 256 if (group.closest(exclusions) || group.getAttribute('data-has-easy') !== 'true') return; 257 var normal = getEasytextBlock(group, '[data-level-6="true"]'); 258 var easy = getEasytextBlock(group, '[data-simple="true"]'); 259 if (normal && easy && hasCompleteSemanticLinkSet(normal, easy, group)) blocks.push(extractReadableText(easy)); 260 }); 261 source.querySelectorAll('[data-simple="true"]').forEach(function(element) { 262 if (!element.closest('[data-mincore-easytext-group]') && !element.closest(exclusions)) blocks.push(extractReadableText(element)); 263 }); 264 source.querySelectorAll('[data-text-easy]').forEach(function(element) { 265 if (element.closest('[data-mincore-easytext-group]') || element.closest(exclusions)) return; 266 var storedText = element.getAttribute('data-text-easy'); 267 if (storedText && storedText.trim()) { 268 var storedRoot = document.createElement('div'); 269 storedRoot.innerHTML = storedText; 270 blocks.push(extractReadableText(storedRoot)); 271 } 272 });
273 return blocks.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim(); 274 } 275 276 function updateVoiceReaderModeAvailability() { 277 var select = document.getElementById('voiceReaderMode'); 278 if (!select) return; 279 var available = Boolean(getStoredEasyText(getVoiceReaderSource(getVoiceReaderArea()))); 280 var summaryAvailable = Boolean(getStoredPageSummary()); 281 if (select.value === 'summary' && summaryAvailable) prepareOriginalSummary(); 282 Array.prototype.forEach.call(select.options, function(option) { 283 if (option.value === 'leichteSprache') { option.disabled = !available; option.hidden = !available; } 284 if (option.value === 'summary') { option.textContent = 'Zusammenfassung (Originalauszug)'; option.disabled = !summaryAvailable; option.hidden = !summaryAvailable; } 285 if (isLegalVoiceReaderPage() && option.value !== 'standard') option.disabled = true; 286 }); 287 if ((!available && select.value === 'leichteSprache') || (!summaryAvailable && select.value === 'summary') || isLegalVoiceReaderPage()) select.value = 'standard'; 288 } 289 290 function isLegalVoiceReaderPage() { 291 var path = (window.location.pathname || '').toLowerCase(); 292 return /\/(impressum|datenschutz|datenschutzerklaerung|agb|widerruf|haftung|barrierefreiheitserklaerung)(\/|$)/.test(path) || 293 Boolean(document.querySelector('[data-legal-page="true"], [data-no-simple-language="true"], [data-ce-type="min_legal"]')); 294 } 295 296 // Summaries contain complete original blocks only. Provider prose is never used. 297 var SUMMARY_EXCLUSIONS = 'script, style, noscript, svg, button, nav, header, footer, aside, form, [role="dialog"], [role="navigation"], [data-no-readaloud], [data-no-voice-reader], [data-no-simple-language], [data-legal-page], [data-ce-type="min_legal"], [data-easytext-policy="external"], [data-mincore-easytext-policy="external"], [data-external-policy], [data-voicereader-summary], [data-voicereader-variant="summary"], .styleswitcher, .voicereader-pro, .sr-only, .visually-hidden'; 298 299 function getOriginalSummaryUnits() { 300 var source = getVoiceReaderSource('main'); 301 if (!source || isLegalVoiceReaderPage() || source.closest(SUMMARY_EXCLUSIONS)) return []; 302 var legalTitle = /bekanntmachung|satzung|hebesaetz|hebesätz|widerruf|haftung|datenschutz|impressum/i;
303 var pageHeading = source.querySelector('h1'); 304 if (/bekanntmachung|satzung|hebesaetz/i.test(window.location.pathname || '') || (pageHeading && legalTitle.test(pageHeading.textContent || ''))) return []; 305 var exclusions = SUMMARY_EXCLUSIONS + ', [hidden], [aria-hidden="true"], [data-simple="true"], [data-level-3="true"]'; 306 var nodes = Array.prototype.filter.call(source.querySelectorAll('p, li'), function(node) { 307 return !node.closest(exclusions) && !(node.tagName === 'P' && node.parentElement && node.parentElement.closest('li')); 308 }); 309 if (!nodes.length) nodes = Array.prototype.filter.call(source.querySelectorAll('h1, h2, h3'), function(node) { return !node.closest(exclusions); }); 310 var units = [], length = 0; 311 nodes.forEach(function(node) { 312 if (units.length >= 64) return; 313 var section = node.closest('section, article'); 314 if (section && section !== source) { 315 var heading = section.querySelector('h2, h3'); 316 if (heading && legalTitle.test(heading.textContent || '')) return; 317 } 318 // Compare textContent on a sanitized clone; layout never supplies new text. 319 var clone = node.cloneNode(true); 320 var originals = Array.prototype.slice.call(node.querySelectorAll('*')); 321 var copies = Array.prototype.slice.call(clone.querySelectorAll('*')); 322 originals.forEach(function(child, index) { 323 var style = window.getComputedStyle(child); 324 if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') copies[index].remove(); 325 });
326 clone.querySelectorAll(exclusions).forEach(function(child) { child.remove(); }); 327 var text = (clone.textContent || '').replace(/\s+/g, ' ').trim(); 328 if (!text || units.indexOf(text) !== -1 || length + text.length > 6000) return; 329 // A CSS-hidden block must never become a fallback summary. 330 for (var parent = node; parent && parent !== source.parentElement; parent = parent.parentElement) { 331 var style = window.getComputedStyle(parent); 332 if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') return; 333 } 334 units.push(text); length += text.length; 335 }); 336 return units; 337 } 338 339 function getOriginalSummaryFallback(units) { 340 var substantive = units.filter(function(unit) { return unit.length >= 60 || unit.split(/\s+/).length >= 8 || /[.!?]$/.test(unit); }); 341 if (substantive.length) units = substantive; 342 var selected = [], length = 0; 343 units.some(function(unit) { 344 if (selected.length && (selected.length >= 3 || length + unit.length > 900)) return true; 345 selected.push(unit); length += unit.length; 346 return false; 347 }); 348 return selected.join(' '); 349 } 350 351 function getStoredPageSummary() { 352 // Recompute from the current DOM on every use: no stale stored summary. 353 return getOriginalSummaryFallback(getOriginalSummaryUnits()); 354 } 355 356 function prepareOriginalSummary() {} 357 358 function getReadableText(options) { 359 options = options || {}; 360 var area = options.area || getVoiceReaderArea(), source = getVoiceReaderSource(area); 361 if (!source) return ''; 362 if (options.mode === 'summary') return getStoredPageSummary(source); 363 if (options.mode === 'leichteSprache') return getStoredEasyText(source); 364 if (area === 'whole') { 365 return [document.querySelector('header'), getVoiceReaderSource('main'), document.querySelector('footer')].filter(function(region, index, all) { return region && all.indexOf(region) === index; }).map(function(region) { return extractReadableText(region, true, true); }).filter(Boolean).join(' '); 366 } 367 return extractReadableText(source, false, true); 368 } 369 370 function releaseVoiceReaderAudio() { 371 if (voiceReaderObjectUrl) { 372 URL.revokeObjectURL(voiceReaderObjectUrl); 373 voiceReaderObjectUrl = null; 374 } 375 } 376 377 function isVoiceReaderActive() { 378 return voiceReaderLoading || 379 voiceReaderExtensionActive || 380 voiceReaderSpeechActive || 381 Boolean(voiceReaderAudio && !voiceReaderAudio.paused && !voiceReaderAudio.ended); 382 } 383 384 function setVoiceReaderStatus(message, kind, timeout) { 385 voiceReaderStatusMessage = message || ''; 386 voiceReaderStatusKind = kind || 'idle'; 387 window.clearTimeout(voiceReaderStatusTimer); 388 if (timeout) { 389 voiceReaderStatusTimer = window.setTimeout(function() { 390 voiceReaderStatusMessage = ''; 391 voiceReaderStatusKind = 'idle'; 392 updateToolbarState(); 393 }, timeout); 394 } 395 updateToolbarState(); 396 } 397 398 function resetVoiceReaderSpeech() { 399 voiceReaderSpeechUtterance = null; 400 voiceReaderSpeechQueue = []; 401 voiceReaderSpeechIndex = 0; 402 voiceReaderSpeechActive = false; 403 } 404 405 function resetVoiceReaderAudio() { 406 releaseVoiceReaderAudio(); 407 voiceReaderAudio = null; 408 voiceReaderLoading = false; 409 voiceReaderRequestController = null; 410 updateToolbarState(); 411 } 412 413 function resetVoiceReaderExtension() { 414 voiceReaderExtensionQueue = []; 415 voiceReaderExtensionIndex = 0; 416 voiceReaderExtensionActive = false; 417 } 418 419 function stopVoiceReader(silent) { 420 voiceReaderGeneration += 1; 421 voiceReaderNeedsResume = false; 422 if (voiceReaderVoiceWaiter) { window.speechSynthesis.removeEventListener('voiceschanged', voiceReaderVoiceWaiter.listener); window.clearTimeout(voiceReaderVoiceWaiter.timer); voiceReaderVoiceWaiter = null; } 423 if (voiceReaderPrefetchController) { voiceReaderPrefetchController.abort(); voiceReaderPrefetchController = null; } 424 voiceReaderPrefetch = null; 425 if (voiceReaderRequestController) { 426 voiceReaderRequestController.abort(); 427 voiceReaderRequestController = null; 428 } 429 430 if (voiceReaderAudio) { 431 voiceReaderAudio.pause(); 432 voiceReaderAudio.currentTime = 0; 433 } 434 435 if (window.speechSynthesis) { 436 window.speechSynthesis.cancel(); 437 } 438 439 resetVoiceReaderExtension(); 440 resetVoiceReaderSpeech(); 441 voiceReaderPlayback = null; 442 resetVoiceReaderAudio(); 443 if (!silent) { 444 setVoiceReaderStatus('Vorlesen gestoppt.', 'stopped'); 445 } 446 } 447 448 function getVoiceReaderSpeed() { 449 var speedSelect = document.getElementById('voiceReaderSpeed') || document.querySelector('.voicereader-pro [data-action="speed"]'); 450 var speed = speedSelect ? parseFloat(speedSelect.value) : 1.0; 451 452 return Math.max(0.5, Math.min(2.0, Number.isFinite(speed) ? speed : 1.0)); 453 } 454 455 function getVoiceReaderVoice() { 456 var voiceSelect = document.getElementById('voiceReaderVoice'); 457 return voiceSelect && voiceSelect.value ? voiceSelect.value : 'eva_k'; 458 } 459 460 function getVoiceReaderMode() { 461 if (isLegalVoiceReaderPage()) { 462 return 'standard'; 463 } 464 var modeSelect = document.getElementById('voiceReaderMode'); 465 if (modeSelect && modeSelect.value) { 466 return modeSelect.value; 467 } 468 469 return easyTextActive ? 'leichteSprache' : 'standard'; 470 } 471 472 function getVoiceReaderPlayback() { 473 var mode = getVoiceReaderMode(); 474 return { 475 voice: getVoiceReaderVoice(), 476 speed: getVoiceReaderSpeed(), 477 mode: mode, 478 area: getVoiceReaderArea(), 479 allowFallback: mode === 'standard' || mode === 'leichteSprache' 480 }; 481 } 482 483 // Deliberately exposes only QA metadata. Extracted page text never leaves this 484 // closure through the public A11y helper. 485 function getVoiceReaderState() { 486 var playback = voiceReaderPlayback || getVoiceReaderPlayback(); 487 var standardText = getVoiceReaderRequestText({ area: playback.area, mode: '
487standard' }); 488 var easyText = getVoiceReaderRequestText({ area: playback.area, mode: 'leichteSprache' }); 489 return { 490 active: isVoiceReaderActive(), 491 loading: voiceReaderLoading, 492 paused: false, 493 status: voiceReaderStatusKind, 494 statusMessage: voiceReaderStatusMessage, 495 voice: playback.voice, 496 speed: playback.speed, 497 audioPlaybackRate: voiceReaderAudio ? voiceReaderAudio.playbackRate : null, 498 mode: playback.mode, 499 area: playback.area, 500 legalPage: isLegalVoiceReaderPage(), 501 standardTextLength: standardText.length, 502 easyTextAvailable: Boolean(easyText), 503 easyTextLength: easyText.length 504 }; 505 } 506 507 function splitSpeechText(text) { 508 var chunks = [], remaining = text.replace(/\s+/g, ' ').trim(); 509 while (remaining.length) { 510 var limit = chunks.length ? 600 : 120; 511 if (remaining.length <= limit) { chunks.push(remaining); break; } 512 var breakAt = Math.max(remaining.lastIndexOf('. ', limit - 1), remaining.lastIndexOf('! ', limit - 1), remaining.lastIndexOf('? ', limit - 1), remaining.lastIndexOf('; ', limit - 1), remaining.lastIndexOf(', ', limit - 1)); 513 if (breakAt < Math.floor(limit / 3)) { 514 breakAt = remaining.lastIndexOf(' ', limit); 515 if (breakAt < 0) breakAt = remaining.indexOf(' ', limit); 516 if (breakAt < 0) { chunks.push(remaining); break; } 517 } 518 chunks.push(remaining.slice(0, breakAt + 1).trim()); 519 remaining = remaining.slice(breakAt + 1).trim(); 520 } 521 return chunks.filter(Boolean); 522 } 523 524 function getVoiceReaderRequestText(playback) { 525 var text = getReadableText(playback); 526 if (!text) { 527 return ''; 528 } 529 530 return VOICE_READER_MAX_TEXT_LENGTH ? text.slice(0, VOICE_READER_MAX_TEXT_LENGTH).trim() : text; 531 } 532 533 function scheduleVoiceReaderWarm() {} 534 535 function resolveSpeechVoice(playback) { 536 if (!window.speechSynthesis || typeof window.speechSynthesis.getVoices !== 'function') { 537 return null; 538 } 539 540 var selectedVoice = (playback && playback.voice ? playback.voice : getVoiceReaderVoice()).toLowerCase(); 541 var voices = window.speechSynthesis.getVoices(); 542 var matchingName = voices.find(function(voice) { 543 return /^de([-_]|$)/i.test(voice.lang || '') && voice.name && voice.name.toLowerCase().indexOf(selectedVoice) !== -1; 544 }); 545 546 if (matchingName) { 547 return matchingName; 548 } 549 550 return voices.find(function(voice) { 551 return /^de([-_]|$)/i.test(voice.lang || ''); 552 }) || null; 553 } 554 555 function speakNextLocalChunk() { 556 if (!window.speechSynthesis || !voiceReaderSpeechActive) { 557 resetVoiceReaderSpeech(); 558 updateToolbarState(); 559 return; 560 } 561 562 if (voiceReaderSpeechIndex >= voiceReaderSpeechQueue.length) { 563 resetVoiceReaderSpeech(); 564 voiceReaderPlayback = null; 565 setVoiceReaderStatus('Vorlesen beendet.', 'complete', 5000); 566 updateToolbarState(); 567 return; 568 } 569 570 var speechGeneration = voiceReaderGeneration; 571 var utterance = new SpeechSynthesisUtterance(voiceReaderSpeechQueue[voiceReaderSpeechIndex]); 572 var voice = resolveSpeechVoice(voiceReaderPlayback); 573 utterance.lang = voice && voice.lang ? voice.lang : 'de-DE'; 574 utterance.rate = voiceReaderPlayback ? voiceReaderPlayback.speed : getVoiceReaderSpeed(); 575 utterance.pitch = 1; 576 if (voice) { 577 utterance.voice = voice; 578 } 579 580 utterance.onend = function() { 581 if (speechGeneration !== voiceReaderGeneration || voiceReaderSpeechUtterance !== utterance) return; 582 voiceReaderSpeechIndex += 1; 583 speakNextLocalChunk(); 584 }; 585 utterance.onerror = function(event) { 586 if (speechGeneration !== voiceReaderGeneration || voiceReaderSpeechUtterance !== utterance) return; 587 if (!event || event.error !== 'interrupted') { 588 console.warn('VoiceReader local speech failed.', event); 589 } 590 resetVoiceReaderSpeech(); 591 voiceReaderPlayback = null; 592 setVoiceReaderStatus('Vorlesen im Browser ist fehlgeschlagen.', 'error'); 593 updateToolbarState(); 594 }; 595 596 voiceReaderSpeechUtterance = utterance; 597 window.speechSynthesis.speak(utterance); 598 updateToolbarState(); 599 } 600 601 function playWithLocalSpeech(text, playback) { 602 stopVoiceReader(true); 603 if (!('speechSynthesis' in window) || typeof window.SpeechSynthesisUtterance === 'undefined') { 604 console.warn('VoiceReader fallback unavailable: speechSynthesis is not supported.'); 605 voiceReaderPlayback = null; 606 setVoiceReaderStatus('Vorlesen ist in diesem Browser nicht verfügbar.', 'error'); 607 return; 608 } 609 610 stopVoiceReader(true); 611 voiceReaderPlayback = playback || getVoiceReaderPlayback(); 612 voiceReaderSpeechQueue = splitSpeechText(text); 613 voiceReaderSpeechIndex = 0; 614 voiceReaderSpeechActive = voiceReaderSpeechQueue.length > 0; 615 voiceReaderLoading = false;
616 617 if (!voiceReaderSpeechActive) { 618 voiceReaderPlayback = null; 619 setVoiceReaderStatus('Für diesen Bereich ist kein vorlesbarer Text vorhanden.', 'error'); 620 updateToolbarState(); 621 return; 622 } 623 624 var generation = voiceReaderGeneration; 625 function finishWaiting() { 626 if (voiceReaderVoiceWaiter && voiceReaderVoiceWaiter.generation === generation) { 627 window.speechSynthesis.removeEventListener('voiceschanged', voiceReaderVoiceWaiter.listener); 628 window.clearTimeout(voiceReaderVoiceWaiter.timer); 629 voiceReaderVoiceWaiter = null; 630 } 631 } 632 function startLocal(timedOut) { 633 if (generation !== voiceReaderGeneration || !voiceReaderSpeechActive || voiceReaderSpeechUtterance) return; 634 if (resolveSpeechVoice(voiceReaderPlayback)) { 635 finishWaiting(); 636 speakNextLocalChunk(); 637 setVoiceReaderStatus('Vorlesen läuft im Browser.', 'playing'); 638 } else if (timedOut) { 639 finishWaiting(); 640 resetVoiceReaderSpeech(); 641 voiceReaderPlayback = null; 642 setVoiceReaderStatus('Keine deutsche Browserstimme ist verfügbar.', 'error'); 643 } 644 } 645 if (resolveSpeechVoice(voiceReaderPlayback)) { 646 startLocal(false); 647 } else { 648 setVoiceReaderStatus('Deutsche Browserstimme wird vorbereitet.', 'loading'); 649 var onVoices = function() { startLocal(false); }; 650 voiceReaderVoiceWaiter = { generation: generation, listener: onVoices, timer: null }; 651 window.speechSynthesis.addEventListener('voiceschanged', onVoices); 652 voiceReaderVoiceWaiter.timer = window.setTimeout(function() { startLocal(true); }, 1500); 653 } 654 } 655 656 function failVoiceReaderPlayback(error, fallbackText) { 657 if (voiceReaderSpeechActive || (error && error.name === 'AbortError')) return; 658 var playback = voiceReaderPlayback; 659 stopVoiceReader(true); 660 console.warn('VoiceReader playback failed.', error); 661 if (playback && playback.allowFallback && fallbackText) { 662 playWithLocalSpeech(fallbackText, playback); 663 } else { 664 setVoiceReaderStatus('Vorlesen ist derzeit nicht verfügbar. Bitte erneut versuchen.', 'error'); 665 } 666 } 667 668 function fetchExtensionChunk(index, playback, generation, controller) { 669 if (voiceReaderExtensionQueue[index].length > 600) return Promise.reject(new Error('VoiceReader text segment exceeds the request limit.')); 670 return fetch(VOICE_READER_ENDPOINT, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, signal: controller ? controller.signal : undefined, body: JSON.stringify({ text: voiceReaderExtensionQueue[index], voice: playback.voice, speed: 1, language: 'de', mode: playback.mode }) }).then(function(response) { if (!response.ok) throw new Error('VoiceReader endpoint returned ' + response.status + '.'); return response.blob(); }).then(function(blob) { if (!blob || !blob.size) throw new Error('VoiceReader returned empty audio.'); if (generation !== voiceReaderGeneration) throw new DOMException('stale', 'AbortError'); return blob; }); 671 } 672 function playNextExtensionChunk() { 673 var playback = voiceReaderPlayback, generation = playback && playback.generation, index = voiceReaderExtensionIndex; 674 if (!voiceReaderExtensionActive || generation !== voiceReaderGeneration) return Promise.resolve(); 675 if (index >= voiceReaderExtensionQueue.length) { resetVoiceReaderExtension(); voiceReaderPlayback = null; resetVoiceReaderAudio(); setVoiceReaderStatus('Vorlesen beendet.', 'complete', 5000); return Promise.resolve(); } 676 voiceReaderLoading = true; updateToolbarState(); 677 var controller = 'AbortController' in window ? new AbortController() : null; 678 voiceReaderRequestController = controller; 679 var cached = voiceReaderPrefetch && voiceReaderPrefetch.index === index ? voiceReaderPrefetch : null; 680 var current = cached ? cached.promise : fetchExtensionChunk(index, playback, generation, controller); 681 if (cached) { voiceReaderRequestController = cached.controller; voiceReaderPrefetch = null; voiceReaderPrefetchController = null; } 682 return current.then(function(blob) { if (generation !== voiceReaderGeneration) return; if (index + 1 < voiceReaderExtensionQueue.length && !voiceReaderPrefetch) { voiceReaderPrefetchController = 'AbortController' in window ? new AbortController() : null; voiceReaderPrefetch = { index: index + 1, controller: voiceReaderPrefetchController, promise: fetchExtensionChunk(index + 1, playback, generation, voiceReaderPrefetchController) }; voiceReaderPrefetch.promise.catch(function() {}); } releaseVoiceReaderAudio(); voiceReaderObjectUrl = URL.createObjectURL(blob); voiceReaderAudio = new Audio(voiceReaderObjectUrl); voiceReaderAudio.playbackRate = playback.speed; voiceReaderAudio.preservesPitch = true; if ('webkitPreservesPitch' in voiceReaderAudio) voiceReaderAudio.webkitPreservesPitch = true; voiceReaderAudio.addEventListener('ended', function() { if (generation === voiceReaderGeneration) { releaseVoiceReaderAudio(); voiceReaderAudio = null; voiceReaderExtensionIndex += 1; playNextExtensionChunk(); } }, { once: true }); voiceReaderAudio.addEventListener('error', function() { if (generation === voiceReaderGeneration) failVoiceReaderPlayback(new Error('Audio playback failed.'), voiceReaderExtensionQueue.slice(index).join(' ')); }, { once: true }); voiceReaderLoading = false; voiceReaderRequestController = null; updateToolbarState(); return voiceReaderAudio.play().then(function() { if (generation === voiceReaderGeneration) setVoiceReaderStatus('Wird vorgelesen.', 'playing'); }); }).catch(function(error) { if (generation !== voiceReaderGeneration) return;
682 if (error && error.name === 'NotAllowedError' && voiceReaderAudio) { voiceReaderNeedsResume = true; voiceReaderLoading = false; setVoiceReaderStatus('Zum Starten bitte noch einmal auf Vorlesen klicken.', 'paused'); } else { failVoiceReaderPlayback(error, voiceReaderExtensionQueue.slice(index).join(' ')); } }); 683 } 684 function playWithExtension(text, playback) { 685 if (!window.fetch || typeof window.Audio === 'undefined') { 686 if (playback.allowFallback) { 687 playWithLocalSpeech(text, playback); 688 } else { 689 setVoiceReaderStatus('Vorlesen ist für den gewählten Modus in diesem Browser nicht verfügbar.', 'error'); 690 } 691 return Promise.resolve(); 692 } 693 694 stopVoiceReader(true); 695 voiceReaderPlayback = playback; 696 voiceReaderPlayback.generation = voiceReaderGeneration; 697 voiceReaderExtensionQueue = splitSpeechText(text); 698 voiceReaderExtensionIndex = 0; 699 voiceReaderExtensionActive = voiceReaderExtensionQueue.length > 0; 700 701 if (!voiceReaderExtensionActive) { 702 voiceReaderPlayback = null; 703 setVoiceReaderStatus('Für diesen Bereich ist kein vorlesbarer Text vorhanden.', 'error'); 704 updateToolbarState(); 705 return Promise.resolve(); 706 } 707 708 setVoiceReaderStatus('Vorlesen wird vorbereitet.', 'loading'); 709 return playNextExtensionChunk(); 710 } 711 712 function restartVoiceReaderForSettings() { updateVoiceReaderModeAvailability(); if (!isVoiceReaderActive()) { return; } stopVoiceReader(true); toggleVorlesen(); } 713 714 function toggleVorlesen() { 715 if (voiceReaderNeedsResume && voiceReaderAudio) { 716 var resumeGeneration = voiceReaderGeneration; 717 voiceReaderNeedsResume = false; 718 voiceReaderAudio.play().then(function() { 719 if (resumeGeneration === voiceReaderGeneration) setVoiceReaderStatus('Wird vorgelesen.', 'playing'); 720 }).catch(function(error) { 721 if (resumeGeneration === voiceReaderGeneration) failVoiceReaderPlayback(error, voiceReaderExtensionQueue.slice(voiceReaderExtensionIndex).join(' ')); 722 }); 723 return; 724 } 725 var voiceReaderControl = findVoiceReaderControl(); 726 if (voiceReaderControl) { 727 voiceReaderControl.click(); 728 return; 729 } 730 731 if (isVoiceReaderActive()) { 732 stopVoiceReader(); 733 return; 734 } 735 736 var playback = getVoiceReaderPlayback(); 737 if (playback.mode === 'summary') prepareOriginalSummary(); 738 var text = getVoiceReaderRequestText(playback); 739 if (playback.mode === 'leichteSprache' && !text) { 740 setVoiceReaderStatus('Für diese Seite liegt noch kein hinterlegter Text in Leichter Sprache vor.', 'unavailable'); 741 return; 742 } 743 if (!text) { 744 setVoiceReaderStatus('Für diesen Bereich ist kein vorlesbarer Text vorhanden.', 'error'); 745 updateToolbarState(); 746 return; 747 } 748 749 playWithExtension(text, playback); 750 } 751 752 // ========================================================================== 753 // TOOLBAR 754 // ========================================================================== 755 756 function createToolbar() { 757 if (document.querySelector('.styleswitcher') || document.querySelector('.min-style-switcher') || document.querySelector('.lotte-a11y-toolbar')) { 758 return; 759 } 760 761 var toolbar = document.createElement('div'); 762 toolbar.className = 'lotte-a11y-toolbar'; 763 toolbar.setAttribute('role', 'toolbar'); 764 toolbar.setAttribute('aria-label', 'Barrierefreiheit-Optionen'); 765 766 // Pruefen ob Seite Einfache-Sprache-Texte hat 767 var hasEasyText = document.querySelector('[data-text-easy], [data-simple="true"], [data-mincore-easytext-group][data-has-easy="true"]'); 768 769 toolbar.innerHTML = 770 /* Dark Mode */ 771 '<button class="lotte-a11y-toolbar__btn" data-action="theme" title="Dark Mode" aria-label="Dark Mode umschalten" aria-pressed="false">' + 772 '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' + 773 '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>' + 774 '</svg>' + 775 '</button>' + 776 /* Schrift groesser */ 777 '<button class="lotte-a11y-toolbar__btn" data-action="font-increase" title="Schrift gr\u00f6\u00dfer" aria-label="Schrift vergr\u00f6\u00dfern">' + 778 '<span class="lotte-a11y-toolbar__text-symbol lotte-a11y-toolbar__text-symbol--large">A+</span>' + 779 '</button>' + 780 /* Schrift kleiner */ 781 '<button class="lotte-a11y-toolbar__btn" data-action="font-decrease" title="Schrift kleiner" aria-label="Schrift verkleinern">' + 782 '<span class="lotte-a11y-toolbar__text-symbol lotte-a11y-toolbar__text-symbol--small">A\u2212</span>' + 783 '</button>' + 784 /* Hoher Kontrast */ 785 '<button class="lotte-a11y-toolbar__btn" data-action="contrast" title="Hoher Kontrast" aria-label="Hohen Kontrast umschalten" aria-pressed="false">' + 786 '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' + 787 '<circle cx="12" cy="12" r="10"/>' + 788 '<path d="M12 2a10 10 0 0 1 0 20V2z" fill="currentColor"/>' + 789 '</svg>' + 790 '</button>' + 791 /* Layout toggle: full-width vs. boxed (max-width 1300px) */ 792 '<button class="lotte-a11y-toolbar__btn" data-action="layout-toggle" title="Boxed Layout (1300px)" aria-label="Boxed/Full-Width Layout umschalten" aria-pressed="false">' + 793 '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' + 794 '<polyline points="15 3 21 3 21 9"/>' + 795 '<polyline points="9 21 3 21 3 15"/>' + 796 '<line x1="21" y1="3" x2="14" y2="10"/>' +
797 '<line x1="3" y1="21" x2="10" y2="14"/>' + 798 '</svg>' + 799 '</button>' + 800 /* Leichte Sprache (nur wenn Seite data-text-easy hat) */ 801 (hasEasyText ? 802 '<button class="lotte-a11y-toolbar__btn lotte-a11y-toolbar__btn--divider" data-action="easy-text" title="Leichte Sprache" aria-label="Leichte Sprache umschalten" aria-pressed="false">' + 803 '<span class="lotte-a11y-toolbar__text-symbol lotte-a11y-toolbar__text-symbol--small">ES</span>' + 804 '</button>' : '') + 805 /* Vorlesen */ 806 '<button class="lotte-a11y-toolbar__btn" data-action="vorlesen" title="Seite vorlesen" aria-label="Seite vorlesen">' + 807 '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' + 808 '<path d="M11 5L6 9H2v6h4l5 4V5z"/>' + 809 '<path d="M15.54 8.46a5 5 0 010 7.07"/>' + 810 '</svg>' + 811 '</button>'; 812 813 document.body.appendChild(toolbar); 814 815 toolbar.addEventListener('click', function(e) { 816 var btn = e.target.closest('.lotte-a11y-toolbar__btn'); 817 if (!btn) return; 818 819 var action = btn.dataset.action; 820 821 switch(action) { 822 case 'theme': 823 applyTheme(currentTheme === 'light' ? 'dark' : 'light'); 824 break; 825 case 'font-increase': 826 increaseFontSize(); 827 break; 828 case 'font-decrease': 829 decreaseFontSize(); 830 break; 831 case 'contrast': 832 applyContrast(!highContrast); 833 break; 834 case 'layout-toggle': 835 applyLayout(currentLayout === 'full' ? 'boxed' : 'full'); 836 break; 837 case 'easy-text': 838 applyEasyText(!easyTextActive); 839 break; 840 case 'vorlesen': 841 toggleVorlesen(); 842 break; 843 } 844 }); 845 846 updateToolbarState(); 847 } 848 849 function updateToolbarState() { 850 var themeBtn = document.querySelector('[data-action="theme"]'); 851 if (themeBtn) { 852 themeBtn.classList.toggle('active', currentTheme === 'dark'); 853 themeBtn.setAttribute('aria-pressed', String(currentTheme === 'dark')); 854 } 855 var contrastBtn = document.querySelector('[data-action="contrast"]'); 856 if (contrastBtn) { 857 contrastBtn.classList.toggle('active', highContrast); 858 contrastBtn.setAttribute('aria-pressed', String(highContrast)); 859 } 860 var easyBtn = document.querySelector('[data-action="easy-text"]'); 861 if (easyBtn) { 862 easyBtn.classList.toggle('active', easyTextActive); 863 easyBtn.setAttribute('aria-pressed', String(easyTextActive)); 864 } 865 var layoutBtn = document.querySelector('[data-action="layout-toggle"]'); 866 if (layoutBtn) { 867 // Button is "active" (pressed) when boxed mode is on 868 layoutBtn.classList.toggle('active', currentLayout === 'boxed'); 869 layoutBtn.setAttribute('aria-pressed', String(currentLayout === 'boxed')); 870 } 871 var readBtn = document.querySelector('[data-action="vorlesen"]'); 872 var prototypeReadBtn = document.getElementById('voiceReaderToggle'); 873 var prototypeStatus = document.getElementById('voiceReaderStatus'); 874 var prototypeStatusText = document.getElementById('voiceReaderStatusText'); 875 var prototypeProgress = document.getElementById('voiceReaderProgress'); 876 var isReading = isVoiceReaderActive(); 877 878 if (readBtn) { 879 readBtn.classList.toggle('active', isReading); 880 readBtn.setAttribute('aria-pressed', String(isReading)); 881 readBtn.setAttribute('aria-busy', String(voiceReaderLoading)); 882 readBtn.setAttribute('aria-label', voiceReaderLoading ? 'Vorlesen wird vorbereitet' : (isReading ? 'Vorlesen stoppen' : 'Seite vorlesen')); 883 readBtn.setAttribute('title', voiceReaderLoading ? 'Vorlesen wird vorbereitet' : (isReading ? 'Vorlesen stoppen' : 'Seite vorlesen')); 884 } 885 if (prototypeReadBtn) { 886 prototypeReadBtn.classList.toggle('styleswitcher__toggle--active', isReading); 887 prototypeReadBtn.setAttribute('aria-pressed', String(isReading)); 888 prototypeReadBtn.setAttribute('aria-busy', String(voiceReaderLoading)); 889 prototypeReadBtn.textContent = voiceReaderLoading ? '...' : (isReading ? 'â ' : 'â¶'); 890 prototypeReadBtn.setAttribute('aria-label', voiceReaderLoading ? 'Vorlesen wird vorbereitet' : (isReading ? 'Vorlesen stoppen' : 'Seite vorlesen lassen')); 891 prototypeReadBtn.setAttribute('title', voiceReaderLoading ? 'Vorlesen wird vorbereitet' : (isReading ? 'Vorlesen stoppen' : 'Vorlesen starten')); 892 } 893 if (prototypeStatus) { 894 prototypeStatus.hidden = !(voiceReaderLoading || isReading || voiceReaderStatusMessage); 895 prototypeStatus.setAttribute('data-voicereader-status', voiceReaderStatusKind); 896 prototypeStatus.setAttribute('role', 'status'); 897 prototypeStatus.setAttribute('aria-live', 'polite'); 898 } 899 if (prototypeStatusText) { 900 prototypeStatusText.setAttribute('role', 'status'); 901 prototypeStatusText.setAttribute('aria-live', 'polite'); 902 prototypeStatusText.textContent = voiceReaderStatusMessage || (voiceReaderLoading ? 'Vorlesen wird vorbereitet.' : (isReading ? 'Wird vorgelesen.' : '')); 903 } 904 if (prototypeProgress) { 905 prototypeProgress.classList.toggle('styleswitcher__progress-fill--active', isReading); 906 } 907 } 908 909 function increaseFontSize() { 910 var idx = FONT_SIZES.indexOf(currentFontSize); 911 if (idx < FONT_SIZES.length - 1) { 912 applyFontSize(FONT_SIZES[idx + 1]); 913 } 914 } 915 916 function decreaseFontSize() { 917 var idx = FONT_SIZES.indexOf(currentFontSize); 918 if (idx > 0) {
919 applyFontSize(FONT_SIZES[idx - 1]); 920 } 921 } 922 923 // ========================================================================== 924 // KEYBOARD NAVIGATION 925 // ========================================================================== 926 927 function initKeyboardNav() { 928 document.addEventListener('keydown', function(e) { 929 if (e.key === 'Tab') { 930 document.body.classList.add('keyboard-nav'); 931 } 932 }); 933 934 document.addEventListener('mousedown', function() { 935 document.body.classList.remove('keyboard-nav'); 936 }); 937 } 938 939 function bindPrototypeVoiceReaderControls() { 940 updateVoiceReaderModeAvailability(); 941 var readButton = document.getElementById('voiceReaderToggle'); 942 var speedInput = document.getElementById('voiceReaderSpeed'); 943 var speedLabel = document.getElementById('voiceReaderSpeedLabel'); 944 var areaButton = document.getElementById('voiceReaderArea'); 945 946 if (readButton && readButton.dataset.lotteVoiceBound !== 'true') { 947 readButton.dataset.lotteVoiceBound = 'true'; 948 readButton.addEventListener('click', function() { 949 toggleVorlesen(); 950 }); 951 } 952 953 if (speedInput && speedLabel) { 954 function updateSpeedLabel() { 955 speedLabel.textContent = parseFloat(speedInput.value || '1').toString().replace('.', ',') + 'x'; 956 } 957 if (speedInput.dataset.lotteVoiceBound !== 'true') { 958 speedInput.dataset.lotteVoiceBound = 'true'; 959 speedInput.addEventListener('input', updateSpeedLabel); 960 speedInput.addEventListener('change', function() { 961 restartVoiceReaderForSettings(); 962 }); 963 } 964 updateSpeedLabel(); 965 } 966 967 var voiceSelect = document.getElementById('voiceReaderVoice'); 968 if (voiceSelect && voiceSelect.dataset.lotteVoiceWarmBound !== 'true') { 969 voiceSelect.dataset.lotteVoiceWarmBound = 'true'; 970 voiceSelect.addEventListener('change', function() { 971 restartVoiceReaderForSettings(); 972 }); 973 } 974 975 var modeSelect = document.getElementById('voiceReaderMode'); 976 if (modeSelect && isLegalVoiceReaderPage()) { 977 modeSelect.value = 'standard'; 978 Array.prototype.forEach.call(modeSelect.options, function(option) { 979 option.disabled = option.value !== 'standard'; 980 }); 981 modeSelect.setAttribute('aria-describedby', 'voiceReaderStatusText'); 982 } 983 if (modeSelect && modeSelect.dataset.lotteVoiceWarmBound !== 'true') { 984 modeSelect.dataset.lotteVoiceWarmBound = 'true'; 985 modeSelect.addEventListener('change', function() { 986 if (isLegalVoiceReaderPage()) { 987 modeSelect.value = 'standard'; 988 } 989 restartVoiceReaderForSettings(); 990 }); 991 } 992 993 if (areaButton && areaButton.dataset.lotteVoiceBound !== 'true') { 994 areaButton.dataset.lotteVoiceBound = 'true'; 995 areaButton.addEventListener('click', function() { 996 var mainOnly = areaButton.getAttribute('aria-pressed') !== 'true'; 997 areaButton.setAttribute('aria-pressed', String(mainOnly)); 998 areaButton.classList.toggle('styleswitcher__toggle--active', mainOnly); 999 areaButton.textContent = mainOnly ? 'Hauptinhalt' : 'Ganze Seite'; 1000 restartVoiceReaderForSettings(); 1001 }); 1002 } 1003 1004 updateToolbarState(); 1005 } 1006 1007 // ========================================================================== 1008 // BACK TO TOP (scroll listener fuer den Button aus footer.php) 1009 // ========================================================================== 1010 1011 function initBackToTop() { 1012 var btn = document.querySelector('.lotte-back-to-top, .back-to-top'); 1013 if (!btn) return; 1014 1015 if (btn.dataset.lotteA11yBound === 'true') return; 1016 btn.dataset.lotteA11yBound = 'true'; 1017 1018 btn.addEventListener('click', function() { 1019 window.scrollTo({ top: 0, behavior: 'smooth' }); 1020 }); 1021 1022 function updateBackToTop() { 1023 if (window.scrollY > 400) { 1024 btn.classList.add('is-visible'); 1025 btn.hidden = false; 1026 } else { 1027 btn.classList.remove('is-visible'); 1028 if (btn.classList.contains('lotte-back-to-top')) { 1029 btn.hidden = true; 1030 } 1031 } 1032 } 1033 1034 updateBackToTop(); 1035 window.addEventListener('scroll', updateBackToTop, { passive: true }); 1036 } 1037 1038 // ========================================================================== 1039 // REDUCED MOTION 1040 // ========================================================================== 1041 1042 function checkReducedMotion() { 1043 var mq = window.matchMedia('(prefers-reduced-motion: reduce)'); 1044 function handle(e) { 1045 if (e.matches) { 1046 document.documentElement.setAttribute('data-reduced-motion', 'true'); 1047 } else { 1048 document.documentElement.removeAttribute('data-reduced-motion'); 1049 } 1050 } 1051 handle(mq); 1052 mq.addEventListener('change', handle); 1053 } 1054 1055 // ========================================================================== 1056 // SYSTEM THEME DETECTION 1057 // ========================================================================== 1058 1059 function checkSystemTheme() { 1060 if (localStorage.getItem(STORAGE_KEYS.theme)) return; 1061 var prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); 1062 function handle(e) { 1063 if (!localStorage.getItem(STORAGE_KEYS.theme)) { 1064 applyTheme(e.matches ? 'dark' : 'light'); 1065 } 1066 } 1067 handle(prefersDark); 1068 prefersDark.addEventListener('change', handle); 1069 } 1070 1071 // ========================================================================== 1072 // INIT 1073 // ========================================================================== 1074
1075 function init() { 1076 applyTheme(currentTheme); 1077 applyFontSize(currentFontSize); 1078 applyContrast(highContrast); 1079 // Restore layout preference on page load (default is 'full' â no attribute set) 1080 applyLayout(currentLayout); 1081 bindPrototypeVoiceReaderControls(); 1082 if (!document.querySelector('.min-style-switcher') && !document.querySelector('.styleswitcher')) { 1083 createToolbar(); 1084 } 1085 initBackToTop(); 1086 initKeyboardNav(); 1087 checkReducedMotion(); 1088 checkSystemTheme(); 1089 1090 // Leichte Sprache wiederherstellen falls gespeichert 1091 if (easyTextActive) { 1092 applyEasyText(true); 1093 } 1094 1095 // Do not prewarm on every page load: the same-origin warm endpoint is rate 1096 // limited and should not create console noise before a user uses VoiceReader. 1097 } 1098 1099 if (document.readyState === 'loading') { 1100 document.addEventListener('DOMContentLoaded', init); 1101 } else { 1102 init(); 1103 } 1104 1105 window.lotteA11y = { 1106 getState: function() { 1107 return { 1108 theme: currentTheme, 1109 fontSize: currentFontSize, 1110 contrast: highContrast, 1111 easyText: easyTextActive, 1112 layout: currentLayout, 1113 reading: isVoiceReaderActive(), 1114 paused: false, 1115 voiceReader: getVoiceReaderState() 1116 }; 1117 }, 1118 applyTheme: applyTheme, 1119 applyFontSize: applyFontSize, 1120 applyContrast: applyContrast, 1121 applyLayout: applyLayout, 1122 applyEasyText: applyEasyText, 1123 toggleVorlesen: toggleVorlesen, 1124 getVoiceReaderState: getVoiceReaderState 1125 }; 1126 1127})();
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.