| OLD | NEW |
| (Empty) |
| 1 // Copyright 2012 Google Inc. | |
| 2 // | |
| 3 // Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 // you may not use this file except in compliance with the License. | |
| 5 // You may obtain a copy of the License at | |
| 6 // | |
| 7 // http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 // | |
| 9 // Unless required by applicable law or agreed to in writing, software | |
| 10 // distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 // See the License for the specific language governing permissions and | |
| 13 // limitations under the License. | |
| 14 | |
| 15 axs = {}; | |
| 16 axs.utils = {}; | |
| 17 | |
| 18 /** | |
| 19 * @constant | |
| 20 * @type {string} | |
| 21 */ | |
| 22 axs.utils.FOCUSABLE_ELEMENTS_SELECTOR = | |
| 23 'input:not([type=hidden]):not([disabled]),' + | |
| 24 'select:not([disabled]),' + | |
| 25 'textarea:not([disabled]),' + | |
| 26 'button:not([disabled]),' + | |
| 27 'a[href],' + | |
| 28 'iframe,' + | |
| 29 '[tabindex]'; | |
| 30 | |
| 31 /** | |
| 32 * @constructor | |
| 33 * @param {number} red | |
| 34 * @param {number} green | |
| 35 * @param {number} blue | |
| 36 * @param {number} alpha | |
| 37 */ | |
| 38 axs.utils.Color = function(red, green, blue, alpha) { | |
| 39 /** @type {number} */ | |
| 40 this.red = red; | |
| 41 | |
| 42 /** @type {number} */ | |
| 43 this.green = green; | |
| 44 | |
| 45 /** @type {number} */ | |
| 46 this.blue = blue; | |
| 47 | |
| 48 /** @type {number} */ | |
| 49 this.alpha = alpha; | |
| 50 }; | |
| 51 | |
| 52 /** | |
| 53 * Calculate the contrast ratio between the two given colors. Returns the ratio | |
| 54 * to 1, for example for two two colors with a contrast ratio of 21:1, this | |
| 55 * function will return 21. | |
| 56 * @param {axs.utils.Color} fgColor | |
| 57 * @param {axs.utils.Color} bgColor | |
| 58 * @return {?number} | |
| 59 */ | |
| 60 axs.utils.calculateContrastRatio = function(fgColor, bgColor) { | |
| 61 if (!fgColor || !bgColor) | |
| 62 return null; | |
| 63 | |
| 64 if (fgColor.alpha < 1) | |
| 65 fgColor = axs.utils.flattenColors(fgColor, bgColor); | |
| 66 | |
| 67 var fgLuminance = axs.utils.calculateLuminance(fgColor); | |
| 68 var bgLuminance = axs.utils.calculateLuminance(bgColor); | |
| 69 var contrastRatio = (Math.max(fgLuminance, bgLuminance) + 0.05) / | |
| 70 (Math.min(fgLuminance, bgLuminance) + 0.05); | |
| 71 return contrastRatio; | |
| 72 }; | |
| 73 | |
| 74 axs.utils.luminanceRatio = function(luminance1, luminance2) { | |
| 75 return (Math.max(luminance1, luminance2) + 0.05) / | |
| 76 (Math.min(luminance1, luminance2) + 0.05); | |
| 77 } | |
| 78 | |
| 79 /** | |
| 80 * Returns the nearest ancestor which is an Element. | |
| 81 * @param {Node} node | |
| 82 * @return {Element} | |
| 83 */ | |
| 84 axs.utils.parentElement = function(node) { | |
| 85 if (!node) | |
| 86 return null; | |
| 87 if (node.nodeType == Node.DOCUMENT_FRAGMENT_NODE) | |
| 88 return node.host; | |
| 89 | |
| 90 var parentElement = node.parentElement; | |
| 91 if (parentElement) | |
| 92 return parentElement; | |
| 93 | |
| 94 var parentNode = node.parentNode; | |
| 95 if (!parentNode) | |
| 96 return null; | |
| 97 | |
| 98 switch (parentNode.nodeType) { | |
| 99 case Node.ELEMENT_NODE: | |
| 100 return /** @type {Element} */ (parentNode); | |
| 101 case Node.DOCUMENT_FRAGMENT_NODE: | |
| 102 return parentNode.host; | |
| 103 default: | |
| 104 return null; | |
| 105 } | |
| 106 }; | |
| 107 | |
| 108 /** | |
| 109 * Return the corresponding element for the given node. | |
| 110 * @param {Node} node | |
| 111 * @return {Element} | |
| 112 * @suppress {checkTypes} | |
| 113 */ | |
| 114 axs.utils.asElement = function(node) { | |
| 115 /** @type {Element} */ var element; | |
| 116 switch (node.nodeType) { | |
| 117 case Node.COMMENT_NODE: | |
| 118 return null; // Skip comments | |
| 119 case Node.ELEMENT_NODE: | |
| 120 element = /** (@type {Element}) */ node; | |
| 121 if (element.tagName.toLowerCase() == 'script') | |
| 122 return null; // Skip script elements | |
| 123 break; | |
| 124 case Node.TEXT_NODE: | |
| 125 element = axs.utils.parentElement(node); | |
| 126 break; | |
| 127 default: | |
| 128 console.warn('Unhandled node type: ', node.nodeType); | |
| 129 return null; | |
| 130 } | |
| 131 return element; | |
| 132 } | |
| 133 | |
| 134 /** | |
| 135 * @param {Element} element | |
| 136 * @return {boolean} | |
| 137 */ | |
| 138 axs.utils.elementIsTransparent = function(element) { | |
| 139 return element.style.opacity == '0'; | |
| 140 }; | |
| 141 | |
| 142 /** | |
| 143 * @param {Element} element | |
| 144 * @return {boolean} | |
| 145 */ | |
| 146 axs.utils.elementHasZeroArea = function(element) { | |
| 147 var rect = element.getBoundingClientRect(); | |
| 148 var width = rect.right - rect.left; | |
| 149 var height = rect.top - rect.bottom; | |
| 150 if (!width || !height) | |
| 151 return true; | |
| 152 return false; | |
| 153 }; | |
| 154 | |
| 155 /** | |
| 156 * @param {Element} element | |
| 157 * @return {boolean} | |
| 158 */ | |
| 159 axs.utils.elementIsOutsideScrollArea = function(element) { | |
| 160 var parent = axs.utils.parentElement(element); | |
| 161 | |
| 162 var defaultView = element.ownerDocument.defaultView; | |
| 163 while (parent != defaultView.document.body) { | |
| 164 if (axs.utils.isClippedBy(element, parent)) | |
| 165 return true; | |
| 166 | |
| 167 if (axs.utils.canScrollTo(element, parent) && !axs.utils.elementIsOutsid
eScrollArea(parent)) | |
| 168 return false; | |
| 169 | |
| 170 parent = axs.utils.parentElement(parent); | |
| 171 } | |
| 172 | |
| 173 return !axs.utils.canScrollTo(element, defaultView.document.body); | |
| 174 }; | |
| 175 | |
| 176 /** | |
| 177 * Checks whether it's possible to scroll to the given element within the given
container. | |
| 178 * Assumes that |container| is an ancestor of |element|. | |
| 179 * If |container| cannot be scrolled, returns True if the element is within its
bounding client | |
| 180 * rect. | |
| 181 * @param {Element} element | |
| 182 * @param {Element} container | |
| 183 * @return {boolean} True iff it's possible to scroll to |element| within |conta
iner|. | |
| 184 */ | |
| 185 axs.utils.canScrollTo = function(element, container) { | |
| 186 var rect = element.getBoundingClientRect(); | |
| 187 var containerRect = container.getBoundingClientRect(); | |
| 188 var containerTop = containerRect.top; | |
| 189 var containerLeft = containerRect.left; | |
| 190 var containerScrollArea = | |
| 191 { top: containerTop - container.scrollTop, | |
| 192 bottom: containerTop - container.scrollTop + container.scrollHeight, | |
| 193 left: containerLeft - container.scrollLeft, | |
| 194 right: containerLeft - container.scrollLeft + container.scrollWidth }; | |
| 195 | |
| 196 if (rect.right < containerScrollArea.left || rect.bottom < containerScrollAr
ea.top || | |
| 197 rect.left > containerScrollArea.right || rect.top > containerScrollA
rea.bottom) { | |
| 198 return false; | |
| 199 } | |
| 200 | |
| 201 var defaultView = element.ownerDocument.defaultView; | |
| 202 var style = defaultView.getComputedStyle(container); | |
| 203 | |
| 204 if (rect.left > containerRect.right || rect.top > containerRect.bottom) { | |
| 205 return (style.overflow == 'scroll' || style.overflow == 'auto' || | |
| 206 container instanceof defaultView.HTMLBodyElement); | |
| 207 } | |
| 208 | |
| 209 return true; | |
| 210 }; | |
| 211 | |
| 212 /** | |
| 213 * Checks whether the given element is clipped by the given container. | |
| 214 * Assumes that |container| is an ancestor of |element|. | |
| 215 * @param {Element} element | |
| 216 * @param {Element} container | |
| 217 * @return {boolean} True iff |element| is clipped by |container|. | |
| 218 */ | |
| 219 axs.utils.isClippedBy = function(element, container) { | |
| 220 var rect = element.getBoundingClientRect(); | |
| 221 var containerRect = container.getBoundingClientRect(); | |
| 222 var containerTop = containerRect.top; | |
| 223 var containerLeft = containerRect.left; | |
| 224 var containerScrollArea = | |
| 225 { top: containerTop - container.scrollTop, | |
| 226 bottom: containerTop - container.scrollTop + container.scrollHeight, | |
| 227 left: containerLeft - container.scrollLeft, | |
| 228 right: containerLeft - container.scrollLeft + container.scrollWidth }; | |
| 229 | |
| 230 var defaultView = element.ownerDocument.defaultView; | |
| 231 var style = defaultView.getComputedStyle(container); | |
| 232 | |
| 233 if ((rect.right < containerRect.left || rect.bottom < containerRect.top || | |
| 234 rect.left > containerRect.right || rect.top > containerRect.bottom)
&& | |
| 235 style.overflow == 'hidden') { | |
| 236 return true; | |
| 237 } | |
| 238 | |
| 239 if (rect.right < containerScrollArea.left || rect.bottom < containerScrollAr
ea.top) | |
| 240 return (style.overflow != 'visible'); | |
| 241 | |
| 242 return false; | |
| 243 }; | |
| 244 | |
| 245 /** | |
| 246 * @param {Node} ancestor A potential ancestor of |node|. | |
| 247 * @param {Node} node | |
| 248 * @return {boolean} true if |ancestor| is an ancestor of |node| (including | |
| 249 * |ancestor| === |node|). | |
| 250 */ | |
| 251 axs.utils.isAncestor = function(ancestor, node) { | |
| 252 if (node == null) | |
| 253 return false; | |
| 254 if (node === ancestor) | |
| 255 return true; | |
| 256 | |
| 257 return axs.utils.isAncestor(ancestor, node.parentNode); | |
| 258 } | |
| 259 | |
| 260 /** | |
| 261 * @param {Element} element | |
| 262 * @return {Array.<Element>} An array of any non-transparent elements which | |
| 263 * overlap the given element. | |
| 264 */ | |
| 265 axs.utils.overlappingElements = function(element) { | |
| 266 if (axs.utils.elementHasZeroArea(element)) | |
| 267 return null; | |
| 268 | |
| 269 var overlappingElements = []; | |
| 270 var clientRects = element.getClientRects(); | |
| 271 for (var i = 0; i < clientRects.length; i++) { | |
| 272 var rect = clientRects[i]; | |
| 273 var center_x = (rect.left + rect.right) / 2; | |
| 274 var center_y = (rect.top + rect.bottom) / 2; | |
| 275 var elementAtPoint = document.elementFromPoint(center_x, center_y); | |
| 276 | |
| 277 if (elementAtPoint == null || elementAtPoint == element || | |
| 278 axs.utils.isAncestor(elementAtPoint, element) || | |
| 279 axs.utils.isAncestor(element, elementAtPoint)) { | |
| 280 continue; | |
| 281 } | |
| 282 | |
| 283 var overlappingElementStyle = window.getComputedStyle(elementAtPoint, nu
ll); | |
| 284 if (!overlappingElementStyle) | |
| 285 continue; | |
| 286 | |
| 287 var overlappingElementBg = axs.utils.getBgColor(overlappingElementStyle, | |
| 288 elementAtPoint); | |
| 289 if (overlappingElementBg && overlappingElementBg.alpha > 0 && | |
| 290 overlappingElements.indexOf(elementAtPoint) < 0) { | |
| 291 overlappingElements.push(elementAtPoint); | |
| 292 } | |
| 293 } | |
| 294 | |
| 295 return overlappingElements; | |
| 296 }; | |
| 297 | |
| 298 /** | |
| 299 * @param {Element} element | |
| 300 * @return boolean | |
| 301 */ | |
| 302 axs.utils.elementIsHtmlControl = function(element) { | |
| 303 var defaultView = element.ownerDocument.defaultView; | |
| 304 | |
| 305 // HTML control | |
| 306 if (element instanceof defaultView.HTMLButtonElement) | |
| 307 return true; | |
| 308 if (element instanceof defaultView.HTMLInputElement) | |
| 309 return true; | |
| 310 if (element instanceof defaultView.HTMLSelectElement) | |
| 311 return true; | |
| 312 if (element instanceof defaultView.HTMLTextAreaElement) | |
| 313 return true; | |
| 314 | |
| 315 return false; | |
| 316 }; | |
| 317 | |
| 318 /** | |
| 319 * @param {Element} element | |
| 320 * @return boolean | |
| 321 */ | |
| 322 axs.utils.elementIsAriaWidget = function(element) { | |
| 323 if (element.hasAttribute('role')) { | |
| 324 var roleValue = element.getAttribute('role'); | |
| 325 // TODO is this correct? | |
| 326 if (roleValue) { | |
| 327 var role = axs.constants.ARIA_ROLES[roleValue]; | |
| 328 if (role && 'widget' in role['allParentRolesSet']) | |
| 329 return true; | |
| 330 } | |
| 331 } | |
| 332 return false; | |
| 333 } | |
| 334 | |
| 335 /** | |
| 336 * @param {Element} element | |
| 337 * @return {boolean} | |
| 338 */ | |
| 339 axs.utils.elementIsVisible = function(element) { | |
| 340 if (axs.utils.elementIsTransparent(element)) | |
| 341 return false; | |
| 342 if (axs.utils.elementHasZeroArea(element)) | |
| 343 return false; | |
| 344 if (axs.utils.elementIsOutsideScrollArea(element)) | |
| 345 return false; | |
| 346 | |
| 347 var overlappingElements = axs.utils.overlappingElements(element); | |
| 348 if (overlappingElements.length) | |
| 349 return false; | |
| 350 | |
| 351 return true; | |
| 352 }; | |
| 353 | |
| 354 /** | |
| 355 * @param {CSSStyleDeclaration} style | |
| 356 * @return {boolean} | |
| 357 */ | |
| 358 axs.utils.isLargeFont = function(style) { | |
| 359 var fontSize = style.fontSize; | |
| 360 var bold = style.fontWeight == 'bold'; | |
| 361 var matches = fontSize.match(/(\d+)px/); | |
| 362 if (matches) { | |
| 363 var fontSizePx = parseInt(matches[1], 10); | |
| 364 var bodyStyle = window.getComputedStyle(document.body, null); | |
| 365 var bodyFontSize = bodyStyle.fontSize; | |
| 366 matches = bodyFontSize.match(/(\d+)px/); | |
| 367 if (matches) { | |
| 368 var bodyFontSizePx = parseInt(matches[1], 10); | |
| 369 var boldLarge = bodyFontSizePx * 1.2; | |
| 370 var large = bodyFontSizePx * 1.5; | |
| 371 } else { | |
| 372 var boldLarge = 19.2; | |
| 373 var large = 24; | |
| 374 } | |
| 375 return (bold && fontSizePx >= boldLarge || fontSizePx >= large); | |
| 376 } | |
| 377 matches = fontSize.match(/(\d+)em/); | |
| 378 if (matches) { | |
| 379 var fontSizeEm = parseInt(matches[1], 10); | |
| 380 if (bold && fontSizeEm >= 1.2 || fontSizeEm >= 1.5) | |
| 381 return true; | |
| 382 return false; | |
| 383 } | |
| 384 matches = fontSize.match(/(\d+)%/); | |
| 385 if (matches) { | |
| 386 var fontSizePercent = parseInt(matches[1], 10); | |
| 387 if (bold && fontSizePercent >= 120 || fontSizePercent >= 150) | |
| 388 return true; | |
| 389 return false; | |
| 390 } | |
| 391 matches = fontSize.match(/(\d+)pt/); | |
| 392 if (matches) { | |
| 393 var fontSizePt = parseInt(matches[1], 10); | |
| 394 if (bold && fontSizePt >= 14 || fontSizePt >= 18) | |
| 395 return true; | |
| 396 return false; | |
| 397 } | |
| 398 return false; | |
| 399 }; | |
| 400 | |
| 401 /** | |
| 402 * @param {CSSStyleDeclaration} style | |
| 403 * @param {Element} element | |
| 404 * @return {?axs.utils.Color} | |
| 405 */ | |
| 406 axs.utils.getBgColor = function(style, element) { | |
| 407 var bgColorString = style.backgroundColor; | |
| 408 var bgColor = axs.utils.parseColor(bgColorString); | |
| 409 if (!bgColor) | |
| 410 return null; | |
| 411 | |
| 412 if (style.opacity < 1) | |
| 413 bgColor.alpha = bgColor.alpha * style.opacity | |
| 414 | |
| 415 if (bgColor.alpha < 1) { | |
| 416 var parentBg = axs.utils.getParentBgColor(element); | |
| 417 if (parentBg == null) | |
| 418 return null; | |
| 419 | |
| 420 bgColor = axs.utils.flattenColors(bgColor, parentBg); | |
| 421 } | |
| 422 return bgColor; | |
| 423 }; | |
| 424 | |
| 425 /** | |
| 426 * Gets the effective background color of the parent of |element|. | |
| 427 * @param {Element} element | |
| 428 * @return {?axs.utils.Color} | |
| 429 */ | |
| 430 axs.utils.getParentBgColor = function(element) { | |
| 431 /** @type {Element} */ var parent = element; | |
| 432 var bgStack = []; | |
| 433 var foundSolidColor = null; | |
| 434 while (parent = axs.utils.parentElement(parent)) { | |
| 435 var computedStyle = window.getComputedStyle(parent, null); | |
| 436 if (!computedStyle) | |
| 437 continue; | |
| 438 | |
| 439 var parentBg = axs.utils.parseColor(computedStyle.backgroundColor); | |
| 440 if (!parentBg) | |
| 441 continue; | |
| 442 | |
| 443 if (computedStyle.opacity < 1) | |
| 444 parentBg.alpha = parentBg.alpha * computedStyle.opacity; | |
| 445 | |
| 446 if (parentBg.alpha == 0) | |
| 447 continue; | |
| 448 | |
| 449 bgStack.push(parentBg); | |
| 450 | |
| 451 if (parentBg.alpha == 1) { | |
| 452 foundSolidColor = true; | |
| 453 break; | |
| 454 } | |
| 455 } | |
| 456 | |
| 457 if (!foundSolidColor) | |
| 458 bgStack.push(new axs.utils.Color(255, 255, 255, 1)); | |
| 459 | |
| 460 var bg = bgStack.pop(); | |
| 461 while (bgStack.length) { | |
| 462 var fg = bgStack.pop(); | |
| 463 bg = axs.utils.flattenColors(fg, bg); | |
| 464 } | |
| 465 return bg; | |
| 466 } | |
| 467 | |
| 468 /** | |
| 469 * @param {CSSStyleDeclaration} style | |
| 470 * @param {axs.utils.Color} bgColor The background color, which may come from | |
| 471 * another element (such as a parent element), for flattening into the | |
| 472 * foreground color. | |
| 473 * @return {?axs.utils.Color} | |
| 474 */ | |
| 475 axs.utils.getFgColor = function(style, element, bgColor) { | |
| 476 var fgColorString = style.color; | |
| 477 var fgColor = axs.utils.parseColor(fgColorString); | |
| 478 if (!fgColor) | |
| 479 return null; | |
| 480 | |
| 481 if (fgColor.alpha < 1) | |
| 482 fgColor = axs.utils.flattenColors(fgColor, bgColor); | |
| 483 | |
| 484 if (style.opacity < 1) { | |
| 485 var parentBg = axs.utils.getParentBgColor(element); | |
| 486 fgColor.alpha = fgColor.alpha * style.opacity; | |
| 487 fgColor = axs.utils.flattenColors(fgColor, parentBg); | |
| 488 } | |
| 489 | |
| 490 return fgColor; | |
| 491 }; | |
| 492 | |
| 493 /** | |
| 494 * @param {string} colorString The color string from CSS. | |
| 495 * @return {?axs.utils.Color} | |
| 496 */ | |
| 497 axs.utils.parseColor = function(colorString) { | |
| 498 var rgbRegex = /^rgb\((\d+), (\d+), (\d+)\)$/; | |
| 499 var match = colorString.match(rgbRegex); | |
| 500 | |
| 501 if (match) { | |
| 502 var r = parseInt(match[1], 10); | |
| 503 var g = parseInt(match[2], 10); | |
| 504 var b = parseInt(match[3], 10); | |
| 505 var a = 1 | |
| 506 return new axs.utils.Color(r, g, b, a); | |
| 507 } | |
| 508 | |
| 509 var rgbaRegex = /^rgba\((\d+), (\d+), (\d+), (\d*(\.\d+)?)\)/; | |
| 510 match = colorString.match(rgbaRegex); | |
| 511 if (match) { | |
| 512 var r = parseInt(match[1], 10); | |
| 513 var g = parseInt(match[2], 10); | |
| 514 var b = parseInt(match[3] ,10); | |
| 515 var a = parseFloat(match[4]); | |
| 516 return new axs.utils.Color(r, g, b, a); | |
| 517 } | |
| 518 | |
| 519 return null; | |
| 520 }; | |
| 521 | |
| 522 /** | |
| 523 * @param {number} value The value of a color channel, 0 <= value <= 0xFF | |
| 524 * @return {string} | |
| 525 */ | |
| 526 axs.utils.colorChannelToString = function(value) { | |
| 527 value = Math.round(value); | |
| 528 if (value <= 0xF) | |
| 529 return '0' + value.toString(16); | |
| 530 return value.toString(16); | |
| 531 }; | |
| 532 | |
| 533 /** | |
| 534 * @param {axs.utils.Color} color | |
| 535 * @return {string} | |
| 536 */ | |
| 537 axs.utils.colorToString = function(color) { | |
| 538 if (color.alpha == 1) { | |
| 539 return '#' + axs.utils.colorChannelToString(color.red) + | |
| 540 axs.utils.colorChannelToString(color.green) + axs.utils.colorChannelToS
tring(color.blue); | |
| 541 } | |
| 542 else | |
| 543 return 'rgba(' + [color.red, color.green, color.blue, color.alpha].join(
',') + ')'; | |
| 544 }; | |
| 545 | |
| 546 axs.utils.luminanceFromContrastRatio = function(luminance, contrast, higher) { | |
| 547 if (higher) { | |
| 548 var newLuminance = (luminance + 0.05) * contrast - 0.05; | |
| 549 return newLuminance; | |
| 550 } else { | |
| 551 var newLuminance = (luminance + 0.05) / contrast - 0.05; | |
| 552 return newLuminance; | |
| 553 } | |
| 554 }; | |
| 555 | |
| 556 axs.utils.translateColor = function(ycc, luminance) { | |
| 557 var oldLuminance = ycc[0]; | |
| 558 if (oldLuminance > luminance) | |
| 559 var endpoint = 0 | |
| 560 else | |
| 561 var endpoint = 1; | |
| 562 | |
| 563 var d = luminance - oldLuminance; | |
| 564 var scale = d / (endpoint - oldLuminance) | |
| 565 | |
| 566 /** @type {Array.<number>} */ var translatedColor = [ luminance, | |
| 567 ycc[1] - ycc[1] * scal
e, | |
| 568 ycc[2] - ycc[2] * scal
e ]; | |
| 569 var rgb = axs.utils.fromYCC(translatedColor); | |
| 570 return rgb; | |
| 571 }; | |
| 572 | |
| 573 /** | |
| 574 * @param {axs.utils.Color} fgColor | |
| 575 * @param {axs.utils.Color} bgColor | |
| 576 * @param {number} contrastRatio | |
| 577 * @param {CSSStyleDeclaration} style | |
| 578 * @return {Object} | |
| 579 */ | |
| 580 axs.utils.suggestColors = function(bgColor, fgColor, contrastRatio, style) { | |
| 581 if (!axs.utils.isLowContrast(contrastRatio, style, true)) | |
| 582 return null; | |
| 583 var colors = {}; | |
| 584 var bgLuminance = axs.utils.calculateLuminance(bgColor); | |
| 585 var fgLuminance = axs.utils.calculateLuminance(fgColor); | |
| 586 | |
| 587 var levelAAContrast = axs.utils.isLargeFont(style) ? 3.0 : 4.5; | |
| 588 var levelAAAContrast = axs.utils.isLargeFont(style) ? 4.5 : 7.0; | |
| 589 var fgLuminanceIsHigher = fgLuminance > bgLuminance; | |
| 590 var desiredFgLuminanceAA = axs.utils.luminanceFromContrastRatio(bgLuminance,
levelAAContrast + 0.02, fgLuminanceIsHigher); | |
| 591 var desiredFgLuminanceAAA = axs.utils.luminanceFromContrastRatio(bgLuminance
, levelAAAContrast + 0.02, fgLuminanceIsHigher); | |
| 592 var fgYCC = axs.utils.toYCC(fgColor); | |
| 593 | |
| 594 if (axs.utils.isLowContrast(contrastRatio, style, false) && | |
| 595 desiredFgLuminanceAA <= 1 && desiredFgLuminanceAA >= 0) { | |
| 596 var newFgColorAA = axs.utils.translateColor(fgYCC, desiredFgLuminanceAA)
; | |
| 597 var newContrastRatioAA = axs.utils.calculateContrastRatio(newFgColorAA,
bgColor); | |
| 598 var newLuminance = axs.utils.calculateLuminance(newFgColorAA); | |
| 599 var suggestedColorsAA = {} | |
| 600 suggestedColorsAA['fg'] = axs.utils.colorToString(newFgColorAA); | |
| 601 suggestedColorsAA['bg'] = axs.utils.colorToString(bgColor); | |
| 602 suggestedColorsAA['contrast'] = newContrastRatioAA.toFixed(2); | |
| 603 colors['AA'] = suggestedColorsAA; | |
| 604 } | |
| 605 if (axs.utils.isLowContrast(contrastRatio, style, true) && | |
| 606 desiredFgLuminanceAAA <= 1 && desiredFgLuminanceAAA >= 0) { | |
| 607 var newFgColorAAA = axs.utils.translateColor(fgYCC, desiredFgLuminanceAA
A); | |
| 608 var newContrastRatioAAA = axs.utils.calculateContrastRatio(newFgColorAAA
, bgColor); | |
| 609 var suggestedColorsAAA = {}; | |
| 610 suggestedColorsAAA['fg'] = axs.utils.colorToString(newFgColorAAA); | |
| 611 suggestedColorsAAA['bg'] = axs.utils.colorToString(bgColor); | |
| 612 suggestedColorsAAA['contrast'] = newContrastRatioAAA.toFixed(2); | |
| 613 colors['AAA'] = suggestedColorsAAA; | |
| 614 } | |
| 615 var desiredBgLuminanceAA = axs.utils.luminanceFromContrastRatio(fgLuminance,
levelAAContrast + 0.02, !fgLuminanceIsHigher); | |
| 616 var desiredBgLuminanceAAA = axs.utils.luminanceFromContrastRatio(fgLuminance
, levelAAAContrast + 0.02, !fgLuminanceIsHigher); | |
| 617 var bgLuminanceBoundary = fgLuminanceIsHigher ? 0 : 1; | |
| 618 var bgYCC = axs.utils.toYCC(bgColor); | |
| 619 | |
| 620 if (!('AA' in colors) && axs.utils.isLowContrast(contrastRatio, style, false
) && | |
| 621 desiredBgLuminanceAA <= 1 && desiredBgLuminanceAA >= 0) { | |
| 622 var newBgColorAA = axs.utils.translateColor(bgYCC, desiredBgLuminanceAA)
; | |
| 623 var newContrastRatioAA = axs.utils.calculateContrastRatio(fgColor, newBg
ColorAA); | |
| 624 var suggestedColorsAA = {}; | |
| 625 suggestedColorsAA['bg'] = axs.utils.colorToString(newBgColorAA); | |
| 626 suggestedColorsAA['fg'] = axs.utils.colorToString(fgColor); | |
| 627 suggestedColorsAA['contrast'] = newContrastRatioAA.toFixed(2); | |
| 628 colors['AA'] = suggestedColorsAA; | |
| 629 } | |
| 630 if (!('AAA' in colors) && axs.utils.isLowContrast(contrastRatio, style, true
) && | |
| 631 desiredBgLuminanceAAA <= 1 && desiredBgLuminanceAAA >= 0) { | |
| 632 var newBgColorAAA = axs.utils.translateColor(bgYCC, desiredBgLuminanceAA
A); | |
| 633 var newContrastRatioAAA = axs.utils.calculateContrastRatio(fgColor, newB
gColorAAA); | |
| 634 var suggestedColorsAAA = {}; | |
| 635 suggestedColorsAAA['bg'] = axs.utils.colorToString(newBgColorAAA); | |
| 636 suggestedColorsAAA['fg'] = axs.utils.colorToString(fgColor); | |
| 637 suggestedColorsAAA['contrast'] = newContrastRatioAAA.toFixed(2); | |
| 638 colors['AAA'] = suggestedColorsAAA; | |
| 639 } | |
| 640 return colors; | |
| 641 } | |
| 642 | |
| 643 /** | |
| 644 * Combine the two given color according to alpha blending. | |
| 645 * @param {axs.utils.Color} fgColor | |
| 646 * @param {axs.utils.Color} bgColor | |
| 647 * @return {axs.utils.Color} | |
| 648 */ | |
| 649 axs.utils.flattenColors = function(fgColor, bgColor) { | |
| 650 var alpha = fgColor.alpha; | |
| 651 var r = ((1 - alpha) * bgColor.red) + (alpha * fgColor.red); | |
| 652 var g = ((1 - alpha) * bgColor.green) + (alpha * fgColor.green); | |
| 653 var b = ((1 - alpha) * bgColor.blue) + (alpha * fgColor.blue); | |
| 654 var a = fgColor.alpha + (bgColor.alpha * (1 - fgColor.alpha)); | |
| 655 | |
| 656 return new axs.utils.Color(r, g, b, a); | |
| 657 }; | |
| 658 | |
| 659 /** | |
| 660 * Calculate the luminance of the given color using the WCAG algorithm. | |
| 661 * @param {axs.utils.Color} color | |
| 662 * @return {number} | |
| 663 */ | |
| 664 axs.utils.calculateLuminance = function(color) { | |
| 665 /* var rSRGB = color.red / 255; | |
| 666 var gSRGB = color.green / 255; | |
| 667 var bSRGB = color.blue / 255; | |
| 668 | |
| 669 var r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow(((rSRGB + 0.055)/1.055),
2.4); | |
| 670 var g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow(((gSRGB + 0.055)/1.055),
2.4); | |
| 671 var b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow(((bSRGB + 0.055)/1.055),
2.4); | |
| 672 | |
| 673 return 0.2126 * r + 0.7152 * g + 0.0722 * b; */ | |
| 674 var ycc = axs.utils.toYCC(color); | |
| 675 return ycc[0]; | |
| 676 }; | |
| 677 | |
| 678 /** | |
| 679 * Returns an RGB to YCC conversion matrix for the given kR, kB constants. | |
| 680 * @param {number} kR | |
| 681 * @param {number} kB | |
| 682 * @return {Array.<Array.<number>>} | |
| 683 */ | |
| 684 axs.utils.RGBToYCCMatrix = function(kR, kB) { | |
| 685 return [ | |
| 686 [ | |
| 687 kR, | |
| 688 (1 - kR - kB), | |
| 689 kB | |
| 690 ], | |
| 691 [ | |
| 692 -kR/(2 - 2*kB), | |
| 693 (kR + kB - 1)/(2 - 2*kB), | |
| 694 (1 - kB)/(2 - 2*kB) | |
| 695 ], | |
| 696 [ | |
| 697 (1 - kR)/(2 - 2*kR), | |
| 698 (kR + kB - 1)/(2 - 2*kR), | |
| 699 -kB/(2 - 2*kR) | |
| 700 ] | |
| 701 ]; | |
| 702 } | |
| 703 | |
| 704 /** | |
| 705 * Return the inverse of the given 3x3 matrix. | |
| 706 * @param {Array.<Array.<number>>} matrix | |
| 707 * @return Array.<Array.<number>> The inverse of the given matrix. | |
| 708 */ | |
| 709 axs.utils.invert3x3Matrix = function(matrix) { | |
| 710 var a = matrix[0][0]; | |
| 711 var b = matrix[0][1]; | |
| 712 var c = matrix[0][2]; | |
| 713 var d = matrix[1][0]; | |
| 714 var e = matrix[1][1]; | |
| 715 var f = matrix[1][2]; | |
| 716 var g = matrix[2][0]; | |
| 717 var h = matrix[2][1]; | |
| 718 var k = matrix[2][2]; | |
| 719 | |
| 720 var A = (e*k - f*h); | |
| 721 var B = (f*g - d*k); | |
| 722 var C = (d*h - e*g); | |
| 723 var D = (c*h - b*k); | |
| 724 var E = (a*k - c*g); | |
| 725 var F = (g*b - a*h); | |
| 726 var G = (b*f - c*e); | |
| 727 var H = (c*d - a*f); | |
| 728 var K = (a*e - b*d); | |
| 729 | |
| 730 var det = a * (e*k - f*h) - b * (k*d - f*g) + c * (d*h - e*g); | |
| 731 var z = 1/det; | |
| 732 | |
| 733 return axs.utils.scalarMultiplyMatrix([ | |
| 734 [ A, D, G ], | |
| 735 [ B, E, H ], | |
| 736 [ C, F, K ] | |
| 737 ], z); | |
| 738 }; | |
| 739 | |
| 740 axs.utils.scalarMultiplyMatrix = function(matrix, scalar) { | |
| 741 var result = []; | |
| 742 result[0] = []; | |
| 743 result[1] = []; | |
| 744 result[2] = []; | |
| 745 | |
| 746 for (var i = 0; i < 3; i++) { | |
| 747 for (var j = 0; j < 3; j++) { | |
| 748 result[i][j] = matrix[i][j] * scalar; | |
| 749 } | |
| 750 } | |
| 751 | |
| 752 return result; | |
| 753 } | |
| 754 | |
| 755 axs.utils.kR = 0.2126; | |
| 756 axs.utils.kB = 0.0722; | |
| 757 axs.utils.YCC_MATRIX = axs.utils.RGBToYCCMatrix(axs.utils.kR, axs.utils.kB); | |
| 758 axs.utils.INVERTED_YCC_MATRIX = axs.utils.invert3x3Matrix(axs.utils.YCC_MATRIX); | |
| 759 | |
| 760 /** | |
| 761 * Multiply the given color vector by the given transformation matrix. | |
| 762 * @param {Array.<Array.<number>>} matrix A 3x3 conversion matrix | |
| 763 * @param {Array.<number>} vector A 3-element color vector | |
| 764 * @return {Array.<number>} A 3-element color vector | |
| 765 */ | |
| 766 axs.utils.convertColor = function(matrix, vector) { | |
| 767 var a = matrix[0][0]; | |
| 768 var b = matrix[0][1]; | |
| 769 var c = matrix[0][2]; | |
| 770 var d = matrix[1][0]; | |
| 771 var e = matrix[1][1]; | |
| 772 var f = matrix[1][2]; | |
| 773 var g = matrix[2][0]; | |
| 774 var h = matrix[2][1]; | |
| 775 var k = matrix[2][2]; | |
| 776 | |
| 777 var x = vector[0]; | |
| 778 var y = vector[1]; | |
| 779 var z = vector[2]; | |
| 780 | |
| 781 return [ | |
| 782 a*x + b*y + c*z, | |
| 783 d*x + e*y + f*z, | |
| 784 g*x + h*y + k*z | |
| 785 ]; | |
| 786 }; | |
| 787 | |
| 788 axs.utils.multiplyMatrices = function(matrix1, matrix2) { | |
| 789 var result = []; | |
| 790 result[0] = []; | |
| 791 result[1] = []; | |
| 792 result[2] = []; | |
| 793 | |
| 794 for (var i = 0; i < 3; i++) { | |
| 795 for (var j = 0; j < 3; j++) { | |
| 796 result[i][j] = matrix1[i][0] * matrix2[0][j] + | |
| 797 matrix1[i][1] * matrix2[1][j] + | |
| 798 matrix1[i][2] * matrix2[2][j]; | |
| 799 } | |
| 800 } | |
| 801 return result; | |
| 802 } | |
| 803 | |
| 804 /** | |
| 805 * Convert a given RGB color to YCC. | |
| 806 */ | |
| 807 axs.utils.toYCC = function(color) { | |
| 808 var rSRGB = color.red / 255; | |
| 809 var gSRGB = color.green / 255; | |
| 810 var bSRGB = color.blue / 255; | |
| 811 | |
| 812 var r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow(((rSRGB + 0.055)/1.055),
2.4); | |
| 813 var g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow(((gSRGB + 0.055)/1.055),
2.4); | |
| 814 var b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow(((bSRGB + 0.055)/1.055),
2.4); | |
| 815 | |
| 816 return axs.utils.convertColor(axs.utils.YCC_MATRIX, [r, g, b]); | |
| 817 }; | |
| 818 | |
| 819 /** | |
| 820 * Convert a color from a YCC color (as a vector) to an RGB color | |
| 821 * @param {Array.<number>} yccColor | |
| 822 * @return {axs.utils.Color} | |
| 823 */ | |
| 824 axs.utils.fromYCC = function(yccColor) { | |
| 825 var rgb = axs.utils.convertColor(axs.utils.INVERTED_YCC_MATRIX, yccColor); | |
| 826 | |
| 827 var r = rgb[0]; | |
| 828 var g = rgb[1]; | |
| 829 var b = rgb[2]; | |
| 830 var rSRGB = r <= 0.00303949 ? (r * 12.92) : (Math.pow(r, (1/2.4)) * 1.055) -
0.055; | |
| 831 var gSRGB = g <= 0.00303949 ? (g * 12.92) : (Math.pow(g, (1/2.4)) * 1.055) -
0.055; | |
| 832 var bSRGB = b <= 0.00303949 ? (b * 12.92) : (Math.pow(b, (1/2.4)) * 1.055) -
0.055; | |
| 833 | |
| 834 var red = Math.min(Math.max(Math.round(rSRGB * 255), 0), 255); | |
| 835 var green = Math.min(Math.max(Math.round(gSRGB * 255), 0), 255); | |
| 836 var blue = Math.min(Math.max(Math.round(bSRGB * 255), 0), 255); | |
| 837 | |
| 838 return new axs.utils.Color(red, green, blue, 1); | |
| 839 }; | |
| 840 | |
| 841 axs.utils.scalarMultiplyMatrix = function(matrix, scalar) { | |
| 842 var result = []; | |
| 843 result[0] = []; | |
| 844 result[1] = []; | |
| 845 result[2] = []; | |
| 846 | |
| 847 for (var i = 0; i < 3; i++) { | |
| 848 for (var j = 0; j < 3; j++) { | |
| 849 result[i][j] = matrix[i][j] * scalar; | |
| 850 } | |
| 851 } | |
| 852 | |
| 853 return result; | |
| 854 } | |
| 855 | |
| 856 axs.utils.multiplyMatrices = function(matrix1, matrix2) { | |
| 857 var result = []; | |
| 858 result[0] = []; | |
| 859 result[1] = []; | |
| 860 result[2] = []; | |
| 861 | |
| 862 for (var i = 0; i < 3; i++) { | |
| 863 for (var j = 0; j < 3; j++) { | |
| 864 result[i][j] = matrix1[i][0] * matrix2[0][j] + | |
| 865 matrix1[i][1] * matrix2[1][j] + | |
| 866 matrix1[i][2] * matrix2[2][j]; | |
| 867 } | |
| 868 } | |
| 869 return result; | |
| 870 } | |
| 871 | |
| 872 /** | |
| 873 * @param {Element} element | |
| 874 * @return {?number} | |
| 875 */ | |
| 876 axs.utils.getContrastRatioForElement = function(element) { | |
| 877 var style = window.getComputedStyle(element, null); | |
| 878 return axs.utils.getContrastRatioForElementWithComputedStyle(style, element)
; | |
| 879 }; | |
| 880 | |
| 881 /** | |
| 882 * @param {CSSStyleDeclaration} style | |
| 883 * @param {Element} element | |
| 884 * @return {?number} | |
| 885 */ | |
| 886 axs.utils.getContrastRatioForElementWithComputedStyle = function(style, element)
{ | |
| 887 if (axs.utils.isElementHidden(element)) | |
| 888 return null; | |
| 889 | |
| 890 var bgColor = axs.utils.getBgColor(style, element); | |
| 891 if (!bgColor) | |
| 892 return null; | |
| 893 | |
| 894 var fgColor = axs.utils.getFgColor(style, element, bgColor); | |
| 895 if (!fgColor) | |
| 896 return null; | |
| 897 | |
| 898 return axs.utils.calculateContrastRatio(fgColor, bgColor); | |
| 899 }; | |
| 900 | |
| 901 /** | |
| 902 * @param {Element} element | |
| 903 * @return {boolean} | |
| 904 */ | |
| 905 axs.utils.isNativeTextElement = function(element) { | |
| 906 var tagName = element.tagName.toLowerCase(); | |
| 907 var type = element.type ? element.type.toLowerCase() : ''; | |
| 908 if (tagName == 'textarea') | |
| 909 return true; | |
| 910 if (tagName != 'input') | |
| 911 return false; | |
| 912 | |
| 913 switch (type) { | |
| 914 case 'email': | |
| 915 case 'number': | |
| 916 case 'password': | |
| 917 case 'search': | |
| 918 case 'text': | |
| 919 case 'tel': | |
| 920 case 'url': | |
| 921 case '': | |
| 922 return true; | |
| 923 default: | |
| 924 return false; | |
| 925 } | |
| 926 }; | |
| 927 | |
| 928 /** | |
| 929 * @param {number} contrastRatio | |
| 930 * @param {CSSStyleDeclaration} style | |
| 931 * @param {boolean=} opt_strict Whether to use AA (false) or AAA (true) level | |
| 932 * @return {boolean} | |
| 933 */ | |
| 934 axs.utils.isLowContrast = function(contrastRatio, style, opt_strict) { | |
| 935 // Round to nearest 0.1 | |
| 936 var roundedContrastRatio = (Math.round(contrastRatio * 10) / 10); | |
| 937 if (!opt_strict) { | |
| 938 return roundedContrastRatio < 3.0 || | |
| 939 (!axs.utils.isLargeFont(style) && roundedContrastRatio < 4.5); | |
| 940 } else { | |
| 941 return roundedContrastRatio < 4.5 || | |
| 942 (!axs.utils.isLargeFont(style) && roundedContrastRatio < 7.0); | |
| 943 } | |
| 944 }; | |
| 945 | |
| 946 /** | |
| 947 * @param {Element} element | |
| 948 * @return {boolean} | |
| 949 */ | |
| 950 axs.utils.hasLabel = function(element) { | |
| 951 var tagName = element.tagName.toLowerCase(); | |
| 952 var type = element.type ? element.type.toLowerCase() : ''; | |
| 953 | |
| 954 if (element.hasAttribute('aria-label')) | |
| 955 return true; | |
| 956 if (element.hasAttribute('title')) | |
| 957 return true; | |
| 958 if (tagName == 'img' && element.hasAttribute('alt')) | |
| 959 return true; | |
| 960 if (tagName == 'input' && type == 'image' && element.hasAttribute('alt')) | |
| 961 return true; | |
| 962 if (tagName == 'input' && (type == 'submit' || type == 'reset')) | |
| 963 return true; | |
| 964 | |
| 965 // There's a separate audit that makes sure this points to an actual element
or elements. | |
| 966 if (element.hasAttribute('aria-labelledby')) | |
| 967 return true; | |
| 968 | |
| 969 if (axs.utils.isNativeTextElement(element) && element.hasAttribute('placehol
der')) | |
| 970 return true; | |
| 971 | |
| 972 if (element.hasAttribute('id')) { | |
| 973 var labelsFor = document.querySelectorAll('label[for="' + element.id + '
"]'); | |
| 974 if (labelsFor.length > 0) | |
| 975 return true; | |
| 976 } | |
| 977 | |
| 978 var parent = axs.utils.parentElement(element); | |
| 979 while (parent) { | |
| 980 if (parent.tagName.toLowerCase() == 'label') { | |
| 981 var parentLabel = /** HTMLLabelElement */ parent; | |
| 982 if (parentLabel.control == element) | |
| 983 return true; | |
| 984 } | |
| 985 parent = axs.utils.parentElement(parent); | |
| 986 } | |
| 987 return false; | |
| 988 }; | |
| 989 | |
| 990 /** | |
| 991 * @param {Element} element An element to check. | |
| 992 * @return {boolean} True if the element is hidden from accessibility. | |
| 993 */ | |
| 994 axs.utils.isElementHidden = function(element) { | |
| 995 if (!(element instanceof element.ownerDocument.defaultView.HTMLElement)) | |
| 996 return false; | |
| 997 | |
| 998 if (element.hasAttribute('chromevoxignoreariahidden')) | |
| 999 var chromevoxignoreariahidden = true; | |
| 1000 | |
| 1001 var style = window.getComputedStyle(element, null); | |
| 1002 if (style.display == 'none' || style.visibility == 'hidden') | |
| 1003 return true; | |
| 1004 | |
| 1005 if (element.hasAttribute('aria-hidden') && | |
| 1006 element.getAttribute('aria-hidden').toLowerCase() == 'true') { | |
| 1007 return !chromevoxignoreariahidden; | |
| 1008 } | |
| 1009 | |
| 1010 return false; | |
| 1011 }; | |
| 1012 | |
| 1013 /** | |
| 1014 * @param {Element} element An element to check. | |
| 1015 * @return {boolean} True if the element or one of its ancestors is | |
| 1016 * hidden from accessibility. | |
| 1017 */ | |
| 1018 axs.utils.isElementOrAncestorHidden = function(element) { | |
| 1019 if (axs.utils.isElementHidden(element)) | |
| 1020 return true; | |
| 1021 | |
| 1022 if (axs.utils.parentElement(element)) | |
| 1023 return axs.utils.isElementOrAncestorHidden(axs.utils.parentElement(eleme
nt)); | |
| 1024 else | |
| 1025 return false; | |
| 1026 }; | |
| 1027 | |
| 1028 /** | |
| 1029 * @param {Element} element An element to check | |
| 1030 * @return {boolean} True if the given element is an inline element, false | |
| 1031 * otherwise. | |
| 1032 */ | |
| 1033 axs.utils.isInlineElement = function(element) { | |
| 1034 var tagName = element.tagName.toUpperCase(); | |
| 1035 return axs.constants.InlineElements[tagName]; | |
| 1036 } | |
| 1037 | |
| 1038 /** | |
| 1039 * @param {Element} element | |
| 1040 * @return {Object|boolean} | |
| 1041 */ | |
| 1042 axs.utils.getRoles = function(element) { | |
| 1043 if (!element.hasAttribute('role')) | |
| 1044 return false; | |
| 1045 var roleValue = element.getAttribute('role'); | |
| 1046 var roleNames = roleValue.split(' '); | |
| 1047 var roles = [] | |
| 1048 var valid = true; | |
| 1049 for (var i = 0; i < roleNames.length; i++) { | |
| 1050 var role = roleNames[i]; | |
| 1051 if (axs.constants.ARIA_ROLES[role]) | |
| 1052 roles.push({'name': role, 'details': axs.constants.ARIA_ROLES[role],
'valid': true}); | |
| 1053 else { | |
| 1054 roles.push({'name': role, 'valid': false}); | |
| 1055 valid = false; | |
| 1056 } | |
| 1057 } | |
| 1058 | |
| 1059 return { 'roles': roles, 'valid': valid }; | |
| 1060 }; | |
| 1061 | |
| 1062 /** | |
| 1063 * @param {!string} propertyName | |
| 1064 * @param {!string} value | |
| 1065 * @param {!Element} element | |
| 1066 * @return {!Object} | |
| 1067 */ | |
| 1068 axs.utils.getAriaPropertyValue = function(propertyName, value, element) { | |
| 1069 var propertyKey = propertyName.replace(/^aria-/, ''); | |
| 1070 var property = axs.constants.ARIA_PROPERTIES[propertyKey]; | |
| 1071 var result = { 'name': propertyName, 'rawValue': value }; | |
| 1072 if (!property) { | |
| 1073 result.valid = false; | |
| 1074 result.reason = '"' + propertyName + '" is not a valid ARIA property'; | |
| 1075 return result; | |
| 1076 } | |
| 1077 | |
| 1078 var propertyType = property.valueType; | |
| 1079 if (!propertyType) { | |
| 1080 result.valid = false; | |
| 1081 result.reason = '"' + propertyName + '" is not a valid ARIA property'; | |
| 1082 return result; | |
| 1083 } | |
| 1084 | |
| 1085 switch (propertyType) { | |
| 1086 case "idref": | |
| 1087 var isValid = axs.utils.isValidIDRefValue(value, element); | |
| 1088 result.valid = isValid.valid; | |
| 1089 result.reason = isValid.reason; | |
| 1090 result.idref = isValid.idref; | |
| 1091 case "idref_list": | |
| 1092 var idrefValues = value.split(/\s+/); | |
| 1093 result.valid = true; | |
| 1094 for (var i = 0; i < idrefValues.length; i++) { | |
| 1095 var refIsValid = axs.utils.isValidIDRefValue(idrefValues[i], elemen
t); | |
| 1096 if (!refIsValid.valid) | |
| 1097 result.valid = false; | |
| 1098 if (result.values) | |
| 1099 result.values.push(refIsValid); | |
| 1100 else | |
| 1101 result.values = [refIsValid]; | |
| 1102 } | |
| 1103 return result; | |
| 1104 case "integer": | |
| 1105 case "decimal": | |
| 1106 var validNumber = axs.utils.isValidNumber(value); | |
| 1107 if (!validNumber.valid) { | |
| 1108 result.valid = false; | |
| 1109 result.reason = validNumber.reason; | |
| 1110 return result; | |
| 1111 } | |
| 1112 if (Math.floor(validNumber.value) != validNumber.value) { | |
| 1113 result.valid = false; | |
| 1114 result.reason = '' + value + ' is not a whole integer'; | |
| 1115 } else { | |
| 1116 result.valid = true; | |
| 1117 result.value = validNumber.value; | |
| 1118 } | |
| 1119 return result; | |
| 1120 case "number": | |
| 1121 var validNumber = axs.utils.isValidNumber(value); | |
| 1122 if (validNumber.valid) { | |
| 1123 result.valid = true; | |
| 1124 result.value = validNumber.value; | |
| 1125 } | |
| 1126 case "string": | |
| 1127 result.valid = true; | |
| 1128 result.value = value; | |
| 1129 return result; | |
| 1130 case "token": | |
| 1131 var validTokenValue = axs.utils.isValidTokenValue(propertyName, value.to
LowerCase()); | |
| 1132 if (validTokenValue.valid) { | |
| 1133 result.valid = true; | |
| 1134 result.value = validTokenValue.value; | |
| 1135 return result; | |
| 1136 } else { | |
| 1137 result.valid = false; | |
| 1138 result.value = value; | |
| 1139 result.reason = validTokenValue.reason; | |
| 1140 return result; | |
| 1141 } | |
| 1142 case "token_list": | |
| 1143 var tokenValues = value.split(/\s+/); | |
| 1144 result.valid = true; | |
| 1145 for (var i = 0; i < tokenValues.length; i++) { | |
| 1146 var validTokenValue = axs.utils.isValidTokenValue(propertyName, toke
nValues[i].toLowerCase()); | |
| 1147 if (!validTokenValue.valid) { | |
| 1148 result.valid = false; | |
| 1149 if (result.reason) { | |
| 1150 result.reason = [ result.reason ]; | |
| 1151 result.reason.push(validTokenValue.reason); | |
| 1152 } else { | |
| 1153 result.reason = validTokenValue.reason; | |
| 1154 result.possibleValues = validTokenValue.possibleValues; | |
| 1155 } | |
| 1156 } | |
| 1157 // TODO (more structured result) | |
| 1158 if (result.values) | |
| 1159 result.values.push(validTokenValue.value); | |
| 1160 else | |
| 1161 result.values = [validTokenValue.value]; | |
| 1162 } | |
| 1163 return result; | |
| 1164 case "tristate": | |
| 1165 var validTristate = axs.utils.isPossibleValue(value.toLowerCase(), axs.c
onstants.MIXED_VALUES, propertyName); | |
| 1166 if (validTristate.valid) { | |
| 1167 result.valid = true; | |
| 1168 result.value = validTristate.value; | |
| 1169 } else { | |
| 1170 result.valid = false; | |
| 1171 result.value = value; | |
| 1172 result.reason = validTristate.reason; | |
| 1173 } | |
| 1174 return result; | |
| 1175 case "boolean": | |
| 1176 var validBoolean = axs.utils.isValidBoolean(value); | |
| 1177 if (validBoolean.valid) { | |
| 1178 result.valid = true; | |
| 1179 result.value = validBoolean.value; | |
| 1180 } else { | |
| 1181 result.valid = false; | |
| 1182 result.value = value; | |
| 1183 result.reason = validBoolean.reason; | |
| 1184 } | |
| 1185 return result; | |
| 1186 } | |
| 1187 result.valid = false | |
| 1188 result.reason = 'Not a valid ARIA property'; | |
| 1189 return result; | |
| 1190 }; | |
| 1191 | |
| 1192 /** | |
| 1193 * @param {string} propertyName The name of the property. | |
| 1194 * @param {string} value The value to check. | |
| 1195 * @return {!Object} | |
| 1196 */ | |
| 1197 axs.utils.isValidTokenValue = function(propertyName, value) { | |
| 1198 var propertyKey = propertyName.replace(/^aria-/, ''); | |
| 1199 var propertyDetails = axs.constants.ARIA_PROPERTIES[propertyKey]; | |
| 1200 var possibleValues = propertyDetails.valuesSet; | |
| 1201 return axs.utils.isPossibleValue(value, possibleValues, propertyName); | |
| 1202 }; | |
| 1203 | |
| 1204 /** | |
| 1205 * @param {string} value | |
| 1206 * @param {Object.<string, boolean>} possibleValues | |
| 1207 * @return {!Object} | |
| 1208 */ | |
| 1209 axs.utils.isPossibleValue = function(value, possibleValues, propertyName) { | |
| 1210 if (!possibleValues[value]) | |
| 1211 return { 'valid': false, | |
| 1212 'value': value, | |
| 1213 'reason': '"' + value + '" is not a valid value for ' + propert
yName, | |
| 1214 'possibleValues': Object.keys(possibleValues) }; | |
| 1215 return { 'valid': true, 'value': value }; | |
| 1216 }; | |
| 1217 | |
| 1218 /** | |
| 1219 * @param {string} value | |
| 1220 * @return {!Object} | |
| 1221 */ | |
| 1222 axs.utils.isValidBoolean = function(value) { | |
| 1223 try { | |
| 1224 var parsedValue = JSON.parse(value); | |
| 1225 } catch (e) { | |
| 1226 parsedValue = ''; | |
| 1227 } | |
| 1228 if (typeof(parsedValue) != 'boolean') | |
| 1229 return { 'valid': false, | |
| 1230 'value': value, | |
| 1231 'reason': '"' + value + '" is not a true/false value' }; | |
| 1232 return { 'valid': true, 'value': parsedValue }; | |
| 1233 }; | |
| 1234 | |
| 1235 /** | |
| 1236 * @param {string} value | |
| 1237 * @param {!Element} element | |
| 1238 * @return {!Object} | |
| 1239 */ | |
| 1240 axs.utils.isValidIDRefValue = function(value, element) { | |
| 1241 if (value.length == 0) | |
| 1242 return { 'valid': true, 'idref': value }; | |
| 1243 if (!element.ownerDocument.getElementById(value)) | |
| 1244 return { 'valid': false, | |
| 1245 'idref': value, | |
| 1246 'reason': 'No element with ID "' + value + '"' }; | |
| 1247 return { 'valid': true, 'idref': value }; | |
| 1248 }; | |
| 1249 | |
| 1250 /** | |
| 1251 * @param {string} value | |
| 1252 * @return {!Object} | |
| 1253 */ | |
| 1254 axs.utils.isValidNumber = function(value) { | |
| 1255 try { | |
| 1256 var parsedValue = JSON.parse(value); | |
| 1257 } catch (ex) { | |
| 1258 return { 'valid': false, | |
| 1259 'value': value, | |
| 1260 'reason': '"' + value + '" is not a number' }; | |
| 1261 } | |
| 1262 if (typeof(parsedValue) != 'number') { | |
| 1263 return { 'valid': false, | |
| 1264 'value': value, | |
| 1265 'reason': '"' + value + '" is not a number' }; | |
| 1266 } | |
| 1267 return { 'valid': true, 'value': parsedValue }; | |
| 1268 }; | |
| 1269 | |
| 1270 /** | |
| 1271 * @param {Element} element | |
| 1272 * @return {boolean} | |
| 1273 */ | |
| 1274 axs.utils.isElementImplicitlyFocusable = function(element) { | |
| 1275 var defaultView = element.ownerDocument.defaultView; | |
| 1276 | |
| 1277 if (element instanceof defaultView.HTMLAnchorElement || | |
| 1278 element instanceof defaultView.HTMLAreaElement) | |
| 1279 return element.hasAttribute('href'); | |
| 1280 if (element instanceof defaultView.HTMLInputElement || | |
| 1281 element instanceof defaultView.HTMLSelectElement || | |
| 1282 element instanceof defaultView.HTMLTextAreaElement || | |
| 1283 element instanceof defaultView.HTMLButtonElement || | |
| 1284 element instanceof defaultView.HTMLIFrameElement) | |
| 1285 return !element.disabled; | |
| 1286 return false; | |
| 1287 }; | |
| 1288 | |
| 1289 /** | |
| 1290 * Returns an array containing the values of the given JSON-compatible object. | |
| 1291 * (Simply ignores any function values.) | |
| 1292 * @param {Object} obj | |
| 1293 * @return {Array} | |
| 1294 */ | |
| 1295 axs.utils.values = function(obj) { | |
| 1296 var values = []; | |
| 1297 for (var key in obj) { | |
| 1298 if (obj.hasOwnProperty(key) && typeof obj[key] != 'function') | |
| 1299 values.push(obj[key]); | |
| 1300 } | |
| 1301 return values; | |
| 1302 }; | |
| 1303 | |
| 1304 /** | |
| 1305 * Returns an object containing the same keys and values as the given | |
| 1306 * JSON-compatible object. (Simply ignores any function values.) | |
| 1307 * @param {Object} obj | |
| 1308 * @return {Object} | |
| 1309 */ | |
| 1310 axs.utils.namedValues = function(obj) { | |
| 1311 var values = {}; | |
| 1312 for (var key in obj) { | |
| 1313 if (obj.hasOwnProperty(key) && typeof obj[key] != 'function') | |
| 1314 values[key] = obj[key]; | |
| 1315 } | |
| 1316 return values | |
| 1317 }; | |
| 1318 | |
| 1319 /** Gets a CSS selector text for a DOM object. | |
| 1320 * @param {Node} obj The DOM object. | |
| 1321 * @return {string} CSS selector text for the DOM object. | |
| 1322 */ | |
| 1323 axs.utils.getQuerySelectorText = function(obj) { | |
| 1324 if (obj == null || obj.tagName == 'HTML') { | |
| 1325 return 'html'; | |
| 1326 } else if (obj.tagName == 'BODY') { | |
| 1327 return 'body'; | |
| 1328 } | |
| 1329 | |
| 1330 if (obj.hasAttribute) { | |
| 1331 if (obj.id) { | |
| 1332 return '#' + obj.id; | |
| 1333 } | |
| 1334 | |
| 1335 if (obj.className) { | |
| 1336 var selector = ''; | |
| 1337 for (var i = 0; i < obj.classList.length; i++) | |
| 1338 selector += '.' + obj.classList[i]; | |
| 1339 | |
| 1340 var total = 0; | |
| 1341 if (obj.parentNode) { | |
| 1342 for (i = 0; i < obj.parentNode.children.length; i++) { | |
| 1343 var similar = obj.parentNode.children[i]; | |
| 1344 if (axs.browserUtils.matchSelector(similar, selector)) | |
| 1345 total++; | |
| 1346 if (similar === obj) | |
| 1347 break; | |
| 1348 } | |
| 1349 } else { | |
| 1350 total = 1; | |
| 1351 } | |
| 1352 | |
| 1353 if (total == 1) { | |
| 1354 return axs.utils.getQuerySelectorText(obj.parentNode) + | |
| 1355 ' > ' + selector; | |
| 1356 } | |
| 1357 } | |
| 1358 | |
| 1359 if (obj.parentNode) { | |
| 1360 var similarTags = obj.parentNode.children; | |
| 1361 var total = 1; | |
| 1362 var i = 0; | |
| 1363 while (similarTags[i] !== obj) { | |
| 1364 if (similarTags[i].tagName == obj.tagName) { | |
| 1365 total++; | |
| 1366 } | |
| 1367 i++; | |
| 1368 } | |
| 1369 | |
| 1370 var next = ''; | |
| 1371 if (obj.parentNode.tagName != 'BODY') { | |
| 1372 next = axs.utils.getQuerySelectorText(obj.parentNode) + | |
| 1373 ' > '; | |
| 1374 } | |
| 1375 | |
| 1376 if (total == 1) { | |
| 1377 return next + | |
| 1378 obj.tagName; | |
| 1379 } else { | |
| 1380 return next + | |
| 1381 obj.tagName + | |
| 1382 ':nth-of-type(' + total + ')'; | |
| 1383 } | |
| 1384 } | |
| 1385 | |
| 1386 } else if (obj.selectorText) { | |
| 1387 return obj.selectorText; | |
| 1388 } | |
| 1389 | |
| 1390 return ''; | |
| 1391 }; | |
| OLD | NEW |