diff options
Diffstat (limited to 'chromium/chrome/browser/resources/chromeos/chromevox/common/traverse_util.js')
-rw-r--r-- | chromium/chrome/browser/resources/chromeos/chromevox/common/traverse_util.js | 927 |
1 files changed, 927 insertions, 0 deletions
diff --git a/chromium/chrome/browser/resources/chromeos/chromevox/common/traverse_util.js b/chromium/chrome/browser/resources/chromeos/chromevox/common/traverse_util.js new file mode 100644 index 00000000000..18781e376d6 --- /dev/null +++ b/chromium/chrome/browser/resources/chromeos/chromevox/common/traverse_util.js @@ -0,0 +1,927 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview Low-level DOM traversal utility functions to find the + * next (or previous) character, word, sentence, line, or paragraph, + * in a completely stateless manner without actually manipulating the + * selection. + */ + +goog.provide('cvox.TraverseUtil'); + +goog.require('cvox.Cursor'); +goog.require('cvox.DomPredicates'); +goog.require('cvox.DomUtil'); + +/** + * Utility functions for stateless DOM traversal. + * @constructor + */ +cvox.TraverseUtil = function() {}; + +/** + * Gets the text representation of a node. This allows us to substitute + * alt text, names, or titles for html elements that provide them. + * @param {Node} node A DOM node. + * @return {string} A text string representation of the node. + */ +cvox.TraverseUtil.getNodeText = function(node) { + if (node.constructor == Text) { + return node.data; + } else { + return ''; + } +}; + +/** + * Return true if a node should be treated as a leaf node, because + * its children are properties of the object that shouldn't be traversed. + * + * TODO(dmazzoni): replace this with a predicate that detects nodes with + * ARIA roles and other objects that have their own description. + * For now we just detect a couple of common cases. + * + * @param {Node} node A DOM node. + * @return {boolean} True if the node should be treated as a leaf node. + */ +cvox.TraverseUtil.treatAsLeafNode = function(node) { + return node.childNodes.length == 0 || + node.nodeName == 'SELECT' || + node.getAttribute('role') == 'listbox' || + node.nodeName == 'OBJECT'; +}; + +/** + * Return true only if a single character is whitespace. + * From https://developer.mozilla.org/en/Whitespace_in_the_DOM, + * whitespace is defined as one of the characters + * "\t" TAB \u0009 + * "\n" LF \u000A + * "\r" CR \u000D + * " " SPC \u0020. + * + * @param {string} c A string containing a single character. + * @return {boolean} True if the character is whitespace, otherwise false. + */ +cvox.TraverseUtil.isWhitespace = function(c) { + return (c == ' ' || c == '\n' || c == '\r' || c == '\t'); +}; + +/** + * Set the selection to the range between the given start and end cursors. + * @param {cvox.Cursor} start The desired start of the selection. + * @param {cvox.Cursor} end The desired end of the selection. + * @return {Selection} the selection object. + */ +cvox.TraverseUtil.setSelection = function(start, end) { + var sel = window.getSelection(); + sel.removeAllRanges(); + var range = document.createRange(); + range.setStart(start.node, start.index); + range.setEnd(end.node, end.index); + sel.addRange(range); + + return sel; +}; + +// TODO(dtseng): Combine with cvox.DomUtil.hasContent. +/** + * Check if this DOM node has the attribute aria-hidden='true', which should + * hide it from screen readers. + * @param {Node} node An HTML DOM node. + * @return {boolean} Whether or not the html node should be traversed. + */ +cvox.TraverseUtil.isHidden = function(node) { + if (node instanceof HTMLElement && + node.getAttribute('aria-hidden') == 'true') { + return true; + } + switch (node.tagName) { + case 'SCRIPT': + case 'NOSCRIPT': + return true; + } + return false; +}; + +/** + * Moves the cursor forwards until it has crossed exactly one character. + * @param {cvox.Cursor} cursor The cursor location where the search should + * start. On exit, the cursor will be immediately to the right of the + * character returned. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * @return {?string} The character found, or null if the bottom of the + * document has been reached. + */ +cvox.TraverseUtil.forwardsChar = function( + cursor, elementsEntered, elementsLeft) { + while (true) { + // Move down until we get to a leaf node. + var childNode = null; + if (!cvox.TraverseUtil.treatAsLeafNode(cursor.node)) { + for (var i = cursor.index; i < cursor.node.childNodes.length; i++) { + var node = cursor.node.childNodes[i]; + if (cvox.TraverseUtil.isHidden(node)) { + if (node instanceof HTMLElement) { + elementsEntered.push(node); + } + continue; + } + if (cvox.DomUtil.isVisible(node, {checkAncestors: false})) { + childNode = node; + break; + } + } + } + if (childNode) { + cursor.node = childNode; + cursor.index = 0; + cursor.text = cvox.TraverseUtil.getNodeText(cursor.node); + if (cursor.node instanceof HTMLElement) { + elementsEntered.push(cursor.node); + } + continue; + } + + // Return the next character from this leaf node. + if (cursor.index < cursor.text.length) + return cursor.text[cursor.index++]; + + // Move to the next sibling, going up the tree as necessary. + while (cursor.node != null) { + // Try to move to the next sibling. + var siblingNode = null; + for (var node = cursor.node.nextSibling; + node != null; + node = node.nextSibling) { + if (cvox.TraverseUtil.isHidden(node)) { + if (node instanceof HTMLElement) { + elementsEntered.push(node); + } + continue; + } + if (cvox.DomUtil.isVisible(node, {checkAncestors: false})) { + siblingNode = node; + break; + } + } + if (siblingNode) { + if (cursor.node instanceof HTMLElement) { + elementsLeft.push(cursor.node); + } + + cursor.node = siblingNode; + cursor.text = cvox.TraverseUtil.getNodeText(siblingNode); + cursor.index = 0; + + if (cursor.node instanceof HTMLElement) { + elementsEntered.push(cursor.node); + } + + break; + } + + // Otherwise, move to the parent. + if (cursor.node.parentNode && + cursor.node.parentNode.constructor != HTMLBodyElement) { + if (cursor.node instanceof HTMLElement) { + elementsLeft.push(cursor.node); + } + cursor.node = cursor.node.parentNode; + cursor.text = null; + cursor.index = 0; + } else { + return null; + } + } + } +}; + +/** + * Moves the cursor backwards until it has crossed exactly one character. + * @param {cvox.Cursor} cursor The cursor location where the search should + * start. On exit, the cursor will be immediately to the left of the + * character returned. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * @return {?string} The previous character, or null if the top of the + * document has been reached. + */ +cvox.TraverseUtil.backwardsChar = function( + cursor, elementsEntered, elementsLeft) { + while (true) { + // Move down until we get to a leaf node. + var childNode = null; + if (!cvox.TraverseUtil.treatAsLeafNode(cursor.node)) { + for (var i = cursor.index - 1; i >= 0; i--) { + var node = cursor.node.childNodes[i]; + if (cvox.TraverseUtil.isHidden(node)) { + if (node instanceof HTMLElement) { + elementsEntered.push(node); + } + continue; + } + if (cvox.DomUtil.isVisible(node, {checkAncestors: false})) { + childNode = node; + break; + } + } + } + if (childNode) { + cursor.node = childNode; + cursor.text = cvox.TraverseUtil.getNodeText(cursor.node); + if (cursor.text.length) + cursor.index = cursor.text.length; + else + cursor.index = cursor.node.childNodes.length; + if (cursor.node instanceof HTMLElement) { + elementsEntered.push(cursor.node); + } + continue; + } + + // Return the previous character from this leaf node. + if (cursor.text.length > 0 && cursor.index > 0) { + return cursor.text[--cursor.index]; + } + + // Move to the previous sibling, going up the tree as necessary. + while (true) { + // Try to move to the previous sibling. + var siblingNode = null; + for (var node = cursor.node.previousSibling; + node != null; + node = node.previousSibling) { + if (cvox.TraverseUtil.isHidden(node)) { + if (node instanceof HTMLElement) { + elementsEntered.push(node); + } + continue; + } + if (cvox.DomUtil.isVisible(node, {checkAncestors: false})) { + siblingNode = node; + break; + } + } + if (siblingNode) { + if (cursor.node instanceof HTMLElement) { + elementsLeft.push(cursor.node); + } + + cursor.node = siblingNode; + cursor.text = cvox.TraverseUtil.getNodeText(siblingNode); + if (cursor.text.length) + cursor.index = cursor.text.length; + else + cursor.index = cursor.node.childNodes.length; + + if (cursor.node instanceof HTMLElement) { + elementsEntered.push(cursor.node); + } + break; + } + + // Otherwise, move to the parent. + if (cursor.node.parentNode && + cursor.node.parentNode.constructor != HTMLBodyElement) { + if (cursor.node instanceof HTMLElement) { + elementsLeft.push(cursor.node); + } + cursor.node = cursor.node.parentNode; + cursor.text = null; + cursor.index = 0; + } else { + return null; + } + } + } +}; + +/** + * Finds the next character, starting from endCursor. Upon exit, startCursor + * and endCursor will surround the next character. If skipWhitespace is + * true, will skip until a real character is found. Otherwise, it will + * attempt to select all of the whitespace between the initial position + * of endCursor and the next non-whitespace character. + * @param {!cvox.Cursor} startCursor On exit, points to the position before + * the char. + * @param {!cvox.Cursor} endCursor The position to start searching for the next + * char. On exit, will point to the position past the char. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * initial and final cursor position will be pushed onto this array. + * @param {boolean} skipWhitespace If true, will keep scanning until a + * non-whitespace character is found. + * @return {?string} The next char, or null if the bottom of the + * document has been reached. + */ +cvox.TraverseUtil.getNextChar = function( + startCursor, endCursor, elementsEntered, elementsLeft, skipWhitespace) { + + // Save the starting position and get the first character. + startCursor.copyFrom(endCursor); + var c = cvox.TraverseUtil.forwardsChar( + endCursor, elementsEntered, elementsLeft); + if (c == null) + return null; + + // Keep track of whether the first character was whitespace. + var initialWhitespace = cvox.TraverseUtil.isWhitespace(c); + + // Keep scanning until we find a non-whitespace or non-skipped character. + while ((cvox.TraverseUtil.isWhitespace(c)) || + (cvox.TraverseUtil.isHidden(endCursor.node))) { + c = cvox.TraverseUtil.forwardsChar( + endCursor, elementsEntered, elementsLeft); + if (c == null) + return null; + } + if (skipWhitespace || !initialWhitespace) { + // If skipWhitepace is true, or if the first character we encountered + // was not whitespace, return that non-whitespace character. + startCursor.copyFrom(endCursor); + startCursor.index--; + return c; + } + else { + for (var i = 0; i < elementsEntered.length; i++) { + if (cvox.TraverseUtil.isHidden(elementsEntered[i])) { + // We need to make sure that startCursor and endCursor aren't + // surrounding a skippable node. + endCursor.index--; + startCursor.copyFrom(endCursor); + startCursor.index--; + return ' '; + } + } + // Otherwise, return all of the whitespace before that last character. + endCursor.index--; + return ' '; + } +}; + +/** + * Finds the previous character, starting from startCursor. Upon exit, + * startCursor and endCursor will surround the previous character. + * If skipWhitespace is true, will skip until a real character is found. + * Otherwise, it will attempt to select all of the whitespace between + * the initial position of endCursor and the next non-whitespace character. + * @param {!cvox.Cursor} startCursor The position to start searching for the + * char. On exit, will point to the position before the char. + * @param {!cvox.Cursor} endCursor The position to start searching for the next + * char. On exit, will point to the position past the char. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * initial and final cursor position will be pushed onto this array. + * @param {boolean} skipWhitespace If true, will keep scanning until a + * non-whitespace character is found. + * @return {?string} The previous char, or null if the top of the + * document has been reached. + */ +cvox.TraverseUtil.getPreviousChar = function( + startCursor, endCursor, elementsEntered, elementsLeft, skipWhitespace) { + + // Save the starting position and get the first character. + endCursor.copyFrom(startCursor); + var c = cvox.TraverseUtil.backwardsChar( + startCursor, elementsEntered, elementsLeft); + if (c == null) + return null; + + // Keep track of whether the first character was whitespace. + var initialWhitespace = cvox.TraverseUtil.isWhitespace(c); + + // Keep scanning until we find a non-whitespace or non-skipped character. + while ((cvox.TraverseUtil.isWhitespace(c)) || + (cvox.TraverseUtil.isHidden(startCursor.node))) { + c = cvox.TraverseUtil.backwardsChar( + startCursor, elementsEntered, elementsLeft); + if (c == null) + return null; + } + if (skipWhitespace || !initialWhitespace) { + // If skipWhitepace is true, or if the first character we encountered + // was not whitespace, return that non-whitespace character. + endCursor.copyFrom(startCursor); + endCursor.index++; + return c; + } else { + for (var i = 0; i < elementsEntered.length; i++) { + if (cvox.TraverseUtil.isHidden(elementsEntered[i])) { + startCursor.index++; + endCursor.copyFrom(startCursor); + endCursor.index++; + return ' '; + } + } + // Otherwise, return all of the whitespace before that last character. + startCursor.index++; + return ' '; + } +}; + +/** + * Finds the next word, starting from endCursor. Upon exit, startCursor + * and endCursor will surround the next word. A word is defined to be + * a string of 1 or more non-whitespace characters in the same DOM node. + * @param {cvox.Cursor} startCursor On exit, will point to the beginning of the + * word returned. + * @param {cvox.Cursor} endCursor The position to start searching for the next + * word. On exit, will point to the end of the word returned. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * @return {?string} The next word, or null if the bottom of the + * document has been reached. + */ +cvox.TraverseUtil.getNextWord = function(startCursor, endCursor, + elementsEntered, elementsLeft) { + + // Find the first non-whitespace or non-skipped character. + var cursor = endCursor.clone(); + var c = cvox.TraverseUtil.forwardsChar(cursor, elementsEntered, elementsLeft); + if (c == null) + return null; + while ((cvox.TraverseUtil.isWhitespace(c)) || + (cvox.TraverseUtil.isHidden(cursor.node))) { + c = cvox.TraverseUtil.forwardsChar(cursor, elementsEntered, elementsLeft); + if (c == null) + return null; + } + + // Set startCursor to the position immediately before the first + // character in our word. It's safe to decrement |index| because + // forwardsChar guarantees that the cursor will be immediately to the + // right of the returned character on exit. + startCursor.copyFrom(cursor); + startCursor.index--; + + // Keep building up our word until we reach a whitespace character or + // would cross a tag. Don't actually return any tags crossed, because this + // word goes up until the tag boundary but not past it. + endCursor.copyFrom(cursor); + var word = c; + var newEntered = []; + var newLeft = []; + c = cvox.TraverseUtil.forwardsChar(cursor, newEntered, newLeft); + if (c == null) { + return word; + } + while (!cvox.TraverseUtil.isWhitespace(c) && + newEntered.length == 0 && + newLeft == 0) { + word += c; + endCursor.copyFrom(cursor); + c = cvox.TraverseUtil.forwardsChar(cursor, newEntered, newLeft); + if (c == null) { + return word; + } + } + + return word; +}; + +/** + * Finds the previous word, starting from startCursor. Upon exit, startCursor + * and endCursor will surround the previous word. A word is defined to be + * a string of 1 or more non-whitespace characters in the same DOM node. + * @param {cvox.Cursor} startCursor The position to start searching for the + * previous word. On exit, will point to the beginning of the + * word returned. + * @param {cvox.Cursor} endCursor On exit, will point to the end of the + * word returned. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * @return {?string} The previous word, or null if the bottom of the + * document has been reached. + */ +cvox.TraverseUtil.getPreviousWord = function(startCursor, endCursor, + elementsEntered, elementsLeft) { + // Find the first non-whitespace or non-skipped character. + var cursor = startCursor.clone(); + var c = cvox.TraverseUtil.backwardsChar( + cursor, elementsEntered, elementsLeft); + if (c == null) + return null; + while ((cvox.TraverseUtil.isWhitespace(c) || + (cvox.TraverseUtil.isHidden(cursor.node)))) { + c = cvox.TraverseUtil.backwardsChar(cursor, elementsEntered, elementsLeft); + if (c == null) + return null; + } + + // Set endCursor to the position immediately after the first + // character we've found (the last character of the word, since we're + // searching backwards). + endCursor.copyFrom(cursor); + endCursor.index++; + + // Keep building up our word until we reach a whitespace character or + // would cross a tag. Don't actually return any tags crossed, because this + // word goes up until the tag boundary but not past it. + startCursor.copyFrom(cursor); + var word = c; + var newEntered = []; + var newLeft = []; + c = cvox.TraverseUtil.backwardsChar(cursor, newEntered, newLeft); + if (c == null) + return word; + while (!cvox.TraverseUtil.isWhitespace(c) && + newEntered.length == 0 && + newLeft.length == 0) { + word = c + word; + startCursor.copyFrom(cursor); + + c = cvox.TraverseUtil.backwardsChar(cursor, newEntered, newLeft); + if (c == null) + return word; + } + + return word; +}; + + +/** + * Given elements entered and left, and break tags, returns true if the + * current word should break. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * @param {Object.<string, boolean>} breakTags Associative array of tags + * that should break. + * @return {boolean} True if elementsEntered or elementsLeft include an + * element with one of these tags. + */ +cvox.TraverseUtil.includesBreakTagOrSkippedNode = function( + elementsEntered, elementsLeft, breakTags) { + for (var i = 0; i < elementsEntered.length; i++) { + if (cvox.TraverseUtil.isHidden(elementsEntered[i])) { + return true; + } + var style = window.getComputedStyle(elementsEntered[i], null); + if ((style && style.display != 'inline') || + breakTags[elementsEntered[i].tagName]) { + return true; + } + } + for (i = 0; i < elementsLeft.length; i++) { + var style = window.getComputedStyle(elementsLeft[i], null); + if ((style && style.display != 'inline') || + breakTags[elementsLeft[i].tagName]) { + return true; + } + } + return false; +}; + + +/** + * Finds the next sentence, starting from endCursor. Upon exit, + * startCursor and endCursor will surround the next sentence. + * + * @param {cvox.Cursor} startCursor On exit, marks the beginning of the + * sentence. + * @param {cvox.Cursor} endCursor The position to start searching for the next + * sentence. On exit, will point to the end of the returned string. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * @param {Object.<string, boolean>} breakTags Associative array of tags + * that should break the sentence. + * @return {?string} The next sentence, or null if the bottom of the + * document has been reached. + */ +cvox.TraverseUtil.getNextSentence = function( + startCursor, endCursor, elementsEntered, elementsLeft, breakTags) { + return cvox.TraverseUtil.getNextString( + startCursor, endCursor, elementsEntered, elementsLeft, + function(str, word, elementsEntered, elementsLeft) { + if (str.substr(-1) == '.') + return true; + return cvox.TraverseUtil.includesBreakTagOrSkippedNode( + elementsEntered, elementsLeft, breakTags); + }); +}; + +/** + * Finds the previous sentence, starting from startCursor. Upon exit, + * startCursor and endCursor will surround the previous sentence. + * + * @param {cvox.Cursor} startCursor The position to start searching for the next + * sentence. On exit, will point to the start of the returned string. + * @param {cvox.Cursor} endCursor On exit, the end of the returned string. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * @param {Object.<string, boolean>} breakTags Associative array of tags + * that should break the sentence. + * @return {?string} The previous sentence, or null if the bottom of the + * document has been reached. + */ +cvox.TraverseUtil.getPreviousSentence = function( + startCursor, endCursor, elementsEntered, elementsLeft, breakTags) { + return cvox.TraverseUtil.getPreviousString( + startCursor, endCursor, elementsEntered, elementsLeft, + function(str, word, elementsEntered, elementsLeft) { + if (word.substr(-1) == '.') + return true; + return cvox.TraverseUtil.includesBreakTagOrSkippedNode( + elementsEntered, elementsLeft, breakTags); + }); +}; + +/** + * Finds the next line, starting from endCursor. Upon exit, + * startCursor and endCursor will surround the next line. + * + * @param {cvox.Cursor} startCursor On exit, marks the beginning of the line. + * @param {cvox.Cursor} endCursor The position to start searching for the next + * line. On exit, will point to the end of the returned string. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * @param {Object.<string, boolean>} breakTags Associative array of tags + * that should break the line. + * @return {?string} The next line, or null if the bottom of the + * document has been reached. + */ +cvox.TraverseUtil.getNextLine = function( + startCursor, endCursor, elementsEntered, elementsLeft, breakTags) { + var range = document.createRange(); + var currentRect = null; + var rightMostRect = null; + var prevCursor = endCursor.clone(); + return cvox.TraverseUtil.getNextString( + startCursor, endCursor, elementsEntered, elementsLeft, + function(str, word, elementsEntered, elementsLeft) { + range.setStart(startCursor.node, startCursor.index); + range.setEnd(endCursor.node, endCursor.index); + var currentRect = range.getBoundingClientRect(); + if (!rightMostRect) { + rightMostRect = currentRect; + } + + // Break at new lines except when within a link. + if (currentRect.bottom != rightMostRect.bottom && + !cvox.DomPredicates.linkPredicate(cvox.DomUtil.getAncestors( + endCursor.node))) { + endCursor.copyFrom(prevCursor); + return true; + } + + rightMostRect = currentRect; + prevCursor.copyFrom(endCursor); + + return cvox.TraverseUtil.includesBreakTagOrSkippedNode( + elementsEntered, elementsLeft, breakTags); + }); +}; + +/** + * Finds the previous line, starting from startCursor. Upon exit, + * startCursor and endCursor will surround the previous line. + * + * @param {cvox.Cursor} startCursor The position to start searching for the next + * line. On exit, will point to the start of the returned string. + * @param {cvox.Cursor} endCursor On exit, the end of the returned string. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * @param {Object.<string, boolean>} breakTags Associative array of tags + * that should break the line. + * @return {?string} The previous line, or null if the bottom of the + * document has been reached. + */ +cvox.TraverseUtil.getPreviousLine = function( + startCursor, endCursor, elementsEntered, elementsLeft, breakTags) { + var range = document.createRange(); + var currentRect = null; + var leftMostRect = null; + var prevCursor = startCursor.clone(); + return cvox.TraverseUtil.getPreviousString( + startCursor, endCursor, elementsEntered, elementsLeft, + function(str, word, elementsEntered, elementsLeft) { + range.setStart(startCursor.node, startCursor.index); + range.setEnd(endCursor.node, endCursor.index); + var currentRect = range.getBoundingClientRect(); + if (!leftMostRect) { + leftMostRect = currentRect; + } + + // Break at new lines except when within a link. + if (currentRect.top != leftMostRect.top && + !cvox.DomPredicates.linkPredicate(cvox.DomUtil.getAncestors( + startCursor.node))) { + startCursor.copyFrom(prevCursor); + return true; + } + + leftMostRect = currentRect; + prevCursor.copyFrom(startCursor); + + return cvox.TraverseUtil.includesBreakTagOrSkippedNode( + elementsEntered, elementsLeft, breakTags); + }); +}; + +/** + * Finds the next paragraph, starting from endCursor. Upon exit, + * startCursor and endCursor will surround the next paragraph. + * + * @param {cvox.Cursor} startCursor On exit, marks the beginning of the + * paragraph. + * @param {cvox.Cursor} endCursor The position to start searching for the next + * paragraph. On exit, will point to the end of the returned string. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * @return {?string} The next paragraph, or null if the bottom of the + * document has been reached. + */ +cvox.TraverseUtil.getNextParagraph = function(startCursor, endCursor, + elementsEntered, elementsLeft) { + return cvox.TraverseUtil.getNextString( + startCursor, endCursor, elementsEntered, elementsLeft, + function(str, word, elementsEntered, elementsLeft) { + for (var i = 0; i < elementsEntered.length; i++) { + if (cvox.TraverseUtil.isHidden(elementsEntered[i])) { + return true; + } + var style = window.getComputedStyle(elementsEntered[i], null); + if (style && style.display != 'inline') { + return true; + } + } + for (i = 0; i < elementsLeft.length; i++) { + var style = window.getComputedStyle(elementsLeft[i], null); + if (style && style.display != 'inline') { + return true; + } + } + return false; + }); +}; + +/** + * Finds the previous paragraph, starting from startCursor. Upon exit, + * startCursor and endCursor will surround the previous paragraph. + * + * @param {cvox.Cursor} startCursor The position to start searching for the next + * paragraph. On exit, will point to the start of the returned string. + * @param {cvox.Cursor} endCursor On exit, the end of the returned string. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * @return {?string} The previous paragraph, or null if the bottom of the + * document has been reached. + */ +cvox.TraverseUtil.getPreviousParagraph = function( + startCursor, endCursor, elementsEntered, elementsLeft) { + return cvox.TraverseUtil.getPreviousString( + startCursor, endCursor, elementsEntered, elementsLeft, + function(str, word, elementsEntered, elementsLeft) { + for (var i = 0; i < elementsEntered.length; i++) { + if (cvox.TraverseUtil.isHidden(elementsEntered[i])) { + return true; + } + var style = window.getComputedStyle(elementsEntered[i], null); + if (style && style.display != 'inline') { + return true; + } + } + for (i = 0; i < elementsLeft.length; i++) { + var style = window.getComputedStyle(elementsLeft[i], null); + if (style && style.display != 'inline') { + return true; + } + } + return false; + }); +}; + +/** + * Customizable function to return the next string of words in the DOM, based + * on provided functions to decide when to break one string and start + * the next. This can be used to get the next sentence, line, paragraph, + * or potentially other granularities. + * + * Finds the next contiguous string, starting from endCursor. Upon exit, + * startCursor and endCursor will surround the next string. + * + * The breakBefore function takes four parameters, and + * should return true if the string should be broken before the proposed + * next word: + * str The string so far. + * word The next word to be added. + * elementsEntered The elements entered in reaching this next word. + * elementsLeft The elements left in reaching this next word. + * + * @param {cvox.Cursor} startCursor On exit, will point to the beginning of the + * next string. + * @param {cvox.Cursor} endCursor The position to start searching for the next + * string. On exit, will point to the end of the returned string. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * @param {function(string, string, Array.<Element>, Array.<Element>)} + * breakBefore Function that takes the string so far, next word to be + * added, and elements entered and left, and returns true if the string + * should be ended before adding this word. + * @return {?string} The next string, or null if the bottom of the + * document has been reached. + */ +cvox.TraverseUtil.getNextString = function( + startCursor, endCursor, elementsEntered, elementsLeft, breakBefore) { + // Get the first word and set the start cursor to the start of the + // first word. + var wordStartCursor = endCursor.clone(); + var wordEndCursor = endCursor.clone(); + var newEntered = []; + var newLeft = []; + var str = ''; + var word = cvox.TraverseUtil.getNextWord( + wordStartCursor, wordEndCursor, newEntered, newLeft); + if (word == null) + return null; + startCursor.copyFrom(wordStartCursor); + + // Always add the first word when the string is empty, and then keep + // adding more words as long as breakBefore returns false + while (!str || !breakBefore(str, word, newEntered, newLeft)) { + // Append this word, set the end cursor to the end of this word, and + // update the returned list of nodes crossed to include ones we crossed + // in reaching this word. + if (str) + str += ' '; + str += word; + elementsEntered = elementsEntered.concat(newEntered); + elementsLeft = elementsLeft.concat(newLeft); + endCursor.copyFrom(wordEndCursor); + + // Get the next word and go back to the top of the loop. + newEntered = []; + newLeft = []; + word = cvox.TraverseUtil.getNextWord( + wordStartCursor, wordEndCursor, newEntered, newLeft); + if (word == null) + return str; + } + + return str; +}; + +/** + * Customizable function to return the previous string of words in the DOM, + * based on provided functions to decide when to break one string and start + * the next. See getNextString, above, for more details. + * + * Finds the previous contiguous string, starting from startCursor. Upon exit, + * startCursor and endCursor will surround the next string. + * + * @param {cvox.Cursor} startCursor The position to start searching for the + * previous string. On exit, will point to the beginning of the + * string returned. + * @param {cvox.Cursor} endCursor On exit, will point to the end of the + * string returned. + * @param {Array.<Element>} elementsEntered Any HTML elements entered. + * @param {Array.<Element>} elementsLeft Any HTML elements left. + * @param {function(string, string, Array.<Element>, Array.<Element>)} + * breakBefore Function that takes the string so far, the word to be + * added, and nodes crossed, and returns true if the string should be + * ended before adding this word. + * @return {?string} The next string, or null if the top of the + * document has been reached. + */ +cvox.TraverseUtil.getPreviousString = function( + startCursor, endCursor, elementsEntered, elementsLeft, breakBefore) { + // Get the first word and set the end cursor to the end of the + // first word. + var wordStartCursor = startCursor.clone(); + var wordEndCursor = startCursor.clone(); + var newEntered = []; + var newLeft = []; + var str = ''; + var word = cvox.TraverseUtil.getPreviousWord( + wordStartCursor, wordEndCursor, newEntered, newLeft); + if (word == null) + return null; + endCursor.copyFrom(wordEndCursor); + + // Always add the first word when the string is empty, and then keep + // adding more words as long as breakBefore returns false + while (!str || !breakBefore(str, word, newEntered, newLeft)) { + // Prepend this word, set the start cursor to the start of this word, and + // update the returned list of nodes crossed to include ones we crossed + // in reaching this word. + if (str) + str = ' ' + str; + str = word + str; + elementsEntered = elementsEntered.concat(newEntered); + elementsLeft = elementsLeft.concat(newLeft); + startCursor.copyFrom(wordStartCursor); + + // Get the previous word and go back to the top of the loop. + newEntered = []; + newLeft = []; + word = cvox.TraverseUtil.getPreviousWord( + wordStartCursor, wordEndCursor, newEntered, newLeft); + if (word == null) + return str; + } + + return str; +}; |