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