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,
197 * @param {Node} node The node to check the ancestor chain for.
198 * @return {boolean} True if a descendant is invisible.
201 cvox.DomUtil.hasInvisibleAncestor_ = function(node) {
203 while (ancestor = ancestor.parentElement) {
204 var style = document.defaultView.getComputedStyle(ancestor, null);
205 if (cvox.DomUtil.isInvisibleStyle(style, true)) {
214 * Checks for a visible node in the subtree defined by root.
215 * @param {Node} root The root of the subtree to check.
216 * @param {boolean} recursive Whether or not to check beyond the root of the
217 * subtree for visible nodes. This option exists for performance tuning.
218 * Sometimes we already have information about the descendants, and we do
219 * not need to check them again.
220 * @return {boolean} True if the subtree contains a visible node.
223 cvox.DomUtil.hasVisibleNodeSubtree_ = function(root, recursive) {
224 if (!(root instanceof Element)) {
225 var parentStyle = document.defaultView
226 .getComputedStyle(root.parentElement, null);
227 var isVisibleParent = !cvox.DomUtil.isInvisibleStyle(parentStyle);
228 return isVisibleParent;
231 var rootStyle = document.defaultView.getComputedStyle(root, null);
232 var isRootVisible = !cvox.DomUtil.isInvisibleStyle(rootStyle);
236 var isSubtreeInvisible = cvox.DomUtil.isInvisibleStyle(rootStyle, true);
237 if (!recursive || isSubtreeInvisible) {
241 // Carry on with a recursive check of the descendants.
242 var children = root.childNodes;
243 for (var i = 0; i < children.length; i++) {
244 var child = children[i];
245 if (cvox.DomUtil.hasVisibleNodeSubtree_(child, recursive)) {
254 * Determines whether or a node is not visible according to any CSS criteria
256 * @param {CSSStyleDeclaration} style The style of the node to determine as
258 * @param {boolean=} opt_strict If set to true, we do not check the visibility
259 * style attribute. False by default.
260 * CAUTION: Checking the visibility style attribute can result in returning
261 * true (invisible) even when an element has have visible descendants. This
262 * is because an element with visibility:hidden can have descendants that
264 * @return {boolean} True if the node is invisible.
266 cvox.DomUtil.isInvisibleStyle = function(style, opt_strict) {
270 if (style.display == 'none') {
273 // Opacity values range from 0.0 (transparent) to 1.0 (fully opaque).
274 if (parseFloat(style.opacity) == 0) {
277 // Visibility style tests for non-strict checking.
279 (style.visibility == 'hidden' || style.visibility == 'collapse')) {
287 * Determines whether a control should be announced as disabled.
289 * @param {Node} node The node to be examined.
290 * @return {boolean} Whether or not the node is disabled.
292 cvox.DomUtil.isDisabled = function(node) {
297 while (ancestor = ancestor.parentElement) {
298 if (ancestor.tagName == 'FIELDSET' && ancestor.disabled) {
307 * Determines whether a node is an HTML5 semantic element
309 * @param {Node} node The node to be checked.
310 * @return {boolean} True if the node is an HTML5 semantic element.
312 cvox.DomUtil.isSemanticElt = function(node) {
314 var tag = node.tagName;
315 if ((tag == 'SECTION') || (tag == 'NAV') || (tag == 'ARTICLE') ||
316 (tag == 'ASIDE') || (tag == 'HGROUP') || (tag == 'HEADER') ||
317 (tag == 'FOOTER') || (tag == 'TIME') || (tag == 'MARK')) {
326 * Determines whether or not a node is a leaf node.
327 * TODO (adu): This function is doing a lot more than just checking for the
328 * presence of descendants. We should be more precise in the documentation
329 * about what we mean by leaf node.
331 * @param {Node} node The node to be checked.
332 * @param {boolean=} opt_allowHidden Allows hidden nodes during descent.
333 * @return {boolean} True if the node is a leaf node.
335 cvox.DomUtil.isLeafNode = function(node, opt_allowHidden) {
336 // If it's not an Element, then it's a leaf if it has no first child.
337 if (!(node instanceof Element)) {
338 return (node.firstChild == null);
341 // Now we know for sure it's an element.
342 var element = /** @type {Element} */(node);
343 if (!opt_allowHidden &&
344 !cvox.DomUtil.isVisible(element, {checkAncestors: false})) {
347 if (!opt_allowHidden && cvox.AriaUtil.isHidden(element)) {
350 if (cvox.AriaUtil.isLeafElement(element)) {
353 switch (element.tagName) {
363 if (!!cvox.DomPredicates.linkPredicate([element])) {
364 return !cvox.DomUtil.findNode(element, function(node) {
365 return !!cvox.DomPredicates.headingPredicate([node]);
368 if (cvox.DomUtil.isLeafLevelControl(element)) {
371 if (!element.firstChild) {
374 if (cvox.DomUtil.isMath(element)) {
377 if (cvox.DomPredicates.headingPredicate([element])) {
378 return !cvox.DomUtil.findNode(element, function(n) {
379 return !!cvox.DomPredicates.controlPredicate([n]);
387 * Determines whether or not a node is or is the descendant of a node
388 * with a particular tag or class name.
390 * @param {Node} node The node to be checked.
391 * @param {?string} tagName The tag to check for, or null if the tag
393 * @param {?string=} className The class to check for, or null if the class
395 * @return {boolean} True if the node or one of its ancestor has the specified
398 cvox.DomUtil.isDescendantOf = function(node, tagName, className) {
401 if (tagName && className &&
402 (node.tagName && (node.tagName == tagName)) &&
403 (node.className && (node.className == className))) {
405 } else if (tagName && !className &&
406 (node.tagName && (node.tagName == tagName))) {
408 } else if (!tagName && className &&
409 (node.className && (node.className == className))) {
412 node = node.parentNode;
419 * Determines whether or not a node is or is the descendant of another node.
421 * @param {Object} node The node to be checked.
422 * @param {Object} ancestor The node to see if it's a descendant of.
423 * @return {boolean} True if the node is ancestor or is a descendant of it.
425 cvox.DomUtil.isDescendantOfNode = function(node, ancestor) {
426 while (node && ancestor) {
427 if (node.isSameNode(ancestor)) {
430 node = node.parentNode;
437 * Remove all whitespace from the beginning and end, and collapse all
438 * inner strings of whitespace to a single space.
439 * @param {string} str The input string.
440 * @return {string} The string with whitespace collapsed.
442 cvox.DomUtil.collapseWhitespace = function(str) {
443 return str.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '');
447 * Gets the base label of a node. I don't know exactly what this is.
449 * @param {Node} node The node to get the label from.
450 * @param {boolean=} recursive Whether or not the element's subtree
451 * should be used; true by default.
452 * @param {boolean=} includeControls Whether or not controls in the subtree
453 * should be included; true by default.
454 * @return {string} The base label of the node.
457 cvox.DomUtil.getBaseLabel_ = function(node, recursive, includeControls) {
459 if (node.hasAttribute) {
460 if (node.hasAttribute('aria-labelledby')) {
461 var labelNodeIds = node.getAttribute('aria-labelledby').split(' ');
462 for (var labelNodeId, i = 0; labelNodeId = labelNodeIds[i]; i++) {
463 var labelNode = document.getElementById(labelNodeId);
465 label += ' ' + cvox.DomUtil.getName(
466 labelNode, true, includeControls, true);
469 } else if (node.hasAttribute('aria-label')) {
470 label = node.getAttribute('aria-label');
471 } else if (node.constructor == HTMLImageElement) {
472 label = cvox.DomUtil.getImageTitle(node);
473 } else if (node.tagName == 'FIELDSET') {
474 // Other labels will trump fieldset legend with this implementation.
475 // Depending on how this works out on the web, we may later switch this
476 // to appending the fieldset legend to any existing label.
477 var legends = node.getElementsByTagName('LEGEND');
479 for (var legend, i = 0; legend = legends[i]; i++) {
480 label += ' ' + cvox.DomUtil.getName(legend, true, includeControls);
484 if (label.length == 0 && node && node.id) {
485 var labelFor = document.querySelector('label[for="' + node.id + '"]');
487 label = cvox.DomUtil.getName(labelFor, recursive, includeControls);
491 return cvox.DomUtil.collapseWhitespace(label);
495 * Gets the nearest label in the ancestor chain, if one exists.
496 * @param {Node} node The node to start from.
497 * @return {string} The label.
500 cvox.DomUtil.getNearestAncestorLabel_ = function(node) {
502 var enclosingLabel = node;
503 while (enclosingLabel && enclosingLabel.tagName != 'LABEL') {
504 enclosingLabel = enclosingLabel.parentElement;
506 if (enclosingLabel && !enclosingLabel.hasAttribute('for')) {
507 // Get all text from the label but don't include any controls.
508 label = cvox.DomUtil.getName(enclosingLabel, true, false);
515 * Gets the name for an input element.
516 * @param {Node} node The node.
517 * @return {string} The name.
520 cvox.DomUtil.getInputName_ = function(node) {
522 if (node.type == 'image') {
523 label = cvox.DomUtil.getImageTitle(node);
524 } else if (node.type == 'submit') {
525 if (node.hasAttribute('value')) {
526 label = node.getAttribute('value');
530 } else if (node.type == 'reset') {
531 if (node.hasAttribute('value')) {
532 label = node.getAttribute('value');
536 } else if (node.type == 'button') {
537 if (node.hasAttribute('value')) {
538 label = node.getAttribute('value');
545 * Wraps getName_ with marking and unmarking nodes so that infinite loops
546 * don't occur. This is the ugly way to solve this; getName should not ever
547 * do a recursive call somewhere above it in the tree.
548 * @param {Node} node See getName_.
549 * @param {boolean=} recursive See getName_.
550 * @param {boolean=} includeControls See getName_.
551 * @param {boolean=} opt_allowHidden Allows hidden nodes in name computation.
552 * @return {string} See getName_.
554 cvox.DomUtil.getName = function(
555 node, recursive, includeControls, opt_allowHidden) {
556 if (!node || node.cvoxGetNameMarked == true) {
559 node.cvoxGetNameMarked = true;
561 cvox.DomUtil.getName_(node, recursive, includeControls, opt_allowHidden);
562 node.cvoxGetNameMarked = false;
563 var prefix = cvox.DomUtil.getPrefixText(node);
567 // TODO(dtseng): Seems like this list should be longer...
569 * Determines if a node has a name obtained from concatinating the names of its
571 * @param {!Node} node The node under consideration.
572 * @param {boolean=} opt_allowHidden Allows hidden nodes in name computation.
573 * @return {boolean} True if node has name based on children.
576 cvox.DomUtil.hasChildrenBasedName_ = function(node, opt_allowHidden) {
577 if (!!cvox.DomPredicates.linkPredicate([node]) ||
578 !!cvox.DomPredicates.headingPredicate([node]) ||
579 node.tagName == 'BUTTON' ||
580 cvox.AriaUtil.isControlWidget(node) ||
581 !cvox.DomUtil.isLeafNode(node, opt_allowHidden)) {
589 * Get the name of a node: this includes all static text content and any
590 * HTML-author-specified label, title, alt text, aria-label, etc. - but
592 * - the user-generated control value (use getValue)
593 * - the current state (use getState)
594 * - the role (use getRole)
596 * Order of precedence:
597 * Text content if it's a text node.
602 * label (for a control)
603 * placeholder (for an input element)
604 * recursive calls to getName on all children
606 * @param {Node} node The node to get the name from.
607 * @param {boolean=} recursive Whether or not the element's subtree should
608 * be used; true by default.
609 * @param {boolean=} includeControls Whether or not controls in the subtree
610 * should be included; true by default.
611 * @param {boolean=} opt_allowHidden Allows hidden nodes in name computation.
612 * @return {string} The name of the node.
615 cvox.DomUtil.getName_ = function(
616 node, recursive, includeControls, opt_allowHidden) {
617 if (typeof(recursive) === 'undefined') {
620 if (typeof(includeControls) === 'undefined') {
621 includeControls = true;
624 if (node.constructor == Text) {
628 var label = cvox.DomUtil.getBaseLabel_(node, recursive, includeControls);
630 if (label.length == 0 && cvox.DomUtil.isControl(node)) {
631 label = cvox.DomUtil.getNearestAncestorLabel_(node);
634 if (label.length == 0 && node.constructor == HTMLInputElement) {
635 label = cvox.DomUtil.getInputName_(node);
638 if (cvox.DomUtil.isInputTypeText(node) && node.hasAttribute('placeholder')) {
639 var placeholder = node.getAttribute('placeholder');
640 if (label.length > 0) {
641 if (cvox.DomUtil.getValue(node).length > 0) {
644 return label + ' with hint ' + placeholder;
651 if (label.length > 0) {
655 // Fall back to naming via title only if there is no text content.
656 if (cvox.DomUtil.collapseWhitespace(node.textContent).length == 0 &&
658 node.hasAttribute('title')) {
659 return node.getAttribute('title');
666 if (cvox.AriaUtil.isCompositeControl(node)) {
669 if (cvox.DomUtil.hasChildrenBasedName_(node, opt_allowHidden)) {
670 return cvox.DomUtil.getNameFromChildren(
671 node, includeControls, opt_allowHidden);
678 * Get the name from the children of a node, not including the node itself.
680 * @param {Node} node The node to get the name from.
681 * @param {boolean=} includeControls Whether or not controls in the subtree
682 * should be included; true by default.
683 * @param {boolean=} opt_allowHidden Allow hidden nodes in name computation.
684 * @return {string} The concatenated text of all child nodes.
686 cvox.DomUtil.getNameFromChildren = function(
687 node, includeControls, opt_allowHidden) {
688 if (includeControls == undefined) {
689 includeControls = true;
693 for (var i = 0; i < node.childNodes.length; i++) {
694 var child = node.childNodes[i];
695 var prevChild = node.childNodes[i - 1] || child;
696 if (!includeControls && cvox.DomUtil.isControl(child)) {
699 var isVisible = cvox.DomUtil.isVisible(child, {checkAncestors: false});
700 if (opt_allowHidden || (isVisible && !cvox.AriaUtil.isHidden(child))) {
701 delimiter = (prevChild.tagName == 'SPAN' ||
702 child.tagName == 'SPAN' ||
703 child.parentNode.tagName == 'SPAN') ?
705 name += delimiter + cvox.DomUtil.getName(child, true, includeControls);
713 * Get any prefix text for the given node.
714 * This includes list style text for the leftmost leaf node under a listitem.
715 * @param {Node} node Compute prefix for this node.
716 * @param {number=} opt_index Starting offset into the given node's text.
717 * @return {string} Prefix text, if any.
719 cvox.DomUtil.getPrefixText = function(node, opt_index) {
720 opt_index = opt_index || 0;
722 // Generate list style text.
723 var ancestors = cvox.DomUtil.getAncestors(node);
725 var firstListitem = cvox.DomPredicates.listItemPredicate(ancestors);
727 var leftmost = firstListitem;
728 while (leftmost && leftmost.firstChild) {
729 leftmost = leftmost.firstChild;
732 // Do nothing if we're not at the leftmost leaf.
734 firstListitem.parentNode &&
736 firstListitem.parentNode.tagName == 'OL' &&
738 document.defaultView.getComputedStyle(firstListitem.parentNode)
739 .listStyleType != 'none') {
740 var items = cvox.DomUtil.toArray(firstListitem.parentNode.children).filter(
741 function(li) { return li.tagName == 'LI'; });
742 var position = items.indexOf(firstListitem) + 1;
743 // TODO(dtseng): Support all list style types.
744 if (document.defaultView.getComputedStyle(
745 firstListitem.parentNode).listStyleType.indexOf('latin') != -1) {
747 prefix = String.fromCharCode('A'.charCodeAt(0) + position % 26);
758 * Use heuristics to guess at the label of a control, to be used if one
759 * is not explicitly set in the DOM. This is useful when a control
760 * field gets focus, but probably not useful when browsing the page
762 * @param {Node} node The node to get the label from.
763 * @return {string} The name of the control, using heuristics.
765 cvox.DomUtil.getControlLabelHeuristics = function(node) {
766 // If the node explicitly has aria-label or title set to '',
767 // treat it the same way as alt='' and do not guess - just assume
768 // the web developer knew what they were doing and wanted
769 // no title/label for that control.
770 if (node.hasAttribute &&
771 ((node.hasAttribute('aria-label') &&
772 (node.getAttribute('aria-label') == '')) ||
773 (node.hasAttribute('aria-title') &&
774 (node.getAttribute('aria-title') == '')))) {
778 // TODO (clchen, rshearer): Implement heuristics for getting the label
779 // information from the table headers once the code for getting table
780 // headers quickly is implemented.
782 // If no description has been found yet and heuristics are enabled,
783 // then try getting the content from the closest node.
784 var prevNode = cvox.DomUtil.previousLeafNode(node);
785 var prevTraversalCount = 0;
786 while (prevNode && (!cvox.DomUtil.hasContent(prevNode) ||
787 cvox.DomUtil.isControl(prevNode))) {
788 prevNode = cvox.DomUtil.previousLeafNode(prevNode);
789 prevTraversalCount++;
791 var nextNode = cvox.DomUtil.directedNextLeafNode(node);
792 var nextTraversalCount = 0;
793 while (nextNode && (!cvox.DomUtil.hasContent(nextNode) ||
794 cvox.DomUtil.isControl(nextNode))) {
795 nextNode = cvox.DomUtil.directedNextLeafNode(nextNode);
796 nextTraversalCount++;
798 var guessedLabelNode;
799 if (prevNode && nextNode) {
800 var parentNode = node;
801 // Count the number of parent nodes until there is a shared parent; the
802 // label is most likely in the same branch of the DOM as the control.
803 // TODO (chaitanyag): Try to generalize this algorithm and move it to
804 // its own function in DOM Utils.
807 if (cvox.DomUtil.isDescendantOfNode(prevNode, parentNode)) {
810 parentNode = parentNode.parentNode;
816 if (cvox.DomUtil.isDescendantOfNode(nextNode, parentNode)) {
819 parentNode = parentNode.parentNode;
822 guessedLabelNode = nextCount < prevCount ? nextNode : prevNode;
824 guessedLabelNode = prevNode || nextNode;
826 if (guessedLabelNode) {
827 return cvox.DomUtil.collapseWhitespace(
828 cvox.DomUtil.getValue(guessedLabelNode) + ' ' +
829 cvox.DomUtil.getName(guessedLabelNode));
837 * Get the text value of a node: the selected value of a select control or the
838 * current text of a text control. Does not return the state of a checkbox
843 * @param {Node} node The node to get the value from.
844 * @return {string} The value of the node.
846 cvox.DomUtil.getValue = function(node) {
847 var activeDescendant = cvox.AriaUtil.getActiveDescendant(node);
848 if (activeDescendant) {
849 return cvox.DomUtil.collapseWhitespace(
850 cvox.DomUtil.getValue(activeDescendant) + ' ' +
851 cvox.DomUtil.getName(activeDescendant));
854 if (node.constructor == HTMLSelectElement) {
855 node = /** @type {HTMLSelectElement} */(node);
857 var start = node.selectedOptions ? node.selectedOptions[0] : null;
858 var end = node.selectedOptions ?
859 node.selectedOptions[node.selectedOptions.length - 1] : null;
860 // TODO(dtseng): Keeping this stateless means we describe the start and end
861 // of the selection only since we don't know which was added or
862 // removed. Once we keep the previous selection, we can read the diff.
863 if (start && end && start != end) {
864 value = cvox.ChromeVox.msgs.getMsg(
865 'selected_options_value', [start.text, end.text]);
867 value = start.text + '';
872 if (node.constructor == HTMLTextAreaElement) {
876 if (node.constructor == HTMLInputElement) {
878 // Returning '' for inputs that are covered by getName.
888 return node.value.replace(/./g, 'dot ');
894 if (node.isContentEditable) {
895 return cvox.DomUtil.getNameFromChildren(node, true);
903 * Given an image node, return its title as a string. The preferred title
904 * is always the alt text, and if that's not available, then the title
905 * attribute. If neither of those are available, it attempts to construct
906 * a title from the filename, and if all else fails returns the word Image.
907 * @param {Node} node The image node.
908 * @return {string} The title of the image.
910 cvox.DomUtil.getImageTitle = function(node) {
912 if (node.hasAttribute('alt')) {
914 } else if (node.hasAttribute('title')) {
918 if (url.substring(0, 4) != 'data') {
919 var filename = url.substring(
920 url.lastIndexOf('/') + 1, url.lastIndexOf('.'));
922 // Hack to not speak the filename if it's ridiculously long.
923 if (filename.length >= 1 && filename.length <= 16) {
924 text = filename + ' Image';
937 * Search the whole page for any aria-labelledby attributes and collect
938 * the complete set of ids they map to, so that we can skip elements that
939 * just label other elements and not double-speak them. We cache this
940 * result and then throw it away at the next event loop.
941 * @return {Object.<string, boolean>} Set of all ids that are mapped
942 * by aria-labelledby.
944 cvox.DomUtil.getLabelledByTargets = function() {
945 if (cvox.labelledByTargets) {
946 return cvox.labelledByTargets;
949 // Start by getting all elements with
950 // aria-labelledby on the page since that's probably a short list,
951 // then see if any of those ids overlap with an id in this element's
953 var labelledByElements = document.querySelectorAll('[aria-labelledby]');
954 var labelledByTargets = {};
955 for (var i = 0; i < labelledByElements.length; ++i) {
956 var element = labelledByElements[i];
957 var attrValue = element.getAttribute('aria-labelledby');
958 var ids = attrValue.split(/ +/);
959 for (var j = 0; j < ids.length; j++) {
960 labelledByTargets[ids[j]] = true;
963 cvox.labelledByTargets = labelledByTargets;
965 window.setTimeout(function() {
966 cvox.labelledByTargets = null;
969 return labelledByTargets;
974 * Determines whether or not a node has content.
976 * @param {Node} node The node to be checked.
977 * @return {boolean} True if the node has content.
979 cvox.DomUtil.hasContent = function(node) {
980 // Memoize the result of the internal content computation so that
981 // within the same call stack, we don't need to redo the computation
982 // on the same node twice.
983 return /** @type {boolean} */ (cvox.Memoize.memoize(
984 cvox.DomUtil.computeHasContent_.bind(this), 'hasContent', node));
988 * Internal implementation of |cvox.DomUtil.hasContent|.
990 * @param {Node} node The node to be checked.
991 * @return {boolean} True if the node has content.
994 cvox.DomUtil.computeHasContent_ = function(node) {
995 // nodeType:8 == COMMENT_NODE
996 if (node.nodeType == 8) {
1000 // Exclude anything in the head
1001 if (cvox.DomUtil.isDescendantOf(node, 'HEAD')) {
1005 // Exclude script nodes
1006 if (cvox.DomUtil.isDescendantOf(node, 'SCRIPT')) {
1010 // Exclude noscript nodes
1011 if (cvox.DomUtil.isDescendantOf(node, 'NOSCRIPT')) {
1015 // Exclude noembed nodes since NOEMBED is deprecated. We treat
1016 // noembed as having not content rather than try to get its content since
1017 // Chrome will return raw HTML content rather than a valid DOM subtree.
1018 if (cvox.DomUtil.isDescendantOf(node, 'NOEMBED')) {
1022 // Exclude style nodes that have been dumped into the body.
1023 if (cvox.DomUtil.isDescendantOf(node, 'STYLE')) {
1027 // Check the style to exclude undisplayed/hidden nodes.
1028 if (!cvox.DomUtil.isVisible(node)) {
1032 // Ignore anything that is hidden by ARIA.
1033 if (cvox.AriaUtil.isHidden(node)) {
1037 // We need to speak controls, including those with no value entered. We
1038 // therefore treat visible controls as if they had content, and return true
1040 if (cvox.DomUtil.isControl(node)) {
1044 // Videos are always considered to have content so that we can navigate to
1045 // and use the controls of the video widget.
1046 if (cvox.DomUtil.isDescendantOf(node, 'VIDEO')) {
1049 // Audio elements are always considered to have content so that we can
1050 // navigate to and use the controls of the audio widget.
1051 if (cvox.DomUtil.isDescendantOf(node, 'AUDIO')) {
1055 // We want to try to jump into an iframe iff it has a src attribute.
1056 // For right now, we will avoid iframes without any content in their src since
1057 // ChromeVox is not being injected in those cases and will cause the user to
1059 // TODO (clchen, dmazzoni): Manually inject ChromeVox for iframes without src.
1060 if ((node.tagName == 'IFRAME') && (node.src) &&
1061 (node.src.indexOf('javascript:') != 0)) {
1065 var controlQuery = 'button,input,select,textarea';
1067 // Skip any non-control content inside of a label if the label is
1068 // correctly associated with a control, the label text will get spoken
1069 // when the control is reached.
1070 var enclosingLabel = node.parentElement;
1071 while (enclosingLabel && enclosingLabel.tagName != 'LABEL') {
1072 enclosingLabel = enclosingLabel.parentElement;
1074 if (enclosingLabel) {
1075 var embeddedControl = enclosingLabel.querySelector(controlQuery);
1076 if (enclosingLabel.hasAttribute('for')) {
1077 var targetId = enclosingLabel.getAttribute('for');
1078 var targetNode = document.getElementById(targetId);
1080 cvox.DomUtil.isControl(targetNode) &&
1084 } else if (embeddedControl) {
1089 // Skip any non-control content inside of a legend if the legend is correctly
1090 // nested within a fieldset. The legend text will get spoken when the fieldset
1092 var enclosingLegend = node.parentElement;
1093 while (enclosingLegend && enclosingLegend.tagName != 'LEGEND') {
1094 enclosingLegend = enclosingLegend.parentElement;
1096 if (enclosingLegend) {
1097 var legendAncestor = enclosingLegend.parentElement;
1098 while (legendAncestor && legendAncestor.tagName != 'FIELDSET') {
1099 legendAncestor = legendAncestor.parentElement;
1101 var embeddedControl =
1102 legendAncestor && legendAncestor.querySelector(controlQuery);
1103 if (legendAncestor && !embeddedControl) {
1108 if (!!cvox.DomPredicates.linkPredicate([node])) {
1112 // At this point, any non-layout tables are considered to have content.
1113 // For layout tables, it is safe to consider them as without content since the
1114 // sync operation would select a descendant of a layout table if possible. The
1115 // only instance where |hasContent| gets called on a layout table is if no
1116 // descendants have content (see |AbstractNodeWalker.next|).
1117 if (node.tagName == 'TABLE' && !cvox.DomUtil.isLayoutTable(node)) {
1121 // Math is always considered to have content.
1122 if (cvox.DomUtil.isMath(node)) {
1126 if (cvox.DomPredicates.headingPredicate([node])) {
1130 if (cvox.DomUtil.isFocusable(node)) {
1134 // Skip anything referenced by another element on the page
1135 // via aria-labelledby.
1136 var labelledByTargets = cvox.DomUtil.getLabelledByTargets();
1137 var enclosingNodeWithId = node;
1138 while (enclosingNodeWithId) {
1139 if (enclosingNodeWithId.id &&
1140 labelledByTargets[enclosingNodeWithId.id]) {
1141 // If we got here, some element on this page has an aria-labelledby
1142 // attribute listing this node as its id. As long as that "some" element
1143 // is not this element, we should return false, indicating this element
1144 // should be skipped.
1145 var attrValue = enclosingNodeWithId.getAttribute('aria-labelledby');
1147 var ids = attrValue.split(/ +/);
1148 if (ids.indexOf(enclosingNodeWithId.id) == -1) {
1155 enclosingNodeWithId = enclosingNodeWithId.parentElement;
1158 var text = cvox.DomUtil.getValue(node) + ' ' + cvox.DomUtil.getName(node);
1159 var state = cvox.DomUtil.getState(node, true);
1160 if (text.match(/^\s+$/) && state === '') {
1161 // Text only contains whitespace
1170 * Returns a list of all the ancestors of a given node. The last element
1171 * is the current node.
1173 * @param {Node} targetNode The node to get ancestors for.
1174 * @return {Array.<Node>} An array of ancestors for the targetNode.
1176 cvox.DomUtil.getAncestors = function(targetNode) {
1177 var ancestors = new Array();
1178 while (targetNode) {
1179 ancestors.push(targetNode);
1180 targetNode = targetNode.parentNode;
1182 ancestors.reverse();
1183 while (ancestors.length && !ancestors[0].tagName && !ancestors[0].nodeValue) {
1191 * Compares Ancestors of A with Ancestors of B and returns
1192 * the index value in B at which B diverges from A.
1193 * If there is no divergence, the result will be -1.
1194 * Note that if B is the same as A except B has more nodes
1195 * even after A has ended, that is considered a divergence.
1196 * The first node that B has which A does not have will
1197 * be treated as the divergence point.
1199 * @param {Object} ancestorsA The array of ancestors for Node A.
1200 * @param {Object} ancestorsB The array of ancestors for Node B.
1201 * @return {number} The index of the divergence point (the first node that B has
1202 * which A does not have in B's list of ancestors).
1204 cvox.DomUtil.compareAncestors = function(ancestorsA, ancestorsB) {
1206 while (ancestorsA[i] && ancestorsB[i] && (ancestorsA[i] == ancestorsB[i])) {
1209 if (!ancestorsA[i] && !ancestorsB[i]) {
1217 * Returns an array of ancestors that are unique for the currentNode when
1218 * compared to the previousNode. Having such an array is useful in generating
1219 * the node information (identifying when interesting node boundaries have been
1222 * @param {Node} previousNode The previous node.
1223 * @param {Node} currentNode The current node.
1224 * @param {boolean=} opt_fallback True returns node's ancestors in the case
1225 * where node's ancestors is a subset of previousNode's ancestors.
1226 * @return {Array.<Node>} An array of unique ancestors for the current node
1229 cvox.DomUtil.getUniqueAncestors = function(
1230 previousNode, currentNode, opt_fallback) {
1231 var prevAncestors = cvox.DomUtil.getAncestors(previousNode);
1232 var currentAncestors = cvox.DomUtil.getAncestors(currentNode);
1233 var divergence = cvox.DomUtil.compareAncestors(prevAncestors,
1235 var diff = currentAncestors.slice(divergence);
1236 return (diff.length == 0 && opt_fallback) ? currentAncestors : diff;
1241 * Returns a role message identifier for a node.
1242 * For a localized string, see cvox.DomUtil.getRole.
1243 * @param {Node} targetNode The node to get the role name for.
1244 * @param {number} verbosity The verbosity setting to use.
1245 * @return {string} The role message identifier for the targetNode.
1247 cvox.DomUtil.getRoleMsg = function(targetNode, verbosity) {
1249 info = cvox.AriaUtil.getRoleNameMsg(targetNode);
1251 if (targetNode.tagName == 'INPUT') {
1252 info = cvox.DomUtil.INPUT_TYPE_TO_INFORMATION_TABLE_MSG[targetNode.type];
1253 } else if (targetNode.tagName == 'A' &&
1254 cvox.DomUtil.isInternalLink(targetNode)) {
1255 info = 'internal_link';
1256 } else if (targetNode.tagName == 'A' &&
1257 targetNode.getAttribute('name')) {
1258 info = ''; // Don't want to add any role to anchors.
1259 } else if (targetNode.isContentEditable) {
1260 info = 'input_type_text';
1261 } else if (cvox.DomUtil.isMath(targetNode)) {
1263 } else if (targetNode.tagName == 'TABLE' &&
1264 cvox.DomUtil.isLayoutTable(targetNode)) {
1267 if (verbosity == cvox.VERBOSITY_BRIEF) {
1269 cvox.DomUtil.TAG_TO_INFORMATION_TABLE_BRIEF_MSG[targetNode.tagName];
1271 info = cvox.DomUtil.TAG_TO_INFORMATION_TABLE_VERBOSE_MSG[
1272 targetNode.tagName];
1274 if (cvox.DomUtil.hasLongDesc(targetNode)) {
1275 info = 'image_with_long_desc';
1278 if (!info && targetNode.onclick) {
1290 * Returns a string to be presented to the user that identifies what the
1291 * targetNode's role is.
1292 * ARIA roles are given priority; if there is no ARIA role set, the role
1293 * will be determined by the HTML tag for the node.
1295 * @param {Node} targetNode The node to get the role name for.
1296 * @param {number} verbosity The verbosity setting to use.
1297 * @return {string} The role name for the targetNode.
1299 cvox.DomUtil.getRole = function(targetNode, verbosity) {
1300 var roleMsg = cvox.DomUtil.getRoleMsg(targetNode, verbosity) || '';
1301 var role = roleMsg && roleMsg != ' ' ?
1302 cvox.ChromeVox.msgs.getMsg(roleMsg) : '';
1303 return role ? role : roleMsg;
1308 * Count the number of items in a list node.
1310 * @param {Node} targetNode The list node.
1311 * @return {number} The number of items in the list.
1313 cvox.DomUtil.getListLength = function(targetNode) {
1315 for (var node = targetNode.firstChild;
1317 node = node.nextSibling) {
1318 if (cvox.DomUtil.isVisible(node) &&
1319 (node.tagName == 'LI' ||
1320 (node.getAttribute && node.getAttribute('role') == 'listitem'))) {
1321 if (node.hasAttribute('aria-setsize')) {
1322 var ariaLength = parseInt(node.getAttribute('aria-setsize'), 10);
1323 if (!isNaN(ariaLength)) {
1335 * Returns a NodeState that gives information about the state of the targetNode.
1337 * @param {Node} targetNode The node to get the state information for.
1338 * @param {boolean} primary Whether this is the primary node we're
1339 * interested in, where we might want extra information - as
1340 * opposed to an ancestor, where we might be more brief.
1341 * @return {cvox.NodeState} The status information about the node.
1343 cvox.DomUtil.getStateMsgs = function(targetNode, primary) {
1344 var activeDescendant = cvox.AriaUtil.getActiveDescendant(targetNode);
1345 if (activeDescendant) {
1346 return cvox.DomUtil.getStateMsgs(activeDescendant, primary);
1349 var role = targetNode.getAttribute ? targetNode.getAttribute('role') : '';
1350 info = cvox.AriaUtil.getStateMsgs(targetNode, primary);
1355 if (targetNode.tagName == 'INPUT') {
1356 if (!targetNode.hasAttribute('aria-checked')) {
1358 'checkbox-true': 'checkbox_checked_state',
1359 'checkbox-false': 'checkbox_unchecked_state',
1360 'radio-true': 'radio_selected_state',
1361 'radio-false': 'radio_unselected_state' };
1362 var msgId = INPUT_MSGS[targetNode.type + '-' + !!targetNode.checked];
1367 } else if (targetNode.tagName == 'SELECT') {
1368 if (targetNode.selectedOptions && targetNode.selectedOptions.length <= 1) {
1369 info.push(['list_position',
1370 cvox.ChromeVox.msgs.getNumber(targetNode.selectedIndex + 1),
1371 cvox.ChromeVox.msgs.getNumber(targetNode.options.length)]);
1373 info.push(['selected_options_state',
1374 cvox.ChromeVox.msgs.getNumber(targetNode.selectedOptions.length)]);
1376 } else if (targetNode.tagName == 'UL' ||
1377 targetNode.tagName == 'OL' ||
1379 info.push(['list_with_items',
1380 cvox.ChromeVox.msgs.getNumber(
1381 cvox.DomUtil.getListLength(targetNode))]);
1384 if (cvox.DomUtil.isDisabled(targetNode)) {
1385 info.push(['aria_disabled_true']);
1388 if (cvox.DomPredicates.linkPredicate([targetNode]) &&
1389 cvox.ChromeVox.visitedUrls[targetNode.href]) {
1390 info.push(['visited_url']);
1393 if (targetNode.accessKey) {
1394 info.push(['access_key', targetNode.accessKey]);
1402 * Returns a string that gives information about the state of the targetNode.
1404 * @param {Node} targetNode The node to get the state information for.
1405 * @param {boolean} primary Whether this is the primary node we're
1406 * interested in, where we might want extra information - as
1407 * opposed to an ancestor, where we might be more brief.
1408 * @return {string} The status information about the node.
1410 cvox.DomUtil.getState = function(targetNode, primary) {
1411 return cvox.NodeStateUtil.expand(
1412 cvox.DomUtil.getStateMsgs(targetNode, primary));
1417 * Return whether a node is focusable. This includes nodes whose tabindex
1418 * attribute is set to "-1" explicitly - these nodes are not in the tab
1419 * order, but they should still be focused if the user navigates to them
1420 * using linear or smart DOM navigation.
1422 * Note that when the tabIndex property of an Element is -1, that doesn't
1423 * tell us whether the tabIndex attribute is missing or set to "-1" explicitly,
1424 * so we have to check the attribute.
1426 * @param {Object} targetNode The node to check if it's focusable.
1427 * @return {boolean} True if the node is focusable.
1429 cvox.DomUtil.isFocusable = function(targetNode) {
1430 if (!targetNode || typeof(targetNode.tabIndex) != 'number') {
1434 // Workaround for http://code.google.com/p/chromium/issues/detail?id=153904
1435 if ((targetNode.tagName == 'A') && !targetNode.hasAttribute('href') &&
1436 !targetNode.hasAttribute('tabindex')) {
1440 if (targetNode.tabIndex >= 0) {
1444 if (targetNode.hasAttribute &&
1445 targetNode.hasAttribute('tabindex') &&
1446 targetNode.getAttribute('tabindex') == '-1') {
1455 * Find a focusable descendant of a given node. This includes nodes whose
1456 * tabindex attribute is set to "-1" explicitly - these nodes are not in the
1457 * tab order, but they should still be focused if the user navigates to them
1458 * using linear or smart DOM navigation.
1460 * @param {Node} targetNode The node whose descendants to check if focusable.
1461 * @return {Node} The focusable descendant node. Null if no descendant node
1464 cvox.DomUtil.findFocusableDescendant = function(targetNode) {
1465 // Search down the descendants chain until a focusable node is found
1468 cvox.DomUtil.findNode(targetNode, cvox.DomUtil.isFocusable);
1469 if (focusableNode) {
1470 return focusableNode;
1478 * Returns the number of focusable nodes in root's subtree. The count does not
1481 * @param {Node} targetNode The node whose descendants to check are focusable.
1482 * @return {number} The number of focusable descendants.
1484 cvox.DomUtil.countFocusableDescendants = function(targetNode) {
1486 cvox.DomUtil.countNodes(targetNode, cvox.DomUtil.isFocusable) : 0;
1491 * Checks if the targetNode is still attached to the document.
1492 * A node can become detached because of AJAX changes.
1494 * @param {Object} targetNode The node to check.
1495 * @return {boolean} True if the targetNode is still attached.
1497 cvox.DomUtil.isAttachedToDocument = function(targetNode) {
1498 while (targetNode) {
1499 if (targetNode.tagName && (targetNode.tagName == 'HTML')) {
1502 targetNode = targetNode.parentNode;
1509 * Dispatches a left click event on the element that is the targetNode.
1510 * Clicks go in the sequence of mousedown, mouseup, and click.
1511 * @param {Node} targetNode The target node of this operation.
1512 * @param {boolean} shiftKey Specifies if shift is held down.
1513 * @param {boolean} callOnClickDirectly Specifies whether or not to directly
1514 * invoke the onclick method if there is one.
1515 * @param {boolean=} opt_double True to issue a double click.
1516 * @param {boolean=} opt_handleOwnEvents Whether to handle the generated
1517 * events through the normal event processing.
1519 cvox.DomUtil.clickElem = function(
1520 targetNode, shiftKey, callOnClickDirectly, opt_double,
1521 opt_handleOwnEvents) {
1522 // If there is an activeDescendant of the targetNode, then that is where the
1523 // click should actually be targeted.
1524 var activeDescendant = cvox.AriaUtil.getActiveDescendant(targetNode);
1525 if (activeDescendant) {
1526 targetNode = activeDescendant;
1528 if (callOnClickDirectly) {
1529 var onClickFunction = null;
1530 if (targetNode.onclick) {
1531 onClickFunction = targetNode.onclick;
1533 if (!onClickFunction && (targetNode.nodeType != 1) &&
1534 targetNode.parentNode && targetNode.parentNode.onclick) {
1535 onClickFunction = targetNode.parentNode.onclick;
1537 var keepGoing = true;
1538 if (onClickFunction) {
1540 keepGoing = onClickFunction();
1541 } catch (exception) {
1542 // Something went very wrong with the onclick method; we'll ignore it
1543 // and just dispatch a click event normally.
1547 // The onclick method ran successfully and returned false, meaning the
1548 // event should not bubble up, so we will return here.
1553 // Send a mousedown (or simply a double click if requested).
1554 var evt = document.createEvent('MouseEvents');
1555 var evtType = opt_double ? 'dblclick' : 'mousedown';
1556 evt.initMouseEvent(evtType, true, true, document.defaultView,
1557 1, 0, 0, 0, 0, false, false, shiftKey, false, 0, null);
1558 // Unless asked not to, Mark any events we generate so we don't try to
1559 // process our own events.
1560 evt.fromCvox = !opt_handleOwnEvents;
1562 targetNode.dispatchEvent(evt);
1565 evt = document.createEvent('MouseEvents');
1566 evt.initMouseEvent('mouseup', true, true, document.defaultView,
1567 1, 0, 0, 0, 0, false, false, shiftKey, false, 0, null);
1568 evt.fromCvox = !opt_handleOwnEvents;
1570 targetNode.dispatchEvent(evt);
1573 evt = document.createEvent('MouseEvents');
1574 evt.initMouseEvent('click', true, true, document.defaultView,
1575 1, 0, 0, 0, 0, false, false, shiftKey, false, 0, null);
1576 evt.fromCvox = !opt_handleOwnEvents;
1578 targetNode.dispatchEvent(evt);
1581 if (cvox.DomUtil.isInternalLink(targetNode)) {
1582 cvox.DomUtil.syncInternalLink(targetNode);
1588 * Syncs to an internal link.
1589 * @param {Node} node A link whose href's target we want to sync.
1591 cvox.DomUtil.syncInternalLink = function(node) {
1593 var targetId = node.href.split('#')[1];
1594 targetNode = document.getElementById(targetId);
1596 var nodes = document.getElementsByName(targetId);
1597 if (nodes.length > 0) {
1598 targetNode = nodes[0];
1602 // Insert a dummy node to adjust next Tab focus location.
1603 var parent = targetNode.parentNode;
1604 var dummyNode = document.createElement('div');
1605 dummyNode.setAttribute('tabindex', '-1');
1606 parent.insertBefore(dummyNode, targetNode);
1607 dummyNode.setAttribute('chromevoxignoreariahidden', 1);
1609 cvox.ChromeVox.syncToNode(targetNode, false);
1615 * Given an HTMLInputElement, returns true if it's an editable text type.
1616 * This includes input type='text' and input type='password' and a few
1619 * @param {Node} node The node to check.
1620 * @return {boolean} True if the node is an INPUT with an editable text type.
1622 cvox.DomUtil.isInputTypeText = function(node) {
1623 if (!node || node.constructor != HTMLInputElement) {
1627 switch (node.type) {
1644 * Given a node, returns true if it's a control. Controls are *not necessarily*
1645 * leaf-level given that some composite controls may have focusable children
1646 * if they are managing focus with tabindex:
1647 * ( http://www.w3.org/TR/2010/WD-wai-aria-practices-20100916/#visualfocus ).
1649 * @param {Node} node The node to check.
1650 * @return {boolean} True if the node is a control.
1652 cvox.DomUtil.isControl = function(node) {
1653 if (cvox.AriaUtil.isControlWidget(node) &&
1654 cvox.DomUtil.isFocusable(node)) {
1658 switch (node.tagName) {
1664 return node.type != 'hidden';
1667 if (node.isContentEditable) {
1675 * Given a node, returns true if it's a leaf-level control. This includes
1676 * composite controls thare are managing focus for children with
1677 * activedescendant, but not composite controls with focusable children:
1678 * ( http://www.w3.org/TR/2010/WD-wai-aria-practices-20100916/#visualfocus ).
1680 * @param {Node} node The node to check.
1681 * @return {boolean} True if the node is a leaf-level control.
1683 cvox.DomUtil.isLeafLevelControl = function(node) {
1684 if (cvox.DomUtil.isControl(node)) {
1685 return !(cvox.AriaUtil.isCompositeControl(node) &&
1686 cvox.DomUtil.findFocusableDescendant(node));
1693 * Given a node that might be inside of a composite control like a listbox,
1694 * return the surrounding control.
1695 * @param {Node} node The node from which to start looking.
1696 * @return {Node} The surrounding composite control node, or null if none.
1698 cvox.DomUtil.getSurroundingControl = function(node) {
1699 var surroundingControl = null;
1700 if (!cvox.DomUtil.isControl(node) && node.hasAttribute &&
1701 node.hasAttribute('role')) {
1702 surroundingControl = node.parentElement;
1703 while (surroundingControl &&
1704 !cvox.AriaUtil.isCompositeControl(surroundingControl)) {
1705 surroundingControl = surroundingControl.parentElement;
1708 return surroundingControl;
1713 * Given a node and a function for determining when to stop
1714 * descent, return the next leaf-like node.
1716 * @param {!Node} node The node from which to start looking,
1717 * this node *must not* be above document.body.
1718 * @param {boolean} r True if reversed. False by default.
1719 * @param {function(!Node):boolean} isLeaf A function that
1720 * returns true if we should stop descending.
1721 * @return {Node} The next leaf-like node or null if there is no next
1722 * leaf-like node. This function will always return a node below
1723 * document.body and never document.body itself.
1725 cvox.DomUtil.directedNextLeafLikeNode = function(node, r, isLeaf) {
1726 if (node != document.body) {
1727 // if not at the top of the tree, we want to find the next possible
1728 // branch forward in the dom, so we climb up the parents until we find a
1729 // node that has a nextSibling
1730 while (!cvox.DomUtil.directedNextSibling(node, r)) {
1734 // since node is never above document.body, it always has a parent.
1735 // so node.parentNode will never be null.
1736 node = /** @type {!Node} */(node.parentNode);
1737 if (node == document.body) {
1738 // we've readed the end of the document.
1742 if (cvox.DomUtil.directedNextSibling(node, r)) {
1743 // we just checked that next sibling is non-null.
1744 node = /** @type {!Node} */(cvox.DomUtil.directedNextSibling(node, r));
1747 // once we're at our next sibling, we want to descend down into it as
1748 // far as the child class will allow
1749 while (cvox.DomUtil.directedFirstChild(node, r) && !isLeaf(node)) {
1750 node = /** @type {!Node} */(cvox.DomUtil.directedFirstChild(node, r));
1753 // after we've done all that, if we are still at document.body, this must
1754 // be an empty document.
1755 if (node == document.body) {
1763 * Given a node, returns the next leaf node.
1765 * @param {!Node} node The node from which to start looking
1766 * for the next leaf node.
1767 * @param {boolean=} reverse True if reversed. False by default.
1768 * @return {Node} The next leaf node.
1769 * Null if there is no next leaf node.
1771 cvox.DomUtil.directedNextLeafNode = function(node, reverse) {
1772 reverse = !!reverse;
1773 return cvox.DomUtil.directedNextLeafLikeNode(
1774 node, reverse, cvox.DomUtil.isLeafNode);
1779 * Given a node, returns the previous leaf node.
1781 * @param {!Node} node The node from which to start looking
1782 * for the previous leaf node.
1783 * @return {Node} The previous leaf node.
1784 * Null if there is no previous leaf node.
1786 cvox.DomUtil.previousLeafNode = function(node) {
1787 return cvox.DomUtil.directedNextLeafNode(node, true);
1792 * Computes the outer most leaf node of a given node, depending on value
1793 * of the reverse flag r.
1794 * @param {!Node} node in the DOM.
1795 * @param {boolean} r True if reversed. False by default.
1796 * @param {function(!Node):boolean} pred Predicate to decide
1797 * what we consider a leaf.
1798 * @return {Node} The outer most leaf node of that node.
1800 cvox.DomUtil.directedFindFirstNode = function(node, r, pred) {
1801 var child = cvox.DomUtil.directedFirstChild(node, r);
1806 var leaf = cvox.DomUtil.directedFindFirstNode(child, r, pred);
1811 child = cvox.DomUtil.directedNextSibling(child, r);
1818 * Moves to the deepest node satisfying a given predicate under the given node.
1819 * @param {!Node} node in the DOM.
1820 * @param {boolean} r True if reversed. False by default.
1821 * @param {function(!Node):boolean} pred Predicate deciding what a leaf is.
1822 * @return {Node} The deepest node satisfying pred.
1824 cvox.DomUtil.directedFindDeepestNode = function(node, r, pred) {
1825 var next = cvox.DomUtil.directedFindFirstNode(node, r, pred);
1833 return cvox.DomUtil.directedFindDeepestNode(next, r, pred);
1839 * Computes the next node wrt. a predicate that is a descendant of ancestor.
1840 * @param {!Node} node in the DOM.
1841 * @param {!Node} ancestor of the given node.
1842 * @param {boolean} r True if reversed. False by default.
1843 * @param {function(!Node):boolean} pred Predicate to decide
1844 * what we consider a leaf.
1845 * @param {boolean=} above True if the next node can live in the subtree
1846 * directly above the start node. False by default.
1847 * @param {boolean=} deep True if we are looking for the next node that is
1848 * deepest in the tree. Otherwise the next shallow node is returned.
1850 * @return {Node} The next node in the DOM that satisfies the predicate.
1852 cvox.DomUtil.directedFindNextNode = function(
1853 node, ancestor, r, pred, above, deep) {
1856 if (!cvox.DomUtil.isDescendantOfNode(node, ancestor) || node == ancestor) {
1859 var next = cvox.DomUtil.directedNextSibling(node, r);
1861 if (!deep && pred(next)) {
1865 cvox.DomUtil.directedFindDeepestNode :
1866 cvox.DomUtil.directedFindFirstNode)(next, r, pred);
1870 if (deep && pred(next)) {
1873 next = cvox.DomUtil.directedNextSibling(next, r);
1875 var parent = /** @type {!Node} */(node.parentNode);
1876 if (above && pred(parent)) {
1879 return cvox.DomUtil.directedFindNextNode(
1880 parent, ancestor, r, pred, above, deep);
1885 * Get a string representing a control's value and state, i.e. the part
1886 * that changes while interacting with the control
1887 * @param {Element} control A control.
1888 * @return {string} The value and state string.
1890 cvox.DomUtil.getControlValueAndStateString = function(control) {
1891 var parentControl = cvox.DomUtil.getSurroundingControl(control);
1892 if (parentControl) {
1893 return cvox.DomUtil.collapseWhitespace(
1894 cvox.DomUtil.getValue(control) + ' ' +
1895 cvox.DomUtil.getName(control) + ' ' +
1896 cvox.DomUtil.getState(control, true));
1898 return cvox.DomUtil.collapseWhitespace(
1899 cvox.DomUtil.getValue(control) + ' ' +
1900 cvox.DomUtil.getState(control, true));
1906 * Determine whether the given node is an internal link.
1907 * @param {Node} node The node to be examined.
1908 * @return {boolean} True if the node is an internal link, false otherwise.
1910 cvox.DomUtil.isInternalLink = function(node) {
1911 if (node.nodeType == 1) { // Element nodes only.
1912 var href = node.getAttribute('href');
1913 if (href && href.indexOf('#') != -1) {
1914 var path = href.split('#')[0];
1915 return path == '' || path == window.location.pathname;
1923 * Get a string containing the currently selected link's URL.
1924 * @param {Node} node The link from which URL needs to be extracted.
1925 * @return {string} The value of the URL.
1927 cvox.DomUtil.getLinkURL = function(node) {
1928 if (node.tagName == 'A') {
1929 if (node.getAttribute('href')) {
1930 if (cvox.DomUtil.isInternalLink(node)) {
1931 return cvox.ChromeVox.msgs.getMsg('internal_link');
1933 return node.getAttribute('href');
1938 } else if (cvox.AriaUtil.getRoleName(node) ==
1939 cvox.ChromeVox.msgs.getMsg('aria_role_link')) {
1940 return cvox.ChromeVox.msgs.getMsg('unknown_link');
1948 * Checks if a given node is inside a table and returns the table node if it is
1949 * @param {Node} node The node.
1950 * @param {{allowCaptions: (undefined|boolean)}=} kwargs Optional named args.
1951 * allowCaptions: If true, will return true even if inside a caption. False
1953 * @return {Node} If the node is inside a table, the table node. Null if it
1956 cvox.DomUtil.getContainingTable = function(node, kwargs) {
1957 var ancestors = cvox.DomUtil.getAncestors(node);
1958 return cvox.DomUtil.findTableNodeInList(ancestors, kwargs);
1963 * Extracts a table node from a list of nodes.
1964 * @param {Array.<Node>} nodes The list of nodes.
1965 * @param {{allowCaptions: (undefined|boolean)}=} kwargs Optional named args.
1966 * allowCaptions: If true, will return true even if inside a caption. False
1968 * @return {Node} The table node if the list of nodes contains a table node.
1969 * Null if it does not.
1971 cvox.DomUtil.findTableNodeInList = function(nodes, kwargs) {
1972 kwargs = kwargs || {allowCaptions: false};
1973 // Don't include the caption node because it is actually rendered outside
1975 for (var i = nodes.length - 1, node; node = nodes[i]; i--) {
1976 if (node.constructor != Text) {
1977 if (!kwargs.allowCaptions && node.tagName == 'CAPTION') {
1980 if ((node.tagName == 'TABLE') || cvox.AriaUtil.isGrid(node)) {
1990 * Determines whether a given table is a data table or a layout table
1991 * @param {Node} tableNode The table node.
1992 * @return {boolean} If the table is a layout table, returns true. False
1995 cvox.DomUtil.isLayoutTable = function(tableNode) {
1996 // TODO(stoarca): Why are we returning based on this inaccurate heuristic
1997 // instead of first trying the better heuristics below?
1998 if (tableNode.rows && (tableNode.rows.length <= 1 ||
1999 (tableNode.rows[0].childElementCount == 1))) {
2000 // This table has either 0 or one rows, or only "one" column.
2001 // This is a quick check for column count and may not be accurate. See
2002 // TraverseTable.getW3CColCount_ for a more accurate
2003 // (but more complicated) way to determine column count.
2007 // These heuristics are adapted from the Firefox data and layout table.
2008 // heuristics: http://asurkov.blogspot.com/2011/10/data-vs-layout-table.html
2009 if (cvox.AriaUtil.isGrid(tableNode)) {
2010 // This table has an ARIA role identifying it as a grid.
2011 // Not a layout table.
2014 if (cvox.AriaUtil.isLandmark(tableNode)) {
2015 // This table has an ARIA landmark role - not a layout table.
2019 if (tableNode.caption || tableNode.summary) {
2020 // This table has a caption or a summary - not a layout table.
2024 if ((cvox.XpathUtil.evalXPath('tbody/tr/th', tableNode).length > 0) &&
2025 (cvox.XpathUtil.evalXPath('tbody/tr/td', tableNode).length > 0)) {
2026 // This table at least one column and at least one column header.
2027 // Not a layout table.
2031 if (cvox.XpathUtil.evalXPath('colgroup', tableNode).length > 0) {
2032 // This table specifies column groups - not a layout table.
2036 if ((cvox.XpathUtil.evalXPath('thead', tableNode).length > 0) ||
2037 (cvox.XpathUtil.evalXPath('tfoot', tableNode).length > 0)) {
2038 // This table has header or footer rows - not a layout table.
2042 if ((cvox.XpathUtil.evalXPath('tbody/tr/td/embed', tableNode).length > 0) ||
2043 (cvox.XpathUtil.evalXPath('tbody/tr/td/object', tableNode).length > 0) ||
2044 (cvox.XpathUtil.evalXPath('tbody/tr/td/iframe', tableNode).length > 0) ||
2045 (cvox.XpathUtil.evalXPath('tbody/tr/td/applet', tableNode).length > 0)) {
2046 // This table contains embed, object, applet, or iframe elements. It is
2051 // These heuristics are loosely based on Okada and Miura's "Detection of
2052 // Layout-Purpose TABLE Tags Based on Machine Learning" (2007).
2053 // http://books.google.com/books?id=kUbmdqasONwC&lpg=PA116&ots=Lb3HJ7dISZ&lr&pg=PA116
2055 // Increase the points for each heuristic. If there are 3 or more points,
2056 // this is probably a layout table.
2059 if (! cvox.DomUtil.hasBorder(tableNode)) {
2060 // This table has no border.
2064 if (tableNode.rows.length <= 6) {
2065 // This table has a limited number of rows.
2069 if (cvox.DomUtil.countPreviousTags(tableNode) <= 12) {
2070 // This table has a limited number of previous tags.
2074 if (cvox.XpathUtil.evalXPath('tbody/tr/td/table', tableNode).length > 0) {
2075 // This table has nested tables.
2078 return (points >= 3);
2083 * Count previous tags, which we dfine as the number of HTML tags that
2084 * appear before the given node.
2085 * @param {Node} node The given node.
2086 * @return {number} The number of previous tags.
2088 cvox.DomUtil.countPreviousTags = function(node) {
2089 var ancestors = cvox.DomUtil.getAncestors(node);
2090 return ancestors.length + cvox.DomUtil.countPreviousSiblings(node);
2095 * Counts previous siblings, not including text nodes.
2096 * @param {Node} node The given node.
2097 * @return {number} The number of previous siblings.
2099 cvox.DomUtil.countPreviousSiblings = function(node) {
2101 var prev = node.previousSibling;
2102 while (prev != null) {
2103 if (prev.constructor != Text) {
2106 prev = prev.previousSibling;
2113 * Whether a given table has a border or not.
2114 * @param {Node} tableNode The table node.
2115 * @return {boolean} If the table has a border, return true. False otherwise.
2117 cvox.DomUtil.hasBorder = function(tableNode) {
2118 // If .frame contains "void" there is no border.
2119 if (tableNode.frame) {
2120 return (tableNode.frame.indexOf('void') == -1);
2123 // If .border is defined and == "0" then there is no border.
2124 if (tableNode.border) {
2125 if (tableNode.border.length == 1) {
2126 return (tableNode.border != '0');
2128 return (tableNode.border.slice(0, -2) != 0);
2132 // If .style.border-style is 'none' there is no border.
2133 if (tableNode.style.borderStyle && tableNode.style.borderStyle == 'none') {
2137 // If .style.border-width is specified in units of length
2138 // ( https://developer.mozilla.org/en/CSS/border-width ) then we need
2139 // to check if .style.border-width starts with 0[px,em,etc]
2140 if (tableNode.style.borderWidth) {
2141 return (tableNode.style.borderWidth.slice(0, -2) != 0);
2144 // If .style.border-color is defined, then there is a border
2145 if (tableNode.style.borderColor) {
2153 * Return the first leaf node, starting at the top of the document.
2154 * @return {Node?} The first leaf node in the document, if found.
2156 cvox.DomUtil.getFirstLeafNode = function() {
2157 var node = document.body;
2158 while (node && node.firstChild) {
2159 node = node.firstChild;
2161 while (node && !cvox.DomUtil.hasContent(node)) {
2162 node = cvox.DomUtil.directedNextLeafNode(node);
2169 * Finds the first descendant node that matches the filter function, using
2170 * a depth first search. This function offers the most general purpose way
2171 * of finding a matching element. You may also wish to consider
2172 * {@code goog.dom.query} which can express many matching criteria using
2173 * CSS selector expressions. These expressions often result in a more
2174 * compact representation of the desired result.
2175 * This is the findNode function from goog.dom:
2176 * http://code.google.com/p/closure-library/source/browse/trunk/closure/goog/dom/dom.js
2178 * @param {Node} root The root of the tree to search.
2179 * @param {function(Node) : boolean} p The filter function.
2180 * @return {Node|undefined} The found node or undefined if none is found.
2182 cvox.DomUtil.findNode = function(root, p) {
2184 var found = cvox.DomUtil.findNodes_(root, p, rv, true, 10000);
2185 return found ? rv[0] : undefined;
2190 * Finds the number of nodes matching the filter.
2191 * @param {Node} root The root of the tree to search.
2192 * @param {function(Node) : boolean} p The filter function.
2193 * @return {number} The number of nodes selected by filter.
2195 cvox.DomUtil.countNodes = function(root, p) {
2197 cvox.DomUtil.findNodes_(root, p, rv, false, 10000);
2203 * Finds the first or all the descendant nodes that match the filter function,
2204 * using a depth first search.
2205 * @param {Node} root The root of the tree to search.
2206 * @param {function(Node) : boolean} p The filter function.
2207 * @param {Array.<Node>} rv The found nodes are added to this array.
2208 * @param {boolean} findOne If true we exit after the first found node.
2209 * @param {number} maxChildCount The max child count. This is used as a kill
2210 * switch - if there are more nodes than this, terminate the search.
2211 * @return {boolean} Whether the search is complete or not. True in case
2212 * findOne is true and the node is found. False otherwise. This is the
2213 * findNodes_ function from goog.dom:
2214 * http://code.google.com/p/closure-library/source/browse/trunk/closure/goog/dom/dom.js.
2217 cvox.DomUtil.findNodes_ = function(root, p, rv, findOne, maxChildCount) {
2218 if ((root != null) || (maxChildCount == 0)) {
2219 var child = root.firstChild;
2227 maxChildCount = maxChildCount - 1;
2228 if (cvox.DomUtil.findNodes_(child, p, rv, findOne, maxChildCount)) {
2231 child = child.nextSibling;
2239 * Converts a NodeList into an array
2240 * @param {NodeList} nodeList The nodeList.
2241 * @return {Array} The array of nodes in the nodeList.
2243 cvox.DomUtil.toArray = function(nodeList) {
2245 for (var i = 0; i < nodeList.length; i++) {
2246 nodeArray.push(nodeList[i]);
2253 * Creates a new element with the same attributes and no children.
2254 * @param {Node|Text} node A node to clone.
2255 * @param {Object.<string, boolean>} skipattrs Set the attribute to true to
2256 * skip it during cloning.
2257 * @return {Node|Text} The cloned node.
2259 cvox.DomUtil.shallowChildlessClone = function(node, skipattrs) {
2260 if (node.nodeName == '#text') {
2261 return document.createTextNode(node.nodeValue);
2264 if (node.nodeName == '#comment') {
2265 return document.createComment(node.nodeValue);
2268 var ret = document.createElement(node.nodeName);
2269 for (var i = 0; i < node.attributes.length; ++i) {
2270 var attr = node.attributes[i];
2271 if (skipattrs && skipattrs[attr.nodeName]) {
2274 ret.setAttribute(attr.nodeName, attr.nodeValue);
2281 * Creates a new element with the same attributes and clones of children.
2282 * @param {Node|Text} node A node to clone.
2283 * @param {Object.<string, boolean>} skipattrs Set the attribute to true to
2284 * skip it during cloning.
2285 * @return {Node|Text} The cloned node.
2287 cvox.DomUtil.deepClone = function(node, skipattrs) {
2288 var ret = cvox.DomUtil.shallowChildlessClone(node, skipattrs);
2289 for (var i = 0; i < node.childNodes.length; ++i) {
2290 ret.appendChild(cvox.DomUtil.deepClone(node.childNodes[i], skipattrs));
2297 * Returns either node.firstChild or node.lastChild, depending on direction.
2298 * @param {Node|Text} node The node.
2299 * @param {boolean} reverse If reversed.
2300 * @return {Node|Text} The directed first child or null if the node has
2303 cvox.DomUtil.directedFirstChild = function(node, reverse) {
2305 return node.lastChild;
2307 return node.firstChild;
2311 * Returns either node.nextSibling or node.previousSibling, depending on
2313 * @param {Node|Text} node The node.
2314 * @param {boolean=} reverse If reversed.
2315 * @return {Node|Text} The directed next sibling or null if there are
2316 * no more siblings in that direction.
2318 cvox.DomUtil.directedNextSibling = function(node, reverse) {
2323 return node.previousSibling;
2325 return node.nextSibling;
2329 * Creates a function that sends a click. This is because loop closures
2331 * See: http://joust.kano.net/weblog/archive/2005/08/08/
2332 * a-huge-gotcha-with-javascript-closures/
2333 * @param {Node} targetNode The target node to click on.
2334 * @return {function()} A function that will click on the given targetNode.
2336 cvox.DomUtil.createSimpleClickFunction = function(targetNode) {
2337 var target = targetNode.cloneNode(true);
2338 return function() { cvox.DomUtil.clickElem(target, false, false); };
2342 * Adds a node to document.head if that node has not already been added.
2343 * If document.head does not exist, this will add the node to the body.
2344 * @param {Node} node The node to add.
2345 * @param {string=} opt_id The id of the node to ensure the node is only
2348 cvox.DomUtil.addNodeToHead = function(node, opt_id) {
2349 if (opt_id && document.getElementById(opt_id)) {
2352 var p = document.head || document.body;
2353 p.appendChild(node);
2358 * Checks if a given node is inside a math expressions and
2359 * returns the math node if one exists.
2360 * @param {Node} node The node.
2361 * @return {Node} The math node, if the node is inside a math expression.
2362 * Null if it is not.
2364 cvox.DomUtil.getContainingMath = function(node) {
2365 var ancestors = cvox.DomUtil.getAncestors(node);
2366 return cvox.DomUtil.findMathNodeInList(ancestors);
2371 * Extracts a math node from a list of nodes.
2372 * @param {Array.<Node>} nodes The list of nodes.
2373 * @return {Node} The math node if the list of nodes contains a math node.
2374 * Null if it does not.
2376 cvox.DomUtil.findMathNodeInList = function(nodes) {
2377 for (var i = 0, node; node = nodes[i]; i++) {
2378 if (cvox.DomUtil.isMath(node)) {
2387 * Checks to see wether a node is a math node.
2388 * @param {Node} node The node to be tested.
2389 * @return {boolean} Whether or not a node is a math node.
2391 cvox.DomUtil.isMath = function(node) {
2392 return cvox.DomUtil.isMathml(node) ||
2393 cvox.DomUtil.isMathJax(node) ||
2394 cvox.DomUtil.isMathImg(node) ||
2395 cvox.AriaUtil.isMath(node);
2400 * Specifies node classes in which we expect maths expressions a alt text.
2401 * @type {{tex: Array.<string>,
2402 * asciimath: Array.<string>}}
2404 // These are the classes for which we assume they contain Maths in the ALT or
2407 // latex: Wordpress;
2408 // numberedequation, inlineformula, displayformula: MathWorld;
2409 cvox.DomUtil.ALT_MATH_CLASSES = {
2410 tex: ['tex', 'latex'],
2411 asciimath: ['numberedequation', 'inlineformula', 'displayformula']
2416 * Composes a query selector string for image nodes with alt math content by
2418 * @param {string} contentType The content type, e.g., tex, asciimath.
2419 * @return {!string} The query elector string.
2421 cvox.DomUtil.altMathQuerySelector = function(contentType) {
2422 var classes = cvox.DomUtil.ALT_MATH_CLASSES[contentType];
2424 return classes.map(function(x) {return 'img.' + x;}).join(', ');
2431 * Check if a given node is potentially a math image with alternative text in
2433 * @param {Node} node The node to be tested.
2434 * @return {boolean} Whether or not a node has an image with class TeX or LaTeX.
2436 cvox.DomUtil.isMathImg = function(node) {
2437 if (!node || !node.tagName || !node.className) {
2440 if (node.tagName != 'IMG') {
2443 var className = node.className.toLowerCase();
2444 return cvox.DomUtil.ALT_MATH_CLASSES.tex.indexOf(className) != -1 ||
2445 cvox.DomUtil.ALT_MATH_CLASSES.asciimath.indexOf(className) != -1;
2450 * Checks to see whether a node is a MathML node.
2451 * !! This is necessary as Chrome currently does not upperCase Math tags !!
2452 * @param {Node} node The node to be tested.
2453 * @return {boolean} Whether or not a node is a MathML node.
2455 cvox.DomUtil.isMathml = function(node) {
2456 if (!node || !node.tagName) {
2459 return node.tagName.toLowerCase() == 'math';
2464 * Checks to see wether a node is a MathJax node.
2465 * @param {Node} node The node to be tested.
2466 * @return {boolean} Whether or not a node is a MathJax node.
2468 cvox.DomUtil.isMathJax = function(node) {
2469 if (!node || !node.tagName || !node.className) {
2473 function isSpanWithClass(n, cl) {
2474 return (n.tagName == 'SPAN' &&
2475 n.className.split(' ').some(function(x) {
2476 return x.toLowerCase() == cl;}));
2478 if (isSpanWithClass(node, 'math')) {
2479 var ancestors = cvox.DomUtil.getAncestors(node);
2480 return ancestors.some(function(x) {return isSpanWithClass(x, 'mathjax');});
2487 * Computes the id of the math span in a MathJax DOM element.
2488 * @param {string} jaxId The id of the MathJax node.
2489 * @return {string} The id of the span node.
2491 cvox.DomUtil.getMathSpanId = function(jaxId) {
2492 var node = document.getElementById(jaxId + '-Frame');
2494 var span = node.querySelector('span.math');
2503 * Returns true if the node has a longDesc.
2504 * @param {Node} node The node to be tested.
2505 * @return {boolean} Whether or not a node has a longDesc.
2507 cvox.DomUtil.hasLongDesc = function(node) {
2508 if (node && node.longDesc) {
2516 * Returns tag name of a node if it has one.
2517 * @param {Node} node A node.
2518 * @return {string} A the tag name of the node.
2520 cvox.DomUtil.getNodeTagName = function(node) {
2521 if (node.nodeType == Node.ELEMENT_NODE) {
2522 return node.tagName;
2529 * Cleaning up a list of nodes to remove empty text nodes.
2530 * @param {NodeList} nodes The nodes list.
2531 * @return {!Array.<Node|string|null>} The cleaned up list of nodes.
2533 cvox.DomUtil.purgeNodes = function(nodes) {
2534 return cvox.DomUtil.toArray(nodes).
2535 filter(function(node) {
2536 return node.nodeType != Node.TEXT_NODE ||
2537 !node.textContent.match(/^\s+$/);});
2542 * Calculates a hit point for a given node.
2543 * @return {{x:(number), y:(number)}} The position.
2545 cvox.DomUtil.elementToPoint = function(node) {
2547 return {x: 0, y: 0};
2549 if (node.constructor == Text) {
2550 node = node.parentNode;
2552 var r = node.getBoundingClientRect();
2554 x: r.left + (r.width / 2),
2555 y: r.top + (r.height / 2)
2561 * Checks if an input node supports HTML5 selection.
2562 * If the node is not an input element, returns false.
2563 * @param {Node} node The node to check.
2564 * @return {boolean} True if HTML5 selection supported.
2566 cvox.DomUtil.doesInputSupportSelection = function(node) {
2567 return goog.isDef(node) &&
2568 node.tagName == 'INPUT' &&
2569 node.type != 'email' &&
2570 node.type != 'number';
2575 * Gets the hint text for a given element.
2576 * @param {Node} node The target node.
2577 * @return {string} The hint text.
2579 cvox.DomUtil.getHint = function(node) {
2581 if (node.hasAttribute) {
2582 if (node.hasAttribute('aria-describedby')) {
2583 var describedByIds = node.getAttribute('aria-describedby').split(' ');
2584 for (var describedById, i = 0; describedById = describedByIds[i]; i++) {
2585 var describedNode = document.getElementById(describedById);
2586 if (describedNode) {
2587 desc += ' ' + cvox.DomUtil.getName(
2588 describedNode, true, true, true);