Allow only one bookmark to be added for multiple fast starring
[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.
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.
26 * @constructor
27 * @param {Node=} domObj a DOM node (optional).
29 cvox.TraverseContent = function(domObj) {
30 if (domObj != null) {
31 this.currentDomObj = domObj;
32 } else {
33 this.currentDomObj = document.body;
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);
52 goog.addSingletonGetter(cvox.TraverseContent);
54 /**
55 * Whether the last navigated selection only contained whitespace.
56 * @type {boolean}
58 cvox.TraverseContent.prototype.lastSelectionWasWhitespace = false;
60 /**
61 * Whether we should skip whitespace when traversing individual characters.
62 * @type {boolean}
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}
73 cvox.TraverseContent.prototype.skipInvalidSelections = true;
75 /**
76 * If line and sentence navigation should break at <a> links.
77 * @type {boolean}
79 cvox.TraverseContent.prototype.breakAtLinks = true;
81 /**
82 * The string constant for character granularity.
83 * @type {string}
84 * @const
86 cvox.TraverseContent.kCharacter = 'character';
88 /**
89 * The string constant for word granularity.
90 * @type {string}
91 * @const
93 cvox.TraverseContent.kWord = 'word';
95 /**
96 * The string constant for sentence granularity.
97 * @type {string}
98 * @const
100 cvox.TraverseContent.kSentence = 'sentence';
103 * The string constant for line granularity.
104 * @type {string}
105 * @const
107 cvox.TraverseContent.kLine = 'line';
110 * The string constant for paragraph granularity.
111 * @type {string}
112 * @const
114 cvox.TraverseContent.kParagraph = 'paragraph';
117 * A constant array of all granularities.
118 * @type {Array<string>}
119 * @const
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.
131 cvox.TraverseContent.prototype.syncToSelection = function() {
132 this.normalizeSelection();
134 var selection = window.getSelection();
135 if (!selection || !selection.anchorNode || !selection.focusNode) {
136 return;
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.
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.
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.
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.
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 ');
183 return range;
187 * Get the current text content as a string.
188 * @return {string} The current spanned content.
190 cvox.TraverseContent.prototype.getCurrentText = function() {
191 return cvox.SelectionUtil.getRangeText(this.getCurrentRange());
195 * Collapse to the end of the range.
197 cvox.TraverseContent.prototype.collapseToEnd = function() {
198 this.startCursor_ = this.endCursor_.clone();
202 * Collapse to the start of the range.
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.
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;
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);
262 if (str == null) {
263 // We reached the end of the document.
264 return null;
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;
285 } else {
286 this.lastSelectionWasWhitespace = false;
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.
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;
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);
347 if (str == null) {
348 // We reached the end of the document.
349 return null;
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;
370 } else {
371 this.lastSelectionWasWhitespace = false;
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.
385 cvox.TraverseContent.prototype.getBreakTags = function() {
386 return {
387 'A': this.breakAtLinks,
388 'BR': true,
389 'HR': true
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.
404 cvox.TraverseContent.prototype.nextElement = function(grain, domObj) {
405 if (domObj != null) {
406 this.currentDomObj = domObj;
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;
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.
433 cvox.TraverseContent.prototype.prevElement = function(grain, domObj) {
434 if (domObj != null) {
435 this.currentDomObj = domObj;
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;
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.
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));
475 * Resets the selection.
477 * @param {Node=} domObj a DOM node. Optional.
480 cvox.TraverseContent.prototype.reset = function(domObj) {
481 window.getSelection().removeAllRanges();