1/** 2 * accessibility-fix.js 3 * WCAG 2.1 AA / GIGW / STQC Accessibility Helper 4 */ 5 6(function ($, Drupal, once) { 7 8 Drupal.behaviors.accessibilityFix = { 9 attach: function (context) { 10 11 /* ===================================== 12 * 1. FIX LANDMARKS 13 * ===================================== */ 14 15 if (!$('header', context).length) { 16 $('.site-header, .navbar, #header', context).first().wrap('<header role="banner"></header>'); 17 } 18 19 if (!$('main', context).length) { 20 $('#main, .main-content, #content', context).first().wrap('<main id="main-content" role="main"></main>'); 21 } 22 23 if (!$('footer', context).length) { 24 $('.site-footer, #footer', context).first().wrap('<footer role="contentinfo"></footer>'); 25 } 26 27 /* ===================================== 28 * 2. HEADING STRUCTURE 29 * ===================================== */ 30 31 if (!$('body').hasClass('user-logged-in')) { 32 33 const $headings = $('h1,h2,h3,h4,h5,h6', context); 34 35 const $h1s = $('h1', context); 36 37 if ($h1s.length > 1) { 38 $h1s.slice(1).each(function () { 39 $(this).replaceWith('<h2>' + $(this).html() + '</h2>'); 40 }); 41 } 42 43 if (!$('h1', context).length && $('h2', context).length) { 44 $('h2', context).first().replaceWith(function () { 45 return '<h1>' + $(this).html() + '</h1>'; 46 }); 47 } 48 49 let lastLevel = 0; 50 51 $headings.each(function () { 52 53 let level = parseInt(this.tagName.substring(1), 10); 54 55 if (lastLevel && level > lastLevel + 1) { 56 57 let fixed = lastLevel + 1; 58 59 $(this).replaceWith( 60 '<h' + fixed + '>' + $(this).html() + '</h' + fixed + '>' 61 ); 62 63 lastLevel = fixed; 64 65 } 66 else { 67 lastLevel = level; 68 } 69 70 }); 71 72 } 73 74 /* ===================================== 75 * 3. REMOVE FAKE HEADINGS 76 * ===================================== */ 77 78 $('.heading,.title,.section-title', context).each(function () { 79 80 if (!$(this).is('h1,h2,h3,h4,h5,h6')) { 81 $(this).replaceWith('<p>' + $(this).html() + '</p>'); 82 } 83 84 }); 85 86 /* ===================================== 87 * 4. FORM LABEL FIX 88 * ===================================== */ 89 90 $('input,textarea,select', context).each(function () { 91 92 const $field = $(this); 93 94 if ( 95 !$field.attr('aria-label') && 96 !$field.attr('aria-labelledby') 97 ) { 98 99 let label = ''; 100 101 const id = $field.attr('id'); 102 103 if (id && $('label[for="' + id + '"]').length) { 104 label = $('label[for="' + id + '"]').text(); 105 } 106 107 if (!label && $field.attr('placeholder')) { 108 label = $field.attr('placeholder'); 109 } 110 111 if (!label && $field.attr('title')) { 112 label = $field.attr('title'); 113 } 114 115 if (!label) { 116 label = $field.closest('.form-item').find('label').text(); 117 } 118 119 if (label) { 120 $field.attr('aria-label', $.trim(label)); 121 } 122 123 } 124 125 }); 126 127$('.menuparent > span[aria-expanded="false"]').removeAttr('aria-expanded'); 128 129 /* ===================================== 130 * 5. EMPTY LINK FIX 131 * ===================================== */ 132 133 $('a,button', context).each(function () { 134 135 const $el = $(this); 136 137 if ( 138 !$el.text().trim() && 139 !$el.find('img').length && 140 !$el.attr('aria-label') 141 ) { 142 143 if ($el.attr('title')) { 144 $el.attr('aria-label', $.trim($el.attr('title'))); 145 } 146 147 } 148 149 }); 150 151 /* ===================================== 152 * 6. IMAGE ACCESSIBILITY 153 * STQC / GIGW / WCAG 154 * ===================================== */ 155 156$('a:has(img), img', context).each(function () { 157 158 var $img, $link; 159 160 if ($(this).is('img')) { 161 $img = $(this); 162 $link = $img.closest('a'); 163 } 164 else { 165 $link = $(this); 166 $img = $link.find('img').first(); 167 } 168 169 if (!$img.length) { 170 return; 171 } 172 173 // Remove invalid role 174 if ($link.length && $link.attr('role') === 'button') { 175 $link.removeAttr('role'); 176 } 177 178 // Lazy loading 179 if (!$img.attr('loading')) { 180 $img.attr('loading', 'lazy'); 181 } 182 183 // Async decoding 184 if (!$img.attr('decoding')) { 185 $img.attr('decoding', 'async'); 186 } 187 188 var alt = $.trim($img.attr('alt') || ''); 189 var title = $.trim($img.attr('title') || ''); 190 var aria = $.trim($link.attr('aria-label') || ''); 191 192 /* --------------------------------- 193 * Case 1 : Alt already exists 194 * --------------------------------- */ 195 196 if (alt !== '') { 197 198 if ($link.length) { 199 $link.removeAttr('aria-label'); 200 $link.removeAttr('title'); 201 } 202 203 $img.removeAttr('title'); 204 return; 205 } 206 207 /* --------------------------------- 208 * Case 2 : Title exists but Alt missing 209 * --------------------------------- */ 210 211 if (title !== '') { 212 213 $img.attr('alt', title); 214 215 if ($link.length) { 216 $link.removeAttr('aria-label'); 217 $link.removeAttr('title'); 218 } 219 220 $img.removeAttr('title'); 221 return; 222 } 223 224 /* --------------------------------- 225 * Case 3 : Alt & Title both missing 226 * --------------------------------- */ 227 228 if (alt === '' && title === '') { 229 230 var label = ''; 231 232 if ($link.length && $.trim($link.text()) !== '') { 233 label = $.trim($link.text()); 234 } 235 236 if (!label) { 237 238 var cardTitle = $img.closest('.card').find('.card-title').first().text(); 239 240 if (cardTitle) { 241 label = $.trim(cardTitle); 242 } 243 244 } 245 246 if (!label) { 247 label = 'Image'; 248 } 249 250 if ($link.length && aria === '') { 251 $link.attr('aria-label', label); 252 } 253 254 } 255 256 // Width & Height
257 if (!$img.attr('width') && this.naturalWidth) { 258 $img.attr('width', this.naturalWidth); 259 } 260 261 if (!$img.attr('height') && this.naturalHeight) { 262 $img.attr('height', this.naturalHeight); 263 } 264 265}); 266 267 /* ===================================== 268 * 7. IMAGE LINK FIX 269 * ===================================== */ 270 271 $('a:has(img)', context).each(function () { 272 273 const $link = $(this); 274 const $img = $link.find('img').first(); 275 276 // Remove invalid role 277 if ($link.attr('role') === 'button') { 278 $link.removeAttr('role'); 279 } 280 281 // If image already has alt 282 // don't duplicate aria-label 283 if ($img.attr('alt') && $.trim($img.attr('alt')) !== '') { 284 $link.removeAttr('aria-label'); 285 } 286 287 }); 288 289 /* ===================================== 290 * 8. DECORATIVE IMAGES 291 * ===================================== */ 292 293 $('img.decorative', context).each(function () { 294 295 $(this) 296 .attr('alt', '') 297 .attr('role', 'presentation'); 298 299 }); 300 301 /* ===================================== 302 * 9. REMOVE EMPTY TITLE 303 * ===================================== */ 304 305 $('[title]', context).each(function () { 306 307 if (!$.trim($(this).attr('title'))) { 308 $(this).removeAttr('title'); 309 } 310 311 }); 312 313 } 314 315 }; 316 317})(jQuery, Drupal, once); 318 319 320 321(function ($, Drupal) { 322 323 Drupal.behaviors.fixDuplicateIds = { 324 attach: function (context) { 325 326 var ids = {}; 327 328 $('[id]', context).each(function () { 329 330 var $el = $(this); 331 var id = $el.attr('id'); 332 333 if (!id) { 334 return; 335 } 336 337 if (ids[id]) { 338 339 ids[id]++; 340 341 var newId = id + '-' + ids[id]; 342 343 $el.attr('id', newId); 344 345 // Update labels 346 $('label[for="' + id + '"]', context).attr('for', newId); 347 348 // Update anchor links 349 $('a[href="#' + id + '"]', context).attr('href', '#' + newId); 350 351 // Update ARIA references 352 $('[aria-labelledby="' + id + '"]', context).attr('aria-labelledby', newId); 353 $('[aria-describedby="' + id + '"]', context).attr('aria-describedby', newId); 354 $('[aria-controls="' + id + '"]', context).attr('aria-controls', newId); 355 356 } else { 357 358 ids[id] = 1; 359 360 } 361 362 }); 363 364 } 365 }; 366 367})(jQuery, Drupal); 368 369 370/** 371 * WCAG 2.1 - Identify Input Purpose 372 * Add autocomplete attributes automatically 373 */ 374 375(function ($, Drupal) { 376 377 Drupal.behaviors.autoCompleteFix = { 378 attach: function (context) { 379 380 $('input:not([autocomplete])', context).each(function () { 381 382 var $field = $(this); 383 384 var text = ( 385 ($field.attr('name') || '') + ' ' + 386 ($field.attr('id') || '') + ' ' + 387 ($field.attr('placeholder') || '') + ' ' + 388 ($field.closest('.form-item').find('label').text() || '') 389 ).toLowerCase(); 390 391 var value = ''; 392 393 if (text.match(/first.?name|fname|given.?name/)) { 394 value = 'given-name'; 395 } 396 else if (text.match(/last.?name|surname|lname|family.?name/)) { 397 value = 'family-name'; 398 } 399 else if (text.match(/full.?name|your.?name|name/)) { 400 value = 'name'; 401 } 402 else if (text.match(/email|e-mail/)) { 403 value = 'email'; 404 } 405 else if (text.match(/phone|mobile|telephone|contact/)) { 406 value = 'tel'; 407 } 408 else if (text.match(/organisation|organization|company/)) { 409 value = 'organization'; 410 } 411 else if (text.match(/address.?line.?1|address1/)) { 412 value = 'address-line1'; 413 } 414 else if (text.match(/address.?line.?2|address2/)) { 415 value = 'address-line2'; 416 } 417 else if (text.match(/city|town/)) { 418 value = 'address-level2'; 419 } 420 else if (text.match(/district/)) { 421 value = 'address-level2'; 422 } 423 else if (text.match(/state/)) { 424 value = 'address-level1'; 425 } 426 else if (text.match(/postal|zip|pincode|pin.?code/)) { 427 value = 'postal-code'; 428 } 429 else if (text.match(/country/)) { 430 value = 'country'; 431 } 432 else if (text.match(/username|user.?name|login/)) { 433 value = 'username'; 434 } 435 else if (text.match(/password/) && !$field.attr('autocomplete')) { 436 value = 'current-password'; 437 } 438 else if (text.match(/search|keyword|title|owner|publisher|language|registration|publication/)) { 439 // Search/filter fields 440 value = 'off'; 441 } 442 443 if (value !== '') { 444 $field.attr('autocomplete', value); 445 } 446 447 }); 448 449 } 450 }; 451 452})(jQuery, Drupal); 453 454 455
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.