PluginProbe
WP Accessibility / trunk
WP Accessibility vtrunk
2.3.5 trunk 1.0.0 1.2.3 1.2.9 1.3.0 1.3.11 1.4.0 1.4.6 1.5.0 1.5.10 1.6.0 1.6.10 1.7.0 1.7.14 1.8.0 1.8.1 1.9.0 1.9.2 2.0.0 2.0.1 2.1.0 2.1.19 2.2.0 2.2.5 All 29 releases
wp-accessibility / js / wp-accessibility.js

wp-accessibility.js in WP Accessibility trunk, at js/wp-accessibility.js

923 lines 33.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (() => {
2 'use strict';
3 const html = document.querySelector( 'html' );
4 const body = document.querySelector( 'body' );
5 let errors = [];
6 if ( wpa.lang ) {
7 let lang = html.getAttribute( 'lang' );
8 if ( ! lang ) {
9 html.setAttribute( 'lang', wpa.lang );
10 if ( wpa.errors || wpa.tracking ) {
11 errors.push( 'html-lang' );
12 console.log( 'HTML language set by WP Accessibility' );
13 }
14 }
15 }
16
17 if ( wpa.dir ) {
18 let dir = html.getAttribute( 'dir' );
19 if ( ! dir && wpa.dir !== 'ltr' ) {
20 html.setAttributeattr( 'dir', wpa.dir );
21 if ( wpa.errors || wpa.tracking ) {
22 errors.push( 'html-lang-direction' );
23 console.log( 'HTML language direction set by WP Accessibility' );
24 }
25 }
26 }
27
28 if ( wpa.continue ) {
29 let readMore = document.querySelectorAll( '.wp-block-post-excerpt__more-link' );
30 if ( readMore.length !== 0 ) {
31 readMore.forEach( (el) => {
32 if ( ! el.hasAttribute( 'aria-describedby' ) ) {
33 let post = el.closest( '.wp-block-post' );
34 let readMoreId = post.getAttribute( 'class' );
35 readMoreId = readMoreId.replaceAll( ' ', '-' );
36 let heading = post.querySelector( '.wp-block-post-title' );
37 if ( heading ) {
38 if ( ! heading.hasAttribute( 'id' ) ) {
39 heading.setAttribute( 'id', readMoreId );
40 } else {
41 readMoreId = heading.getAttribute( 'id' );
42 }
43 el.setAttribute( 'aria-describedby', readMoreId );
44 if ( wpa.errors || wpa.tracking ) {
45 console.log( 'Continue Reading link description set by WP Accessibility' );
46 }
47 }
48 }
49 });
50 }
51 }
52
53 const viewport = document.querySelector( 'meta[name="viewport"]' );
54 if ( viewport && wpa.viewport ) {
55 let conditionsBefore = viewport.getAttribute( 'content' );
56 let conditionsAfter = viewport.getAttribute( 'content' );
57 if ( conditionsBefore.search(/user-scalable=no/g) ) {
58 conditionsAfter = conditionsBefore.replace( 'user-scalable=no', 'user-scalable=yes' );
59 viewport.setAttribute( 'content', conditionsAfter );
60 if ( ( wpa.errors || wpa.tracking ) && conditionsAfter != conditionsBefore ) {
61 errors.push( 'viewport-scalable' );
62 console.log( 'Viewport made scalable by WP Accessibility' );
63 }
64 }
65 if ( conditionsBefore.search(/maximum-scale=1/g) ) {
66 conditionsAfter = conditionsBefore.replace( 'maximum-scale=1', 'maximum-scale=5' );
67 conditionsAfter = conditionsAfter.replace( 'maximum-scale=0', 'maximum-scale=5' );
68 viewport.setAttribute( 'content', conditionsAfter );
69 if ( ( wpa.errors || wpa.tracking ) && conditionsAfter != conditionsBefore ) {
70 errors.push( 'viewport-maxscale' );
71 console.log( 'Viewport maximum scale set by WP Accessibility' );
72 }
73 }
74 }
75
76 if ( wpa.skiplinks.enabled ) {
77 body.insertAdjacentHTML( 'afterbegin', wpa.skiplinks.output );
78 if ( wpa.errors || wpa.tracking ) {
79 errors.push( 'skiplinks' );
80 console.log( 'Skip links added by WP Accessibility' );
81 }
82 }
83
84 if ( wpa.current ) {
85 let current = document.querySelectorAll( '.current-menu-item a, .current_page_item a' );
86 if ( current.length !== 0 ) {
87 current.forEach((el) => {
88 el.setAttribute( 'aria-current', 'page' );
89 });
90 if ( wpa.errors || wpa.tracking ) {
91 errors.push( 'aria-current' );
92 console.log( 'ARIA current added by WP Accessibility' );
93 }
94 }
95 }
96
97 if ( wpa.labels ) {
98 let wpa_names = [ 's', 'author', 'email', 'url', 'comment' ];
99 wpa_names.forEach((value) => {
100 let fields, field_id, implicit, aria, ariaId, ariaTarget, hasAria, hasAriaId, label, labelText;
101 if ( value == 'comment' ) {
102 fields = document.querySelectorAll( 'textarea[name=' + value + ']' );
103 } else {
104 fields = document.querySelectorAll( 'input[name=' + value + ']:not(#adminbar-search)' );
105 }
106 if ( fields.length !== 0 ) {
107 fields.forEach( (field) => {
108 if ( 0 !== field.length ) {
109 field_id = field.getAttribute( 'id' );
110 implicit = field.closest( 'label' );
111 aria = field.getAttribute( 'aria-label' );
112 ariaId = field.getAttribute( 'aria-labelledby' );
113 ariaTarget = {};
114 if ( ariaId ) {
115 ariaTarget = document.getElementById( ariaId );
116 }
117 hasAria = ( null == aria || '' == aria || 'undefined' == typeof( aria ) ) ? false : true;
118 hasAriaId = ( null == ariaId || '' == ariaId || 'undefined' == typeof( ariaId ) ) ? false : true;
119 // Add label if aria label empty, aria labelledby empty, or aria reference ID does not exist.
120 if ( ( ! hasAria && ! hasAriaId ) || ( ! hasAria && ( hasAriaId && ! ariaTarget ) ) ) {
121 if ( field_id ) {
122 label = document.querySelector( 'label[for=' + field_id + ']' );
123 if ( label ) {
124 labelText = label.innerText;
125 if ( label && ! labelText ) {
126 label.innerText = wpa.wpalabels[value];
127 if ( wpa.errors || wpa.tracking ) {
128 errors.push( ['empty-label', wpa.wpalabels[value]] );
129 console.log( 'Empty label on ' + wpa.wpalabels[value] + ' added by WP Accessibility' );
130 }
131 }
132 }
133 if ( !label && !implicit ) {
134 field.insertAdjacentHTML( 'beforebegin', "<label for='" + field_id + "' class='wpa-screen-reader-text'>" + wpa.wpalabels[value] + "</label>" );
135 if ( wpa.errors || wpa.tracking ) {
136 errors.push( ['explicit-label', wpa.wpalabels[value]] );
137 console.log( 'Explicit label on ' + wpa.wpalabels[value] + ' added by WP Accessibility' );
138 }
139 }
140 } else {
141 if ( !implicit ) {
142 field.setAttribute( 'id', 'wpa_label_' + value );
143 field.insertAdjacentHTML( 'beforebegin', "<label for='wpa_label_" + value + "' class='wpa-screen-reader-text'>" + wpa.wpalabels[value] + "</label>" );
144 if ( wpa.errors || wpa.tracking ) {
145 errors.push( ['implicit-label', wpa.wpalabels[value]] );
146 console.log( 'Implicit label on ' + wpa.wpalabels[value] + ' added by WP Accessibility' );
147 }
148 }
149 }
150 }
151 }
152 });
153 }
154 });
155 }
156
157 if ( wpa.titles ) {
158 let images = 0,
159 controls = 0,
160 fields = 0,
161 noremove = false;
162 const els = document.querySelectorAll( 'img, a, input, textarea, select, button' );
163 if ( els.length !== 0 ) {
164 els.forEach((el) => {
165 let title = el.getAttribute( 'title' );
166 if ( el.classList.contains( 'nturl' ) ) {
167 // Exempt title attributes from Translate WordPress - Google Language Translator, which uses them as a CSS hook.
168 noremove = true;
169 }
170 if ( title && '' !== title ) {
171 switch ( el.tagName ) {
172 case 'IMG':
173 // If image has alt, remove title. If not, set title as alt.
174 let alt = el.getAttribute( 'alt' );
175 title = el.getAttribute( 'title' );
176 if ( ! alt || '' === alt ) {
177 el.setAttribute( 'alt', title );
178 el.removeAttribute( 'title' );
179 } else {
180 if ( title === alt ) {
181 el.removeAttribute( 'title' );
182 }
183 }
184 images++;
185 break;
186 case 'A':
187 case 'BUTTON':
188 // If link or button has contained text or an img with alt, remove title. Otherwise, set title as aria-label unless element already has aria-label.
189 let linkText = wpaElementText(el);
190 if ( ! linkText || '' === linkText ) {
191 let ariaLabel = el.getAttribute( 'aria-label' );
192 if ( ! ariaLabel || '' === ariaLabel ) {
193 el.setAttribute( 'aria-label', title );
194 if ( ! noremove ) {
195 el.removeAttribute( 'title' );
196 }
197 }
198 } else {
199 el.removeAttribute( 'title' );
200 }
201 controls++;
202 break;
203 case 'INPUT':
204 case 'SELECT':
205 case 'TEXTAREA':
206 // If input field has an aria-label, aria-labelledby, associated label, or wrapping label, remove title. Else, add title as aria-label.
207 let ariaLabel = el.getAttribute( 'aria-label' );
208 let ariaLabelled = el.getAttribute( 'aria-labelledby' );
209 let ariaLabeller = ( ariaLabelled ) ? document.getElementById( ariaLabelled ) : false;
210 let labelId = el.getAttribute( 'id' );
211 let label = ( labelId ) ? document.querySelector( 'label[for="' + labelId + '"]' ) : false;
212 let parentLabel = el.closest( 'label' );
213 let hasAriaLabel = ( ariaLabel && '' !== ariaLabel ) ? true : false;
214 let hasRealLabel = ( label && '' !== wpaElementText( label ) ) ? true : false;
215 let hasImplicitLabel = ( parentLabel && '' !== wpaElementText( parentLabel ) ) ? true : false;
216 let hasAriaLabelled = ( ariaLabeller && '' !== wpaElementText( ariaLabeller ) ) ? true : false;
217 if ( hasAriaLabel || hasRealLabel || hasImplicitLabel || hasAriaLabelled ) {
218 // This has a label.
219 el.removeAttribute( 'title' );
220 } else {
221 el.setAttribute( 'aria-label', title );
222 el.removeAttribute( 'title' );
223 }
224 fields++;
225 break;
226 }
227 }
228 });
229 if ( wpa.errors || wpa.tracking ) {
230 if ( images > 0 ) {
231 errors.push( ['images-titles', images] );
232 console.log( images + ' title attributes removed from images by WP Accessibility' );
233 }
234 if ( controls > 0 ) {
235 errors.push( ['control-titles', controls] );
236 console.log( controls + ' title attributes removed from links and buttons by WP Accessibility' );
237 }
238 if ( fields > 0 ) {
239 errors.push( ['input-titles', fields] );
240 console.log( fields + ' title attributes removed from input fields by WP Accessibility' );
241 }
242 }
243 }
244 }
245
246 if ( wpa.target ) {
247 let targeted = document.querySelectorAll('a:not(.wpa-allow-target)');
248 let targetRemoved = 0;
249 if ( targeted.length !== 0 ) {
250 targeted.forEach( (el) => {
251 let target = el.getAttribute( 'target' );
252 let href = el.getAttribute( 'href' );
253
254 if ( target ) {
255 // For on-page fragments.
256 if ( href.startsWith( '#') ) {
257 el.removeAttribute( 'target' );
258 } else {
259 try {
260 let url = new URL( href );
261 let hostname = url.hostname;
262 if ( ! hostname.includes( 'facebook' ) ) {
263 el.removeAttribute( 'target' );
264 targetRemoved++;
265 }
266 } catch (exception) {
267 console.log( "href value is not a URL or a fragment:", href );
268 // No action; the `href` attribute didn't resolve as a URL.
269 }
270 }
271 }
272 });
273 if ( targetRemoved > 0 && ( wpa.errors || wpa.tracking ) ) {
274 errors.push( ['link-targets', targetRemoved] );
275 console.log( targetRemoved + ' target attributes removed from links by WP Accessibility' );
276 }
277 }
278 }
279
280 if ( wpa.tabindex ) {
281 // Remove tabindex from elements that should be natively focusable.
282 let focusable = document.querySelectorAll('input:not(.wpa-ignore-tabindex),a[href]:not(.wpa-ignore-tabindex),select:not(.wpa-ignore-tabindex),textarea:not(.wpa-ignore-tabindex),button:not(.wpa-ignore-tabindex)');
283 let tabRemoved = 0;
284 if ( focusable.length !== 0 ) {
285 focusable.forEach( (el) => {
286 let tabindex = el.getAttribute('tabindex');
287 let skip = false;
288 // Check for intentionally hidden elements with tabindex of -1.
289 if ( el.hasAttribute('tabindex') && tabindex === '-1' ) {
290 let hasAriaHidden = el.getAttribute('aria-hidden');
291 let hasHidden = el.hasAttribute('hidden');
292 if ( hasAriaHidden === 'true' || hasHidden ) {
293 skip = true;
294 }
295 }
296 if ( tabindex && !skip ) {
297 el.removeAttribute('tabindex');
298 tabRemoved++;
299 }
300 });
301
302 if ( tabRemoved > 0 && ( wpa.errors || wpa.tracking ) ) {
303 errors.push( ['control-tabindex', tabRemoved] );
304 console.log( tabRemoved + ' tabindex attributes removed from links, buttons and inputs by WP Accessibility' );
305 }
306 }
307
308 // Add tabindex to elements that appear active but are not natively focusable.
309 let fakeButtons = document.querySelectorAll( '[role="button"]:not([tabindex]):not(a):not(button)' );
310 let buttonLinks = document.querySelectorAll( 'a[role="button"]:not([tabindex]):not([href])');
311 let fakeLinks = document.querySelectorAll( '[role="link"]:not([tabindex]):not(a)');
312 let linkLinks = document.querySelectorAll( 'a:not([href]):not([tabindex])' );
313 if ( fakeButtons.length !== 0 ) {
314 fakeButtons.forEach( (el) => {
315 el.setAttribute( 'tabindex', '0' );
316 el.classList.add('wpa-focusable');
317 });
318 if ( fakeButtons.length > 0 && ( wpa.errors || wpa.tracking ) ) {
319 errors.push( ['button-add-tabindex', fakeButtons.length] );
320 console.log( fakeButtons.length + ' tabindex attributes added to divs with the button role by WP Accessibility' );
321 }
322 }
323 if ( buttonLinks.length !== 0 ) {
324 buttonLinks.forEach( (el) => {
325 el.setAttribute( 'tabindex', '0' );
326 el.classList.add('wpa-focusable');
327 });
328 if ( buttonLinks.length > 0 && ( wpa.errors || wpa.tracking ) ) {
329 errors.push( ['link-add-tabindex', buttonLinks.length] );
330 console.log( buttonLinks.length + ' tabindex attributes added to anchor elements with the button role and no href value by WP Accessibility' );
331 }
332 }
333 if ( fakeLinks.length !== 0 ) {
334 fakeLinks.forEach( (el) => {
335 el.setAttribute( 'tabindex', '0' );
336 el.classList.add('wpa-focusable');
337 });
338 if ( fakeLinks.length > 0 && ( wpa.errors || wpa.tracking ) ) {
339 errors.push( ['fakelink-add-tabindex', fakeLinks.length] );
340 console.log( fakeLinks.length + ' tabindex attributes added to elements with the link role not using the a element by WP Accessibility' );
341 }
342 }
343 if ( linkLinks.length !== 0 ) {
344 linkLinks.forEach( (el) => {
345 el.setAttribute( 'tabindex', '0' );
346 el.classList.add('wpa-focusable');
347 });
348 if ( linkLinks.length > 0 && ( wpa.errors || wpa.tracking ) ) {
349 errors.push( ['links-add-tabindex', linkLinks.length] );
350 console.log( linkLinks.length + ' tabindex attributes added to a with no href attribute by WP Accessibility' );
351 }
352 }
353 }
354
355 // If stats are disabled, skip this.
356 if ( 'disabled' !== wpa.url ) {
357 /*
358 * fingerprintJS 0.1 - Fast browser fingerprint library
359 * https://github.com/Valve/fingerprintJS
360 * Copyright (c) 2013 Valentin Vasilyev (iamvalentin@gmail.com)
361 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
362 * Modified by Joe Dolson 2023.
363 */
364 class Fingerprint {
365 constructor(hasher) {
366 var nativeForEach = Array.prototype.forEach;
367 var nativeMap = Array.prototype.map;
368 this.each = function (obj, iterator, context) {
369 if (obj == null) return;
370 if (nativeForEach && obj.forEach === nativeForEach) {
371 obj.forEach(iterator, context);
372 } else if (obj.length === +obj.length) {
373 for (var i = 0, l = obj.length; i < l; i++) {
374 if (JSON.stringify(iterator.call(context, obj[i], i, obj)) === '{}') return;
375 }
376 } else {
377 for (var key in obj) {
378 if (obj.hasOwnProperty(key)) {
379 if (JSON.stringify(iterator.call(context, obj[key], key, obj)) === '{}') return;
380 }
381 }
382 }
383 };
384 this.map = function (obj, iterator, context) {
385 var results = [];
386 if (obj == null) return results;
387 if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
388 this.each(obj, function (value, index, list) {
389 results[results.length] = iterator.call(context, value, index, list);
390 });
391 return results;
392 };
393
394 if (hasher) {
395 this.hasher = hasher;
396 }
397 }
398 get() {
399 let keys = [];
400 keys.push(navigator.userAgent);
401 keys.push([screen.height, screen.width, screen.colorDepth].join('x'));
402 keys.push(new Date().getTimezoneOffset());
403 keys.push(!!window.sessionStorage);
404 keys.push(!!window.localStorage);
405 var pluginsString = this.map(navigator.plugins, function (p) {
406 var mimeTypes = this.map(p, function (mt) {
407 return [mt.type, mt.suffixes].join('~');
408 }).join(',');
409 return [p.name, p.description, mimeTypes].join('::');
410 }, this).join(';');
411 keys.push(pluginsString);
412 if (this.hasher) {
413 return this.hasher(keys.join('###'), 31);
414 } else {
415 return this.murmurhash3_32_gc(keys.join('###'), 31);
416 }
417 }
418 /**
419 * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011)
420 *
421 * @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
422 * @see http://github.com/garycourt/murmurhash-js
423 * @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
424 * @see http://sites.google.com/site/murmurhash/
425 *
426 * @param {string} key ASCII only
427 * @param {number} seed Positive integer only
428 * @return {number} 32-bit positive integer hash
429 */
430 murmurhash3_32_gc(key, seed) {
431 let remainder, bytes, h1, h1b, c1, c2, k1, i;
432
433 remainder = key.length & 3; // key.length % 4
434 bytes = key.length - remainder;
435 h1 = seed;
436 c1 = 0xcc9e2d51;
437 c2 = 0x1b873593;
438 i = 0;
439
440 while (i < bytes) {
441 k1 =
442 ((key.charCodeAt(i) & 0xff)) |
443 ((key.charCodeAt(++i) & 0xff) << 8) |
444 ((key.charCodeAt(++i) & 0xff) << 16) |
445 ((key.charCodeAt(++i) & 0xff) << 24);
446 ++i;
447
448 k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff;
449 k1 = (k1 << 15) | (k1 >>> 17);
450 k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff;
451
452 h1 ^= k1;
453 h1 = (h1 << 13) | (h1 >>> 19);
454 h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff;
455 h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16));
456 }
457
458 k1 = 0;
459
460 switch (remainder) {
461 case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
462 case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
463 case 1: k1 ^= (key.charCodeAt(i) & 0xff);
464
465 k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
466 k1 = (k1 << 15) | (k1 >>> 17);
467 k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
468 h1 ^= k1;
469 }
470
471 h1 ^= key.length;
472
473 h1 ^= h1 >>> 16;
474 h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
475 h1 ^= h1 >>> 13;
476 h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff;
477 h1 ^= h1 >>> 16;
478
479 return h1 >>> 0;
480 }
481 }
482
483 let fingerprint = new Fingerprint().get();
484
485 function logStats(event,fingerprint,type='event') {
486 event = JSON.stringify( event );
487 const data = new FormData();
488 data.append( 'action', wpa.action );
489 data.append( 'security', wpa.security );
490 data.append( 'stats', event );
491 data.append( 'post_id', wpa.post_id );
492 data.append( 'title', fingerprint );
493 data.append( 'type', type );
494 fetch( wpa.ajaxurl, {
495 method: 'POST',
496 body: data
497 });
498 }
499 let contrastButton = document.querySelector( '.toggle-contrast' );
500 if ( contrastButton ) {
501 contrastButton.addEventListener('click', function () {
502 let event;
503 // This fires after the contrast change happens, and the ID is already changed.
504 if ( this.getAttribute( 'data-id' ) == "is_normal_contrast") {
505 // high contrast turned on.
506 event = {'contrast' : 'disabled'};
507 } else {
508 // high contrast turned off.
509 event = {'contrast' : 'enabled'};
510 }
511 logStats(event,fingerprint);
512 });
513 }
514
515 let fontsizeButton = document.querySelector( '.toggle-fontsize' );
516 if ( fontsizeButton ) {
517 fontsizeButton.addEventListener('click', function () {
518 let event;
519 // This fires after the fontsize change happens, and the ID is already changed.
520 if ( this.getAttribute( 'data-id' ) == "is_normal_fontsize") {
521 // fontsizes turned on.
522 event = {'fontsize' : 'disabled'};
523 } else {
524 // fontsizes turned off.
525 event = {'fontsize' : 'enabled'};
526 }
527 logStats(event,fingerprint);
528 });
529 }
530
531 wpaWaitForElement('.wpa-ld button').then((el) => {
532 el.addEventListener( 'click', function(e) {
533 // For descriptions, we aren't concerned about state changes; just usage.
534 let visible = ( 'true' === el.getAttribute( 'aria-expanded' ) ) ? true : false;
535 if ( visible ) {
536 let img = el.closest( 'div' );
537 let image_id = img.getAttribute( 'class' ).replace( 'wpa-ld wp-image-', '' );
538 let event = { 'longdesc' : image_id };
539 logStats(event,fingerprint);
540 }
541 });
542 });
543
544 let altButtons = document.querySelectorAll( '.wpa-alt button' );
545 if ( altButtons.length > 0 ) {
546 altButtons.forEach( (el) => {
547 el.addEventListener( 'click', function(e) {
548 // For alt text, we aren't concerned about state changes; just usage.
549 let visible = ( 'true' === el.getAttribute( 'aria-expanded' ) ) ? true : false;
550 if ( visible ) {
551 let img = el.closest( 'div' );
552 let image_id = img.getAttribute( 'class' ).replace( 'wpa-alt wp-image-', '' );
553 let event = { 'alttext' : image_id };
554 logStats(event,fingerprint);
555 }
556 });
557 });
558 }
559 if ( wpa.tracking && errors.length >= 1 ) {
560 logStats(errors,wpa.url,'view');
561 }
562
563 }
564 if ( wpa.underline.enabled ) {
565 // Underline any link not inside a `nav` region. Using JS for this avoids problems with cascade precedence.
566 let targetEls = document.querySelectorAll( wpa.underline.target + ':not(nav ' + wpa.underline.target + '):not(#wpadminbar a), .wpa-focusable[role=link]' );
567 if ( targetEls.length > 0 ) {
568 targetEls.forEach( (el) => {
569 let originalOutline = el.style.outlineWidth;
570 let originalOffset = el.style.outlineOffset;
571 let textColor = el.style.color;
572 let originalColor = el.style.outlineColor;
573 el.style.textDecoration = 'underline';
574 el.addEventListener( 'mouseenter', function() {
575 this.style.textDecoration = 'none';
576 });
577 el.addEventListener( 'mouseleave', function() {
578 this.style.textDecoration = 'underline';
579 });
580 el.addEventListener( 'focusin', function() {
581 let newOutline = '2px';
582 if ( originalOutline == '2px' ) {
583 newOutline = '4px';
584 }
585 // Ensure there's a visible change of appearance on hover or focus.
586 this.style.outlineWidth = newOutline;
587 this.style.outlineColor = textColor;
588 this.style.outlineOffset = '2px';
589 });
590 el.addEventListener( 'focusout', function() {
591 // Reset visible appearance on exit.
592 this.style.outlineWidth = originalOutline;
593 this.style.outlineColor = originalColor;
594 this.style.outlineOffset = originalOffset;
595 });
596 });
597 }
598 }
599
600 if ( wpa.videos ) {
601 // Add a pause/play button to autoplaying videos without controls.
602 let motionQuery = matchMedia( '(prefers-reduced-motion)' );
603 let initialState = 'false';
604 let autoplayVideos = document.querySelectorAll( 'video[autoplay]:not([controls])' );
605 if ( autoplayVideos.length > 0 ) {
606 autoplayVideos.forEach( (el) => {
607 if ( motionQuery.matches ) {
608 el.pause();
609 initialState = 'true';
610 }
611 let parentEl = el.parentElement;
612 let pauseButton = document.createElement( 'button' );
613 let buttonIcon = document.createElement( 'span' );
614 let buttonText = document.createElement( 'span' );
615 pauseButton.setAttribute( 'type', 'button' );
616 pauseButton.classList.add( 'wpa-video' );
617 pauseButton.setAttribute( 'aria-pressed', initialState );
618 buttonIcon.classList.add( 'dashicons-controls-pause', 'dashicons' );
619 buttonIcon.setAttribute( 'aria-hidden', 'true' );
620 buttonText.classList.add( 'screen-reader-text' );
621 buttonText.innerText = wpa.pause;
622 pauseButton.append( buttonIcon, buttonText );
623 parentEl.append( pauseButton );
624 pauseButton.addEventListener( 'click', function() {
625 let pressed = this.getAttribute( 'aria-pressed' );
626 console.log( pressed );
627 if ( 'true' === pressed ) {
628 el.play();
629 this.setAttribute( 'aria-pressed', 'false' );
630 } else {
631 el.pause();
632 this.setAttribute( 'aria-pressed', 'true' );
633 }
634 });
635 });
636 }
637 }
638
639 if ( wpa.alt ) {
640 let selector = ( wpa.altSelector ) ? wpa.altSelector : '.hentry img[alt]:not([alt=""]), .comment-content img[alt]:not([alt=""]), #content img[alt]:not([alt=""]),.entry-content img[alt]:not([alt=""])';
641 let collection = document.querySelectorAll( selector );
642 let wrap = (elem, wrapper) => {
643 elem.parentElement.insertBefore(wrapper, elem);
644 wrapper.appendChild(elem);
645 };
646 collection.forEach((el) => {
647 let img = el;
648 let closestLink = img.closest( 'a' );
649 let closestButton = img.closest( 'button' );
650 let inLink = ( closestLink ) ? true : false;
651 let inButton = ( closestButton ) ? true : false;
652 let width = img.width;
653 let height = img.height;
654 if ( width || height ) {
655 width = ( ! width ) ? 56 : width; // Enough width for button to be visible.
656 height = ( ! height ) ? 56 : height; // Enough height for button & first line to be visible.
657 let area = height * width;
658 if ( area < ( 150 * 300 ) ) {
659 // Small images won't get displayed alt text containers.
660 return;
661 }
662 if ( el.classList.contains( 'wpa-skip' ) || el.closest( '.wpa-skip' ) ) {
663 // Skip images that have been marked to skip.
664 return;
665 }
666 }
667 let alt = img.getAttribute('alt');
668
669 let classes = [...img.classList];
670 img.setAttribute('class', '');
671 let elContainer = document.createElement( 'div' );
672 elContainer.classList.add( 'wpa-alt' );
673 if ( inLink || inButton ) {
674 let wrapper = ( inLink ) ? closestLink : closestButton;
675 wrap( wrapper, elContainer );
676 } else {
677 wrap( img, elContainer );
678 }
679 classes.forEach(className => {
680 elContainer.classList.add(className);
681 });
682
683 elContainer.insertAdjacentHTML('afterbegin', '<button aria-expanded="false" type="button" class="wpa-toggle">alt</button>');
684 elContainer.insertAdjacentHTML('beforeend', '<div class="wpa-alt-text"></div>');
685 let container = elContainer.querySelector('.wpa-alt-text');
686 container.style.display = 'none';
687 container.innerText = alt;
688 let button = elContainer.querySelector('button');
689 button.addEventListener( 'click', function(e) {
690 let visible = container.checkVisibility();
691 if ( visible ) {
692 this.setAttribute( 'aria-expanded', 'false' );
693 container.style.display = 'none';
694 } else {
695 this.setAttribute( 'aria-expanded', 'true' );
696 container.style.display = 'block';
697 }
698 });
699 });
700 }
701
702 if ( wpa.ldType ) {
703 let wrap = (elem, wrapper) => {
704 elem.parentElement.insertBefore(wrapper, elem);
705 wrapper.appendChild(elem);
706 };
707
708 let longDescImgs = document.querySelectorAll( 'img[longdesc]' );
709 if ( 'link' === wpa.ldType ) {
710 // Links with an actual longdesc attribute.
711 if ( longDescImgs.length > 0 ) {
712 longDescImgs.forEach( (el) => {
713 let wrapper = document.createElement( 'div' );
714 wrapper.classList.add( 'wpa-ld' );
715 let longdesc = el.getAttribute('longdesc');
716 let alt = el.getAttribute('alt');
717 let classes = [...el.classList];
718 wrap(el,wrapper);
719 classes.forEach(className => {
720 wrapper.classList.add(className);
721 });
722 el.setAttribute('class', '');
723 let newLink = document.createElement( 'a' );
724 newLink.setAttribute( 'href', longdesc );
725 newLink.classList.add( 'longdesc-link' );
726 let span = document.createElement( 'span' );
727 span.classList.add( 'screen-reader-text' );
728 span.textContent = ' of ' + alt;
729 newLink.textContent = 'Description';
730 newLink.appendChild( span );
731 el.insertAdjacentElement( 'afterend', newLink );
732 });
733 }
734 // Links with the block style.
735 let hasStyleLongdesc = document.querySelectorAll( 'figure.is-style-longdesc' );
736 if ( hasStyleLongdesc.length > 0 ) {
737 hasStyleLongdesc.forEach( (el) => {
738 let img = el.querySelector( 'img' );
739 wpa_load_image_control( img );
740 });
741 }
742
743 } else {
744 // Handle longdescriptions with buttons.
745 if ( longDescImgs.length > 0 ) {
746 longDescImgs.forEach( (img) => {
747 wpa_load_image_control( img );
748 });
749 }
750
751 // Links with the block style.
752 let hasStyleLongdesc = document.querySelectorAll( 'figure.is-style-longdesc' );
753 if ( hasStyleLongdesc.length > 0 ) {
754 hasStyleLongdesc.forEach( (el) => {
755 let img = el.querySelector( 'img' );
756 wpa_load_image_control( img );
757 });
758 }
759 }
760
761 function wpa_load_image_control( img ) {
762 let classes = img.getAttribute( 'class' );
763 let idAtt = img.getAttribute( 'id' );
764 let id;
765 if ( idAtt && idAtt.includes( 'longdesc' ) ) {
766 id = idAtt.replace( 'longdesc-return-', '' );
767 } else {
768 if ( null === classes || '' === classes ) {
769 parent = img.closest( '.wpa-alt' );
770 classes = parent.getAttribute( 'class' ).replace( 'wpa-alt ', '' );
771 }
772
773 id = classes.replace( 'wp-image-', '' );
774 }
775 let api = wpa.restUrl + '/' + id;
776 fetch( api )
777 .then( response => response.json())
778 .then(data => {
779 let rawdesc = data.description.rendered;
780 rawdesc = rawdesc.replace(/(<([^>]+)>)/gi, '').trim();
781 if ( '' !== rawdesc ) {
782 let url = new URL( wpa.ldHome );
783 url.searchParams.set( 'longdesc', id );
784 url.searchParams.set( 'p', id );
785 url.toString();
786 wpa_draw_longdesc( img, id, url, rawdesc );
787 }
788 })
789 .catch( error => {
790 console.log( error );
791 });
792 }
793
794 function wpa_draw_longdesc( img, image_id, longdesc, rawdesc ) {
795 let wrapper = document.createElement( 'div' );
796 wrapper.classList.add( 'wpa-ld' );
797 let alt = img.getAttribute('alt');
798 if ( '' === alt ) {
799 alt = '<code>' + img.getAttribute('src').replace( wpa.ldHome, '' ) + '</code>';
800 }
801 let post_classes = document.body.className.split(/\s+/);
802 let post_id = '';
803
804 post_classes.forEach( (value) => {
805 if ( value.match( /postid-/gi ) ) {
806 post_id = value.replace( 'postid-', '', value );
807 }
808 if ( value.match( /page-id-/gi ) ) {
809 post_id = value.replace( 'page-id-', '', value );
810 }
811 });
812 let url = new URL(longdesc);
813 url.searchParams.set( 'referrer', post_id );
814 url.toString();
815 let classes = [...img.classList];
816 classes.forEach( (c) => {
817 wrapper.classList.add(c);
818 });
819 wrap( img, wrapper );
820
821 img.setAttribute('alt', '');
822 img.setAttribute('class', '');
823 let newLink = document.createElement( 'a' );
824 newLink.setAttribute( 'href', url );
825 newLink.classList.add( 'longdesc-link' );
826 let span = document.createElement( 'span' );
827 span.classList.add( 'screen-reader-text' );
828 span.textContent = ' of ' + alt;
829 newLink.textContent = 'Description';
830 newLink.appendChild( span );
831 if ( 'link' === wpa.ldType ) {
832 wrapper.insertAdjacentElement( 'beforeend', newLink );
833 } else {
834 wrapper.insertAdjacentHTML( 'beforeend', '<button aria-expanded="false" class="wpa-toggle">' + wpa.ldText + '</button>');
835 wrapper.insertAdjacentHTML( 'beforeend', '<div class="longdesc"></div>');
836 let container = wrapper.querySelector('.longdesc');
837 container.style.display = 'none';
838
839 fetch( longdesc )
840 .then( response => {
841 if ( response.ok ) {
842 return response.text();
843 } else {
844 container.insertAdjacentHTML( 'beforeend', rawdesc );
845 }
846 }).then (html => {
847 const parser = new DOMParser();
848 const doc = parser.parseFromString( html, "text/html" );
849 container.insertAdjacentElement( 'beforeend', doc.querySelector( '#desc_' + image_id ) );
850 });
851
852 wrapper.querySelector('button').addEventListener( 'click', function(e) {
853 let visible = container.checkVisibility();
854 if ( visible ) {
855 this.setAttribute( 'aria-expanded', 'false' );
856 container.style.display = 'none';
857 } else {
858 this.setAttribute( 'aria-expanded', 'true' );
859 container.style.display = 'block';
860 }
861 });
862 }
863 }
864 }
865 })();
866
867 /**
868 * Check whether an element contains text, including inspecting contained content for image alt attributes or aria-label attributes.
869 *
870 * @arg el DOM element to check.
871 *
872 * Based on work by Roger Johansson https://www.456bereastreet.com/archive/201105/get_element_text_including_alt_text_for_images_with_javascript/
873 */
874 function wpaElementText(el) {
875 let text = '';
876 // Text node (3) or CDATA node (4) - return its text
877 if ( (el.nodeType === 3) || (el.nodeType === 4) ) {
878 text = el.nodeValue;
879 // If node is an element (1) and an img, input[type=image], or area element, return its alt text
880 } else if ( (el.nodeType === 1) && (
881 (el.tagName.toLowerCase() == 'img') ||
882 (el.tagName.toLowerCase() == 'area') ||
883 ((el.tagName.toLowerCase() == 'input') && el.getAttribute('type') && (el.getAttribute('type').toLowerCase() == 'image'))
884 ) ) {
885 text = el.getAttribute('alt') || '';
886 // Traverse children unless this is a script or style element
887 } else if ( (el.nodeType === 1) && !el.tagName.match(/^(script|style)$/i) ) {
888 let children = el.childNodes;
889 for (let i = 0, l = children.length; i < l; i++) {
890 // If an element has an aria-label, that will override any other contained text.
891 let ariaLabel = el.getAttribute( 'aria-label' );
892 text += ( ariaLabel ) ? ariaLabel : wpaElementText( children[i] ) + ' ';
893 }
894 }
895
896 return text;
897 };
898
899 /**
900 * Wait to see whether an element becomes available.
901 *
902 * @param {string} selector
903 * @returns
904 */
905 function wpaWaitForElement(selector) {
906 return new Promise(resolve => {
907 if (document.querySelector(selector)) {
908 return resolve(document.querySelector(selector));
909 }
910
911 const observer = new MutationObserver(mutations => {
912 if (document.querySelector(selector)) {
913 observer.disconnect();
914 resolve(document.querySelector(selector));
915 }
916 });
917
918 observer.observe(document.body, {
919 childList: true,
920 subtree: true
921 });
922 });
923 }