| OLD | NEW |
| (Empty) | |
| 1 // Copyright (c) 2011 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 DOM traversal interface for moving a selection around a |
| 7 * webpage. Provides multiple granularities: |
| 8 * 1. Move by paragraph. |
| 9 * 2. Move by sentence. |
| 10 * 3. Move by line. |
| 11 * 4. Move by word. |
| 12 * 5. Move by character. |
| 13 */ |
| 14 |
| 15 goog.provide('cvox.TraverseContent'); |
| 16 |
| 17 goog.require('cvox.SelectionUtil'); |
| 18 goog.require('cvox.TraverseUtil'); |
| 19 |
| 20 /** |
| 21 * Moves a selection around a document or within a provided DOM object. |
| 22 * |
| 23 * @constructor |
| 24 * @param {Node=} domObj a DOM node (optional). |
| 25 */ |
| 26 cvox.TraverseContent = function(domObj) { |
| 27 if (domObj != null) { |
| 28 this.currentDomObj = domObj; |
| 29 } else { |
| 30 this.currentDomObj = document.body; |
| 31 } |
| 32 }; |
| 33 |
| 34 /** |
| 35 * Whether we should skip whitespace when traversing individual characters. |
| 36 * @type {boolean} |
| 37 */ |
| 38 cvox.TraverseContent.prototype.skipWhitespace = true; |
| 39 |
| 40 /** |
| 41 * The maximum number of characters that can be on one line when doing |
| 42 * line-based traversal. |
| 43 * @type {number} |
| 44 */ |
| 45 cvox.TraverseContent.prototype.lineLength = 40; |
| 46 |
| 47 /** |
| 48 * If moveNext and movePrev should skip past an invalid selection, |
| 49 * so the user never gets stuck. Ideally the navigation code should never |
| 50 * return a range that's not a valid selection, but this keeps the user from |
| 51 * getting stuck if that code fails. This is set to false for unit testing. |
| 52 * @type {boolean} |
| 53 */ |
| 54 cvox.TraverseContent.prototype.skipInvalidSelections = true; |
| 55 |
| 56 /** |
| 57 * If line and sentence navigation should break at <a> links. |
| 58 * @type {boolean} |
| 59 */ |
| 60 cvox.TraverseContent.prototype.breakAtLinks = false; |
| 61 |
| 62 /** |
| 63 * The string constant for character granularity. |
| 64 * @type {string} |
| 65 * @const |
| 66 */ |
| 67 cvox.TraverseContent.kCharacter = 'character'; |
| 68 |
| 69 /** |
| 70 * The string constant for word granularity. |
| 71 * @type {string} |
| 72 * @const |
| 73 */ |
| 74 cvox.TraverseContent.kWord = 'word'; |
| 75 |
| 76 /** |
| 77 * The string constant for sentence granularity. |
| 78 * @type {string} |
| 79 * @const |
| 80 */ |
| 81 cvox.TraverseContent.kSentence = 'sentence'; |
| 82 |
| 83 /** |
| 84 * The string constant for line granularity. |
| 85 * @type {string} |
| 86 * @const |
| 87 */ |
| 88 cvox.TraverseContent.kLine = 'line'; |
| 89 |
| 90 /** |
| 91 * The string constant for paragraph granularity. |
| 92 * @type {string} |
| 93 * @const |
| 94 */ |
| 95 cvox.TraverseContent.kParagraph = 'paragraph'; |
| 96 |
| 97 /** |
| 98 * A constant array of all granularities. |
| 99 * @type {Array.<string>} |
| 100 * @const |
| 101 */ |
| 102 cvox.TraverseContent.kAllGrains = |
| 103 [cvox.TraverseContent.kParagraph, |
| 104 cvox.TraverseContent.kSentence, |
| 105 cvox.TraverseContent.kLine, |
| 106 cvox.TraverseContent.kWord, |
| 107 cvox.TraverseContent.kCharacter]; |
| 108 |
| 109 /** |
| 110 * Moves selection forward. |
| 111 * |
| 112 * @param {string} grain specifies "sentence", "word", "character", |
| 113 * or "paragraph" granularity. |
| 114 * @return {Selection} Either: |
| 115 * 1) The fixed-up selection. |
| 116 * 2) null if the end of the domObj has been reached. |
| 117 */ |
| 118 cvox.TraverseContent.prototype.moveNext = function(grain) { |
| 119 this.normalizeSelection(); |
| 120 |
| 121 var selection = window.getSelection(); |
| 122 var startCursor = new Cursor( |
| 123 selection.anchorNode, selection.anchorOffset, |
| 124 cvox.TraverseUtil.getNodeText(selection.anchorNode)); |
| 125 var endCursor = new Cursor( |
| 126 selection.focusNode, selection.focusOffset, |
| 127 cvox.TraverseUtil.getNodeText(selection.focusNode)); |
| 128 var breakTags = this.getBreakTags(); |
| 129 // As a special case, if the current selection is empty or all |
| 130 // whitespace, ensure that the next returned selection will NOT be |
| 131 // only whitespace - otherwise you can get trapped. |
| 132 var skipWhitespace = this.skipWhitespace; |
| 133 if (!cvox.SelectionUtil.isSelectionValid(selection)) |
| 134 skipWhitespace = true; |
| 135 |
| 136 var nodesCrossed = []; |
| 137 var str; |
| 138 |
| 139 do { |
| 140 if (grain === cvox.TraverseContent.kSentence) { |
| 141 str = cvox.TraverseUtil.getNextSentence( |
| 142 startCursor, endCursor, nodesCrossed, breakTags); |
| 143 } else if (grain === cvox.TraverseContent.kWord) { |
| 144 str = cvox.TraverseUtil.getNextWord(startCursor, endCursor, |
| 145 nodesCrossed); |
| 146 } else if (grain === cvox.TraverseContent.kCharacter) { |
| 147 str = cvox.TraverseUtil.getNextChar(startCursor, endCursor, |
| 148 nodesCrossed, skipWhitespace); |
| 149 } else if (grain === cvox.TraverseContent.kParagraph) { |
| 150 str = cvox.TraverseUtil.getNextParagraph(startCursor, endCursor, |
| 151 nodesCrossed); |
| 152 } else if (grain === cvox.TraverseContent.kLine) { |
| 153 str = cvox.TraverseUtil.getNextLine( |
| 154 startCursor, endCursor, nodesCrossed, this.lineLength, breakTags); |
| 155 } else { |
| 156 // User has provided an invalid string. |
| 157 // Fall through to default: extend by sentence |
| 158 console.log('Invalid selection granularity: "' + grain + '"'); |
| 159 grain = cvox.TraverseContent.kSentence; |
| 160 str = cvox.TraverseUtil.getNextSentence( |
| 161 startCursor, endCursor, nodesCrossed, breakTags); |
| 162 } |
| 163 |
| 164 // Select the new object. |
| 165 selection = cvox.TraverseUtil.setSelection(startCursor, endCursor); |
| 166 |
| 167 if (str == null) { |
| 168 // We reached the end of the document. |
| 169 return null; |
| 170 } |
| 171 } while (this.skipInvalidSelections && selection.isCollapsed); |
| 172 |
| 173 if (!cvox.SelectionUtil.isWithinBound(selection.focusNode, |
| 174 this.currentDomObj)) { |
| 175 // Extended outside of specified domObj; trim it back to just the domObj |
| 176 // if we are dealing with a text node. |
| 177 // Return NULL to indicate that we are wrapping to the beginning. |
| 178 if (selection.anchorNode.nodeType == 3) { // NODETYPE 3 == text node |
| 179 cvox.SelectionUtil.selectText(selection.anchorNode, |
| 180 selection.anchorOffset, |
| 181 selection.anchorNode.textContent.length); |
| 182 } |
| 183 return null; |
| 184 } |
| 185 return selection; |
| 186 }; |
| 187 |
| 188 |
| 189 /** |
| 190 * Moves selection backward. |
| 191 * |
| 192 * @param {string} grain specifies "sentence", "word", "character", |
| 193 * or "paragraph" granularity. |
| 194 * @return {Selection} Either: |
| 195 * 1) The fixed-up selection. |
| 196 * 2) null if the beginning of the domObj has been reached. |
| 197 */ |
| 198 cvox.TraverseContent.prototype.movePrev = function(grain) { |
| 199 this.normalizeSelection(); |
| 200 |
| 201 var selection = window.getSelection(); |
| 202 var startCursor = new Cursor( |
| 203 selection.anchorNode, selection.anchorOffset, |
| 204 cvox.TraverseUtil.getNodeText(selection.anchorNode)); |
| 205 var endCursor = new Cursor( |
| 206 selection.focusNode, selection.focusOffset, |
| 207 cvox.TraverseUtil.getNodeText(selection.focusNode)); |
| 208 var breakTags = this.getBreakTags(); |
| 209 // As a special case, if the current selection is empty or all |
| 210 // whitespace, ensure that the next returned selection will NOT be |
| 211 // only whitespace - otherwise you can get trapped. |
| 212 var skipWhitespace = this.skipWhitespace; |
| 213 if (!cvox.SelectionUtil.isSelectionValid(selection)) |
| 214 skipWhitespace = true; |
| 215 |
| 216 var nodesCrossed = []; |
| 217 var str; |
| 218 |
| 219 do { |
| 220 if (grain === cvox.TraverseContent.kSentence) { |
| 221 str = cvox.TraverseUtil.getPreviousSentence( |
| 222 startCursor, endCursor, nodesCrossed, breakTags); |
| 223 } else if (grain === cvox.TraverseContent.kWord) { |
| 224 str = cvox.TraverseUtil.getPreviousWord(startCursor, endCursor, |
| 225 nodesCrossed); |
| 226 } else if (grain === cvox.TraverseContent.kCharacter) { |
| 227 var skipWhitespace = this.skipWhitespace; |
| 228 if (!cvox.SelectionUtil.isSelectionValid(selection)) |
| 229 skipWhitespace = true; |
| 230 str = cvox.TraverseUtil.getPreviousChar(startCursor, endCursor, |
| 231 nodesCrossed, skipWhitespace); |
| 232 } else if (grain === cvox.TraverseContent.kParagraph) { |
| 233 str = cvox.TraverseUtil.getPreviousParagraph( |
| 234 startCursor, endCursor, nodesCrossed); |
| 235 } else if (grain === cvox.TraverseContent.kLine) { |
| 236 str = cvox.TraverseUtil.getPreviousLine( |
| 237 startCursor, endCursor, nodesCrossed, this.lineLength, breakTags); |
| 238 } else { |
| 239 // User has provided an invalid string. |
| 240 // Fall through to default: extend by sentence |
| 241 console.log('Invalid selection granularity: "' + grain + '"'); |
| 242 grain = cvox.TraverseContent.kSentence; |
| 243 str = cvox.TraverseUtil.getPreviousSentence( |
| 244 startCursor, endCursor, nodesCrossed, breakTags); |
| 245 } |
| 246 |
| 247 // Select the new object. |
| 248 selection = cvox.TraverseUtil.setSelection(startCursor, endCursor); |
| 249 |
| 250 if (str == null) { |
| 251 // We reached the end of the document. |
| 252 return null; |
| 253 } |
| 254 |
| 255 } while (this.skipInvalidSelections && selection.isCollapsed); |
| 256 |
| 257 if (!cvox.SelectionUtil.isWithinBound(selection.focusNode, |
| 258 this.currentDomObj)) { |
| 259 console.log('not within bound'); |
| 260 // Extended outside of specified domObj, nothing more to be done |
| 261 // Return NULL to indicate that we are wrapping to the beginning |
| 262 return null; |
| 263 } |
| 264 |
| 265 return selection; |
| 266 }; |
| 267 |
| 268 /** |
| 269 * Get the tag names that should break a sentence or line. Currently |
| 270 * just an anchor 'A' should break a sentence or line if the breakAtLinks |
| 271 * flag is true, but in the future we might have other rules for breaking. |
| 272 * |
| 273 * @return {Object} An associative array mapping a tag name to true if |
| 274 * it should break a sentence or line. |
| 275 */ |
| 276 cvox.TraverseContent.prototype.getBreakTags = function() { |
| 277 return this.breakAtLinks ? {'A': true} : {}; |
| 278 }; |
| 279 |
| 280 /** |
| 281 * Selects the next element of the document or within the provided DOM object. |
| 282 * Scrolls the window as appropriate. |
| 283 * |
| 284 * @param {string} grain specifies "sentence", "word", "character", |
| 285 * or "paragraph" granularity. |
| 286 * @param {Node=} domObj a DOM node (optional). |
| 287 * @return {Selection} Either: |
| 288 * 1) The current selection. |
| 289 * 2) null if the end of the domObj has been reached. |
| 290 */ |
| 291 cvox.TraverseContent.prototype.nextElement = function(grain, domObj) { |
| 292 |
| 293 if (domObj != null) { |
| 294 this.currentDomObj = domObj; |
| 295 } |
| 296 |
| 297 if (! ((grain === 'sentence') || (grain === 'word') || |
| 298 (grain === 'character') || (grain === 'paragraph'))) { |
| 299 // User has provided an invalid string. |
| 300 // Fall through to default: extend by sentence |
| 301 console.log('Invalid selection granularity: "' + grain + '"'); |
| 302 grain = 'sentence'; |
| 303 } |
| 304 |
| 305 var status = this.moveNext(grain); |
| 306 if (status != null) { |
| 307 // Force window scroll to current selection |
| 308 cvox.SelectionUtil.scrollToSelection(window.getSelection()); |
| 309 } |
| 310 |
| 311 return status; |
| 312 }; |
| 313 |
| 314 |
| 315 /** |
| 316 * Selects the previous element of the document or within the provided DOM |
| 317 * object. Scrolls the window as appropriate. |
| 318 * |
| 319 * @param {string} grain specifies "sentence", "word", "character", |
| 320 * or "paragraph" granularity. |
| 321 * @param {Node=} domObj a DOM node (optional). |
| 322 * @return {Selection} Either: |
| 323 * 1) The current selection. |
| 324 * 2) null if the beginning of the domObj has been reached. |
| 325 */ |
| 326 cvox.TraverseContent.prototype.prevElement = function(grain, domObj) { |
| 327 |
| 328 if (domObj != null) { |
| 329 this.currentDomObj = domObj; |
| 330 } |
| 331 |
| 332 if (! ((grain === 'sentence') || (grain === 'word') || |
| 333 (grain === 'character') || (grain === 'paragraph'))) { |
| 334 // User has provided an invalid string. |
| 335 // Fall through to default: extend by sentence |
| 336 console.log('Invalid selection granularity: "' + grain + '"'); |
| 337 grain = 'sentence'; |
| 338 } |
| 339 |
| 340 var status = this.movePrev(grain); |
| 341 if (status != null) { |
| 342 // Force window scroll to current selection |
| 343 cvox.SelectionUtil.scrollToSelection(window.getSelection()); |
| 344 } |
| 345 |
| 346 return status; |
| 347 }; |
| 348 |
| 349 /** |
| 350 * Make sure that exactly one item is selected. If there's no selection, |
| 351 * set the selection to the start of the document. |
| 352 */ |
| 353 cvox.TraverseContent.prototype.normalizeSelection = function() { |
| 354 var selection = window.getSelection(); |
| 355 if (selection.rangeCount < 1) { |
| 356 // Before the user has clicked a freshly-loaded page |
| 357 |
| 358 var range = document.createRange(); |
| 359 range.setStart(this.currentDomObj, 0); |
| 360 range.setEnd(this.currentDomObj, 0); |
| 361 |
| 362 selection.removeAllRanges(); |
| 363 selection.addRange(range); |
| 364 |
| 365 } else if (selection.rangeCount > 1) { |
| 366 // Multiple ranges exist - remove all ranges but the last one |
| 367 for (var i = 0; i < (selection.rangeCount - 1); i++) { |
| 368 selection.removeRange(selection.getRangeAt(i)); |
| 369 } |
| 370 } |
| 371 }; |
| 372 |
| 373 /** |
| 374 * Resets the selection. |
| 375 * |
| 376 * @param {Node=} domObj a DOM node. Optional. |
| 377 * |
| 378 */ |
| 379 cvox.TraverseContent.prototype.reset = function(domObj) { |
| 380 window.getSelection().removeAllRanges(); |
| 381 }; |
| OLD | NEW |