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