PageSourceSearch

https://www.lotte.de/typo3temp/assets/compressed/Accessibility-81f830ce6783667ffc6b964d28a4dee8.js?1789174820

js lotte.de collected 2026-10-02 18:59:13 UTC 33,122 bytes, 896 lines download raw bytes

1/**
2 * minCore Accessibility Module
3 * Native accessibility features that take WCAG 2.1 AAA seriously
4 *
5 * Components:
6 * - StyleSwitcher: Font size, line height, letter spacing, word spacing (the readability quartet)
7 * - ContrastSwitcher: 5 color schemes for every eye under the sun
8 * - EasytextSwitcher: 3-level language system (because not everyone speaks legalese)
9 * - FocusManager: Focus trap, skip links, focus restore (keyboard users are VIPs here)
10 * - AriaAnnouncer: Central ARIA live announcements (the town crier for screen readers)
11 * - ReducedMotion: Animation control (gentle on the vestibular system)
12 *
13 * IMPORTANT: All features work without JS too (Progressive Enhancement FTW)
14 *
15 * @version 2.0.0
16 * @since TYPO3 13 LTS
17 */
18(function() {
19    'use strict';
20
21    // Storage Keys
22    const STORAGE_PREFIX = 'mincore-a11y-';
23    const STORAGE_KEYS = {
24        fontSize: STORAGE_PREFIX + 'fontsize',
25        lineHeight: STORAGE_PREFIX + 'lineheight',
26        letterSpacing: STORAGE_PREFIX + 'letterspacing',
27        wordSpacing: STORAGE_PREFIX + 'wordspacing',
28        contrast: STORAGE_PREFIX + 'contrast',
29        easytext: STORAGE_PREFIX + 'easytext',
30        reducedMotion: STORAGE_PREFIX + 'reducedmotion'
31    };
32
33    // ==========================================================================
34    // AriaAnnouncer - The town crier for screen readers
35    // ==========================================================================
36    class AriaAnnouncer {
37        constructor() {
38            this.container = null;
39            this.init();
40        }
41
42        init() {
43            // Create persistent live region container - always listening, always ready
44            this.container = document.createElement('div');
45            this.container.id = 'mincore-aria-announcer';
46            this.container.setAttribute('role', 'status');
47            this.container.setAttribute('aria-live', 'polite');
48            this.container.setAttribute('aria-atomic', 'true');
49            this.container.className = 'mincore-sr-only';
50            document.body.appendChild(this.container);
51        }
52
53        /**
54         * Announce message to screen readers - politely or assertively
55         * @param {string} message - Message to announce
56         * @param {string} priority - 'polite' (waits its turn) or 'assertive' (interrupts immediately)
57         */
58        announce(message, priority = 'polite') {
59            if (!this.container) return;
60
61            this.container.setAttribute('aria-live', priority);
62
63            // Brief delay for screen reader compatibility - timing is everything
64            this.container.textContent = '';
65            requestAnimationFrame(() => {
66                this.container.textContent = message;
67            });
68        }
69
70        /**
71         * Important announcement - interrupts current output (use sparingly!)
72         * @param {string} message
73         */
74        announceImportant(message) {
75            this.announce(message, 'assertive');
76        }
77    }
78
79    // ==========================================================================
80    // ReducedMotion - Animations with an off switch
81    // ==========================================================================
82    class ReducedMotion {
83        constructor(announcer) {
84            this.announcer = announcer;
85            this.prefersReducedMotion = false;
86            this.userOverride = null;
87            this.init();
88        }
89
90        init() {
91            // Check system preference - respect the user's wishes
92            this.mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
93            this.prefersReducedMotion = this.mediaQuery.matches;
94
95            // Load user override - their choice trumps system settings
96            this.userOverride = this.loadSetting(STORAGE_KEYS.reducedMotion);
97
98            // Apply initial state - setting the stage
99            this.applyState();
100
101            // Listen for system preference changes - stay in sync
102            this.mediaQuery.addEventListener('change', (e) => {
103                this.prefersReducedMotion = e.matches;
104                if (this.userOverride === null) {
105                    this.applyState();
106                }
107            });
108
109            this.bindEvents();
110        }
111
112        bindEvents() {
113            document.querySelectorAll('[data-mincore-reducedmotion]').forEach(button => {
114                button.addEventListener('click', (e) => {
115                    e.preventDefault();
116                    this.toggle();
117                });
118            });
119        }
120
121        toggle() {
122            const currentState = this.isReducedMotionActive();
123            this.userOverride = !currentState;
124            this.saveSetting(STORAGE_KEYS.reducedMotion, this.userOverride.toString());
125            this.applyState();
126
127            const message = this.userOverride
128                ? 'Animations disabled'
129                : 'Animations enabled';
130            this.announcer.announce(message);
131        }
132
133        isReducedMotionActive() {
134            if (this.userOverride !== null) {
135                return this.userOverride === true || this.userOverride === 'true';
136            }
137            return this.prefersReducedMotion;
138        }
139
140        applyState() {
141            const isReduced = this.isReducedMotionActive();
142            document.documentElement.classList.toggle('mincore-reduced-motion', isReduced);
143            document.documentElement.setAttribute('data-reduced-motion', isReduced.toString());
144        }
145
146        loadSetting(key) {
147            try {
148                return localStorage.getItem(key);
149            } catch (e) {
150                return null;
151            }
152        }
153
154        saveSetting(key, value) {
155            try {
156                localStorage.setItem(key, value);
157            } catch (e) {
158                // localStorage not available
159            }
160        }
161    }
162
163    // ==========================================================================
164    // FocusManager - Keeping keyboard users on track
165    // ==========================================================================
166    class FocusManager {
167        constructor(announcer) {
168            this.announcer = announcer;
169            this.trapElement = null;
170            this.previousFocus = null;
171            this.focusableSelector = [
172                'a[href]',
173                'button:not([disabled])',
174                'input:not([disabled]):not([type="hidden"])',
175                'select:not([disabled])',
176                'textarea:not([disabled])',
177                '[tabindex]:not([tabindex="-1"])',
178                '[contenteditable="true"]'
179            ].join(', ');
180
181            this.init();
182        }
183
184        init() {
185            this.initSkipLinks();
186            this.initFocusVisible();
187        }
188
189        /**
190         * Initialize skip links - the express lane for keyboard users
191         */
192        initSkipLinks() {
193            document.querySelectorAll('.mincore-skip-link, [data-mincore-skiplink]').forEach(link => {
194                link.addEventListener('click', (e) => {
195                    const targetId = link.getAttribute('href')?.substring(1);
196                    if (!targetId) return;
197
198                    const target = document.getElementById(targetId);
199                    if (target) {
200                        e.preventDefault();
201                        this.focusElement(target);
202                        this.announcer.announce('Navigation skipped');
203                    }
204                });
205            });
206        }
207
208        /**
209         * Focus-Visible polyfill for older browsers - because everyone deserves focus
210         */
211        initFocusVisible() {
212            // Check if :focus-visible is supported
213            try {
214                document.querySelector(':focus-visible');
215            } catch (e) {
216                // Fallback: Add .focus-visible class manually
217                document.body.addEventListener('keydown', (e) => {
218                    if (e.key === 'Tab') {
219                        document.body.classList.add('mincore-keyboard-nav');
220                    }
221                });
222
223                document.body.addEventListener('mousedown', () => {
224                    document.body.classList.remove('mincore-keyboard-nav');
225                });
226            }
227        }
228
229        /**
230         * Focus an element (adding tabindex if needed)
231         * @param {HTMLElement} element
232         */
233        focusElement(element) {
234            if (!element) return;
235
236            // Scroll element into view - red carpet treatment
237            element.scrollIntoView({ behavior: 'smooth', block: 'start' });
238
239            // Set tabindex if element is not natively focusable
240            if (!element.matches(this.focusableSelector)) {
241                element.setAttribute('tabindex', '-1');
242            }
243
244            // Focus after short delay (for smooth scroll to complete)
245            setTimeout(() => {
246                element.focus({ preventScroll: true });
247            }, 100);
248        }
249
250        /**
251         * Activate focus trap - no escape for the keyboard (until you press Escape)
252         * @param {HTMLElement} container
253         */
254        trapFocus(container) {
255            if (!container) return;
256
257            this.previousFocus = document.activeElement;
258            this.trapElement = container;
259
260            // Get all focusable elements in the container
261            const focusables = container.querySelectorAll(this.focusableSelector);
262            if (focusables.length === 0) return;
263
264            const firstFocusable = focusables[0];
265            const lastFocusable = focusables[focusables.length - 1];
266
267            // Event handler for Tab navigation - keeping focus where it belongs
268            this.trapHandler = (e) => {
269                if (e.key !== 'Tab') return;
270
271                if (e.shiftKey) {
272                    if (document.activeElement === firstFocusable) {
273                        e.preventDefault();
274                        lastFocusable.focus();
275                    }
276                } else {
277                    if (document.activeElement === lastFocusable) {
278                        e.preventDefault();
279                        firstFocusable.focus();
280                    }
281                }
282            };
283
284            container.addEventListener('keydown', this.trapHandler);
285
286            // Focus the first element - welcome to the party
287            firstFocusable.focus();
288        }
289
290        /**
291         * Release focus trap - freedom at last
292         */
293        releaseFocus() {
294            if (this.trapElement && this.trapHandler) {
295                this.trapElement.removeEventListener('keydown', this.trapHandler);
296            }
297
298            this.trapElement = null;
299            this.trapHandler = null;
300
301            // Restore previous focus - back to where you came from
302            if (this.previousFocus && this.previousFocus.focus) {
303                this.previousFocus.focus();
304            }
305
306            this.previousFocus = null;
307        }
308    }
309
310    // ==========================================================================
311    // StyleSwitcher - Typography controls for the discerning reader
312    // ==========================================================================
313    class StyleSwitcher {
314        constructor(announcer) {
315            this.announcer = announcer;
316
317            // Configuration - because one size doesn't fit all
318            this.config = {
319                fontSize: {
320                    steps: ['normal', 'large', 'xlarge', 'xxlarge'],
321                    increments: [100, 125, 150, 175],
322                    labels: {
323                        'normal': 'Normal (100%)',
324                        'large': 'Large (125%) - for those who appreciate bigger things',
325                        'xlarge': 'Extra large (150%) - because size matters',
326                        'xxlarge': 'Maximum (175%) - go big or go home'
327                    }
328                },
329                lineHeight: {
330                    steps: ['normal', 'relaxed', 'loose'],
331                    values: [1.5, 1.75, 2.0],
332                    labels: {
333                        'normal': 'Normal',
334                        'relaxed': 'Relaxed - room to breathe',
335                        'loose': 'Loose - wide open spaces'
336                    }
337                },
338                letterSpacing: {
339                    steps: ['normal', 'wide', 'wider'],
340                    values: [0, 0.05, 0.1],
341                    labels: {
342                        'normal': 'Normal',
343                        'wide': 'Wide - giving letters personal space',
344                        'wider': 'Extra wide - social distancing for fonts'
345                    }
346                },
347                wordSpacing: {
348                    steps: ['normal', 'wide'],
349                    values: [0, 0.1],
350                    labels: {
351                        'normal': 'Normal',
352                        'wide': 'Wide - words need breathing room too'
353                    }
354                }
355            };
356
357            // Current settings - user preferences we actually remember
358            this.currentSettings = {
359                fontSize: this.loadSetting(STORAGE_KEYS.fontSize) || 'normal',
360                lineHeight: this.loadSetting(STORAGE_KEYS.lineHeight) || 'normal',
361                letterSpacing: this.loadSetting(STORAGE_KEYS.letterSpacing) || 'normal',
362                wordSpacing: this.loadSetting(STORAGE_KEYS.wordSpacing) || 'normal'
363            };
364
365            this.init();
366        }
367
368        init() {
369            this.applyAllSettings();
370            this.bindEvents();
371        }
372
373        bindEvents() {
374            // Font Size Buttons
375            document.querySelectorAll('[data-mincore-fontsize]').forEach(button => {
376                button.addEventListener('click', (e) => {
377                    e.preventDefault();
378                    this.setSetting('fontSize', button.dataset.mincoreFontsize);
379                });
380            });
381
382            // Line Height Buttons
383            document.querySelectorAll('[data-mincore-lineheight]').forEach(button => {
384                button.addEventListener('click', (e) => {
385                    e.preventDefault();
386                    this.setSetting('lineHeight', button.dataset.mincoreLineheight);
387                });
388            });
389
390            // Letter Spacing Buttons
391            document.querySelectorAll('[data-mincore-letterspacing]').forEach(button => {
392                button.addEventListener('click', (e) => {
393                    e.preventDefault();
394                    this.setSetting('letterSpacing', button.dataset.minoreLetterspacing);
395                });
396            });
397
398            // Word Spacing Buttons
399            document.querySelectorAll('[data-mincore-wordspacing]').forEach(button => {
400                button.addEventListener('click', (e) => {
401                    e.preventDefault();
402                    this.setSetting('wordSpacing', button.dataset.mincoreWordspacing);
403                });
404            });
405
406            // Reset Button
407            document.querySelectorAll('[data-mincore-style-reset]').forEach(button => {
408                button.addEventListener('click', (e) => {
409                    e.preventDefault();
410                    this.resetAll();
411                });
412            });
413        }
414
415        setSetting(type, value) {
416            const config = this.config[type];
417            if (!config || !config.steps.includes(value)) return;
418
419            this.currentSettings[type] = value;
420            this.saveSetting(STORAGE_KEYS[type], value);
421            this.applySetting(type, value);
422            this.updateButtons(type);
423
424            const label = config.labels[value] || value;
425            this.announcer.announce(`${this.getTypeLabel(type)}: ${label}`);
426        }
427
428        getTypeLabel(type) {
429            const labels = {
430                fontSize: 'Font size',
431                lineHeight: 'Line height',
432                letterSpacing: 'Letter spacing',
433                wordSpacing: 'Word spacing'
434            };
435            return labels[type] || type;
436        }
437
438        applySetting(type, value) {
439            const html = document.documentElement;
440            const config = this.config[type];
441
442            // Remove old classes - out with the old
443            config.steps.forEach(step => {
444                html.classList.remove(`mincore-${type.toLowerCase()}-${step}`);
445            });
446
447            // Add new class - in with the new
448            html.classList.add(`mincore-${type.toLowerCase()}-${value}`);
449
450            // Set CSS custom property - the magic behind the scenes
451            const index = config.steps.indexOf(value);
452            if (config.increments) {
453                html.style.setProperty(`--mincore-${type.toLowerCase()}`, config.increments[index] + '%');
454            } else if (config.values) {
455                const unit = type === 'lineHeight' ? '' : 'em';
456                html.style.setProperty(`--mincore-${type.toLowerCase()}`, config.values[index] + unit);
457            }
458        }
459
460        applyAllSettings() {
461            Object.keys(this.currentSettings).forEach(type => {
462                this.applySetting(type, this.currentSettings[type]);
463                this.updateButtons(type);
464            });
465        }
466
467        updateButtons(type) {
468            const attributeName = `data-mincore-${type.toLowerCase()}`;
469            document.querySelectorAll(`[${attributeName}]`).forEach(button => {
470                const isActive = button.getAttribute(attributeName) === this.currentSettings[type];
471                button.setAttribute('aria-pressed', isActive.toString());
472                button.classList.toggle('is-active', isActive);
473            });
474        }
475
476        resetAll() {
477            Object.keys(this.currentSettings).forEach(type => {
478                this.currentSettings[type] = 'normal';
479                this.saveSetting(STORAGE_KEYS[type], 'normal');
480                this.applySetting(type, 'normal');
481                this.updateButtons(type);
482            });
483
484            this.announcer.announce('All typography settings reset');
485        }
486
487        loadSetting(key) {
488            try {
489                return localStorage.getItem(key);
490            } catch (e) {
491                return null;
492            }
493        }
494
495        saveSetting(key, value) {
496            try {
497                localStorage.setItem(key, value);
498            } catch (e) {
499                // localStorage not available
500            }
501        }
502    }
503
504    // ==========================================================================
505    // ContrastSwitcher - 5 color schemes for every eye under the sun
506    // ==========================================================================
507    class ContrastSwitcher {
508        constructor(announcer) {
509            this.announcer = announcer;
510
511            // Configuration (5 Themes) - pick your visual poison
512            this.themes = {
513                'default': {
514                    label: 'System default',
515                    description: 'Follows your system preference'
516                },
517                'high-contrast-light': {
518                    label: 'High contrast (light)',
519                    description: 'Black text on white - classic for a reason'
520                },
521                'high-contrast-dark': {
522                    label: 'High contrast (dark)',
523                    description: 'White text on black - easy on the eyes at night'
524                },
525                'inverted': {
526                    label: 'Inverted',
527                    description: 'Colors flipped - because sometimes different is better'
528                },
529                'sepia': {
530                    label: 'Sepia (eye-friendly)',
531                    description: 'Warm tones for marathon reading sessions'
532                }
533            };
534
535            this.themeOrder = ['default', 'high-contrast-light', 'high-contrast-dark', 'inverted', 'sepia'];
536            this.currentTheme = this.loadSetting(STORAGE_KEYS.contrast) || 'default';
537            this.autoDetect = true;
538
539            this.init();
540        }
541
542        init() {
543            // System-Praeferenz erkennen
544            this.detectSystemPreference();
545
546            // Theme anwenden
547            this.applyTheme(this.currentTheme);
548
549            // Events binden
550            this.bindEvents();
551
552            // Listen for system preference changes - stay in sync
553            this.watchSystemPreference();
554        }
555
556        detectSystemPreference() {
557            const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
558            const prefersContrast = window.matchMedia('(prefers-contrast: more)').matches;
559
560            if (this.currentTheme === 'default' && this.autoDetect) {
561                // Set data attribute for system preference
562                document.documentElement.setAttribute(
563                    'data-system-theme',
564                    prefersDark ? 'dark' : 'light'
565                );
566                document.documentElement.setAttribute(
567                    'data-system-contrast',
568                    prefersContrast ? 'high' : 'normal'
569                );
570            }
571        }
572
573        watchSystemPreference() {
574            window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
575                if (this.currentTheme === 'default') {
576                    document.documentElement.setAttribute('data-system-theme', e.matches ? 'dark' : 'light');
577                }
578            });
579
580            window.matchMedia('(prefers-contrast: more)').addEventListener('change', (e) => {
581                if (this.currentTheme === 'default') {
582                    document.documentElement.setAttribute('data-system-contrast', e.matches ? 'high' : 'normal');
583                }
584            });
585        }
586
587        bindEvents() {
588            // Direct theme buttons - one click, new look
589            document.querySelectorAll('[data-mincore-contrast]').forEach(button => {
590                button.addEventListener('click', (e) => {
591                    e.preventDefault();
592                    const theme = button.dataset.mincoreContrast;
593                    this.setTheme(theme);
594                });
595            });
596
597            // Cycle button - round and round we go
598            document.querySelectorAll('[data-mincore-contrast-cycle]').forEach(button => {
599                button.addEventListener('click', (e) => {
600                    e.preventDefault();
601                    this.cycleTheme();
602                });
603            });
604
605            // High-Contrast Toggle
606            document.querySelectorAll('[data-mincore-contrast-toggle]').forEach(button => {
607                button.addEventListener('click', (e) => {
608                    e.preventDefault();
609                    this.toggleHighContrast();
610                });
611            });
612        }
613
614        setTheme(theme) {
615            if (!this.themes[theme]) return;
616
617            this.currentTheme = theme;
618            this.saveSetting(STORAGE_KEYS.contrast, theme);
619            this.applyTheme(theme);
620            this.updateButtons();
621
622            this.announcer.announce(`Color scheme: ${this.themes[theme].label}`);
623        }
624
625        cycleTheme() {
626            const currentIndex = this.themeOrder.indexOf(this.currentTheme);
627            const nextIndex = (currentIndex + 1) % this.themeOrder.length;
628            this.setTheme(this.themeOrder[nextIndex]);
629        }
630
631        toggleHighContrast() {
632            if (this.currentTheme === 'default') {
633                // Switch to high contrast (based on system theme preference)
634                const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
635                this.setTheme(prefersDark ? 'high-contrast-dark' : 'high-contrast-light');
636            } else {
637                this.setTheme('default');
638            }
639        }
640
641        applyTheme(theme) {
642            const html = document.documentElement;
643
644            // Remove old theme classes - clean slate
645            this.themeOrder.forEach(t => {
646                html.classList.remove(`mincore-contrast-${t}`);
647            });
648
649            // Add new theme class - fresh coat of paint
650            html.classList.add(`mincore-contrast-${theme}`);
651
652            // Set data attribute
653            html.setAttribute('data-contrast', theme);
654
655            // Update system preference if default
656            if (theme === 'default') {
657                this.detectSystemPreference();
658            }
659        }
660
661        updateButtons() {
662            document.querySelectorAll('[data-mincore-contrast]').forEach(button => {
663                const isActive = button.dataset.mincoreContrast === this.currentTheme;
664                button.setAttribute('aria-pressed', isActive.toString());
665                button.classList.toggle('is-active', isActive);
666            });
667
668            // Update toggle button states
669            document.querySelectorAll('[data-mincore-contrast-toggle]').forEach(button => {
670                const isHighContrast = this.currentTheme.includes('high-contrast');
671                button.setAttribute('aria-pressed', isHighContrast.toString());
672                button.classList.toggle('is-active', isHighContrast);
673            });
674        }
675
676        loadSetting(key) {
677            try {
678                return localStorage.getItem(key);
679            } catch (e) {
680                return null;
681            }
682        }
683
684        saveSetting(key, value) {
685            try {
686                localStorage.setItem(key, value);
687            } catch (e) {
688                // localStorage not available
689            }
690        }
691    }
692
693    // ==========================================================================
694    // EasytextSwitcher - Because not everyone speaks legalese
695    // ==========================================================================
696    class EasytextSwitcher {
697        constructor(announcer) {
698            this.announcer = announcer;
699
700            this.levels = {
701                'normal': {
702                    label: 'Standard',
703                    description: 'Originaler Text',
704                    field: 'bodytext'
705                },
706                'flesch': {
707                    label: 'Kurz & klar',
708                    description: 'Flesch-Index >= 70, max. 15 Woerter pro Satz',
709                    field: 'minTexter'
710                },
711                'easy': {
712                    label: 'Einfache Sprache',
713                    description: 'Nach Regelwerk fuer Leichte Sprache',
714                    field: 'minEasy'
715                }
716            };
717
718            this.levelOrder = ['normal', 'flesch', 'easy'];
719            this.currentLevel = this.loadSetting(STORAGE_KEYS.easytext) || 'normal';
720
721            this.init();
722        }
723
724        init() {
725            this.applyLevel(this.currentLevel);
726            this.bindEvents();
727        }
728
729        bindEvents() {
730            // Direct level buttons - choose your reading level
731            document.querySelectorAll('[data-mincore-easytext]').forEach(button => {
732                button.addEventListener('click', (e) => {
733                    e.preventDefault();
734                    const level = button.dataset.mincoreEasytext;
735                    this.setLevel(level);
736                });
737
738                // Tastaturnavigation
739                button.addEventListener('keydown', (e) => {
740                    this.handleKeyboard(e, button);
741                });
742            });
743        }
744
745        handleKeyboard(e, button) {
746            const buttons = Array.from(document.querySelectorAll('[data-mincore-easytext]'));
747            const currentIndex = buttons.indexOf(button);
748
749            switch (e.key) {
750                case 'ArrowRight':
751                case 'ArrowDown':
752                    e.preventDefault();
753                    const nextIndex = (currentIndex + 1) % buttons.length;
754                    buttons[nextIndex].focus();
755                    buttons[nextIndex].click();
756                    break;
757
758                case 'ArrowLeft':
759                case 'ArrowUp':
760                    e.preventDefault();
761                    const prevIndex = (currentIndex - 1 + buttons.length) % buttons.length;
762                    buttons[prevIndex].focus();
763                    buttons[prevIndex].click();
764                    break;
765
766                case 'Home':
767                    e.preventDefault();
768                    buttons[0].focus();
769                    buttons[0].click();
770                    break;
771
772                case 'End':
773                    e.preventDefault();
774                    buttons[buttons.length - 1].focus();
775                    buttons[buttons.length - 1].click();
776                    break;
777            }
778        }
779
780        setLevel(level) {
781            if (!this.levels[level]) return;
782
783            this.currentLevel = level;
784            this.saveSetting(STORAGE_KEYS.easytext, level);
785            this.applyLevel(level);
786            this.updateContent();
787            this.updateButtons();
788
789            this.announcer.announce(`Language level: ${this.levels[level].label}`);
790        }
791
792        applyLevel(level) {
793            const html = document.documentElement;
794
795            // Remove old level classes
796            this.levelOrder.forEach(l => {
797                html.classList.remove(`mincore-easytext-${l}`);
798            });
799
800            // Add new level class
801            html.classList.add(`mincore-easytext-${level}`);
802
803            // Set data attribute
804            html.setAttribute('data-easytext', level);
805        }
806
807        updateContent() {
808            // All text containers with variants - show the right version
809            document.querySelectorAll('[data-mincore-textcontainer]').forEach(container => {
810                this.levelOrder.forEach(level => {
811                    const textElement = container.querySelector(`[data-mincore-text="${level}"]`);
812                    if (textElement) {
813                        textElement.hidden = level !== this.currentLevel;
814                        textElement.setAttribute('aria-hidden', (level !== this.currentLevel).toString());
815                    }
816                });
817            });
818
819            // Individual text elements (without container)
820            this.levelOrder.forEach(level => {
821                document.querySelectorAll(`[data-mincore-text="${level}"]:not([data-mincore-textcontainer] *)`).forEach(el => {
822                    el.hidden = level !== this.currentLevel;
823                    el.setAttribute('aria-hidden', (level !== this.currentLevel).toString());
824                });
825            });
826        }
827
828        updateButtons() {
829            document.querySelectorAll('[data-mincore-easytext]').forEach(button => {
830                const isActive = button.dataset.mincoreEasytext === this.currentLevel;
831                button.setAttribute('aria-pressed', isActive.toString());
832                button.classList.toggle('is-active', isActive);
833            });
834        }
835
836        loadSetting(key) {
837            try {
838                return localStorage.getItem(key);
839            } catch (e) {
840                return null;
841            }
842        }
843
844        saveSetting(key, value) {
845            try {
846                localStorage.setItem(key, value);
847            } catch (e) {
848                // localStorage not available
849            }
850        }
851    }
852
853    // ==========================================================================
854    // Initialization
855    // ==========================================================================
856    function init() {
857        if (document.readyState === 'loading') {
858            document.addEventListener('DOMContentLoaded', initModules);
859        } else {
860            initModules();
861        }
862    }
863
864    function initModules() {
865        // Central announcer for all modules - one voice to rule them all
866        const announcer = new AriaAnnouncer();
867
868        // Initialize modules and make them globally available
869        window.mincoreAccessibility = {
870            announcer: announcer,
871            reducedMotion: new ReducedMotion(announcer),
872            focusManager: new FocusManager(announcer),
873            styleSwitcher: new StyleSwitcher(announcer),
874            contrastSwitcher: new ContrastSwitcher(announcer),
875            easytextSwitcher: new EasytextSwitcher(announcer)
876        };
877
878        // Backward Compatibility
879        window.mincoreStyleSwitcher = window.mincoreAccessibility.styleSwitcher;
880        window.mincoreContrastSwitcher = window.mincoreAccessibility.contrastSwitcher;
881        window.mincoreEasytextSwitcher = window.mincoreAccessibility.easytextSwitcher;
882
883        // Mark JS as loaded (for NoScript fallback styling)
884        document.documentElement.classList.add('mincore-js-loaded');
885        document.documentElement.classList.remove('mincore-no-js');
886
887        // Custom event for integration - let others know we're ready
888        document.dispatchEvent(new CustomEvent('mincore:accessibility:ready', {
889            detail: window.mincoreAccessibility
890        }));
891    }
892
893    // Auto-init
894    init();
895
896})();

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.