1/** 2 * Frontend JavaScript for the Open Accessibility plugin 3 * 4 * Handles the accessibility widget functionality 5 */ 6 7(function($) { 8 'use strict'; 9 10 // Store state in local storage - site-wide key 11 const storageKey = 'open-accessibility-settings'; 12 const DEFAULT_ACCESSIBILITY_STATE = { 13 active: false, 14 contrast: '', 15 grayscale: false, 16 textSize: 0, 17 selectedFont: 'default', 18 linksUnderline: false, 19 hideImages: false, 20 readingGuide: false, 21 focusOutline: false, 22 lineHeightLevel: 0, 23 textAlign: '', 24 pauseAnimations: false, 25 letterSpacingLevel: 0, 26 wordSpacingLevel: 0 27 }; 28 let accessibilityState = Object.assign({}, DEFAULT_ACCESSIBILITY_STATE); 29 30 const MAX_SPACING_LEVEL = 3; 31 const MAX_TEXT_SIZE = 5; 32 const VALID_CONTRAST_MODES = ['', 'high', 'negative', 'light', 'dark']; 33 const VALID_FONT_VALUES = ['default', 'atkinson', 'opendyslexic']; 34 const VALID_TEXT_ALIGN_VALUES = ['', 'left', 'center', 'right']; 35 36 let isShortcodeEmbed = false; 37 const DEFAULT_TARGET_CONFIG = { 38 roots: [ 39 'main', 40 'article', 41 '[role="main"]', 42 '.entry-content', 43 '.post-content', 44 '.page-content', 45 '.wp-block-post-content', 46 '.site-content', 47 '.site-main', 48 '#content', 49 '#primary' 50 ], 51 groups: { 52 readable_text: [ 53 'p', 54 'li', 55 'blockquote', 56 'dd', 57 'dt', 58 'figcaption', 59 'caption', 60 'td', 61 'th', 62 'label' 63 ], 64 headings: [ 65 'h1', 66 'h2', 67 'h3', 68 'h4', 69 'h5', 70 'h6' 71 ], 72 links: [ 73 'a[href]' 74 ], 75 media: [ 76 'img', 77 'picture', 78 'video', 79 'audio', 80 'iframe', 81 'embed', 82 'object', 83 'svg', 84 'canvas' 85 ], 86 interactive: [ 87 'a[href]', 88 'button', 89 'input', 90 'select', 91 'textarea', 92 'summary', 93 '[tabindex]:not([tabindex="-1"])' 94 ] 95 }, 96 layout_containers: [ 97 '[data-oa-relax-layout]' 98 ], 99 excluded: [ 100 '.open-accessibility-widget-wrapper', 101 '.open-accessibility-reading-guide', 102 '.open-accessibility-skip-to-content-link', 103 '.open-accessibility-skip-to-content-backdrop', 104 '.open-accessibility-ignore', 105 '[data-oa-ignore]', 106 'aside', 107 '[role="complementary"]', 108 '.sidebar', 109 '.sidebar-primary', 110 '.sidebar-secondary', 111 '.widget', 112 '.widget-area', 113 'nav', 114 '.nav', 115 '.menu', 116 '.navigation', 117 '.pagination', 118 '.breadcrumbs', 119 '.breadcrumb', 120 'code', 121 'pre', 122 'kbd', 123 'samp', 124 '.screen-reader-text' 125 ] 126 }; 127 const TEXT_SIZE_MULTIPLIERS = [1, 1.1, 1.2, 1.3, 1.4, 1.5]; 128 const LINE_HEIGHT_MULTIPLIERS = [0, 1.6, 1.8, 2.0]; 129 const LETTER_SPACING_STEPS = [0, 0.12, 0.18, 0.24]; 130 const WORD_SPACING_STEPS = [0, 0.16, 0.24, 0.32]; 131 132 let targetResolver = null; 133 134 function clampLevel(value, maxLevel) { 135 const parsed = Number.parseInt(value, 10); 136 137 if (!Number.isFinite(parsed)) { 138 return 0; 139 } 140 141 return Math.min(Math.max(parsed, 0), maxLevel); 142 } 143 144 function normalizeChoice(value, validValues, fallback) { 145 return validValues.includes(value) ? value : fallback; 146 } 147 148 function normalizeAccessibilityState(state) { 149 const source = state && typeof state === 'object' ? state : {}; 150 151 return { 152 active: Boolean(source.active),
153 contrast: normalizeChoice(source.contrast, VALID_CONTRAST_MODES, ''), 154 grayscale: Boolean(source.grayscale), 155 textSize: clampLevel(source.textSize, MAX_TEXT_SIZE), 156 selectedFont: normalizeChoice(source.selectedFont, VALID_FONT_VALUES, 'default'), 157 linksUnderline: Boolean(source.linksUnderline), 158 hideImages: Boolean(source.hideImages), 159 readingGuide: Boolean(source.readingGuide), 160 focusOutline: Boolean(source.focusOutline), 161 lineHeightLevel: clampLevel(source.lineHeightLevel, MAX_SPACING_LEVEL), 162 textAlign: normalizeChoice(source.textAlign, VALID_TEXT_ALIGN_VALUES, ''), 163 pauseAnimations: Boolean(source.pauseAnimations), 164 letterSpacingLevel: clampLevel(source.letterSpacingLevel, MAX_SPACING_LEVEL), 165 wordSpacingLevel: clampLevel(source.wordSpacingLevel, MAX_SPACING_LEVEL) 166 }; 167 } 168 169 function isDebugStorageEnabled() { 170 try { 171 return typeof localStorage !== 'undefined' && 172 localStorage.getItem('openAccessibilityDebug') === '1'; 173 } catch (error) { 174 return false; 175 } 176 } 177 178 const debugEnabled = Boolean( 179 typeof open_accessibility_data !== 'undefined' && 180 open_accessibility_data && 181 open_accessibility_data.options && 182 open_accessibility_data.options.debug 183 ) || isDebugStorageEnabled(); 184 185 function normalizeSelectorList(value, fallback) { 186 const fallbackSelectors = Array.isArray(fallback) ? fallback : []; 187 188 if (!Array.isArray(value)) { 189 return fallbackSelectors.slice(); 190 } 191 192 const selectors = value 193 .filter((selector) => typeof selector === 'string' && selector.trim().length > 0) 194 .map((selector) => selector.trim()); 195 196 return Array.from(new Set(selectors)); 197 } 198 199 function normalizeTargetConfig(config) { 200 const source = config && typeof config === 'object' ? config : {}; 201 const hasOwn = Object.prototype.hasOwnProperty; 202 const hasGroups = hasOwn.call(source, 'groups') && source.groups && typeof source.groups === 'object'; 203 const sourceGroups = hasGroups ? source.groups : DEFAULT_TARGET_CONFIG.groups; 204 const normalizedGroups = {}; 205 206 Object.keys(sourceGroups).forEach((groupName) => { 207 normalizedGroups[groupName] = normalizeSelectorList( 208 sourceGroups[groupName], 209 hasGroups ? [] : DEFAULT_TARGET_CONFIG.groups[groupName] 210 ); 211 }); 212 213 const roots = normalizeSelectorList(source.roots, DEFAULT_TARGET_CONFIG.roots); 214 const hasExplicitEmptyRoots = hasOwn.call(source, 'roots') && 215 Array.isArray(source.roots) && 216 roots.length === 0; 217 218 return { 219 roots, 220 groups: normalizedGroups, 221 layoutContainers: normalizeSelectorList(source.layout_containers, DEFAULT_TARGET_CONFIG.layout_containers), 222 excluded: normalizeSelectorList(source.excluded, DEFAULT_TARGET_CONFIG.excluded), 223 useBodyFallback: !hasExplicitEmptyRoots 224 }; 225 } 226 227 const targetConfig = normalizeTargetConfig( 228 typeof open_accessibility_data !== 'undefined' && 229 open_accessibility_data && 230 open_accessibility_data.options && 231 open_accessibility_data.options.target_config 232 ? open_accessibility_data.options.target_config 233 : {} 234 ); 235 236 function exposeOpenAccessibilityApi() { 237 const api = window.OpenAccessibility || {}; 238 239 api.refresh = function() { 240 targetResolver.refresh(); 241 applyState(); 242 }; 243 api.getState = function() { 244 return $.extend(true, {}, accessibilityState); 245 }; 246 api.setState = function(partialState) { 247 if (!partialState || typeof partialState !== 'object') { 248 return; 249 } 250 251 accessibilityState = normalizeAccessibilityState($.extend({}, accessibilityState, partialState)); 252 saveState(); 253 applyState(); 254 }; 255 api.getTargets = function(groupName) { 256 return targetResolver.getTargets(groupName); 257 }; 258 api.debug = function() { 259 return { 260 state: $.extend(true, {}, accessibilityState), 261 targets: targetResolver.getSummary() 262 }; 263 }; 264 265 window.OpenAccessibility = api; 266 } 267 268 function dispatchReadyEvent() { 269 dispatchOpenAccessibilityEvent('openAccessibility:ready', window.OpenAccessibility.debug()); 270 } 271 272 // Initialize 273 function initAccessibility() { 274 // Load saved state 275 loadState(); 276 277 // Detect if widget is embedded via shortcode (inline positioning) 278 isShortcodeEmbed = $('.open-accessibility-widget-wrapper').closest('.open-accessibility-shortcode').length > 0; 279 280 targetResolver = createTargetResolver(targetConfig); 281 exposeOpenAccessibilityApi(); 282 283 // Create reading guide element 284 if ($('.open-accessibility-reading-guide').length === 0) { 285 $('body').append('<div class="open-accessibility-reading-guide"></div>'); 286 } 287 288 // Apply saved state 289 applyState(); 290 applyLinkTargetPolicy(); 291 initSkipLink(); 292 dispatchReadyEvent(); 293 294 // Button click handler 295 $('.open-accessibility-toggle-button').on('click', toggleAccessibilityPanel); 296 297 // Close button 298 $('.open-accessibility-close').on('click', closeAccessibilityPanel); 299 300 // Hide widget button 301 $('.open-accessibility-hide-widget').on('click', hideAccessibilityWidget); 302 303 // Action buttons 304 $('.open-accessibility-action-button').on('click', function(e) { 305 handleActionButton.call(this, e); 306 }); 307
308 // Reading guide mouse movement 309 $(document).on('mousemove', handleReadingGuide); 310 311 // Close panel when clicking outside 312 $(document).on('click', function(e) { 313 const $panel = $('.open-accessibility-widget-panel'); 314 const $button = $('.open-accessibility-toggle-button'); 315 if ( 316 $panel.hasClass('oa-panel-is-active') && 317 !$(e.target).closest($panel).length && 318 !$(e.target).closest($button).length 319 ) { 320 closeAccessibilityPanel(); 321 } 322 }); 323 324 // Keyboard shortcuts 325 $(document).on('keydown', function(e) { 326 // ESC key closes the panel 327 if (e.key === 'Escape' && $('.open-accessibility-widget-panel').hasClass('oa-panel-is-active')) { 328 closeAccessibilityPanel(); 329 $('.open-accessibility-toggle-button').trigger('focus'); 330 } 331 }); 332 333 // Ensure widget wrapper stays in viewport on scroll 334 $(window).on('scroll', function() { 335 if (isShortcodeEmbed) { 336 // Re-resolve placement so an open panel stays in view while 337 // the embedded toggle moves with the page 338 if ($('.open-accessibility-widget-panel').hasClass('oa-panel-is-active')) { 339 positionShortcodePanel(); 340 } 341 return; 342 } 343 344 const $widgetWrapper = $('.open-accessibility-widget-wrapper'); 345 const $panel = $('.open-accessibility-widget-panel'); 346 const isMobile = window.innerWidth < 768; 347 348 if (!($panel.hasClass('oa-panel-is-active') && isMobile)) { 349 if ($widgetWrapper.hasClass('position-left') || $widgetWrapper.hasClass('position-right')) { 350 $widgetWrapper.css({ 351 'top': '50vh', 352 'transform': 'translateY(-50%)' 353 }); 354 } else { 355 $widgetWrapper.css({'transform': '', 'top': ''}); // Reset for corner positions 356 } 357 } 358 }); 359 360 // Handle window resize 361 $(window).on('resize', function() { 362 if (isShortcodeEmbed) { 363 if ($('.open-accessibility-widget-panel').hasClass('oa-panel-is-active')) { 364 positionShortcodePanel(); 365 } 366 return; 367 } 368 369 const $panel = $('.open-accessibility-widget-panel'); 370 if ($panel.hasClass('oa-panel-is-active')) { 371 const $wrapper = $('.open-accessibility-widget-wrapper'); 372 const isMobile = window.innerWidth < 768; 373 374 if (isMobile) { 375 if ($wrapper.hasClass('position-left') || $wrapper.hasClass('position-right')) { 376 const wrapperHeight = $wrapper.outerHeight(); 377 $wrapper.css('top', `calc(50vh - ${wrapperHeight / 2}px)`); 378 } 379 $wrapper.css('transform', 'none'); 380 $panel.css({ 381 'position': 'fixed', 'display': 'block', 'top': '0px', 'left': '0px', 382 'width': '100vw', 'height': '100vh', 'max-height': '100vh', 383 'transform': 'none', 'z-index': '1000001' 384 }); 385 } else { // Became desktop 386 $wrapper.css({'transform': '', 'top': ''}); 387 $panel.css({ 388 'position': '', 'display': '', 'top': '', 'left': '', 389 'width': '', 'height': '', 'max-height': '', 'transform': '', 'z-index': '' 390 }); 391 } 392 } 393 }); 394 } 395 396 function mergeUniqueElements() { 397 const seen = new Set(); 398 const merged = []; 399 400 Array.from(arguments).forEach((elements) => { 401 elements.forEach((element) => { 402 if (!seen.has(element)) { 403 seen.add(element); 404 merged.push(element); 405 } 406 }); 407 }); 408 409 return merged; 410 } 411 412 function logDebug() { 413 if (!debugEnabled || !window.console || !window.console.log) { 414 return; 415 } 416 417 window.console.log.apply(window.console, arguments); 418 } 419 420 function dispatchOpenAccessibilityEvent(name, detail) { 421 document.dispatchEvent(new CustomEvent(name, { 422 detail: detail || {} 423 })); 424 } 425 426 function getLifecycleEventDetail() { 427 return { 428 state: $.extend(true, {}, accessibilityState), 429 targets: targetResolver ? targetResolver.getSummary() : {} 430 }; 431 } 432 433 function createTargetResolver(config) { 434 const invalidSelectors = []; 435 let roots = []; 436 let targets = {}; 437 let layoutContainers = []; 438 let excludedCount = 0; 439 440 function queryAll(selector, root, context) { 441 if (!selector) { 442 return []; 443 } 444 445 try { 446 return Array.from((root || document).querySelectorAll(selector)); 447 } catch (error) { 448 invalidSelectors.push({ selector, context, message: error.message }); 449 return []; 450 } 451 } 452 453 function queryAllSelectors(selectors, root, context) { 454 const collected = []; 455
456 selectors.forEach((selector) => { 457 collected.push.apply(collected, queryAll(selector, root, context)); 458 }); 459 460 return collected; 461 } 462 463 function matches(element, selector, context) { 464 if (!selector || !(element instanceof Element)) { 465 return false; 466 } 467 468 try { 469 return element.matches(selector); 470 } catch (error) { 471 invalidSelectors.push({ selector, context, message: error.message }); 472 return false; 473 } 474 } 475 476 function matchesAny(element, selectors, context) { 477 return selectors.some((selector) => matches(element, selector, context)); 478 } 479 480 function closest(element, selector, context) { 481 if (!selector || !(element instanceof Element)) { 482 return null; 483 } 484 485 try { 486 return element.closest(selector); 487 } catch (error) { 488 invalidSelectors.push({ selector, context, message: error.message }); 489 return null; 490 } 491 } 492 493 function closestAny(element, selectors, context) { 494 let match = null; 495 496 selectors.some((selector) => { 497 match = closest(element, selector, context); 498 return Boolean(match); 499 }); 500 501 return match; 502 } 503 504 function isExcluded(element, options) { 505 const settings = options || {}; 506 507 if (!(element instanceof Element)) { 508 return true; 509 } 510 511 if (!document.body.contains(element)) { 512 return true; 513 } 514 515 if (closest(element, '.open-accessibility-widget-wrapper, .open-accessibility-reading-guide, .open-accessibility-skip-to-content-link, .open-accessibility-skip-to-content-backdrop, .open-accessibility-ignore, [data-oa-ignore]', 'built-in exclusions')) { 516 return true; 517 } 518 519 if (matchesAny(element, config.excluded, 'excluded') || closestAny(element, config.excluded, 'excluded')) { 520 excludedCount++; 521 return true; 522 } 523 524 if (settings.preserveLayout && closest(element, '[data-oa-preserve-layout]', 'preserve layout')) { 525 excludedCount++; 526 return true; 527 } 528 529 return false; 530 } 531 532 function uniqueElements(elements, options) { 533 const seen = new Set(); 534 const unique = []; 535 536 elements.forEach((element) => { 537 if (seen.has(element) || isExcluded(element, options)) { 538 return; 539 } 540 541 seen.add(element); 542 unique.push(element); 543 }); 544 545 return unique; 546 } 547 548 function collectRoots() { 549 const configuredRoots = queryAllSelectors(config.roots, document, 'roots') 550 .concat(queryAll('[data-oa-root]', document, 'data roots')); 551 552 roots = uniqueElements(configuredRoots); 553 554 if (!roots.length && config.useBodyFallback) { 555 roots = [document.body]; 556 } 557 } 558 559 function collectGroup(groupName) { 560 const selectors = config.groups[groupName] || []; 561 const dataSelector = `[data-oa-target~="${groupName}"]`; 562 const collected = []; 563 564 roots.forEach((root) => { 565 if (matchesAny(root, selectors, groupName)) { 566 collected.push(root); 567 } 568 569 collected.push.apply(collected, queryAllSelectors(selectors, root, groupName)); 570 collected.push.apply(collected, queryAll(dataSelector, root, `${groupName} data target`)); 571 }); 572 573 targets[groupName] = uniqueElements(collected); 574 } 575 576 function collectLayoutContainers() { 577 const selectors = config.layoutContainers.concat(['[data-oa-relax-layout]']); 578 const collected = []; 579 580 roots.forEach((root) => { 581 if (matchesAny(root, selectors, 'layout containers')) { 582 collected.push(root); 583 } 584 585 collected.push.apply(collected, queryAllSelectors(selectors, root, 'layout containers')); 586 }); 587 588 layoutContainers = uniqueElements(collected, { preserveLayout: true }); 589 } 590 591 function refresh() { 592 invalidSelectors.length = 0; 593 excludedCount = 0; 594 targets = {}; 595 596 collectRoots();
597 Object.keys(config.groups).forEach(collectGroup); 598 collectLayoutContainers(); 599 600 dispatchOpenAccessibilityEvent('openAccessibility:targetsRefreshed', getSummary()); 601 logDebug('Open Accessibility targets', getSummary()); 602 } 603 604 function getTargets(groupName) { 605 if (groupName === 'layout_containers') { 606 return layoutContainers.slice(); 607 } 608 609 return targets[groupName] ? targets[groupName].slice() : []; 610 } 611 612 function getSummary() { 613 const groupCounts = {}; 614 615 Object.keys(targets).forEach((groupName) => { 616 groupCounts[groupName] = targets[groupName].length; 617 }); 618 619 return { 620 roots: roots.length, 621 groups: groupCounts, 622 layoutContainers: layoutContainers.length, 623 invalidSelectors: invalidSelectors.slice(), 624 excludedCount 625 }; 626 } 627 628 refresh(); 629 630 return { 631 refresh, 632 getTargets, 633 getSummary 634 }; 635 } 636 637 function getTypographyTargets() { 638 const contentTargets = targetResolver ? targetResolver.getTargets('readable_text') : []; 639 const headingTargets = targetResolver ? targetResolver.getTargets('headings') : []; 640 const readableTargets = mergeUniqueElements(contentTargets, headingTargets); 641 642 return { 643 contentTargets, 644 readableTargets, 645 allTargets: mergeUniqueElements(contentTargets, readableTargets) 646 }; 647 } 648 649 function parsePixelValue(value, fallback) { 650 if (typeof value !== 'string' || value === '' || value === 'normal') { 651 return fallback; 652 } 653 654 const parsed = Number.parseFloat(value); 655 return Number.isFinite(parsed) ? parsed : fallback; 656 } 657 658 function getComputedLineHeightPx(style, fontSizePx) { 659 return parsePixelValue(style.lineHeight, fontSizePx * 1.2); 660 } 661 662 function getComputedSpacingPx(value) { 663 return parsePixelValue(value, 0); 664 } 665 666 function roundPx(value) { 667 return Math.round(value * 1000) / 1000; 668 } 669 670 function captureOriginalInlineStyles(element) { 671 if (element.dataset.oaTypographyInlineCaptured === '1') { 672 return; 673 } 674 675 element.dataset.oaTypographyInlineCaptured = '1'; 676 element.dataset.oaOriginalFontSize = element.style.fontSize || ''; 677 element.dataset.oaOriginalLineHeight = element.style.lineHeight || ''; 678 element.dataset.oaOriginalLetterSpacing = element.style.letterSpacing || ''; 679 element.dataset.oaOriginalWordSpacing = element.style.wordSpacing || ''; 680 element.dataset.oaOriginalTextAlign = element.style.textAlign || ''; 681 } 682 683 function refreshTypographyBaseline(element) { 684 captureOriginalInlineStyles(element); 685 686 const style = window.getComputedStyle(element); 687 const fontSizePx = parsePixelValue(style.fontSize, 16); 688 689 element.dataset.oaTypographyManaged = '1'; 690 element.dataset.oaBaseFontSize = String(fontSizePx); 691 element.dataset.oaBaseLineHeight = String(getComputedLineHeightPx(style, fontSizePx)); 692 element.dataset.oaBaseLetterSpacing = String(getComputedSpacingPx(style.letterSpacing)); 693 element.dataset.oaBaseWordSpacing = String(getComputedSpacingPx(style.wordSpacing)); 694 } 695 696 function restoreTypographyProperty(element, propertyName, originalValueKey) { 697 if (element.dataset[originalValueKey]) { 698 element.style[propertyName] = element.dataset[originalValueKey]; 699 } else { 700 element.style[propertyName] = ''; 701 } 702 } 703 704 function captureOriginalStyle(element, datasetKey, propertyName) { 705 if (element.dataset[`${datasetKey}Captured`] === '1') { 706 return; 707 } 708 709 element.dataset[`${datasetKey}Captured`] = '1'; 710 element.dataset[datasetKey] = element.style[propertyName] || ''; 711 } 712 713 function restoreOriginalStyle(element, datasetKey, propertyName) { 714 if (element.dataset[datasetKey]) { 715 element.style[propertyName] = element.dataset[datasetKey]; 716 } else { 717 element.style[propertyName] = ''; 718 } 719 } 720 721 function clearManagedStyles(selector, restoreCallback) { 722 document.querySelectorAll(selector).forEach((element) => { 723 restoreCallback(element); 724 }); 725 } 726 727 function clearDynamicTypographyStyles() { 728 document.querySelectorAll('[data-oa-typography-managed="1"]').forEach((element) => { 729 restoreTypographyProperty(element, 'fontSize', 'oaOriginalFontSize'); 730 restoreTypographyProperty(element, 'lineHeight', 'oaOriginalLineHeight'); 731 restoreTypographyProperty(element, 'letterSpacing', 'oaOriginalLetterSpacing'); 732 restoreTypographyProperty(element, 'wordSpacing', 'oaOriginalWordSpacing'); 733 restoreTypographyProperty(element, 'textAlign', 'oaOriginalTextAlign'); 734 }); 735 } 736 737 function hasTextAffectingControls() { 738 const selectedFont = accessibilityState.selectedFont || 'default'; 739 740 return accessibilityState.textSize > 0 || 741 accessibilityState.lineHeightLevel > 0 || 742 accessibilityState.letterSpacingLevel > 0 || 743 accessibilityState.wordSpacingLevel > 0 || 744 selectedFont !== 'default'; 745 } 746 747 function captureOriginalLayoutStyles(element) { 748 if (element.dataset.oaLayoutReliefCaptured === '1') { 749 return; 750 } 751 752 element.dataset.oaLayoutReliefCaptured = '1'; 753 element.dataset.oaOriginalHeight = element.style.height || ''; 754 element.dataset.oaOriginalMaxHeight = element.style.maxHeight || ''; 755 element.dataset.oaOriginalOverflow = element.style.overflow || ''; 756 element.dataset.oaOriginalOverflowX = element.style.overflowX || ''; 757 element.dataset.oaOriginalOverflowY = element.style.overflowY || ''; 758 element.dataset.oaOriginalDisplay = element.style.display || ''; 759 element.dataset.oaOriginalWebkitLineClamp = element.style.webkitLineClamp || ''; 760 element.dataset.oaOriginalLineClamp = element.style.lineClamp || ''; 761 } 762 763 function restoreLayoutProperty(element, propertyName, originalValueKey) { 764 if (element.dataset[originalValueKey]) { 765 element.style[propertyName] = element.dataset[originalValueKey]; 766 } else { 767 element.style[propertyName] = ''; 768 } 769 } 770 771 function restoreLayoutRelief() { 772 document.querySelectorAll('[data-oa-layout-relief-managed="1"]').forEach((element) => { 773 restoreLayoutProperty(element, 'height', 'oaOriginalHeight'); 774 restoreLayoutProperty(element, 'maxHeight', 'oaOriginalMaxHeight'); 775 restoreLayoutProperty(element, 'overflow', 'oaOriginalOverflow'); 776 restoreLayoutProperty(element, 'overflowX', 'oaOriginalOverflowX'); 777 restoreLayoutProperty(element, 'overflowY', 'oaOriginalOverflowY'); 778 restoreLayoutProperty(element, 'display', 'oaOriginalDisplay'); 779 restoreLayoutProperty(element, 'webkitLineClamp', 'oaOriginalWebkitLineClamp'); 780 restoreLayoutProperty(element, 'lineClamp', 'oaOriginalLineClamp'); 781 delete element.dataset.oaLayoutReliefManaged; 782 delete element.dataset.oaLayoutReliefCaptured; 783 delete element.dataset.oaOriginalHeight; 784 delete element.dataset.oaOriginalMaxHeight; 785 delete element.dataset.oaOriginalOverflow; 786 delete element.dataset.oaOriginalOverflowX; 787 delete element.dataset.oaOriginalOverflowY; 788 delete element.dataset.oaOriginalDisplay; 789 delete element.dataset.oaOriginalWebkitLineClamp; 790 delete element.dataset.oaOriginalLineClamp; 791 }); 792 } 793 794 function applyLayoutRelief() { 795 targetResolver.getTargets('layout_containers').forEach((element) => { 796 const computedStyle = window.getComputedStyle(element); 797 798 captureOriginalLayoutStyles(element); 799 element.dataset.oaLayoutReliefManaged = '1'; 800 element.style.height = 'auto'; 801 element.style.maxHeight = 'none'; 802 element.style.overflow = 'visible'; 803 element.style.overflowX = 'visible'; 804 element.style.overflowY = 'visible'; 805 element.style.webkitLineClamp = 'unset'; 806 element.style.lineClamp = 'unset'; 807 808 if (computedStyle.display === '-webkit-box') { 809 element.style.display = 'block'; 810 } 811 }); 812 } 813 814 function syncLayoutRelief() { 815 restoreLayoutRelief(); 816 817 if (hasTextAffectingControls()) { 818 applyLayoutRelief(); 819 } 820 } 821 822 function getReadableFontTargets() { 823 if (!targetResolver) { 824 return []; 825 } 826 827 return mergeUniqueElements( 828 targetResolver.getTargets('readable_text'), 829 targetResolver.getTargets('headings') 830 ); 831 } 832
833 function getReadableFontFamily(fontValue) { 834 if (fontValue === 'atkinson') { 835 return "'Atkinson Hyperlegible', sans-serif"; 836 } 837 838 if (fontValue === 'opendyslexic') { 839 return "'OpenDyslexic', sans-serif"; 840 } 841 842 return ''; 843 } 844 845 function restoreReadableFontTarget(element) { 846 restoreOriginalStyle(element, 'oaReadableFontOriginal', 'fontFamily'); 847 delete element.dataset.oaReadableFontManaged; 848 delete element.dataset.oaReadableFontOriginal; 849 delete element.dataset.oaReadableFontOriginalCaptured; 850 } 851 852 function applyReadableFontTargets(fontValue) { 853 const fontFamily = getReadableFontFamily(fontValue); 854 855 clearManagedStyles('[data-oa-readable-font-managed="1"]', restoreReadableFontTarget); 856 857 if (!fontFamily) { 858 return; 859 } 860 861 getReadableFontTargets().forEach((element) => { 862 captureOriginalStyle(element, 'oaReadableFontOriginal', 'fontFamily'); 863 element.dataset.oaReadableFontManaged = '1'; 864 element.style.fontFamily = fontFamily; 865 }); 866 } 867 868 function restoreLinksUnderlineTarget(element) { 869 restoreOriginalStyle(element, 'oaLinksUnderlineOriginal', 'textDecoration'); 870 delete element.dataset.oaLinksUnderlineManaged; 871 delete element.dataset.oaLinksUnderlineOriginal; 872 delete element.dataset.oaLinksUnderlineOriginalCaptured; 873 } 874 875 function applyLinksUnderlineTargets() { 876 clearManagedStyles('[data-oa-links-underline-managed="1"]', restoreLinksUnderlineTarget); 877 878 if (!accessibilityState.linksUnderline || !targetResolver) { 879 return; 880 } 881 882 targetResolver.getTargets('links').forEach((element) => { 883 captureOriginalStyle(element, 'oaLinksUnderlineOriginal', 'textDecoration'); 884 element.dataset.oaLinksUnderlineManaged = '1'; 885 element.style.textDecoration = 'underline'; 886 }); 887 } 888 889 function restoreHideImagesTarget(element) { 890 restoreOriginalStyle(element, 'oaHideImagesOriginal', 'visibility'); 891 delete element.dataset.oaHideImagesManaged; 892 delete element.dataset.oaHideImagesOriginal; 893 delete element.dataset.oaHideImagesOriginalCaptured; 894 } 895 896 function applyHideImagesTargets() { 897 clearManagedStyles('[data-oa-hide-images-managed="1"]', restoreHideImagesTarget); 898 899 if (!accessibilityState.hideImages || !targetResolver) { 900 return; 901 } 902 903 targetResolver.getTargets('media').forEach((element) => { 904 if (!['IMG', 'PICTURE', 'SVG'].includes(element.tagName)) { 905 return; 906 } 907 908 captureOriginalStyle(element, 'oaHideImagesOriginal', 'visibility'); 909 element.dataset.oaHideImagesManaged = '1'; 910 element.style.visibility = 'hidden'; 911 }); 912 } 913 914 function restoreGrayscaleTarget(element) { 915 restoreOriginalStyle(element, 'oaGrayscaleOriginal', 'filter'); 916 delete element.dataset.oaGrayscaleManaged; 917 delete element.dataset.oaGrayscaleOriginal; 918 delete element.dataset.oaGrayscaleOriginalCaptured; 919 } 920 921 function getGrayscaleTargets() { 922 if (!targetResolver) { 923 return []; 924 } 925 926 return mergeUniqueElements( 927 targetResolver.getTargets('readable_text'), 928 targetResolver.getTargets('headings'), 929 targetResolver.getTargets('links'), 930 targetResolver.getTargets('media') 931 ); 932 } 933 934 function applyGrayscaleTargets() { 935 const $button = $('.open-accessibility-toggle-button'); 936 const $panel = $('.open-accessibility-widget-panel'); 937 938 clearManagedStyles('[data-oa-grayscale-managed="1"]', restoreGrayscaleTarget); 939 $button.toggleClass('widget-grayscale', accessibilityState.grayscale); 940 $panel.toggleClass('widget-grayscale', accessibilityState.grayscale); 941 942 if (!accessibilityState.grayscale) { 943 return; 944 } 945 946 getGrayscaleTargets().forEach((element) => { 947 captureOriginalStyle(element, 'oaGrayscaleOriginal', 'f
947ilter'); 948 element.dataset.oaGrayscaleManaged = '1'; 949 element.style.filter = 'grayscale(100%)'; 950 }); 951 } 952 953 function removeLegacyTypographyClasses() { 954 $('body').removeClass('open-accessibility-text-align-left open-accessibility-text-align-center open-accessibility-text-align-right'); 955 956 for (let i = 1; i <= MAX_TEXT_SIZE; i++) { 957 $('body').removeClass(`open-accessibility-text-size-${i}`); 958 } 959 960 for (let i = 1; i <= MAX_SPACING_LEVEL; i++) { 961 $('body').removeClass(`open-accessibility-letter-spacing-${i}`); 962 $('body').removeClass(`open-accessibility-word-spacing-${i}`); 963 $('body').removeClass(`open-accessibility-line-height-${i}`); 964 } 965 } 966 967 function applyDynamicTypographyAdjustments() { 968 removeLegacyTypographyClasses(); 969 clearDynamicTypographyStyles(); 970 971 const typographyElements = getTypographyTargets(); 972 if (!typographyElements.allTargets.length) { 973 syncLayoutRelief(); 974 return; 975 } 976 977 typographyElements.allTargets.forEach((element) => { 978 refreshTypographyBaseline(element); 979 }); 980 981 const textSizeMultiplier = TEXT_SIZE_MULTIPLIERS[accessibilityState.textSize] || 1; 982 const lineHeightMultiplier = LINE_HEIGHT_MULTIPLIERS[accessibilityState.lineHeightLevel] || 0; 983 const letterSpacingStep = LETTER_SPACING_STEPS[accessibilityState.letterSpacingLevel] || 0; 984 const wordSpacingStep = WORD_SPACING_STEPS[accessibilityState.wordSpacingLevel] || 0; 985 const resizedTargets = new Set(typographyElements.readableTargets); 986 987 typographyElements.readableTargets.forEach((element) => { 988 if (accessibilityState.textSize <= 0) { 989 return; 990 } 991 992 const baseFontSize = parsePixelValue(element.dataset.oaBaseFontSize, 16); 993 element.style.fontSize = `${roundPx(baseFontSize * textSizeMultiplier)}px`; 994 }); 995 996 typographyElements.contentTargets.forEach((element) => { 997 const baseFontSize = parsePixelValue(element.dataset.oaBaseFontSize, 16); 998 const effectiveFontSize = 999 resizedTargets.has(element) && accessibilityState.textSize > 0 1000 ? baseFontSize * textSizeMultiplier 1001 : baseFontSize; 1002 1003 if (accessibilityState.lineHeightLevel > 0) { 1004 const baseLineHeight = parsePixelValue(element.dataset.oaBaseLineHeight, effectiveFontSize * 1.2); 1005 element.style.lineHeight = `${roundPx(Math.max(baseLineHeight, effectiveFontSize * lineHeightMultiplier))}px`; 1006 } 1007 1008 if (accessibilityState.letterSpacingLevel > 0) { 1009 const baseLetterSpacing = parsePixelValue(element.dataset.oaBaseLetterSpacing, 0); 1010 element.style.letterSpacing = `${roundPx(baseLetterSpacing + effectiveFontSize * letterSpacingStep)}px`; 1011 } 1012 1013 if (accessibilityState.wordSpacingLevel > 0) { 1014 const baseWordSpacing = parsePixelValue(element.dataset.oaBaseWordSpacing, 0); 1015 element.style.wordSpacing = `${roundPx(baseWordSpacing + effectiveFontSize * wordSpacingStep)}px`; 1016 } 1017 }); 1018 1019 if (accessibilityState.textAlign) { 1020 typographyElements.readableTargets.forEach((element) => { 1021 element.style.textAlign = accessibilityState.textAlign; 1022 }); 1023 } 1024 1025 syncLayoutRelief(); 1026 } 1027 1028 // Resolve which way a shortcode-embedded panel opens so it stays inside 1029 // the viewport. Explicit direction/align shortcode attributes are 1030 // honored; 'auto' picks the side with the most available space. 1031 function positionShortcodePanel() { 1032 if (!isShortcodeEmbed) { 1033 return; 1034 } 1035 1036 const $root = $('.open-accessibility-widget-wrapper').closest('.open-accessibility-shortcode'); 1037 const $panel = $root.find('.open-accessibility-widget-panel'); 1038 const $button = $root.find('.open-accessibility-toggle-button'); 1039 1040 if (!$root.length || !$panel.length || !$button.length) { 1041 return; 1042 } 1043 1044 // Small screens get the full-screen panel from CSS 1045 if (window.matchMedia('(max-width: 600px)').matches) { 1046 $panel.css('max-height', ''); 1047 return; 1048 } 1049 1050 const preferredDirection = $root.attr('data-oa-direction') || 'auto'; 1051 const preferredAlign = $root.attr('data-oa-align') || 'auto'; 1052 const rect = $button[0].getBoundingClientRect(); 1053 const panelGap = 15; // matches the CSS panel offset 1054 const viewportMargin = 10; // breathing room from the viewport edge 1055 const spaceAbove = rect.top - panelGap - viewportMargin; 1056 const spaceBelow = window.innerHeight - rect.bottom - panelGap - viewportMargin; 1057 const panelHeight = Math.min($panel.prop('scrollHeight') || 0, window.innerHeight * 0.8); 1058 1059 let direction = preferredDirection; 1060 if (direction !== 'up' && direction !== 'down') { 1061 direction = (spaceBelow >= panelHeight || spaceBelow >= spaceAbove) ? 'down' : 'up'; 1062 } 1063 1064 let align = preferredAlign; 1065 if (align !== 'left' && align !== 'right') { 1066 const panelWidth = $panel.outerWidth() || 350; 1067 const spaceRight = window.innerWidth - rect.left - viewportMargin; 1068 const spaceLeft = rect.right - viewportMargin; 1069 align = (spaceRight >= panelWidth || spaceRight >= spaceLeft) ? 'left' : 'right'; 1070 } 1071 1072 $root.toggleClass('oa-direction-up', direction === 'up') 1073 .toggleClass('oa-direction-down', direction === 'down') 1074 .toggleClass('oa-align-left', align === 'left') 1075 .toggleClass('oa-align-right', align === 'right'); 1076 1077 // Cap the panel to the space on its open side; content scrolls 1078 const availableSpace = direction === 'down' ? spaceBelow : spaceAbove; 1079 $panel.css('max-height', Math.max(Math.round(availableSpace), 200) + 'px'); 1080 } 1081 1082 // Toggle widget panel 1083 function toggleAccessibilityPanel() { 1084 const $panel = $('.open-accessibility-widget-panel'); 1085 const $toggle = $('.open-accessibility-toggle-button'); 1086 const $wrapper = $('.open-accessibility-widget-wrapper'); 1087 const isMobile = window.innerWidth < 768; 1088 1089 $panel.toggleClass('oa-panel-is-active'); 1090 const isActive = $panel.hasClass('oa-panel-is-active'); 1091 $panel.attr('aria-hidden', isActive ? 'false' : 'true'); 1092 $toggle.attr('aria-expanded', isActive ? 'true' : 'false'); 1093 1094 // Shortcode embed: resolve placement against the viewport on open 1095 if (isShortcodeEmbed) { 1096 if (isActive) { 1097 positionShortcodePanel(); 1098 } 1099 return; 1100 } 1101 1102 if (isActive) { // Panel is OPENING 1103 if (isMobile) { 1104 if ($wrapper.hasClass('position-left') || $wrapper.hasClass('position-right')) { 1105 const wrapperHeight = $wrapper.outerHeight(); 1106 $wrapper.css('top', `calc(50vh - ${wrapperHeight / 2}px)`); 1107 } 1108 $wrapper.css('transform', 'none'); 1109 $panel.css({ 1110 'position': 'fixed', 'display': 'block', 'top': '0px', 'left': '0px', 1111 'width': '100vw', 'height': '100vh', 'max-height': '100vh', 1112 'transform': 'none', 'z-index': '1000001' 1113 }); 1114 } else { // Desktop OPENING 1115 $wrapper.css({'transform': '', 'top': ''}); 1116 $panel.css({ 1117 'position': '', 'display': '', 'top': '', 'left': '', 1118 'width': '', 'height': '', 'max-height': '', 'transform': '', 'z-index': '' 1119 }); 1120 } 1121 } else { // Panel is CLOSING (via toggle button click) 1122 if (isMobile) { 1123 $panel.css({ // Re-assert full-screen styles for fade-out 1124 'position': 'fixed', 'display': 'block', 'top': '0px', 'left': '0px', 1125 'width': '100vw', 'height': '100vh', 'max-height': '100vh', 1126 'transform': 'none', 'z-index': '1000001' 1127 }); 1128 setTimeout(function() { 1129 $panel.css({ // Hide and reset panel 1130 'display': 'none', 'position': '', 'top': '', 'left': '', 'width': '', 1131 'height': '', 'max-height': '', 'transform': '', 'z-index': '' 1132 }); 1133 // Reset wrapper transform/top AFTER panel is hidden 1134 $wrapper.css({'transform': '', 'top': ''}); 1135 }, 350); 1136 } else { // Desktop CLOSING 1137 $wrapper.css({'transform': '', 'top': ''}); // Reset wrapper immediately for desktop 1138 $panel.css({ 1139 'position': '', 'display': '', 'top': '','left': '', 1140 'width': '', 'height': '', 'max-height': '', 'transform': '', 'z-index': '' 1141 }); 1142 } 1143 } 1144 } 1145 1146 // Close widget panel (called by close button, ESC, click-outside) 1147 function closeAccessibilityPanel() { 1148 const $panel = $('.open-accessibility-widget-panel'); 1149 const $toggle = $('.open-accessibility-toggle-button'); 1150 const $wrapper = $('.open-accessibility-widget-wrapper'); 1151 const isMobile = window.innerWidth < 768; 1152 1153 $panel.removeClass('oa-panel-is-active'); 1154 $panel.attr('aria-hidden', 'true'); 1155 $toggle.attr('aria-expanded', 'false'); 1156 1157 // Shortcode embed: simple hide, no positioning logic 1158 if (isShortcodeEmbed) { 1159 return; 1160 } 1161 1162 if (isMobile) { 1163 $panel.css({ // Re-assert full-screen styles for fade-out 1164 'position': 'fixed', 'display': 'block', 'top': '0px', 'left': '0px', 1165 'width': '100vw', 'height': '100vh', 'max-height': '100vh', 1166 'transform': 'none', 'z-index': '1000001' 1167 }); 1168 setTimeout(function() { 1169 $panel.css({ // Hide and reset panel 1170 'display': 'none', 'position': '', 'top': '', 'left': '', 'width': '', 1171 'height': '', 'max-height': '', 'transform': '', 'z-index': '' 1172 }); 1173 // Reset wrapper transform/top AFTER panel is hidden 1174 $wrapper.css({'transform': '', 'top': ''}); 1175 }, 350); 1176 } else { // Desktop 1177 $wrapper.css({'transform': '', 'top': ''}); // Reset wrapper immediately for desktop 1178 $panel.css({ 1179 'position': '', 'display': '', 'top': '','left': '', 1180 'width': '', 'height': '', 'max-height': '', 'transform': '', 'z-index': '' 1181 }); 1182 } 1183 } 1184 1185 // Hide widget (user choice) 1186 function hideAccessibilityWidget() { 1187 $('.open-accessibility-toggle-button, .open-accessibility-widget-panel').css('display', 'none'); 1188 1189 // Set a cookie to remember this choice temporarily
1190 setCookie('open_accessibility_hidden', '1', 1); // Hide for 1 day 1191 1192 closeAccessibilityPanel(); 1193 } 1194 1195 // Handle action button clicks 1196 function handleActionButton(e) { 1197 e.stopPropagation(); 1198 1199 const $btn = $(this); 1200 const action = $btn.data('action'); 1201 const value = $btn.data('value'); 1202 1203 switch (action) { 1204 case 'contrast': 1205 handleContrast(value); 1206 break; 1207 1208 case 'grayscale': 1209 toggleGrayscale(); 1210 break; 1211 1212 case 'text-size': 1213 adjustTextSize(value); 1214 break; 1215 1216 case 'set-font': 1217 setFont(value); 1218 break; 1219 1220 case 'links-underline': 1221 toggleLinksUnderline(); 1222 break; 1223 1224 case 'hide-images': 1225 toggleHideImages(); 1226 break; 1227 1228 case 'reading-guide': 1229 toggleReadingGuide(); 1230 break; 1231 1232 case 'focus-outline': 1233 toggleFocusOutline(); 1234 break; 1235 1236 case 'line-height': 1237 adjustLineHeight(value); 1238 break; 1239 1240 case 'text-align': 1241 setTextAlign(value); 1242 break; 1243 1244 case 'pause-animations': 1245 togglePauseAnimations(); 1246 break; 1247 1248 case 'letter-spacing': 1249 adjustLetterSpacing(value); 1250 break; 1251 1252 case 'word-spacing': 1253 adjustWordSpacing(value); 1254 break; 1255 } 1256 1257 // Update state 1258 syncActionButtonStates(); 1259 saveState(); 1260 1261 // Logged after the sync so toggles report the state they landed in. 1262 logFeatureUsage($btn, action, value); 1263 } 1264 1265 function setButtonPressed(action, value, pressed) { 1266 const $button = $('.open-accessibility-action-button[aria-pressed]').filter(function() { 1267 const $actionButton = $(this); 1268 1269 return $actionButton.attr('data-action') === action && 1270 $actionButton.attr('data-value') === value; 1271 }); 1272 $button.toggleClass('active', pressed); 1273 $button.attr('aria-pressed', pressed ? 'true' : 'false'); 1274 } 1275 1276 function syncActionButtonStates() { 1277 $('.open-accessibility-action-button[aria-pressed]') 1278 .removeClass('active') 1279 .attr('aria-pressed', 'false'); 1280 1281 if (accessibilityState.contrast) { 1282 setButtonPressed('contrast', accessibilityState.contrast, true); 1283 } 1284 1285 setButtonPressed('grayscale', 'toggle', accessibilityState.grayscale); 1286 setButtonPressed('set-font', accessibilityState.selectedFont || 'default', true); 1287 setButtonPressed('links-underline', 'toggle', accessibilityState.linksUnderline); 1288 setButtonPressed('hide-images', 'toggle', accessibilityState.hideImages); 1289 setButtonPressed('reading-guide', 'toggle', accessibilityState.readingGuide); 1290 setButtonPressed('focus-outline', 'toggle', accessibilityState.focusOutline); 1291 1292 if (accessibilityState.textAlign) { 1293 setButtonPressed('text-align', accessibilityState.textAlign, true); 1294 } 1295 1296 setButtonPressed('pause-animations', 'toggle', accessibilityState.pauseAnimations); 1297 } 1298 1299 // Handle contrast modes 1300 function handleContrast(mode) { 1301 mode = normalizeChoice(mode, VALID_CONTRAST_MODES, ''); 1302 1303 // Remove existing contrast classes 1304 $('body').removeClass('open-accessibility-high-contrast open-accessibility-negative-contrast open-accessibility-light-background open-accessibility-dark-background'); 1305 1306 // Clear active state on all contrast buttons 1307 $('.open-accessibility-action-button[data-action="contrast"]').removeClass('active'); 1308 1309 // If the same mode is clicked again, deactivate it 1310 if (accessibilityState.contrast === mode) { 1311 accessibilityState.contrast = ''; 1312 return; 1313 } 1314 1315 // Apply new contrast mode 1316 switch (mode) { 1317 case 'high': 1318 $('body').addClass('open-accessibility-high-contrast'); 1319 $('.open-accessibility-action-button[data-action="contrast"][data-value="high"]').addClass('active'); 1320 break; 1321 1322 case 'negative': 1323 $('body').addClass('open-accessibility-negative-contrast'); 1324 $('.open-accessibility-action-button[data-action="contrast"][data-value="negative"]').addClass('active'); 1325 break; 1326 1327 case 'light': 1328 $('body').addClass('open-accessibility-light-background'); 1329 $('.open-accessibility-action-button[data-action="contrast"][data-value="light"]').addClass('active'); 1330 break; 1331 1332 case 'dark': 1333 $('body').addClass('open-accessibility-dark-background'); 1334 $('.open-accessibility-action-button[data-action="contrast"][data-value="dark"]').addClass('active'); 1335 break; 1336 } 1337 1338 accessibilityState.contrast = mode; 1339 } 1340 1341 // Apply contrast from saved state (without toggle logic) 1342 function applyContrast(mode) { 1343 // Remove existing contrast classes 1344 $('body').removeClass('open-accessibility-high-contrast open-accessibility-negative-contrast open-accessibility-light-background open-accessibility-dark-background'); 1345 1346 // Clear active state on all contrast buttons 1347 $('.open-accessibility-action-button[data-action="contrast"]').removeClass('active'); 1348 1349 // Apply the contrast mode and activate the correct button 1350 switch (mode) { 1351 case 'high': 1352 $('body').addClass('open-accessibility-high-contrast'); 1353 $('.open-accessibility-action-button[data-action="contrast"][data-value="high"]').addClass('active'); 1354 break; 1355 1356 case 'negative': 1357 $('body').addClass('open-accessibility-negative-contrast'); 1358 $('.open-accessibility-action-button[data-action="contrast"][data-value="negative"]').addClass('active'); 1359 break; 1360 1361 case 'light': 1362 $('body').addClass('open-accessibility-light-background'); 1363 $('.open-accessibility-action-button[data-action="contrast"][data-value="light"]').addClass('active'); 1364 break; 1365 1366 case 'dark': 1367 $('body').addClass('open-accessibility-dark-background'); 1368 $('.open-accessibility-action-button[data-action="contrast"][data-value="dark"]').addClass('active'); 1369 break; 1370 } 1371 } 1372 1373 // Toggle grayscale 1374 function toggleGrayscale() { 1375 accessibilityState.grayscale = !accessibilityState.grayscale; 1376 applyGrayscaleTargets(); 1377 syncActionButtonStates(); 1378 } 1379 1380 // Adjust text size 1381 function adjustTextSize(direction) {
1382 if (direction === 'increase') { 1383 accessibilityState.textSize = Math.min(accessibilityState.textSize + 1, MAX_TEXT_SIZE); 1384 } else if (direction === 'decrease') { 1385 accessibilityState.textSize = Math.max(accessibilityState.textSize - 1, 0); 1386 } 1387 1388 applyDynamicTypographyAdjustments(); 1389 1390 // Update button states and indicator 1391 updateSpacingButtonStates('text-size', accessibilityState.textSize); 1392 updateIndicator('text-size', accessibilityState.textSize); 1393 } 1394 1395 // Set selected font 1396 function setFont(fontValue) { 1397 fontValue = normalizeChoice(fontValue, VALID_FONT_VALUES, 'default'); 1398 1399 // Remove previous legacy font classes 1400 $('body').removeClass('open-accessibility-font-atkinson open-accessibility-font-opendyslexic'); 1401 // Remove active class from all font buttons 1402 $('.open-accessibility-action-button[data-action="set-font"]').removeClass('active'); 1403 1404 // If the same font is clicked again, revert to default 1405 if (accessibilityState.selectedFont === fontValue) { 1406 accessibilityState.selectedFont = 'default'; 1407 $('.open-accessibility-action-button[data-action="set-font"][data-value="default"]').addClass('active'); // Activate default button 1408 } else { 1409 accessibilityState.selectedFont = fontValue; 1410 // Add active class to the clicked button 1411 setButtonPressed('set-font', fontValue, true); 1412 } 1413 1414 // Ensure default button is active if state is default 1415 if (accessibilityState.selectedFont === 'default') { 1416 $('.open-accessibility-action-button[data-action="set-font"][data-value="default"]').addClass('active'); 1417 } 1418 1419 applyDynamicTypographyAdjustments(); 1420 applyReadableFontTargets(accessibilityState.selectedFont || 'default'); 1421 } 1422 1423 // Apply font from saved state (without toggle logic) 1424 function applyFont(fontValue) { 1425 // Remove previous legacy font classes 1426 $('body').removeClass('open-accessibility-font-atkinson open-accessibility-font-opendyslexic'); 1427 // Remove active class from all font buttons 1428 $('.open-accessibility-action-button[data-action="set-font"]').removeClass('active'); 1429 1430 // Activate the correct button; targeted inline styles apply the font. 1431 if (fontValue === 'atkinson') { 1432 $('.open-accessibility-action-button[data-action="set-font"][data-value="atkinson"]').addClass('active'); 1433 } else if (fontValue === 'opendyslexic') { 1434 $('.open-accessibility-action-button[data-action="set-font"][data-value="opendyslexic"]').addClass('active'); 1435 } else { 1436 // Default font - just activate the default button 1437 $('.open-accessibility-action-button[data-action="set-font"][data-value="default"]').addClass('active'); 1438 } 1439 1440 applyReadableFontTargets(accessibilityState.selectedFont || 'default'); 1441 } 1442 1443 // Toggle links underline 1444 function toggleLinksUnderline() { 1445 accessibilityState.linksUnderline = !accessibilityState.linksUnderline; 1446 $('body').removeClass('open-accessibility-links-underline'); 1447 applyLinksUnderlineTargets(); 1448 syncActionButtonStates(); 1449 } 1450 1451 // Toggle hide images 1452 function toggleHideImages() { 1453 accessibilityState.hideImages = !accessibilityState.hideImages; 1454 $('body').removeClass('open-accessibility-hide-images'); 1455 applyHideImagesTargets(); 1456 syncActionButtonStates(); 1457 } 1458 1459 // Toggle reading guide 1460 function toggleReadingGuide() { 1461 accessibilityState.readingGuide = !accessibilityState.readingGuide; 1462 $('.open-accessibility-action-button[data-action="reading-guide"]').toggleClass('active', accessibilityState.readingGuide); 1463 if (accessibilityState.readingGuide) { 1464 $('.open-accessibility-reading-guide').show(); 1465 } else { 1466 $('.open-accessibility-reading-guide').hide(); 1467 } 1468 saveState(); 1469 } 1470 1471 // Handle reading guide mouse movement 1472 function handleReadingGuide(e) { 1473 if (accessibilityState.readingGuide) { 1474 $('.open-accessibility-reading-guide').css('top', e.clientY - 15); 1475 } 1476 } 1477 1478 // Toggle focus outline 1479 function toggleFocusOutline() { 1480 accessibilityState.focusOutline = !accessibilityState.focusOutline; 1481 $('body').toggleClass('open-accessibility-focus-outline', accessibilityState.focusOutline); 1482 $('.open-accessibility-action-button[data-action="focus-outline"]').toggleClass('active', accessibilityState.focusOutline); 1483 } 1484 1485 1486 // Set text align 1487 function setTextAlign(align) { 1488 align = normalizeChoice(align, VALID_TEXT_ALIGN_VALUES, ''); 1489 1490 if (!align) { 1491 return; 1492 } 1493 1494 // Clear active state on all text align buttons 1495 $('.open-accessibility-action-button[data-action="text-align"]').removeClass('active'); 1496 1497 // If the same alignment is clicked again, deactivate it 1498 if (accessibilityState.textAlign === align) { 1499 accessibilityState.textAlign = ''; 1500 applyDynamicTypographyAdjustments(); 1501 return; 1502 } 1503 1504 accessibilityState.textAlign = align; 1505 setButtonPressed('text-align', align, true); 1506 applyDynamicTypographyAdjustments(); 1507 } 1508 1509 // Toggle pause animations 1510 function togglePauseAnimations() { 1511 accessibilityState.pauseAnimations = !accessibilityState.pauseAnimations; 1512 $('body').toggleClass('open-accessibility-pause-animations', accessibilityState.pauseAnimations);
1513 $('.open-accessibility-action-button[data-action="pause-animations"]').toggleClass('active', accessibilityState.pauseAnimations); 1514 } 1515 1516 // Adjust Letter Spacing Level 1517 function adjustLetterSpacing(direction) { 1518 const currentLevel = accessibilityState.letterSpacingLevel; 1519 let newLevel = currentLevel; 1520 1521 if (direction === 'increase') { 1522 newLevel = Math.min(currentLevel + 1, MAX_SPACING_LEVEL); 1523 } else if (direction === 'decrease') { 1524 newLevel = Math.max(currentLevel - 1, 0); 1525 } 1526 1527 if (newLevel !== currentLevel) { 1528 accessibilityState.letterSpacingLevel = newLevel; 1529 applyDynamicTypographyAdjustments(); 1530 updateSpacingButtonStates('letter-spacing', newLevel); 1531 updateIndicator('letter-spacing', newLevel); 1532 } 1533 } 1534 1535 // Adjust Word Spacing Level 1536 function adjustWordSpacing(direction) { 1537 const currentLevel = accessibilityState.wordSpacingLevel; 1538 let newLevel = currentLevel; 1539 1540 if (direction === 'increase') { 1541 newLevel = Math.min(currentLevel + 1, MAX_SPACING_LEVEL); 1542 } else if (direction === 'decrease') { 1543 newLevel = Math.max(currentLevel - 1, 0); 1544 } 1545 1546 if (newLevel !== currentLevel) { 1547 accessibilityState.wordSpacingLevel = newLevel; 1548 applyDynamicTypographyAdjustments(); 1549 updateSpacingButtonStates('word-spacing', newLevel); 1550 updateIndicator('word-spacing', newLevel); 1551 } 1552 } 1553 1554 // Adjust Line Height Level 1555 function adjustLineHeight(direction) { 1556 const currentLevel = accessibilityState.lineHeightLevel; 1557 let newLevel = currentLevel; 1558 1559 if (direction === 'increase') { 1560 newLevel = Math.min(currentLevel + 1, MAX_SPACING_LEVEL); 1561 } else if (direction === 'decrease') { 1562 newLevel = Math.max(currentLevel - 1, 0); 1563 } 1564 1565 if (newLevel !== currentLevel) { 1566 accessibilityState.lineHeightLevel = newLevel; 1567 applyDynamicTypographyAdjustments(); 1568 updateSpacingButtonStates('line-height', newLevel); 1569 updateIndicator('line-height', newLevel); 1570 } 1571 } 1572 1573 // Helper to update Increase/Decrease button states 1574 function updateSpacingButtonStates(action, level) { 1575 const maxLevel = action === 'text-size' ? MAX_TEXT_SIZE : MAX_SPACING_LEVEL; 1576 $(`.open-accessibility-action-button[data-action="${action}"][data-value="decrease"]`).prop('disabled', level <= 0); 1577 $(`.open-accessibility-action-button[data-action="${action}"][data-value="increase"]`).prop('disabled', level >= maxLevel); 1578 } 1579 1580 // Update visual indicator (dots/dashes) for incremental controls 1581 function updateIndicator(action, currentLevel) { 1582 const $indicator = $(`.open-accessibility-indicator[data-action="${action}"]`); 1583 if ($indicator.length === 0) { 1584 return; 1585 } 1586 1587 const maxLevel = parseInt($indicator.data('max'), 10); 1588 const totalDots = maxLevel + 1; // 0 to maxLevel inclusive 1589 1590 // Clear existing dots 1591 $indicator.empty(); 1592 1593 // Create dots for each level 1594 for (let i = 0; i <= maxLevel; i++) { 1595 const $dot = $('<span></span>') 1596 .addClass('open-accessibility-indicator-dot') 1597 .addClass(i <= currentLevel ? 'active' : ''); 1598 $indicator.append($dot); 1599 } 1600 1601 // Update aria-label 1602 $indicator.attr('aria-label', `Level ${currentLevel} of ${maxLevel}`); 1603 $indicator.append($('<span></span>') 1604 .addClass('screen-reader-text') 1605 .text(`Level ${currentLevel} of ${maxLevel}`)); 1606 } 1607 1608 // Reset all settings to default 1609 $('.open-accessibility-reset-button').on('click', function() { 1610 // Remove all accessibility classes 1611 $('body').removeClass('open-accessibility-high-contrast open-accessibility-negative-contrast open-accessibility-light-background open-accessibility-dark-background'); 1612 $('body').removeClass('open-accessibility-grayscale open-accessibility-links-underline'); 1613 $('body').removeClass('open-accessibility-hide-images open-accessibility-reading-guide-active open-accessibility-focus-outline'); 1614 $('body').removeClass('open-accessibility-pause-animations'); 1615 $('body').removeClass('open-accessibility-font-atkinson open-accessibility-font-opendyslexic'); 1616 removeLegacyTypographyClasses(); 1617 clearDynamicTypographyStyles(); 1618 restoreLayoutRelief(); 1619 1620 // Reset all buttons active/disabled state 1621 $('.open-accessibility-action-button').removeClass('active').prop('disabled', false); 1622 1623 // Reset state 1624 accessibilityState = normalizeAccessibilityState($.extend({}, DEFAULT_ACCESSIBILITY_STATE, { active: true })); 1625 1626 // Update all indicators 1627 updateIndicator('text-size', 0);
1628 updateIndicator('letter-spacing', 0); 1629 updateIndicator('word-spacing', 0); 1630 updateIndicator('line-height', 0); 1631 1632 // Explicitly hide the reading guide on reset 1633 $('.open-accessibility-reading-guide').hide(); 1634 1635 applyDynamicTypographyAdjustments(); 1636 applyReadableFontTargets('default'); 1637 applyGrayscaleTargets(); 1638 applyLinksUnderlineTargets(); 1639 applyHideImagesTargets(); 1640 syncActionButtonStates(); 1641 1642 // Save reset state 1643 saveState(); 1644 1645 dispatchOpenAccessibilityEvent('openAccessibility:reset', getLifecycleEventDetail()); 1646 }); 1647 1648 // Save state to local storage 1649 function saveState() { 1650 accessibilityState = normalizeAccessibilityState(accessibilityState); 1651 1652 if (typeof localStorage !== 'undefined') { 1653 localStorage.setItem(storageKey, JSON.stringify(accessibilityState)); 1654 } 1655 } 1656 1657 // Load state from local storage 1658 function loadState() { 1659 if (typeof localStorage !== 'undefined') { 1660 let savedState = localStorage.getItem(storageKey); 1661 1662 // Migration: Look for old page-specific keys and consolidate them 1663 if (!savedState) { 1664 // Check for any page-specific keys that might exist 1665 for (let i = 0; i < localStorage.length; i++) { 1666 const key = localStorage.key(i); 1667 if (key && key.endsWith('_open_accessibility_settings')) { 1668 const pageSpecificState = localStorage.getItem(key); 1669 if (pageSpecificState) { 1670 try { 1671 const parsedState = JSON.parse(pageSpecificState); 1672 // If this state shows accessibility was active, use it as our base 1673 if (parsedState.active) { 1674 savedState = pageSpecificState; 1675 // Save it to the new unified key 1676 localStorage.setItem(storageKey, savedState); 1677 console.log('Open Accessibility: Migrated settings from page-specific storage'); 1678 } 1679 // Clean up the old page-specific key 1680 localStorage.removeItem(key); 1681 } catch (e) { 1682 console.error('Error parsing page-specific accessibility state', e); 1683 localStorage.removeItem(key); // Clean up corrupted data 1684 } 1685 } 1686 } 1687 } 1688 } 1689 1690 if (savedState) { 1691 try { 1692 const parsedState = JSON.parse(savedState); 1693 // Migration: Convert old lineHeight boolean to lineHeightLevel 1694 if (parsedState.hasOwnProperty('lineHeight') && typeof parsedState.lineHeight === 'boolean') { 1695 parsedState.lineHeightLevel = parsedState.lineHeight ? 2 : 0; // Old toggle was equivalent to level 2 1696 delete parsedState.lineHeight; 1697 } 1698 // Ensure lineHeightLevel exists 1699 if (!parsedState.hasOwnProperty('lineHeightLevel')) { 1700 parsedState.lineHeightLevel = 0; 1701 } 1702 accessibilityState = normalizeAccessibilityState(parsedState); 1703 } catch (e) { 1704 console.error('Error parsing saved accessibility state', e); 1705 } 1706 } 1707 } 1708 } 1709 1710 // Apply current state to the UI 1711 function applyState() { 1712 accessibilityState = normalizeAccessibilityState(accessibilityState); 1713 1714 dispatchOpenAccessibilityEvent('openAccessibility:beforeApply', getLifecycleEventDetail()); 1715 1716 // Apply contrast (directly without toggle logic) 1717 applyContrast(accessibilityState.contrast || ''); 1718 1719 // Apply grayscale 1720 applyGrayscaleTargets(); 1721 1722 // Apply selected font (directly without toggle logic) 1723 applyFont(accessibilityState.selectedFont || 'default'); 1724 1725 // Apply links underline 1726 $('body').removeClass('open-accessibility-links-underline'); 1727 applyLinksUnderlineTargets(); 1728 1729 // Apply hide images 1730 $('body').removeClass('open-accessibility-hide-images'); 1731 applyHideImagesTargets(); 1732 1733 // Apply reading guide 1734 $('.open-accessibility-reading-guide').toggle(accessibilityState.readingGuide); 1735 $('.open-accessibility-action-button[data-action="reading-guide"]').toggleClass('active', accessibilityState.readingGuide); 1736 1737 // Apply focus outline 1738 $('body').toggleClass('open-accessibility-focus-outline', accessibilityState.focusOutline); 1739 $('.open-accessibility-action-button[data-action="focus-outline"]').toggleClass('active', accessibilityState.focusOutline); 1740 1741 // Apply pause animations 1742 $('body').toggleClass('open-accessibility-pause-animations', accessibilityState.pauseAnimations);
1743 $('.open-accessibility-action-button[data-action="pause-animations"]').toggleClass('active', accessibilityState.pauseAnimations); 1744 1745 applyDynamicTypographyAdjustments(); 1746 1747 // Update button states and indicators for adaptive typography controls 1748 updateSpacingButtonStates('text-size', accessibilityState.textSize); 1749 updateIndicator('text-size', accessibilityState.textSize); 1750 updateSpacingButtonStates('line-height', accessibilityState.lineHeightLevel); 1751 updateIndicator('line-height', accessibilityState.lineHeightLevel); 1752 updateSpacingButtonStates('letter-spacing', accessibilityState.letterSpacingLevel); 1753 updateIndicator('letter-spacing', accessibilityState.letterSpacingLevel); 1754 updateSpacingButtonStates('word-spacing', accessibilityState.wordSpacingLevel); 1755 updateIndicator('word-spacing', accessibilityState.wordSpacingLevel); 1756 1757 $('.open-accessibility-action-button[data-action="text-align"]').removeClass('active'); 1758 if (accessibilityState.textAlign) { 1759 setButtonPressed('text-align', accessibilityState.textAlign, true); 1760 } 1761 1762 syncActionButtonStates(); 1763 1764 dispatchOpenAccessibilityEvent('openAccessibility:afterApply', getLifecycleEventDetail()); 1765 } 1766 1767 // Helper function to set cookies 1768 // Ordered list of places "skip to content" should land when the configured 1769 // element ID is not on the page. 1770 // 1771 // Order matters: the first match wins, so the most reliably "start of the 1772 // main content" candidate belongs first. Block themes get 1773 // #wp--skip-link--target injected by WordPress core, which is why it leads. 1774 // 1775 // The list is supplied by PHP and can be reordered or extended with the 1776 // open_accessibility_skip_target_candidates filter. The literals here are 1777 // only a fallback for when the option fails to arrive. 1778 function getSkipTargetCandidates() { 1779 const configured = getFrontendOption('skip_target_candidates', null); 1780 1781 if (Array.isArray(configured) && configured.length) { 1782 return configured.filter((s) => typeof s === 'string' && s.trim().length); 1783 } 1784 1785 return [ 1786 '#wp--skip-link--target', 1787 '#content', 1788 '#main', 1789 '#primary', 1790 'main', 1791 '[role="main"]', 1792 '.site-main', 1793 '.entry-content' 1794 ]; 1795 } 1796 1797 // Resolve the element the skip link should actually move focus to. 1798 function resolveSkipTarget() { 1799 const configured = getFrontendOption('skip_to_element_id', 'content'); 1800 1801 // An explicitly configured ID always wins when it is really on the page. 1802 if (configured) { 1803 const byId = document.getElementById(configured); 1804 if (byId) { 1805 return byId; 1806 } 1807 } 1808 1809 const candidates = getSkipTargetCandidates(); 1810 1811 for (let i = 0; i < candidates.length; i++) { 1812 try { 1813 const el = document.querySelector(candidates[i]); 1814 if (el) { 1815 return el; 1816 } 1817 } catch (e) { 1818 // Ignore an invalid selector and keep looking. 1819 } 1820 } 1821 1822 return null; 1823 } 1824 1825 // Point the skip link at something real, and actually move focus. 1826 // 1827 // Two separate problems are handled here. Block themes have no #content, 1828 // so the default href resolves to nothing. And even when the target does 1829 // exist, following an in-page anchor scrolls the viewport without moving 1830 // keyboard focus unless the target can hold it - the classic reason a skip 1831 // link appears to do nothing for screen reader users (WCAG 2.1 SC 2.4.1). 1832 function initSkipLink() { 1833 const $link = $('.open-accessibility-skip-to-content-link'); 1834 1835 if (!$link.length) { 1836 return; 1837 } 1838 1839 const target = resolveSkipTarget(); 1840 1841 if (!target) { 1842 logDebug('Open Accessibility: no skip-to-content target found on this page'); 1843 return; 1844 } 1845 1846 // Give the target an ID if it has none, so the href stays meaningful. 1847 if (!target.id) { 1848 target.id = 'open-accessibility-content'; 1849 } 1850 1851 $link.attr('href', '#' + target.id); 1852 1853 $link.on('click', function(e) { 1854 e.preventDefault(); 1855 1856 // Headings, divs and <main> are not focusable by default. -1 keeps 1857 // them out of the tab order while still allowing programmatic focus. 1858 if (!target.hasAttribute('tabindex')) { 1859 target.setAttribute('tabindex', '-1'); 1860 } 1861
1862 target.focus({ preventScroll: true }); 1863 target.scrollIntoView(); 1864 1865 $link.trigger('blur'); 1866 }); 1867 } 1868 1869 // Read a frontend option pushed through wp_localize_script. 1870 function getFrontendOption(key, fallback) { 1871 if (typeof open_accessibility_data === 'undefined' || 1872 !open_accessibility_data || 1873 !open_accessibility_data.options || 1874 typeof open_accessibility_data.options[key] === 'undefined') { 1875 return fallback; 1876 } 1877 1878 return open_accessibility_data.options[key]; 1879 } 1880 1881 // Open links in the same tab. 1882 // 1883 // An unannounced new tab breaks the back button and disorients screen 1884 // reader and magnifier users (WCAG 2.1 SC 3.2.5). Opt-in, because it 1885 // changes the behaviour of every link on the site. The widget's own links 1886 // and anything explicitly excluded are left alone. 1887 function applyLinkTargetPolicy() { 1888 if (!getFrontendOption('strip_link_targets', false)) { 1889 return; 1890 } 1891 1892 $('a[target="_blank"]').each(function() { 1893 const $link = $(this); 1894 1895 if ($link.closest('.open-accessibility-widget-wrapper, .open-accessibility-ignore, [data-oa-ignore]').length) { 1896 return; 1897 } 1898 1899 $link.removeAttr('target'); 1900 }); 1901 } 1902 1903 // Per-tab identifier so repeated toggles in one visit group together. 1904 // Deliberately sessionStorage, not localStorage: it dies with the tab and 1905 // never becomes a cross-visit identifier. 1906 function getSessionId() { 1907 const key = 'open-accessibility-session'; 1908 1909 try { 1910 let id = sessionStorage.getItem(key); 1911 1912 if (!id) { 1913 id = 'oa-' + Math.random().toString(36).slice(2) + Date.now().toString(36); 1914 sessionStorage.setItem(key, id); 1915 } 1916 1917 return id; 1918 } catch (e) { 1919 // Private browsing or storage disabled: skip logging rather than throw. 1920 return ''; 1921 } 1922 } 1923 1924 // Report a control interaction, when the site owner has opted in. 1925 // 1926 // Sends `feature_action` rather than `action`: admin-ajax reserves `action` 1927 // to route the request, so a field of that name never reaches the handler. 1928 function logFeatureUsage($button, feature, value) { 1929 if (!getFrontendOption('enable_analytics', false)) { 1930 return; 1931 } 1932 1933 if (typeof open_accessibility_data === 'undefined' || 1934 !open_accessibility_data || 1935 !open_accessibility_data.ajaxurl) { 1936 return; 1937 } 1938 1939 const sessionId = getSessionId(); 1940 1941 if (!sessionId || !feature) { 1942 return; 1943 } 1944 1945 // Toggles report the state they landed in; value controls report the 1946 // value that was chosen. 1947 const isToggle = value === 'toggle'; 1948 const pressed = $button && $button.attr('aria-pressed') === 'true'; 1949 1950 $.post(open_accessibility_data.ajaxurl, { 1951 action: 'open_accessibility_log_usage', 1952 nonce: open_accessibility_data.nonce, 1953 session_id: sessionId, 1954 feature: feature, 1955 feature_action: isToggle ? 'toggle' : 'set', 1956 value: isToggle ? (pressed ? 'on' : 'off') : String(value == null ? '' : value) 1957 }).fail(function() { 1958 logDebug('Open Accessibility: usage logging request failed'); 1959 }); 1960 } 1961 1962 function setCookie(name, value, days) { 1963 let expires = ''; 1964 if (days) { 1965 const date = new Date(); 1966 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 1967 expires = '; expires=' + date.toUTCString(); 1968 } 1969 document.cookie = name + '=' + (value || '') + expires + '; path=/; SameSite=Lax'; 1970 } 1971 1972 // Helper function to get cookies 1973 function getCookie(name) { 1974 const nameEQ = name + '='; 1975 const ca = document.cookie.split(';'); 1976 for (let i = 0; i < ca.length; i++) { 1977 let c = ca[i]; 1978 while (c.charAt(0) === ' ') c = c.substring(1, c.length); 1979 if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); 1980 } 1981 return null; 1982 } 1983 1984 // Check if widget should be hidden based on user preference 1985 function checkWidgetVisibility() { 1986 const hidden = getCookie('open_accessibility_hidden'); 1987 if (hidden === '1') { 1988 $('.open-accessibility-toggle-button, .open-accessibility-widget-panel').css('display', 'none'); 1989 } else { 1990 $('.open-accessibility-toggle-button').css('display', ''); 1991 } 1992 } 1993 1994 // Initialize on document ready 1995 $(document).ready(function() { 1996 initAccessibility(); 1997 checkWidgetVisibility(); 1998 }); 1999 2000})(jQuery);
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.