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 |