PageSourceSearch

https://www.iaprm.org.il/wp-content/plugins/emed-accessability/assets/js/emed-accessability.js?ver=1782727220

js iaprm.org.il collected 2026-10-03 02:49:49 UTC 23,684 bytes, 602 lines download raw bytes

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.