dom_util.js revision 116680a4aac90f2aa7413d9095a592090648e557
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.NodeState');
18goog.require('cvox.XpathUtil');
19
20
21
22/**
23 * Create the namespace
24 * @constructor
25 */
26cvox.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 */
36cvox.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 */
67cvox.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 */
99cvox.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 */
112cvox.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 */
131cvox.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 */
174cvox.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 */
196cvox.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 */
239cvox.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 */
265cvox.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 */
285cvox.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 */
308cvox.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 */
371cvox.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 */
398cvox.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 */
415cvox.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 */
430cvox.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 */
473cvox.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 */
493cvox.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 */
527cvox.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 */
549cvox.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 */
588cvox.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 */
659cvox.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 */
692cvox.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 */
738cvox.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 */
819cvox.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 */
883cvox.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 */
917cvox.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 */
952cvox.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 */
1134cvox.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 */
1162cvox.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 */
1187cvox.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 */
1205cvox.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 */
1257cvox.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 */
1271cvox.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 */
1301cvox.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 */
1368cvox.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 */
1387cvox.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 */
1422cvox.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 */
1442cvox.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 */
1455cvox.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 */
1477cvox.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 */
1549cvox.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 */
1580cvox.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 */
1610cvox.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 */
1641cvox.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 */
1656cvox.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 */
1683cvox.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 */
1729cvox.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 */
1744cvox.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 */
1758cvox.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 */
1782cvox.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 */
1810cvox.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 */
1848cvox.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 */
1868cvox.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 */
1885cvox.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 */
1914cvox.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 */
1929cvox.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 */
1953cvox.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 */
2046cvox.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 */
2057cvox.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 */
2075cvox.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 */
2114cvox.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 */
2140cvox.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 */
2153cvox.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 */
2175cvox.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 */
2201cvox.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 */
2217cvox.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 */
2245cvox.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 */
2261cvox.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 */
2276cvox.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 */
2294cvox.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 */
2306cvox.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 */
2322cvox.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 */
2334cvox.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 */
2349cvox.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;
2367cvox.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 */
2379cvox.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 */
2394cvox.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 */
2413cvox.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 */
2426cvox.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 */
2449cvox.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 */
2465cvox.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 */
2478cvox.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 */
2491cvox.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 */
2503cvox.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 */
2524cvox.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 */
2537cvox.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