Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / common / traverse_content.js
blob4ca1ec4183cf5e98f2130e3d1b7278ea8b14cea9
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.
6 /**
7  * @fileoverview A DOM traversal interface for moving a selection around a
8  * webpage. Provides multiple granularities:
9  * 1. Move by paragraph.
10  * 2. Move by sentence.
11  * 3. Move by line.
12  * 4. Move by word.
13  * 5. Move by character.
14  */
16 goog.provide('cvox.TraverseContent');
18 goog.require('cvox.CursorSelection');
19 goog.require('cvox.DomUtil');
20 goog.require('cvox.SelectionUtil');
21 goog.require('cvox.TraverseUtil');
23 /**
24  * Moves a selection around a document or within a provided DOM object.
25  *
26  * @constructor
27  * @param {Node=} domObj a DOM node (optional).
28  */
29 cvox.TraverseContent = function(domObj) {
30   if (domObj != null) {
31     this.currentDomObj = domObj;
32   } else {
33     this.currentDomObj = document.body;
34   }
35   var range = document.createRange();
36   // TODO (dmazzoni): Switch this to avoid using range methods. Range methods
37   // can cause exceptions (such as if the node is not attached to the DOM).
38   try {
39     range.selectNode(this.currentDomObj);
40     this.startCursor_ = new cvox.Cursor(
41         range.startContainer, range.startOffset,
42         cvox.TraverseUtil.getNodeText(range.startContainer));
43     this.endCursor_ = new cvox.Cursor(
44         range.endContainer, range.endOffset,
45         cvox.TraverseUtil.getNodeText(range.endContainer));
46   } catch (e) {
47     // Ignoring this error so that it will not break everything else.
48     window.console.log('Error: Unselectable node:');
49     window.console.log(domObj);
50   }
52 goog.addSingletonGetter(cvox.TraverseContent);
54 /**
55  * Whether the last navigated selection only contained whitespace.
56  * @type {boolean}
57  */
58 cvox.TraverseContent.prototype.lastSelectionWasWhitespace = false;
60 /**
61  * Whether we should skip whitespace when traversing individual characters.
62  * @type {boolean}
63  */
64 cvox.TraverseContent.prototype.skipWhitespace = false;
66 /**
67  * If moveNext and movePrev should skip past an invalid selection,
68  * so the user never gets stuck. Ideally the navigation code should never
69  * return a range that's not a valid selection, but this keeps the user from
70  * getting stuck if that code fails.  This is set to false for unit testing.
71  * @type {boolean}
72  */
73 cvox.TraverseContent.prototype.skipInvalidSelections = true;
75 /**
76  * If line and sentence navigation should break at <a> links.
77  * @type {boolean}
78  */
79 cvox.TraverseContent.prototype.breakAtLinks = true;
81 /**
82  * The string constant for character granularity.
83  * @type {string}
84  * @const
85  */
86 cvox.TraverseContent.kCharacter = 'character';
88 /**
89  * The string constant for word granularity.
90  * @type {string}
91  * @const
92  */
93 cvox.TraverseContent.kWord = 'word';
95 /**
96  * The string constant for sentence granularity.
97  * @type {string}
98  * @const
99  */
100 cvox.TraverseContent.kSentence = 'sentence';
103  * The string constant for line granularity.
104  * @type {string}
105  * @const
106  */
107 cvox.TraverseContent.kLine = 'line';
110  * The string constant for paragraph granularity.
111  * @type {string}
112  * @const
113  */
114 cvox.TraverseContent.kParagraph = 'paragraph';
117  * A constant array of all granularities.
118  * @type {Array<string>}
119  * @const
120  */
121 cvox.TraverseContent.kAllGrains =
122     [cvox.TraverseContent.kParagraph,
123      cvox.TraverseContent.kSentence,
124      cvox.TraverseContent.kLine,
125      cvox.TraverseContent.kWord,
126      cvox.TraverseContent.kCharacter];
129  * Set the current position to match the current WebKit selection.
130  */
131 cvox.TraverseContent.prototype.syncToSelection = function() {
132   this.normalizeSelection();
134   var selection = window.getSelection();
135   if (!selection || !selection.anchorNode || !selection.focusNode) {
136     return;
137   }
138   this.startCursor_ = new cvox.Cursor(
139       selection.anchorNode, selection.anchorOffset,
140       cvox.TraverseUtil.getNodeText(selection.anchorNode));
141   this.endCursor_ = new cvox.Cursor(
142       selection.focusNode, selection.focusOffset,
143       cvox.TraverseUtil.getNodeText(selection.focusNode));
147  * Set the start and end cursors to the selection.
148  * @param {cvox.CursorSelection} sel The selection.
149  */
150 cvox.TraverseContent.prototype.syncToCursorSelection = function(sel) {
151   this.startCursor_ = sel.start.clone();
152   this.endCursor_ = sel.end.clone();
156  * Get the cursor selection.
157  * @return {cvox.CursorSelection} The selection.
158  */
159 cvox.TraverseContent.prototype.getCurrentCursorSelection = function() {
160   return new cvox.CursorSelection(this.startCursor_, this.endCursor_);
164  * Set the WebKit selection based on the current position.
165  */
166 cvox.TraverseContent.prototype.updateSelection = function() {
167   cvox.TraverseUtil.setSelection(this.startCursor_, this.endCursor_);
168   cvox.SelectionUtil.scrollToSelection(window.getSelection());
172  * Get the current position as a range.
173  * @return {Range} The current range.
174  */
175 cvox.TraverseContent.prototype.getCurrentRange = function() {
176   var range = document.createRange();
177   try {
178     range.setStart(this.startCursor_.node, this.startCursor_.index);
179     range.setEnd(this.endCursor_.node, this.endCursor_.index);
180   } catch (e) {
181     console.log('Invalid range ');
182   }
183   return range;
187  * Get the current text content as a string.
188  * @return {string} The current spanned content.
189  */
190 cvox.TraverseContent.prototype.getCurrentText = function() {
191   return cvox.SelectionUtil.getRangeText(this.getCurrentRange());
195  * Collapse to the end of the range.
196  */
197 cvox.TraverseContent.prototype.collapseToEnd = function() {
198   this.startCursor_ = this.endCursor_.clone();
202  * Collapse to the start of the range.
203  */
204 cvox.TraverseContent.prototype.collapseToStart = function() {
205   this.endCursor_ = this.startCursor_.clone();
209  * Moves selection forward.
211  * @param {string} grain specifies "sentence", "word", "character",
212  *     or "paragraph" granularity.
213  * @return {?string} Either:
214  *                1) The new selected text.
215  *                2) null if the end of the domObj has been reached.
216  */
217 cvox.TraverseContent.prototype.moveNext = function(grain) {
218   var breakTags = this.getBreakTags();
220   // As a special case, if the current selection is empty or all
221   // whitespace, ensure that the next returned selection will NOT be
222   // only whitespace - otherwise you can get trapped.
223   var skipWhitespace = this.skipWhitespace;
225   var range = this.getCurrentRange();
226   if (!cvox.SelectionUtil.isRangeValid(range)) {
227     skipWhitespace = true;
228   }
230   var elementsEntered = [];
231   var elementsLeft = [];
232   var str;
233   do {
234     if (grain === cvox.TraverseContent.kSentence) {
235       str = cvox.TraverseUtil.getNextSentence(
236           this.startCursor_, this.endCursor_, elementsEntered, elementsLeft,
237           breakTags);
238     } else if (grain === cvox.TraverseContent.kWord) {
239       str = cvox.TraverseUtil.getNextWord(
240           this.startCursor_, this.endCursor_, elementsEntered, elementsLeft);
241     } else if (grain === cvox.TraverseContent.kCharacter) {
242       str = cvox.TraverseUtil.getNextChar(
243           this.startCursor_, this.endCursor_, elementsEntered, elementsLeft,
244           skipWhitespace);
245     } else if (grain === cvox.TraverseContent.kParagraph) {
246       str = cvox.TraverseUtil.getNextParagraph(
247           this.startCursor_, this.endCursor_, elementsEntered, elementsLeft);
248     } else if (grain === cvox.TraverseContent.kLine) {
249       str = cvox.TraverseUtil.getNextLine(
250           this.startCursor_, this.endCursor_, elementsEntered, elementsLeft,
251           breakTags);
252     } else {
253       // User has provided an invalid string.
254       // Fall through to default: extend by sentence
255       window.console.log('Invalid selection granularity: "' + grain + '"');
256       grain = cvox.TraverseContent.kSentence;
257       str = cvox.TraverseUtil.getNextSentence(
258           this.startCursor_, this.endCursor_, elementsEntered, elementsLeft,
259           breakTags);
260     }
262     if (str == null) {
263       // We reached the end of the document.
264       return null;
265     }
267     range = this.getCurrentRange();
268     var isInvalid = !range.getBoundingClientRect();
269   } while (this.skipInvalidSelections && isInvalid);
271   if (!cvox.SelectionUtil.isRangeValid(range)) {
272     // It's OK if the selection navigation lands on whitespace once (in
273     // character granularity), but if it hits whitespace more than once, then
274     // skip forward until there is real content.
275     if (!this.lastSelectionWasWhitespace &&
276         grain == cvox.TraverseContent.kCharacter) {
277       this.lastSelectionWasWhitespace = true;
278     } else {
279       while (!cvox.SelectionUtil.isRangeValid(this.getCurrentRange())) {
280         if (this.moveNext(grain) == null) {
281           break;
282         }
283       }
284     }
285   } else {
286     this.lastSelectionWasWhitespace = false;
287   }
289   return this.getCurrentText();
294  * Moves selection backward.
296  * @param {string} grain specifies "sentence", "word", "character",
297  *     or "paragraph" granularity.
298  * @return {?string} Either:
299  *                1) The new selected text.
300  *                2) null if the beginning of the domObj has been reached.
301  */
302 cvox.TraverseContent.prototype.movePrev = function(grain) {
303   var breakTags = this.getBreakTags();
305   // As a special case, if the current selection is empty or all
306   // whitespace, ensure that the next returned selection will NOT be
307   // only whitespace - otherwise you can get trapped.
308   var skipWhitespace = this.skipWhitespace;
310   var range = this.getCurrentRange();
311   if (!cvox.SelectionUtil.isRangeValid(range)) {
312     skipWhitespace = true;
313   }
315   var elementsEntered = [];
316   var elementsLeft = [];
317   var str;
318   do {
319     if (grain === cvox.TraverseContent.kSentence) {
320       str = cvox.TraverseUtil.getPreviousSentence(
321           this.startCursor_, this.endCursor_, elementsEntered, elementsLeft,
322           breakTags);
323     } else if (grain === cvox.TraverseContent.kWord) {
324       str = cvox.TraverseUtil.getPreviousWord(
325           this.startCursor_, this.endCursor_, elementsEntered, elementsLeft);
326     } else if (grain === cvox.TraverseContent.kCharacter) {
327       str = cvox.TraverseUtil.getPreviousChar(
328           this.startCursor_, this.endCursor_, elementsEntered, elementsLeft,
329           skipWhitespace);
330     } else if (grain === cvox.TraverseContent.kParagraph) {
331       str = cvox.TraverseUtil.getPreviousParagraph(
332           this.startCursor_, this.endCursor_, elementsEntered, elementsLeft);
333     } else if (grain === cvox.TraverseContent.kLine) {
334       str = cvox.TraverseUtil.getPreviousLine(
335           this.startCursor_, this.endCursor_, elementsEntered, elementsLeft,
336           breakTags);
337     } else {
338       // User has provided an invalid string.
339       // Fall through to default: extend by sentence
340       window.console.log('Invalid selection granularity: "' + grain + '"');
341       grain = cvox.TraverseContent.kSentence;
342       str = cvox.TraverseUtil.getPreviousSentence(
343           this.startCursor_, this.endCursor_, elementsEntered, elementsLeft,
344           breakTags);
345     }
347     if (str == null) {
348       // We reached the end of the document.
349       return null;
350     }
352     range = this.getCurrentRange();
353     var isInvalid = !range.getBoundingClientRect();
354   } while (this.skipInvalidSelections && isInvalid);
356   if (!cvox.SelectionUtil.isRangeValid(range)) {
357     // It's OK if the selection navigation lands on whitespace once (in
358     // character granularity), but if it hits whitespace more than once, then
359     // skip forward until there is real content.
360     if (!this.lastSelectionWasWhitespace &&
361         grain == cvox.TraverseContent.kCharacter) {
362       this.lastSelectionWasWhitespace = true;
363     } else {
364       while (!cvox.SelectionUtil.isRangeValid(this.getCurrentRange())) {
365         if (this.movePrev(grain) == null) {
366           break;
367         }
368       }
369     }
370   } else {
371     this.lastSelectionWasWhitespace = false;
372   }
374   return this.getCurrentText();
378  * Get the tag names that should break a sentence or line. Currently
379  * just an anchor 'A' should break a sentence or line if the breakAtLinks
380  * flag is true, but in the future we might have other rules for breaking.
382  * @return {Object} An associative array mapping a tag name to true if
383  *     it should break a sentence or line.
384  */
385 cvox.TraverseContent.prototype.getBreakTags = function() {
386   return {
387     'A': this.breakAtLinks,
388     'BR': true,
389     'HR': true
390   };
394  * Selects the next element of the document or within the provided DOM object.
395  * Scrolls the window as appropriate.
397  * @param {string} grain specifies "sentence", "word", "character",
398  *     or "paragraph" granularity.
399  * @param {Node=} domObj a DOM node (optional).
400  * @return {?string} Either:
401  *                1) The new selected text.
402  *                2) null if the end of the domObj has been reached.
403  */
404 cvox.TraverseContent.prototype.nextElement = function(grain, domObj) {
405   if (domObj != null) {
406     this.currentDomObj = domObj;
407   }
409   var result = this.moveNext(grain);
410   if (result != null &&
411       (!cvox.DomUtil.isDescendantOfNode(
412           this.startCursor_.node, this.currentDomObj) ||
413        !cvox.DomUtil.isDescendantOfNode(
414            this.endCursor_.node, this.currentDomObj))) {
415     return null;
416   }
418   return result;
423  * Selects the previous element of the document or within the provided DOM
424  * object. Scrolls the window as appropriate.
426  * @param {string} grain specifies "sentence", "word", "character",
427  *     or "paragraph" granularity.
428  * @param {Node=} domObj a DOM node (optional).
429  * @return {?string} Either:
430  *                1) The new selected text.
431  *                2) null if the beginning of the domObj has been reached.
432  */
433 cvox.TraverseContent.prototype.prevElement = function(grain, domObj) {
434   if (domObj != null) {
435     this.currentDomObj = domObj;
436   }
438   var result = this.movePrev(grain);
439   if (result != null &&
440       (!cvox.DomUtil.isDescendantOfNode(
441           this.startCursor_.node, this.currentDomObj) ||
442        !cvox.DomUtil.isDescendantOfNode(
443            this.endCursor_.node, this.currentDomObj))) {
444     return null;
445   }
447   return result;
451  * Make sure that exactly one item is selected. If there's no selection,
452  * set the selection to the start of the document.
453  */
454 cvox.TraverseContent.prototype.normalizeSelection = function() {
455   var selection = window.getSelection();
456   if (selection.rangeCount < 1) {
457     // Before the user has clicked a freshly-loaded page
459     var range = document.createRange();
460     range.setStart(this.currentDomObj, 0);
461     range.setEnd(this.currentDomObj, 0);
463     selection.removeAllRanges();
464     selection.addRange(range);
466   } else if (selection.rangeCount > 1) {
467     //  Multiple ranges exist - remove all ranges but the last one
468     for (var i = 0; i < (selection.rangeCount - 1); i++) {
469       selection.removeRange(selection.getRangeAt(i));
470     }
471   }
475  * Resets the selection.
477  * @param {Node=} domObj a DOM node.  Optional.
479  */
480 cvox.TraverseContent.prototype.reset = function(domObj) {
481   window.getSelection().removeAllRanges();