1const MODULE_CONFIG = { 2 version: "2.0.0", 3 storageKey: "mod_accessibility_sa11y_settings" 4}; 5const FUNCTION_TYPES = { 6 BOOLEAN: "boolean", 7 LEVEL: "level" 8}; 9const ACCESSIBILITY_FUNCTIONS = { 10 // Font Adjustments 11 "font-size": { type: FUNCTION_TYPES.LEVEL, maxLevel: 3 }, 12 "line-height": { type: FUNCTION_TYPES.LEVEL, maxLevel: 3 }, 13 "letter-spacing": { type: FUNCTION_TYPES.LEVEL, maxLevel: 3 }, 14 // Visual Effects 15 "contrast": { type: FUNCTION_TYPES.LEVEL, maxLevel: 3 }, 16 "brightness": { type: FUNCTION_TYPES.LEVEL, maxLevel: 3 }, 17 "saturation": { type: FUNCTION_TYPES.LEVEL, maxLevel: 3 }, 18 // Text Enhancements 19 "readable-font": { type: FUNCTION_TYPES.BOOLEAN }, 20 "dyslexic-font": { type: FUNCTION_TYPES.BOOLEAN }, 21 "text-magnifier": { type: FUNCTION_TYPES.BOOLEAN }, 22 "text-to-speech": { type: FUNCTION_TYPES.BOOLEAN }, 23 // Color Adjustments 24 "invert-colors": { type: FUNCTION_TYPES.BOOLEAN }, 25 "grayscale": { type: FUNCTION_TYPES.LEVEL, maxLevel: 3 }, 26 // Interactions 27 "cursor": { type: FUNCTION_TYPES.BOOLEAN }, 28 "underline-links": { type: FUNCTION_TYPES.BOOLEAN }, 29 "reading-guide": { type: FUNCTION_TYPES.BOOLEAN }, 30 "pause-animations": { type: FUNCTION_TYPES.BOOLEAN } 31}; 32const PROFILE_DEFINITIONS = { 33 "vision-impaired": { 34 name: "Sehbehinderungsmodus", 35 functions: { 36 "font-size": 1, 37 "line-height": 2, 38 "saturation": 1, 39 "readable-font": true, 40 "text-magnifier": true 41 } 42 }, 43 "adhd-friendly": { 44 name: "ADHD Modus", 45 functions: { 46 "line-height": 1, 47 "saturation": 1, 48 "text-magnifier": true, 49 "pause-animations": true 50 } 51 }, 52 "seizure-free": { 53 name: "Anfallsfreier Modus", 54 functions: { 55 "line-height": 1, 56 "grayscale": 2, 57 "pause-animations": true 58 } 59 } 60}; 61const DEFAULT_SETTINGS = { 62 version: MODULE_CONFIG.version, 63 functions: Object.keys(ACCESSIBILITY_FUNCTIONS).reduce((acc, key) => { 64 const func = ACCESSIBILITY_FUNCTIONS[key]; 65 acc[key] = func.type === FUNCTION_TYPES.LEVEL ? 0 : false; 66 return acc; 67 }, {}), 68 profiles: Object.keys(PROFILE_DEFINITIONS).reduce((acc, key) => { 69 acc[key] = false; 70 return acc; 71 }, {}) 72}; 73const EVENTS = { 74 FUNCTION_CHANGED: "accessibility:function:changed", 75 PROFILE_CHANGED: "accessibility:profile:changed", 76 SETTINGS_RESET: "accessibility:settings:reset" 77}; 78class StorageManager { 79 constructor() { 80 this.storageKey = MODULE_CONFIG.storageKey; 81 this.storageVersion = MODULE_CONFIG.storageVersion; 82 } 83 /** 84 * Load settings from localStorage with defaults 85 * @returns {Object} Settings object 86 */ 87 loadSettings() { 88 try { 89 const stored = localStorage.getItem(this.storageKey); 90 if (!stored) { 91 return DEFAULT_SETTINGS; 92 } 93 const settings = JSON.parse(stored); 94 if (settings.version !== this.storageVersion) { 95 return this.mergeWithDefaults(settings); 96 } 97 return settings; 98 } catch (error) { 99 console.error("Error loading settings:", error); 100 return DEFAULT_SETTINGS; 101 } 102 } 103 /** 104 * Save settings to localStorage 105 * @param {Object} settings - Settings to save 106 * @returns {boolean} Success status 107 */ 108 saveSettings(settings) { 109 try { 110 const settingsToSave = { 111 ...settings, 112 version: this.storageVersion 113 }; 114 localStorage.setItem(this.storageKey, JSON.stringify(settingsToSave)); 115 return true; 116 } catch (error) { 117 console.error("Error saving settings:", error); 118 return false; 119 } 120 } 121 /** 122 * Merge stored settings with defaults 123 * @param {Object} stored - Stored settings 124 * @returns {Object} Merged settings 125 */ 126 mergeWithDefaults(stored) { 127 const merged = { ...DEFAULT_SETTINGS }; 128 if (stored.functions) { 129 Object.entries(stored.functions).forEach(([key, value]) => { 130 if (merged.functions.hasOwnProperty(key)) { 131 merged.functions[key] = value; 132 } 133 }); 134 } 135 if (stored.profiles) { 136 Object.entries(stored.profiles).forEach(([key, value]) => { 137 if (merged.profiles.hasOwnProperty(key)) { 138 merged.profiles[key] = value; 139 } 140 }); 141 } 142 if (stored.ui) { 143 merged.ui = { ...merged.ui, ...stored.ui }; 144 } 145 return merged; 146 } 147 /** 148 * Reset all settings to defaults 149 * @returns {Object} Default settings 150 */ 151 resetSettings() { 152 try { 153 const defaults = { ...DEFAULT_SETTINGS }; 154 this.saveSettings(defaults); 155 return defaults; 156 } catch (error) { 157 console.error("Error resetting settings:", error); 158 return DEFAULT_SETTINGS; 159 } 160 } 161 /** 162 * Update specific setting 163 * @param {string} path - Dot notation path (e.g., 'functions.fontSize') 164 * @param {*} value - Value to set 165 * @returns {boolean} Success status 166 */ 167 updateSetting(path, value) { 168 const settings = this.loadSettings();
169 const keys = path.split("."); 170 let current = settings; 171 for (let i = 0; i < keys.length - 1; i++) { 172 if (!current[keys[i]]) { 173 current[keys[i]] = {}; 174 } 175 current = current[keys[i]]; 176 } 177 current[keys[keys.length - 1]] = value; 178 return this.saveSettings(settings); 179 } 180} 181const storage = new StorageManager(); 182class StateManager extends EventTarget { 183 constructor() { 184 super(); 185 this.state = { 186 functions: {}, 187 profiles: {}, 188 version: "2.0.0" 189 }; 190 } 191 /** 192 * Get current state (safe copy) 193 * @returns {Object} Current state 194 */ 195 getState() { 196 return { 197 functions: { ...this.state.functions }, 198 profiles: { ...this.state.profiles }, 199 version: this.state.version 200 }; 201 } 202 /** 203 * Get function state 204 * @param {string} functionName - Function name 205 * @returns {*} Function value 206 */ 207 getFunctionState(functionName) { 208 return this.state.functions[functionName]; 209 } 210 /** 211 * Set function state 212 * @param {string} functionName - Function name 213 * @param {*} value - New value 214 * @param {boolean} skipSave - Skip saving to storage 215 */ 216 setFunctionState(functionName, value, skipSave = false) { 217 const oldValue = this.getFunctionState(functionName); 218 if (oldValue === value) { 219 return; 220 } 221 this.state.functions = this.state.functions || {}; 222 this.state.functions[functionName] = value; 223 if (!skipSave) { 224 storage.saveSettings(this.state); 225 } 226 this.dispatchEvent(new CustomEvent(EVENTS.FUNCTION_CHANGED, { 227 detail: { 228 functionName, 229 value, 230 oldValue 231 } 232 })); 233 } 234 /** 235 * Get profile state 236 * @param {string} profileKey - Profile key 237 * @returns {boolean} Profile active state 238 */ 239 getProfileState(profileKey) { 240 return this.state.profiles[profileKey] || false; 241 } 242 /** 243 * Set profile state 244 * @param {string} profileKey - Profile key 245 * @param {boolean} active - Active state 246 */ 247 setProfileState(profileKey, active) { 248 const oldState = this.getProfileState(profileKey); 249 if (oldState === active) { 250 return; 251 } 252 this.state.profiles = this.state.profiles || {}; 253 this.state.profiles[profileKey] = active; 254 storage.saveSettings(this.state); 255 this.dispatchEvent(new CustomEvent(EVENTS.PROFILE_CHANGED, { 256 detail: { 257 profileKey, 258 active, 259 oldState 260 } 261 })); 262 } 263 /** 264 * Activate profile with mutual exclusivity 265 * @param {string} profileKey - Profile to activate 266 */ 267 activateProfile(profileKey) { 268 if (!PROFILE_DEFINITIONS[profileKey]) { 269 return; 270 } 271 const profile = PROFILE_DEFINITIONS[profileKey]; 272 Object.keys(PROFILE_DEFINITIONS).forEach((key) => { 273 this.state.profiles[key] = key === profileKey; 274 }); 275 const functionUpdates = []; 276 Object.keys(ACCESSIBILITY_FUNCTIONS).forEach((funcName) => { 277 const func = ACCESSIBILITY_FUNCTIONS[funcName]; 278 const defaultValue = func.type === FUNCTION_TYPES.LEVEL ? 0 : false; 279 functionUpdates.push({ name: funcName, value: defaultValue }); 280 }); 281 Object.entries(profile.functions).forEach(([funcName, value]) => { 282 const existingUpdate = functionUpdates.find((u) => u.name === funcName); 283 if (existingUpdate) { 284 existingUpdate.value = value; 285 } else { 286 functionUpdates.push({ name: funcName, value }); 287 } 288 }); 289 const changedFunctions = []; 290 for (const update of functionUpdates) { 291 const oldValue = this.getFunctionState(update.name); 292 if (oldValue !== update.value) { 293 this.state.functions[update.name] = update.value; 294 changedFunctions.push({ name: update.name, value: update.value, oldValue }); 295 } 296 } 297 storage.saveSettings(this.state); 298 this.dispatchEvent(new CustomEvent(EVENTS.PROFILE_CHANGED, { 299 detail: { 300 profileKey, 301 active: true, 302 functions: this.state.functions 303 } 304 })); 305 for (const change of changedFunctions) { 306 this.dispatchEvent(new CustomEvent(EVENTS.FUNCTION_CHANGED, { 307 detail: { 308 functionName: change.name, 309 value: change.value, 310 oldValue: change.oldValue, 311 fromProfile: profileKey 312 } 313 })); 314 } 315 } 316 /** 317 * Deactivate profile 318 * @param {string} profileKey - Profile to deactivate 319 */ 320 deactivateProfile(profileKey) { 321 if (!this.getProfileState(profileKey)) { 322 return; 323 } 324 this.state.profiles[profileKey] = false;
325 Object.keys(ACCESSIBILITY_FUNCTIONS).forEach((funcName) => { 326 const func = ACCESSIBILITY_FUNCTIONS[funcName]; 327 const defaultValue = func.type === FUNCTION_TYPES.LEVEL ? 0 : false; 328 const oldValue = this.state.functions[funcName]; 329 this.state.functions[funcName] = defaultValue; 330 if (oldValue !== defaultValue) { 331 this.dispatchEvent(new CustomEvent(EVENTS.FUNCTION_CHANGED, { 332 detail: { 333 functionName: funcName, 334 value: defaultValue, 335 oldValue, 336 fromProfile: "deactivation" 337 } 338 })); 339 } 340 }); 341 storage.saveSettings(this.state); 342 this.dispatchEvent(new CustomEvent(EVENTS.PROFILE_CHANGED, { 343 detail: { 344 profileKey, 345 active: false 346 } 347 })); 348 } 349 /** 350 * Reset all settings 351 */ 352 resetAll() { 353 this.state = storage.resetSettings(); 354 const wrapper = document.getElementById("site-content-wrapper"); 355 if (wrapper) { 356 const classes = wrapper.className.split(" "); 357 const cleanedClasses = classes.filter((c) => !c.includes("accessibility")); 358 wrapper.className = cleanedClasses.join(" "); 359 } 360 if (wrapper && wrapper.style) { 361 wrapper.style.removeProperty("--accessibility-font-scale"); 362 wrapper.style.removeProperty("--accessibility-line-height"); 363 wrapper.style.removeProperty("--accessibility-line-height-factor"); 364 wrapper.style.removeProperty("--accessibility-letter-spacing"); 365 wrapper.style.removeProperty("--accessibility-contrast"); 366 wrapper.style.removeProperty("--accessibility-brightness"); 367 wrapper.style.removeProperty("--accessibility-saturation"); 368 } 369 const styleElements = document.querySelectorAll('[id*="accessibility"][id*="styles"]'); 370 styleElements.forEach((el) => el.remove()); 371 this.dispatchEvent(new CustomEvent(EVENTS.SETTINGS_RESET, { 372 detail: { reset: true } 373 })); 374 } 375 /** 376 * Get active profile 377 * @returns {string|null} Active profile key or null 378 */ 379 getActiveProfile() { 380 for (const [key, active] of Object.entries(this.state.profiles || {})) { 381 if (active) return key; 382 } 383 return null; 384 } 385} 386const stateManager = new StateManager(); 387/** 388 * Simplified Module System 389 * Centralized implementation of all accessibility functions 390 * 391 * @module modules-simple 392 * @package Villaester.Module 393 * @subpackage AccessibilitySa11y 394 * @version 3.1.0 395 * @author Kiki Schuelling 396 * @copyright 2025 Villaester Moderne Medien 397 * @license GNU GPL v2 or later 398 * 399 * IMPORTANT: Saturation fix - v3.0.1 400 * Saturation is correctly applied ONLY to #site-content-wrapper 401 */ 402class BaseModule { 403 /** 404 * Creates a new BaseModule instance 405 * 406 * @param {string} name - Module identifier (e.g., 'font', 'visual', 'color') 407 */ 408 constructor(name) { 409 this.name = name; 410 this.styleId = `accessibility-${name}-styles`; 411 } 412 /** 413 * Injects CSS styles into the document head 414 * Creates or updates a style element with the module's styles 415 * 416 * @param {string} css - CSS rules to inject 417 * @returns {void} 418 */ 419 injectStyles(css) { 420 let styleEl = document.getElementById(this.styleId); 421 if (!styleEl) { 422 styleEl = document.createElement("style"); 423 styleEl.id = this.styleId; 424 document.head.appendChild(styleEl); 425 } 426 styleEl.textContent = css; 427 } 428 /** 429 * Removes the module's injected styles from the document 430 * 431 * @returns {void} 432 */ 433 removeStyles() { 434 const styleEl = document.getElementById(this.styleId); 435 if (styleEl) styleEl.remove(); 436 } 437 /** 438 * Adds a CSS class to the content wrapper element 439 * Uses cached DOM reference for performance 440 * 441 * @param {string} className - CSS class name to add 442 * @returns {void} 443 */ 444 addClass(className) { 445 const wrapper = document.getElementById("site-content-wrapper"); 446 if (wrapper) { 447 wrapper.classList.add(className); 448 } else { 449 console.warn("site-content-wrapper not found, skipping addClass for:", className); 450 } 451 } 452 /** 453 * Removes a CSS class from the content wrapper element 454 * Uses cached DOM reference for performance 455 * 456 * @param {string} className - CSS class name to remove 457 * @returns {void} 458 */ 459 removeClass(className) { 460 const wrapper = document.getElementById("site-content-wrapper"); 461 if (wrapper) { 462 wrapper.classList.remove(className); 463 } else { 464 console.warn("site-content-wrapper not found, skipping removeClass for:", className); 465 } 466 } 467 /** 468 * Sets a CSS custom property (variable) on the site-content-wrapper 469 * 470 * @param {string} name - CSS variable name (including '--' prefix) 471 * @param {string|number} value - Value to set 472 * @returns {void} 473 */ 474 setCSSVar(name, value) { 475 const wrapper = document.getElementById("site-content-wrapper"); 476 if (wrapper) { 477 wrapper.style.setProperty(name, value); 478 } 479 } 480 /** 481 * Removes a CSS custom property from the site-content-wrapper 482 * 483 * @param {string} name - CSS variable name to remove 484 * @returns {void} 485 */ 486 removeCSSVar(name) { 487 const wrapper = document.getElementById("site-content-wrapper"); 488 if (wrapper) { 489 wrapper.style.removeProperty(name); 490 } 491 } 492} 493class FontModule extends BaseModule { 494 /** 495 * Creates a new FontModule instance 496 * Initializes font scale values for different levels 497 */ 498 constructor() { 499 super("font"); 500 this.scales = [1, 1.125, 1.25, 1.375]; 501 this.lineHeightIncreases = [0, 30, 50, 70]; 502 this.letterSpacings = [0, 0.02, 0.05, 0.08]; 503 } 504 /** 505 * Sets the font size level 506 * Applies appropriate CSS classes and variables based on level 507 * 508 * @param {number} level - Font size level (0-3, where 0 is default) 509 * @returns {void} 510 */ 511 setFontSize(level) { 512 this.removeClass("accessibility-font-size"); 513 this.removeClass("accessibility-font-size-1"); 514 this.removeClass("accessibility-font-size-2"); 515 this.removeClass("accessibility-font-size-3"); 516 this.removeCSSVar("--accessibility-font-scale"); 517 if (level === 0) { 518 const existingStyles = document.getElementById(this.styleId); 519 if (existingStyles) { 520 let content = existingStyles.textContent; 521 content = content.replace(/\/\* Font-Size Level.*?\*\/[\s\S]*?(?=\/\*|$)/g, ""); 522 existingStyles.textContent = content; 523 } 524 } else { 525 const scale = this.scales[Math.min(level, 3)]; 526 this.setCSSVar("--accessibility-font-scale", scale); 527 this.addClass(`accessibility-font-size-${level}`); 528 let existingStyles = document.getElementById(this.styleId); 529 if (!existingStyles) { 530 existingStyles = document.createElement("style"); 531 existingStyles.id = this.styleId; 532 document.head.appendChild(existingStyles); 533 } 534 let content = existingStyles.textContent; 535 content = content.replace(/\/\* Font-Size Level.*?\*\/[\s\S]*?(?=\/\*|$)/g, ""); 536 content += ` 537 /* Font-Size Level ${level} active - scale: ${scale} */ 538 `; 539 existingStyles.textContent = content; 540 } 541 } 542 /** 543 * Sets the line height level 544 * Increases spacing between lines for better readability 545 * 546 * @param {number} level - Line height level (0-3, where 0 is default) 547 * @returns {void} 548 */ 549 setLineHeight(level) { 550 this.removeClass("accessibility-line-height"); 551 this.removeClass("accessibility-line-height-1"); 552 this.removeClass("accessibility-line-height-2"); 553 this.removeClass("accessibility-line-height-3"); 554 this.removeCSSVar("--accessibility-line-height-factor"); 555 if (level === 0) { 556 const existingStyles = document.getElementById(this.styleId); 557 if (existingStyles) { 558 let content = existingStyles.textContent; 559 content = content.replace(/\/\* Line-Height Level.*?\*\/[\s\S]*?(?=\/\*|$)/g, ""); 560 existingStyles.textContent = content; 561 } 562 } else { 563 const increase = this.lineHeightIncreases[Math.min(level, 3)]; 564 const factor = 1 + increase / 100; 565 this.setCSSVar("--accessibility-line-height-factor", factor); 566 this.addClass(`accessibility-line-height`); 567 this.addClass(`accessibility-line-height-${level}`); 568 let existingStyles = document.getElementById(this.styleId); 569 if (!existingStyles) { 570 existingStyles = document.createElement("style"); 571 existingStyles.id = this.styleId; 572 document.head.appendChild(existingStyles); 573 } 574 let content = existingStyles.textContent; 575 content = content.replace(/\/\* Line-Height Level.*?\*\/[\s\S]*?(?=\/\*|$)/g, ""); 576 content += ` 577 /* Line-Height Level ${level} - ${increase}% increase */ 578 .accessibility-line-height * { 579 line-height: calc(1em * var(--accessibility-line-height-factor, 1.3)) !important; 580 } 581 .accessibility-line-height p, 582 .accessibility-line-height li, 583 .accessibility-line-height div, 584 .accessibility-line-height span, 585 .accessibility-line-height h1, 586 .accessibility-line-height h2, 587 .accessibility-line-height h3, 588 .accessibility-line-height h4, 589 .accessibility-line-height h5, 590 .accessibility-line-height h6 { 591 line-height: calc(1.5em * var(--accessibility-line-height-factor, 1.3)) !important; 592 } 593 `; 594 existingStyles.textContent = content; 595 } 596 } 597 /** 598 * Sets the letter spacing level 599 * Increases spacing between characters for better readability 600 * 601 * @param {number} level - Letter spacing level (0-3, where 0 is default) 602 * @returns {void} 603 */ 604 setLetterSpacing(level) { 605 this.removeClass("accessibility-letter-spacing"); 606 this.removeClass("accessibility-letter-spacing-1"); 607 this.removeClass("accessibility-letter-spacing-2"); 608 this.removeClass("accessibility-letter-spacing-3"); 609 this.removeCSSVar("--accessibility-letter-spacing"); 610 if (level === 0) { 611 const existingStyles = document.getElementById(this.styleId); 612 if (existingStyles) { 613 let content = existingStyles.textContent; 614 content = content.replace(/\/\* Letter-Spacing Level.*?\*\/[\s\S]*?(?=\/\*|$)/g, ""); 615 existingStyles.textContent = content; 616 } 617 } else { 618 const spacing = this.letterSpacings[Math.min(level, 3)]; 619 this.setCSSVar("--accessibility-letter-spacing", spacing + "em"); 620 this.addClass("accessibility-letter-spacing"); 621 this.addClass(`accessibility-letter-spacing-${level}`); 622 let existingStyles = document.getElementById(this.styleId); 623 if (!existingStyles) { 624 existingStyles = document.createElement("style"); 625 existingStyles.id = this.styleId; 626 document.head.appendChild(existingStyles); 627 } 628 let content = existingStyles.textContent; 629 content = content.replace(/\/\* Letter-Spacing Level.*?\*\/[\s\S]*?(?=\/\*|$)/g, ""); 630 content += ` 631 /* Letter-Spacing Level ${level} - ${spacing}em */ 632 .accessibility-letter-spacing * { 633 letter-spacing: var(--accessibility-letter-spacing, normal) !important; 634 } 635 `; 636 existingStyles.textContent = content; 637 } 638 } 639 /** 640 * Resets all font settings to default values 641 * 642 * @returns {void} 643 */ 644 reset() { 645 this.setFontSize(0); 646 this.setLineHeight(0); 647 this.setLetterSpacing(0); 648 } 649} 650class VisualModule extends BaseModule { 651 /** 652 * Creates a new VisualModule instance 653 */ 654 constructor() { 655 super("visual"); 656 } 657 /** 658 * Sets the contrast level 659 * Increases color contrast for better visibility 660 * 661 * @param {number} level - Contrast level (0-3, where 0 is default) 662 * @returns {void} 663 */ 664 setContrast(level) { 665 if (level === 0) { 666 this.removeClass("accessibility-contrast"); 667 this.removeCSSVar("--accessibility-contrast"); 668 } else { 669 if (window.ModAccessibilityFunctions && document.getElementById("site-content-wrapper")?.classList.contains("accessibility-invert-colors")) { 670 window.ModAccessibilityFunctions.setFunctionState("invert-colors", false); 671 } 672 const values = [1, 1.25, 1.5, 2]; 673 const contrast = values[Math.min(level - 1, 3)]; 674 this.setCSSVar("--accessibility-contrast", contrast); 675 this.addClass("accessibility-contrast"); 676 this.injectStyles(` 677 .accessibility-contrast { 678 filter: contrast(var(--accessibility-contrast, 1)) !important; 679 } 680 `); 681 } 682 } 683 setBrightness(level) { 684 if (level === 0) { 685 this.removeClass("accessibility-brightness"); 686 this.removeCSSVar("--accessibility-brightness"); 687 } else { 688 if (window.ModAccessibilityFunctions && document.getElementById("site-content-wrapper")?.classList.contains("accessibility-invert-colors")) { 689 window.ModAccessibilityFunctions.setFunctionState("invert-colors", false); 690 } 691 const values = [1, 0.9, 0.8, 0.7]; 692 const brightness = values[Math.min(level - 1, 3)]; 693 this.setCSSVar("--accessibility-brightness", brightness); 694 this.addClass("accessibility-brightness"); 695 this.injectStyles(` 696 .accessibility-brightness { 697 filter: brightness(var(--accessibility-brightness, 1)) !important; 698 } 699 `); 700 } 701 } 702 setSaturation(level) { 703 if (level === 0) { 704 this.removeClass("accessibility-saturation"); 705 this.removeCSSVar("--accessibility-saturation"); 706 } else { 707 const values = [1, 1.5, 2, 2.5]; 708 const saturation = values[Math.min(level - 1, 3)]; 709 this.setCSSVar("--accessibility-saturation", saturation); 710 this.addClass("accessibility-saturation"); 711 this.injectStyles(` 712 .accessibility-saturation { 713 filter: saturate(var(--accessibility-saturation, 1)) !important; 714 } 715 `); 716 } 717 } 718 reset() { 719 this.setContrast(0); 720 this.setBrightness(0); 721 this.setSaturation(0); 722 } 723} 724class ColorModule extends BaseModule { 725 /** 726 * Creates a new ColorModule instance 727 */ 728 constructor() { 729 super("color"); 730 } 731 /** 732 * Toggles color inversion 733 * Inverts all colors on the page for users with specific visual needs 734 * 735 * @param {boolean} enabled - Whether to enable color inversion 736 * @returns {void} 737 */ 738 setInvertColors(enabled) { 739 if (enabled) { 740 if (window.ModAccessibilityFunctions) { 741 window.ModAccessibilityFunctions.setFunctionState("brightness", 0); 742 window.ModAccessibilityFunctions.setFunctionState("contrast", 0); 743 } 744 this.addClass("accessibility-invert-colors"); 745 this.injectStyles(` 746 .accessibility-invert-colors { 747 filter: invert(1) !important; 748 } 749 `); 750 } else { 751 this.removeClass("accessibility-invert-colors"); 752 } 753 } 754 setGrayscale(level) { 755 if (level === 0) { 756 this.removeClass("accessibility-grayscale"); 757 } else { 758 const values = [0, 0.5, 1]; 759 const gray = values[Math.min(level, 2)]; 760 this.addClass("accessibility-grayscale"); 761 this.injectStyles(` 762 .accessibility-grayscale { 763 filter: grayscale(${gray}) !important; 764 } 765 `); 766 } 767 } 768 reset() { 769 this.setInvertColors(false); 770 this.setGrayscale(0); 771 } 772} 773class TextModule extends BaseModule { 774 /** 775 * Creates a new TextModule instance 776 * Initializes text-to-speech components if available 777 */ 778 constructor() { 779 super("text"); 780 this.speechSynthesis = window.speechSynthesis; 781 this.currentUtterance = null; 782 this.clickListenerId = null; 783 this.hoverListenerId = null; 784 this.hoverTimeoutId = null; 785 } 786 /** 787 * Darken a color by a percentage 788 * @param {string} hex - Hex color code 789 * @param {number} percent - Percentage to darken 790 * @returns {string} Darkened hex color 791 */
792 darkenColor(hex, percent) { 793 hex = hex.replace("#", ""); 794 const r = parseInt(hex.substring(0, 2), 16); 795 const g = parseInt(hex.substring(2, 4), 16); 796 const b = parseInt(hex.substring(4, 6), 16); 797 const factor = 1 - percent / 100; 798 const newR = Math.round(r * factor); 799 const newG = Math.round(g * factor); 800 const newB = Math.round(b * factor); 801 return "#" + [newR, newG, newB].map((x) => x.toString(16).padStart(2, "0")).join(""); 802 } 803 /** 804 * Get contrasting text color (black or white) based on background 805 * @param {string} hex - Background hex color 806 * @returns {string} '#ffffff' or '#000000' 807 */ 808 getContrastColor(hex) { 809 hex = hex.replace("#", ""); 810 const r = parseInt(hex.substring(0, 2), 16); 811 const g = parseInt(hex.substring(2, 4), 16); 812 const b = parseInt(hex.substring(4, 6), 16); 813 const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; 814 return luminance > 0.5 ? "#000000" : "#ffffff"; 815 } 816 /** 817 * Toggles readable font (Roboto) 818 * Applies a clean, highly readable font for better text comprehension 819 * 820 * @param {boolean} enabled - Whether to enable readable font 821 * @param {boolean} fromOtherFont - Internal flag to prevent recursion 822 * @returns {void} 823 */
824 setReadableFont(enabled, fromOtherFont = false) { 825 this.removeClass("accessibility-readable-font"); 826 this.removeClass("accessibility-readable-font-roboto"); 827 if (enabled) { 828 if (!fromOtherFont && document.getElementById("site-content-wrapper")?.classList.contains("accessibility-dyslexic-font")) { 829 this.setDyslexicFont(false, true); 830 } 831 this.addClass("accessibility-readable-font"); 832 this.addClass("accessibility-readable-font-roboto"); 833 let existingStyles = document.getElementById(this.styleId); 834 if (!existingStyles) { 835 existingStyles = document.createElement("style"); 836 existingStyles.id = this.styleId; 837 document.head.appendChild(existingStyles); 838 } 839 let content = existingStyles.textContent; 840 content = content.replace(/\/\* Readable Font Active.*?\*\/[\s\S]*?(?=\/\*|$)/g, ""); 841 content += ` 842 /* Readable Font Active */ 843 .accessibility-readable-font * { 844 font-family: 'Roboto', "Segoe UI", Arial, sans-serif !important; 845 text-rendering: optimizeLegibility !important; 846 -webkit-font-smoothing: antialiased !important; 847 -moz-osx-font-smoothing: grayscale !important; 848 } 849 .accessibility-readable-font p, 850 .accessibility-readable-font li, 851 .accessibility-readable-font div, 852 .accessibility-readable-font span, 853 .accessibility-readable-font a, 854 .accessibility-readable-font td, 855 .accessibility-readable-font th { 856 font-family: 'Roboto', "Segoe UI", Arial, sans-serif !important; 857 font-weight: 400 !important; 858 } 859 .accessibility-readable-font h1, 860 .accessibility-readable-font h2, 861 .accessibility-readable-font h3, 862 .accessibility-readable-font h4, 863 .accessibility-readable-font h5, 864 .accessibility-readable-font h6 { 865 font-family: 'Roboto', "Segoe UI", Arial, sans-serif !important; 866 font-weight: 600 !important; 867 } 868 .accessibility-readable-font strong, 869 .accessibility-readable-font b { 870 font-weight: 700 !important; 871 } 872 `; 873 existingStyles.textContent = content; 874 } else { 875 const existingStyles = document.getElementById(this.styleId); 876 if (existingStyles) { 877 let content = existingStyles.textContent; 878 content = content.replace(/\/\* Readable Font Active.*?\*\/[\s\S]*?(?=\/\*|$)/g, ""); 879 existingStyles.textContent = content; 880 } 881 } 882 } 883 setDyslexicFont(enabled, fromOtherFont = false) { 884 this.removeClass("accessibility-dyslexic-font"); 885 if (enabled) { 886 if (!fromOtherFont && document.getElementById("site-content-wrapper")?.classList.contains("accessibility-readable-font")) { 887 this.setReadableFont(false, true); 888 } 889 this.addClass("accessibility-dyslexic-font"); 890 let existingStyles = document.getElementById(this.styleId); 891 if (!existingStyles) { 892 existingStyles = document.createElement("style"); 893 existingStyles.id = this.styleId; 894 document.head.appendChild(existingStyles); 895 } 896 let content = existingStyles.textContent; 897 content = content.replace(/\/\* Dyslexic Font Active.*?\*\/[\s\S]*?(?=\/\*|$)/g, ""); 898 content += ` 899 /* Dyslexic Font Active */ 900 @font-face { 901 font-family: 'OpenDyslexic'; 902 src: url('/media/mod_accessibility_sa11y/fonts/opendyslexic/OpenDyslexic-Regular.woff2') format('woff2'); 903 font-weight: normal; 904 font-style: normal; 905 font-display: swap; 906 } 907 @font-face { 908 font-family: 'OpenDyslexic'; 909 src: url('/media/mod_accessibility_sa11y/fonts/opendyslexic/OpenDyslexic-Bold.woff2') format('woff2'); 910 font-weight: bold; 911 font-style: normal; 912 font-display: swap; 913 } 914 .accessibility-dyslexic-font * { 915 font-family: 'OpenDyslexic', Arial, sans-serif !important; 916 letter-spacing: 0.05em !important; 917 word-spacing: 0.1em !important; 918 line-height: 1.5em !important; 919 } 920 .accessibility-dyslexic-font p, 921 .accessibility-dyslexic-font li, 922 .accessibility-dyslexic-font div, 923 .accessibility-dyslexic-font span, 924 .accessibility-dyslexic-font a { 925 font-family: 'OpenDyslexic', Arial, sans-serif !important; 926 } 927 `; 928 existingStyles.textContent = content; 929 } else { 930 const existingStyles = document.getElementById(this.styleId); 931 if (existingStyles) { 932 let content = existingStyles.textContent; 933 content = content.replace(/\/\* Dyslexic Font Active.*?\*\/[\s\S]*?(?=\/\*|$)/g, ""); 934 existingStyles.textContent = content; 935 } 936 } 937 } 938 setTextMagnifier(enabled) { 939 if (enabled) { 940 this.addClass("accessibility-text-magnifier"); 941 if (window.ModAccessibilityFunctions?.advancedMagnifier) { 942 this.useAdvancedMagnifier(enabled); 943 return; 944 } 945 const getPrimaryColor = () => { 946 const wrapper = document.getElementById("site-content-wrapper"); 947 let color = wrapper ? getComputedStyle(wrapper).getPropertyValue("--a11y-primary-color").trim() : ""; 948 if (!color) { 949 const a11yBox = document.getElementById("a11y-box"); 950 if (a11yBox) { 951 color = a11yBox.dataset.primaryColor; 952 } 953 } 954 if (!color && window.a11yPrimaryColor) { 955 color = window.a11yPrimaryColor; 956 } 957 return color || "#0056b3"; 958 }; 959 if (this.magnifierContainer) { 960 const primaryColor = getPrimaryColor();
961 const darkerShade = this.darkenColor(primaryColor, 10); 962 const textColor = this.getContrastColor(primaryColor); 963 this.magnifierContainer.style.borderColor = primaryColor; 964 const header = this.magnifierContainer.querySelector("div:first-child"); 965 if (header && header !== this.magnifierContent) { 966 header.style.background = `linear-gradient(90deg, ${primaryColor} 0%, ${darkerShade} 100%)`; 967 header.style.borderBottomColor = darkerShade; 968 const headerTexts = header.querySelectorAll("span"); 969 headerTexts.forEach((span) => { 970 span.style.color = textColor; 971 }); 972 const zoomIndicator = header.querySelector("#magnifier-zoom"); 973 if (zoomIndicator) { 974 zoomIndicator.style.background = textColor === "#ffffff" ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.1)"; 975 } 976 } 977 } 978 if (!this.magnifierContainer) { 979 const primaryColorForBorder = getPrimaryColor(); 980 this.magnifierContainer = document.createElement("div"); 981 this.magnifierContainer.id = "a11y-text-magnifier"; 982 this.magnifierContainer.style.cssText = ` 983 position: fixed; 984 width: 600px; 985 min-height: 180px; 986 max-height: 300px; 987 background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); 988 border: 2px solid ${primaryColorForBorder}; 989 border-radius: 12px; 990 box-shadow: 0 8px 32px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.8) inset; 991 pointer-events: none; 992 z-index: 999998; 993 display: none; 994 overflow: hidden; 995 transform-origin: center; 996 transition: opacity 0.2s ease; 997 `; 998 const primaryColor = getPrimaryColor(); 999 const darkerShade = this.darkenColor(primaryColor, 10); 1000 const textColor = this.getContrastColor(primaryColor); 1001 const header = document.createElement("div"); 1002 header.style.cssText = ` 1003 background: linear-gradient(90deg, ${primaryColor} 0%, ${darkerShade} 100%); 1004 padding: 8px 15px; 1005 display: flex; 1006 justify-content: space-between; 1007 align-items: center; 1008 border-bottom: 1px solid ${darkerShade}; 1009 `; 1010 const title = document.createElement("span"); 1011 title.style.cssText = ` 1012 color: ${textColor}; 1013 font-size: 12px; 1014 font-weight: 600; 1015 letter-spacing: 0.5px; 1016 text-transform: uppercase; 1017 `; 1018 title.textContent = "Textlupe"; 1019 const zoomIndicator = document.createElement("span"); 1020 zoomIndicator.id = "magnifier-zoom"; 1021 zoomIndicator.style.cssText = ` 1022 color: ${textColor}; 1023 font-size: 11px; 1024 font-weight: 500; 1025 background: ${textColor === "#ffffff" ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.1)"}; 1026 padding: 2px 8px; 1027 border-radius: 4px; 1028 `; 1029 zoomIndicator.textContent = "2.0x"; 1030 header.appendChild(title); 1031 header.appendChild(zoomIndicator); 1032 this.magnifierContent = document.createElement("div"); 1033 this.magnifierContent.style.cssText = ` 1034 padding: 20px; 1035 font-size: 2em; 1036 line-height: 1.6; 1037 color: #2c3e50; 1038 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 1039 font-weight: 400; 1040 overflow-y: auto; 1041 max-height: 250px; 1042 word-wrap: break-word; 1043 text-align: left; 1044 background: white; 1045 position: relative; 1046 `; 1047 this.wordHighlight = document.createElement("div"); 1048 this.wordHighlight.style.cssText = ` 1049 position: absolute; 1050 background: rgba(255, 235, 59, 0.3); 1051 border: 1px solid rgba(255, 193, 7, 0.5); 1052 border-radius: 3px; 1053 pointer-events: none; 1054 display: none; 1055 padding: 2px 4px; 1056 margin: -2px -4px; 1057 `; 1058 this.magnifierContainer.appendChild(header); 1059 this.magnifierContainer.appendChild(this.magnifierContent); 1060 this.magnifierContent.appendChild(this.wordHighlight); 1061 document.body.appendChild(this.magnifierContainer); 1062 this.magnifierZoom = 2; 1063 } 1064 const handleMagnifier = (e) => { 1065 const target = e.target; 1066 if (!target || !target.textContent || target.textContent.trim().length === 0) { 1067 this.magnifierContainer.style.display = "none"; 1068 return; 1069 } 1070 const primaryColor = getPrimaryColor();
1071 const darkerShade = this.darkenColor(primaryColor, 10); 1072 const textColor = this.getContrastColor(primaryColor); 1073 this.magnifierContainer.style.borderColor = primaryColor; 1074 const header = this.magnifierContainer.querySelector("div:first-child"); 1075 if (header && header !== this.magnifierContent) { 1076 header.style.background = `linear-gradient(90deg, ${primaryColor} 0%, ${darkerShade} 100%)`; 1077 header.style.borderBottomColor = darkerShade; 1078 const headerElements = header.querySelectorAll("*"); 1079 headerElements.forEach((el) => { 1080 if (el.style) { 1081 el.style.color = textColor; 1082 } 1083 }); 1084 const zoomEl = document.getElementById("magnifier-zoom"); 1085 if (zoomEl) { 1086 zoomEl.style.background = textColor === "#ffffff" ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.1)"; 1087 zoomEl.style.color = textColor; 1088 } 1089 } 1090 let contextElement = target; 1091 let text = ""; 1092 let isSpecialElement = false; 1093 if (target.tagName === "IMG") { 1094 text = target.alt || target.title || "Bild ohne Beschreibung"; 1095 isSpecialElement = true; 1096 } else if (target.tagName === "BUTTON" || target.tagName === "A") { 1097 text = target.textContent.trim() || target.title || target.getAttribute("aria-label") || ""; 1098 if (target.tagName === "A" && !text) { 1099 text = target.href; 1100 } 1101 isSpecialElement = true; 1102 } else if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") { 1103 text = target.value || target.placeholder || ""; 1104 isSpecialElement = true; 1105 } 1106 if (!isSpecialElement) { 1107 const parentBlock = target.closest("p, li, h1, h2, h3, h4, h5, h6, td, blockquote, article, section"); 1108 if (parentBlock) { 1109 contextElement = parentBlock; 1110 } 1111 text = contextElement.textContent.trim(); 1112 if (text.length > 300) { 1113 const range = document.caretRangeFromPoint(e.clientX, e.clientY); 1114 if (range && range.startContainer.nodeType === Node.TEXT_NODE) { 1115 const fullText = range.startContainer.textContent; 1116 const offset = range.startOffset; 1117 let start = offset; 1118 let end = offset; 1119 while (start > 0 && start > offset - 100) { 1120 if (fullText[start] === "." || fullText[start] === "!" || fullText[start] === "?") { 1121 start++; 1122 break; 1123 } 1124 start--; 1125 } 1126 while (end < fullText.length && end < offset + 200) { 1127 if (fullText[end] === "." || fullText[end] === "!" || fullText[end] === "?") { 1128 end++; 1129 break; 1130 } 1131 end++; 1132 } 1133 text = fullText.substring(start, end).trim(); 1134 if (start > 0) text = "..." + text; 1135 if (end < fullText.length) text = text + "..."; 1136 const wordStart = offset; 1137 let wordEnd = offset; 1138 while (wordEnd < fullText.length && fullText[wordEnd] !== " " && fullText[wordEnd] !== "," && fullText[wordEnd] !== ".") { 1139 wordEnd++; 1140 } 1141 const currentWord = fullText.substring(wordStart, wordEnd); 1142 if (currentWord && currentWord.trim()) { 1143 const parts = text.split(currentWord); 1144 if (parts.length > 1) { 1145 this.magnifierContent.innerHTML = ""; 1146 parts.forEach((part, index) => { 1147 const textNode = document.createTextNode(part); 1148 this.magnifierContent.appendChild(textNode); 1149 if (index < parts.length - 1) { 1150 const highlight = document.createElement("span"); 1151 highlight.style.cssText = ` 1152 background: linear-gradient(180deg, transparent 40%, rgba(255, 235, 59, 0.6) 40%, rgba(255, 235, 59, 0.6) 95%, transparent 95%); 1153 padding: 0 2px; 1154 font-weight: 500; 1155 `; 1156 highlight.textContent = currentWord; 1157 this.magnifierContent.appendChild(highlight); 1158 } 1159 }); 1160 } else { 1161 this.magnifierContent.textContent = text; 1162 } 1163 } else { 1164 this.magnifierContent.textContent = text; 1165 } 1166 } else { 1167 text = text.substring(0, 300); 1168 if (text.length === 300) text += "..."; 1169 this.magnifierContent.textContent = text; 1170 } 1171 } else { 1172 this.magnifierContent.textContent = text; 1173 } 1174 } else { 1175 this.magnifierContent.textContent = text; 1176 } 1177 if (text) { 1178 const zoomEl = document.getElementById("magnifier-zoom"); 1179 if (zoomEl) { 1180 zoomEl.textContent = this.magnifierZoom.toFixed(1) + "x"; 1181 } 1182 this.magnifierContent.style.fontSize = this.magnifierZoom + "em"; 1183 this.magnifierContainer.style.display = "block"; 1184 this.magnifierContainer.style.opacity = "1"; 1185 const rect = this.magnifierContainer.getBoundingClientRect(); 1186 const magnifierWidth = 600; 1187 const magnifierHeight = Math.min(300, rect.height); 1188 let x = e.clientX - magnifierWidth / 2; 1189 let y = e.clientY - magnifierHeight - 40; 1190 x = Math.max(10, Math.min(x, window.innerWidth - magnifierWidth - 10)); 1191 if (y < 10) { 1192 y = e.clientY + 40; 1193 } 1194 if (y + magnifierHeight > window.innerHeight - 10) { 1195 y = Math.max(10, Math.min(e.clientY - magnifierHeight / 2, window.innerHeight - magnifierHeight - 10)); 1196 if (e.clientX > window.innerWidth / 2) { 1197 x = e.clientX - magnifierWidth - 40; 1198 } else { 1199 x = e.clientX + 40; 1200 } 1201 } 1202 this.magnifierContainer.style.left = x + "px"; 1203 this.magnifierContainer.style.top = y + "px"; 1204 } 1205 }; 1206 const hideMagnifier = () => { 1207 if (this.magnifierContainer) { 1208 this.magnifierContainer.style.opacity = "0"; 1209 setTimeout(() => { 1210 if (this.magnifierContainer) { 1211 this.magnifierContainer.style.display = "none"; 1212 } 1213 }, 200); 1214 } 1215 }; 1216 const handleKeyboard = (e) => { 1217 if (!this.magnifierContainer || this.magnifierContainer.style.display === "none") return; 1218 if ((e.ctrlKey || e.metaKey) && (e.key === "+" || e.key === "=")) { 1219 e.preventDefault(); 1220 this.magnifierZoom = Math.min(4, this.magnifierZoom + 0.5); 1221 const zoomEl = document.getElementById("magnifier-zoom"); 1222 if (zoomEl) zoomEl.textContent = this.magnifierZoom.toFixed(1) + "x"; 1223 this.magnifierContent.style.fontSize = this.magnifierZoom + "em"; 1224 } 1225 if ((e.ctrlKey || e.metaKey) && e.key === "-") { 1226 e.preventDefault(); 1227 this.magnifierZoom = Math.max(1, this.magnifierZoom - 0.5); 1228 const zoomEl = document.getElementById("magnifier-zoom"); 1229 if (zoomEl) zoomEl.textContent = this.magnifierZoom.toFixed(1) + "x"; 1230 this.magnifierContent.style.fontSize = this.magnifierZoom + "em"; 1231 } 1232 if ((e.ctrlKey || e.metaKey) && e.key === "0") { 1233 e.preventDefault(); 1234 this.magnifierZoom = 2; 1235 const zoomEl = document.getElementById("magnifier-zoom"); 1236 if (zoomEl) zoomEl.textContent = this.magnifierZoom.toFixed(1) + "x"; 1237 this.magnifierContent.style.fontSize = this.magnifierZoom + "em"; 1238 } 1239 }; 1240 this.magnifierHandler = handleMagnifier; 1241 this.magnifierHideHandler = hideMagnifier; 1242 this.magnifierKeyboardHandler = handleKeyboard; 1243 document.addEventListener("mousemove", this.magnifierHandler, { passive: true }); 1244 document.addEventListener("mouseleave", this.magnifierHideHandler); 1245 document.addEventListener("keydown", this.magnifierKeyboardHandler); 1246 } else { 1247 this.removeClass("accessibility-text-magnifier"); 1248 if (window.ModAccessibilityFunctions?.advancedMagnifier) { 1249 this.useAdvancedMagnifier(false); 1250 } 1251 if (this.magnifierHandler) { 1252 document.removeEventListener("mousemove", this.magnifierHandler); 1253 this.magnifierHandler = null; 1254 } 1255 if (this.magnifierHideHandler) { 1256 document.removeEventListener("mouseleave", this.magnifierHideHandler); 1257 this.magnifierHideHandler = null; 1258 } 1259 if (this.magnifierKeyboardHandler) { 1260 document.removeEventListener("keydown", this.magnifierKeyboardHandler); 1261 this.magnifierKeyboardHandler = null; 1262 } 1263 if (this.magnifierContainer) { 1264 this.magnifierContainer.remove(); 1265 this.magnifierContainer = null; 1266 this.magnifierContent = null; 1267 this.wordHighlight = null; 1268 } 1269 const existingMagnifier = document.getElementById("a11y-text-magnifier"); 1270 if (existingMagnifier) { 1271 existingMagnifier.remove(); 1272 } 1273 } 1274 } 1275 /** 1276 * Use advanced magnifier if available 1277 */ 1278 useAdvancedMagnifier(enabled) { 1279 const advMagnifier = window.ModAccessibilityFunctions?.advancedMagnifier; 1280 if (!advMagnifier) return; 1281 if (enabled) { 1282 advMagnifier.create(); 1283 let throttleTimer = null; 1284 const handleAdvancedMagnifier = (e) => { 1285 if (throttleTimer) return; 1286 throttleTimer = setTimeout(() => { 1287 throttleTimer = null; 1288 }, 50); 1289 const target = e.target; 1290 if (target.closest("#a11y-text-magnifier-advanced") || target.closest("#accessibility-menu") || target.closest("#a11y-toggle")) { 1291 return; 1292 } 1293 if (!target || !target.textContent || target.textContent.trim().length === 0) { 1294 advMagnifier.hide(); 1295 return; 1296 } 1297 let text = ""; 1298 const parentBlock = target.closest("p, li, h1, h2, h3, h4, h5, h6, td, blockquote, article, span, a"); 1299 if (parentBlock) { 1300 text = parentBlock.textContent.trim(); 1301 } else { 1302 text = target.textContent.trim(); 1303 } 1304 if (text && text.length > 2) { 1305 advMagnifier.setText(text); 1306 advMagnifier.show(e.clientX, e.clientY); 1307 } 1308 }; 1309 const handleMouseLeave = () => { 1310 if (!advMagnifier.isPinned) { 1311 advMagnifier.hide(); 1312 } 1313 }; 1314 const handleKeyboard = (e) => { 1315 const container = document.getElementById("a11y-text-magnifier-advanced"); 1316 if (!container) return; 1317 const isVisible = container.style.display === "block"; 1318 if (e.key === "Escape" && isVisible && !advMagnifier.isPinned) { 1319 advMagnifier.hide(); 1320 return; 1321 } 1322 if (e.key === "p" && isVisible) { 1323 e.preventDefault(); 1324 advMagnifier.togglePin(); 1325 return; 1326 } 1327 if (!isVisible) return; 1328 if (e.key === "+" || e.key === "=") { 1329 e.preventDefault(); 1330 advMagnifier.setZoom(advMagnifier.zoom + 0.5); 1331 } else if (e.key === "-" || e.key === "_") { 1332 e.preventDefault(); 1333 advMagnifier.setZoom(advMagnifier.zoom - 0.5); 1334 } else if (e.key === "0") { 1335 e.preventDefault(); 1336 advMagnifier.setZoom(2); 1337 } 1338 if (e.key === "ArrowLeft") { 1339 e.preventDefault();
1340 advMagnifier.navigateWord(-1); 1341 } else if (e.key === "ArrowRight") { 1342 e.preventDefault(); 1343 advMagnifier.navigateWord(1); 1344 } 1345 if (e.key === "t" && isVisible) { 1346 e.preventDefault(); 1347 advMagnifier.toggleSpeech(); 1348 } 1349 }; 1350 this.advancedMagnifierHandler = handleAdvancedMagnifier; 1351 this.advancedMagnifierLeaveHandler = handleMouseLeave; 1352 this.advancedMagnifierKeyHandler = handleKeyboard; 1353 document.addEventListener("mousemove", this.advancedMagnifierHandler, { passive: true }); 1354 document.addEventListener("mouseleave", this.advancedMagnifierLeaveHandler); 1355 document.addEventListener("keydown", this.advancedMagnifierKeyHandler); 1356 } else { 1357 if (this.advancedMagnifierHandler) { 1358 document.removeEventListener("mousemove", this.advancedMagnifierHandler); 1359 this.advancedMagnifierHandler = null; 1360 } 1361 if (this.advancedMagnifierLeaveHandler) { 1362 document.removeEventListener("mouseleave", this.advancedMagnifierLeaveHandler); 1363 this.advancedMagnifierLeaveHandler = null; 1364 } 1365 if (this.advancedMagnifierKeyHandler) { 1366 document.removeEventListener("keydown", this.advancedMagnifierKeyHandler); 1367 this.advancedMagnifierKeyHandler = null; 1368 } 1369 if (advMagnifier) { 1370 advMagnifier.destroy(); 1371 } 1372 } 1373 } 1374 /** 1375 * Toggles text-to-speech functionality 1376 * Enables reading of text content on hover or click 1377 * 1378 * @param {boolean} enabled - Whether to enable text-to-speech 1379 * @returns {void} 1380 */ 1381 setTextToSpeech(enabled) { 1382 if (enabled) { 1383 this.addClass("accessibility-text-to-speech"); 1384 this.startTextToSpeech(); 1385 } else { 1386 this.removeClass("accessibility-text-to-speech"); 1387 this.stopTextToSpeech(); 1388 } 1389 } 1390 /** 1391 * Starts text-to-speech functionality 1392 * Sets up event listeners for text selection and reading 1393 * 1394 * @private 1395 * @returns {void} 1396 */ 1397 startTextToSpeech() { 1398 if (!this.speechSynthesis) { 1399 console.warn("Text-to-speech not supported in this browser"); 1400 return; 1401 } 1402 this.stopTextToSpeech(); 1403 const clickHandler = (e) => { 1404 const target = e.target; 1405 const text = target.textContent || target.innerText; 1406 if (text && text.trim()) { 1407 this.speak(text.trim()); 1408 } 1409 }; 1410 const hoverHandler = (e) => { 1411 const target = e.target; 1412 if (target.childElementCount === 0) { 1413 const text = target.textContent || target.innerText; 1414 if (text && text.trim() && text.trim().length > 3) { 1415 if (this.hoverTimeoutId) { 1416 clearTimeout(this.hoverTimeoutId); 1417 } 1418 this.hoverTimeoutId = setTimeout(() => { 1419 this.speak(text.trim()); 1420 }, 500); 1421 } 1422 } 1423 }; 1424 this.clickHandler = clickHandler; 1425 this.hoverHandler = hoverHandler; 1426 document.addEventListener("click", this.clickHandler); 1427 document.addEventListener("mouseover", this.hoverHandler); 1428 } 1429 stopTextToSpeech() { 1430 if (this.speechSynthesis) { 1431 this.speechSynthesis.cancel(); 1432 } 1433 if (this.clickHandler) { 1434 document.removeEventListener("click", this.clickHandler); 1435 this.clickHandler = null; 1436 } 1437 if (this.hoverHandler) { 1438 document.removeEventListener("mouseover", this.hoverHandler); 1439 this.hoverHandler = null; 1440 } 1441 if (this.hoverTimeoutId) { 1442 clearTimeout(this.hoverTimeoutId); 1443 this.hoverTimeoutId = null; 1444 } 1445 } 1446 /** 1447 * Speaks the provided text using the Speech Synthesis API 1448 * Prioritizes German voice if available 1449 * 1450 * @param {string} text - Text to be spoken 1451 * @returns {void} 1452 */ 1453 speak(text) { 1454 if (!this.speechSynthesis) return; 1455 this.speechSynthesis.cancel(); 1456 this.currentUtterance = new SpeechSynthesisUtterance(text); 1457 this.currentUtterance.rate = 0.9; 1458 this.currentUtterance.pitch = 1; 1459 this.currentUtterance.volume = 1; 1460 const voices = this.speechSynthesis.getVoices(); 1461 const germanVoice = voices.find((voice) => voice.lang.startsWith("de")); 1462 if (germanVoice) { 1463 this.currentUtterance.voice = germanVoice; 1464 } 1465 this.speechSynthesis.speak(this.currentUtterance); 1466 } 1467 reset() { 1468 this.setReadableFont(false); 1469 this.setDyslexicFont(false); 1470 this.setTextMagnifier(false); 1471 this.setTextToSpeech(false); 1472 if (this.magnifierContainer) { 1473 this.magnifierContainer.remove(); 1474 this.magnifierContainer = null; 1475 this.magnifierContent = null; 1476 this.wordHighlight = null; 1477 } 1478 } 1479} 1480class InteractionModule extends BaseModule { 1481 /** 1482 * Creates a new InteractionModule instance 1483 */ 1484 constructor() { 1485 super("interaction"); 1486 this.readingGuideListenerId = null; 1487 this.readingGuideElement = null; 1488 this.currentCursorLevel = 0; 1489 } 1490 /** 1491 * Sets the big cursor with multiple size levels 1492 * Uses simple larger cursor with visual indicator 1493 * 1494 * @param {number} level - Cursor size level (0-3, where 0 is default) 1495 * @returns {void} 1496 */ 1497 setBigCursor(level) { 1498 this.removeClass("accessibility-cursor"); 1499 this.removeClass("accessibility-cursor-1"); 1500 this.removeClass("accessibility-cursor-2"); 1501 this.removeClass("accessibility-cursor-3"); 1502 const existingCursorStyle = document.getElementById("a11y-cursor-style"); 1503 if (existingCursorStyle) { 1504 existingCursorStyle.remove(); 1505 } 1506 const existingIndicator = document.getElementById("a11y-cursor-indicator"); 1507 if (existingIndicator) { 1508 existingIndicator.remove(); 1509 } 1510 if (level === 0 || !level) { 1511 this.currentCursorLevel = 0; 1512 return; 1513 } 1514 level = Math.min(Math.max(level, 1), 3); 1515 this.currentCursorLevel = level; 1516 const sizes = { 1517 1: 24, 1518 // Small indicator 1519 2: 32, 1520 // Medium indicator 1521 3: 40 1522 // Large indicator 1523 }; 1524 const size = sizes[level]; 1525 this.addClass(`accessibility-cursor-${level}`); 1526 const indicator = document.createElement("div"); 1527 indicator.id = "a11y-cursor-indicator"; 1528 indicator.style.cssText = `
1529 position: fixed; 1530 width: ${size}px; 1531 height: ${size}px; 1532 border: 3px solid #000; 1533 border-radius: 50%; 1534 background: rgba(255, 255, 0, 0.3); 1535 pointer-events: none; 1536 z-index: 9998; 1537 transform: translate(-50%, -50%); 1538 transition: none; 1539 display: none; 1540 `; 1541 document.body.appendChild(indicator); 1542 const moveHandler = (e) => { 1543 indicator.style.left = e.clientX + "px"; 1544 indicator.style.top = e.clientY + "px"; 1545 indicator.style.display = "block"; 1546 }; 1547 const leaveHandler = () => { 1548 indicator.style.display = "none"; 1549 }; 1550 document.addEventListener("mousemove", moveHandler); 1551 document.addEventListener("mouseleave", leaveHandler); 1552 indicator.dataset.moveHandler = "cursor-move"; 1553 indicator.dataset.leaveHandler = "cursor-leave"; 1554 const styleEl = document.createElement("style"); 1555 styleEl.id = "a11y-cursor-style"; 1556 styleEl.textContent = ` 1557 /* Big Cursor Level ${level} - Indicator Size: ${size}px */ 1558 /* Version: 4.0.${Date.now()} */ 1559 1560 /* Keep normal cursor but make it more visible */ 1561 .accessibility-cursor-${level} { 1562 cursor: auto !important; 1563 } 1564 1565 .accessibility-cursor-${level} a, 1566 .accessibility-cursor-${level} button, 1567 .accessibility-cursor-${level} [role="button"], 1568 .accessibility-cursor-${level} [onclick] { 1569 cursor: pointer !important; 1570 } 1571 1572 .accessibility-cursor-${level} input[type="text"], 1573 .accessibility-cursor-${level} textarea, 1574 .accessibility-cursor-${level} [contenteditable="true"] { 1575 cursor: text !important; 1576 } 1577 1578 /* Visual indicator styling */ 1579 #a11y-cursor-indicator { 1580 mix-blend-mode: difference; 1581 } 1582 1583 /* Respect reduced motion preference */ 1584 @media (prefers-reduced-motion: reduce) { 1585 #a11y-cursor-indicator { 1586 display: none !important; 1587 } 1588 } 1589 `; 1590 document.head.appendChild(styleEl); 1591 } 1592 // Keep old setCursor for backwards compatibility 1593 setCursor(enabled) { 1594 this.setBigCursor(enabled ? 1 : 0); 1595 } 1596 setUnderlineLinks(enabled) { 1597 const styleId = "a11y-underline-links-style"; 1598 const existingStyle = document.getElementById(styleId); 1599 if (enabled) { 1600 this.addClass("accessibility-underline-links"); 1601 if (!existingStyle) { 1602 const styleEl = document.createElement("style"); 1603 styleEl.id = styleId; 1604 styleEl.textContent = ` 1605 /* Underline Links Styles */ 1606 #site-content-wrapper.accessibility-underline-links a { 1607 text-decoration: underline !important; 1608 text-decoration-thickness: 2px !important; 1609 text-underline-offset: 2px !important; 1610 } 1611 #site-content-wrapper.accessibility-underline-links a:hover { 1612 text-decoration: underline !important; 1613 text-decoration-thickness: 3px !important; 1614 } 1615 `; 1616 document.head.appendChild(styleEl); 1617 } 1618 } else { 1619 this.removeClass("accessibility-underline-links"); 1620 if (existingStyle) { 1621 existingStyle.remove(); 1622 } 1623 } 1624 } 1625 setPauseAnimations(enabled) { 1626 const styleId = "a11y-pause-animations-style"; 1627 const existingStyle = document.getElementById(styleId); 1628 if (enabled) { 1629 this.addClass("accessibility-pause-animations"); 1630 if (!existingStyle) { 1631 const styleEl = document.createElement("style"); 1632 styleEl.id = styleId; 1633 styleEl.textContent = ` 1634 /* Pause Animations Styles */ 1635 #site-content-wrapper.accessibility-pause-animations *, 1636 #site-content-wrapper.accessibility-pause-animations *:before, 1637 #site-content-wrapper.accessibility-pause-animations *:after { 1638 animation-play-state: paused !important; 1639 animation-duration: 0.01s !important; 1640 transition: none !important; 1641 transition-duration: 0s !important; 1642 } 1643 #site-content-wrapper.accessibility-pause-animations img, 1644 #site-content-wrapper.accessibility-pause-animations video, 1645 #site-content-wrapper.accessibility-pause-animations iframe { 1646 animation: none !important; 1647 transition: none !important; 1648 } 1649 `; 1650 document.head.appendChild(styleEl); 1651 } 1652 } else { 1653 this.removeClass("accessibility-pause-animations"); 1654 if (existingStyle) { 1655 existingStyle.remove(); 1656 } 1657 } 1658 } 1659 /** 1660 * Toggles the reading guide
1661 * Shows a horizontal line that follows the mouse cursor for easier reading 1662 * 1663 * @param {boolean} enabled - Whether to enable the reading guide 1664 * @returns {void} 1665 */ 1666 /** 1667 * Sets page zoom level for better visibility 1668 * Zooms the entire page content 1669 * 1670 * @param {number} level - Zoom level (0-3, where 0 is default) 1671 * @returns {void} 1672 */ 1673 setPageZoom(level) { 1674 this.removeClass("accessibility-zoom-1"); 1675 this.removeClass("accessibility-zoom-2"); 1676 this.removeClass("accessibility-zoom-3"); 1677 if (level === 0 || !level) { 1678 return; 1679 } 1680 level = Math.min(Math.max(level, 1), 3); 1681 const zoomFactors = { 1682 1: 1.1, 1683 // 110% zoom 1684 2: 1.25, 1685 // 125% zoom 1686 3: 1.5 1687 // 150% zoom 1688 }; 1689 const zoomFactor = zoomFactors[level]; 1690 this.addClass(`accessibility-zoom-${level}`); 1691 const wrapper = document.getElementById("site-content-wrapper"); 1692 if (wrapper) { 1693 wrapper.style.zoom = zoomFactor; 1694 wrapper.style.transformOrigin = "top center"; 1695 } 1696 } 1697 setReadingGuide(enabled) { 1698 if (enabled) { 1699 this.addClass("accessibility-reading-guide"); 1700 const updateGuide = (e) => { 1701 if (!this.readingGuideElement) { 1702 this.readingGuideElement = document.createElement("div"); 1703 this.readingGuideElement.id = "a11y-reading-guide"; 1704 this.readingGuideElement.style.cssText = ` 1705 position: fixed; 1706 left: 0; 1707 right: 0; 1708 height: 20px; 1709 background: rgba(255, 255, 0, 0.3); 1710 border-top: 2px solid #333; 1711 border-bottom: 2px solid #333; 1712 pointer-events: none; 1713 z-index: 9998; 1714 transition: top 0.1s ease; 1715 `; 1716 document.body.appendChild(this.readingGuideElement); 1717 } 1718 this.readingGuideElement.style.top = e.clientY - 10 + "px"; 1719 }; 1720 this.readingGuideHandler = updateGuide; 1721 document.addEventListener("mousemove", this.readingGuideHandler, { passive: true }); 1722 } else { 1723 this.removeClass("accessibility-reading-guide"); 1724 if (this.readingGuideHandler) { 1725 document.removeEventListener("mousemove", this.readingGuideHandler); 1726 this.readingGuideHandler = null; 1727 } 1728 if (this.readingGuideElement) { 1729 this.readingGuideElement.remove(); 1730 this.readingGuideElement = null; 1731 } 1732 } 1733 } 1734 reset() { 1735 this.setBigCursor(0); 1736 this.setUnderlineLinks(false); 1737 this.setPauseAnimations(false); 1738 this.setReadingGuide(false); 1739 this.setPageZoom(0); 1740 const wrapper = document.getElementById("site-content-wrapper"); 1741 if (wrapper) { 1742 wrapper.style.zoom = ""; 1743 wrapper.style.transformOrigin = ""; 1744 } 1745 } 1746} 1747class SimpleModules { 1748 /** 1749 * Creates a new SimpleModules instance 1750 * Initializes all sub-modules and defines available functions 1751 */ 1752 constructor() { 1753 this.font = new FontModule(); 1754 this.visual = new VisualModule();
1755 this.color = new ColorModule(); 1756 this.text = new TextModule(); 1757 this.interaction = new InteractionModule(); 1758 this.functions = [ 1759 "font-size", 1760 "line-height", 1761 "letter-spacing", 1762 "contrast", 1763 "brightness", 1764 "saturation", 1765 "invert-colors", 1766 "grayscale", 1767 "readable-font", 1768 "dyslexic-font", 1769 "text-magnifier", 1770 "text-to-speech", 1771 "cursor", 1772 "underline-links", 1773 "pause-animations", 1774 "reading-guide" 1775 ]; 1776 } 1777 /** 1778 * Applies an accessibility function with the specified value 1779 * Routes the request to the appropriate module 1780 * 1781 * @param {string} functionName - Name of the accessibility function 1782 * @param {number|boolean} value - Value to apply (level for multi-state, boolean for toggles) 1783 * @returns {void} 1784 * @example 1785 * modules.apply('font-size', 2); // Set font size to level 2 1786 * modules.apply('readable-font', true); // Enable readable font 1787 */ 1788 apply(functionName, value) { 1789 switch (functionName) { 1790 case "font-size": 1791 this.font.setFontSize(value); 1792 break; 1793 case "line-height": 1794 this.font.setLineHeight(value); 1795 break; 1796 case "letter-spacing": 1797 this.font.setLetterSpacing(value); 1798 break; 1799 case "contrast": 1800 this.visual.setContrast(value); 1801 break; 1802 case "brightness": 1803 this.visual.setBrightness(value); 1804 break; 1805 case "saturation": 1806 this.visual.setSaturation(value); 1807 break; 1808 case "invert-colors": 1809 this.color.setInvertColors(value); 1810 break; 1811 case "grayscale": 1812 this.color.setGrayscale(value); 1813 break; 1814 case "readable-font": 1815 this.text.setReadableFont(value); 1816 break; 1817 case "dyslexic-font": 1818 this.text.setDyslexicFont(value); 1819 break; 1820 case "text-magnifier": 1821 this.text.setTextMagnifier(value); 1822 break; 1823 case "text-to-speech": 1824 this.text.setTextToSpeech(value); 1825 break; 1826 case "cursor": 1827 this.interaction.setBigCursor(value ? value : 0); 1828 break; 1829 case "underline-links": 1830 this.interaction.setUnderlineLinks(value); 1831 break; 1832 case "pause-animations": 1833 this.interaction.setPauseAnimations(value); 1834 break; 1835 case "reading-guide": 1836 this.interaction.setReadingGuide(value); 1837 break; 1838 case "page-zoom": 1839 this.interaction.setPageZoom(value); 1840 break; 1841 } 1842 } 1843 /** 1844 * Resets all accessibility features to default state 1845 * Removes all classes, CSS variables, and injected styles 1846 * 1847 * @returns {void} 1848 */ 1849 reset() { 1850 this.font.reset(); 1851 this.visual.reset(); 1852 this.color.reset(); 1853 this.text.reset(); 1854 this.interaction.reset(); 1855 const wrapper = document.getElementById("site-content-wrapper"); 1856 if (wrapper) { 1857 const classes = Array.from(wrapper.classList); 1858 classes.forEach((c) => { 1859 if (c.startsWith("accessibility-")) { 1860 wrapper.classList.remove(c); 1861 } 1862 }); 1863 const styles = wrapper.style; 1864 for (let i = styles.length - 1; i >= 0; i--) { 1865 const prop = styles[i]; 1866 if (prop.startsWith("--accessibility-")) { 1867 styles.removeProperty(prop); 1868 } 1869 } 1870 } else { 1871 console.warn("site-content-wrapper not found during reset"); 1872 } 1873 document.querySelectorAll('style[id*="accessibility"]').forEach((el) => el.remove()); 1874 } 1875} 1876const simpleModules = new SimpleModules(); 1877class ProfilesManager { 1878 constructor() { 1879 this.profiles = PROFILE_DEFINITIONS; 1880 this.activeProfiles = /* @__PURE__ */ new Set(); 1881 this.stateManager = null; 1882 } 1883 /** 1884 * Toggle a profile - SIMPLIFIED VERSION 1885 * @param {string} profileKey - Profile to toggle 1886 * @returns {boolean} Success status 1887 */ 1888 toggleProfile(profileKey) { 1889 const profile = this.profiles[profileKey]; 1890 if (!profile) { 1891 console.error(`Profile not found: ${profileKey}`); 1892 return false; 1893 } 1894 const isActive = this.activeProfiles.has(profileKey); 1895 if (isActive) { 1896 this.deactivateProfile(profileKey); 1897 } else { 1898 this.activateProfile(profileKey); 1899 } 1900 return true; 1901 } 1902 /** 1903 * Activate a profile - SIMPLIFIED VERSION 1904 * Only one profile can be active at a time 1905 * @param {string} profileKey - Profile to activate 1906 * @returns {boolean} Success status 1907 */ 1908 activateProfile(profileKey) { 1909 const profile = this.profiles[profileKey]; 1910 if (!profile) { 1911 console.error(`Profile not found: ${profileKey}`); 1912 return false;
1913 } 1914 if (this.stateManager) { 1915 this.stateManager.activateProfile(profileKey); 1916 this.activeProfiles.clear(); 1917 this.activeProfiles.add(profileKey); 1918 } 1919 document.dispatchEvent(new CustomEvent("accessibility:profile:activated", { 1920 detail: { profileKey, profile } 1921 })); 1922 return true; 1923 } 1924 /** 1925 * Deactivate a profile - SIMPLIFIED VERSION 1926 * @param {string} profileKey - Profile to deactivate 1927 */ 1928 deactivateProfile(profileKey) { 1929 const profile = this.profiles[profileKey]; 1930 if (!profile || !this.activeProfiles.has(profileKey)) { 1931 return false; 1932 } 1933 this.activeProfiles.delete(profileKey); 1934 if (this.stateManager) { 1935 this.stateManager.setProfileState(profileKey, false); 1936 if (profile.functions) { 1937 Object.entries(profile.functions).forEach(([functionName, value]) => { 1938 const defaultValue = typeof value === "number" ? 0 : false; 1939 this.stateManager.setFunctionState(functionName, defaultValue, true); 1940 }); 1941 } 1942 } 1943 document.dispatchEvent(new CustomEvent("accessibility:profile:deactivated", { 1944 detail: { profileKey, profile } 1945 })); 1946 return true; 1947 } 1948 /** 1949 * Get profile functions 1950 * @param {string} profileKey - Profile key 1951 * @returns {Object|null} Profile functions or null 1952 */ 1953 getProfileFunctions(profileKey) { 1954 const profile = this.profiles[profileKey]; 1955 return profile ? profile.functions : null; 1956 } 1957} 1958const profilesManager = new ProfilesManager(); 1959class AdvancedMagnifier { 1960 constructor() { 1961 this.container = null; 1962 this.contentArea = null; 1963 this.isPinned = false; 1964 this.isResizing = false; 1965 this.currentMode = "light"; 1966 this.zoom = 2; 1967 this.width = 600; 1968 this.height = 200; 1969 this.isSpeaking = false; 1970 this.currentText = ""; 1971 this.wordIndex = 0; 1972 this.words = []; 1973 this.colorModes = { 1974 light: { 1975 bg: "#ffffff", 1976 text: "#2c3e50", 1977 highlight: "rgba(255, 235, 59, 0.4)", 1978 border: "#2c3e50" 1979 }, 1980 dark: { 1981 bg: "#1a1a1a", 1982 text: "#e0e0e0", 1983 highlight: "rgba(100, 200, 255, 0.4)", 1984 border: "#3498db" 1985 }, 1986 highContrast: { 1987 bg: "#000000", 1988 text: "#ffff00", 1989 highlight: "rgba(255, 255, 255, 0.3)", 1990 border: "#ffffff" 1991 }, 1992 sepia: { 1993 bg: "#f4ecd8", 1994 text: "#5c4033", 1995 highlight: "rgba(139, 69, 19, 0.2)", 1996 border: "#8b4513" 1997 } 1998 }; 1999 } 2000 /** 2001 * Get primary color from various sources 2002 */ 2003 getPrimaryColor() {
2004 let color = getComputedStyle(document.documentElement).getPropertyValue("--a11y-primary-color").trim(); 2005 if (!color) { 2006 const a11yBox = document.getElementById("a11y-box"); 2007 if (a11yBox) { 2008 color = a11yBox.dataset.primaryColor; 2009 } 2010 } 2011 if (!color && window.a11yPrimaryColor) { 2012 color = window.a11yPrimaryColor; 2013 } 2014 return color || "#0056b3"; 2015 } 2016 /** 2017 * Darken a color by a percentage 2018 */ 2019 darkenColor(hex, percent) { 2020 hex = hex.replace("#", ""); 2021 const r = parseInt(hex.substring(0, 2), 16); 2022 const g = parseInt(hex.substring(2, 4), 16); 2023 const b = parseInt(hex.substring(4, 6), 16); 2024 const factor = 1 - percent / 100; 2025 const newR = Math.round(r * factor); 2026 const newG = Math.round(g * factor); 2027 const newB = Math.round(b * factor); 2028 return "#" + [newR, newG, newB].map((x) => x.toString(16).padStart(2, "0")).join(""); 2029 } 2030 /** 2031 * Get contrasting text color (black or white) based on background 2032 */ 2033 getContrastColor(hex) { 2034 hex = hex.replace("#", ""); 2035 const r = parseInt(hex.substring(0, 2), 16); 2036 const g = parseInt(hex.substring(2, 4), 16); 2037 const b = parseInt(hex.substring(4, 6), 16); 2038 const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; 2039 return luminance > 0.5 ? "#000000" : "#ffffff"; 2040 } 2041 create() { 2042 if (this.container) return; 2043 this.container = document.createElement("div"); 2044 this.container.id = "a11y-text-magnifier-advanced"; 2045 this.container.className = "magnifier-advanced"; 2046 this.updateContainerStyle(); 2047 const header = this.createHeader(); 2048 this.contentArea = document.createElement("div"); 2049 this.contentArea.className = "magnifier-content"; 2050 this.updateContentStyle(); 2051 const footer = this.createFooter(); 2052 const resizeHandle = document.createElement("div"); 2053 resizeHandle.className = "magnifier-resize"; 2054 resizeHandle.style.cssText = ` 2055 position: absolute; 2056 bottom: 0; 2057 right: 0; 2058 width: 20px; 2059 height: 20px; 2060 cursor: nwse-resize; 2061 background: linear-gradient(135deg, transparent 50%, rgba(52, 152, 219, 0.5) 50%); 2062 border-radius: 0 0 10px 0; 2063 `; 2064 this.container.appendChild(header); 2065 this.container.appendChild(this.contentArea); 2066 this.container.appendChild(footer); 2067 this.container.appendChild(resizeHandle); 2068 document.body.appendChild(this.container); 2069 this.initResize(resizeHandle); 2070 this.synth = window.speechSynthesis; 2071 this.injectStyles(); 2072 } 2073 createHeader() { 2074 const primaryColor = this.getPrimaryColor(); 2075 const darkerShade = this.darkenColor(primaryColor, 10); 2076 const textColor = this.getContrastColor(primaryColor); 2077 const header = document.createElement("div"); 2078 header.className = "magnifier-header"; 2079 header.style.cssText = ` 2080 background: linear-gradient(90deg, ${primaryColor} 0%, ${darkerShade} 100%); 2081 padding: 10px 15px; 2082 display: flex; 2083 justify-content: space-between; 2084 align-items: center; 2085 border-bottom: 1px solid ${darkerShade}; 2086 user-select: none; 2087 cursor: move; 2088 `; 2089 const leftSide = document.createElement("div"); 2090 leftSide.style.cssText = "display: flex; align-items: center; gap: 15px;"; 2091 const title = document.createElement("span"); 2092 title.style.cssText = ` 2093 color: ${textColor}; 2094 font-size: 12px; 2095 font-weight: 600; 2096 letter-spacing: 0.5px; 2097 text-transform: uppercase; 2098 `; 2099 title.textContent = "TEXTLUPE PRO"; 2100 const modeSelector = document.createElement("select"); 2101 modeSelector.className = "mode-selector"; 2102 modeSelector.style.cssText = ` 2103 background: ${textColor === "#ffffff" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)"}; 2104 color: ${textColor}; 2105 border: 1px solid ${textColor === "#ffffff" ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)"}; 2106 padding: 3px 8px; 2107 border-radius: 4px; 2108 font-size: 11px; 2109 cursor: pointer; 2110 `; 2111 Object.keys(this.colorModes).forEach((mode) => { 2112 const option = document.createElement("option"); 2113 option.value = mode; 2114 option.textContent = mode.charAt(0).toUpperCase() + mode.slice(1); 2115 option.style.background = "#34495e"; 2116 modeSelector.appendChild(option); 2117 }); 2118 modeSelector.addEventListener("change", (e) => { 2119 this.setColorMode(e.target.value); 2120 }); 2121 leftSide.appendChild(title); 2122 leftSide.appendChild(modeSelector); 2123 const rightSide = document.createElement("div"); 2124 rightSide.style.cssText = "display: flex; align-items: center; gap: 10px;"; 2125 const zoomIndicator = document.createElement("span"); 2126 zoomIndicator.id = "magnifier-zoom"; 2127 zoomIndicator.style.cssText = `
2128 color: ${textColor}; 2129 font-size: 11px; 2130 font-weight: 500; 2131 background: ${textColor === "#ffffff" ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.1)"}; 2132 padding: 3px 8px; 2133 border-radius: 4px; 2134 min-width: 40px; 2135 text-align: center; 2136 `; 2137 zoomIndicator.textContent = "2.0x"; 2138 const ttsButton = document.createElement("button"); 2139 ttsButton.className = "tts-button"; 2140 ttsButton.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 2141 <path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/> 2142 </svg>`; 2143 ttsButton.title = "Text vorlesen (T)"; 2144 ttsButton.style.cssText = ` 2145 background: ${textColor === "#ffffff" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)"}; 2146 color: ${textColor}; 2147 border: 1px solid ${textColor === "#ffffff" ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)"}; 2148 padding: 5px 8px; 2149 border-radius: 4px; 2150 cursor: pointer; 2151 display: flex; 2152 align-items: center; 2153 justify-content: center; 2154 `; 2155 ttsButton.addEventListener("click", () => this.toggleSpeech()); 2156 const pinButton = document.createElement("button"); 2157 pinButton.className = "pin-button"; 2158 pinButton.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 2159 <path d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z"/> 2160 </svg>`; 2161 pinButton.title = "Lupe fixieren (P)"; 2162 pinButton.style.cssText = ` 2163 background: ${textColor === "#ffffff" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)"}; 2164 color: ${textColor}; 2165 border: 1px solid ${textColor === "#ffffff" ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)"}; 2166 padding: 5px 8px; 2167 border-radius: 4px; 2168 cursor: pointer; 2169 display: flex; 2170 align-items: center; 2171 justify-content: center; 2172 `; 2173 pinButton.addEventListener("click", () => this.togglePin()); 2174 rightSide.appendChild(zoomIndicator); 2175 rightSide.appendChild(ttsButton); 2176 rightSide.appendChild(pinButton); 2177 header.appendChild(leftSide); 2178 header.appendChild(rightSide); 2179 this.makeDraggable(header); 2180 return header; 2181 } 2182 createFooter() { 2183 const primaryColor = this.getPrimaryColor(); 2184 const darkerShade = this.darkenColor(primaryColor, 10); 2185 const textColor = this.getContrastColor(primaryColor); 2186 const footer = document.createElement("div"); 2187 footer.className = "magnifier-footer"; 2188 footer.style.cssText = ` 2189 background: linear-gradient(90deg, ${primaryColor} 0%, ${darkerShade} 100%); 2190 padding: 10px 15px; 2191 border-top: 2px solid ${darkerShade}; 2192 display: flex; 2193 justify-content: space-between; 2194 align-items: center; 2195 gap: 15px; 2196 font-size: 11px; 2197 color: ${textColor}; 2198 flex-wrap: wrap; 2199 `; 2200 const wordNav = document.createElement("div"); 2201 wordNav.style.cssText = "display: flex; align-items: center; gap: 10px;"; 2202 const prevWord = document.createElement("button"); 2203 prevWord.innerHTML = "â"; 2204 prevWord.style.cssText = ` 2205 background: ${textColor === "#ffffff" ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.1)"}; 2206 border: 1px solid ${textColor === "#ffffff" ? "rgba(255,255,255,0.3)" : "rgba(0,0,0,0.2)"}; 2207 color: ${textColor}; 2208 padding: 2px 8px; 2209 border-radius: 3px; 2210 cursor: pointer; 2211 transition: all 0.2s; 2212 `; 2213 prevWord.addEventListener("click", () => this.navigateWord(-1)); 2214 prevWord.addEventListener("mouseover", () => { 2215 prevWord.style.background = textColor === "#ffffff" ? "rgba(255,255,255,0.3)" : "rgba(0,0,0,0.2)"; 2216 }); 2217 prevWord.addEventListener("mouseout", () => { 2218 prevWord.style.background = textColor === "#ffffff" ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.1)"; 2219 }); 2220 const wordIndicator = document.createElement("span"); 2221 wordIndicator.id = "word-indicator"; 2222 wordIndicator.textContent = "Wort 1/1"; 2223 wordIndicator.style.color = textColor; 2224 const nextWord = document.createElement("button"); 2225 nextWord.innerHTML = "â¶"; 2226 nextWord.style.cssText = `
2227 background: ${textColor === "#ffffff" ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.1)"}; 2228 border: 1px solid ${textColor === "#ffffff" ? "rgba(255,255,255,0.3)" : "rgba(0,0,0,0.2)"}; 2229 color: ${textColor}; 2230 padding: 2px 8px; 2231 border-radius: 3px; 2232 cursor: pointer; 2233 transition: all 0.2s; 2234 `; 2235 nextWord.addEventListener("click", () => this.navigateWord(1)); 2236 nextWord.addEventListener("mouseover", () => { 2237 nextWord.style.background = textColor === "#ffffff" ? "rgba(255,255,255,0.3)" : "rgba(0,0,0,0.2)"; 2238 }); 2239 nextWord.addEventListener("mouseout", () => { 2240 nextWord.style.background = textColor === "#ffffff" ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.1)"; 2241 }); 2242 wordNav.appendChild(prevWord); 2243 wordNav.appendChild(wordIndicator); 2244 wordNav.appendChild(nextWord); 2245 const shortcuts = document.createElement("div"); 2246 shortcuts.style.cssText = ` 2247 display: flex; 2248 align-items: center; 2249 gap: 8px; 2250 color: ${textColor}; 2251 font-size: 11px; 2252 font-weight: 500; 2253 background: ${textColor === "#ffffff" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)"}; 2254 padding: 4px 10px; 2255 border-radius: 4px; 2256 border: 1px solid ${textColor === "#ffffff" ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.15)"}; 2257 `; 2258 const createShortcutBadge = (key, description) => { 2259 const badge = document.createElement("span"); 2260 badge.style.cssText = ` 2261 display: inline-flex; 2262 align-items: center; 2263 gap: 4px; 2264 padding: 2px 6px; 2265 background: ${textColor === "#ffffff" ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.15)"}; 2266 border-radius: 3px; 2267 white-space: nowrap; 2268 `; 2269 const keySpan = document.createElement("strong"); 2270 keySpan.style.cssText = ` 2271 font-weight: 700; 2272 color: ${textColor}; 2273 background: ${textColor === "#ffffff" ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)"}; 2274 padding: 0 4px; 2275 border-radius: 2px; 2276 font-size: 10px; 2277 `; 2278 keySpan.textContent = key; 2279 const descSpan = document.createElement("span"); 2280 descSpan.style.cssText = ` 2281 font-size: 10px; 2282 opacity: 0.9; 2283 `; 2284 descSpan.textContent = description; 2285 badge.appendChild(keySpan); 2286 badge.appendChild(descSpan); 2287 return badge; 2288 }; 2289 shortcuts.appendChild(createShortcutBadge("+/-", "Zoom")); 2290 shortcuts.appendChild(createShortcutBadge("P", "Pin")); 2291 shortcuts.appendChild(createShortcutBadge("T", "Vorlesen")); 2292 shortcuts.appendChild(createShortcutBadge("ââ", "Navigation")); 2293 shortcuts.appendChild(createShortcutBadge("Esc", "SchlieÃen")); 2294 footer.appendChild(wordNav); 2295 footer.appendChild(shortcuts); 2296 return footer; 2297 } 2298 updateContainerStyle() { 2299 if (!this.container) return; 2300 this.container.style.cssText = ` 2301 position: fixed; 2302 width: ${this.width}px; 2303 min-height: ${this.height}px; 2304 max-height: 500px; 2305 background: white; 2306 border: 2px solid ${this.colorModes[this.currentMode].border}; 2307 border-radius: 12px; 2308 box-shadow: 0 10px 40px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.8) inset; 2309 z-index: 999998; 2310 display: none; 2311 overflow: hidden; 2312 resize: both; 2313 transition: opacity 0.3s ease, transform 0.3s ease; 2314 ${this.isPinned ? "pointer-events: auto;" : "pointer-events: none;"} 2315 `; 2316 } 2317 updateContentStyle() { 2318 if (!this.contentArea) return; 2319 const mode = this.colorModes[this.currentMode]; 2320 this.contentArea.style.cssText = ` 2321 padding: 20px; 2322 font-size: ${this.zoom}em; 2323 line-height: 1.8; 2324 color: ${mode.text}; 2325 background: ${mode.bg}; 2326 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 2327 overflow-y: auto; 2328 max-height: 350px; 2329 word-wrap: break-word; 2330 text-align: left; 2331 position: relative; 2332 `; 2333 } 2334 setColorMode(mode) { 2335 if (!this.colorModes[mode]) return; 2336 this.currentMode = mode; 2337 this.updateContentStyle(); 2338 this.updateContainerStyle(); 2339 } 2340 setZoom(newZoom) {
2341 this.zoom = Math.max(1, Math.min(5, newZoom)); 2342 this.contentArea.style.fontSize = this.zoom + "em"; 2343 const zoomEl = document.getElementById("magnifier-zoom"); 2344 if (zoomEl) zoomEl.textContent = this.zoom.toFixed(1) + "x"; 2345 } 2346 togglePin() { 2347 this.isPinned = !this.isPinned; 2348 this.updateContainerStyle(); 2349 const pinButton = this.container.querySelector(".pin-button"); 2350 if (pinButton) { 2351 pinButton.style.background = this.isPinned ? "rgba(52, 152, 219, 0.3)" : "rgba(255,255,255,0.1)"; 2352 } 2353 } 2354 toggleSpeech() { 2355 if (this.isSpeaking) { 2356 this.stopSpeech(); 2357 } else { 2358 this.startSpeech(); 2359 } 2360 } 2361 startSpeech() { 2362 if (!this.synth || !this.currentText) return; 2363 this.stopSpeech(); 2364 const utterance = new SpeechSynthesisUtterance(this.currentText); 2365 utterance.rate = 0.9; 2366 utterance.pitch = 1; 2367 utterance.volume = 1; 2368 const voices = this.synth.getVoices(); 2369 const germanVoice = voices.find((v) => v.lang.startsWith("de")); 2370 if (germanVoice) utterance.voice = germanVoice; 2371 utterance.onend = () => { 2372 this.isSpeaking = false; 2373 this.updateTTSButton(); 2374 }; 2375 this.synth.speak(utterance); 2376 this.isSpeaking = true; 2377 this.updateTTSButton(); 2378 } 2379 stopSpeech() { 2380 if (this.synth) { 2381 this.synth.cancel(); 2382 this.isSpeaking = false; 2383 this.updateTTSButton(); 2384 } 2385 } 2386 updateTTSButton() { 2387 const button = this.container?.querySelector(".tts-button"); 2388 if (button) { 2389 if (this.isSpeaking) { 2390 button.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 2391 <path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/> 2392 </svg>`; 2393 button.style.background = "rgba(231, 76, 60, 0.3)"; 2394 } else { 2395 button.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 2396 <path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/> 2397 </svg>`; 2398 button.style.background = "rgba(255,255,255,0.1)"; 2399 } 2400 } 2401 } 2402 setText(text) { 2403 if (!text) return; 2404 this.currentText = text; 2405 this.words = text.split(/\s+/).filter((w) => w.length > 0); 2406 this.wordIndex = 0; 2407 this.updateContent(); 2408 const indicator = document.getElementById("word-indicator"); 2409 if (indicator) { 2410 indicator.textContent = `Wort ${this.wordIndex + 1}/${this.words.length}`; 2411 } 2412 } 2413 updateContent() { 2414 if (!this.contentArea || !this.currentText) return; 2415 if (this.words.length > 0) { 2416 const parts = []; 2417 let currentPos = 0; 2418 this.words.forEach((word, index) => { 2419 const wordPos = this.currentText.indexOf(word, currentPos); 2420 if (wordPos > currentPos) { 2421 parts.push({ 2422 text: this.currentText.substring(currentPos, wordPos), 2423 highlighted: false 2424 }); 2425 } 2426 parts.push({ 2427 text: word, 2428 highlighted: index === this.wordIndex 2429 }); 2430 currentPos = wordPos + word.length; 2431 }); 2432 if (currentPos < this.currentText.length) { 2433 parts.push({ 2434 text: this.currentText.substring(currentPos), 2435 highlighted: false 2436 }); 2437 } 2438 this.contentArea.innerHTML = parts.map((part) => { 2439 if (part.highlighted) { 2440 const mode = this.colorModes[this.currentMode]; 2441 return `<span style=" 2442 background: ${mode.highlight}; 2443 padding: 2px 4px; 2444 border-radius: 3px; 2445 font-weight: 600; 2446 animation: pulse 1s ease-in-out; 2447 ">${part.text}</span>`; 2448 } 2449 return part.text; 2450 }).join(""); 2451 } else { 2452 this.contentArea.textContent = this.currentText; 2453 } 2454 } 2455 navigateWord(direction) { 2456 if (this.words.length === 0) return; 2457 this.wordIndex = Math.max(0, Math.min(this.words.length - 1, this.wordIndex + direction)); 2458 this.updateContent(); 2459 const indicator = document.getElementById("word-indicator"); 2460 if (indicator) { 2461 indicator.textContent = `Wort ${this.wordIndex + 1}/${this.words.length}`; 2462 } 2463 if (this.isSpeaking && this.words[this.wordIndex]) { 2464 this.stopSpeech(); 2465 const utterance = new SpeechSynthesisUtterance(this.words[this.wordIndex]); 2466 utterance.rate = 0.8; 2467 this.synth.speak(utterance); 2468 } 2469 } 2470 show(x, y) { 2471 if (!this.container) return; 2472 this.container.style.display = "block"; 2473 if (!this.isPinned) { 2474 const rect = this.container.getBoundingClientRect(); 2475 let posX = x - rect.width / 2; 2476 let posY = y - rect.height - 30; 2477 posX = Math.max(10, Math.min(posX, window.innerWidth - rect.width - 10)); 2478 if (posY < 10) { 2479 posY = y + 30; 2480 } 2481 this.container.style.left = posX + "px"; 2482 this.container.style.top = posY + "px"; 2483 } 2484 this.container.style.opacity = "1"; 2485 this.container.style.transform = "scale(1)"; 2486 } 2487 hide() { 2488 if (!this.container || this.isPinned) return; 2489 this.container.style.opacity = "0"; 2490 this.container.style.transform = "scale(0.95)"; 2491 setTimeout(() => { 2492 if (this.container && !this.isPinned) { 2493 this.container.style.display = "none"; 2494 } 2495 }, 300); 2496 this.stopSpeech(); 2497 } 2498 makeDraggable(handle) { 2499 let isDragging = false;
2500 let startX, startY, initialX, initialY; 2501 handle.addEventListener("mousedown", (e) => { 2502 if (e.target.tagName === "BUTTON" || e.target.tagName === "SELECT") return; 2503 isDragging = true; 2504 startX = e.clientX; 2505 startY = e.clientY; 2506 const rect = this.container.getBoundingClientRect(); 2507 initialX = rect.left; 2508 initialY = rect.top; 2509 e.preventDefault(); 2510 }); 2511 document.addEventListener("mousemove", (e) => { 2512 if (!isDragging) return; 2513 const dx = e.clientX - startX; 2514 const dy = e.clientY - startY; 2515 this.container.style.left = initialX + dx + "px"; 2516 this.container.style.top = initialY + dy + "px"; 2517 }); 2518 document.addEventListener("mouseup", () => { 2519 isDragging = false; 2520 }); 2521 } 2522 initResize(handle) { 2523 let isResizing = false; 2524 let startX, startY, startWidth, startHeight; 2525 handle.addEventListener("mousedown", (e) => { 2526 isResizing = true; 2527 startX = e.clientX; 2528 startY = e.clientY; 2529 startWidth = this.width; 2530 startHeight = this.height; 2531 this.container.style.transition = "none"; 2532 e.preventDefault(); 2533 e.stopPropagation(); 2534 }); 2535 document.addEventListener("mousemove", (e) => { 2536 if (!isResizing) return; 2537 this.width = Math.max(400, Math.min(1e3, startWidth + e.clientX - startX)); 2538 this.height = Math.max(150, Math.min(500, startHeight + e.clientY - startY)); 2539 this.container.style.width = this.width + "px"; 2540 this.container.style.minHeight = this.height + "px"; 2541 }); 2542 document.addEventListener("mouseup", () => { 2543 if (isResizing) { 2544 isResizing = false; 2545 this.container.style.transition = "opacity 0.3s ease, transform 0.3s ease"; 2546 } 2547 }); 2548 } 2549 injectStyles() { 2550 const style = document.createElement("style"); 2551 style.textContent = ` 2552 @keyframes pulse { 2553 0% { transform: scale(1); } 2554 50% { transform: scale(1.05); } 2555 100% { transform: scale(1); } 2556 } 2557 2558 .magnifier-advanced select, 2559 .magnifier-advanced button { 2560 outline: none; 2561 } 2562 2563 .magnifier-advanced button:hover { 2564 opacity: 0.8; 2565 } 2566 2567 .magnifier-advanced button:active { 2568 transform: scale(0.95); 2569 } 2570 2571 .magnifier-content::-webkit-scrollbar {
2572 width: 8px; 2573 } 2574 2575 .magnifier-content::-webkit-scrollbar-track { 2576 background: rgba(0,0,0,0.1); 2577 border-radius: 4px; 2578 } 2579 2580 .magnifier-content::-webkit-scrollbar-thumb { 2581 background: rgba(0,0,0,0.3); 2582 border-radius: 4px; 2583 } 2584 2585 .magnifier-content::-webkit-scrollbar-thumb:hover { 2586 background: rgba(0,0,0,0.5); 2587 } 2588 `; 2589 document.head.appendChild(style); 2590 } 2591 destroy() { 2592 this.stopSpeech(); 2593 if (this.container) { 2594 this.container.remove(); 2595 this.container = null; 2596 this.contentArea = null; 2597 } 2598 } 2599} 2600class SimpleUI { 2601 constructor() { 2602 this.overlay = null; 2603 this.toggleButton = null; 2604 this.menu = null; 2605 this.isOpen = false; 2606 this.stateManager = null; 2607 } 2608 /** 2609 * Initialize the UI 2610 */ 2611 init() { 2612 this.overlay = document.getElementById("a11y-box"); 2613 this.toggleButton = document.getElementById("a11y-toggle"); 2614 this.menu = document.getElementById("accessibility-menu"); 2615 if (!this.overlay || !this.toggleButton) { 2616 console.error("Accessibility UI elements not found"); 2617 return; 2618 } 2619 this.setupEvents(); 2620 this.isOpen = this.overlay.classList.contains("is-open"); 2621 if (document.readyState === "complete" || document.readyState === "interactive") { 2622 setTimeout(() => this.updateUIFromState(), 100); 2623 } else { 2624 document.addEventListener("DOMContentLoaded", () => { 2625 setTimeout(() => this.updateUIFromState(), 100); 2626 }); 2627 } 2628 } 2629 /** 2630 * Setup event handlers 2631 */ 2632 setupEvents() { 2633 this.toggleButton.addEventListener("click", (e) => { 2634 e.preventDefault(); 2635 this.toggle(); 2636 }); 2637 document.addEventListener("accessibility:profile:activated", (e) => { 2638 const { profileKey } = e.detail; 2639 this.updateProfileUI(profileKey, true); 2640 setTimeout(() => { 2641 this.updateFunctionsFromProfile(profileKey); 2642 this.syncUIWithActualState(); 2643 }, 200); 2644 }); 2645 document.addEventListener("accessibility:profile:deactivated", (e) => { 2646 const { profileKey } = e.detail; 2647 this.updateProfileUI(profileKey, false); 2648 this.resetFunctionsUI(); 2649 }); 2650 document.querySelectorAll("[data-profile]").forEach((profileBtn) => { 2651 profileBtn.addEventListener("click", (e) => { 2652 e.preventDefault(); 2653 e.stopPropagation(); 2654 const profileKey = profileBtn.getAttribute("data-profile"); 2655 if (window.ModAccessibilityFunctions?.toggleProfile) { 2656 window.ModAccessibilityFunctions.toggleProfile(profileKey); 2657 } 2658 }); 2659 }); 2660 document.querySelectorAll("[data-function]").forEach((funcBtn) => { 2661 funcBtn.addEventListener("click", (e) => { 2662 const isLevel = e.target.matches("[data-level]") || e.target.closest("[data-level]"); 2663 if (isLevel) return; 2664 const hasLevels = funcBtn.querySelector(".level-controls-horizontal") || funcBtn.querySelector("[data-level]"); 2665 if (hasLevels) { 2666 e.preventDefault(); 2667 e.stopPropagation(); 2668 return; 2669 } 2670 e.preventDefault(); 2671 e.stopPropagation(); 2672 const funcName = funcBtn.getAttribute("data-function"); 2673 this.toggleFunction(funcName, funcBtn); 2674 }); 2675 }); 2676 document.querySelectorAll("[data-level]").forEach((levelBtn) => { 2677 levelBtn.addEventListener("click", (e) => { 2678 e.preventDefault(); 2679 e.stopPropagation(); 2680 const funcName = levelBtn.closest("[data-function]")?.getAttribute("data-function"); 2681 const level = parseInt(levelBtn.getAttribute("data-level")); 2682 if (funcName && !isNaN(level)) { 2683 this.setFunctionLevel(funcName, level, levelBtn); 2684 } 2685 }); 2686 }); 2687 const resetBtn = document.querySelector('[data-action="reset"]'); 2688 if (resetBtn) { 2689 resetBtn.addEventListener("click", (e) => { 2690 e.preventDefault(); 2691 this.resetAll(); 2692 }); 2693 } 2694 this.setupKeyboardShortcuts(); 2695 document.addEventListener("click", (e) => { 2696 if (this.isOpen && !this.overlay.contains(e.target)) { 2697 this.close(); 2698 } 2699 }); 2700 document.addEventListener("keydown", (e) => { 2701 if (e.key === "Escape" && this.isOpen) { 2702 this.close(); 2703 } 2704 }); 2705 } 2706 /** 2707 * Toggle overlay visibility 2708 */ 2709 toggle() { 2710 if (this.isOpen) { 2711 this.close(); 2712 } else { 2713 this.open(); 2714 } 2715 } 2716 /** 2717 * Open overlay 2718 */ 2719 open() { 2720 if (!this.overlay) return; 2721 this.overlay.classList.remove("is-closed"); 2722 this.overlay.classList.add("is-open"); 2723 this.isOpen = true; 2724 if (this.menu) { 2725 this.menu.style.display = "block"; 2726 } 2727 } 2728 /** 2729 * Close overlay 2730 */ 2731 close() { 2732 if (!this.overlay) return; 2733 this.overlay.classList.remove("is-open"); 2734 this.overlay.classList.add("is-closed"); 2735 this.isOpen = false;
2736 if (this.menu) { 2737 this.menu.style.display = "none"; 2738 } 2739 } 2740 /** 2741 * Setup keyboard shortcuts 2742 */ 2743 setupKeyboardShortcuts() { 2744 document.addEventListener("keydown", (e) => { 2745 if (e.altKey && e.key === "a") { 2746 e.preventDefault(); 2747 this.toggle(); 2748 } 2749 if (e.altKey && e.key === "r") { 2750 e.preventDefault(); 2751 this.resetAll(); 2752 } 2753 }); 2754 } 2755 /** 2756 * Toggle a profile (activate/deactivate) 2757 */ 2758 toggleProfile(profileKey) { 2759 if (window.ModAccessibilityFunctions?.toggleProfile) { 2760 window.ModAccessibilityFunctions.toggleProfile(profileKey); 2761 } 2762 } 2763 /** 2764 * Update profile UI element 2765 */ 2766 updateProfileUI(profileKey, isActive) { 2767 const profileElement = document.querySelector(`[data-profile="${profileKey}"]`); 2768 if (!profileElement) return; 2769 if (isActive) { 2770 profileElement.classList.add("is-active"); 2771 profileElement.setAttribute("data-active", "true"); 2772 } else { 2773 profileElement.classList.remove("is-active"); 2774 profileElement.setAttribute("data-active", "false"); 2775 } 2776 const checkbox = profileElement.querySelector('input[type="checkbox"]'); 2777 if (checkbox) { 2778 checkbox.checked = isActive; 2779 } 2780 const controlToggle = profileElement.querySelector(".control-toggle"); 2781 if (controlToggle) { 2782 controlToggle.setAttribute("data-active", isActive); 2783 if (isActive) { 2784 controlToggle.classList.add("is-active"); 2785 } else { 2786 controlToggle.classList.remove("is-active"); 2787 } 2788 } 2789 const toggleSwitch = profileElement.querySelector(".toggle-switch"); 2790 if (toggleSwitch) { 2791 if (isActive) { 2792 toggleSwitch.classList.add("is-active"); 2793 } else { 2794 toggleSwitch.classList.remove("is-active"); 2795 } 2796 } 2797 const statusLabel = profileElement.querySelector(".toggle-status-label"); 2798 if (statusLabel) { 2799 if (isActive) { 2800 statusLabel.classList.add("is-active"); 2801 } else { 2802 statusLabel.classList.remove("is-active"); 2803 } 2804 } 2805 if (isActive) { 2806 document.querySelectorAll("[data-profile]").forEach((otherProfile) => { 2807 if (otherProfile !== profileElement) { 2808 const otherKey = otherProfile.getAttribute("data-profile"); 2809 if (otherKey !== profileKey) { 2810 otherProfile.classList.remove("is-active"); 2811 otherProfile.setAttribute("data-active", "false"); 2812 const otherCheckbox = otherProfile.querySelector('input[type="checkbox"]'); 2813 if (otherCheckbox) otherCheckbox.checked = false; 2814 const otherStatusLabel = otherProfile.querySelector(".toggle-status-label"); 2815 if (otherStatusLabel) otherStatusLabel.classList.remove("is-active"); 2816 } 2817 } 2818 }); 2819 } 2820 } 2821 /** 2822 * Toggle a function 2823 */ 2824 toggleFunction(funcName, button) { 2825 const isActive = button.getAttribute("data-active") === "true"; 2826 const newState = !isActive; 2827 if (newState && (funcName === "readable-font" || funcName === "dyslexic-font")) { 2828 const otherFontName = funcName === "readable-font" ? "dyslexic-font" : "readable-font"; 2829 const otherButton = document.querySelector(`[data-function="${otherFontName}"]`); 2830 if (otherButton && otherButton.getAttribute("data-active") === "true") { 2831 this.toggleFunction(otherFontName, otherButton); 2832 } 2833 } 2834 if (newState && funcName === "invert-colors") { 2835 const brightnessButtons = document.querySelectorAll('[data-function="brightness"] [data-level]'); 2836 brightnessButtons.forEach((btn) => { 2837 btn.classList.remove("active"); 2838 btn.setAttribute("aria-pressed", "false"); 2839 }); 2840 const brightnessContainer = document.querySelector('[data-function="brightness"]'); 2841 if (brightnessContainer) { 2842 brightnessContainer.setAttribute("data-active", "false"); 2843 brightnessContainer.classList.remove("is-active"); 2844 } 2845 const contrastButtons = document.querySelectorAll('[data-function="contrast"] [data-level]'); 2846 contrastButtons.forEach((btn) => { 2847 btn.classList.remove("active"); 2848 btn.setAttribute("aria-pressed", "false"); 2849 }); 2850 const contrastContainer = document.querySelector('[data-function="contrast"]'); 2851 if (contrastContainer) { 2852 contrastContainer.setAttribute("data-active", "false"); 2853 contrastContainer.classList.remove("is-active"); 2854 } 2855 } 2856 button.setAttribute("data-active", newState); 2857 button.classList.toggle("is-active", newState); 2858 const checkbox = button.querySelector('input[type="checkbox"]'); 2859 if (checkbox) { 2860 checkbox.checked = newState; 2861 } 2862 const controlToggle = button.querySelector(".control-toggle"); 2863 if (controlToggle) { 2864 controlToggle.setAttribute("data-active", newState); 2865 if (newState) { 2866 controlToggle.classList.add("is-active"); 2867 } else { 2868 controlToggle.classList.remove("is-active"); 2869 } 2870 } 2871 const toggleSwitch = button.querySelector(".toggle-switch"); 2872 if (toggleSwitch) { 2873 if (newState) { 2874 toggleSwitch.classList.add("is-active"); 2875 } else { 2876 toggleSwitch.classList.remove("is-active"); 2877 } 2878 } 2879 const statusLabel = button.querySelector(".toggle-status-label"); 2880 if (statusLabel) { 2881 if (newState) { 2882 statusLabel.classList.add("is-active"); 2883 } else { 2884 statusLabel.classList.remove("is-active"); 2885 } 2886 } 2887 if (window.ModAccessibilityFunctions?.setFunctionState) { 2888 window.ModAccessibilityFunctions.setFunctionState(funcName, newState); 2889 } 2890 } 2891 /** 2892 * Set function level 2893 */ 2894 setFunctionLevel(funcName, level, button) { 2895 if ((funcName === "brightness" || funcName === "contrast") && level > 0) { 2896 const invertColorsButton = document.querySelector('[data-function="invert-colors"]'); 2897 if (invertColorsButton && invertColorsButton.getAttribute("data-active") === "true") { 2898 this.toggleFunction("invert-colors", invertColorsButton); 2899 } 2900 } 2901 const container = button.closest("[data-function]"); 2902 if (container) { 2903 const allLevelButtons = container.querySelectorAll("[data-level]"); 2904 const currentActive = container.querySelector("[data-level].active"); 2905 const currentLevel = currentActive ? parseInt(currentActive.getAttribute("data-level")) : 0; 2906 allLevelButtons.forEach((btn) => { 2907 btn.classList.remove("active"); 2908 btn.setAttribute("aria-pressed", "false"); 2909 }); 2910 if (currentLevel === level) { 2911 level = 0; 2912 } else { 2913 button.classList.add("active"); 2914 button.setAttribute("aria-pressed", "true"); 2915 } 2916 } 2917 if (window.ModAccessibilityFunctions?.setFunctionState) { 2918 window.ModAccessibilityFunctions.setFunctionState(funcName, level); 2919 } 2920 } 2921 /** 2922 * Reset all settings 2923 */ 2924 resetAll() { 2925 document.querySelectorAll("[data-profile], [data-function]").forEach((btn) => { 2926 btn.classList.remove("is-active"); 2927 btn.setAttribute("data-active", "false"); 2928 const statusLabel = btn.querySelector(".toggle-status-label"); 2929 if (statusLabel) { 2930 statusLabel.classList.remove("is-active"); 2931 } 2932 }); 2933 document.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => { 2934 checkbox.checked = false;
2935 }); 2936 document.querySelectorAll("[data-level]").forEach((btn) => { 2937 btn.classList.remove("active"); 2938 }); 2939 if (window.ModAccessibilityFunctions?.resetAll) { 2940 window.ModAccessibilityFunctions.resetAll(); 2941 } 2942 } 2943 /** 2944 * Sync UI with actual state from state manager 2945 */ 2946 syncUIWithActualState() { 2947 if (this.stateManager) { 2948 const state = this.stateManager.getState(); 2949 if (state && state.functions) { 2950 Object.entries(state.functions).forEach(([funcName, value]) => { 2951 this.updateFunctionUI(funcName, value); 2952 }); 2953 } 2954 } 2955 } 2956 /** 2957 * Update a single function's UI 2958 */ 2959 updateFunctionUI(funcName, value) { 2960 const funcElement = document.querySelector(`[data-function="${funcName}"]`); 2961 if (funcElement) { 2962 const isActive = value === true || typeof value === "number" && value > 0; 2963 funcElement.setAttribute("data-active", isActive ? "true" : "false"); 2964 if (isActive) { 2965 funcElement.classList.add("is-active"); 2966 } else { 2967 funcElement.classList.remove("is-active"); 2968 } 2969 const checkbox = funcElement.querySelector('input[type="checkbox"]'); 2970 if (checkbox) { 2971 checkbox.checked = isActive; 2972 } 2973 const controlToggle = funcElement.querySelector(".control-toggle"); 2974 if (controlToggle) { 2975 controlToggle.setAttribute("data-active", isActive ? "true" : "false"); 2976 if (isActive) { 2977 controlToggle.classList.add("is-active"); 2978 } else { 2979 controlToggle.classList.remove("is-active"); 2980 } 2981 } 2982 const toggleSwitch = funcElement.querySelector(".toggle-switch"); 2983 if (toggleSwitch) { 2984 if (isActive) { 2985 toggleSwitch.classList.add("is-active"); 2986 } else { 2987 toggleSwitch.classList.remove("is-active"); 2988 } 2989 } 2990 const statusLabel = funcElement.querySelector(".toggle-status-label"); 2991 if (statusLabel) { 2992 if (isActive) { 2993 statusLabel.classList.add("is-active"); 2994 } else { 2995 statusLabel.classList.remove("is-active"); 2996 } 2997 } 2998 if (typeof value === "number") { 2999 funcElement.querySelectorAll("[data-level]").forEach((levelBtn) => { 3000 const level = parseInt(levelBtn.getAttribute("data-level")); 3001 if (value > 0 && level === value) { 3002 levelBtn.classList.add("active"); 3003 levelBtn.setAttribute("aria-pressed", "true"); 3004 } else { 3005 levelBtn.classList.remove("active"); 3006 levelBtn.setAttribute("aria-pressed", "false"); 3007 } 3008 }); 3009 } 3010 } 3011 } 3012 /** 3013 * Update function UI elements based on profile settings 3014 */ 3015 updateFunctionsFromProfile(profileKey) { 3016 if (this.stateManager) { 3017 const state = this.stateManager.getState(); 3018 if (state.functions) { 3019 Object.entries(state.functions).forEach(([funcName, value]) => { 3020 this.updateFunctionUI(funcName, value); 3021 }); 3022 } 3023 } 3024 } 3025 /** 3026 * Reset all function UI elements 3027 */ 3028 resetFunctionsUI() { 3029 document.querySelectorAll("[data-function]").forEach((funcElement) => { 3030 funcElement.classList.remove("is-active"); 3031 funcElement.setAttribute("data-active", "false"); 3032 const toggle = funcElement.querySelector(".control-toggle"); 3033 if (toggle) { 3034 toggle.classList.remove("is-active"); 3035 toggle.setAttribute("data-active", "false"); 3036 } 3037 const checkbox = funcElement.querySelector('input[type="checkbox"]'); 3038 if (checkbox) { 3039 checkbox.checked = false; 3040 } 3041 const statusLabel = funcElement.querySelector(".toggle-status-label"); 3042 if (statusLabel) { 3043 statusLabel.classList.remove("is-active"); 3044 } 3045 const levelIndicator = funcElement.querySelector(".level-indicator"); 3046 if (levelIndicator) { 3047 levelIndicator.textContent = "0"; 3048 } 3049 funcElement.querySelectorAll("[data-level]").forEach((levelBtn) => { 3050 levelBtn.classList.remove("active"); 3051 levelBtn.setAttribute("aria-pressed", "false"); 3052 }); 3053 }); 3054 } 3055 /** 3056 * Update UI from saved state on initialization 3057 */ 3058 updateUIFromState() { 3059 if (this.stateManager) { 3060 const state = this.stateManager.getState(); 3061 if (state.profiles) { 3062 const activeProfiles = Object.entries(state.profiles).filter(([key, active]) => active); 3063 if (activeProfiles.length > 1) { 3064 console.warn("Multiple profiles were active, keeping only the first one:", activeProfiles[0][0]); 3065 const firstActiveKey = activeProfiles[0][0];
3066 Object.entries(state.profiles).forEach(([profileKey, wasActive]) => { 3067 const profileBtn = document.querySelector(`[data-profile="${profileKey}"]`); 3068 if (profileBtn) { 3069 const shouldBeActive = profileKey === firstActiveKey; 3070 if (shouldBeActive) { 3071 profileBtn.classList.add("is-active"); 3072 profileBtn.setAttribute("data-active", "true"); 3073 } else { 3074 profileBtn.classList.remove("is-active"); 3075 profileBtn.setAttribute("data-active", "false"); 3076 } 3077 const checkbox = profileBtn.querySelector('input[type="checkbox"]'); 3078 if (checkbox) { 3079 checkbox.checked = shouldBeActive; 3080 } 3081 const statusLabel = profileBtn.querySelector(".toggle-status-label"); 3082 if (statusLabel) { 3083 if (shouldBeActive) { 3084 statusLabel.classList.add("is-active"); 3085 } else { 3086 statusLabel.classList.remove("is-active"); 3087 } 3088 } 3089 } 3090 }); 3091 Object.keys(state.profiles).forEach((key) => { 3092 const shouldBeActive = key === firstActiveKey; 3093 if (state.profiles[key] !== shouldBeActive) { 3094 this.stateManager.setProfileState(key, shouldBeActive); 3095 } 3096 }); 3097 } else { 3098 Object.entries(state.profiles).forEach(([profileKey, active]) => {
3099 const profileBtn = document.querySelector(`[data-profile="${profileKey}"]`); 3100 if (profileBtn) { 3101 if (active) { 3102 profileBtn.classList.add("is-active"); 3103 profileBtn.setAttribute("data-active", "true"); 3104 } else { 3105 profileBtn.classList.remove("is-active"); 3106 profileBtn.setAttribute("data-active", "false"); 3107 } 3108 const checkbox = profileBtn.querySelector('input[type="checkbox"]'); 3109 if (checkbox) { 3110 checkbox.checked = active; 3111 } 3112 const statusLabel = profileBtn.querySelector(".toggle-status-label"); 3113 if (statusLabel) { 3114 if (active) { 3115 statusLabel.classList.add("is-active"); 3116 } else { 3117 statusLabel.classList.remove("is-active"); 3118 } 3119 } 3120 } 3121 }); 3122 } 3123 } 3124 if (state.functions) { 3125 Object.entries(state.functions).forEach(([funcName, value]) => { 3126 this.updateFunctionUI(funcName, value); 3127 }); 3128 } 3129 } 3130 } 3131} 3132const simpleUI = new SimpleUI(); 3133class AccessibilityModule { 3134 constructor() { 3135 this.version = MODULE_CONFIG.version; 3136 this.modules = /* @__PURE__ */ new Map(); 3137 this.ensureWrapper(); 3138 this.stateManager = stateManager; 3139 this.storage = storage; 3140 this.ui = simpleUI; 3141 this.modules.set("simple", simpleModules); 3142 this.modules.set("profiles", profilesManager); 3143 this.advancedMagnifier = new AdvancedMagnifier(); 3144 this.modules.set("advancedMagnifier", this.advancedMagnifier); 3145 profilesManager.stateManager = stateManager; 3146 stateManager.state = storage.loadSettings(); 3147 this.setupEventListeners(); 3148 simpleUI.stateManager = stateManager; 3149 simpleUI.init(); 3150 this.modules.set("ui", simpleUI); 3151 this.createGlobalInterface(); 3152 this.loadSettings(); 3153 setTimeout(() => { 3154 if (simpleUI && simpleUI.updateUIFromState) { 3155 simpleUI.stateManager = stateManager; 3156 simpleUI.updateUIFromState(); 3157 } 3158 }, 100); 3159 } 3160 /** 3161 * Ensure wrapper element exists 3162 * Creates a wrapper around body content if it doesn't exist 3163 */ 3164 ensureWrapper() { 3165 let wrapper = document.getElementById("site-content-wrapper"); 3166 if (!wrapper) { 3167 wrapper = document.createElement("div"); 3168 wrapper.id = "site-content-wrapper"; 3169 const bodyChildren = Array.from(document.body.children); 3170 bodyChildren.forEach((child) => { 3171 if (child.tagName !== "SCRIPT" && child.tagName !== "STYLE" && child.id !== "a11y-box" && !child.classList.contains("a11y-box")) { 3172 wrapper.appendChild(child); 3173 } 3174 }); 3175 document.body.insertBefore(wrapper, document.body.firstChild); 3176 } 3177 } 3178 /** 3179 * Set up event listeners 3180 */ 3181 setupEventListeners() { 3182 stateManager.addEventListener(EVENTS.FUNCTION_CHANGED, (event) => { 3183 this.handleFunctionChange(event.detail); 3184 }); 3185 stateManager.addEventListener(EVENTS.PROFILE_CHANGED, (event) => { 3186 this.handleProfileChange(event.detail); 3187 }); 3188 stateManager.addEventListener(EVENTS.SETTINGS_RESET, () => { 3189 this.handleSettingsReset(); 3190 }); 3191 } 3192 /** 3193 * Handle function state changes 3194 * @param {Object} detail - Change details 3195 */ 3196 handleFunctionChange(detail) { 3197 const { functionName, value } = detail; 3198 this.applyFunctionCSS(functionName, value); 3199 } 3200 /** 3201 * Handle profile state changes 3202 * @param {Object} detail - Change details 3203 */ 3204 handleProfileChange(detail) { 3205 const { profileKey, active, functions } = detail; 3206 if (active && functions) { 3207 Object.entries(functions).forEach(([functionName, value]) => { 3208 this.applyFunctionCSS(functionName, value); 3209 }); 3210 } 3211 } 3212 /** 3213 * Handle settings reset 3214 */ 3215 handleSettingsReset() { 3216 const styleElement = document.getElementById("a11y-dynamic-styles"); 3217 if (styleElement) { 3218 styleElement.textContent = ""; 3219 } 3220 try { 3221 simpleModules.reset(); 3222 } catch (e) { 3223 console.error(`Error resetting modules: ${e.message}`); 3224 } 3225 } 3226 /** 3227 * Apply CSS for a specific function 3228 * @param {string} functionName - Function name 3229 * @param {*} value - Function value 3230 */ 3231 applyFunctionCSS(functionName, value) { 3232 simpleModules.apply(functionName, value); 3233 } 3234 /** 3235 * Create global interface for backward compatibility 3236 */ 3237 createGlobalInterface() { 3238 if (typeof window === "undefined") return; 3239 window.ModAccessibilityFunctions = { 3240 // State management 3241 getState: () => stateManager.getState(), 3242 getFunctionState: (name) => stateManager.getFunctionState(name), 3243 setFunctionState: (name, value) => stateManager.setFunctionState(name, value), 3244 // Profile management 3245 activateProfile: (profileKey) => profilesManager.activateProfile(profileKey), 3246 deactivateProfile: (profileKey) => profilesManager.deactivateProfile(profileKey), 3247 toggleProfile: (profileKey) => profilesManager.toggleProfile(profileKey), 3248 getActiveProfile: () => stateManager.getActiveProfile(), 3249 // Settings 3250 resetAll: () => stateManager.resetAll(), 3251 loadSettings: () => storage.loadSettings(), 3252 saveSettings: (settings) => storage.saveSettings(settings), 3253 // Module reference 3254 module: this, 3255 version: this.version, 3256 // Advanced magnifier reference 3257 advancedMagnifier: this.advancedMagnifier 3258 }; 3259 } 3260 /** 3261 * Load saved settings 3262 */ 3263 loadSettings() { 3264 const settings = stateManager.state; 3265 const activeProfile = stateManager.getActiveProfile(); 3266 if (settings.functions) {
3267 Object.entries(settings.functions).forEach(([name, value]) => { 3268 if (value !== void 0 && value !== null && value !== 0 && value !== false) { 3269 if (!activeProfile && settings.profiles) { 3270 const anyProfileActive = Object.values(settings.profiles).some((p) => p === true); 3271 if (!anyProfileActive) { 3272 return; 3273 } 3274 } 3275 this.applyFunctionCSS(name, value); 3276 } 3277 }); 3278 } 3279 return settings; 3280 } 3281 /** 3282 * Save current settings 3283 */ 3284 saveSettings() { 3285 const state = stateManager.getState(); 3286 storage.saveSettings(state); 3287 } 3288} 3289const accessibilityModule = new AccessibilityModule(); 3290if (typeof window !== "undefined") { 3291 window.ModAccessibilityFunctions = window.ModAccessibilityFunctions || accessibilityModule; 3292 window.Accessibility = accessibilityModule; 3293} 3294export { 3295 accessibilityModule as default, 3296 profilesManager, 3297 simpleModules, 3298 stateManager, 3299 storage, 3300 simpleUI as ui 3301}; 3302//# sourceMappingURL=accessibility.js.map
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.