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