1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * @fileoverview A collection of JavaScript utilities used to simplify working
11 goog
.provide('cvox.DomUtil');
13 goog
.require('cvox.AbstractTts');
14 goog
.require('cvox.AriaUtil');
15 goog
.require('cvox.ChromeVox');
16 goog
.require('cvox.DomPredicates');
17 goog
.require('cvox.Memoize');
18 goog
.require('cvox.NodeState');
19 goog
.require('cvox.XpathUtil');
24 * Create the namespace
27 cvox
.DomUtil = function() {
32 * Note: If you are adding a new mapping, the new message identifier needs a
33 * corresponding braille message. For example, a message id 'tag_button'
34 * requires another message 'tag_button_brl' within messages.js.
37 cvox
.DomUtil
.INPUT_TYPE_TO_INFORMATION_TABLE_MSG
= {
38 'button' : 'input_type_button',
39 'checkbox' : 'input_type_checkbox',
40 'color' : 'input_type_color',
41 'datetime' : 'input_type_datetime',
42 'datetime-local' : 'input_type_datetime_local',
43 'date' : 'input_type_date',
44 'email' : 'input_type_email',
45 'file' : 'input_type_file',
46 'image' : 'input_type_image',
47 'month' : 'input_type_month',
48 'number' : 'input_type_number',
49 'password' : 'input_type_password',
50 'radio' : 'input_type_radio',
51 'range' : 'input_type_range',
52 'reset' : 'input_type_reset',
53 'search' : 'input_type_search',
54 'submit' : 'input_type_submit',
55 'tel' : 'input_type_tel',
56 'text' : 'input_type_text',
57 'url' : 'input_type_url',
58 'week' : 'input_type_week'
63 * Note: If you are adding a new mapping, the new message identifier needs a
64 * corresponding braille message. For example, a message id 'tag_button'
65 * requires another message 'tag_button_brl' within messages.js.
68 cvox
.DomUtil
.TAG_TO_INFORMATION_TABLE_VERBOSE_MSG
= {
70 'ARTICLE' : 'tag_article',
71 'ASIDE' : 'tag_aside',
72 'AUDIO' : 'tag_audio',
73 'BUTTON' : 'tag_button',
74 'FOOTER' : 'tag_footer',
81 'HEADER' : 'tag_header',
82 'HGROUP' : 'tag_hgroup',
87 'SECTION' : 'tag_section',
88 'SELECT' : 'tag_select',
89 'TABLE' : 'tag_table',
90 'TEXTAREA' : 'tag_textarea',
97 * ChromeVox does not speak the omitted tags.
100 cvox
.DomUtil
.TAG_TO_INFORMATION_TABLE_BRIEF_MSG
= {
101 'AUDIO' : 'tag_audio',
102 'BUTTON' : 'tag_button',
103 'SELECT' : 'tag_select',
104 'TABLE' : 'tag_table',
105 'TEXTAREA' : 'tag_textarea',
106 'VIDEO' : 'tag_video'
110 * These tags are treated as text formatters.
111 * @type {Array<string>}
113 cvox
.DomUtil
.FORMATTING_TAGS
=
114 ['B', 'BIG', 'CITE', 'CODE', 'DFN', 'EM', 'I', 'KBD', 'SAMP', 'SMALL',
115 'SPAN', 'STRIKE', 'STRONG', 'SUB', 'SUP', 'U', 'VAR'];
118 * Determine if the given node is visible on the page. This does not check if
119 * it is inside the document view-port as some sites try to communicate with
120 * screen readers with such elements.
121 * @param {Node} node The node to determine as visible or not.
122 * @param {{checkAncestors: (boolean|undefined),
123 checkDescendants: (boolean|undefined)}=} opt_options
124 * In certain cases, we already have information
125 * on the context of the node. To improve performance and avoid redundant
126 * operations, you may wish to turn certain visibility checks off by
127 * passing in an options object. The following properties are configurable:
128 * checkAncestors: {boolean=} True if we should check the ancestor chain
129 * for forced invisibility traits of descendants. True by default.
130 * checkDescendants: {boolean=} True if we should consider descendants of
131 * the given node for visible elements. True by default.
132 * @return {boolean} True if the node is visible.
134 cvox
.DomUtil
.isVisible = function(node
, opt_options
) {
135 var checkAncestors
= true;
136 var checkDescendants
= true;
138 if (opt_options
.checkAncestors
!== undefined) {
139 checkAncestors
= opt_options
.checkAncestors
;
141 if (opt_options
.checkDescendants
!== undefined) {
142 checkDescendants
= opt_options
.checkDescendants
;
146 // Generate a unique function name based on the arguments, and
147 // memoize the result of the internal visibility computation so that
148 // within the same call stack, we don't need to recompute the visibility
150 var fname
= 'isVisible-' + checkAncestors
+ '-' + checkDescendants
;
151 return /** @type {boolean} */ (cvox
.Memoize
.memoize(
152 cvox
.DomUtil
.computeIsVisible_
.bind(
153 this, node
, checkAncestors
, checkDescendants
), fname
, node
));
157 * Implementation of |cvox.DomUtil.isVisible|.
158 * @param {Node} node The node to determine as visible or not.
159 * @param {boolean} checkAncestors True if we should check the ancestor chain
160 * for forced invisibility traits of descendants.
161 * @param {boolean} checkDescendants True if we should consider descendants of
162 * the given node for visible elements.
163 * @return {boolean} True if the node is visible.
166 cvox
.DomUtil
.computeIsVisible_ = function(
167 node
, checkAncestors
, checkDescendants
) {
168 // If the node is an iframe that we can never inject into, consider it hidden.
169 if (node
.tagName
== 'IFRAME' && !node
.src
) {
173 // If the node is being forced visible by ARIA, ARIA wins.
174 if (cvox
.AriaUtil
.isForcedVisibleRecursive(node
)) {
178 // Confirm that no subtree containing node is invisible.
179 if (checkAncestors
&&
180 cvox
.DomUtil
.hasInvisibleAncestor_(node
)) {
184 // If the node's subtree has a visible node, we declare it as visible.
185 if (cvox
.DomUtil
.hasVisibleNodeSubtree_(node
, checkDescendants
)) {
194 * Checks the ancestor chain for the given node for invisibility. If an
195 * ancestor is invisible and this cannot be overriden by a descendant,
196 * we return true. If the element is not a descendant of the document
197 * element it will return true (invisible).
198 * @param {Node} node The node to check the ancestor chain for.
199 * @return {boolean} True if a descendant is invisible.
202 cvox
.DomUtil
.hasInvisibleAncestor_ = function(node
) {
204 while (ancestor
= ancestor
.parentElement
) {
205 var style
= document
.defaultView
.getComputedStyle(ancestor
, null);
206 if (cvox
.DomUtil
.isInvisibleStyle(style
, true)) {
209 // Once we reach the document element and we haven't found anything
210 // invisible yet, we're done. If we exit the while loop and never found
211 // the document element, the element wasn't part of the DOM and thus it's
213 if (ancestor
== document
.documentElement
) {
222 * Checks for a visible node in the subtree defined by root.
223 * @param {Node} root The root of the subtree to check.
224 * @param {boolean} recursive Whether or not to check beyond the root of the
225 * subtree for visible nodes. This option exists for performance tuning.
226 * Sometimes we already have information about the descendants, and we do
227 * not need to check them again.
228 * @return {boolean} True if the subtree contains a visible node.
231 cvox
.DomUtil
.hasVisibleNodeSubtree_ = function(root
, recursive
) {
232 if (!(root
instanceof Element
)) {
233 if (!root
.parentElement
) {
236 var parentStyle
= document
.defaultView
237 .getComputedStyle(root
.parentElement
, null);
238 var isVisibleParent
= !cvox
.DomUtil
.isInvisibleStyle(parentStyle
);
239 return isVisibleParent
;
242 var rootStyle
= document
.defaultView
.getComputedStyle(root
, null);
243 var isRootVisible
= !cvox
.DomUtil
.isInvisibleStyle(rootStyle
);
247 var isSubtreeInvisible
= cvox
.DomUtil
.isInvisibleStyle(rootStyle
, true);
248 if (!recursive
|| isSubtreeInvisible
) {
252 // Carry on with a recursive check of the descendants.
253 var children
= root
.childNodes
;
254 for (var i
= 0; i
< children
.length
; i
++) {
255 var child
= children
[i
];
256 if (cvox
.DomUtil
.hasVisibleNodeSubtree_(child
, recursive
)) {
265 * Determines whether or a node is not visible according to any CSS criteria
267 * @param {CSSStyleDeclaration} style The style of the node to determine as
269 * @param {boolean=} opt_strict If set to true, we do not check the visibility
270 * style attribute. False by default.
271 * CAUTION: Checking the visibility style attribute can result in returning
272 * true (invisible) even when an element has have visible descendants. This
273 * is because an element with visibility:hidden can have descendants that
275 * @return {boolean} True if the node is invisible.
277 cvox
.DomUtil
.isInvisibleStyle = function(style
, opt_strict
) {
281 if (style
.display
== 'none') {
284 // Opacity values range from 0.0 (transparent) to 1.0 (fully opaque).
285 if (parseFloat(style
.opacity
) == 0) {
288 // Visibility style tests for non-strict checking.
290 (style
.visibility
== 'hidden' || style
.visibility
== 'collapse')) {
298 * Determines whether a control should be announced as disabled.
300 * @param {Node} node The node to be examined.
301 * @return {boolean} Whether or not the node is disabled.
303 cvox
.DomUtil
.isDisabled = function(node
) {
308 while (ancestor
= ancestor
.parentElement
) {
309 if (ancestor
.tagName
== 'FIELDSET' && ancestor
.disabled
) {
318 * Determines whether a node is an HTML5 semantic element
320 * @param {Node} node The node to be checked.
321 * @return {boolean} True if the node is an HTML5 semantic element.
323 cvox
.DomUtil
.isSemanticElt = function(node
) {
325 var tag
= node
.tagName
;
326 if ((tag
== 'SECTION') || (tag
== 'NAV') || (tag
== 'ARTICLE') ||
327 (tag
== 'ASIDE') || (tag
== 'HGROUP') || (tag
== 'HEADER') ||
328 (tag
== 'FOOTER') || (tag
== 'TIME') || (tag
== 'MARK')) {
337 * Determines whether or not a node is a leaf node.
338 * TODO (adu): This function is doing a lot more than just checking for the
339 * presence of descendants. We should be more precise in the documentation
340 * about what we mean by leaf node.
342 * @param {Node} node The node to be checked.
343 * @param {boolean=} opt_allowHidden Allows hidden nodes during descent.
344 * @return {boolean} True if the node is a leaf node.
346 cvox
.DomUtil
.isLeafNode = function(node
, opt_allowHidden
) {
347 // If it's not an Element, then it's a leaf if it has no first child.
348 if (!(node
instanceof Element
)) {
349 return (node
.firstChild
== null);
352 // Now we know for sure it's an element.
353 var element
= /** @type {Element} */(node
);
354 if (!opt_allowHidden
&&
355 !cvox
.DomUtil
.isVisible(element
, {checkAncestors
: false})) {
358 if (!opt_allowHidden
&& cvox
.AriaUtil
.isHidden(element
)) {
361 if (cvox
.AriaUtil
.isLeafElement(element
)) {
364 switch (element
.tagName
) {
374 if (!!cvox
.DomPredicates
.linkPredicate([element
])) {
375 return !cvox
.DomUtil
.findNode(element
, function(node
) {
376 return !!cvox
.DomPredicates
.headingPredicate([node
]);
379 if (cvox
.DomUtil
.isLeafLevelControl(element
)) {
382 if (!element
.firstChild
) {
385 if (cvox
.DomUtil
.isMath(element
)) {
388 if (cvox
.DomPredicates
.headingPredicate([element
])) {
389 return !cvox
.DomUtil
.findNode(element
, function(n
) {
390 return !!cvox
.DomPredicates
.controlPredicate([n
]);
398 * Determines whether or not a node is or is the descendant of a node
399 * with a particular tag or class name.
401 * @param {Node} node The node to be checked.
402 * @param {?string} tagName The tag to check for, or null if the tag
404 * @param {?string=} className The class to check for, or null if the class
406 * @return {boolean} True if the node or one of its ancestor has the specified
409 cvox
.DomUtil
.isDescendantOf = function(node
, tagName
, className
) {
412 if (tagName
&& className
&&
413 (node
.tagName
&& (node
.tagName
== tagName
)) &&
414 (node
.className
&& (node
.className
== className
))) {
416 } else if (tagName
&& !className
&&
417 (node
.tagName
&& (node
.tagName
== tagName
))) {
419 } else if (!tagName
&& className
&&
420 (node
.className
&& (node
.className
== className
))) {
423 node
= node
.parentNode
;
430 * Determines whether or not a node is or is the descendant of another node.
432 * @param {Object} node The node to be checked.
433 * @param {Object} ancestor The node to see if it's a descendant of.
434 * @return {boolean} True if the node is ancestor or is a descendant of it.
436 cvox
.DomUtil
.isDescendantOfNode = function(node
, ancestor
) {
437 while (node
&& ancestor
) {
438 if (node
.isSameNode(ancestor
)) {
441 node
= node
.parentNode
;
448 * Remove all whitespace from the beginning and end, and collapse all
449 * inner strings of whitespace to a single space.
450 * @param {string} str The input string.
451 * @return {string} The string with whitespace collapsed.
453 cvox
.DomUtil
.collapseWhitespace = function(str
) {
454 return str
.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '');
458 * Gets the base label of a node. I don't know exactly what this is.
460 * @param {Node} node The node to get the label from.
461 * @param {boolean=} recursive Whether or not the element's subtree
462 * should be used; true by default.
463 * @param {boolean=} includeControls Whether or not controls in the subtree
464 * should be included; true by default.
465 * @return {string} The base label of the node.
468 cvox
.DomUtil
.getBaseLabel_ = function(node
, recursive
, includeControls
) {
470 if (node
.hasAttribute
) {
471 if (node
.hasAttribute('aria-labelledby')) {
472 var labelNodeIds
= node
.getAttribute('aria-labelledby').split(' ');
473 for (var labelNodeId
, i
= 0; labelNodeId
= labelNodeIds
[i
]; i
++) {
474 var labelNode
= document
.getElementById(labelNodeId
);
476 label
+= ' ' + cvox
.DomUtil
.getName(
477 labelNode
, true, includeControls
, true);
480 } else if (node
.hasAttribute('aria-label')) {
481 label
= node
.getAttribute('aria-label');
482 } else if (node
.constructor == HTMLImageElement
) {
483 label
= cvox
.DomUtil
.getImageTitle(node
);
484 } else if (node
.tagName
== 'FIELDSET') {
485 // Other labels will trump fieldset legend with this implementation.
486 // Depending on how this works out on the web, we may later switch this
487 // to appending the fieldset legend to any existing label.
488 var legends
= node
.getElementsByTagName('LEGEND');
490 for (var legend
, i
= 0; legend
= legends
[i
]; i
++) {
491 label
+= ' ' + cvox
.DomUtil
.getName(legend
, true, includeControls
);
495 if (label
.length
== 0 && node
&& node
.id
) {
496 var labelFor
= document
.querySelector('label[for="' + node
.id
+ '"]');
498 label
= cvox
.DomUtil
.getName(labelFor
, recursive
, includeControls
);
502 return cvox
.DomUtil
.collapseWhitespace(label
);
506 * Gets the nearest label in the ancestor chain, if one exists.
507 * @param {Node} node The node to start from.
508 * @return {string} The label.
511 cvox
.DomUtil
.getNearestAncestorLabel_ = function(node
) {
513 var enclosingLabel
= node
;
514 while (enclosingLabel
&& enclosingLabel
.tagName
!= 'LABEL') {
515 enclosingLabel
= enclosingLabel
.parentElement
;
517 if (enclosingLabel
&& !enclosingLabel
.hasAttribute('for')) {
518 // Get all text from the label but don't include any controls.
519 label
= cvox
.DomUtil
.getName(enclosingLabel
, true, false);
526 * Gets the name for an input element.
527 * @param {Node} node The node.
528 * @return {string} The name.
531 cvox
.DomUtil
.getInputName_ = function(node
) {
533 if (node
.type
== 'image') {
534 label
= cvox
.DomUtil
.getImageTitle(node
);
535 } else if (node
.type
== 'submit') {
536 if (node
.hasAttribute('value')) {
537 label
= node
.getAttribute('value');
541 } else if (node
.type
== 'reset') {
542 if (node
.hasAttribute('value')) {
543 label
= node
.getAttribute('value');
547 } else if (node
.type
== 'button') {
548 if (node
.hasAttribute('value')) {
549 label
= node
.getAttribute('value');
556 * Wraps getName_ with marking and unmarking nodes so that infinite loops
557 * don't occur. This is the ugly way to solve this; getName should not ever
558 * do a recursive call somewhere above it in the tree.
559 * @param {Node} node See getName_.
560 * @param {boolean=} recursive See getName_.
561 * @param {boolean=} includeControls See getName_.
562 * @param {boolean=} opt_allowHidden Allows hidden nodes in name computation.
563 * @return {string} See getName_.
565 cvox
.DomUtil
.getName = function(
566 node
, recursive
, includeControls
, opt_allowHidden
) {
567 if (!node
|| node
.cvoxGetNameMarked
== true) {
570 node
.cvoxGetNameMarked
= true;
572 cvox
.DomUtil
.getName_(node
, recursive
, includeControls
, opt_allowHidden
);
573 node
.cvoxGetNameMarked
= false;
574 var prefix
= cvox
.DomUtil
.getPrefixText(node
);
578 // TODO(dtseng): Seems like this list should be longer...
580 * Determines if a node has a name obtained from concatinating the names of its
582 * @param {!Node} node The node under consideration.
583 * @param {boolean=} opt_allowHidden Allows hidden nodes in name computation.
584 * @return {boolean} True if node has name based on children.
587 cvox
.DomUtil
.hasChildrenBasedName_ = function(node
, opt_allowHidden
) {
588 if (!!cvox
.DomPredicates
.linkPredicate([node
]) ||
589 !!cvox
.DomPredicates
.headingPredicate([node
]) ||
590 node
.tagName
== 'BUTTON' ||
591 cvox
.AriaUtil
.isControlWidget(node
) ||
592 !cvox
.DomUtil
.isLeafNode(node
, opt_allowHidden
)) {
600 * Get the name of a node: this includes all static text content and any
601 * HTML-author-specified label, title, alt text, aria-label, etc. - but
603 * - the user-generated control value (use getValue)
604 * - the current state (use getState)
605 * - the role (use getRole)
607 * Order of precedence:
608 * Text content if it's a text node.
613 * label (for a control)
614 * placeholder (for an input element)
615 * recursive calls to getName on all children
617 * @param {Node} node The node to get the name from.
618 * @param {boolean=} recursive Whether or not the element's subtree should
619 * be used; true by default.
620 * @param {boolean=} includeControls Whether or not controls in the subtree
621 * should be included; true by default.
622 * @param {boolean=} opt_allowHidden Allows hidden nodes in name computation.
623 * @return {string} The name of the node.
626 cvox
.DomUtil
.getName_ = function(
627 node
, recursive
, includeControls
, opt_allowHidden
) {
628 if (typeof(recursive
) === 'undefined') {
631 if (typeof(includeControls
) === 'undefined') {
632 includeControls
= true;
635 if (node
.constructor == Text
) {
639 var label
= cvox
.DomUtil
.getBaseLabel_(node
, recursive
, includeControls
);
641 if (label
.length
== 0 && cvox
.DomUtil
.isControl(node
)) {
642 label
= cvox
.DomUtil
.getNearestAncestorLabel_(node
);
645 if (label
.length
== 0 && node
.constructor == HTMLInputElement
) {
646 label
= cvox
.DomUtil
.getInputName_(node
);
649 if (cvox
.DomUtil
.isInputTypeText(node
) && node
.hasAttribute('placeholder')) {
650 var placeholder
= node
.getAttribute('placeholder');
651 if (label
.length
> 0) {
652 if (cvox
.DomUtil
.getValue(node
).length
> 0) {
655 return label
+ ' with hint ' + placeholder
;
662 if (label
.length
> 0) {
666 // Fall back to naming via title only if there is no text content.
667 if (cvox
.DomUtil
.collapseWhitespace(node
.textContent
).length
== 0 &&
669 node
.hasAttribute('title')) {
670 return node
.getAttribute('title');
677 if (cvox
.AriaUtil
.isCompositeControl(node
)) {
680 if (cvox
.DomUtil
.hasChildrenBasedName_(node
, opt_allowHidden
)) {
681 return cvox
.DomUtil
.getNameFromChildren(
682 node
, includeControls
, opt_allowHidden
);
689 * Get the name from the children of a node, not including the node itself.
691 * @param {Node} node The node to get the name from.
692 * @param {boolean=} includeControls Whether or not controls in the subtree
693 * should be included; true by default.
694 * @param {boolean=} opt_allowHidden Allow hidden nodes in name computation.
695 * @return {string} The concatenated text of all child nodes.
697 cvox
.DomUtil
.getNameFromChildren = function(
698 node
, includeControls
, opt_allowHidden
) {
699 if (includeControls
== undefined) {
700 includeControls
= true;
704 for (var i
= 0; i
< node
.childNodes
.length
; i
++) {
705 var child
= node
.childNodes
[i
];
706 var prevChild
= node
.childNodes
[i
- 1] || child
;
707 if (!includeControls
&& cvox
.DomUtil
.isControl(child
)) {
710 var isVisible
= cvox
.DomUtil
.isVisible(child
, {checkAncestors
: false});
711 if (opt_allowHidden
|| (isVisible
&& !cvox
.AriaUtil
.isHidden(child
))) {
712 delimiter
= (prevChild
.tagName
== 'SPAN' ||
713 child
.tagName
== 'SPAN' ||
714 child
.parentNode
.tagName
== 'SPAN') ?
716 name
+= delimiter
+ cvox
.DomUtil
.getName(child
, true, includeControls
);
724 * Get any prefix text for the given node.
725 * This includes list style text for the leftmost leaf node under a listitem.
726 * @param {Node} node Compute prefix for this node.
727 * @param {number=} opt_index Starting offset into the given node's text.
728 * @return {string} Prefix text, if any.
730 cvox
.DomUtil
.getPrefixText = function(node
, opt_index
) {
731 opt_index
= opt_index
|| 0;
733 // Generate list style text.
734 var ancestors
= cvox
.DomUtil
.getAncestors(node
);
736 var firstListitem
= cvox
.DomPredicates
.listItemPredicate(ancestors
);
738 var leftmost
= firstListitem
;
739 while (leftmost
&& leftmost
.firstChild
) {
740 leftmost
= leftmost
.firstChild
;
743 // Do nothing if we're not at the leftmost leaf.
745 firstListitem
.parentNode
&&
747 firstListitem
.parentNode
.tagName
== 'OL' &&
749 document
.defaultView
.getComputedStyle(firstListitem
.parentNode
)
750 .listStyleType
!= 'none') {
751 var items
= cvox
.DomUtil
.toArray(firstListitem
.parentNode
.children
).filter(
752 function(li
) { return li
.tagName
== 'LI'; });
753 var position
= items
.indexOf(firstListitem
) + 1;
754 // TODO(dtseng): Support all list style types.
755 if (document
.defaultView
.getComputedStyle(
756 firstListitem
.parentNode
).listStyleType
.indexOf('latin') != -1) {
758 prefix
= String
.fromCharCode('A'.charCodeAt(0) + position
% 26);
769 * Use heuristics to guess at the label of a control, to be used if one
770 * is not explicitly set in the DOM. This is useful when a control
771 * field gets focus, but probably not useful when browsing the page
773 * @param {Node} node The node to get the label from.
774 * @return {string} The name of the control, using heuristics.
776 cvox
.DomUtil
.getControlLabelHeuristics = function(node
) {
777 // If the node explicitly has aria-label or title set to '',
778 // treat it the same way as alt='' and do not guess - just assume
779 // the web developer knew what they were doing and wanted
780 // no title/label for that control.
781 if (node
.hasAttribute
&&
782 ((node
.hasAttribute('aria-label') &&
783 (node
.getAttribute('aria-label') == '')) ||
784 (node
.hasAttribute('aria-title') &&
785 (node
.getAttribute('aria-title') == '')))) {
789 // TODO (clchen, rshearer): Implement heuristics for getting the label
790 // information from the table headers once the code for getting table
791 // headers quickly is implemented.
793 // If no description has been found yet and heuristics are enabled,
794 // then try getting the content from the closest node.
795 var prevNode
= cvox
.DomUtil
.previousLeafNode(node
);
796 var prevTraversalCount
= 0;
797 while (prevNode
&& (!cvox
.DomUtil
.hasContent(prevNode
) ||
798 cvox
.DomUtil
.isControl(prevNode
))) {
799 prevNode
= cvox
.DomUtil
.previousLeafNode(prevNode
);
800 prevTraversalCount
++;
802 var nextNode
= cvox
.DomUtil
.directedNextLeafNode(node
);
803 var nextTraversalCount
= 0;
804 while (nextNode
&& (!cvox
.DomUtil
.hasContent(nextNode
) ||
805 cvox
.DomUtil
.isControl(nextNode
))) {
806 nextNode
= cvox
.DomUtil
.directedNextLeafNode(nextNode
);
807 nextTraversalCount
++;
809 var guessedLabelNode
;
810 if (prevNode
&& nextNode
) {
811 var parentNode
= node
;
812 // Count the number of parent nodes until there is a shared parent; the
813 // label is most likely in the same branch of the DOM as the control.
814 // TODO (chaitanyag): Try to generalize this algorithm and move it to
815 // its own function in DOM Utils.
818 if (cvox
.DomUtil
.isDescendantOfNode(prevNode
, parentNode
)) {
821 parentNode
= parentNode
.parentNode
;
827 if (cvox
.DomUtil
.isDescendantOfNode(nextNode
, parentNode
)) {
830 parentNode
= parentNode
.parentNode
;
833 guessedLabelNode
= nextCount
< prevCount
? nextNode
: prevNode
;
835 guessedLabelNode
= prevNode
|| nextNode
;
837 if (guessedLabelNode
) {
838 return cvox
.DomUtil
.collapseWhitespace(
839 cvox
.DomUtil
.getValue(guessedLabelNode
) + ' ' +
840 cvox
.DomUtil
.getName(guessedLabelNode
));
848 * Get the text value of a node: the selected value of a select control or the
849 * current text of a text control. Does not return the state of a checkbox
854 * @param {Node} node The node to get the value from.
855 * @return {string} The value of the node.
857 cvox
.DomUtil
.getValue = function(node
) {
858 var activeDescendant
= cvox
.AriaUtil
.getActiveDescendant(node
);
859 if (activeDescendant
) {
860 return cvox
.DomUtil
.collapseWhitespace(
861 cvox
.DomUtil
.getValue(activeDescendant
) + ' ' +
862 cvox
.DomUtil
.getName(activeDescendant
));
865 if (node
.constructor == HTMLSelectElement
) {
866 node
= /** @type {HTMLSelectElement} */(node
);
868 var start
= node
.selectedOptions
? node
.selectedOptions
[0] : null;
869 var end
= node
.selectedOptions
?
870 node
.selectedOptions
[node
.selectedOptions
.length
- 1] : null;
871 // TODO(dtseng): Keeping this stateless means we describe the start and end
872 // of the selection only since we don't know which was added or
873 // removed. Once we keep the previous selection, we can read the diff.
874 if (start
&& end
&& start
!= end
) {
875 value
= cvox
.ChromeVox
.msgs
.getMsg(
876 'selected_options_value', [start
.text
, end
.text
]);
878 value
= start
.text
+ '';
883 if (node
.constructor == HTMLTextAreaElement
) {
887 if (node
.constructor == HTMLInputElement
) {
889 // Returning '' for inputs that are covered by getName.
899 return node
.value
.replace(/./g
, 'dot ');
905 if (node
.isContentEditable
) {
906 return cvox
.DomUtil
.getNameFromChildren(node
, true);
914 * Given an image node, return its title as a string. The preferred title
915 * is always the alt text, and if that's not available, then the title
916 * attribute. If neither of those are available, it attempts to construct
917 * a title from the filename, and if all else fails returns the word Image.
918 * @param {Node} node The image node.
919 * @return {string} The title of the image.
921 cvox
.DomUtil
.getImageTitle = function(node
) {
923 if (node
.hasAttribute('alt')) {
925 } else if (node
.hasAttribute('title')) {
929 if (url
.substring(0, 4) != 'data') {
930 var filename
= url
.substring(
931 url
.lastIndexOf('/') + 1, url
.lastIndexOf('.'));
933 // Hack to not speak the filename if it's ridiculously long.
934 if (filename
.length
>= 1 && filename
.length
<= 16) {
935 text
= filename
+ ' Image';
948 * Search the whole page for any aria-labelledby attributes and collect
949 * the complete set of ids they map to, so that we can skip elements that
950 * just label other elements and not double-speak them. We cache this
951 * result and then throw it away at the next event loop.
952 * @return {Object<boolean>} Set of all ids that are mapped by aria-labelledby.
954 cvox
.DomUtil
.getLabelledByTargets = function() {
955 if (cvox
.labelledByTargets
) {
956 return cvox
.labelledByTargets
;
959 // Start by getting all elements with
960 // aria-labelledby on the page since that's probably a short list,
961 // then see if any of those ids overlap with an id in this element's
963 var labelledByElements
= document
.querySelectorAll('[aria-labelledby]');
964 var labelledByTargets
= {};
965 for (var i
= 0; i
< labelledByElements
.length
; ++i
) {
966 var element
= labelledByElements
[i
];
967 var attrValue
= element
.getAttribute('aria-labelledby');
968 var ids
= attrValue
.split(/ +/);
969 for (var j
= 0; j
< ids
.length
; j
++) {
970 labelledByTargets
[ids
[j
]] = true;
973 cvox
.labelledByTargets
= labelledByTargets
;
975 window
.setTimeout(function() {
976 cvox
.labelledByTargets
= null;
979 return labelledByTargets
;
984 * Determines whether or not a node has content.
986 * @param {Node} node The node to be checked.
987 * @return {boolean} True if the node has content.
989 cvox
.DomUtil
.hasContent = function(node
) {
990 // Memoize the result of the internal content computation so that
991 // within the same call stack, we don't need to redo the computation
992 // on the same node twice.
993 return /** @type {boolean} */ (cvox
.Memoize
.memoize(
994 cvox
.DomUtil
.computeHasContent_
.bind(this), 'hasContent', node
));
998 * Internal implementation of |cvox.DomUtil.hasContent|.
1000 * @param {Node} node The node to be checked.
1001 * @return {boolean} True if the node has content.
1004 cvox
.DomUtil
.computeHasContent_ = function(node
) {
1005 // nodeType:8 == COMMENT_NODE
1006 if (node
.nodeType
== 8) {
1010 // Exclude anything in the head
1011 if (cvox
.DomUtil
.isDescendantOf(node
, 'HEAD')) {
1015 // Exclude script nodes
1016 if (cvox
.DomUtil
.isDescendantOf(node
, 'SCRIPT')) {
1020 // Exclude noscript nodes
1021 if (cvox
.DomUtil
.isDescendantOf(node
, 'NOSCRIPT')) {
1025 // Exclude noembed nodes since NOEMBED is deprecated. We treat
1026 // noembed as having not content rather than try to get its content since
1027 // Chrome will return raw HTML content rather than a valid DOM subtree.
1028 if (cvox
.DomUtil
.isDescendantOf(node
, 'NOEMBED')) {
1032 // Exclude style nodes that have been dumped into the body.
1033 if (cvox
.DomUtil
.isDescendantOf(node
, 'STYLE')) {
1037 // Check the style to exclude undisplayed/hidden nodes.
1038 if (!cvox
.DomUtil
.isVisible(node
)) {
1042 // Ignore anything that is hidden by ARIA.
1043 if (cvox
.AriaUtil
.isHidden(node
)) {
1047 // We need to speak controls, including those with no value entered. We
1048 // therefore treat visible controls as if they had content, and return true
1050 if (cvox
.DomUtil
.isControl(node
)) {
1054 // Videos are always considered to have content so that we can navigate to
1055 // and use the controls of the video widget.
1056 if (cvox
.DomUtil
.isDescendantOf(node
, 'VIDEO')) {
1059 // Audio elements are always considered to have content so that we can
1060 // navigate to and use the controls of the audio widget.
1061 if (cvox
.DomUtil
.isDescendantOf(node
, 'AUDIO')) {
1065 // We want to try to jump into an iframe iff it has a src attribute.
1066 // For right now, we will avoid iframes without any content in their src since
1067 // ChromeVox is not being injected in those cases and will cause the user to
1069 // TODO (clchen, dmazzoni): Manually inject ChromeVox for iframes without src.
1070 if ((node
.tagName
== 'IFRAME') && (node
.src
) &&
1071 (node
.src
.indexOf('javascript:') != 0)) {
1075 var controlQuery
= 'button,input,select,textarea';
1077 // Skip any non-control content inside of a label if the label is
1078 // correctly associated with a control, the label text will get spoken
1079 // when the control is reached.
1080 var enclosingLabel
= node
.parentElement
;
1081 while (enclosingLabel
&& enclosingLabel
.tagName
!= 'LABEL') {
1082 enclosingLabel
= enclosingLabel
.parentElement
;
1084 if (enclosingLabel
) {
1085 var embeddedControl
= enclosingLabel
.querySelector(controlQuery
);
1086 if (enclosingLabel
.hasAttribute('for')) {
1087 var targetId
= enclosingLabel
.getAttribute('for');
1088 var targetNode
= document
.getElementById(targetId
);
1090 cvox
.DomUtil
.isControl(targetNode
) &&
1094 } else if (embeddedControl
) {
1099 // Skip any non-control content inside of a legend if the legend is correctly
1100 // nested within a fieldset. The legend text will get spoken when the fieldset
1102 var enclosingLegend
= node
.parentElement
;
1103 while (enclosingLegend
&& enclosingLegend
.tagName
!= 'LEGEND') {
1104 enclosingLegend
= enclosingLegend
.parentElement
;
1106 if (enclosingLegend
) {
1107 var legendAncestor
= enclosingLegend
.parentElement
;
1108 while (legendAncestor
&& legendAncestor
.tagName
!= 'FIELDSET') {
1109 legendAncestor
= legendAncestor
.parentElement
;
1111 var embeddedControl
=
1112 legendAncestor
&& legendAncestor
.querySelector(controlQuery
);
1113 if (legendAncestor
&& !embeddedControl
) {
1118 if (!!cvox
.DomPredicates
.linkPredicate([node
])) {
1122 // At this point, any non-layout tables are considered to have content.
1123 // For layout tables, it is safe to consider them as without content since the
1124 // sync operation would select a descendant of a layout table if possible. The
1125 // only instance where |hasContent| gets called on a layout table is if no
1126 // descendants have content (see |AbstractNodeWalker.next|).
1127 if (node
.tagName
== 'TABLE' && !cvox
.DomUtil
.isLayoutTable(node
)) {
1131 // Math is always considered to have content.
1132 if (cvox
.DomUtil
.isMath(node
)) {
1136 if (cvox
.DomPredicates
.headingPredicate([node
])) {
1140 if (cvox
.DomUtil
.isFocusable(node
)) {
1144 // Skip anything referenced by another element on the page
1145 // via aria-labelledby.
1146 var labelledByTargets
= cvox
.DomUtil
.getLabelledByTargets();
1147 var enclosingNodeWithId
= node
;
1148 while (enclosingNodeWithId
) {
1149 if (enclosingNodeWithId
.id
&&
1150 labelledByTargets
[enclosingNodeWithId
.id
]) {
1151 // If we got here, some element on this page has an aria-labelledby
1152 // attribute listing this node as its id. As long as that "some" element
1153 // is not this element, we should return false, indicating this element
1154 // should be skipped.
1155 var attrValue
= enclosingNodeWithId
.getAttribute('aria-labelledby');
1157 var ids
= attrValue
.split(/ +/);
1158 if (ids
.indexOf(enclosingNodeWithId
.id
) == -1) {
1165 enclosingNodeWithId
= enclosingNodeWithId
.parentElement
;
1168 var text
= cvox
.DomUtil
.getValue(node
) + ' ' + cvox
.DomUtil
.getName(node
);
1169 var state
= cvox
.DomUtil
.getState(node
, true);
1170 if (text
.match(/^\s+$/) && state
=== '') {
1171 // Text only contains whitespace
1180 * Returns a list of all the ancestors of a given node. The last element
1181 * is the current node.
1183 * @param {Node} targetNode The node to get ancestors for.
1184 * @return {Array<Node>} An array of ancestors for the targetNode.
1186 cvox
.DomUtil
.getAncestors = function(targetNode
) {
1187 var ancestors
= new Array();
1188 while (targetNode
) {
1189 ancestors
.push(targetNode
);
1190 targetNode
= targetNode
.parentNode
;
1192 ancestors
.reverse();
1193 while (ancestors
.length
&& !ancestors
[0].tagName
&& !ancestors
[0].nodeValue
) {
1201 * Compares Ancestors of A with Ancestors of B and returns
1202 * the index value in B at which B diverges from A.
1203 * If there is no divergence, the result will be -1.
1204 * Note that if B is the same as A except B has more nodes
1205 * even after A has ended, that is considered a divergence.
1206 * The first node that B has which A does not have will
1207 * be treated as the divergence point.
1209 * @param {Object} ancestorsA The array of ancestors for Node A.
1210 * @param {Object} ancestorsB The array of ancestors for Node B.
1211 * @return {number} The index of the divergence point (the first node that B has
1212 * which A does not have in B's list of ancestors).
1214 cvox
.DomUtil
.compareAncestors = function(ancestorsA
, ancestorsB
) {
1216 while (ancestorsA
[i
] && ancestorsB
[i
] && (ancestorsA
[i
] == ancestorsB
[i
])) {
1219 if (!ancestorsA
[i
] && !ancestorsB
[i
]) {
1227 * Returns an array of ancestors that are unique for the currentNode when
1228 * compared to the previousNode. Having such an array is useful in generating
1229 * the node information (identifying when interesting node boundaries have been
1232 * @param {Node} previousNode The previous node.
1233 * @param {Node} currentNode The current node.
1234 * @param {boolean=} opt_fallback True returns node's ancestors in the case
1235 * where node's ancestors is a subset of previousNode's ancestors.
1236 * @return {Array<Node>} An array of unique ancestors for the current node
1239 cvox
.DomUtil
.getUniqueAncestors = function(
1240 previousNode
, currentNode
, opt_fallback
) {
1241 var prevAncestors
= cvox
.DomUtil
.getAncestors(previousNode
);
1242 var currentAncestors
= cvox
.DomUtil
.getAncestors(currentNode
);
1243 var divergence
= cvox
.DomUtil
.compareAncestors(prevAncestors
,
1245 var diff
= currentAncestors
.slice(divergence
);
1246 return (diff
.length
== 0 && opt_fallback
) ? currentAncestors
: diff
;
1251 * Returns a role message identifier for a node.
1252 * For a localized string, see cvox.DomUtil.getRole.
1253 * @param {Node} targetNode The node to get the role name for.
1254 * @param {number} verbosity The verbosity setting to use.
1255 * @return {string} The role message identifier for the targetNode.
1257 cvox
.DomUtil
.getRoleMsg = function(targetNode
, verbosity
) {
1259 info
= cvox
.AriaUtil
.getRoleNameMsg(targetNode
);
1261 if (targetNode
.tagName
== 'INPUT') {
1262 info
= cvox
.DomUtil
.INPUT_TYPE_TO_INFORMATION_TABLE_MSG
[targetNode
.type
];
1263 } else if (targetNode
.tagName
== 'A' &&
1264 cvox
.DomUtil
.isInternalLink(targetNode
)) {
1265 info
= 'internal_link';
1266 } else if (targetNode
.tagName
== 'A' &&
1267 targetNode
.getAttribute('href') &&
1268 cvox
.ChromeVox
.visitedUrls
[targetNode
.href
]) {
1269 info
= 'visited_link';
1270 } else if (targetNode
.tagName
== 'A' &&
1271 targetNode
.getAttribute('name')) {
1272 info
= ''; // Don't want to add any role to anchors.
1273 } else if (targetNode
.isContentEditable
) {
1274 info
= 'input_type_text';
1275 } else if (cvox
.DomUtil
.isMath(targetNode
)) {
1277 } else if (targetNode
.tagName
== 'TABLE' &&
1278 cvox
.DomUtil
.isLayoutTable(targetNode
)) {
1281 if (verbosity
== cvox
.VERBOSITY_BRIEF
) {
1283 cvox
.DomUtil
.TAG_TO_INFORMATION_TABLE_BRIEF_MSG
[targetNode
.tagName
];
1285 info
= cvox
.DomUtil
.TAG_TO_INFORMATION_TABLE_VERBOSE_MSG
[
1286 targetNode
.tagName
];
1288 if (cvox
.DomUtil
.hasLongDesc(targetNode
)) {
1289 info
= 'image_with_long_desc';
1292 if (!info
&& targetNode
.onclick
) {
1304 * Returns a string to be presented to the user that identifies what the
1305 * targetNode's role is.
1306 * ARIA roles are given priority; if there is no ARIA role set, the role
1307 * will be determined by the HTML tag for the node.
1309 * @param {Node} targetNode The node to get the role name for.
1310 * @param {number} verbosity The verbosity setting to use.
1311 * @return {string} The role name for the targetNode.
1313 cvox
.DomUtil
.getRole = function(targetNode
, verbosity
) {
1314 var roleMsg
= cvox
.DomUtil
.getRoleMsg(targetNode
, verbosity
) || '';
1315 var role
= roleMsg
&& roleMsg
!= ' ' ?
1316 cvox
.ChromeVox
.msgs
.getMsg(roleMsg
) : '';
1317 return role
? role
: roleMsg
;
1322 * Count the number of items in a list node.
1324 * @param {Node} targetNode The list node.
1325 * @return {number} The number of items in the list.
1327 cvox
.DomUtil
.getListLength = function(targetNode
) {
1329 for (var node
= targetNode
.firstChild
;
1331 node
= node
.nextSibling
) {
1332 if (cvox
.DomUtil
.isVisible(node
) &&
1333 (node
.tagName
== 'LI' ||
1334 (node
.getAttribute
&& node
.getAttribute('role') == 'listitem'))) {
1335 if (node
.hasAttribute('aria-setsize')) {
1336 var ariaLength
= parseInt(node
.getAttribute('aria-setsize'), 10);
1337 if (!isNaN(ariaLength
)) {
1349 * Returns a NodeState that gives information about the state of the targetNode.
1351 * @param {Node} targetNode The node to get the state information for.
1352 * @param {boolean} primary Whether this is the primary node we're
1353 * interested in, where we might want extra information - as
1354 * opposed to an ancestor, where we might be more brief.
1355 * @return {cvox.NodeState} The status information about the node.
1357 cvox
.DomUtil
.getStateMsgs = function(targetNode
, primary
) {
1358 var activeDescendant
= cvox
.AriaUtil
.getActiveDescendant(targetNode
);
1359 if (activeDescendant
) {
1360 return cvox
.DomUtil
.getStateMsgs(activeDescendant
, primary
);
1363 var role
= targetNode
.getAttribute
? targetNode
.getAttribute('role') : '';
1364 info
= cvox
.AriaUtil
.getStateMsgs(targetNode
, primary
);
1369 if (targetNode
.tagName
== 'INPUT') {
1370 if (!targetNode
.hasAttribute('aria-checked')) {
1372 'checkbox-true': 'checkbox_checked_state',
1373 'checkbox-false': 'checkbox_unchecked_state',
1374 'radio-true': 'radio_selected_state',
1375 'radio-false': 'radio_unselected_state' };
1376 var msgId
= INPUT_MSGS
[targetNode
.type
+ '-' + !!targetNode
.checked
];
1381 } else if (targetNode
.tagName
== 'SELECT') {
1382 if (targetNode
.selectedOptions
&& targetNode
.selectedOptions
.length
<= 1) {
1383 info
.push(['list_position',
1384 cvox
.ChromeVox
.msgs
.getNumber(targetNode
.selectedIndex
+ 1),
1385 cvox
.ChromeVox
.msgs
.getNumber(targetNode
.options
.length
)]);
1387 info
.push(['selected_options_state',
1388 cvox
.ChromeVox
.msgs
.getNumber(targetNode
.selectedOptions
.length
)]);
1390 } else if (targetNode
.tagName
== 'UL' ||
1391 targetNode
.tagName
== 'OL' ||
1393 info
.push(['list_with_items_not_pluralized',
1394 cvox
.ChromeVox
.msgs
.getNumber(
1395 cvox
.DomUtil
.getListLength(targetNode
))]);
1398 if (cvox
.DomUtil
.isDisabled(targetNode
)) {
1399 info
.push(['aria_disabled_true']);
1402 if (targetNode
.accessKey
) {
1403 info
.push(['access_key', targetNode
.accessKey
]);
1411 * Returns a string that gives information about the state of the targetNode.
1413 * @param {Node} targetNode The node to get the state information for.
1414 * @param {boolean} primary Whether this is the primary node we're
1415 * interested in, where we might want extra information - as
1416 * opposed to an ancestor, where we might be more brief.
1417 * @return {string} The status information about the node.
1419 cvox
.DomUtil
.getState = function(targetNode
, primary
) {
1420 return cvox
.NodeStateUtil
.expand(
1421 cvox
.DomUtil
.getStateMsgs(targetNode
, primary
));
1426 * Return whether a node is focusable. This includes nodes whose tabindex
1427 * attribute is set to "-1" explicitly - these nodes are not in the tab
1428 * order, but they should still be focused if the user navigates to them
1429 * using linear or smart DOM navigation.
1431 * Note that when the tabIndex property of an Element is -1, that doesn't
1432 * tell us whether the tabIndex attribute is missing or set to "-1" explicitly,
1433 * so we have to check the attribute.
1435 * @param {Object} targetNode The node to check if it's focusable.
1436 * @return {boolean} True if the node is focusable.
1438 cvox
.DomUtil
.isFocusable = function(targetNode
) {
1439 if (!targetNode
|| typeof(targetNode
.tabIndex
) != 'number') {
1443 // Workaround for http://code.google.com/p/chromium/issues/detail?id=153904
1444 if ((targetNode
.tagName
== 'A') && !targetNode
.hasAttribute('href') &&
1445 !targetNode
.hasAttribute('tabindex')) {
1449 if (targetNode
.tabIndex
>= 0) {
1453 if (targetNode
.hasAttribute
&&
1454 targetNode
.hasAttribute('tabindex') &&
1455 targetNode
.getAttribute('tabindex') == '-1') {
1464 * Find a focusable descendant of a given node. This includes nodes whose
1465 * tabindex attribute is set to "-1" explicitly - these nodes are not in the
1466 * tab order, but they should still be focused if the user navigates to them
1467 * using linear or smart DOM navigation.
1469 * @param {Node} targetNode The node whose descendants to check if focusable.
1470 * @return {Node} The focusable descendant node. Null if no descendant node
1473 cvox
.DomUtil
.findFocusableDescendant = function(targetNode
) {
1474 // Search down the descendants chain until a focusable node is found
1477 cvox
.DomUtil
.findNode(targetNode
, cvox
.DomUtil
.isFocusable
);
1478 if (focusableNode
) {
1479 return focusableNode
;
1487 * Returns the number of focusable nodes in root's subtree. The count does not
1490 * @param {Node} targetNode The node whose descendants to check are focusable.
1491 * @return {number} The number of focusable descendants.
1493 cvox
.DomUtil
.countFocusableDescendants = function(targetNode
) {
1495 cvox
.DomUtil
.countNodes(targetNode
, cvox
.DomUtil
.isFocusable
) : 0;
1500 * Checks if the targetNode is still attached to the document.
1501 * A node can become detached because of AJAX changes.
1503 * @param {Object} targetNode The node to check.
1504 * @return {boolean} True if the targetNode is still attached.
1506 cvox
.DomUtil
.isAttachedToDocument = function(targetNode
) {
1507 while (targetNode
) {
1508 if (targetNode
.tagName
&& (targetNode
.tagName
== 'HTML')) {
1511 targetNode
= targetNode
.parentNode
;
1518 * Dispatches a left click event on the element that is the targetNode.
1519 * Clicks go in the sequence of mousedown, mouseup, and click.
1520 * @param {Node} targetNode The target node of this operation.
1521 * @param {boolean} shiftKey Specifies if shift is held down.
1522 * @param {boolean} callOnClickDirectly Specifies whether or not to directly
1523 * invoke the onclick method if there is one.
1524 * @param {boolean=} opt_double True to issue a double click.
1525 * @param {boolean=} opt_handleOwnEvents Whether to handle the generated
1526 * events through the normal event processing.
1528 cvox
.DomUtil
.clickElem = function(
1529 targetNode
, shiftKey
, callOnClickDirectly
, opt_double
,
1530 opt_handleOwnEvents
) {
1531 // If there is an activeDescendant of the targetNode, then that is where the
1532 // click should actually be targeted.
1533 var activeDescendant
= cvox
.AriaUtil
.getActiveDescendant(targetNode
);
1534 if (activeDescendant
) {
1535 targetNode
= activeDescendant
;
1537 if (callOnClickDirectly
) {
1538 var onClickFunction
= null;
1539 if (targetNode
.onclick
) {
1540 onClickFunction
= targetNode
.onclick
;
1542 if (!onClickFunction
&& (targetNode
.nodeType
!= 1) &&
1543 targetNode
.parentNode
&& targetNode
.parentNode
.onclick
) {
1544 onClickFunction
= targetNode
.parentNode
.onclick
;
1546 var keepGoing
= true;
1547 if (onClickFunction
) {
1549 keepGoing
= onClickFunction();
1550 } catch (exception
) {
1551 // Something went very wrong with the onclick method; we'll ignore it
1552 // and just dispatch a click event normally.
1556 // The onclick method ran successfully and returned false, meaning the
1557 // event should not bubble up, so we will return here.
1562 // Send a mousedown (or simply a double click if requested).
1563 var evt
= document
.createEvent('MouseEvents');
1564 var evtType
= opt_double
? 'dblclick' : 'mousedown';
1565 evt
.initMouseEvent(evtType
, true, true, document
.defaultView
,
1566 1, 0, 0, 0, 0, false, false, shiftKey
, false, 0, null);
1567 // Unless asked not to, Mark any events we generate so we don't try to
1568 // process our own events.
1569 evt
.fromCvox
= !opt_handleOwnEvents
;
1571 targetNode
.dispatchEvent(evt
);
1574 evt
= document
.createEvent('MouseEvents');
1575 evt
.initMouseEvent('mouseup', true, true, document
.defaultView
,
1576 1, 0, 0, 0, 0, false, false, shiftKey
, false, 0, null);
1577 evt
.fromCvox
= !opt_handleOwnEvents
;
1579 targetNode
.dispatchEvent(evt
);
1582 evt
= document
.createEvent('MouseEvents');
1583 evt
.initMouseEvent('click', true, true, document
.defaultView
,
1584 1, 0, 0, 0, 0, false, false, shiftKey
, false, 0, null);
1585 evt
.fromCvox
= !opt_handleOwnEvents
;
1587 targetNode
.dispatchEvent(evt
);
1590 if (cvox
.DomUtil
.isInternalLink(targetNode
)) {
1591 cvox
.DomUtil
.syncInternalLink(targetNode
);
1597 * Syncs to an internal link.
1598 * @param {Node} node A link whose href's target we want to sync.
1600 cvox
.DomUtil
.syncInternalLink = function(node
) {
1602 var targetId
= node
.href
.split('#')[1];
1603 targetNode
= document
.getElementById(targetId
);
1605 var nodes
= document
.getElementsByName(targetId
);
1606 if (nodes
.length
> 0) {
1607 targetNode
= nodes
[0];
1611 // Insert a dummy node to adjust next Tab focus location.
1612 var parent
= targetNode
.parentNode
;
1613 var dummyNode
= document
.createElement('div');
1614 dummyNode
.setAttribute('tabindex', '-1');
1615 parent
.insertBefore(dummyNode
, targetNode
);
1616 dummyNode
.setAttribute('chromevoxignoreariahidden', 1);
1618 cvox
.ChromeVox
.syncToNode(targetNode
, false);
1624 * Given an HTMLInputElement, returns true if it's an editable text type.
1625 * This includes input type='text' and input type='password' and a few
1628 * @param {Node} node The node to check.
1629 * @return {boolean} True if the node is an INPUT with an editable text type.
1631 cvox
.DomUtil
.isInputTypeText = function(node
) {
1632 if (!node
|| node
.constructor != HTMLInputElement
) {
1636 switch (node
.type
) {
1653 * Given a node, returns true if it's a control. Controls are *not necessarily*
1654 * leaf-level given that some composite controls may have focusable children
1655 * if they are managing focus with tabindex:
1656 * ( http://www.w3.org/TR/2010/WD-wai-aria-practices-20100916/#visualfocus ).
1658 * @param {Node} node The node to check.
1659 * @return {boolean} True if the node is a control.
1661 cvox
.DomUtil
.isControl = function(node
) {
1662 if (cvox
.AriaUtil
.isControlWidget(node
) &&
1663 cvox
.DomUtil
.isFocusable(node
)) {
1667 switch (node
.tagName
) {
1673 return node
.type
!= 'hidden';
1676 if (node
.isContentEditable
) {
1684 * Given a node, returns true if it's a leaf-level control. This includes
1685 * composite controls thare are managing focus for children with
1686 * activedescendant, but not composite controls with focusable children:
1687 * ( http://www.w3.org/TR/2010/WD-wai-aria-practices-20100916/#visualfocus ).
1689 * @param {Node} node The node to check.
1690 * @return {boolean} True if the node is a leaf-level control.
1692 cvox
.DomUtil
.isLeafLevelControl = function(node
) {
1693 if (cvox
.DomUtil
.isControl(node
)) {
1694 return !(cvox
.AriaUtil
.isCompositeControl(node
) &&
1695 cvox
.DomUtil
.findFocusableDescendant(node
));
1702 * Given a node that might be inside of a composite control like a listbox,
1703 * return the surrounding control.
1704 * @param {Node} node The node from which to start looking.
1705 * @return {Node} The surrounding composite control node, or null if none.
1707 cvox
.DomUtil
.getSurroundingControl = function(node
) {
1708 var surroundingControl
= null;
1709 if (!cvox
.DomUtil
.isControl(node
) && node
.hasAttribute
&&
1710 node
.hasAttribute('role')) {
1711 surroundingControl
= node
.parentElement
;
1712 while (surroundingControl
&&
1713 !cvox
.AriaUtil
.isCompositeControl(surroundingControl
)) {
1714 surroundingControl
= surroundingControl
.parentElement
;
1717 return surroundingControl
;
1722 * Given a node and a function for determining when to stop
1723 * descent, return the next leaf-like node.
1725 * @param {!Node} node The node from which to start looking,
1726 * this node *must not* be above document.body.
1727 * @param {boolean} r True if reversed. False by default.
1728 * @param {function(!Node):boolean} isLeaf A function that
1729 * returns true if we should stop descending.
1730 * @return {Node} The next leaf-like node or null if there is no next
1731 * leaf-like node. This function will always return a node below
1732 * document.body and never document.body itself.
1734 cvox
.DomUtil
.directedNextLeafLikeNode = function(node
, r
, isLeaf
) {
1735 if (node
!= document
.body
) {
1736 // if not at the top of the tree, we want to find the next possible
1737 // branch forward in the dom, so we climb up the parents until we find a
1738 // node that has a nextSibling
1739 while (!cvox
.DomUtil
.directedNextSibling(node
, r
)) {
1743 // since node is never above document.body, it always has a parent.
1744 // so node.parentNode will never be null.
1745 node
= /** @type {!Node} */(node
.parentNode
);
1746 if (node
== document
.body
) {
1747 // we've readed the end of the document.
1751 if (cvox
.DomUtil
.directedNextSibling(node
, r
)) {
1752 // we just checked that next sibling is non-null.
1753 node
= /** @type {!Node} */(cvox
.DomUtil
.directedNextSibling(node
, r
));
1756 // once we're at our next sibling, we want to descend down into it as
1757 // far as the child class will allow
1758 while (cvox
.DomUtil
.directedFirstChild(node
, r
) && !isLeaf(node
)) {
1759 node
= /** @type {!Node} */(cvox
.DomUtil
.directedFirstChild(node
, r
));
1762 // after we've done all that, if we are still at document.body, this must
1763 // be an empty document.
1764 if (node
== document
.body
) {
1772 * Given a node, returns the next leaf node.
1774 * @param {!Node} node The node from which to start looking
1775 * for the next leaf node.
1776 * @param {boolean=} reverse True if reversed. False by default.
1777 * @return {Node} The next leaf node.
1778 * Null if there is no next leaf node.
1780 cvox
.DomUtil
.directedNextLeafNode = function(node
, reverse
) {
1781 reverse
= !!reverse
;
1782 return cvox
.DomUtil
.directedNextLeafLikeNode(
1783 node
, reverse
, cvox
.DomUtil
.isLeafNode
);
1788 * Given a node, returns the previous leaf node.
1790 * @param {!Node} node The node from which to start looking
1791 * for the previous leaf node.
1792 * @return {Node} The previous leaf node.
1793 * Null if there is no previous leaf node.
1795 cvox
.DomUtil
.previousLeafNode = function(node
) {
1796 return cvox
.DomUtil
.directedNextLeafNode(node
, true);
1801 * Computes the outer most leaf node of a given node, depending on value
1802 * of the reverse flag r.
1803 * @param {!Node} node in the DOM.
1804 * @param {boolean} r True if reversed. False by default.
1805 * @param {function(!Node):boolean} pred Predicate to decide
1806 * what we consider a leaf.
1807 * @return {Node} The outer most leaf node of that node.
1809 cvox
.DomUtil
.directedFindFirstNode = function(node
, r
, pred
) {
1810 var child
= cvox
.DomUtil
.directedFirstChild(node
, r
);
1815 var leaf
= cvox
.DomUtil
.directedFindFirstNode(child
, r
, pred
);
1820 child
= cvox
.DomUtil
.directedNextSibling(child
, r
);
1827 * Moves to the deepest node satisfying a given predicate under the given node.
1828 * @param {!Node} node in the DOM.
1829 * @param {boolean} r True if reversed. False by default.
1830 * @param {function(!Node):boolean} pred Predicate deciding what a leaf is.
1831 * @return {Node} The deepest node satisfying pred.
1833 cvox
.DomUtil
.directedFindDeepestNode = function(node
, r
, pred
) {
1834 var next
= cvox
.DomUtil
.directedFindFirstNode(node
, r
, pred
);
1842 return cvox
.DomUtil
.directedFindDeepestNode(next
, r
, pred
);
1848 * Computes the next node wrt. a predicate that is a descendant of ancestor.
1849 * @param {!Node} node in the DOM.
1850 * @param {!Node} ancestor of the given node.
1851 * @param {boolean} r True if reversed. False by default.
1852 * @param {function(!Node):boolean} pred Predicate to decide
1853 * what we consider a leaf.
1854 * @param {boolean=} above True if the next node can live in the subtree
1855 * directly above the start node. False by default.
1856 * @param {boolean=} deep True if we are looking for the next node that is
1857 * deepest in the tree. Otherwise the next shallow node is returned.
1859 * @return {Node} The next node in the DOM that satisfies the predicate.
1861 cvox
.DomUtil
.directedFindNextNode = function(
1862 node
, ancestor
, r
, pred
, above
, deep
) {
1865 if (!cvox
.DomUtil
.isDescendantOfNode(node
, ancestor
) || node
== ancestor
) {
1868 var next
= cvox
.DomUtil
.directedNextSibling(node
, r
);
1870 if (!deep
&& pred(next
)) {
1874 cvox
.DomUtil
.directedFindDeepestNode
:
1875 cvox
.DomUtil
.directedFindFirstNode
)(next
, r
, pred
);
1879 if (deep
&& pred(next
)) {
1882 next
= cvox
.DomUtil
.directedNextSibling(next
, r
);
1884 var parent
= /** @type {!Node} */(node
.parentNode
);
1885 if (above
&& pred(parent
)) {
1888 return cvox
.DomUtil
.directedFindNextNode(
1889 parent
, ancestor
, r
, pred
, above
, deep
);
1894 * Get a string representing a control's value and state, i.e. the part
1895 * that changes while interacting with the control
1896 * @param {Element} control A control.
1897 * @return {string} The value and state string.
1899 cvox
.DomUtil
.getControlValueAndStateString = function(control
) {
1900 var parentControl
= cvox
.DomUtil
.getSurroundingControl(control
);
1901 if (parentControl
) {
1902 return cvox
.DomUtil
.collapseWhitespace(
1903 cvox
.DomUtil
.getValue(control
) + ' ' +
1904 cvox
.DomUtil
.getName(control
) + ' ' +
1905 cvox
.DomUtil
.getState(control
, true));
1907 return cvox
.DomUtil
.collapseWhitespace(
1908 cvox
.DomUtil
.getValue(control
) + ' ' +
1909 cvox
.DomUtil
.getState(control
, true));
1915 * Determine whether the given node is an internal link.
1916 * @param {Node} node The node to be examined.
1917 * @return {boolean} True if the node is an internal link, false otherwise.
1919 cvox
.DomUtil
.isInternalLink = function(node
) {
1920 if (node
.nodeType
== 1) { // Element nodes only.
1921 var href
= node
.getAttribute('href');
1922 if (href
&& href
.indexOf('#') != -1) {
1923 var path
= href
.split('#')[0];
1924 return path
== '' || path
== window
.location
.pathname
;
1932 * Get a string containing the currently selected link's URL.
1933 * @param {Node} node The link from which URL needs to be extracted.
1934 * @return {string} The value of the URL.
1936 cvox
.DomUtil
.getLinkURL = function(node
) {
1937 if (node
.tagName
== 'A') {
1938 if (node
.getAttribute('href')) {
1939 if (cvox
.DomUtil
.isInternalLink(node
)) {
1940 return cvox
.ChromeVox
.msgs
.getMsg('internal_link');
1942 return node
.getAttribute('href');
1947 } else if (cvox
.AriaUtil
.getRoleName(node
) ==
1948 cvox
.ChromeVox
.msgs
.getMsg('aria_role_link')) {
1949 return cvox
.ChromeVox
.msgs
.getMsg('unknown_link');
1957 * Checks if a given node is inside a table and returns the table node if it is
1958 * @param {Node} node The node.
1959 * @param {{allowCaptions: (undefined|boolean)}=} kwargs Optional named args.
1960 * allowCaptions: If true, will return true even if inside a caption. False
1962 * @return {Node} If the node is inside a table, the table node. Null if it
1965 cvox
.DomUtil
.getContainingTable = function(node
, kwargs
) {
1966 var ancestors
= cvox
.DomUtil
.getAncestors(node
);
1967 return cvox
.DomUtil
.findTableNodeInList(ancestors
, kwargs
);
1972 * Extracts a table node from a list of nodes.
1973 * @param {Array<Node>} nodes The list of nodes.
1974 * @param {{allowCaptions: (undefined|boolean)}=} kwargs Optional named args.
1975 * allowCaptions: If true, will return true even if inside a caption. False
1977 * @return {Node} The table node if the list of nodes contains a table node.
1978 * Null if it does not.
1980 cvox
.DomUtil
.findTableNodeInList = function(nodes
, kwargs
) {
1981 kwargs
= kwargs
|| {allowCaptions
: false};
1982 // Don't include the caption node because it is actually rendered outside
1984 for (var i
= nodes
.length
- 1, node
; node
= nodes
[i
]; i
--) {
1985 if (node
.constructor != Text
) {
1986 if (!kwargs
.allowCaptions
&& node
.tagName
== 'CAPTION') {
1989 if ((node
.tagName
== 'TABLE') || cvox
.AriaUtil
.isGrid(node
)) {
1999 * Determines whether a given table is a data table or a layout table
2000 * @param {Node} tableNode The table node.
2001 * @return {boolean} If the table is a layout table, returns true. False
2004 cvox
.DomUtil
.isLayoutTable = function(tableNode
) {
2005 // TODO(stoarca): Why are we returning based on this inaccurate heuristic
2006 // instead of first trying the better heuristics below?
2007 if (tableNode
.rows
&& (tableNode
.rows
.length
<= 1 ||
2008 (tableNode
.rows
[0].childElementCount
== 1))) {
2009 // This table has either 0 or one rows, or only "one" column.
2010 // This is a quick check for column count and may not be accurate. See
2011 // TraverseTable.getW3CColCount_ for a more accurate
2012 // (but more complicated) way to determine column count.
2016 // These heuristics are adapted from the Firefox data and layout table.
2017 // heuristics: http://asurkov.blogspot.com/2011/10/data-vs-layout-table.html
2018 if (cvox
.AriaUtil
.isGrid(tableNode
)) {
2019 // This table has an ARIA role identifying it as a grid.
2020 // Not a layout table.
2023 if (cvox
.AriaUtil
.isLandmark(tableNode
)) {
2024 // This table has an ARIA landmark role - not a layout table.
2028 if (tableNode
.caption
|| tableNode
.summary
) {
2029 // This table has a caption or a summary - not a layout table.
2033 if ((cvox
.XpathUtil
.evalXPath('tbody/tr/th', tableNode
).length
> 0) &&
2034 (cvox
.XpathUtil
.evalXPath('tbody/tr/td', tableNode
).length
> 0)) {
2035 // This table at least one column and at least one column header.
2036 // Not a layout table.
2040 if (cvox
.XpathUtil
.evalXPath('colgroup', tableNode
).length
> 0) {
2041 // This table specifies column groups - not a layout table.
2045 if ((cvox
.XpathUtil
.evalXPath('thead', tableNode
).length
> 0) ||
2046 (cvox
.XpathUtil
.evalXPath('tfoot', tableNode
).length
> 0)) {
2047 // This table has header or footer rows - not a layout table.
2051 if ((cvox
.XpathUtil
.evalXPath('tbody/tr/td/embed', tableNode
).length
> 0) ||
2052 (cvox
.XpathUtil
.evalXPath('tbody/tr/td/object', tableNode
).length
> 0) ||
2053 (cvox
.XpathUtil
.evalXPath('tbody/tr/td/iframe', tableNode
).length
> 0) ||
2054 (cvox
.XpathUtil
.evalXPath('tbody/tr/td/applet', tableNode
).length
> 0)) {
2055 // This table contains embed, object, applet, or iframe elements. It is
2060 // These heuristics are loosely based on Okada and Miura's "Detection of
2061 // Layout-Purpose TABLE Tags Based on Machine Learning" (2007).
2062 // http://books.google.com/books?id=kUbmdqasONwC&lpg=PA116&ots=Lb3HJ7dISZ&lr&pg=PA116
2064 // Increase the points for each heuristic. If there are 3 or more points,
2065 // this is probably a layout table.
2068 if (! cvox
.DomUtil
.hasBorder(tableNode
)) {
2069 // This table has no border.
2073 if (tableNode
.rows
.length
<= 6) {
2074 // This table has a limited number of rows.
2078 if (cvox
.DomUtil
.countPreviousTags(tableNode
) <= 12) {
2079 // This table has a limited number of previous tags.
2083 if (cvox
.XpathUtil
.evalXPath('tbody/tr/td/table', tableNode
).length
> 0) {
2084 // This table has nested tables.
2087 return (points
>= 3);
2092 * Count previous tags, which we dfine as the number of HTML tags that
2093 * appear before the given node.
2094 * @param {Node} node The given node.
2095 * @return {number} The number of previous tags.
2097 cvox
.DomUtil
.countPreviousTags = function(node
) {
2098 var ancestors
= cvox
.DomUtil
.getAncestors(node
);
2099 return ancestors
.length
+ cvox
.DomUtil
.countPreviousSiblings(node
);
2104 * Counts previous siblings, not including text nodes.
2105 * @param {Node} node The given node.
2106 * @return {number} The number of previous siblings.
2108 cvox
.DomUtil
.countPreviousSiblings = function(node
) {
2110 var prev
= node
.previousSibling
;
2111 while (prev
!= null) {
2112 if (prev
.constructor != Text
) {
2115 prev
= prev
.previousSibling
;
2122 * Whether a given table has a border or not.
2123 * @param {Node} tableNode The table node.
2124 * @return {boolean} If the table has a border, return true. False otherwise.
2126 cvox
.DomUtil
.hasBorder = function(tableNode
) {
2127 // If .frame contains "void" there is no border.
2128 if (tableNode
.frame
) {
2129 return (tableNode
.frame
.indexOf('void') == -1);
2132 // If .border is defined and == "0" then there is no border.
2133 if (tableNode
.border
) {
2134 if (tableNode
.border
.length
== 1) {
2135 return (tableNode
.border
!= '0');
2137 return (tableNode
.border
.slice(0, -2) != 0);
2141 // If .style.border-style is 'none' there is no border.
2142 if (tableNode
.style
.borderStyle
&& tableNode
.style
.borderStyle
== 'none') {
2146 // If .style.border-width is specified in units of length
2147 // ( https://developer.mozilla.org/en/CSS/border-width ) then we need
2148 // to check if .style.border-width starts with 0[px,em,etc]
2149 if (tableNode
.style
.borderWidth
) {
2150 return (tableNode
.style
.borderWidth
.slice(0, -2) != 0);
2153 // If .style.border-color is defined, then there is a border
2154 if (tableNode
.style
.borderColor
) {
2162 * Return the first leaf node, starting at the top of the document.
2163 * @return {Node?} The first leaf node in the document, if found.
2165 cvox
.DomUtil
.getFirstLeafNode = function() {
2166 var node
= document
.body
;
2167 while (node
&& node
.firstChild
) {
2168 node
= node
.firstChild
;
2170 while (node
&& !cvox
.DomUtil
.hasContent(node
)) {
2171 node
= cvox
.DomUtil
.directedNextLeafNode(node
);
2178 * Finds the first descendant node that matches the filter function, using
2179 * a depth first search. This function offers the most general purpose way
2180 * of finding a matching element. You may also wish to consider
2181 * {@code goog.dom.query} which can express many matching criteria using
2182 * CSS selector expressions. These expressions often result in a more
2183 * compact representation of the desired result.
2184 * This is the findNode function from goog.dom:
2185 * http://code.google.com/p/closure-library/source/browse/trunk/closure/goog/dom/dom.js
2187 * @param {Node} root The root of the tree to search.
2188 * @param {function(Node) : boolean} p The filter function.
2189 * @return {Node|undefined} The found node or undefined if none is found.
2191 cvox
.DomUtil
.findNode = function(root
, p
) {
2193 var found
= cvox
.DomUtil
.findNodes_(root
, p
, rv
, true, 10000);
2194 return found
? rv
[0] : undefined;
2199 * Finds the number of nodes matching the filter.
2200 * @param {Node} root The root of the tree to search.
2201 * @param {function(Node) : boolean} p The filter function.
2202 * @return {number} The number of nodes selected by filter.
2204 cvox
.DomUtil
.countNodes = function(root
, p
) {
2206 cvox
.DomUtil
.findNodes_(root
, p
, rv
, false, 10000);
2212 * Finds the first or all the descendant nodes that match the filter function,
2213 * using a depth first search.
2214 * @param {Node} root The root of the tree to search.
2215 * @param {function(Node) : boolean} p The filter function.
2216 * @param {Array<Node>} rv The found nodes are added to this array.
2217 * @param {boolean} findOne If true we exit after the first found node.
2218 * @param {number} maxChildCount The max child count. This is used as a kill
2219 * switch - if there are more nodes than this, terminate the search.
2220 * @return {boolean} Whether the search is complete or not. True in case
2221 * findOne is true and the node is found. False otherwise. This is the
2222 * findNodes_ function from goog.dom:
2223 * http://code.google.com/p/closure-library/source/browse/trunk/closure/goog/dom/dom.js.
2226 cvox
.DomUtil
.findNodes_ = function(root
, p
, rv
, findOne
, maxChildCount
) {
2227 if ((root
!= null) || (maxChildCount
== 0)) {
2228 var child
= root
.firstChild
;
2236 maxChildCount
= maxChildCount
- 1;
2237 if (cvox
.DomUtil
.findNodes_(child
, p
, rv
, findOne
, maxChildCount
)) {
2240 child
= child
.nextSibling
;
2248 * Converts a NodeList into an array
2249 * @param {NodeList} nodeList The nodeList.
2250 * @return {Array} The array of nodes in the nodeList.
2252 cvox
.DomUtil
.toArray = function(nodeList
) {
2254 for (var i
= 0; i
< nodeList
.length
; i
++) {
2255 nodeArray
.push(nodeList
[i
]);
2262 * Creates a new element with the same attributes and no children.
2263 * @param {Node|Text} node A node to clone.
2264 * @param {Object<boolean>} skipattrs Set the attribute to true to skip it
2266 * @return {Node|Text} The cloned node.
2268 cvox
.DomUtil
.shallowChildlessClone = function(node
, skipattrs
) {
2269 if (node
.nodeName
== '#text') {
2270 return document
.createTextNode(node
.nodeValue
);
2273 if (node
.nodeName
== '#comment') {
2274 return document
.createComment(node
.nodeValue
);
2277 var ret
= document
.createElement(node
.nodeName
);
2278 for (var i
= 0; i
< node
.attributes
.length
; ++i
) {
2279 var attr
= node
.attributes
[i
];
2280 if (skipattrs
&& skipattrs
[attr
.nodeName
]) {
2283 ret
.setAttribute(attr
.nodeName
, attr
.nodeValue
);
2290 * Creates a new element with the same attributes and clones of children.
2291 * @param {Node|Text} node A node to clone.
2292 * @param {Object<boolean>} skipattrs Set the attribute to true to skip it
2294 * @return {Node|Text} The cloned node.
2296 cvox
.DomUtil
.deepClone = function(node
, skipattrs
) {
2297 var ret
= cvox
.DomUtil
.shallowChildlessClone(node
, skipattrs
);
2298 for (var i
= 0; i
< node
.childNodes
.length
; ++i
) {
2299 ret
.appendChild(cvox
.DomUtil
.deepClone(node
.childNodes
[i
], skipattrs
));
2306 * Returns either node.firstChild or node.lastChild, depending on direction.
2307 * @param {Node|Text} node The node.
2308 * @param {boolean} reverse If reversed.
2309 * @return {Node|Text} The directed first child or null if the node has
2312 cvox
.DomUtil
.directedFirstChild = function(node
, reverse
) {
2314 return node
.lastChild
;
2316 return node
.firstChild
;
2320 * Returns either node.nextSibling or node.previousSibling, depending on
2322 * @param {Node|Text} node The node.
2323 * @param {boolean=} reverse If reversed.
2324 * @return {Node|Text} The directed next sibling or null if there are
2325 * no more siblings in that direction.
2327 cvox
.DomUtil
.directedNextSibling = function(node
, reverse
) {
2332 return node
.previousSibling
;
2334 return node
.nextSibling
;
2338 * Creates a function that sends a click. This is because loop closures
2340 * See: http://joust.kano.net/weblog/archive/2005/08/08/
2341 * a-huge-gotcha-with-javascript-closures/
2342 * @param {Node} targetNode The target node to click on.
2343 * @return {function()} A function that will click on the given targetNode.
2345 cvox
.DomUtil
.createSimpleClickFunction = function(targetNode
) {
2346 var target
= targetNode
.cloneNode(true);
2347 return function() { cvox
.DomUtil
.clickElem(target
, false, false); };
2351 * Adds a node to document.head if that node has not already been added.
2352 * If document.head does not exist, this will add the node to the body.
2353 * @param {Node} node The node to add.
2354 * @param {string=} opt_id The id of the node to ensure the node is only
2357 cvox
.DomUtil
.addNodeToHead = function(node
, opt_id
) {
2358 if (opt_id
&& document
.getElementById(opt_id
)) {
2361 var p
= document
.head
|| document
.body
;
2362 p
.appendChild(node
);
2367 * Checks if a given node is inside a math expressions and
2368 * returns the math node if one exists.
2369 * @param {Node} node The node.
2370 * @return {Node} The math node, if the node is inside a math expression.
2371 * Null if it is not.
2373 cvox
.DomUtil
.getContainingMath = function(node
) {
2374 var ancestors
= cvox
.DomUtil
.getAncestors(node
);
2375 return cvox
.DomUtil
.findMathNodeInList(ancestors
);
2380 * Extracts a math node from a list of nodes.
2381 * @param {Array<Node>} nodes The list of nodes.
2382 * @return {Node} The math node if the list of nodes contains a math node.
2383 * Null if it does not.
2385 cvox
.DomUtil
.findMathNodeInList = function(nodes
) {
2386 for (var i
= 0, node
; node
= nodes
[i
]; i
++) {
2387 if (cvox
.DomUtil
.isMath(node
)) {
2396 * Checks to see wether a node is a math node.
2397 * @param {Node} node The node to be tested.
2398 * @return {boolean} Whether or not a node is a math node.
2400 cvox
.DomUtil
.isMath = function(node
) {
2401 return cvox
.DomUtil
.isMathml(node
) ||
2402 cvox
.DomUtil
.isMathJax(node
) ||
2403 cvox
.DomUtil
.isMathImg(node
) ||
2404 cvox
.AriaUtil
.isMath(node
);
2409 * Specifies node classes in which we expect maths expressions a alt text.
2410 * @type {{tex: Array<string>,
2411 * asciimath: Array<string>}}
2413 // These are the classes for which we assume they contain Maths in the ALT or
2416 // latex: Wordpress;
2417 // numberedequation, inlineformula, displayformula: MathWorld;
2418 cvox
.DomUtil
.ALT_MATH_CLASSES
= {
2419 tex
: ['tex', 'latex'],
2420 asciimath
: ['numberedequation', 'inlineformula', 'displayformula']
2425 * Composes a query selector string for image nodes with alt math content by
2427 * @param {string} contentType The content type, e.g., tex, asciimath.
2428 * @return {!string} The query elector string.
2430 cvox
.DomUtil
.altMathQuerySelector = function(contentType
) {
2431 var classes
= cvox
.DomUtil
.ALT_MATH_CLASSES
[contentType
];
2433 return classes
.map(function(x
) {return 'img.' + x
;}).join(', ');
2440 * Check if a given node is potentially a math image with alternative text in
2442 * @param {Node} node The node to be tested.
2443 * @return {boolean} Whether or not a node has an image with class TeX or LaTeX.
2445 cvox
.DomUtil
.isMathImg = function(node
) {
2446 if (!node
|| !node
.tagName
|| !node
.className
) {
2449 if (node
.tagName
!= 'IMG') {
2452 for (var i
= 0, className
; className
= node
.classList
.item(i
); i
++) {
2453 className
= className
.toLowerCase();
2454 if (cvox
.DomUtil
.ALT_MATH_CLASSES
.tex
.indexOf(className
) != -1 ||
2455 cvox
.DomUtil
.ALT_MATH_CLASSES
.asciimath
.indexOf(className
) != -1) {
2464 * Checks to see whether a node is a MathML node.
2465 * !! This is necessary as Chrome currently does not upperCase Math tags !!
2466 * @param {Node} node The node to be tested.
2467 * @return {boolean} Whether or not a node is a MathML node.
2469 cvox
.DomUtil
.isMathml = function(node
) {
2470 if (!node
|| !node
.tagName
) {
2473 return node
.tagName
.toLowerCase() == 'math';
2478 * Checks to see wether a node is a MathJax node.
2479 * @param {Node} node The node to be tested.
2480 * @return {boolean} Whether or not a node is a MathJax node.
2482 cvox
.DomUtil
.isMathJax = function(node
) {
2483 if (!node
|| !node
.tagName
|| !node
.className
) {
2487 function isSpanWithClass(n
, cl
) {
2488 return (n
.tagName
== 'SPAN' &&
2489 n
.className
.split(' ').some(function(x
) {
2490 return x
.toLowerCase() == cl
;}));
2492 if (isSpanWithClass(node
, 'math')) {
2493 var ancestors
= cvox
.DomUtil
.getAncestors(node
);
2494 return ancestors
.some(function(x
) {return isSpanWithClass(x
, 'mathjax');});
2501 * Computes the id of the math span in a MathJax DOM element.
2502 * @param {string} jaxId The id of the MathJax node.
2503 * @return {string} The id of the span node.
2505 cvox
.DomUtil
.getMathSpanId = function(jaxId
) {
2506 var node
= document
.getElementById(jaxId
+ '-Frame');
2508 var span
= node
.querySelector('span.math');
2517 * Returns true if the node has a longDesc.
2518 * @param {Node} node The node to be tested.
2519 * @return {boolean} Whether or not a node has a longDesc.
2521 cvox
.DomUtil
.hasLongDesc = function(node
) {
2522 if (node
&& node
.longDesc
) {
2530 * Returns tag name of a node if it has one.
2531 * @param {Node} node A node.
2532 * @return {string} A the tag name of the node.
2534 cvox
.DomUtil
.getNodeTagName = function(node
) {
2535 if (node
.nodeType
== Node
.ELEMENT_NODE
) {
2536 return node
.tagName
;
2543 * Cleaning up a list of nodes to remove empty text nodes.
2544 * @param {NodeList} nodes The nodes list.
2545 * @return {!Array<Node|string|null>} The cleaned up list of nodes.
2547 cvox
.DomUtil
.purgeNodes = function(nodes
) {
2548 return cvox
.DomUtil
.toArray(nodes
).
2549 filter(function(node
) {
2550 return node
.nodeType
!= Node
.TEXT_NODE
||
2551 !node
.textContent
.match(/^\s+$/);});
2556 * Calculates a hit point for a given node.
2557 * @return {{x:(number), y:(number)}} The position.
2559 cvox
.DomUtil
.elementToPoint = function(node
) {
2561 return {x
: 0, y
: 0};
2563 if (node
.constructor == Text
) {
2564 node
= node
.parentNode
;
2566 var r
= node
.getBoundingClientRect();
2568 x
: r
.left
+ (r
.width
/ 2),
2569 y
: r
.top
+ (r
.height
/ 2)
2575 * Checks if an input node supports HTML5 selection.
2576 * If the node is not an input element, returns false.
2577 * @param {Node} node The node to check.
2578 * @return {boolean} True if HTML5 selection supported.
2580 cvox
.DomUtil
.doesInputSupportSelection = function(node
) {
2581 return goog
.isDef(node
) &&
2582 node
.tagName
== 'INPUT' &&
2583 node
.type
!= 'email' &&
2584 node
.type
!= 'number';
2589 * Gets the hint text for a given element.
2590 * @param {Node} node The target node.
2591 * @return {string} The hint text.
2593 cvox
.DomUtil
.getHint = function(node
) {
2595 if (node
.hasAttribute
) {
2596 if (node
.hasAttribute('aria-describedby')) {
2597 var describedByIds
= node
.getAttribute('aria-describedby').split(' ');
2598 for (var describedById
, i
= 0; describedById
= describedByIds
[i
]; i
++) {
2599 var describedNode
= document
.getElementById(describedById
);
2600 if (describedNode
) {
2601 desc
+= ' ' + cvox
.DomUtil
.getName(
2602 describedNode
, true, true, true);