1/* global EmedA11yConfig */ 2(function () { 3 'use strict'; 4 5 var CONFIG = (typeof EmedA11yConfig === 'object' && EmedA11yConfig) || {}; 6 var STORAGE_KEY = CONFIG.storageKey || 'emed-a11y'; 7 var CLASS_KEYS = Array.isArray(CONFIG.classKeys) ? CONFIG.classKeys : []; 8 var STEPPERS = (CONFIG.steppers && typeof CONFIG.steppers === 'object') ? CONFIG.steppers : {}; 9 var I18N = (CONFIG.i18n && typeof CONFIG.i18n === 'object') ? CONFIG.i18n : {}; 10 var LANGUAGES = (CONFIG.languages && typeof CONFIG.languages === 'object') ? CONFIG.languages : {}; 11 var STMT_URLS = (CONFIG.statementUrls && typeof CONFIG.statementUrls === 'object') ? CONFIG.statementUrls : {}; 12 var DEFAULT_LANG = CONFIG.defaultLang || 'he'; 13 var CLASS_PREFIX = 'emed-a11y-'; 14 15 // Elements that get text-size scaling applied (toolbar excluded at runtime). 16 var FONT_SELECTOR = 'p,span,a,li,td,th,h1,h2,h3,h4,h5,h6,label,button,input,select,textarea,em,strong,b,i,small,blockquote,figcaption,dt,dd,caption,abbr,cite,code,div'; 17 18 function readState() { 19 try { 20 var raw = window.localStorage.getItem(STORAGE_KEY); 21 var parsed = raw ? JSON.parse(raw) : null; 22 return (parsed && typeof parsed === 'object') ? parsed : {}; 23 } catch (e) { return {}; } 24 } 25 26 function writeState(state) { 27 try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} 28 } 29 30 function clampStep(stateKey, value) { 31 var meta = STEPPERS[stateKey]; 32 var n = parseInt(value, 10) || 0; 33 if (!meta || !meta.enabled) { return 0; } 34 return Math.max(meta.min, Math.min(meta.max, n)); 35 } 36 37 function init() { 38 var widget = document.getElementById('emed-a11y'); 39 if (!widget) { return; } 40 41 var trigger = document.getElementById('emed-a11y-trigger'); 42 var panel = document.getElementById('emed-a11y-panel'); 43 var root = document.documentElement; 44 45 // Build the map of exclusive sets -> [values] from the rendered buttons. 46 var sets = {}; 47 Array.prototype.forEach.call(widget.querySelectorAll('[data-emed-a11y-set]'), function (btn) { 48 var set = btn.getAttribute('data-emed-a11y-set'); 49 (sets[set] = sets[set] || []).push(btn.getAttribute('data-emed-a11y-value')); 50 }); 51 52 /* ---- State (sanitized against what's actually enabled) ----------- */ 53 var raw = readState(); 54 var state = { 55 color: null, 56 cursor: null, 57 toggles: [], 58 fontStep: clampStep('fontStep', raw.fontStep), 59 zoomStep: clampStep('zoomStep', raw.zoomStep), 60 lang: (raw.lang && I18N[raw.lang]) ? raw.lang : DEFAULT_LANG 61 }; 62 Object.keys(sets).forEach(function (set) { 63 if (raw[set] && sets[set].indexOf(raw[set]) !== -1 && CLASS_KEYS.indexOf(raw[set]) !== -1) { 64 state[set] = raw[set]; 65 } 66 }); 67 if (Array.isArray(raw.toggles)) { 68 state.toggles = raw.toggles.filter(function (k) { return CLASS_KEYS.indexOf(k) !== -1; }); 69 } 70 71 /* ---- Appliers ---------------------------------------------------- */ 72 function applyExclusive(set) { 73 (sets[set] || []).forEach(function (v) { root.classList.remove(CLASS_PREFIX + v); }); 74 if (state[set]) { root.classList.add(CLASS_PREFIX + state[set]); } 75 } 76 77 function applyToggle(key) { 78 var on = state.toggles.indexOf(key) !== -1; 79 root.classList.toggle(CLASS_PREFIX + key, on); 80 if (key === 'tooltips') { tooltips.enable(on); } 81 if (key === 'image-descriptions') { imageCaptions.enable(on); } 82 if (key === 'reading-ruler') { readingRuler.enable(on); } 83 } 84 85 function applyAll() { 86 Object.keys(sets).forEach(applyExclusive); 87 CLASS_KEYS.forEach(function (key) { 88 // Toggles only (exclusive option keys live inside sets[]). 89 var isExclusive = Object.keys(sets).some(function (s) { return sets[s].indexOf(key) !== -1; }); 90 if (!isExclusive) { applyToggle(key); } 91 }); 92 applyFont(state.fontStep); 93 applyZoom(state.zoomStep); 94 applyLanguage(state.lang); 95 updateObserver(); 96 syncControls(); 97 } 98 99 /* ---- Text size (per-element scaling) ----------------------------- */ 100 function scaleNode(el, step) { 101 if (widget.contains(el)) { return; } 102 var base = el.getAttribute('data-emed-a11y-fs'); 103 if (base === null) { 104 base = parseFloat(window.getComputedStyle(el).fontSize); 105 if (!base) { return; } 106 el.setAttribute('data-emed-a11y-fs', base); 107 } else { 108 base = parseFloat(base); 109 } 110 el.style.fontSize = (step === 0) ? '' : (base * (1 + step * 0.1)).toFixed(2) + 'px'; 111 } 112 function applyFont(step) { 113 Array.prototype.forEach.call(document.body.querySelectorAll(FONT_SELECTOR), function (el) { 114 scaleNode(el, step); 115 }); 116 } 117 // Scale text inside (or equal to) each root â used for nodes added later. 118 function scaleWithin(roots, step) {
119 roots.forEach(function (root) { 120 if (root.matches && root.matches(FONT_SELECTOR)) { scaleNode(root, step); } 121 if (root.querySelectorAll) { 122 Array.prototype.forEach.call(root.querySelectorAll(FONT_SELECTOR), function (el) { scaleNode(el, step); }); 123 } 124 }); 125 } 126 127 /* ---- Dynamic content: re-apply captions/scaling to new nodes ----- */ 128 var dynObserver = null; 129 function needsObserver() { 130 return state.toggles.indexOf('image-descriptions') !== -1 || state.fontStep !== 0; 131 } 132 function updateObserver() { 133 if (needsObserver()) { 134 if (!dynObserver) { 135 dynObserver = new MutationObserver(function (muts) { 136 var added = []; 137 muts.forEach(function (m) { 138 Array.prototype.forEach.call(m.addedNodes, function (n) { 139 if (n.nodeType === 1 && !widget.contains(n)) { added.push(n); } 140 }); 141 }); 142 if (!added.length) { return; } 143 if (state.toggles.indexOf('image-descriptions') !== -1) { imageCaptions.captionWithin(added); } 144 if (state.fontStep !== 0) { scaleWithin(added, state.fontStep); } 145 }); 146 } 147 dynObserver.observe(document.body, { childList: true, subtree: true }); 148 } else if (dynObserver) { 149 dynObserver.disconnect(); 150 } 151 } 152 153 /* ---- Screen zoom (whole document, toolbar countered) ------------- */ 154 function applyZoom(step) { 155 var factor = 1 + step * 0.1; 156 document.body.style.zoom = (factor === 1) ? '' : String(factor); 157 widget.style.zoom = (factor === 1) ? '' : String(1 / factor); 158 } 159 160 /* ---- Hover/focus tooltips --------------------------------------- */ 161 var tooltips = (function () { 162 var el = null, active = false; 163 function desc(node) { 164 return node.getAttribute('title') || node.getAttribute('aria-label') || 165 (node.tagName === 'IMG' ? node.getAttribute('alt') : '') || ''; 166 } 167 function ensure() { 168 if (el) { return el; } 169 el = document.createElement('div'); 170 el.className = 'emed-a11y-tip'; 171 el.setAttribute('role', 'tooltip'); 172 document.body.appendChild(el); 173 return el; 174 } 175 function show(e) { 176 var node = e.target.closest ? e.target.closest('[title],[aria-label],img[alt]') : null; 177 if (!node || widget.contains(node)) { return hide(); } 178 var text = desc(node); 179 if (!text) { return hide(); } 180 var box = ensure(); 181 box.textContent = text; 182 var r = node.getBoundingClientRect(); 183 box.style.top = (window.scrollY + r.top - box.offsetHeight - 8) + 'px'; 184 box.style.left = (window.scrollX + r.left) + 'px'; 185 box.classList.add('is-visible'); 186 } 187 function hide() { if (el) { el.classList.remove('is-visible'); } } 188 return { 189 enable: function (on) { 190 if (on === active) { return; } 191 active = on; 192 if (on) { 193 document.addEventListener('mouseover', show, true); 194 document.addEventListener('focusin', show, true); 195 document.addEventListener('mouseout', hide, true); 196 document.addEventListener('focusout', hide, true); 197 } else { 198 document.removeEventListener('mouseover', show, true); 199 document.removeEventListener('focusin', show, true); 200 document.removeEventListener('mouseout', hide, true); 201 document.removeEventListener('focusout', hide, true); 202 hide(); 203 } 204 } 205 }; 206 })(); 207 208 /* ---- Permanent image captions ----------------------------------- */ 209 var imageCaptions = (function () { 210 function captionImg(img) { 211 if (widget.contains(img) || img.getAttribute('data-emed-a11y-cap')) { return; } 212 var alt = (img.getAttribute('alt') || '').trim(); 213 if (!alt) { return; } 214 var cap = document.createElement('span'); 215 cap.className = 'emed-a11y-imgcap'; 216 cap.textContent = alt; 217 img.setAttribute('data-emed-a11y-cap', '1'); 218 if (img.parentNode) { img.parentNode.insertBefore(cap, img.nextSibling); } 219 } 220 // Caption images inside (or equal to) each of the given roots â used 221 // both for the initial pass and for nodes added later (observer). 222 function captionWithin(roots) { 223 roots.forEach(function (root) { 224 if (root.tagName === 'IMG') { captionImg(root); } 225 if (root.querySelectorAll) { 226 Array.prototype.forEach.call(root.querySelectorAll('img[alt]'), captionImg); 227 } 228 }); 229 } 230 return { 231 captionWithin: captionWithin, 232 enable: function (on) { 233 if (on) { 234 captionWithin([document.body]); 235 } else { 236 Array.prototype.forEach.call(document.querySelectorAll('.emed-a11y-imgcap'), function (c) { c.remove(); }); 237 Array.prototype.forEach.call(document.querySelectorAll('[data-emed-a11y-cap]'), function (i) { i.removeAttribute('data-emed-a11y-cap'); }); 238 } 239 } 240 }; 241 })(); 242 243 /* ---- Reading ruler (horizontal guide following the pointer) ------ */ 244 var readingRuler = (function () { 245 var bar = null, active = false;
246 function move(e) { 247 if (!bar) { return; } 248 var y = (e.touches && e.touches[0]) ? e.touches[0].clientY : e.clientY; 249 if (typeof y !== 'number') { return; } 250 bar.style.top = y + 'px'; 251 } 252 return { 253 enable: function (on) { 254 if (on === active) { return; } 255 active = on; 256 if (on) { 257 if (!bar) { 258 bar = document.createElement('div'); 259 bar.className = 'emed-a11y-ruler'; 260 bar.setAttribute('aria-hidden', 'true'); 261 document.body.appendChild(bar); 262 } 263 bar.style.display = 'block'; 264 document.addEventListener('mousemove', move, true); 265 document.addEventListener('touchmove', move, true); 266 } else { 267 document.removeEventListener('mousemove', move, true); 268 document.removeEventListener('touchmove', move, true); 269 if (bar) { bar.style.display = 'none'; } 270 } 271 } 272 }; 273 })(); 274 275 /* ---- Language ---------------------------------------------------- */ 276 // Translate a key in the active language, falling back to default then key. 277 function tr(key) { 278 var d = I18N[state.lang] || I18N[DEFAULT_LANG] || {}; 279 return (d[key] != null) ? d[key] : key; 280 } 281 282 function applyLanguage(lang) { 283 var dict = I18N[lang]; 284 if (!dict) { return; } 285 var meta = LANGUAGES[lang] || {}; 286 287 widget.setAttribute('lang', lang); 288 if (meta.dir) { widget.setAttribute('dir', meta.dir); } 289 var modal = document.getElementById('emed-a11y-report'); 290 if (modal && meta.dir) { modal.setAttribute('dir', meta.dir); } 291 292 Array.prototype.forEach.call(widget.querySelectorAll('[data-emed-i18n]'), function (el) { 293 var v = dict[el.getAttribute('data-emed-i18n')]; 294 if (v != null) { el.textContent = v; } 295 }); 296 Array.prototype.forEach.call(widget.querySelectorAll('[data-emed-i18n-title]'), function (el) { 297 var v = dict[el.getAttribute('data-emed-i18n-title')]; 298 if (v != null) { el.setAttribute('title', v); } 299 }); 300 Array.prototype.forEach.call(widget.querySelectorAll('[data-emed-i18n-aria]'), function (el) { 301 var v = dict[el.getAttribute('data-emed-i18n-aria')]; 302 if (v != null) { el.setAttribute('aria-label', v); } 303 }); 304 // Hint: rebuild with the "Esc" token wrapped in a styled <kbd>. 305 Array.prototype.forEach.call(widget.querySelectorAll('[data-emed-i18n-kbd]'), function (el) { 306 var v = dict[el.getAttribute('data-emed-i18n-kbd')]; 307 if (v == null) { return; } 308 var parts = v.split('Esc'); 309 el.textContent = ''; 310 parts.forEach(function (part, i) { 311 el.appendChild(document.createTextNode(part)); 312 if (i < parts.length - 1) { 313 var k = document.createElement('kbd'); 314 k.className = 'emed-a11y-kbd'; 315 k.textContent = 'Esc'; 316 el.appendChild(k); 317 } 318 }); 319 }); 320 321 // Swap the accessibility-statement link to this language (hide if none). 322 var stmt = widget.querySelector('[data-emed-statement]'); 323 if (stmt) { 324 var url = STMT_URLS[lang] || ''; 325 if (url) { stmt.setAttribute('href', url); stmt.hidden = false; } 326 else { stmt.hidden = true; } 327 } 328 329 Array.prototype.forEach.call(widget.querySelectorAll('[data-emed-a11y-lang]'), function (btn) { 330 btn.setAttribute('aria-pressed', btn.getAttribute('data-emed-a11y-lang') === lang ? 'true' : 'false'); 331 }); 332 333 // The skip link lives outside the widget (top of <body>), so translate it here. 334 var skip = document.getElementById('emed-a11y-skip'); 335 if (skip) { 336 if (meta.dir) { skip.setAttribute('dir', meta.dir); } 337 skip.setAttribute('lang', lang); 338 if (dict['ui.skip'] != null) { skip.textContent = dict['ui.skip']; } 339 } 340 } 341 342 function setLanguage(lang) { 343 if (!I18N[lang] || state.lang === lang) { return; } 344 state.lang = lang; 345 applyLanguage(lang); 346 writeState(state); 347 } 348 349 /* ---- Control state sync ----------------------------------------- */ 350 function syncControls() { 351 Array.prototype.forEach.call(widget.querySelectorAll('[data-emed-a11y-set]'), function (btn) { 352 var set = btn.getAttribute('data-emed-a11y-set'); 353 var on = state[set] === btn.getAttribute('data-emed-a11y-value'); 354 btn.setAttribute('aria-pressed', on ? 'true' : 'false'); 355 }); 356 Array.prototype.forEach.call(widget.querySelectorAll('[data-emed-a11y-toggle]'), function (btn) { 357 var on = state.toggles.indexOf(btn.getAttribute('data-emed-a11y-toggle')) !== -1; 358 btn.setAttribute('aria-pressed', on ? 'true' : 'false'); 359 }); 360 Array.prototype.forEach.call(widget.querySelectorAll('[data-emed-a11y-step-value]'), function (span) { 361 span.textContent = String(state[span.getAttribute('data-emed-a11y-step-value')] || 0); 362 }); 363 // Grey out a stepper button once its bound is reached. 364 Array.prototype.forEach.call(widget.querySelectorAll('[data-emed-a11y-step]'), function (btn) { 365 var key = btn.getAttribute('data-emed-a11y-step'); 366 var meta = STEPPERS[key]; 367 if (!meta) { return; } 368 var val = state[key] || 0; 369 btn.disabled = (btn.getAttribute('data-emed-a11y-dir') === 'dec') ? (val <= meta.min) : (val >= meta.max); 370 }); 371 } 372 373 /* ---- Mutations -------------------------------------------------- */ 374 function setExclusive(set, value) { 375 state[set] = (state[set] === value) ? null : value; 376 applyExclusive(set); 377 syncControls(); 378 writeState(state); 379 } 380
381 function toggleFeature(key) { 382 var i = state.toggles.indexOf(key); 383 if (i === -1) { state.toggles.push(key); } else { state.toggles.splice(i, 1); } 384 applyToggle(key); 385 if (key === 'image-descriptions') { updateObserver(); } 386 syncControls(); 387 writeState(state); 388 } 389 390 function step(stateKey, dir) { 391 state[stateKey] = clampStep(stateKey, (state[stateKey] || 0) + (dir === 'inc' ? 1 : -1)); 392 if (stateKey === 'fontStep') { applyFont(state.fontStep); updateObserver(); } 393 if (stateKey === 'zoomStep') { applyZoom(state.zoomStep); } 394 syncControls(); 395 writeState(state); 396 } 397 398 function resetAll() { 399 // Clear toggle side-effects before wiping state. 400 tooltips.enable(false); 401 imageCaptions.enable(false); 402 readingRuler.enable(false); 403 // Language is a UI preference, not an accessibility adjustment â keep it. 404 state = { color: null, cursor: null, toggles: [], fontStep: 0, zoomStep: 0, lang: state.lang }; 405 Object.keys(sets).forEach(applyExclusive); 406 CLASS_KEYS.forEach(function (k) { root.classList.remove(CLASS_PREFIX + k); }); 407 applyFont(0); 408 applyZoom(0); 409 updateObserver(); 410 syncControls(); 411 writeState(state); 412 } 413 414 /* ---- Panel open / close ----------------------------------------- */ 415 function focusables() { 416 return Array.prototype.slice.call( 417 panel.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])') 418 ).filter(function (el) { return el.offsetParent !== null; }); 419 } 420 function isOpen() { return widget.classList.contains('is-open'); } 421 function openPanel() { 422 widget.classList.add('is-open'); 423 trigger.setAttribute('aria-expanded', 'true'); 424 panel.setAttribute('aria-hidden', 'false'); 425 var f = focusables(); 426 if (f.length) { f[0].focus(); } 427 document.addEventListener('keydown', onKeydown, true); 428 } 429 function closePanel(returnFocus) { 430 widget.classList.remove('is-open'); 431 trigger.setAttribute('aria-expanded', 'false'); 432 panel.setAttribute('aria-hidden', 'true'); 433 document.removeEventListener('keydown', onKeydown, true); 434 if (returnFocus !== false) { trigger.focus(); } 435 } 436 function onKeydown(e) { 437 if (e.key === 'Escape' || e.key === 'Esc') { e.preventDefault(); closePanel(true); return; } 438 if (e.key === 'Tab' && isOpen()) { 439 var f = focusables(); 440 if (!f.length) { return; } 441 var first = f[0], last = f[f.length - 1]; 442 if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } 443 else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } 444 } 445 } 446 447 /* ---- Wire up ----------------------------------------------------- */ 448 trigger.addEventListener('click', function () { isOpen() ? closePanel(true) : openPanel(); }); 449 450 widget.addEventListener('click', function (e) { 451 var btn = e.target.closest ? e.target.closest('button') : null; 452 if (!btn || !widget.contains(btn) || btn === trigger) { return; } 453 454 if (btn.hasAttribute('data-emed-a11y-lang')) { 455 setLanguage(btn.getAttribute('data-emed-a11y-lang')); 456 } else if (btn.hasAttribute('data-emed-a11y-value')) { 457 setExclusive(btn.getAttribute('data-emed-a11y-set'), btn.getAttribute('data-emed-a11y-value')); 458 } else if (btn.hasAttribute('data-emed-a11y-toggle')) { 459 toggleFeature(btn.getAttribute('data-emed-a11y-toggle')); 460 } else if (btn.hasAttribute('data-emed-a11y-step')) { 461 step(btn.getAttribute('data-emed-a11y-step'), btn.getAttribute('data-emed-a11y-dir')); 462 } else if (btn.getAttribute('data-emed-a11y-action') === 'close') { 463 closePanel(true); 464 } else if (btn.getAttribute('data-emed-a11y-action') === 'reset') { 465 resetAll(); 466 } 467 }); 468 469 // Click outside closes. 470 document.addEventListener('click', function (e) { 471 if (isOpen() && !widget.contains(e.target)) { closePanel(false); } 472 }); 473 474 // Esc opens the panel when it (and the report modal) are closed â unless 475 // the visitor is typing in a field. Closing is handled by the panel/modal 476 // own keydown handlers, so this only fires for the open action. 477 document.addEventListener('keydown', function (e) { 478 if (e.key !== 'Escape' && e.key !== 'Esc') { return; } 479 if (isOpen()) { return; } 480 var modal = document.getElementById('emed-a11y-report'); 481 if (modal && modal.classList.contains('is-open')) { return; } 482 var ae = document.activeElement; 483 if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.tagName === 'SELECT' || ae.isContentEditable)) { return; } 484 e.preventDefault(); 485 // Reading mode follows the pointer everywhere, so let Esc exit it first; 486 // otherwise Esc opens the panel. 487 if (state.toggles.indexOf('reading-ruler') !== -1) {
488 toggleFeature('reading-ruler'); 489 } else { 490 openPanel(); 491 } 492 }, true); 493 494 setupSkipLink(); 495 setupReport(); 496 applyAll(); 497 498 /* ---- Skip-to-content link --------------------------------------- */ 499 function setupSkipLink() { 500 var link = document.getElementById('emed-a11y-skip'); 501 if (!link) { return; } 502 var target = document.querySelector('main, [role="main"], #main, #content, #primary'); 503 if (!target) { return; } // no landmark found â leave the link hidden 504 if (!target.id) { target.id = 'emed-a11y-main'; } 505 link.setAttribute('href', '#' + target.id); 506 if (!target.hasAttribute('tabindex')) { target.setAttribute('tabindex', '-1'); } 507 link.hidden = false; 508 } 509 510 /* ---- Report-a-violation modal ----------------------------------- */ 511 function setupReport() { 512 var modal = document.getElementById('emed-a11y-report'); 513 if (!modal) { return; } 514 515 var form = modal.querySelector('.emed-a11y-report-form'); 516 var status = modal.querySelector('.emed-a11y-report-status'); 517 var lastFocus = null; 518 519 function modalFocusables() { 520 return Array.prototype.slice.call( 521 modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])') 522 ).filter(function (el) { return el.offsetParent !== null; }); 523 } 524 function onModalKeydown(e) { 525 if (e.key === 'Escape' || e.key === 'Esc') { e.preventDefault(); close(); return; } 526 if (e.key === 'Tab') { 527 var f = modalFocusables(); 528 if (!f.length) { return; } 529 var first = f[0], last = f[f.length - 1]; 530 if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } 531 else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } 532 } 533 } 534 function open() { 535 lastFocus = document.activeElement; 536 if (isOpen()) { closePanel(false); } // close the side panel behind it 537 modal.classList.add('is-open'); 538 modal.setAttribute('aria-hidden', 'false'); 539 if (status) { status.textContent = ''; status.className = 'emed-a11y-report-status'; } 540 var f = modalFocusables(); 541 if (f.length) { f[0].focus(); } 542 document.addEventListener('keydown', onModalKeydown, true); 543 } 544 function close() { 545 modal.classList.remove('is-open'); 546 modal.setAttribute('aria-hidden', 'true'); 547 document.removeEventListener('keydown', onModalKeydown, true); 548 if (lastFocus && lastFocus.focus) { lastFocus.focus(); } 549 } 550 551 modal.addEventListener('click', function (e) { 552 var t = e.target.closest ? e.target.closest('[data-emed-a11y-action="report-close"]') : null; 553 if (t) { close(); } 554 }); 555 // Open trigger lives inside the panel (button delegation in widget). 556 widget.addEventListener('click', function (e) { 557 var btn = e.target.closest ? e.target.closest('[data-emed-a11y-action="report-open"]') : null; 558 if (btn) { open(); } 559 }); 560 561 if (form) { 562 form.addEventListener('submit', function (e) { 563 e.preventDefault(); 564 if (!CONFIG.ajaxUrl) { return; } 565 566 var data = new FormData(form); 567 data.append('action', CONFIG.reportAction || 'emed_a11y_report'); 568 data.append('nonce', CONFIG.reportNonce || ''); 569 data.append('page_url', window.location.href); 570 data.append('lang', state.lang || DEFAULT_LANG); 571 572 var submit = form.querySelector('.emed-a11y-report-submit'); 573 if (submit) { submit.disabled = true; } 574 if (status) { status.className = 'emed-a11y-report-status'; status.textContent = tr('ui.rf_sending'); } 575 576 fetch(CONFIG.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: data }) 577 .then(function (r) { return r.json(); }) 578 .then(function (res) { 579 var ok = res && res.success; 580 var msg = (res && res.data && res.data.message) ? res.data.message 581 : tr(ok ? 'ui.rf_success' : 'ui.rf_error'); 582 if (status) { 583 status.textContent = msg; 584 status.className = 'emed-a11y-report-status ' + (ok ? 'is-success' : 'is-error'); 585 } 586 if (ok) { form.reset(); } 587 }) 588 .catch(function () { 589 if (status) { status.textContent = tr('ui.rf_neterror'); status.className = 'emed-a11y-report-status is-error'; } 590 }) 591 .then(function () { if (submit) { submit.disabled = false; } }); 592 }); 593 } 594 } 595 } 596 597 if (document.readyState === 'loading') { 598 document.addEventListener('DOMContentLoaded', init); 599 } else { 600 init(); 601 } 602})();
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.