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