Home | History | Annotate | Download | only in common
      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.
      4 
      5 /**
      6  * @fileoverview A collection of JavaScript utilities used to simplify working
      7  * with the DOM.
      8  */
      9 
     10 
     11 goog.provide('cvox.DomUtil');
     12 
     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');
     20 
     21 
     22 
     23 /**
     24  * Create the namespace
     25  * @constructor
     26  */
     27 cvox.DomUtil = function() {
     28 };
     29 
     30 
     31 /**
     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.
     35  * @type {Object}
     36  */
     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'
     59 };
     60 
     61 
     62 /**
     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.
     66  * @type {Object}
     67  */
     68 cvox.DomUtil.TAG_TO_INFORMATION_TABLE_VERBOSE_MSG = {
     69   'A' : 'tag_link',
     70   'ARTICLE' : 'tag_article',
     71   'ASIDE' : 'tag_aside',
     72   'AUDIO' : 'tag_audio',
     73   'BUTTON' : 'tag_button',
     74   'FOOTER' : 'tag_footer',
     75   'H1' : 'tag_h1',
     76   'H2' : 'tag_h2',
     77   'H3' : 'tag_h3',
     78   'H4' : 'tag_h4',
     79   'H5' : 'tag_h5',
     80   'H6' : 'tag_h6',
     81   'HEADER' : 'tag_header',
     82   'HGROUP' : 'tag_hgroup',
     83   'LI' : 'tag_li',
     84   'MARK' : 'tag_mark',
     85   'NAV' : 'tag_nav',
     86   'OL' : 'tag_ol',
     87   'SECTION' : 'tag_section',
     88   'SELECT' : 'tag_select',
     89   'TABLE' : 'tag_table',
     90   'TEXTAREA' : 'tag_textarea',
     91   'TIME' : 'tag_time',
     92   'UL' : 'tag_ul',
     93   'VIDEO' : 'tag_video'
     94 };
     95 
     96 /**
     97  * ChromeVox does not speak the omitted tags.
     98  * @type {Object}
     99  */
    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'
    107 };
    108 
    109 /**
    110  * These tags are treated as text formatters.
    111  * @type {Array.<string>}
    112  */
    113 cvox.DomUtil.FORMATTING_TAGS =
    114     ['B', 'BIG', 'CITE', 'CODE', 'DFN', 'EM', 'I', 'KBD', 'SAMP', 'SMALL',
    115      'SPAN', 'STRIKE', 'STRONG', 'SUB', 'SUP', 'U', 'VAR'];
    116 
    117 /**
    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.
    133  */
    134 cvox.DomUtil.isVisible = function(node, opt_options) {
    135   var checkAncestors = true;
    136   var checkDescendants = true;
    137   if (opt_options) {
    138     if (opt_options.checkAncestors !== undefined) {
    139       checkAncestors = opt_options.checkAncestors;
    140     }
    141     if (opt_options.checkDescendants !== undefined) {
    142       checkDescendants = opt_options.checkDescendants;
    143     }
    144   }
    145 
    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
    149   // of the same node.
    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));
    154 };
    155 
    156 /**
    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.
    164  * @private
    165  */
    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) {
    170     return false;
    171   }
    172 
    173   // If the node is being forced visible by ARIA, ARIA wins.
    174   if (cvox.AriaUtil.isForcedVisibleRecursive(node)) {
    175     return true;
    176   }
    177 
    178   // Confirm that no subtree containing node is invisible.
    179   if (checkAncestors &&
    180       cvox.DomUtil.hasInvisibleAncestor_(node)) {
    181     return false;
    182   }
    183 
    184   // If the node's subtree has a visible node, we declare it as visible.
    185   if (cvox.DomUtil.hasVisibleNodeSubtree_(node, checkDescendants)) {
    186     return true;
    187   }
    188 
    189   return false;
    190 };
    191 
    192 
    193 /**
    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.
    200  * @private
    201  */
    202 cvox.DomUtil.hasInvisibleAncestor_ = function(node) {
    203   var ancestor = node;
    204   while (ancestor = ancestor.parentElement) {
    205     var style = document.defaultView.getComputedStyle(ancestor, null);
    206     if (cvox.DomUtil.isInvisibleStyle(style, true)) {
    207       return true;
    208     }
    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
    212     // invisible.
    213     if (ancestor == document.documentElement) {
    214       return false;
    215     }
    216   }
    217   return true;
    218 };
    219 
    220 
    221 /**
    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.
    229  * @private
    230  */
    231 cvox.DomUtil.hasVisibleNodeSubtree_ = function(root, recursive) {
    232   if (!(root instanceof Element)) {
    233     var parentStyle = document.defaultView
    234         .getComputedStyle(root.parentElement, null);
    235     var isVisibleParent = !cvox.DomUtil.isInvisibleStyle(parentStyle);
    236     return isVisibleParent;
    237   }
    238 
    239   var rootStyle = document.defaultView.getComputedStyle(root, null);
    240   var isRootVisible = !cvox.DomUtil.isInvisibleStyle(rootStyle);
    241   if (isRootVisible) {
    242     return true;
    243   }
    244   var isSubtreeInvisible = cvox.DomUtil.isInvisibleStyle(rootStyle, true);
    245   if (!recursive || isSubtreeInvisible) {
    246     return false;
    247   }
    248 
    249   // Carry on with a recursive check of the descendants.
    250   var children = root.childNodes;
    251   for (var i = 0; i < children.length; i++) {
    252     var child = children[i];
    253     if (cvox.DomUtil.hasVisibleNodeSubtree_(child, recursive)) {
    254       return true;
    255     }
    256   }
    257   return false;
    258 };
    259 
    260 
    261 /**
    262  * Determines whether or a node is not visible according to any CSS criteria
    263  * that can hide it.
    264  * @param {CSSStyleDeclaration} style The style of the node to determine as
    265  *     invsible or not.
    266  * @param {boolean=} opt_strict If set to true, we do not check the visibility
    267  *     style attribute. False by default.
    268  * CAUTION: Checking the visibility style attribute can result in returning
    269  *     true (invisible) even when an element has have visible descendants. This
    270  *     is because an element with visibility:hidden can have descendants that
    271  *     are visible.
    272  * @return {boolean} True if the node is invisible.
    273  */
    274 cvox.DomUtil.isInvisibleStyle = function(style, opt_strict) {
    275   if (!style) {
    276     return false;
    277   }
    278   if (style.display == 'none') {
    279     return true;
    280   }
    281   // Opacity values range from 0.0 (transparent) to 1.0 (fully opaque).
    282   if (parseFloat(style.opacity) == 0) {
    283     return true;
    284   }
    285   // Visibility style tests for non-strict checking.
    286   if (!opt_strict &&
    287       (style.visibility == 'hidden' || style.visibility == 'collapse')) {
    288     return true;
    289   }
    290   return false;
    291 };
    292 
    293 
    294 /**
    295  * Determines whether a control should be announced as disabled.
    296  *
    297  * @param {Node} node The node to be examined.
    298  * @return {boolean} Whether or not the node is disabled.
    299  */
    300 cvox.DomUtil.isDisabled = function(node) {
    301   if (node.disabled) {
    302     return true;
    303   }
    304   var ancestor = node;
    305   while (ancestor = ancestor.parentElement) {
    306     if (ancestor.tagName == 'FIELDSET' && ancestor.disabled) {
    307       return true;
    308     }
    309   }
    310   return false;
    311 };
    312 
    313 
    314 /**
    315  * Determines whether a node is an HTML5 semantic element
    316  *
    317  * @param {Node} node The node to be checked.
    318  * @return {boolean} True if the node is an HTML5 semantic element.
    319  */
    320 cvox.DomUtil.isSemanticElt = function(node) {
    321   if (node.tagName) {
    322     var tag = node.tagName;
    323     if ((tag == 'SECTION') || (tag == 'NAV') || (tag == 'ARTICLE') ||
    324         (tag == 'ASIDE') || (tag == 'HGROUP') || (tag == 'HEADER') ||
    325         (tag == 'FOOTER') || (tag == 'TIME') || (tag == 'MARK')) {
    326       return true;
    327     }
    328   }
    329   return false;
    330 };
    331 
    332 
    333 /**
    334  * Determines whether or not a node is a leaf node.
    335  * TODO (adu): This function is doing a lot more than just checking for the
    336  *     presence of descendants. We should be more precise in the documentation
    337  *     about what we mean by leaf node.
    338  *
    339  * @param {Node} node The node to be checked.
    340  * @param {boolean=} opt_allowHidden Allows hidden nodes during descent.
    341  * @return {boolean} True if the node is a leaf node.
    342  */
    343 cvox.DomUtil.isLeafNode = function(node, opt_allowHidden) {
    344   // If it's not an Element, then it's a leaf if it has no first child.
    345   if (!(node instanceof Element)) {
    346     return (node.firstChild == null);
    347   }
    348 
    349   // Now we know for sure it's an element.
    350   var element = /** @type {Element} */(node);
    351   if (!opt_allowHidden &&
    352       !cvox.DomUtil.isVisible(element, {checkAncestors: false})) {
    353     return true;
    354   }
    355   if (!opt_allowHidden && cvox.AriaUtil.isHidden(element)) {
    356     return true;
    357   }
    358   if (cvox.AriaUtil.isLeafElement(element)) {
    359     return true;
    360   }
    361   switch (element.tagName) {
    362     case 'OBJECT':
    363     case 'EMBED':
    364     case 'VIDEO':
    365     case 'AUDIO':
    366     case 'IFRAME':
    367     case 'FRAME':
    368       return true;
    369   }
    370 
    371   if (!!cvox.DomPredicates.linkPredicate([element])) {
    372     return !cvox.DomUtil.findNode(element, function(node) {
    373       return !!cvox.DomPredicates.headingPredicate([node]);
    374     });
    375   }
    376   if (cvox.DomUtil.isLeafLevelControl(element)) {
    377     return true;
    378   }
    379   if (!element.firstChild) {
    380     return true;
    381   }
    382   if (cvox.DomUtil.isMath(element)) {
    383     return true;
    384   }
    385   if (cvox.DomPredicates.headingPredicate([element])) {
    386     return !cvox.DomUtil.findNode(element, function(n) {
    387       return !!cvox.DomPredicates.controlPredicate([n]);
    388     });
    389   }
    390   return false;
    391 };
    392 
    393 
    394 /**
    395  * Determines whether or not a node is or is the descendant of a node
    396  * with a particular tag or class name.
    397  *
    398  * @param {Node} node The node to be checked.
    399  * @param {?string} tagName The tag to check for, or null if the tag
    400  * doesn't matter.
    401  * @param {?string=} className The class to check for, or null if the class
    402  * doesn't matter.
    403  * @return {boolean} True if the node or one of its ancestor has the specified
    404  * tag.
    405  */
    406 cvox.DomUtil.isDescendantOf = function(node, tagName, className) {
    407   while (node) {
    408 
    409     if (tagName && className &&
    410         (node.tagName && (node.tagName == tagName)) &&
    411         (node.className && (node.className == className))) {
    412       return true;
    413     } else if (tagName && !className &&
    414                (node.tagName && (node.tagName == tagName))) {
    415       return true;
    416     } else if (!tagName && className &&
    417                (node.className && (node.className == className))) {
    418       return true;
    419     }
    420     node = node.parentNode;
    421   }
    422   return false;
    423 };
    424 
    425 
    426 /**
    427  * Determines whether or not a node is or is the descendant of another node.
    428  *
    429  * @param {Object} node The node to be checked.
    430  * @param {Object} ancestor The node to see if it's a descendant of.
    431  * @return {boolean} True if the node is ancestor or is a descendant of it.
    432  */
    433 cvox.DomUtil.isDescendantOfNode = function(node, ancestor) {
    434   while (node && ancestor) {
    435     if (node.isSameNode(ancestor)) {
    436       return true;
    437     }
    438     node = node.parentNode;
    439   }
    440   return false;
    441 };
    442 
    443 
    444 /**
    445  * Remove all whitespace from the beginning and end, and collapse all
    446  * inner strings of whitespace to a single space.
    447  * @param {string} str The input string.
    448  * @return {string} The string with whitespace collapsed.
    449  */
    450 cvox.DomUtil.collapseWhitespace = function(str) {
    451   return str.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '');
    452 };
    453 
    454 /**
    455  * Gets the base label of a node. I don't know exactly what this is.
    456  *
    457  * @param {Node} node The node to get the label from.
    458  * @param {boolean=} recursive Whether or not the element's subtree
    459  *  should be used; true by default.
    460  * @param {boolean=} includeControls Whether or not controls in the subtree
    461  *  should be included; true by default.
    462  * @return {string} The base label of the node.
    463  * @private
    464  */
    465 cvox.DomUtil.getBaseLabel_ = function(node, recursive, includeControls) {
    466   var label = '';
    467   if (node.hasAttribute) {
    468     if (node.hasAttribute('aria-labelledby')) {
    469       var labelNodeIds = node.getAttribute('aria-labelledby').split(' ');
    470       for (var labelNodeId, i = 0; labelNodeId = labelNodeIds[i]; i++) {
    471         var labelNode = document.getElementById(labelNodeId);
    472         if (labelNode) {
    473           label += ' ' + cvox.DomUtil.getName(
    474               labelNode, true, includeControls, true);
    475         }
    476       }
    477     } else if (node.hasAttribute('aria-label')) {
    478       label = node.getAttribute('aria-label');
    479     } else if (node.constructor == HTMLImageElement) {
    480       label = cvox.DomUtil.getImageTitle(node);
    481     } else if (node.tagName == 'FIELDSET') {
    482       // Other labels will trump fieldset legend with this implementation.
    483       // Depending on how this works out on the web, we may later switch this
    484       // to appending the fieldset legend to any existing label.
    485       var legends = node.getElementsByTagName('LEGEND');
    486       label = '';
    487       for (var legend, i = 0; legend = legends[i]; i++) {
    488         label += ' ' + cvox.DomUtil.getName(legend, true, includeControls);
    489       }
    490     }
    491 
    492     if (label.length == 0 && node && node.id) {
    493       var labelFor = document.querySelector('label[for="' + node.id + '"]');
    494       if (labelFor) {
    495         label = cvox.DomUtil.getName(labelFor, recursive, includeControls);
    496       }
    497     }
    498   }
    499   return cvox.DomUtil.collapseWhitespace(label);
    500 };
    501 
    502 /**
    503  * Gets the nearest label in the ancestor chain, if one exists.
    504  * @param {Node} node The node to start from.
    505  * @return {string} The label.
    506  * @private
    507  */
    508 cvox.DomUtil.getNearestAncestorLabel_ = function(node) {
    509   var label = '';
    510   var enclosingLabel = node;
    511   while (enclosingLabel && enclosingLabel.tagName != 'LABEL') {
    512     enclosingLabel = enclosingLabel.parentElement;
    513   }
    514   if (enclosingLabel && !enclosingLabel.hasAttribute('for')) {
    515     // Get all text from the label but don't include any controls.
    516     label = cvox.DomUtil.getName(enclosingLabel, true, false);
    517   }
    518   return label;
    519 };
    520 
    521 
    522 /**
    523  * Gets the name for an input element.
    524  * @param {Node} node The node.
    525  * @return {string} The name.
    526  * @private
    527  */
    528 cvox.DomUtil.getInputName_ = function(node) {
    529   var label = '';
    530   if (node.type == 'image') {
    531     label = cvox.DomUtil.getImageTitle(node);
    532   } else if (node.type == 'submit') {
    533     if (node.hasAttribute('value')) {
    534       label = node.getAttribute('value');
    535     } else {
    536       label = 'Submit';
    537     }
    538   } else if (node.type == 'reset') {
    539     if (node.hasAttribute('value')) {
    540       label = node.getAttribute('value');
    541     } else {
    542       label = 'Reset';
    543     }
    544   } else if (node.type == 'button') {
    545     if (node.hasAttribute('value')) {
    546       label = node.getAttribute('value');
    547     }
    548   }
    549   return label;
    550 };
    551 
    552 /**
    553  * Wraps getName_ with marking and unmarking nodes so that infinite loops
    554  * don't occur. This is the ugly way to solve this; getName should not ever
    555  * do a recursive call somewhere above it in the tree.
    556  * @param {Node} node See getName_.
    557  * @param {boolean=} recursive See getName_.
    558  * @param {boolean=} includeControls See getName_.
    559  * @param {boolean=} opt_allowHidden Allows hidden nodes in name computation.
    560  * @return {string} See getName_.
    561  */
    562 cvox.DomUtil.getName = function(
    563     node, recursive, includeControls, opt_allowHidden) {
    564   if (!node || node.cvoxGetNameMarked == true) {
    565     return '';
    566   }
    567   node.cvoxGetNameMarked = true;
    568   var ret =
    569       cvox.DomUtil.getName_(node, recursive, includeControls, opt_allowHidden);
    570   node.cvoxGetNameMarked = false;
    571   var prefix = cvox.DomUtil.getPrefixText(node);
    572   return prefix + ret;
    573 };
    574 
    575 // TODO(dtseng): Seems like this list should be longer...
    576 /**
    577  * Determines if a node has a name obtained from concatinating the names of its
    578  * children.
    579  * @param {!Node} node The node under consideration.
    580  * @param {boolean=} opt_allowHidden Allows hidden nodes in name computation.
    581  * @return {boolean} True if node has name based on children.
    582  * @private
    583  */
    584 cvox.DomUtil.hasChildrenBasedName_ = function(node, opt_allowHidden) {
    585   if (!!cvox.DomPredicates.linkPredicate([node]) ||
    586       !!cvox.DomPredicates.headingPredicate([node]) ||
    587       node.tagName == 'BUTTON' ||
    588       cvox.AriaUtil.isControlWidget(node) ||
    589       !cvox.DomUtil.isLeafNode(node, opt_allowHidden)) {
    590     return true;
    591   } else {
    592     return false;
    593   }
    594 };
    595 
    596 /**
    597  * Get the name of a node: this includes all static text content and any
    598  * HTML-author-specified label, title, alt text, aria-label, etc. - but
    599  * does not include:
    600  * - the user-generated control value (use getValue)
    601  * - the current state (use getState)
    602  * - the role (use getRole)
    603  *
    604  * Order of precedence:
    605  *   Text content if it's a text node.
    606  *   aria-labelledby
    607  *   aria-label
    608  *   alt (for an image)
    609  *   title
    610  *   label (for a control)
    611  *   placeholder (for an input element)
    612  *   recursive calls to getName on all children
    613  *
    614  * @param {Node} node The node to get the name from.
    615  * @param {boolean=} recursive Whether or not the element's subtree should
    616  *     be used; true by default.
    617  * @param {boolean=} includeControls Whether or not controls in the subtree
    618  *     should be included; true by default.
    619  * @param {boolean=} opt_allowHidden Allows hidden nodes in name computation.
    620  * @return {string} The name of the node.
    621  * @private
    622  */
    623 cvox.DomUtil.getName_ = function(
    624     node, recursive, includeControls, opt_allowHidden) {
    625   if (typeof(recursive) === 'undefined') {
    626     recursive = true;
    627   }
    628   if (typeof(includeControls) === 'undefined') {
    629     includeControls = true;
    630   }
    631 
    632   if (node.constructor == Text) {
    633     return node.data;
    634   }
    635 
    636   var label = cvox.DomUtil.getBaseLabel_(node, recursive, includeControls);
    637 
    638   if (label.length == 0 && cvox.DomUtil.isControl(node)) {
    639     label = cvox.DomUtil.getNearestAncestorLabel_(node);
    640   }
    641 
    642   if (label.length == 0 && node.constructor == HTMLInputElement) {
    643     label = cvox.DomUtil.getInputName_(node);
    644   }
    645 
    646   if (cvox.DomUtil.isInputTypeText(node) && node.hasAttribute('placeholder')) {
    647     var placeholder = node.getAttribute('placeholder');
    648     if (label.length > 0) {
    649       if (cvox.DomUtil.getValue(node).length > 0) {
    650         return label;
    651       } else {
    652         return label + ' with hint ' + placeholder;
    653       }
    654     } else {
    655       return placeholder;
    656     }
    657   }
    658 
    659   if (label.length > 0) {
    660     return label;
    661   }
    662 
    663   // Fall back to naming via title only if there is no text content.
    664   if (cvox.DomUtil.collapseWhitespace(node.textContent).length == 0 &&
    665       node.hasAttribute &&
    666       node.hasAttribute('title')) {
    667     return node.getAttribute('title');
    668   }
    669 
    670   if (!recursive) {
    671     return '';
    672   }
    673 
    674   if (cvox.AriaUtil.isCompositeControl(node)) {
    675     return '';
    676   }
    677   if (cvox.DomUtil.hasChildrenBasedName_(node, opt_allowHidden)) {
    678     return cvox.DomUtil.getNameFromChildren(
    679         node, includeControls, opt_allowHidden);
    680   }
    681   return '';
    682 };
    683 
    684 
    685 /**
    686  * Get the name from the children of a node, not including the node itself.
    687  *
    688  * @param {Node} node The node to get the name from.
    689  * @param {boolean=} includeControls Whether or not controls in the subtree
    690  *     should be included; true by default.
    691  * @param {boolean=} opt_allowHidden Allow hidden nodes in name computation.
    692  * @return {string} The concatenated text of all child nodes.
    693  */
    694 cvox.DomUtil.getNameFromChildren = function(
    695     node, includeControls, opt_allowHidden) {
    696   if (includeControls == undefined) {
    697     includeControls = true;
    698   }
    699   var name = '';
    700   var delimiter = '';
    701   for (var i = 0; i < node.childNodes.length; i++) {
    702     var child = node.childNodes[i];
    703     var prevChild = node.childNodes[i - 1] || child;
    704     if (!includeControls && cvox.DomUtil.isControl(child)) {
    705       continue;
    706     }
    707     var isVisible = cvox.DomUtil.isVisible(child, {checkAncestors: false});
    708     if (opt_allowHidden || (isVisible && !cvox.AriaUtil.isHidden(child))) {
    709       delimiter = (prevChild.tagName == 'SPAN' ||
    710                    child.tagName == 'SPAN' ||
    711                    child.parentNode.tagName == 'SPAN') ?
    712           '' : ' ';
    713       name += delimiter + cvox.DomUtil.getName(child, true, includeControls);
    714     }
    715   }
    716 
    717   return name;
    718 };
    719 
    720 /**
    721  * Get any prefix text for the given node.
    722  * This includes list style text for the leftmost leaf node under a listitem.
    723  * @param {Node} node Compute prefix for this node.
    724  * @param {number=} opt_index Starting offset into the given node's text.
    725  * @return {string} Prefix text, if any.
    726  */
    727 cvox.DomUtil.getPrefixText = function(node, opt_index) {
    728   opt_index = opt_index || 0;
    729 
    730   // Generate list style text.
    731   var ancestors = cvox.DomUtil.getAncestors(node);
    732   var prefix = '';
    733   var firstListitem = cvox.DomPredicates.listItemPredicate(ancestors);
    734 
    735   var leftmost = firstListitem;
    736   while (leftmost && leftmost.firstChild) {
    737     leftmost = leftmost.firstChild;
    738   }
    739 
    740   // Do nothing if we're not at the leftmost leaf.
    741   if (firstListitem &&
    742       firstListitem.parentNode &&
    743       opt_index == 0 &&
    744       firstListitem.parentNode.tagName == 'OL' &&
    745           node == leftmost &&
    746       document.defaultView.getComputedStyle(firstListitem.parentNode)
    747           .listStyleType != 'none') {
    748     var items = cvox.DomUtil.toArray(firstListitem.parentNode.children).filter(
    749         function(li) { return li.tagName == 'LI'; });
    750     var position = items.indexOf(firstListitem) + 1;
    751     // TODO(dtseng): Support all list style types.
    752     if (document.defaultView.getComputedStyle(
    753             firstListitem.parentNode).listStyleType.indexOf('latin') != -1) {
    754       position--;
    755       prefix = String.fromCharCode('A'.charCodeAt(0) + position % 26);
    756     } else {
    757       prefix = position;
    758     }
    759     prefix += '. ';
    760   }
    761   return prefix;
    762 };
    763 
    764 
    765 /**
    766  * Use heuristics to guess at the label of a control, to be used if one
    767  * is not explicitly set in the DOM. This is useful when a control
    768  * field gets focus, but probably not useful when browsing the page
    769  * element at a time.
    770  * @param {Node} node The node to get the label from.
    771  * @return {string} The name of the control, using heuristics.
    772  */
    773 cvox.DomUtil.getControlLabelHeuristics = function(node) {
    774   // If the node explicitly has aria-label or title set to '',
    775   // treat it the same way as alt='' and do not guess - just assume
    776   // the web developer knew what they were doing and wanted
    777   // no title/label for that control.
    778   if (node.hasAttribute &&
    779       ((node.hasAttribute('aria-label') &&
    780       (node.getAttribute('aria-label') == '')) ||
    781       (node.hasAttribute('aria-title') &&
    782       (node.getAttribute('aria-title') == '')))) {
    783     return '';
    784   }
    785 
    786   // TODO (clchen, rshearer): Implement heuristics for getting the label
    787   // information from the table headers once the code for getting table
    788   // headers quickly is implemented.
    789 
    790   // If no description has been found yet and heuristics are enabled,
    791   // then try getting the content from the closest node.
    792   var prevNode = cvox.DomUtil.previousLeafNode(node);
    793   var prevTraversalCount = 0;
    794   while (prevNode && (!cvox.DomUtil.hasContent(prevNode) ||
    795       cvox.DomUtil.isControl(prevNode))) {
    796     prevNode = cvox.DomUtil.previousLeafNode(prevNode);
    797     prevTraversalCount++;
    798   }
    799   var nextNode = cvox.DomUtil.directedNextLeafNode(node);
    800   var nextTraversalCount = 0;
    801   while (nextNode && (!cvox.DomUtil.hasContent(nextNode) ||
    802       cvox.DomUtil.isControl(nextNode))) {
    803     nextNode = cvox.DomUtil.directedNextLeafNode(nextNode);
    804     nextTraversalCount++;
    805   }
    806   var guessedLabelNode;
    807   if (prevNode && nextNode) {
    808     var parentNode = node;
    809     // Count the number of parent nodes until there is a shared parent; the
    810     // label is most likely in the same branch of the DOM as the control.
    811     // TODO (chaitanyag): Try to generalize this algorithm and move it to
    812     // its own function in DOM Utils.
    813     var prevCount = 0;
    814     while (parentNode) {
    815       if (cvox.DomUtil.isDescendantOfNode(prevNode, parentNode)) {
    816         break;
    817       }
    818       parentNode = parentNode.parentNode;
    819       prevCount++;
    820     }
    821     parentNode = node;
    822     var nextCount = 0;
    823     while (parentNode) {
    824       if (cvox.DomUtil.isDescendantOfNode(nextNode, parentNode)) {
    825         break;
    826       }
    827       parentNode = parentNode.parentNode;
    828       nextCount++;
    829     }
    830     guessedLabelNode = nextCount < prevCount ? nextNode : prevNode;
    831   } else {
    832     guessedLabelNode = prevNode || nextNode;
    833   }
    834   if (guessedLabelNode) {
    835     return cvox.DomUtil.collapseWhitespace(
    836         cvox.DomUtil.getValue(guessedLabelNode) + ' ' +
    837         cvox.DomUtil.getName(guessedLabelNode));
    838   }
    839 
    840   return '';
    841 };
    842 
    843 
    844 /**
    845  * Get the text value of a node: the selected value of a select control or the
    846  * current text of a text control. Does not return the state of a checkbox
    847  * or radio button.
    848  *
    849  * Not recursive.
    850  *
    851  * @param {Node} node The node to get the value from.
    852  * @return {string} The value of the node.
    853  */
    854 cvox.DomUtil.getValue = function(node) {
    855   var activeDescendant = cvox.AriaUtil.getActiveDescendant(node);
    856   if (activeDescendant) {
    857     return cvox.DomUtil.collapseWhitespace(
    858         cvox.DomUtil.getValue(activeDescendant) + ' ' +
    859         cvox.DomUtil.getName(activeDescendant));
    860   }
    861 
    862   if (node.constructor == HTMLSelectElement) {
    863     node = /** @type {HTMLSelectElement} */(node);
    864     var value = '';
    865     var start = node.selectedOptions ? node.selectedOptions[0] : null;
    866     var end = node.selectedOptions ?
    867         node.selectedOptions[node.selectedOptions.length - 1] : null;
    868     // TODO(dtseng): Keeping this stateless means we describe the start and end
    869     // of the selection only since we don't know which was added or
    870     // removed. Once we keep the previous selection, we can read the diff.
    871     if (start && end && start != end) {
    872       value = cvox.ChromeVox.msgs.getMsg(
    873         'selected_options_value', [start.text, end.text]);
    874     } else if (start) {
    875       value = start.text + '';
    876     }
    877     return value;
    878   }
    879 
    880   if (node.constructor == HTMLTextAreaElement) {
    881     return node.value;
    882   }
    883 
    884   if (node.constructor == HTMLInputElement) {
    885     switch (node.type) {
    886       // Returning '' for inputs that are covered by getName.
    887       case 'hidden':
    888       case 'image':
    889       case 'submit':
    890       case 'reset':
    891       case 'button':
    892       case 'checkbox':
    893       case 'radio':
    894         return '';
    895       case 'password':
    896         return node.value.replace(/./g, 'dot ');
    897       default:
    898         return node.value;
    899     }
    900   }
    901 
    902   if (node.isContentEditable) {
    903     return cvox.DomUtil.getNameFromChildren(node, true);
    904   }
    905 
    906   return '';
    907 };
    908 
    909 
    910 /**
    911  * Given an image node, return its title as a string. The preferred title
    912  * is always the alt text, and if that's not available, then the title
    913  * attribute. If neither of those are available, it attempts to construct
    914  * a title from the filename, and if all else fails returns the word Image.
    915  * @param {Node} node The image node.
    916  * @return {string} The title of the image.
    917  */
    918 cvox.DomUtil.getImageTitle = function(node) {
    919   var text;
    920   if (node.hasAttribute('alt')) {
    921     text = node.alt;
    922   } else if (node.hasAttribute('title')) {
    923     text = node.title;
    924   } else {
    925     var url = node.src;
    926     if (url.substring(0, 4) != 'data') {
    927       var filename = url.substring(
    928           url.lastIndexOf('/') + 1, url.lastIndexOf('.'));
    929 
    930       // Hack to not speak the filename if it's ridiculously long.
    931       if (filename.length >= 1 && filename.length <= 16) {
    932         text = filename + ' Image';
    933       } else {
    934         text = 'Image';
    935       }
    936     } else {
    937       text = 'Image';
    938     }
    939   }
    940   return text;
    941 };
    942 
    943 
    944 /**
    945  * Search the whole page for any aria-labelledby attributes and collect
    946  * the complete set of ids they map to, so that we can skip elements that
    947  * just label other elements and not double-speak them. We cache this
    948  * result and then throw it away at the next event loop.
    949  * @return {Object.<string, boolean>} Set of all ids that are mapped
    950  *     by aria-labelledby.
    951  */
    952 cvox.DomUtil.getLabelledByTargets = function() {
    953   if (cvox.labelledByTargets) {
    954     return cvox.labelledByTargets;
    955   }
    956 
    957   // Start by getting all elements with
    958   // aria-labelledby on the page since that's probably a short list,
    959   // then see if any of those ids overlap with an id in this element's
    960   // ancestor chain.
    961   var labelledByElements = document.querySelectorAll('[aria-labelledby]');
    962   var labelledByTargets = {};
    963   for (var i = 0; i < labelledByElements.length; ++i) {
    964     var element = labelledByElements[i];
    965     var attrValue = element.getAttribute('aria-labelledby');
    966     var ids = attrValue.split(/ +/);
    967     for (var j = 0; j < ids.length; j++) {
    968       labelledByTargets[ids[j]] = true;
    969     }
    970   }
    971   cvox.labelledByTargets = labelledByTargets;
    972 
    973   window.setTimeout(function() {
    974     cvox.labelledByTargets = null;
    975   }, 0);
    976 
    977   return labelledByTargets;
    978 };
    979 
    980 
    981 /**
    982  * Determines whether or not a node has content.
    983  *
    984  * @param {Node} node The node to be checked.
    985  * @return {boolean} True if the node has content.
    986  */
    987 cvox.DomUtil.hasContent = function(node) {
    988   // Memoize the result of the internal content computation so that
    989   // within the same call stack, we don't need to redo the computation
    990   // on the same node twice.
    991   return /** @type {boolean} */ (cvox.Memoize.memoize(
    992       cvox.DomUtil.computeHasContent_.bind(this), 'hasContent', node));
    993 };
    994 
    995 /**
    996  * Internal implementation of |cvox.DomUtil.hasContent|.
    997  *
    998  * @param {Node} node The node to be checked.
    999  * @return {boolean} True if the node has content.
   1000  * @private
   1001  */
   1002 cvox.DomUtil.computeHasContent_ = function(node) {
   1003   // nodeType:8 == COMMENT_NODE
   1004   if (node.nodeType == 8) {
   1005     return false;
   1006   }
   1007 
   1008   // Exclude anything in the head
   1009   if (cvox.DomUtil.isDescendantOf(node, 'HEAD')) {
   1010     return false;
   1011   }
   1012 
   1013   // Exclude script nodes
   1014   if (cvox.DomUtil.isDescendantOf(node, 'SCRIPT')) {
   1015     return false;
   1016   }
   1017 
   1018   // Exclude noscript nodes
   1019   if (cvox.DomUtil.isDescendantOf(node, 'NOSCRIPT')) {
   1020     return false;
   1021   }
   1022 
   1023   // Exclude noembed nodes since NOEMBED is deprecated. We treat
   1024   // noembed as having not content rather than try to get its content since
   1025   // Chrome will return raw HTML content rather than a valid DOM subtree.
   1026   if (cvox.DomUtil.isDescendantOf(node, 'NOEMBED')) {
   1027     return false;
   1028   }
   1029 
   1030   // Exclude style nodes that have been dumped into the body.
   1031   if (cvox.DomUtil.isDescendantOf(node, 'STYLE')) {
   1032     return false;
   1033   }
   1034 
   1035   // Check the style to exclude undisplayed/hidden nodes.
   1036   if (!cvox.DomUtil.isVisible(node)) {
   1037     return false;
   1038   }
   1039 
   1040   // Ignore anything that is hidden by ARIA.
   1041   if (cvox.AriaUtil.isHidden(node)) {
   1042     return false;
   1043   }
   1044 
   1045   // We need to speak controls, including those with no value entered. We
   1046   // therefore treat visible controls as if they had content, and return true
   1047   // below.
   1048   if (cvox.DomUtil.isControl(node)) {
   1049     return true;
   1050   }
   1051 
   1052   // Videos are always considered to have content so that we can navigate to
   1053   // and use the controls of the video widget.
   1054   if (cvox.DomUtil.isDescendantOf(node, 'VIDEO')) {
   1055     return true;
   1056   }
   1057   // Audio elements are always considered to have content so that we can
   1058   // navigate to and use the controls of the audio widget.
   1059   if (cvox.DomUtil.isDescendantOf(node, 'AUDIO')) {
   1060     return true;
   1061   }
   1062 
   1063   // We want to try to jump into an iframe iff it has a src attribute.
   1064   // For right now, we will avoid iframes without any content in their src since
   1065   // ChromeVox is not being injected in those cases and will cause the user to
   1066   // get stuck.
   1067   // TODO (clchen, dmazzoni): Manually inject ChromeVox for iframes without src.
   1068   if ((node.tagName == 'IFRAME') && (node.src) &&
   1069       (node.src.indexOf('javascript:') != 0)) {
   1070     return true;
   1071   }
   1072 
   1073   var controlQuery = 'button,input,select,textarea';
   1074 
   1075   // Skip any non-control content inside of a label if the label is
   1076   // correctly associated with a control, the label text will get spoken
   1077   // when the control is reached.
   1078   var enclosingLabel = node.parentElement;
   1079   while (enclosingLabel && enclosingLabel.tagName != 'LABEL') {
   1080     enclosingLabel = enclosingLabel.parentElement;
   1081   }
   1082   if (enclosingLabel) {
   1083     var embeddedControl = enclosingLabel.querySelector(controlQuery);
   1084     if (enclosingLabel.hasAttribute('for')) {
   1085       var targetId = enclosingLabel.getAttribute('for');
   1086       var targetNode = document.getElementById(targetId);
   1087       if (targetNode &&
   1088           cvox.DomUtil.isControl(targetNode) &&
   1089           !embeddedControl) {
   1090         return false;
   1091       }
   1092     } else if (embeddedControl) {
   1093       return false;
   1094     }
   1095   }
   1096 
   1097   // Skip any non-control content inside of a legend if the legend is correctly
   1098   // nested within a fieldset. The legend text will get spoken when the fieldset
   1099   // is reached.
   1100   var enclosingLegend = node.parentElement;
   1101   while (enclosingLegend && enclosingLegend.tagName != 'LEGEND') {
   1102     enclosingLegend = enclosingLegend.parentElement;
   1103   }
   1104   if (enclosingLegend) {
   1105     var legendAncestor = enclosingLegend.parentElement;
   1106     while (legendAncestor && legendAncestor.tagName != 'FIELDSET') {
   1107       legendAncestor = legendAncestor.parentElement;
   1108     }
   1109     var embeddedControl =
   1110         legendAncestor && legendAncestor.querySelector(controlQuery);
   1111     if (legendAncestor && !embeddedControl) {
   1112       return false;
   1113     }
   1114   }
   1115 
   1116   if (!!cvox.DomPredicates.linkPredicate([node])) {
   1117     return true;
   1118   }
   1119 
   1120   // At this point, any non-layout tables are considered to have content.
   1121   // For layout tables, it is safe to consider them as without content since the
   1122   // sync operation would select a descendant of a layout table if possible. The
   1123   // only instance where |hasContent| gets called on a layout table is if no
   1124   // descendants have content (see |AbstractNodeWalker.next|).
   1125   if (node.tagName == 'TABLE' && !cvox.DomUtil.isLayoutTable(node)) {
   1126     return true;
   1127   }
   1128 
   1129   // Math is always considered to have content.
   1130   if (cvox.DomUtil.isMath(node)) {
   1131     return true;
   1132   }
   1133 
   1134   if (cvox.DomPredicates.headingPredicate([node])) {
   1135     return true;
   1136   }
   1137 
   1138   if (cvox.DomUtil.isFocusable(node)) {
   1139     return true;
   1140   }
   1141 
   1142   // Skip anything referenced by another element on the page
   1143   // via aria-labelledby.
   1144   var labelledByTargets = cvox.DomUtil.getLabelledByTargets();
   1145   var enclosingNodeWithId = node;
   1146   while (enclosingNodeWithId) {
   1147     if (enclosingNodeWithId.id &&
   1148         labelledByTargets[enclosingNodeWithId.id]) {
   1149       // If we got here, some element on this page has an aria-labelledby
   1150       // attribute listing this node as its id. As long as that "some" element
   1151       // is not this element, we should return false, indicating this element
   1152       // should be skipped.
   1153       var attrValue = enclosingNodeWithId.getAttribute('aria-labelledby');
   1154       if (attrValue) {
   1155         var ids = attrValue.split(/ +/);
   1156         if (ids.indexOf(enclosingNodeWithId.id) == -1) {
   1157           return false;
   1158         }
   1159       } else {
   1160         return false;
   1161       }
   1162     }
   1163     enclosingNodeWithId = enclosingNodeWithId.parentElement;
   1164   }
   1165 
   1166   var text = cvox.DomUtil.getValue(node) + ' ' + cvox.DomUtil.getName(node);
   1167   var state = cvox.DomUtil.getState(node, true);
   1168   if (text.match(/^\s+$/) && state === '') {
   1169     // Text only contains whitespace
   1170     return false;
   1171   }
   1172 
   1173   return true;
   1174 };
   1175 
   1176 
   1177 /**
   1178  * Returns a list of all the ancestors of a given node. The last element
   1179  * is the current node.
   1180  *
   1181  * @param {Node} targetNode The node to get ancestors for.
   1182  * @return {Array.<Node>} An array of ancestors for the targetNode.
   1183  */
   1184 cvox.DomUtil.getAncestors = function(targetNode) {
   1185   var ancestors = new Array();
   1186   while (targetNode) {
   1187     ancestors.push(targetNode);
   1188     targetNode = targetNode.parentNode;
   1189   }
   1190   ancestors.reverse();
   1191   while (ancestors.length && !ancestors[0].tagName && !ancestors[0].nodeValue) {
   1192     ancestors.shift();
   1193   }
   1194   return ancestors;
   1195 };
   1196 
   1197 
   1198 /**
   1199  * Compares Ancestors of A with Ancestors of B and returns
   1200  * the index value in B at which B diverges from A.
   1201  * If there is no divergence, the result will be -1.
   1202  * Note that if B is the same as A except B has more nodes
   1203  * even after A has ended, that is considered a divergence.
   1204  * The first node that B has which A does not have will
   1205  * be treated as the divergence point.
   1206  *
   1207  * @param {Object} ancestorsA The array of ancestors for Node A.
   1208  * @param {Object} ancestorsB The array of ancestors for Node B.
   1209  * @return {number} The index of the divergence point (the first node that B has
   1210  * which A does not have in B's list of ancestors).
   1211  */
   1212 cvox.DomUtil.compareAncestors = function(ancestorsA, ancestorsB) {
   1213   var i = 0;
   1214   while (ancestorsA[i] && ancestorsB[i] && (ancestorsA[i] == ancestorsB[i])) {
   1215     i++;
   1216   }
   1217   if (!ancestorsA[i] && !ancestorsB[i]) {
   1218     i = -1;
   1219   }
   1220   return i;
   1221 };
   1222 
   1223 
   1224 /**
   1225  * Returns an array of ancestors that are unique for the currentNode when
   1226  * compared to the previousNode. Having such an array is useful in generating
   1227  * the node information (identifying when interesting node boundaries have been
   1228  * crossed, etc.).
   1229  *
   1230  * @param {Node} previousNode The previous node.
   1231  * @param {Node} currentNode The current node.
   1232  * @param {boolean=} opt_fallback True returns node's ancestors in the case
   1233  * where node's ancestors is a subset of previousNode's ancestors.
   1234  * @return {Array.<Node>} An array of unique ancestors for the current node
   1235  * (inclusive).
   1236  */
   1237 cvox.DomUtil.getUniqueAncestors = function(
   1238     previousNode, currentNode, opt_fallback) {
   1239   var prevAncestors = cvox.DomUtil.getAncestors(previousNode);
   1240   var currentAncestors = cvox.DomUtil.getAncestors(currentNode);
   1241   var divergence = cvox.DomUtil.compareAncestors(prevAncestors,
   1242       currentAncestors);
   1243   var diff = currentAncestors.slice(divergence);
   1244   return (diff.length == 0 && opt_fallback) ? currentAncestors : diff;
   1245 };
   1246 
   1247 
   1248 /**
   1249  * Returns a role message identifier for a node.
   1250  * For a localized string, see cvox.DomUtil.getRole.
   1251  * @param {Node} targetNode The node to get the role name for.
   1252  * @param {number} verbosity The verbosity setting to use.
   1253  * @return {string} The role message identifier for the targetNode.
   1254  */
   1255 cvox.DomUtil.getRoleMsg = function(targetNode, verbosity) {
   1256   var info;
   1257   info = cvox.AriaUtil.getRoleNameMsg(targetNode);
   1258   if (!info) {
   1259     if (targetNode.tagName == 'INPUT') {
   1260       info = cvox.DomUtil.INPUT_TYPE_TO_INFORMATION_TABLE_MSG[targetNode.type];
   1261     } else if (targetNode.tagName == 'A' &&
   1262         cvox.DomUtil.isInternalLink(targetNode)) {
   1263       info = 'internal_link';
   1264     } else if (targetNode.tagName == 'A' &&
   1265         targetNode.getAttribute('name')) {
   1266       info = ''; // Don't want to add any role to anchors.
   1267     } else if (targetNode.isContentEditable) {
   1268       info = 'input_type_text';
   1269     } else if (cvox.DomUtil.isMath(targetNode)) {
   1270       info = 'math_expr';
   1271     } else if (targetNode.tagName == 'TABLE' &&
   1272         cvox.DomUtil.isLayoutTable(targetNode)) {
   1273       info = '';
   1274     } else {
   1275       if (verbosity == cvox.VERBOSITY_BRIEF) {
   1276         info =
   1277             cvox.DomUtil.TAG_TO_INFORMATION_TABLE_BRIEF_MSG[targetNode.tagName];
   1278       } else {
   1279         info = cvox.DomUtil.TAG_TO_INFORMATION_TABLE_VERBOSE_MSG[
   1280           targetNode.tagName];
   1281 
   1282         if (cvox.DomUtil.hasLongDesc(targetNode)) {
   1283           info = 'image_with_long_desc';
   1284         }
   1285 
   1286         if (!info && targetNode.onclick) {
   1287           info = 'clickable';
   1288         }
   1289       }
   1290     }
   1291   }
   1292 
   1293   return info;
   1294 };
   1295 
   1296 
   1297 /**
   1298  * Returns a string to be presented to the user that identifies what the
   1299  * targetNode's role is.
   1300  * ARIA roles are given priority; if there is no ARIA role set, the role
   1301  * will be determined by the HTML tag for the node.
   1302  *
   1303  * @param {Node} targetNode The node to get the role name for.
   1304  * @param {number} verbosity The verbosity setting to use.
   1305  * @return {string} The role name for the targetNode.
   1306  */
   1307 cvox.DomUtil.getRole = function(targetNode, verbosity) {
   1308   var roleMsg = cvox.DomUtil.getRoleMsg(targetNode, verbosity) || '';
   1309   var role = roleMsg && roleMsg != ' ' ?
   1310       cvox.ChromeVox.msgs.getMsg(roleMsg) : '';
   1311   return role ? role : roleMsg;
   1312 };
   1313 
   1314 
   1315 /**
   1316  * Count the number of items in a list node.
   1317  *
   1318  * @param {Node} targetNode The list node.
   1319  * @return {number} The number of items in the list.
   1320  */
   1321 cvox.DomUtil.getListLength = function(targetNode) {
   1322   var count = 0;
   1323   for (var node = targetNode.firstChild;
   1324        node;
   1325        node = node.nextSibling) {
   1326     if (cvox.DomUtil.isVisible(node) &&
   1327         (node.tagName == 'LI' ||
   1328         (node.getAttribute && node.getAttribute('role') == 'listitem'))) {
   1329       if (node.hasAttribute('aria-setsize')) {
   1330         var ariaLength = parseInt(node.getAttribute('aria-setsize'), 10);
   1331         if (!isNaN(ariaLength)) {
   1332           return ariaLength;
   1333         }
   1334       }
   1335       count++;
   1336     }
   1337   }
   1338   return count;
   1339 };
   1340 
   1341 
   1342 /**
   1343  * Returns a NodeState that gives information about the state of the targetNode.
   1344  *
   1345  * @param {Node} targetNode The node to get the state information for.
   1346  * @param {boolean} primary Whether this is the primary node we're
   1347  *     interested in, where we might want extra information - as
   1348  *     opposed to an ancestor, where we might be more brief.
   1349  * @return {cvox.NodeState} The status information about the node.
   1350  */
   1351 cvox.DomUtil.getStateMsgs = function(targetNode, primary) {
   1352   var activeDescendant = cvox.AriaUtil.getActiveDescendant(targetNode);
   1353   if (activeDescendant) {
   1354     return cvox.DomUtil.getStateMsgs(activeDescendant, primary);
   1355   }
   1356   var info = [];
   1357   var role = targetNode.getAttribute ? targetNode.getAttribute('role') : '';
   1358   info = cvox.AriaUtil.getStateMsgs(targetNode, primary);
   1359   if (!info) {
   1360     info = [];
   1361   }
   1362 
   1363   if (targetNode.tagName == 'INPUT') {
   1364     if (!targetNode.hasAttribute('aria-checked')) {
   1365       var INPUT_MSGS = {
   1366         'checkbox-true': 'checkbox_checked_state',
   1367         'checkbox-false': 'checkbox_unchecked_state',
   1368         'radio-true': 'radio_selected_state',
   1369         'radio-false': 'radio_unselected_state' };
   1370       var msgId = INPUT_MSGS[targetNode.type + '-' + !!targetNode.checked];
   1371       if (msgId) {
   1372         info.push([msgId]);
   1373       }
   1374     }
   1375   } else if (targetNode.tagName == 'SELECT') {
   1376     if (targetNode.selectedOptions && targetNode.selectedOptions.length <= 1) {
   1377       info.push(['list_position',
   1378                  cvox.ChromeVox.msgs.getNumber(targetNode.selectedIndex + 1),
   1379                  cvox.ChromeVox.msgs.getNumber(targetNode.options.length)]);
   1380     } else {
   1381       info.push(['selected_options_state',
   1382           cvox.ChromeVox.msgs.getNumber(targetNode.selectedOptions.length)]);
   1383     }
   1384   } else if (targetNode.tagName == 'UL' ||
   1385              targetNode.tagName == 'OL' ||
   1386              role == 'list') {
   1387     info.push(['list_with_items',
   1388                cvox.ChromeVox.msgs.getNumber(
   1389                    cvox.DomUtil.getListLength(targetNode))]);
   1390   }
   1391 
   1392   if (cvox.DomUtil.isDisabled(targetNode)) {
   1393     info.push(['aria_disabled_true']);
   1394   }
   1395 
   1396   if (cvox.DomPredicates.linkPredicate([targetNode]) &&
   1397       cvox.ChromeVox.visitedUrls[targetNode.href]) {
   1398     info.push(['visited_url']);
   1399   }
   1400 
   1401   if (targetNode.accessKey) {
   1402     info.push(['access_key', targetNode.accessKey]);
   1403   }
   1404 
   1405   return info;
   1406 };
   1407 
   1408 
   1409 /**
   1410  * Returns a string that gives information about the state of the targetNode.
   1411  *
   1412  * @param {Node} targetNode The node to get the state information for.
   1413  * @param {boolean} primary Whether this is the primary node we're
   1414  *     interested in, where we might want extra information - as
   1415  *     opposed to an ancestor, where we might be more brief.
   1416  * @return {string} The status information about the node.
   1417  */
   1418 cvox.DomUtil.getState = function(targetNode, primary) {
   1419   return cvox.NodeStateUtil.expand(
   1420       cvox.DomUtil.getStateMsgs(targetNode, primary));
   1421 };
   1422 
   1423 
   1424 /**
   1425  * Return whether a node is focusable. This includes nodes whose tabindex
   1426  * attribute is set to "-1" explicitly - these nodes are not in the tab
   1427  * order, but they should still be focused if the user navigates to them
   1428  * using linear or smart DOM navigation.
   1429  *
   1430  * Note that when the tabIndex property of an Element is -1, that doesn't
   1431  * tell us whether the tabIndex attribute is missing or set to "-1" explicitly,
   1432  * so we have to check the attribute.
   1433  *
   1434  * @param {Object} targetNode The node to check if it's focusable.
   1435  * @return {boolean} True if the node is focusable.
   1436  */
   1437 cvox.DomUtil.isFocusable = function(targetNode) {
   1438   if (!targetNode || typeof(targetNode.tabIndex) != 'number') {
   1439     return false;
   1440   }
   1441 
   1442   // Workaround for http://code.google.com/p/chromium/issues/detail?id=153904
   1443   if ((targetNode.tagName == 'A') && !targetNode.hasAttribute('href') &&
   1444       !targetNode.hasAttribute('tabindex')) {
   1445     return false;
   1446   }
   1447 
   1448   if (targetNode.tabIndex >= 0) {
   1449     return true;
   1450   }
   1451 
   1452   if (targetNode.hasAttribute &&
   1453       targetNode.hasAttribute('tabindex') &&
   1454       targetNode.getAttribute('tabindex') == '-1') {
   1455     return true;
   1456   }
   1457 
   1458   return false;
   1459 };
   1460 
   1461 
   1462 /**
   1463  * Find a focusable descendant of a given node. This includes nodes whose
   1464  * tabindex attribute is set to "-1" explicitly - these nodes are not in the
   1465  * tab order, but they should still be focused if the user navigates to them
   1466  * using linear or smart DOM navigation.
   1467  *
   1468  * @param {Node} targetNode The node whose descendants to check if focusable.
   1469  * @return {Node} The focusable descendant node. Null if no descendant node
   1470  * was found.
   1471  */
   1472 cvox.DomUtil.findFocusableDescendant = function(targetNode) {
   1473   // Search down the descendants chain until a focusable node is found
   1474   if (targetNode) {
   1475     var focusableNode =
   1476         cvox.DomUtil.findNode(targetNode, cvox.DomUtil.isFocusable);
   1477     if (focusableNode) {
   1478       return focusableNode;
   1479     }
   1480   }
   1481   return null;
   1482 };
   1483 
   1484 
   1485 /**
   1486  * Returns the number of focusable nodes in root's subtree. The count does not
   1487  * include root.
   1488  *
   1489  * @param {Node} targetNode The node whose descendants to check are focusable.
   1490  * @return {number} The number of focusable descendants.
   1491  */
   1492 cvox.DomUtil.countFocusableDescendants = function(targetNode) {
   1493   return targetNode ?
   1494       cvox.DomUtil.countNodes(targetNode, cvox.DomUtil.isFocusable) : 0;
   1495 };
   1496 
   1497 
   1498 /**
   1499  * Checks if the targetNode is still attached to the document.
   1500  * A node can become detached because of AJAX changes.
   1501  *
   1502  * @param {Object} targetNode The node to check.
   1503  * @return {boolean} True if the targetNode is still attached.
   1504  */
   1505 cvox.DomUtil.isAttachedToDocument = function(targetNode) {
   1506   while (targetNode) {
   1507     if (targetNode.tagName && (targetNode.tagName == 'HTML')) {
   1508       return true;
   1509     }
   1510     targetNode = targetNode.parentNode;
   1511   }
   1512   return false;
   1513 };
   1514 
   1515 
   1516 /**
   1517  * Dispatches a left click event on the element that is the targetNode.
   1518  * Clicks go in the sequence of mousedown, mouseup, and click.
   1519  * @param {Node} targetNode The target node of this operation.
   1520  * @param {boolean} shiftKey Specifies if shift is held down.
   1521  * @param {boolean} callOnClickDirectly Specifies whether or not to directly
   1522  * invoke the onclick method if there is one.
   1523  * @param {boolean=} opt_double True to issue a double click.
   1524  * @param {boolean=} opt_handleOwnEvents Whether to handle the generated
   1525  *     events through the normal event processing.
   1526  */
   1527 cvox.DomUtil.clickElem = function(
   1528     targetNode, shiftKey, callOnClickDirectly, opt_double,
   1529     opt_handleOwnEvents) {
   1530   // If there is an activeDescendant of the targetNode, then that is where the
   1531   // click should actually be targeted.
   1532   var activeDescendant = cvox.AriaUtil.getActiveDescendant(targetNode);
   1533   if (activeDescendant) {
   1534     targetNode = activeDescendant;
   1535   }
   1536   if (callOnClickDirectly) {
   1537     var onClickFunction = null;
   1538     if (targetNode.onclick) {
   1539       onClickFunction = targetNode.onclick;
   1540     }
   1541     if (!onClickFunction && (targetNode.nodeType != 1) &&
   1542         targetNode.parentNode && targetNode.parentNode.onclick) {
   1543       onClickFunction = targetNode.parentNode.onclick;
   1544     }
   1545     var keepGoing = true;
   1546     if (onClickFunction) {
   1547       try {
   1548         keepGoing = onClickFunction();
   1549       } catch (exception) {
   1550         // Something went very wrong with the onclick method; we'll ignore it
   1551         // and just dispatch a click event normally.
   1552       }
   1553     }
   1554     if (!keepGoing) {
   1555       // The onclick method ran successfully and returned false, meaning the
   1556       // event should not bubble up, so we will return here.
   1557       return;
   1558     }
   1559   }
   1560 
   1561   // Send a mousedown (or simply a double click if requested).
   1562   var evt = document.createEvent('MouseEvents');
   1563   var evtType = opt_double ? 'dblclick' : 'mousedown';
   1564   evt.initMouseEvent(evtType, true, true, document.defaultView,
   1565                      1, 0, 0, 0, 0, false, false, shiftKey, false, 0, null);
   1566   // Unless asked not to, Mark any events we generate so we don't try to
   1567   // process our own events.
   1568   evt.fromCvox = !opt_handleOwnEvents;
   1569   try {
   1570     targetNode.dispatchEvent(evt);
   1571   } catch (e) {}
   1572   //Send a mouse up
   1573   evt = document.createEvent('MouseEvents');
   1574   evt.initMouseEvent('mouseup', true, true, document.defaultView,
   1575                      1, 0, 0, 0, 0, false, false, shiftKey, false, 0, null);
   1576   evt.fromCvox = !opt_handleOwnEvents;
   1577   try {
   1578     targetNode.dispatchEvent(evt);
   1579   } catch (e) {}
   1580   //Send a click
   1581   evt = document.createEvent('MouseEvents');
   1582   evt.initMouseEvent('click', true, true, document.defaultView,
   1583                      1, 0, 0, 0, 0, false, false, shiftKey, false, 0, null);
   1584   evt.fromCvox = !opt_handleOwnEvents;
   1585   try {
   1586     targetNode.dispatchEvent(evt);
   1587   } catch (e) {}
   1588 
   1589   if (cvox.DomUtil.isInternalLink(targetNode)) {
   1590     cvox.DomUtil.syncInternalLink(targetNode);
   1591   }
   1592 };
   1593 
   1594 
   1595 /**
   1596  * Syncs to an internal link.
   1597  * @param {Node} node A link whose href's target we want to sync.
   1598  */
   1599 cvox.DomUtil.syncInternalLink = function(node) {
   1600   var targetNode;
   1601   var targetId = node.href.split('#')[1];
   1602   targetNode = document.getElementById(targetId);
   1603   if (!targetNode) {
   1604     var nodes = document.getElementsByName(targetId);
   1605     if (nodes.length > 0) {
   1606       targetNode = nodes[0];
   1607     }
   1608   }
   1609   if (targetNode) {
   1610     // Insert a dummy node to adjust next Tab focus location.
   1611     var parent = targetNode.parentNode;
   1612     var dummyNode = document.createElement('div');
   1613     dummyNode.setAttribute('tabindex', '-1');
   1614     parent.insertBefore(dummyNode, targetNode);
   1615     dummyNode.setAttribute('chromevoxignoreariahidden', 1);
   1616     dummyNode.focus();
   1617     cvox.ChromeVox.syncToNode(targetNode, false);
   1618   }
   1619 };
   1620 
   1621 
   1622 /**
   1623  * Given an HTMLInputElement, returns true if it's an editable text type.
   1624  * This includes input type='text' and input type='password' and a few
   1625  * others.
   1626  *
   1627  * @param {Node} node The node to check.
   1628  * @return {boolean} True if the node is an INPUT with an editable text type.
   1629  */
   1630 cvox.DomUtil.isInputTypeText = function(node) {
   1631   if (!node || node.constructor != HTMLInputElement) {
   1632     return false;
   1633   }
   1634 
   1635   switch (node.type) {
   1636     case 'email':
   1637     case 'number':
   1638     case 'password':
   1639     case 'search':
   1640     case 'text':
   1641     case 'tel':
   1642     case 'url':
   1643     case '':
   1644       return true;
   1645     default:
   1646       return false;
   1647   }
   1648 };
   1649 
   1650 
   1651 /**
   1652  * Given a node, returns true if it's a control. Controls are *not necessarily*
   1653  * leaf-level given that some composite controls may have focusable children
   1654  * if they are managing focus with tabindex:
   1655  * ( http://www.w3.org/TR/2010/WD-wai-aria-practices-20100916/#visualfocus ).
   1656  *
   1657  * @param {Node} node The node to check.
   1658  * @return {boolean} True if the node is a control.
   1659  */
   1660 cvox.DomUtil.isControl = function(node) {
   1661   if (cvox.AriaUtil.isControlWidget(node) &&
   1662       cvox.DomUtil.isFocusable(node)) {
   1663     return true;
   1664   }
   1665   if (node.tagName) {
   1666     switch (node.tagName) {
   1667       case 'BUTTON':
   1668       case 'TEXTAREA':
   1669       case 'SELECT':
   1670         return true;
   1671       case 'INPUT':
   1672         return node.type != 'hidden';
   1673     }
   1674   }
   1675   if (node.isContentEditable) {
   1676     return true;
   1677   }
   1678   return false;
   1679 };
   1680 
   1681 
   1682 /**
   1683  * Given a node, returns true if it's a leaf-level control. This includes
   1684  * composite controls thare are managing focus for children with
   1685  * activedescendant, but not composite controls with focusable children:
   1686  * ( http://www.w3.org/TR/2010/WD-wai-aria-practices-20100916/#visualfocus ).
   1687  *
   1688  * @param {Node} node The node to check.
   1689  * @return {boolean} True if the node is a leaf-level control.
   1690  */
   1691 cvox.DomUtil.isLeafLevelControl = function(node) {
   1692   if (cvox.DomUtil.isControl(node)) {
   1693     return !(cvox.AriaUtil.isCompositeControl(node) &&
   1694              cvox.DomUtil.findFocusableDescendant(node));
   1695   }
   1696   return false;
   1697 };
   1698 
   1699 
   1700 /**
   1701  * Given a node that might be inside of a composite control like a listbox,
   1702  * return the surrounding control.
   1703  * @param {Node} node The node from which to start looking.
   1704  * @return {Node} The surrounding composite control node, or null if none.
   1705  */
   1706 cvox.DomUtil.getSurroundingControl = function(node) {
   1707   var surroundingControl = null;
   1708   if (!cvox.DomUtil.isControl(node) && node.hasAttribute &&
   1709       node.hasAttribute('role')) {
   1710     surroundingControl = node.parentElement;
   1711     while (surroundingControl &&
   1712         !cvox.AriaUtil.isCompositeControl(surroundingControl)) {
   1713       surroundingControl = surroundingControl.parentElement;
   1714     }
   1715   }
   1716   return surroundingControl;
   1717 };
   1718 
   1719 
   1720 /**
   1721  * Given a node and a function for determining when to stop
   1722  * descent, return the next leaf-like node.
   1723  *
   1724  * @param {!Node} node The node from which to start looking,
   1725  * this node *must not* be above document.body.
   1726  * @param {boolean} r True if reversed. False by default.
   1727  * @param {function(!Node):boolean} isLeaf A function that
   1728  *   returns true if we should stop descending.
   1729  * @return {Node} The next leaf-like node or null if there is no next
   1730  *   leaf-like node.  This function will always return a node below
   1731  *   document.body and never document.body itself.
   1732  */
   1733 cvox.DomUtil.directedNextLeafLikeNode = function(node, r, isLeaf) {
   1734   if (node != document.body) {
   1735     // if not at the top of the tree, we want to find the next possible
   1736     // branch forward in the dom, so we climb up the parents until we find a
   1737     // node that has a nextSibling
   1738     while (!cvox.DomUtil.directedNextSibling(node, r)) {
   1739       if (!node) {
   1740         return null;
   1741       }
   1742       // since node is never above document.body, it always has a parent.
   1743       // so node.parentNode will never be null.
   1744       node = /** @type {!Node} */(node.parentNode);
   1745       if (node == document.body) {
   1746         // we've readed the end of the document.
   1747         return null;
   1748       }
   1749     }
   1750     if (cvox.DomUtil.directedNextSibling(node, r)) {
   1751       // we just checked that next sibling is non-null.
   1752       node = /** @type {!Node} */(cvox.DomUtil.directedNextSibling(node, r));
   1753     }
   1754   }
   1755   // once we're at our next sibling, we want to descend down into it as
   1756   // far as the child class will allow
   1757   while (cvox.DomUtil.directedFirstChild(node, r) && !isLeaf(node)) {
   1758     node = /** @type {!Node} */(cvox.DomUtil.directedFirstChild(node, r));
   1759   }
   1760 
   1761   // after we've done all that, if we are still at document.body, this must
   1762   // be an empty document.
   1763   if (node == document.body) {
   1764     return null;
   1765   }
   1766   return node;
   1767 };
   1768 
   1769 
   1770 /**
   1771  * Given a node, returns the next leaf node.
   1772  *
   1773  * @param {!Node} node The node from which to start looking
   1774  * for the next leaf node.
   1775  * @param {boolean=} reverse True if reversed. False by default.
   1776  * @return {Node} The next leaf node.
   1777  * Null if there is no next leaf node.
   1778  */
   1779 cvox.DomUtil.directedNextLeafNode = function(node, reverse) {
   1780   reverse = !!reverse;
   1781   return cvox.DomUtil.directedNextLeafLikeNode(
   1782       node, reverse, cvox.DomUtil.isLeafNode);
   1783 };
   1784 
   1785 
   1786 /**
   1787  * Given a node, returns the previous leaf node.
   1788  *
   1789  * @param {!Node} node The node from which to start looking
   1790  * for the previous leaf node.
   1791  * @return {Node} The previous leaf node.
   1792  * Null if there is no previous leaf node.
   1793  */
   1794 cvox.DomUtil.previousLeafNode = function(node) {
   1795   return cvox.DomUtil.directedNextLeafNode(node, true);
   1796 };
   1797 
   1798 
   1799 /**
   1800  * Computes the outer most leaf node of a given node, depending on value
   1801  * of the reverse flag r.
   1802  * @param {!Node} node in the DOM.
   1803  * @param {boolean} r True if reversed. False by default.
   1804  * @param {function(!Node):boolean} pred Predicate to decide
   1805  * what we consider a leaf.
   1806  * @return {Node} The outer most leaf node of that node.
   1807  */
   1808 cvox.DomUtil.directedFindFirstNode = function(node, r, pred) {
   1809   var child = cvox.DomUtil.directedFirstChild(node, r);
   1810   while (child) {
   1811     if (pred(child)) {
   1812       return child;
   1813     } else {
   1814       var leaf = cvox.DomUtil.directedFindFirstNode(child, r, pred);
   1815       if (leaf) {
   1816         return leaf;
   1817       }
   1818     }
   1819     child = cvox.DomUtil.directedNextSibling(child, r);
   1820   }
   1821   return null;
   1822 };
   1823 
   1824 
   1825 /**
   1826  * Moves to the deepest node satisfying a given predicate under the given node.
   1827  * @param {!Node} node in the DOM.
   1828  * @param {boolean} r True if reversed. False by default.
   1829  * @param {function(!Node):boolean} pred Predicate deciding what a leaf is.
   1830  * @return {Node} The deepest node satisfying pred.
   1831  */
   1832 cvox.DomUtil.directedFindDeepestNode = function(node, r, pred) {
   1833   var next = cvox.DomUtil.directedFindFirstNode(node, r, pred);
   1834   if (!next) {
   1835     if (pred(node)) {
   1836       return node;
   1837     } else {
   1838       return null;
   1839     }
   1840   } else {
   1841     return cvox.DomUtil.directedFindDeepestNode(next, r, pred);
   1842   }
   1843 };
   1844 
   1845 
   1846 /**
   1847  * Computes the next node wrt. a predicate that is a descendant of ancestor.
   1848  * @param {!Node} node in the DOM.
   1849  * @param {!Node} ancestor of the given node.
   1850  * @param {boolean} r True if reversed. False by default.
   1851  * @param {function(!Node):boolean} pred Predicate to decide
   1852  * what we consider a leaf.
   1853  * @param {boolean=} above True if the next node can live in the subtree
   1854  * directly above the start node. False by default.
   1855  * @param {boolean=} deep True if we are looking for the next node that is
   1856  * deepest in the tree. Otherwise the next shallow node is returned.
   1857  * False by default.
   1858  * @return {Node} The next node in the DOM that satisfies the predicate.
   1859  */
   1860 cvox.DomUtil.directedFindNextNode = function(
   1861     node, ancestor, r, pred, above, deep) {
   1862   above = !!above;
   1863   deep = !!deep;
   1864   if (!cvox.DomUtil.isDescendantOfNode(node, ancestor) || node == ancestor) {
   1865     return null;
   1866   }
   1867   var next = cvox.DomUtil.directedNextSibling(node, r);
   1868   while (next) {
   1869     if (!deep && pred(next)) {
   1870       return next;
   1871     }
   1872     var leaf = (deep ?
   1873                 cvox.DomUtil.directedFindDeepestNode :
   1874                 cvox.DomUtil.directedFindFirstNode)(next, r, pred);
   1875     if (leaf) {
   1876       return leaf;
   1877     }
   1878     if (deep && pred(next)) {
   1879       return next;
   1880     }
   1881     next = cvox.DomUtil.directedNextSibling(next, r);
   1882   }
   1883   var parent = /** @type {!Node} */(node.parentNode);
   1884   if (above && pred(parent)) {
   1885     return parent;
   1886   }
   1887   return cvox.DomUtil.directedFindNextNode(
   1888       parent, ancestor, r, pred, above, deep);
   1889 };
   1890 
   1891 
   1892 /**
   1893  * Get a string representing a control's value and state, i.e. the part
   1894  *     that changes while interacting with the control
   1895  * @param {Element} control A control.
   1896  * @return {string} The value and state string.
   1897  */
   1898 cvox.DomUtil.getControlValueAndStateString = function(control) {
   1899   var parentControl = cvox.DomUtil.getSurroundingControl(control);
   1900   if (parentControl) {
   1901     return cvox.DomUtil.collapseWhitespace(
   1902         cvox.DomUtil.getValue(control) + ' ' +
   1903         cvox.DomUtil.getName(control) + ' ' +
   1904         cvox.DomUtil.getState(control, true));
   1905   } else {
   1906     return cvox.DomUtil.collapseWhitespace(
   1907         cvox.DomUtil.getValue(control) + ' ' +
   1908         cvox.DomUtil.getState(control, true));
   1909   }
   1910 };
   1911 
   1912 
   1913 /**
   1914  * Determine whether the given node is an internal link.
   1915  * @param {Node} node The node to be examined.
   1916  * @return {boolean} True if the node is an internal link, false otherwise.
   1917  */
   1918 cvox.DomUtil.isInternalLink = function(node) {
   1919   if (node.nodeType == 1) { // Element nodes only.
   1920     var href = node.getAttribute('href');
   1921     if (href && href.indexOf('#') != -1) {
   1922       var path = href.split('#')[0];
   1923       return path == '' || path == window.location.pathname;
   1924     }
   1925   }
   1926   return false;
   1927 };
   1928 
   1929 
   1930 /**
   1931  * Get a string containing the currently selected link's URL.
   1932  * @param {Node} node The link from which URL needs to be extracted.
   1933  * @return {string} The value of the URL.
   1934  */
   1935 cvox.DomUtil.getLinkURL = function(node) {
   1936   if (node.tagName == 'A') {
   1937     if (node.getAttribute('href')) {
   1938       if (cvox.DomUtil.isInternalLink(node)) {
   1939         return cvox.ChromeVox.msgs.getMsg('internal_link');
   1940       } else {
   1941         return node.getAttribute('href');
   1942       }
   1943     } else {
   1944       return '';
   1945     }
   1946   } else if (cvox.AriaUtil.getRoleName(node) ==
   1947              cvox.ChromeVox.msgs.getMsg('aria_role_link')) {
   1948     return cvox.ChromeVox.msgs.getMsg('unknown_link');
   1949   }
   1950 
   1951   return '';
   1952 };
   1953 
   1954 
   1955 /**
   1956  * Checks if a given node is inside a table and returns the table node if it is
   1957  * @param {Node} node The node.
   1958  * @param {{allowCaptions: (undefined|boolean)}=} kwargs Optional named args.
   1959  *  allowCaptions: If true, will return true even if inside a caption. False
   1960  *    by default.
   1961  * @return {Node} If the node is inside a table, the table node. Null if it
   1962  * is not.
   1963  */
   1964 cvox.DomUtil.getContainingTable = function(node, kwargs) {
   1965   var ancestors = cvox.DomUtil.getAncestors(node);
   1966   return cvox.DomUtil.findTableNodeInList(ancestors, kwargs);
   1967 };
   1968 
   1969 
   1970 /**
   1971  * Extracts a table node from a list of nodes.
   1972  * @param {Array.<Node>} nodes The list of nodes.
   1973  * @param {{allowCaptions: (undefined|boolean)}=} kwargs Optional named args.
   1974  *  allowCaptions: If true, will return true even if inside a caption. False
   1975  *    by default.
   1976  * @return {Node} The table node if the list of nodes contains a table node.
   1977  * Null if it does not.
   1978  */
   1979 cvox.DomUtil.findTableNodeInList = function(nodes, kwargs) {
   1980   kwargs = kwargs || {allowCaptions: false};
   1981   // Don't include the caption node because it is actually rendered outside
   1982   // of the table.
   1983   for (var i = nodes.length - 1, node; node = nodes[i]; i--) {
   1984     if (node.constructor != Text) {
   1985       if (!kwargs.allowCaptions && node.tagName == 'CAPTION') {
   1986         return null;
   1987       }
   1988       if ((node.tagName == 'TABLE') || cvox.AriaUtil.isGrid(node)) {
   1989         return node;
   1990       }
   1991     }
   1992   }
   1993   return null;
   1994 };
   1995 
   1996 
   1997 /**
   1998  * Determines whether a given table is a data table or a layout table
   1999  * @param {Node} tableNode The table node.
   2000  * @return {boolean} If the table is a layout table, returns true. False
   2001  * otherwise.
   2002  */
   2003 cvox.DomUtil.isLayoutTable = function(tableNode) {
   2004   // TODO(stoarca): Why are we returning based on this inaccurate heuristic
   2005   // instead of first trying the better heuristics below?
   2006   if (tableNode.rows && (tableNode.rows.length <= 1 ||
   2007       (tableNode.rows[0].childElementCount == 1))) {
   2008     // This table has either 0 or one rows, or only "one" column.
   2009     // This is a quick check for column count and may not be accurate. See
   2010     // TraverseTable.getW3CColCount_ for a more accurate
   2011     // (but more complicated) way to determine column count.
   2012     return true;
   2013   }
   2014 
   2015   // These heuristics are adapted from the Firefox data and layout table.
   2016   // heuristics: http://asurkov.blogspot.com/2011/10/data-vs-layout-table.html
   2017   if (cvox.AriaUtil.isGrid(tableNode)) {
   2018     // This table has an ARIA role identifying it as a grid.
   2019     // Not a layout table.
   2020     return false;
   2021   }
   2022   if (cvox.AriaUtil.isLandmark(tableNode)) {
   2023     // This table has an ARIA landmark role - not a layout table.
   2024     return false;
   2025   }
   2026 
   2027   if (tableNode.caption || tableNode.summary) {
   2028     // This table has a caption or a summary - not a layout table.
   2029     return false;
   2030   }
   2031 
   2032   if ((cvox.XpathUtil.evalXPath('tbody/tr/th', tableNode).length > 0) &&
   2033       (cvox.XpathUtil.evalXPath('tbody/tr/td', tableNode).length > 0)) {
   2034     // This table at least one column and at least one column header.
   2035     // Not a layout table.
   2036     return false;
   2037   }
   2038 
   2039   if (cvox.XpathUtil.evalXPath('colgroup', tableNode).length > 0) {
   2040     // This table specifies column groups - not a layout table.
   2041     return false;
   2042   }
   2043 
   2044   if ((cvox.XpathUtil.evalXPath('thead', tableNode).length > 0) ||
   2045       (cvox.XpathUtil.evalXPath('tfoot', tableNode).length > 0)) {
   2046     // This table has header or footer rows - not a layout table.
   2047     return false;
   2048   }
   2049 
   2050   if ((cvox.XpathUtil.evalXPath('tbody/tr/td/embed', tableNode).length > 0) ||
   2051       (cvox.XpathUtil.evalXPath('tbody/tr/td/object', tableNode).length > 0) ||
   2052       (cvox.XpathUtil.evalXPath('tbody/tr/td/iframe', tableNode).length > 0) ||
   2053       (cvox.XpathUtil.evalXPath('tbody/tr/td/applet', tableNode).length > 0)) {
   2054     // This table contains embed, object, applet, or iframe elements. It is
   2055     // a layout table.
   2056     return true;
   2057   }
   2058 
   2059   // These heuristics are loosely based on Okada and Miura's "Detection of
   2060   // Layout-Purpose TABLE Tags Based on Machine Learning" (2007).
   2061   // http://books.google.com/books?id=kUbmdqasONwC&lpg=PA116&ots=Lb3HJ7dISZ&lr&pg=PA116
   2062 
   2063   // Increase the points for each heuristic. If there are 3 or more points,
   2064   // this is probably a layout table.
   2065   var points = 0;
   2066 
   2067   if (! cvox.DomUtil.hasBorder(tableNode)) {
   2068     // This table has no border.
   2069     points++;
   2070   }
   2071 
   2072   if (tableNode.rows.length <= 6) {
   2073     // This table has a limited number of rows.
   2074     points++;
   2075   }
   2076 
   2077   if (cvox.DomUtil.countPreviousTags(tableNode) <= 12) {
   2078     // This table has a limited number of previous tags.
   2079     points++;
   2080   }
   2081 
   2082  if (cvox.XpathUtil.evalXPath('tbody/tr/td/table', tableNode).length > 0) {
   2083    // This table has nested tables.
   2084    points++;
   2085  }
   2086   return (points >= 3);
   2087 };
   2088 
   2089 
   2090 /**
   2091  * Count previous tags, which we dfine as the number of HTML tags that
   2092  * appear before the given node.
   2093  * @param {Node} node The given node.
   2094  * @return {number} The number of previous tags.
   2095  */
   2096 cvox.DomUtil.countPreviousTags = function(node) {
   2097   var ancestors = cvox.DomUtil.getAncestors(node);
   2098   return ancestors.length + cvox.DomUtil.countPreviousSiblings(node);
   2099 };
   2100 
   2101 
   2102 /**
   2103  * Counts previous siblings, not including text nodes.
   2104  * @param {Node} node The given node.
   2105  * @return {number} The number of previous siblings.
   2106  */
   2107 cvox.DomUtil.countPreviousSiblings = function(node) {
   2108   var count = 0;
   2109   var prev = node.previousSibling;
   2110   while (prev != null) {
   2111     if (prev.constructor != Text) {
   2112       count++;
   2113     }
   2114     prev = prev.previousSibling;
   2115   }
   2116   return count;
   2117 };
   2118 
   2119 
   2120 /**
   2121  * Whether a given table has a border or not.
   2122  * @param {Node} tableNode The table node.
   2123  * @return {boolean} If the table has a border, return true. False otherwise.
   2124  */
   2125 cvox.DomUtil.hasBorder = function(tableNode) {
   2126   // If .frame contains "void" there is no border.
   2127   if (tableNode.frame) {
   2128     return (tableNode.frame.indexOf('void') == -1);
   2129   }
   2130 
   2131   // If .border is defined and  == "0" then there is no border.
   2132   if (tableNode.border) {
   2133     if (tableNode.border.length == 1) {
   2134       return (tableNode.border != '0');
   2135     } else {
   2136       return (tableNode.border.slice(0, -2) != 0);
   2137     }
   2138   }
   2139 
   2140   // If .style.border-style is 'none' there is no border.
   2141   if (tableNode.style.borderStyle && tableNode.style.borderStyle == 'none') {
   2142     return false;
   2143   }
   2144 
   2145   // If .style.border-width is specified in units of length
   2146   // ( https://developer.mozilla.org/en/CSS/border-width ) then we need
   2147   // to check if .style.border-width starts with 0[px,em,etc]
   2148   if (tableNode.style.borderWidth) {
   2149     return (tableNode.style.borderWidth.slice(0, -2) != 0);
   2150   }
   2151 
   2152   // If .style.border-color is defined, then there is a border
   2153   if (tableNode.style.borderColor) {
   2154     return true;
   2155   }
   2156   return false;
   2157 };
   2158 
   2159 
   2160 /**
   2161  * Return the first leaf node, starting at the top of the document.
   2162  * @return {Node?} The first leaf node in the document, if found.
   2163  */
   2164 cvox.DomUtil.getFirstLeafNode = function() {
   2165   var node = document.body;
   2166   while (node && node.firstChild) {
   2167     node = node.firstChild;
   2168   }
   2169   while (node && !cvox.DomUtil.hasContent(node)) {
   2170     node = cvox.DomUtil.directedNextLeafNode(node);
   2171   }
   2172   return node;
   2173 };
   2174 
   2175 
   2176 /**
   2177  * Finds the first descendant node that matches the filter function, using
   2178  * a depth first search. This function offers the most general purpose way
   2179  * of finding a matching element. You may also wish to consider
   2180  * {@code goog.dom.query} which can express many matching criteria using
   2181  * CSS selector expressions. These expressions often result in a more
   2182  * compact representation of the desired result.
   2183  * This is the findNode function from goog.dom:
   2184  * http://code.google.com/p/closure-library/source/browse/trunk/closure/goog/dom/dom.js
   2185  *
   2186  * @param {Node} root The root of the tree to search.
   2187  * @param {function(Node) : boolean} p The filter function.
   2188  * @return {Node|undefined} The found node or undefined if none is found.
   2189  */
   2190 cvox.DomUtil.findNode = function(root, p) {
   2191   var rv = [];
   2192   var found = cvox.DomUtil.findNodes_(root, p, rv, true, 10000);
   2193   return found ? rv[0] : undefined;
   2194 };
   2195 
   2196 
   2197 /**
   2198  * Finds the number of nodes matching the filter.
   2199  * @param {Node} root The root of the tree to search.
   2200  * @param {function(Node) : boolean} p The filter function.
   2201  * @return {number} The number of nodes selected by filter.
   2202  */
   2203 cvox.DomUtil.countNodes = function(root, p) {
   2204   var rv = [];
   2205   cvox.DomUtil.findNodes_(root, p, rv, false, 10000);
   2206   return rv.length;
   2207 };
   2208 
   2209 
   2210 /**
   2211  * Finds the first or all the descendant nodes that match the filter function,
   2212  * using a depth first search.
   2213  * @param {Node} root The root of the tree to search.
   2214  * @param {function(Node) : boolean} p The filter function.
   2215  * @param {Array.<Node>} rv The found nodes are added to this array.
   2216  * @param {boolean} findOne If true we exit after the first found node.
   2217  * @param {number} maxChildCount The max child count. This is used as a kill
   2218  * switch - if there are more nodes than this, terminate the search.
   2219  * @return {boolean} Whether the search is complete or not. True in case
   2220  * findOne is true and the node is found. False otherwise. This is the
   2221  * findNodes_ function from goog.dom:
   2222  * http://code.google.com/p/closure-library/source/browse/trunk/closure/goog/dom/dom.js.
   2223  * @private
   2224  */
   2225 cvox.DomUtil.findNodes_ = function(root, p, rv, findOne, maxChildCount) {
   2226   if ((root != null) || (maxChildCount == 0)) {
   2227     var child = root.firstChild;
   2228     while (child) {
   2229       if (p(child)) {
   2230         rv.push(child);
   2231         if (findOne) {
   2232           return true;
   2233         }
   2234       }
   2235       maxChildCount = maxChildCount - 1;
   2236       if (cvox.DomUtil.findNodes_(child, p, rv, findOne, maxChildCount)) {
   2237         return true;
   2238       }
   2239       child = child.nextSibling;
   2240     }
   2241   }
   2242   return false;
   2243 };
   2244 
   2245 
   2246 /**
   2247  * Converts a NodeList into an array
   2248  * @param {NodeList} nodeList The nodeList.
   2249  * @return {Array} The array of nodes in the nodeList.
   2250  */
   2251 cvox.DomUtil.toArray = function(nodeList) {
   2252   var nodeArray = [];
   2253   for (var i = 0; i < nodeList.length; i++) {
   2254     nodeArray.push(nodeList[i]);
   2255   }
   2256   return nodeArray;
   2257 };
   2258 
   2259 
   2260 /**
   2261  * Creates a new element with the same attributes and no children.
   2262  * @param {Node|Text} node A node to clone.
   2263  * @param {Object.<string, boolean>} skipattrs Set the attribute to true to
   2264  * skip it during cloning.
   2265  * @return {Node|Text} The cloned node.
   2266  */
   2267 cvox.DomUtil.shallowChildlessClone = function(node, skipattrs) {
   2268   if (node.nodeName == '#text') {
   2269     return document.createTextNode(node.nodeValue);
   2270   }
   2271 
   2272   if (node.nodeName == '#comment') {
   2273     return document.createComment(node.nodeValue);
   2274   }
   2275 
   2276   var ret = document.createElement(node.nodeName);
   2277   for (var i = 0; i < node.attributes.length; ++i) {
   2278     var attr = node.attributes[i];
   2279     if (skipattrs && skipattrs[attr.nodeName]) {
   2280       continue;
   2281     }
   2282     ret.setAttribute(attr.nodeName, attr.nodeValue);
   2283   }
   2284   return ret;
   2285 };
   2286 
   2287 
   2288 /**
   2289  * Creates a new element with the same attributes and clones of children.
   2290  * @param {Node|Text} node A node to clone.
   2291  * @param {Object.<string, boolean>} skipattrs Set the attribute to true to
   2292  * skip it during cloning.
   2293  * @return {Node|Text} The cloned node.
   2294  */
   2295 cvox.DomUtil.deepClone = function(node, skipattrs) {
   2296   var ret = cvox.DomUtil.shallowChildlessClone(node, skipattrs);
   2297   for (var i = 0; i < node.childNodes.length; ++i) {
   2298     ret.appendChild(cvox.DomUtil.deepClone(node.childNodes[i], skipattrs));
   2299   }
   2300   return ret;
   2301 };
   2302 
   2303 
   2304 /**
   2305  * Returns either node.firstChild or node.lastChild, depending on direction.
   2306  * @param {Node|Text} node The node.
   2307  * @param {boolean} reverse If reversed.
   2308  * @return {Node|Text} The directed first child or null if the node has
   2309  *   no children.
   2310  */
   2311 cvox.DomUtil.directedFirstChild = function(node, reverse) {
   2312   if (reverse) {
   2313     return node.lastChild;
   2314   }
   2315   return node.firstChild;
   2316 };
   2317 
   2318 /**
   2319  * Returns either node.nextSibling or node.previousSibling, depending on
   2320  * direction.
   2321  * @param {Node|Text} node The node.
   2322  * @param {boolean=} reverse If reversed.
   2323  * @return {Node|Text} The directed next sibling or null if there are
   2324  *   no more siblings in that direction.
   2325  */
   2326 cvox.DomUtil.directedNextSibling = function(node, reverse) {
   2327   if (!node) {
   2328     return null;
   2329   }
   2330   if (reverse) {
   2331     return node.previousSibling;
   2332   }
   2333   return node.nextSibling;
   2334 };
   2335 
   2336 /**
   2337  * Creates a function that sends a click. This is because loop closures
   2338  * are dangerous.
   2339  * See: http://joust.kano.net/weblog/archive/2005/08/08/
   2340  * a-huge-gotcha-with-javascript-closures/
   2341  * @param {Node} targetNode The target node to click on.
   2342  * @return {function()} A function that will click on the given targetNode.
   2343  */
   2344 cvox.DomUtil.createSimpleClickFunction = function(targetNode) {
   2345   var target = targetNode.cloneNode(true);
   2346   return function() { cvox.DomUtil.clickElem(target, false, false); };
   2347 };
   2348 
   2349 /**
   2350  * Adds a node to document.head if that node has not already been added.
   2351  * If document.head does not exist, this will add the node to the body.
   2352  * @param {Node} node The node to add.
   2353  * @param {string=} opt_id The id of the node to ensure the node is only
   2354  *     added once.
   2355  */
   2356 cvox.DomUtil.addNodeToHead = function(node, opt_id) {
   2357   if (opt_id && document.getElementById(opt_id)) {
   2358       return;
   2359   }
   2360   var p = document.head || document.body;
   2361   p.appendChild(node);
   2362 };
   2363 
   2364 
   2365 /**
   2366  * Checks if a given node is inside a math expressions and
   2367  * returns the math node if one exists.
   2368  * @param {Node} node The node.
   2369  * @return {Node} The math node, if the node is inside a math expression.
   2370  * Null if it is not.
   2371  */
   2372 cvox.DomUtil.getContainingMath = function(node) {
   2373   var ancestors = cvox.DomUtil.getAncestors(node);
   2374   return cvox.DomUtil.findMathNodeInList(ancestors);
   2375 };
   2376 
   2377 
   2378 /**
   2379  * Extracts a math node from a list of nodes.
   2380  * @param {Array.<Node>} nodes The list of nodes.
   2381  * @return {Node} The math node if the list of nodes contains a math node.
   2382  * Null if it does not.
   2383  */
   2384 cvox.DomUtil.findMathNodeInList = function(nodes) {
   2385   for (var i = 0, node; node = nodes[i]; i++) {
   2386     if (cvox.DomUtil.isMath(node)) {
   2387       return node;
   2388     }
   2389   }
   2390   return null;
   2391 };
   2392 
   2393 
   2394 /**
   2395  * Checks to see wether a node is a math node.
   2396  * @param {Node} node The node to be tested.
   2397  * @return {boolean} Whether or not a node is a math node.
   2398  */
   2399 cvox.DomUtil.isMath = function(node) {
   2400   return cvox.DomUtil.isMathml(node) ||
   2401       cvox.DomUtil.isMathJax(node) ||
   2402           cvox.DomUtil.isMathImg(node) ||
   2403               cvox.AriaUtil.isMath(node);
   2404 };
   2405 
   2406 
   2407 /**
   2408  * Specifies node classes in which we expect maths expressions a alt text.
   2409  * @type {{tex: Array.<string>,
   2410  *         asciimath: Array.<string>}}
   2411  */
   2412 // These are the classes for which we assume they contain Maths in the ALT or
   2413 // TITLE attribute.
   2414 // tex: Wikipedia;
   2415 // latex: Wordpress;
   2416 // numberedequation, inlineformula, displayformula: MathWorld;
   2417 cvox.DomUtil.ALT_MATH_CLASSES = {
   2418   tex: ['tex', 'latex'],
   2419   asciimath: ['numberedequation', 'inlineformula', 'displayformula']
   2420 };
   2421 
   2422 
   2423 /**
   2424  * Composes a query selector string for image nodes with alt math content by
   2425  * type of content.
   2426  * @param {string} contentType The content type, e.g., tex, asciimath.
   2427  * @return {!string} The query elector string.
   2428  */
   2429 cvox.DomUtil.altMathQuerySelector = function(contentType) {
   2430   var classes = cvox.DomUtil.ALT_MATH_CLASSES[contentType];
   2431   if (classes) {
   2432     return classes.map(function(x) {return 'img.' + x;}).join(', ');
   2433   }
   2434   return '';
   2435 };
   2436 
   2437 
   2438 /**
   2439  * Check if a given node is potentially a math image with alternative text in
   2440  * LaTeX.
   2441  * @param {Node} node The node to be tested.
   2442  * @return {boolean} Whether or not a node has an image with class TeX or LaTeX.
   2443  */
   2444 cvox.DomUtil.isMathImg = function(node) {
   2445   if (!node || !node.tagName || !node.className) {
   2446     return false;
   2447   }
   2448   if (node.tagName != 'IMG') {
   2449     return false;
   2450   }
   2451   var className = node.className.toLowerCase();
   2452   return cvox.DomUtil.ALT_MATH_CLASSES.tex.indexOf(className) != -1 ||
   2453       cvox.DomUtil.ALT_MATH_CLASSES.asciimath.indexOf(className) != -1;
   2454 };
   2455 
   2456 
   2457 /**
   2458  * Checks to see whether a node is a MathML node.
   2459  * !! This is necessary as Chrome currently does not upperCase Math tags !!
   2460  * @param {Node} node The node to be tested.
   2461  * @return {boolean} Whether or not a node is a MathML node.
   2462  */
   2463 cvox.DomUtil.isMathml = function(node) {
   2464   if (!node || !node.tagName) {
   2465     return false;
   2466   }
   2467   return node.tagName.toLowerCase() == 'math';
   2468 };
   2469 
   2470 
   2471 /**
   2472  * Checks to see wether a node is a MathJax node.
   2473  * @param {Node} node The node to be tested.
   2474  * @return {boolean} Whether or not a node is a MathJax node.
   2475  */
   2476 cvox.DomUtil.isMathJax = function(node) {
   2477   if (!node || !node.tagName || !node.className) {
   2478     return false;
   2479   }
   2480 
   2481   function isSpanWithClass(n, cl) {
   2482     return (n.tagName == 'SPAN' &&
   2483             n.className.split(' ').some(function(x) {
   2484                                           return x.toLowerCase() == cl;}));
   2485   };
   2486   if (isSpanWithClass(node, 'math')) {
   2487     var ancestors = cvox.DomUtil.getAncestors(node);
   2488     return ancestors.some(function(x) {return isSpanWithClass(x, 'mathjax');});
   2489   }
   2490   return false;
   2491 };
   2492 
   2493 
   2494 /**
   2495  * Computes the id of the math span in a MathJax DOM element.
   2496  * @param {string} jaxId The id of the MathJax node.
   2497  * @return {string} The id of the span node.
   2498  */
   2499 cvox.DomUtil.getMathSpanId = function(jaxId) {
   2500   var node = document.getElementById(jaxId + '-Frame');
   2501   if (node) {
   2502     var span = node.querySelector('span.math');
   2503     if (span) {
   2504       return span.id;
   2505     }
   2506   }
   2507 };
   2508 
   2509 
   2510 /**
   2511  * Returns true if the node has a longDesc.
   2512  * @param {Node} node The node to be tested.
   2513  * @return {boolean} Whether or not a node has a longDesc.
   2514  */
   2515 cvox.DomUtil.hasLongDesc = function(node) {
   2516   if (node && node.longDesc) {
   2517     return true;
   2518   }
   2519   return false;
   2520 };
   2521 
   2522 
   2523 /**
   2524  * Returns tag name of a node if it has one.
   2525  * @param {Node} node A node.
   2526  * @return {string} A the tag name of the node.
   2527  */
   2528 cvox.DomUtil.getNodeTagName = function(node) {
   2529   if (node.nodeType == Node.ELEMENT_NODE) {
   2530     return node.tagName;
   2531   }
   2532   return '';
   2533 };
   2534 
   2535 
   2536 /**
   2537  * Cleaning up a list of nodes to remove empty text nodes.
   2538  * @param {NodeList} nodes The nodes list.
   2539  * @return {!Array.<Node|string|null>} The cleaned up list of nodes.
   2540  */
   2541 cvox.DomUtil.purgeNodes = function(nodes) {
   2542   return cvox.DomUtil.toArray(nodes).
   2543       filter(function(node) {
   2544                return node.nodeType != Node.TEXT_NODE ||
   2545                    !node.textContent.match(/^\s+$/);});
   2546 };
   2547 
   2548 
   2549 /**
   2550  * Calculates a hit point for a given node.
   2551  * @return {{x:(number), y:(number)}} The position.
   2552  */
   2553 cvox.DomUtil.elementToPoint = function(node) {
   2554   if (!node) {
   2555     return {x: 0, y: 0};
   2556   }
   2557   if (node.constructor == Text) {
   2558     node = node.parentNode;
   2559   }
   2560   var r = node.getBoundingClientRect();
   2561   return {
   2562     x: r.left + (r.width / 2),
   2563     y: r.top + (r.height / 2)
   2564   };
   2565 };
   2566 
   2567 
   2568 /**
   2569  * Checks if an input node supports HTML5 selection.
   2570  * If the node is not an input element, returns false.
   2571  * @param {Node} node The node to check.
   2572  * @return {boolean} True if HTML5 selection supported.
   2573  */
   2574 cvox.DomUtil.doesInputSupportSelection = function(node) {
   2575   return goog.isDef(node) &&
   2576       node.tagName == 'INPUT' &&
   2577       node.type != 'email' &&
   2578       node.type != 'number';
   2579 };
   2580 
   2581 
   2582 /**
   2583  * Gets the hint text for a given element.
   2584  * @param {Node} node The target node.
   2585  * @return {string} The hint text.
   2586  */
   2587 cvox.DomUtil.getHint = function(node) {
   2588   var desc = '';
   2589   if (node.hasAttribute) {
   2590     if (node.hasAttribute('aria-describedby')) {
   2591       var describedByIds = node.getAttribute('aria-describedby').split(' ');
   2592       for (var describedById, i = 0; describedById = describedByIds[i]; i++) {
   2593         var describedNode = document.getElementById(describedById);
   2594         if (describedNode) {
   2595           desc += ' ' + cvox.DomUtil.getName(
   2596               describedNode, true, true, true);
   2597         }
   2598       }
   2599     }
   2600   }
   2601   return desc;
   2602 };
   2603