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