1/** 2 * @author noahm 3 */ 4var accessibility = angular.module('wpd-accessibility', []); 5 6//class SelfResetRegex { 7// constructor(reg) { 8// this.reg = reg; 9// } 10// exec(str) { 11// var result = this.reg.exec(str); 12// this.reg.lastIndex = 0; 13// return result; 14// } 15//} 16// 17//class WPDColorObject { 18// constructor(R, G, B, A) { 19// this.R = R; 20// this.G = G; 21// this.B = B; 22// this.A = A; 23// } 24//} 25 26/** 27 * @constructor 28 * @param {RegExp} reg 29 * @returns {SelfResetRegex} 30 */ 31function SelfResetRegex(reg) { 32 this.reg = reg; 33}; 34SelfResetRegex.prototype.exec = function(str) { 35 var result = this.reg.exec(str); 36 this.reg.lastIndex = 0; 37 return result; 38}; 39 40/** 41 * @constructor 42 * @param {Number} R (0-255) 43 * @param {Number} G 44 * @param {Number} B 45 * @param {Number} A (0.0-1.0) 46 * @returns {WPDColorObject} 47 */ 48function WPDColorObject(R, G, B, A) { 49 this.R = R; 50 this.G = G; 51 this.B = B; 52 this.A = A; 53}; 54 55accessibility.constant('accessibilityKeyCodes', { 56 'ENTER' : 13, 57 'SPACE' : 32, 58 'LEFT_ARROW' : 37, 59 'UP_ARROW' : 38, 60 'RIGHT_ARROW' : 39, 61 'DOWN_ARROW' : 40 62}); 63accessibility.constant('baseFocusableElements', [ 64 'button', 65 'a', 66 '.focusable-element', 67 'select', 68 'input', 69 'textarea' 70]); 71accessibility.constant('baseFocusableExceptions', [ 72 'a.skiptocontent' 73]); 74accessibility.constant('focusVariables', { 75 transparencyThreshold: 0.1, //anything <= this consider to be transparent 76 opacityThreshold: 0.9, //anything >= this consider to be opaque 77 blendLevels: 2, //how deep should recursion go to blend with parent(s) 78 defaultDarkFocus: (new WPDColorObject(48, 124, 127, 1)), // #307c7f green 79 defaultLightFocus: (new WPDColorObject(168, 229, 240, 1)), // #a8e5f0 cold-blue 80 transparentObj: {effectiveColor: (new WPDColorObject(255, 255, 255, 0))}, 81 whiteObj: {effectiveColor: (new WPDColorObject(255, 255, 255, 1))}, 82 minimumContrastReqLarge: 3, // https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html 83 minimumContrastReqSmall: 4.5, 84 textBoldSize: 18.5, 85 textLargeSize: 24, 86 rgbaRegex: (new SelfResetRegex(/^rgba?\(([0-9]{1,3})\s?,\s?([0-9]{1,3})\s?,\s?([0-9]{1,3})\s?(,\s?([0-9]{1,3}\.?[0-9]*))?\)$/gi)), 87 hexaRegex: (new SelfResetRegex(/^#?([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})([0-9A-Fa-f]{2})?$/gi)) // this one assumes that it won't see an invalid hex value with the format RGBaa (using hexa means you need RRGGBBaa) 88 89}); 90 91/** 92 * Inject accessibilitySettingsProvider into your config() function to make setting changes. 93 * 94 * NOTE: If your focusColors in your CSS differ from the default ones above, you need to set them to match your CSS values, 95 * otherwise this script will be comparing luminance for the wrong colors! 96 */ 97accessibility.provider('accessibilitySettings', ['focusVariables', function(focusVariables) { 98 var self = this; 99 100 self.focusableElements = []; 101 self.focusableExceptions = []; 102 self.darkFocus = focusVariables.defaultDarkFocus; 103 self.lightFocus = focusVariables.defaultLightFocus; 104 105 /** 106 * 107 * @param {String[] | String} what 108 * @returns {undefined} 109 */ 110 self.addFocusableElement = function(what) { 111 if (typeof what === 'string') { 112 this.focusableElements.push(what); 113 } 114 else if (Array.isArray(what)) { 115 for(var i = 0; i < what.length; i++) { 116 if (typeof what[i] === 'string') { 117 this.focusableElements.push(what[i]); 118 } 119 } 120 } 121 }; 122 123 /** 124 * 125 * @param {String[] | String} what 126 * @returns {undefined} 127 */ 128 self.addFocusableException = function(what) { 129 if (typeof what === 'string') { 130 this.focusableExceptions.push(what); 131 } 132 else if (Array.isArray(what)) { 133 for(var i = 0; i < what.length; i++) { 134 if (typeof what[i] === 'string') { 135 this.focusableExceptions.push(what[i]); 136 } 137 } 138 } 139 }; 140 141 /** 142 * 143 * @param {Number} r 144 * @param {Number} g 145 * @param {Number} b 146 * @param {Number} a 147 * @returns {undefined} 148 */ 149 self.setLightFocus = function(r,g,b,a) { 150 this.lightFocus = new WPDColorObject(r,g,b,a); 151 }; 152 153 /** 154 * 155 * @param {Number} r 156 * @param {Number} g 157 * @param {Number} b 158 * @param {Number} a 159 * @returns {undefined} 160 */ 161 self.setDarkFocus = function(r,g,b,a) { 162 this.darkFocus = new WPDColorObject(r,g,b,a); 163 }; 164 165 // Service object (not available for use in config() functions) 166 this.$get = [ 167 'baseFocusableElements', 168 'baseFocusableExceptions', 169 function(baseFocusableElements, baseFocusableExceptions) { 170 return { 171 getFocusableElements: function() { 172 return baseFocusableElements.concat(self.focusableElements); 173 }, 174 getFocusableExceptions: function() { 175 return baseFocusableExceptions.concat(self.focusableExceptions); 176 }, 177 getLightFocusColor: function() { 178 return self.lightFocus; 179 }, 180 getDarkFocusColor: function() { 181 return self.darkFocus; 182 } 183 }; 184 } 185 ]; 186 }] 187); 188 189// [WSME-590] 190/** 191 * @author noahm 192 */ 193accessibility.service('colorService', ['focusVariables', function(focusVariables){ 194 var service = {}; 195 196 /** 197 * Take a hex or 0-255 and determine its worth in a 0-1 range 198 * @param {String | Number} value 199 * @returns {null | Number} 200 */
201 service.normaliseValue = function(value) { 202 if (value !== null && value !== undefined) { 203 var isHex = /^[0-9a-fA-f]+$/gi; 204 if (isHex.test(value)) { 205 value = parseInt('0x'+value); 206 } 207 if (!isNaN(parseFloat(value))) { 208 var max = 255; 209 var min = 0; 210 var value = parseFloat(value); 211 212 return (value - min) / (max - min); 213 } 214 } 215 return null; 216 }; 217 218 /** 219 * 220 * @param {Number} alpha 221 * @param {Number} col1 222 * @param {Number} col2 223 * @returns {Number} 224 */ 225 service.blendColorValue = function(alpha, col1, col2) { 226 alpha = parseFloat(alpha); 227 col1 = parseFloat(col1); 228 col2 = parseFloat(col2); 229 return (alpha * col1) + ((1-alpha) * col2); 230 }; 231 232 /** 233 * 234 * @param {WPDColorObject} back 235 * @param {WPDColorObject} fore 236 * @returns {WPDColorObject} 237 */ 238 service.blendColorObj = function(back, fore) { 239 return new WPDColorObject( 240 service.blendColorValue(fore.A, fore.R, back.R), 241 service.blendColorValue(fore.A, fore.G, back.G), 242 service.blendColorValue(fore.A, fore.B, back.B), 243 (parseFloat(fore.A) + (parseFloat(back.A) * (1-parseFloat(fore.A)))) 244 ); 245 }; 246 247 248 /** 249 * This is not foolproof! It can't take into consideration background images, or the visual contents of img elements. 250 * 251 * Returns null if the element is deemed to be invisible/transparent according to display, visibility, opacity and bgColor alpha value (if below threshold) 252 * Colours above opacityThreahold are returned as-is. 253 * Colours deemed semi-transparent will attempt to blend with their parent's colours (up to blendLevel times) to get an approximate colour of what the user is actually seeing on screen. 254 * This action is recursive, and will terminate when the blendDepth is reached, or the document object has been reached. 255 * 256 * The returned object's effectiveColour.A value is for the purpose of blending within this function. 257 * For all intents and purposes the alpha of the ultimately returned effective colour should always be considered 1. 258 * 259 * @param {jQuery} $elem 260 * @param {Number} blendDepth 261 * @returns {null | Object} 262 */ 263 service.getEffectiveColor = function($elem, blendDepth) { 264 //console.log($elem); 265 266 if ($elem[0] === document) return null; 267 268 if (blendDepth === undefined || blendDepth === null || blendDepth > focusVariables.blendLevels) { 269 blendDepth = focusVariables.blendLevels; 270 } 271 //console.log('blendDepth: '+blendDepth); 272 273 var display = $elem.css('display'); 274 if (display === 'none') { 275 return null; 276 } 277 278 var visibility = $elem.css('visibility'); 279 if (visibility === 'hidden') { 280 return null; 281 } 282 283 // Technically I should be doing the blending thing with this value too, but considering this opacity change will propagate to any text inside the element, 284 // I'm making the assumption that it isn't supposed to be focusable anyway. 285 var opacity = $elem.css('opacity'); 286 if (opacity !== '' && opacity <= focusVariables.transparencyThreshold) { 287 return null; 288 } 289 290 var bgColor = $elem.css('background-color'); 291 //console.log(bgColor); 292 if (bgColor === null || bgColor === undefined || bgColor === '' || bgColor === 'transparent') { 293 return null; 294 } 295 296 var rgbaMatches = focusVariables.rgbaRegex.exec(bgColor); 297 var hexaMatches = focusVariables.hexaRegex.exec(bgColor); 298 299 var bgColorAlpha = 1; 300 if (rgbaMatches && rgbaMatches.length > 1) { 301 if (rgbaMatches[5] !== null && rgbaMatches[5] !== undefined) bgColorAlpha = rgbaMatches[5]; //apparently the rgba value .css() returns normalises the alpha value by itself. 302 } 303 else if (hexaMatches && hexaMatches.length > 1) {
304 var x = service.normaliseValue(hexaMatches[2]); 305 if (x !== null) bgColorAlpha = x; 306 } 307 308 if (bgColorAlpha !== null && bgColorAlpha !== undefined && bgColorAlpha !== '') { 309 if (bgColorAlpha <= focusVariables.transparencyThreshold) { 310 return null; 311 } 312 } 313 314 var trueColorObj = null; 315 if (rgbaMatches && rgbaMatches.length > 1) { 316 trueColorObj = new WPDColorObject( 317 (rgbaMatches[1] || 0), 318 (rgbaMatches[2] || 0), 319 (rgbaMatches[3] || 0), 320 bgColorAlpha 321 ); 322 } 323 else if (hexaMatches && hexaMatches.length > 1) { 324 if (hexaMatches[1] !== null && hexaMatches[1] !== undefined) { 325 if (hexaMatches[1].length === 3) { 326 trueColorObj = new WPDColorObject( 327 (parseInt('0x' + hexaMatches[1][0] + hexaMatches[1][0]) || 0), 328 (parseInt('0x' + hexaMatches[1][1] + hexaMatches[1][1]) || 0), 329 (parseInt('0x' + hexaMatches[1][2] + hexaMatches[1][2]) || 0), 330 bgColorAlpha 331 ); 332 } 333 else if (hexaMatches[1].length === 6) { 334 trueColorObj = new WPDColorObject( 335 (parseInt('0x' + hexaMatches[1][0] + hexaMatches[1][1]) || 0), 336 (parseInt('0x' + hexaMatches[1][2] + hexaMatches[1][3]) || 0), 337 (parseInt('0x' + hexaMatches[1][4] + hexaMatches[1][5]) || 0), 338 bgColorAlpha 339 ); 340 } 341 } 342 } 343 344 if (trueColorObj === null) return null; 345 346 var effectiveColor = trueColorObj; 347 348 //start blending with parent bg Colors if this isn't deemed to be opaque enough. 349 if (bgColorAlpha < focusVariables.opacityThreshold && $elem.parent()[0] !== document && blendDepth > 0) { 350 //console.log('blend with parent'); 351 var $parent = $elem.parent(); 352 //console.log($parent); 353 var parentCol; 354 //attempt to skip over transparent parents. 355 while (true) { 356 if ($parent[0] === document) { 357 parentCol = focusVariables.whiteObj; 358 break; 359 } 360 parentCol = service.getEffectiveColor($parent, blendDepth - 1); //recurse 361 if (parentCol) break; 362 //console.log('parent was transparent, try again for depth ' + blendDepth ); 363 $parent = $parent.parent(); 364 //console.log($parent); 365 } 366 367 parentCol = parentCol || focusVariables.transparentObj; 368 if (parentCol) { 369 effectiveColor = service.blendColorObj(parentCol.effectiveColor, trueColorObj); 370 } 371 } 372 373 // console.log('returned effectiveColor for depth '+blendDepth); 374 //console.log(effectiveColor); 375 376 return { 377 trueColor: bgColor, 378 trueColorObj: trueColorObj, 379 effectiveColor: effectiveColor 380 }; 381 }; 382 383 /** 384 * Traverse up parents and return the first *effectively determined* background-colour that isn't transparent. 385 * If all parents are transparent, you'll eventually hit the document object, which will return white as its bgColor. 386 * 387 * Again, not foolproof! This assumes that the elements have static positioning and are visually nested in their containers. 388 * 389 * @param {jQuery} $elem 390 * @param {boolean} includingElem 391 * @returns {WPDColorObj} 392 */ 393 service.determineBgColor = function($elem, includingElem) { 394 if (includingElem) { 395 var elemCol = service.getEffectiveColor($elem); 396 if (elemCol) { 397 return elemCol; 398 } 399 } 400 var $parent = $elem.parent(); 401 while (true) { 402 if ($parent[0] === document) return focusVariables.whiteObj; 403 var parentCol = service.getEffectiveColor($parent); 404 if (parentCol) { 405 return parentCol; 406 } 407 $parent = $parent.parent(); 408 } 409 }; 410 411 412 /** 413 * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef 414 * 415 * Calculate the luminance value of an rgb colour, according to the definition in the link above. 416 * 417 * @param {WPDColorObj} colorObj 418 * @returns {Number} 419 */ 420 service.determineLuminance = function(colorObj) { 421 var r, g, b; 422 var srgbColObj = { 423 R: colorObj.R / 255, 424 G: colorObj.G / 255, 425 B: colorObj.B / 255, 426 calculate: function(what) { 427 if (what <= 0.03928) { 428 return what / 12.92; 429 } 430 else { 431 return Math.pow(((what + 0.055) / 1.055), 2.4); 432 } 433 } 434 }; 435 436 r = srgbColObj.calculate(srgbColObj.R); 437 g = srgbColObj.calculate(srgbColObj.G); 438 b = srgbColObj.calculate(srgbColObj.B); 439 440 return (0.2126 * r) + (0.7152 * g) + (0.0722 * b); 441 }; 442 443 /** 444 * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef 445 * 446 * Calculate the contrast ratio between two luminance values, accordiong to the definition in the link above. 447 * 448 * @param {Number} luminance1 449 * @param {Number} luminance2 450 * @returns {Number} 451 */ 452 service.contrastRatio = function(luminance1, luminance2) { 453 var lighter = Math.max(luminance1, luminance2); 454 var darker = Math.min(luminance1, luminance2); 455 return (lighter + 0.05) / (darker + 0.05); 456 }; 457 458 return service; 459}]); 460 461accessibility.run([ 462 'colorService', 463 'accessibilitySettings',
464 'focusVariables', 465 'DOMObserver', 466 '$timeout', 467 function(colorService, accessibilitySettings, focusVariables, DOMObserver, $timeout) { 468 var lightFocusLum = colorService.determineLuminance(accessibilitySettings.getLightFocusColor()); 469 var darkFocusLum = colorService.determineLuminance(accessibilitySettings.getDarkFocusColor()); 470 var whiteLum = 1; 471 var blackLum = 0; 472 473 /** 474 * 475 * @param {jQuery} $elements 476 * @returns {undefined} 477 */ 478 var setupEventListeners = function($elements) { 479 $elements.each(function(idx, $elem) { 480 $elem = angular.element($elem); 481 $elem.on('focus', function(){ 482 var backgroundColor = colorService.determineBgColor($elem); 483 //console.log(backgroundColor); 484 var luminance = colorService.determineLuminance(backgroundColor.effectiveColor); 485 486 var contrastWithLight = colorService.contrastRatio(luminance, lightFocusLum); 487 var contrastWithDark = colorService.contrastRatio(luminance, darkFocusLum); 488 489 //console.log(contrastWithLight); 490 //console.log(contrastWithDark); 491 492 // https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html 493 var finalContrastReq = (parseFloat($elem.css('font-size')) >= focusVariables.textLargeSize || 494 ((parseFloat($elem.css('font-size')) >= focusVariables.textBoldSize) && 495 ($elem.css('font-weight') === 'bold' || 496 $elem.css('font-weight') === 'bolder' || 497 $elem.css('font-weight') === '700' || 498 $elem.css('font-weight') === '800' || 499 $elem.css('font-weight') === '900') 500 ) 501 ) ? focusVariables.minimumContrastReqLarge : focusVariables.minimumContrastReqSmall; 502 var finalClass = 'focusable-element-'; 503 504 // console.log(finalContrastReq); 505 506 //light is passes, dark doesn't 507 if (contrastWithLight >= finalContrastReq && contrastWithDark < finalContrastReq) finalClass += 'light'; 508 //dark passes, light doesn't 509 else if (contrastWithDark >= finalContrastReq && contrastWithLight < finalContrastReq) finalClass += 'dark'; 510 //both pass, so pick the better of the two 511 else if (contrastWithLight >= finalContrastReq && contrastWithDark >= finalContrastReq) { 512 finalClass += (contrastWithLight >= contrastWithDark ? 'light' : 'dark'); 513 } 514 //neither custom colours cut it so go straight to either black or white outline. 515 else { 516 contrastWithLight = colorService.contrastRatio(luminance, whiteLum); 517 contrastWithDark = colorService.contrastRatio(luminance, blackLum); 518 519 finalClass += (contrastWithLight >= contrastWithDark ? 'white' : 'black'); 520 } 521 $elem.removeClass('focusable-element-light', 'focusable-element-dark', 'focusable-element-white', 'focusable-element-black'); 522 $elem.addClass(finalClass); 523 }); 524 $elem.on('blur', function() { 525 $elem.removeClass('focusable-element-light', 'focusable-element-dark', 'focusable-element-white', 'focusable-element-black'); 526 }); 527 }); 528 }; 529 530 $timeout(function() { 531 //console.log('accessibility run!'); 532 // it might have been nicer to watch document.activeElement, but apparently this doesn't get assigned for anything other than text type inputs for iOS. 533 var $allFocusableElements = angular.element(document).find(accessibilitySettings.getFocusableElements().join(', ')).not(accessibilitySettings.getFocusableExceptions().join(', ')); 534 //console.log($allFocusableElements); 535 setupEventListeners($allFocusableElements); 536 537 //setup something to add more listeners for focusable elements added to the DOM later on. 538 DOMObserver.addObserver(document, function(mutations) { 539 for(var i = 0; i < mutations.length; i ++) { 540 if (mutations[i].addedNodes.length) { 541 var $newFocusables = angular.element(Array.prototype.slice.call(mutations[i].addedNodes).filter(function(element) { 542 return element.nodeType === 1; 543 })).find(accessibilitySettings.getFocusableElements().join(', ')).not(accessibilitySettings.getFocusableExceptions().join(', ')); 544 //console.log($newFocusables); 545 setupEventListeners($newFocusables); 546 } 547 } 548 }); 549 }); 550 } 551]); 552 553 554//https://stackoverflow.com/questions/3219758/detect-changes-in-the-dom 555accessibility.service('DOMObserver', ['$timeout', function($timeout) { 556 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver; 557 558 this.addObserver = function(obj, callback) { 559 if( !obj || (obj.nodeType !== 1 && obj.nodeType !== 9)) return null; //allow Elements and Document 560 561 if (MutationObserver) { 562 var observer = new MutationObserver(function(mutations, observer){ 563 $timeout(function() { 564 callback(mutations); 565 }); 566 }); 567 // have the observer observe obj for changes in children 568 observer.observe( obj, { childList:true, subtree:true }); 569 } 570 else if(window.addEventListener){ 571 //console.log('window.addeventListener'); 572 var f = function(mutations){ 573 $timeout(function() { 574 callback(mutations); 575 }); 576 }; 577 obj.addEventListener('DOMNodeInserted', f, false); 578 obj.addEventListener('DOMNodeRemoved', f, false); 579 } 580 }; 581}]); 582 583 584accessibility.controller('MaxLengthNotice', [ 585 '$scope', 586 '$element', 587 '$timeout', 588 function($scope, $element, $timeout) { 589 var mln = this; 590 var timeoutQueued = false;
591 var timeoutValue = 1000; 592 593 mln.setAriaLive = function(remainingCount, $message) { 594 if (remainingCount > mln.politeThreshold) { 595 $message.attr('aria-live', 'off'); 596 597 if (timeoutValue !== null && timeoutValue !== undefined) { 598 timeoutValue = 1000; 599 } 600 } 601 else { 602 if (remainingCount > mln.assertiveThreshold) { 603 $message.attr('aria-live', 'polite'); 604 605 if (timeoutValue !== null && timeoutValue !== undefined) { 606 607 if (remainingCount > mln.assertiveThreshold + 50) { 608 timeoutValue = 500; 609 } 610 else { 611 timeoutValue = 200; 612 } 613 } 614 } 615 else { 616 $message.attr('aria-live', 'assertive'); 617 618 if (timeoutValue !== null && timeoutValue !== undefined) { 619 timeoutValue = 0; 620 } 621 } 622 } 623 }; 624 625 mln.init = function() { 626 // we have to wait until our template is rendered (how $timeout works here I have no clue) 627 $timeout(function() { 628 var $message = $element.find('[data-mln-message]'); 629 var $messageTagon = $('<span data-mln-refresh-message></span>'); 630 $message.append($messageTagon); 631 var $textField = $element.find('[data-mln-field]'); 632 var maxCharacters = $textField.attr('maxlength'); 633 var toggleTagon = false; 634 635 if (mln.makeAtomic) { 636 $message.attr('aria-atomic','true'); 637 } 638 639 //initally get textfield char limit and set aria-live 640 mln.setAriaLive(maxCharacters - $textField.val().length, $message); 641 642 var remainingCount = maxCharacters - $textField.val().length; 643 644 $textField.on('keyup', function() { 645 //don't spam evaluating this on every input event, so use timeouts 646 // let us know if we already have one queued so we don't end up stacking them. 647 if (!timeoutQueued) { 648 $timeout(function() { 649 remainingCount = maxCharacters - $textField.val().length; 650 651 mln.setAriaLive(remainingCount, $message); 652 653 // reiterate message if they keep trying to type once limit is reached 654 if (remainingCount <= 0) { 655 toggleTagon = !toggleTagon; 656 $messageTagon.html(toggleTagon ? '': ' '); // This is enough to get the screenreader to repeat the outer message. 657 } // if you're wondering why we can't just tale and repaste the .text() 658 // of the $message, it's because we'd lose the angular expression generating it. 659 660 661 timeoutQueued = false; 662 }, timeoutValue); 663 664 timeoutQueued = true; 665 } 666 }); 667 }); 668 }; 669 } 670]); 671 672accessibility.directive('maxLengthNotice', [ 673 function() { 674 return { 675 scope: { 676 makeAtomic: '<', 677 politeThreshold: '<', 678 assertiveThreshold: '<' 679 }, 680 transclude: true, 681 bindToController: true, 682 controllerAs: 'mln', 683 controller: 'MaxLengthNotice', 684 template: '<div ng-init="mln.init()"><ng-transclude></ng-transclude></div>' 685 }; 686 } 687]);
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.