traverse_util.js revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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 Low-level DOM traversal utility functions to find the 7 * next (or previous) character, word, sentence, line, or paragraph, 8 * in a completely stateless manner without actually manipulating the 9 * selection. 10 */ 11 12goog.provide('cvox.TraverseUtil'); 13 14goog.require('cvox.Cursor'); 15goog.require('cvox.DomPredicates'); 16goog.require('cvox.DomUtil'); 17 18/** 19 * Utility functions for stateless DOM traversal. 20 * @constructor 21 */ 22cvox.TraverseUtil = function() {}; 23 24/** 25 * Gets the text representation of a node. This allows us to substitute 26 * alt text, names, or titles for html elements that provide them. 27 * @param {Node} node A DOM node. 28 * @return {string} A text string representation of the node. 29 */ 30cvox.TraverseUtil.getNodeText = function(node) { 31 if (node.constructor == Text) { 32 return node.data; 33 } else { 34 return ''; 35 } 36}; 37 38/** 39 * Return true if a node should be treated as a leaf node, because 40 * its children are properties of the object that shouldn't be traversed. 41 * 42 * TODO(dmazzoni): replace this with a predicate that detects nodes with 43 * ARIA roles and other objects that have their own description. 44 * For now we just detect a couple of common cases. 45 * 46 * @param {Node} node A DOM node. 47 * @return {boolean} True if the node should be treated as a leaf node. 48 */ 49cvox.TraverseUtil.treatAsLeafNode = function(node) { 50 return node.childNodes.length == 0 || 51 node.nodeName == 'SELECT' || 52 node.getAttribute('role') == 'listbox' || 53 node.nodeName == 'OBJECT'; 54}; 55 56/** 57 * Return true only if a single character is whitespace. 58 * From https://developer.mozilla.org/en/Whitespace_in_the_DOM, 59 * whitespace is defined as one of the characters 60 * "\t" TAB \u0009 61 * "\n" LF \u000A 62 * "\r" CR \u000D 63 * " " SPC \u0020. 64 * 65 * @param {string} c A string containing a single character. 66 * @return {boolean} True if the character is whitespace, otherwise false. 67 */ 68cvox.TraverseUtil.isWhitespace = function(c) { 69 return (c == ' ' || c == '\n' || c == '\r' || c == '\t'); 70}; 71 72/** 73 * Set the selection to the range between the given start and end cursors. 74 * @param {cvox.Cursor} start The desired start of the selection. 75 * @param {cvox.Cursor} end The desired end of the selection. 76 * @return {Selection} the selection object. 77 */ 78cvox.TraverseUtil.setSelection = function(start, end) { 79 var sel = window.getSelection(); 80 sel.removeAllRanges(); 81 var range = document.createRange(); 82 range.setStart(start.node, start.index); 83 range.setEnd(end.node, end.index); 84 sel.addRange(range); 85 86 return sel; 87}; 88 89// TODO(dtseng): Combine with cvox.DomUtil.hasContent. 90/** 91 * Check if this DOM node has the attribute aria-hidden='true', which should 92 * hide it from screen readers. 93 * @param {Node} node An HTML DOM node. 94 * @return {boolean} Whether or not the html node should be traversed. 95 */ 96cvox.TraverseUtil.isHidden = function(node) { 97 if (node instanceof HTMLElement && 98 node.getAttribute('aria-hidden') == 'true') { 99 return true; 100 } 101 switch (node.tagName) { 102 case 'SCRIPT': 103 case 'NOSCRIPT': 104 return true; 105 } 106 return false; 107}; 108 109/** 110 * Moves the cursor forwards until it has crossed exactly one character. 111 * @param {cvox.Cursor} cursor The cursor location where the search should 112 * start. On exit, the cursor will be immediately to the right of the 113 * character returned. 114 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 115 * @param {Array.<Element>} elementsLeft Any HTML elements left. 116 * @return {?string} The character found, or null if the bottom of the 117 * document has been reached. 118 */ 119cvox.TraverseUtil.forwardsChar = function( 120 cursor, elementsEntered, elementsLeft) { 121 while (true) { 122 // Move down until we get to a leaf node. 123 var childNode = null; 124 if (!cvox.TraverseUtil.treatAsLeafNode(cursor.node)) { 125 for (var i = cursor.index; i < cursor.node.childNodes.length; i++) { 126 var node = cursor.node.childNodes[i]; 127 if (cvox.TraverseUtil.isHidden(node)) { 128 if (node instanceof HTMLElement) { 129 elementsEntered.push(node); 130 } 131 continue; 132 } 133 if (cvox.DomUtil.isVisible(node, {checkAncestors: false})) { 134 childNode = node; 135 break; 136 } 137 } 138 } 139 if (childNode) { 140 cursor.node = childNode; 141 cursor.index = 0; 142 cursor.text = cvox.TraverseUtil.getNodeText(cursor.node); 143 if (cursor.node instanceof HTMLElement) { 144 elementsEntered.push(cursor.node); 145 } 146 continue; 147 } 148 149 // Return the next character from this leaf node. 150 if (cursor.index < cursor.text.length) 151 return cursor.text[cursor.index++]; 152 153 // Move to the next sibling, going up the tree as necessary. 154 while (cursor.node != null) { 155 // Try to move to the next sibling. 156 var siblingNode = null; 157 for (var node = cursor.node.nextSibling; 158 node != null; 159 node = node.nextSibling) { 160 if (cvox.TraverseUtil.isHidden(node)) { 161 if (node instanceof HTMLElement) { 162 elementsEntered.push(node); 163 } 164 continue; 165 } 166 if (cvox.DomUtil.isVisible(node, {checkAncestors: false})) { 167 siblingNode = node; 168 break; 169 } 170 } 171 if (siblingNode) { 172 if (cursor.node instanceof HTMLElement) { 173 elementsLeft.push(cursor.node); 174 } 175 176 cursor.node = siblingNode; 177 cursor.text = cvox.TraverseUtil.getNodeText(siblingNode); 178 cursor.index = 0; 179 180 if (cursor.node instanceof HTMLElement) { 181 elementsEntered.push(cursor.node); 182 } 183 184 break; 185 } 186 187 // Otherwise, move to the parent. 188 if (cursor.node.parentNode && 189 cursor.node.parentNode.constructor != HTMLBodyElement) { 190 if (cursor.node instanceof HTMLElement) { 191 elementsLeft.push(cursor.node); 192 } 193 cursor.node = cursor.node.parentNode; 194 cursor.text = null; 195 cursor.index = 0; 196 } else { 197 return null; 198 } 199 } 200 } 201}; 202 203/** 204 * Moves the cursor backwards until it has crossed exactly one character. 205 * @param {cvox.Cursor} cursor The cursor location where the search should 206 * start. On exit, the cursor will be immediately to the left of the 207 * character returned. 208 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 209 * @param {Array.<Element>} elementsLeft Any HTML elements left. 210 * @return {?string} The previous character, or null if the top of the 211 * document has been reached. 212 */ 213cvox.TraverseUtil.backwardsChar = function( 214 cursor, elementsEntered, elementsLeft) { 215 while (true) { 216 // Move down until we get to a leaf node. 217 var childNode = null; 218 if (!cvox.TraverseUtil.treatAsLeafNode(cursor.node)) { 219 for (var i = cursor.index - 1; i >= 0; i--) { 220 var node = cursor.node.childNodes[i]; 221 if (cvox.TraverseUtil.isHidden(node)) { 222 if (node instanceof HTMLElement) { 223 elementsEntered.push(node); 224 } 225 continue; 226 } 227 if (cvox.DomUtil.isVisible(node, {checkAncestors: false})) { 228 childNode = node; 229 break; 230 } 231 } 232 } 233 if (childNode) { 234 cursor.node = childNode; 235 cursor.text = cvox.TraverseUtil.getNodeText(cursor.node); 236 if (cursor.text.length) 237 cursor.index = cursor.text.length; 238 else 239 cursor.index = cursor.node.childNodes.length; 240 if (cursor.node instanceof HTMLElement) { 241 elementsEntered.push(cursor.node); 242 } 243 continue; 244 } 245 246 // Return the previous character from this leaf node. 247 if (cursor.text.length > 0 && cursor.index > 0) { 248 return cursor.text[--cursor.index]; 249 } 250 251 // Move to the previous sibling, going up the tree as necessary. 252 while (true) { 253 // Try to move to the previous sibling. 254 var siblingNode = null; 255 for (var node = cursor.node.previousSibling; 256 node != null; 257 node = node.previousSibling) { 258 if (cvox.TraverseUtil.isHidden(node)) { 259 if (node instanceof HTMLElement) { 260 elementsEntered.push(node); 261 } 262 continue; 263 } 264 if (cvox.DomUtil.isVisible(node, {checkAncestors: false})) { 265 siblingNode = node; 266 break; 267 } 268 } 269 if (siblingNode) { 270 if (cursor.node instanceof HTMLElement) { 271 elementsLeft.push(cursor.node); 272 } 273 274 cursor.node = siblingNode; 275 cursor.text = cvox.TraverseUtil.getNodeText(siblingNode); 276 if (cursor.text.length) 277 cursor.index = cursor.text.length; 278 else 279 cursor.index = cursor.node.childNodes.length; 280 281 if (cursor.node instanceof HTMLElement) { 282 elementsEntered.push(cursor.node); 283 } 284 break; 285 } 286 287 // Otherwise, move to the parent. 288 if (cursor.node.parentNode && 289 cursor.node.parentNode.constructor != HTMLBodyElement) { 290 if (cursor.node instanceof HTMLElement) { 291 elementsLeft.push(cursor.node); 292 } 293 cursor.node = cursor.node.parentNode; 294 cursor.text = null; 295 cursor.index = 0; 296 } else { 297 return null; 298 } 299 } 300 } 301}; 302 303/** 304 * Finds the next character, starting from endCursor. Upon exit, startCursor 305 * and endCursor will surround the next character. If skipWhitespace is 306 * true, will skip until a real character is found. Otherwise, it will 307 * attempt to select all of the whitespace between the initial position 308 * of endCursor and the next non-whitespace character. 309 * @param {!cvox.Cursor} startCursor On exit, points to the position before 310 * the char. 311 * @param {!cvox.Cursor} endCursor The position to start searching for the next 312 * char. On exit, will point to the position past the char. 313 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 314 * @param {Array.<Element>} elementsLeft Any HTML elements left. 315 * initial and final cursor position will be pushed onto this array. 316 * @param {boolean} skipWhitespace If true, will keep scanning until a 317 * non-whitespace character is found. 318 * @return {?string} The next char, or null if the bottom of the 319 * document has been reached. 320 */ 321cvox.TraverseUtil.getNextChar = function( 322 startCursor, endCursor, elementsEntered, elementsLeft, skipWhitespace) { 323 324 // Save the starting position and get the first character. 325 startCursor.copyFrom(endCursor); 326 var c = cvox.TraverseUtil.forwardsChar( 327 endCursor, elementsEntered, elementsLeft); 328 if (c == null) 329 return null; 330 331 // Keep track of whether the first character was whitespace. 332 var initialWhitespace = cvox.TraverseUtil.isWhitespace(c); 333 334 // Keep scanning until we find a non-whitespace or non-skipped character. 335 while ((cvox.TraverseUtil.isWhitespace(c)) || 336 (cvox.TraverseUtil.isHidden(endCursor.node))) { 337 c = cvox.TraverseUtil.forwardsChar( 338 endCursor, elementsEntered, elementsLeft); 339 if (c == null) 340 return null; 341 } 342 if (skipWhitespace || !initialWhitespace) { 343 // If skipWhitepace is true, or if the first character we encountered 344 // was not whitespace, return that non-whitespace character. 345 startCursor.copyFrom(endCursor); 346 startCursor.index--; 347 return c; 348 } 349 else { 350 for (var i = 0; i < elementsEntered.length; i++) { 351 if (cvox.TraverseUtil.isHidden(elementsEntered[i])) { 352 // We need to make sure that startCursor and endCursor aren't 353 // surrounding a skippable node. 354 endCursor.index--; 355 startCursor.copyFrom(endCursor); 356 startCursor.index--; 357 return ' '; 358 } 359 } 360 // Otherwise, return all of the whitespace before that last character. 361 endCursor.index--; 362 return ' '; 363 } 364}; 365 366/** 367 * Finds the previous character, starting from startCursor. Upon exit, 368 * startCursor and endCursor will surround the previous character. 369 * If skipWhitespace is true, will skip until a real character is found. 370 * Otherwise, it will attempt to select all of the whitespace between 371 * the initial position of endCursor and the next non-whitespace character. 372 * @param {!cvox.Cursor} startCursor The position to start searching for the 373 * char. On exit, will point to the position before the char. 374 * @param {!cvox.Cursor} endCursor The position to start searching for the next 375 * char. On exit, will point to the position past the char. 376 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 377 * @param {Array.<Element>} elementsLeft Any HTML elements left. 378 * initial and final cursor position will be pushed onto this array. 379 * @param {boolean} skipWhitespace If true, will keep scanning until a 380 * non-whitespace character is found. 381 * @return {?string} The previous char, or null if the top of the 382 * document has been reached. 383 */ 384cvox.TraverseUtil.getPreviousChar = function( 385 startCursor, endCursor, elementsEntered, elementsLeft, skipWhitespace) { 386 387 // Save the starting position and get the first character. 388 endCursor.copyFrom(startCursor); 389 var c = cvox.TraverseUtil.backwardsChar( 390 startCursor, elementsEntered, elementsLeft); 391 if (c == null) 392 return null; 393 394 // Keep track of whether the first character was whitespace. 395 var initialWhitespace = cvox.TraverseUtil.isWhitespace(c); 396 397 // Keep scanning until we find a non-whitespace or non-skipped character. 398 while ((cvox.TraverseUtil.isWhitespace(c)) || 399 (cvox.TraverseUtil.isHidden(startCursor.node))) { 400 c = cvox.TraverseUtil.backwardsChar( 401 startCursor, elementsEntered, elementsLeft); 402 if (c == null) 403 return null; 404 } 405 if (skipWhitespace || !initialWhitespace) { 406 // If skipWhitepace is true, or if the first character we encountered 407 // was not whitespace, return that non-whitespace character. 408 endCursor.copyFrom(startCursor); 409 endCursor.index++; 410 return c; 411 } else { 412 for (var i = 0; i < elementsEntered.length; i++) { 413 if (cvox.TraverseUtil.isHidden(elementsEntered[i])) { 414 startCursor.index++; 415 endCursor.copyFrom(startCursor); 416 endCursor.index++; 417 return ' '; 418 } 419 } 420 // Otherwise, return all of the whitespace before that last character. 421 startCursor.index++; 422 return ' '; 423 } 424}; 425 426/** 427 * Finds the next word, starting from endCursor. Upon exit, startCursor 428 * and endCursor will surround the next word. A word is defined to be 429 * a string of 1 or more non-whitespace characters in the same DOM node. 430 * @param {cvox.Cursor} startCursor On exit, will point to the beginning of the 431 * word returned. 432 * @param {cvox.Cursor} endCursor The position to start searching for the next 433 * word. On exit, will point to the end of the word returned. 434 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 435 * @param {Array.<Element>} elementsLeft Any HTML elements left. 436 * @return {?string} The next word, or null if the bottom of the 437 * document has been reached. 438 */ 439cvox.TraverseUtil.getNextWord = function(startCursor, endCursor, 440 elementsEntered, elementsLeft) { 441 442 // Find the first non-whitespace or non-skipped character. 443 var cursor = endCursor.clone(); 444 var c = cvox.TraverseUtil.forwardsChar(cursor, elementsEntered, elementsLeft); 445 if (c == null) 446 return null; 447 while ((cvox.TraverseUtil.isWhitespace(c)) || 448 (cvox.TraverseUtil.isHidden(cursor.node))) { 449 c = cvox.TraverseUtil.forwardsChar(cursor, elementsEntered, elementsLeft); 450 if (c == null) 451 return null; 452 } 453 454 // Set startCursor to the position immediately before the first 455 // character in our word. It's safe to decrement |index| because 456 // forwardsChar guarantees that the cursor will be immediately to the 457 // right of the returned character on exit. 458 startCursor.copyFrom(cursor); 459 startCursor.index--; 460 461 // Keep building up our word until we reach a whitespace character or 462 // would cross a tag. Don't actually return any tags crossed, because this 463 // word goes up until the tag boundary but not past it. 464 endCursor.copyFrom(cursor); 465 var word = c; 466 var newEntered = []; 467 var newLeft = []; 468 c = cvox.TraverseUtil.forwardsChar(cursor, newEntered, newLeft); 469 if (c == null) { 470 return word; 471 } 472 while (!cvox.TraverseUtil.isWhitespace(c) && 473 newEntered.length == 0 && 474 newLeft == 0) { 475 word += c; 476 endCursor.copyFrom(cursor); 477 c = cvox.TraverseUtil.forwardsChar(cursor, newEntered, newLeft); 478 if (c == null) { 479 return word; 480 } 481 } 482 483 return word; 484}; 485 486/** 487 * Finds the previous word, starting from startCursor. Upon exit, startCursor 488 * and endCursor will surround the previous word. A word is defined to be 489 * a string of 1 or more non-whitespace characters in the same DOM node. 490 * @param {cvox.Cursor} startCursor The position to start searching for the 491 * previous word. On exit, will point to the beginning of the 492 * word returned. 493 * @param {cvox.Cursor} endCursor On exit, will point to the end of the 494 * word returned. 495 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 496 * @param {Array.<Element>} elementsLeft Any HTML elements left. 497 * @return {?string} The previous word, or null if the bottom of the 498 * document has been reached. 499 */ 500cvox.TraverseUtil.getPreviousWord = function(startCursor, endCursor, 501 elementsEntered, elementsLeft) { 502 // Find the first non-whitespace or non-skipped character. 503 var cursor = startCursor.clone(); 504 var c = cvox.TraverseUtil.backwardsChar( 505 cursor, elementsEntered, elementsLeft); 506 if (c == null) 507 return null; 508 while ((cvox.TraverseUtil.isWhitespace(c) || 509 (cvox.TraverseUtil.isHidden(cursor.node)))) { 510 c = cvox.TraverseUtil.backwardsChar(cursor, elementsEntered, elementsLeft); 511 if (c == null) 512 return null; 513 } 514 515 // Set endCursor to the position immediately after the first 516 // character we've found (the last character of the word, since we're 517 // searching backwards). 518 endCursor.copyFrom(cursor); 519 endCursor.index++; 520 521 // Keep building up our word until we reach a whitespace character or 522 // would cross a tag. Don't actually return any tags crossed, because this 523 // word goes up until the tag boundary but not past it. 524 startCursor.copyFrom(cursor); 525 var word = c; 526 var newEntered = []; 527 var newLeft = []; 528 c = cvox.TraverseUtil.backwardsChar(cursor, newEntered, newLeft); 529 if (c == null) 530 return word; 531 while (!cvox.TraverseUtil.isWhitespace(c) && 532 newEntered.length == 0 && 533 newLeft.length == 0) { 534 word = c + word; 535 startCursor.copyFrom(cursor); 536 537 c = cvox.TraverseUtil.backwardsChar(cursor, newEntered, newLeft); 538 if (c == null) 539 return word; 540 } 541 542 return word; 543}; 544 545 546/** 547 * Given elements entered and left, and break tags, returns true if the 548 * current word should break. 549 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 550 * @param {Array.<Element>} elementsLeft Any HTML elements left. 551 * @param {Object.<string, boolean>} breakTags Associative array of tags 552 * that should break. 553 * @return {boolean} True if elementsEntered or elementsLeft include an 554 * element with one of these tags. 555 */ 556cvox.TraverseUtil.includesBreakTagOrSkippedNode = function( 557 elementsEntered, elementsLeft, breakTags) { 558 for (var i = 0; i < elementsEntered.length; i++) { 559 if (cvox.TraverseUtil.isHidden(elementsEntered[i])) { 560 return true; 561 } 562 var style = window.getComputedStyle(elementsEntered[i], null); 563 if ((style && style.display != 'inline') || 564 breakTags[elementsEntered[i].tagName]) { 565 return true; 566 } 567 } 568 for (i = 0; i < elementsLeft.length; i++) { 569 var style = window.getComputedStyle(elementsLeft[i], null); 570 if ((style && style.display != 'inline') || 571 breakTags[elementsLeft[i].tagName]) { 572 return true; 573 } 574 } 575 return false; 576}; 577 578 579/** 580 * Finds the next sentence, starting from endCursor. Upon exit, 581 * startCursor and endCursor will surround the next sentence. 582 * 583 * @param {cvox.Cursor} startCursor On exit, marks the beginning of the 584 * sentence. 585 * @param {cvox.Cursor} endCursor The position to start searching for the next 586 * sentence. On exit, will point to the end of the returned string. 587 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 588 * @param {Array.<Element>} elementsLeft Any HTML elements left. 589 * @param {Object.<string, boolean>} breakTags Associative array of tags 590 * that should break the sentence. 591 * @return {?string} The next sentence, or null if the bottom of the 592 * document has been reached. 593 */ 594cvox.TraverseUtil.getNextSentence = function( 595 startCursor, endCursor, elementsEntered, elementsLeft, breakTags) { 596 return cvox.TraverseUtil.getNextString( 597 startCursor, endCursor, elementsEntered, elementsLeft, 598 function(str, word, elementsEntered, elementsLeft) { 599 if (str.substr(-1) == '.') 600 return true; 601 return cvox.TraverseUtil.includesBreakTagOrSkippedNode( 602 elementsEntered, elementsLeft, breakTags); 603 }); 604}; 605 606/** 607 * Finds the previous sentence, starting from startCursor. Upon exit, 608 * startCursor and endCursor will surround the previous sentence. 609 * 610 * @param {cvox.Cursor} startCursor The position to start searching for the next 611 * sentence. On exit, will point to the start of the returned string. 612 * @param {cvox.Cursor} endCursor On exit, the end of the returned string. 613 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 614 * @param {Array.<Element>} elementsLeft Any HTML elements left. 615 * @param {Object.<string, boolean>} breakTags Associative array of tags 616 * that should break the sentence. 617 * @return {?string} The previous sentence, or null if the bottom of the 618 * document has been reached. 619 */ 620cvox.TraverseUtil.getPreviousSentence = function( 621 startCursor, endCursor, elementsEntered, elementsLeft, breakTags) { 622 return cvox.TraverseUtil.getPreviousString( 623 startCursor, endCursor, elementsEntered, elementsLeft, 624 function(str, word, elementsEntered, elementsLeft) { 625 if (word.substr(-1) == '.') 626 return true; 627 return cvox.TraverseUtil.includesBreakTagOrSkippedNode( 628 elementsEntered, elementsLeft, breakTags); 629 }); 630}; 631 632/** 633 * Finds the next line, starting from endCursor. Upon exit, 634 * startCursor and endCursor will surround the next line. 635 * 636 * @param {cvox.Cursor} startCursor On exit, marks the beginning of the line. 637 * @param {cvox.Cursor} endCursor The position to start searching for the next 638 * line. On exit, will point to the end of the returned string. 639 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 640 * @param {Array.<Element>} elementsLeft Any HTML elements left. 641 * @param {Object.<string, boolean>} breakTags Associative array of tags 642 * that should break the line. 643 * @return {?string} The next line, or null if the bottom of the 644 * document has been reached. 645 */ 646cvox.TraverseUtil.getNextLine = function( 647 startCursor, endCursor, elementsEntered, elementsLeft, breakTags) { 648 var range = document.createRange(); 649 var currentRect = null; 650 var rightMostRect = null; 651 var prevCursor = endCursor.clone(); 652 return cvox.TraverseUtil.getNextString( 653 startCursor, endCursor, elementsEntered, elementsLeft, 654 function(str, word, elementsEntered, elementsLeft) { 655 range.setStart(startCursor.node, startCursor.index); 656 range.setEnd(endCursor.node, endCursor.index); 657 var currentRect = range.getBoundingClientRect(); 658 if (!rightMostRect) { 659 rightMostRect = currentRect; 660 } 661 662 // Break at new lines except when within a link. 663 if (currentRect.bottom != rightMostRect.bottom && 664 !cvox.DomPredicates.linkPredicate(cvox.DomUtil.getAncestors( 665 endCursor.node))) { 666 endCursor.copyFrom(prevCursor); 667 return true; 668 } 669 670 rightMostRect = currentRect; 671 prevCursor.copyFrom(endCursor); 672 673 return cvox.TraverseUtil.includesBreakTagOrSkippedNode( 674 elementsEntered, elementsLeft, breakTags); 675 }); 676}; 677 678/** 679 * Finds the previous line, starting from startCursor. Upon exit, 680 * startCursor and endCursor will surround the previous line. 681 * 682 * @param {cvox.Cursor} startCursor The position to start searching for the next 683 * line. On exit, will point to the start of the returned string. 684 * @param {cvox.Cursor} endCursor On exit, the end of the returned string. 685 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 686 * @param {Array.<Element>} elementsLeft Any HTML elements left. 687 * @param {Object.<string, boolean>} breakTags Associative array of tags 688 * that should break the line. 689 * @return {?string} The previous line, or null if the bottom of the 690 * document has been reached. 691 */ 692cvox.TraverseUtil.getPreviousLine = function( 693 startCursor, endCursor, elementsEntered, elementsLeft, breakTags) { 694 var range = document.createRange(); 695 var currentRect = null; 696 var leftMostRect = null; 697 var prevCursor = startCursor.clone(); 698 return cvox.TraverseUtil.getPreviousString( 699 startCursor, endCursor, elementsEntered, elementsLeft, 700 function(str, word, elementsEntered, elementsLeft) { 701 range.setStart(startCursor.node, startCursor.index); 702 range.setEnd(endCursor.node, endCursor.index); 703 var currentRect = range.getBoundingClientRect(); 704 if (!leftMostRect) { 705 leftMostRect = currentRect; 706 } 707 708 // Break at new lines except when within a link. 709 if (currentRect.top != leftMostRect.top && 710 !cvox.DomPredicates.linkPredicate(cvox.DomUtil.getAncestors( 711 startCursor.node))) { 712 startCursor.copyFrom(prevCursor); 713 return true; 714 } 715 716 leftMostRect = currentRect; 717 prevCursor.copyFrom(startCursor); 718 719 return cvox.TraverseUtil.includesBreakTagOrSkippedNode( 720 elementsEntered, elementsLeft, breakTags); 721 }); 722}; 723 724/** 725 * Finds the next paragraph, starting from endCursor. Upon exit, 726 * startCursor and endCursor will surround the next paragraph. 727 * 728 * @param {cvox.Cursor} startCursor On exit, marks the beginning of the 729 * paragraph. 730 * @param {cvox.Cursor} endCursor The position to start searching for the next 731 * paragraph. On exit, will point to the end of the returned string. 732 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 733 * @param {Array.<Element>} elementsLeft Any HTML elements left. 734 * @return {?string} The next paragraph, or null if the bottom of the 735 * document has been reached. 736 */ 737cvox.TraverseUtil.getNextParagraph = function(startCursor, endCursor, 738 elementsEntered, elementsLeft) { 739 return cvox.TraverseUtil.getNextString( 740 startCursor, endCursor, elementsEntered, elementsLeft, 741 function(str, word, elementsEntered, elementsLeft) { 742 for (var i = 0; i < elementsEntered.length; i++) { 743 if (cvox.TraverseUtil.isHidden(elementsEntered[i])) { 744 return true; 745 } 746 var style = window.getComputedStyle(elementsEntered[i], null); 747 if (style && style.display != 'inline') { 748 return true; 749 } 750 } 751 for (i = 0; i < elementsLeft.length; i++) { 752 var style = window.getComputedStyle(elementsLeft[i], null); 753 if (style && style.display != 'inline') { 754 return true; 755 } 756 } 757 return false; 758 }); 759}; 760 761/** 762 * Finds the previous paragraph, starting from startCursor. Upon exit, 763 * startCursor and endCursor will surround the previous paragraph. 764 * 765 * @param {cvox.Cursor} startCursor The position to start searching for the next 766 * paragraph. On exit, will point to the start of the returned string. 767 * @param {cvox.Cursor} endCursor On exit, the end of the returned string. 768 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 769 * @param {Array.<Element>} elementsLeft Any HTML elements left. 770 * @return {?string} The previous paragraph, or null if the bottom of the 771 * document has been reached. 772 */ 773cvox.TraverseUtil.getPreviousParagraph = function( 774 startCursor, endCursor, elementsEntered, elementsLeft) { 775 return cvox.TraverseUtil.getPreviousString( 776 startCursor, endCursor, elementsEntered, elementsLeft, 777 function(str, word, elementsEntered, elementsLeft) { 778 for (var i = 0; i < elementsEntered.length; i++) { 779 if (cvox.TraverseUtil.isHidden(elementsEntered[i])) { 780 return true; 781 } 782 var style = window.getComputedStyle(elementsEntered[i], null); 783 if (style && style.display != 'inline') { 784 return true; 785 } 786 } 787 for (i = 0; i < elementsLeft.length; i++) { 788 var style = window.getComputedStyle(elementsLeft[i], null); 789 if (style && style.display != 'inline') { 790 return true; 791 } 792 } 793 return false; 794 }); 795}; 796 797/** 798 * Customizable function to return the next string of words in the DOM, based 799 * on provided functions to decide when to break one string and start 800 * the next. This can be used to get the next sentence, line, paragraph, 801 * or potentially other granularities. 802 * 803 * Finds the next contiguous string, starting from endCursor. Upon exit, 804 * startCursor and endCursor will surround the next string. 805 * 806 * The breakBefore function takes four parameters, and 807 * should return true if the string should be broken before the proposed 808 * next word: 809 * str The string so far. 810 * word The next word to be added. 811 * elementsEntered The elements entered in reaching this next word. 812 * elementsLeft The elements left in reaching this next word. 813 * 814 * @param {cvox.Cursor} startCursor On exit, will point to the beginning of the 815 * next string. 816 * @param {cvox.Cursor} endCursor The position to start searching for the next 817 * string. On exit, will point to the end of the returned string. 818 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 819 * @param {Array.<Element>} elementsLeft Any HTML elements left. 820 * @param {function(string, string, Array.<Element>, Array.<Element>)} 821 * breakBefore Function that takes the string so far, next word to be 822 * added, and elements entered and left, and returns true if the string 823 * should be ended before adding this word. 824 * @return {?string} The next string, or null if the bottom of the 825 * document has been reached. 826 */ 827cvox.TraverseUtil.getNextString = function( 828 startCursor, endCursor, elementsEntered, elementsLeft, breakBefore) { 829 // Get the first word and set the start cursor to the start of the 830 // first word. 831 var wordStartCursor = endCursor.clone(); 832 var wordEndCursor = endCursor.clone(); 833 var newEntered = []; 834 var newLeft = []; 835 var str = ''; 836 var word = cvox.TraverseUtil.getNextWord( 837 wordStartCursor, wordEndCursor, newEntered, newLeft); 838 if (word == null) 839 return null; 840 startCursor.copyFrom(wordStartCursor); 841 842 // Always add the first word when the string is empty, and then keep 843 // adding more words as long as breakBefore returns false 844 while (!str || !breakBefore(str, word, newEntered, newLeft)) { 845 // Append this word, set the end cursor to the end of this word, and 846 // update the returned list of nodes crossed to include ones we crossed 847 // in reaching this word. 848 if (str) 849 str += ' '; 850 str += word; 851 elementsEntered = elementsEntered.concat(newEntered); 852 elementsLeft = elementsLeft.concat(newLeft); 853 endCursor.copyFrom(wordEndCursor); 854 855 // Get the next word and go back to the top of the loop. 856 newEntered = []; 857 newLeft = []; 858 word = cvox.TraverseUtil.getNextWord( 859 wordStartCursor, wordEndCursor, newEntered, newLeft); 860 if (word == null) 861 return str; 862 } 863 864 return str; 865}; 866 867/** 868 * Customizable function to return the previous string of words in the DOM, 869 * based on provided functions to decide when to break one string and start 870 * the next. See getNextString, above, for more details. 871 * 872 * Finds the previous contiguous string, starting from startCursor. Upon exit, 873 * startCursor and endCursor will surround the next string. 874 * 875 * @param {cvox.Cursor} startCursor The position to start searching for the 876 * previous string. On exit, will point to the beginning of the 877 * string returned. 878 * @param {cvox.Cursor} endCursor On exit, will point to the end of the 879 * string returned. 880 * @param {Array.<Element>} elementsEntered Any HTML elements entered. 881 * @param {Array.<Element>} elementsLeft Any HTML elements left. 882 * @param {function(string, string, Array.<Element>, Array.<Element>)} 883 * breakBefore Function that takes the string so far, the word to be 884 * added, and nodes crossed, and returns true if the string should be 885 * ended before adding this word. 886 * @return {?string} The next string, or null if the top of the 887 * document has been reached. 888 */ 889cvox.TraverseUtil.getPreviousString = function( 890 startCursor, endCursor, elementsEntered, elementsLeft, breakBefore) { 891 // Get the first word and set the end cursor to the end of the 892 // first word. 893 var wordStartCursor = startCursor.clone(); 894 var wordEndCursor = startCursor.clone(); 895 var newEntered = []; 896 var newLeft = []; 897 var str = ''; 898 var word = cvox.TraverseUtil.getPreviousWord( 899 wordStartCursor, wordEndCursor, newEntered, newLeft); 900 if (word == null) 901 return null; 902 endCursor.copyFrom(wordEndCursor); 903 904 // Always add the first word when the string is empty, and then keep 905 // adding more words as long as breakBefore returns false 906 while (!str || !breakBefore(str, word, newEntered, newLeft)) { 907 // Prepend this word, set the start cursor to the start of this word, and 908 // update the returned list of nodes crossed to include ones we crossed 909 // in reaching this word. 910 if (str) 911 str = ' ' + str; 912 str = word + str; 913 elementsEntered = elementsEntered.concat(newEntered); 914 elementsLeft = elementsLeft.concat(newLeft); 915 startCursor.copyFrom(wordStartCursor); 916 917 // Get the previous word and go back to the top of the loop. 918 newEntered = []; 919 newLeft = []; 920 word = cvox.TraverseUtil.getPreviousWord( 921 wordStartCursor, wordEndCursor, newEntered, newLeft); 922 if (word == null) 923 return str; 924 } 925 926 return str; 927}; 928