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