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