Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / common / selection_util.js
blob3057d2efc9bec9e49bb50c523bace282cee140d6
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.
5 /**
6  * @fileoverview A collection of JavaScript utilities used to improve selection
7  * at different granularities.
8  */
11 goog.provide('cvox.SelectionUtil');
13 goog.require('cvox.DomUtil');
14 goog.require('cvox.XpathUtil');
16 /**
17  * Utilities for improving selection.
18  * @constructor
19  */
20 cvox.SelectionUtil = function() {};
22 /**
23  * Cleans up a paragraph selection acquired by extending forward.
24  * In this context, a paragraph selection is 'clean' when the focus
25  * node (the end of the selection) is not on a text node.
26  * @param {Selection} sel The paragraph-length selection.
27  * @return {boolean} True if the selection has been cleaned.
28  * False if the selection cannot be cleaned without invalid extension.
29  */
30 cvox.SelectionUtil.cleanUpParagraphForward = function(sel) {
31   var expand = true;
33   // nodeType:3 == TEXT_NODE
34   while (sel.focusNode.nodeType == 3) {
35     // Ending with a text node, which is incorrect. Keep extending forward.
36     var fnode = sel.focusNode;
37     var foffset = sel.focusOffset;
39     sel.modify('extend', 'forward', 'sentence');
40     if ((fnode == sel.focusNode) && (foffset == sel.focusOffset)) {
41       // Nothing more to be done, cannot extend forward further.
42       return false;
43     }
44   }
46   return true;
49 /**
50  * Cleans up a paragraph selection acquired by extending backward.
51  * In this context, a paragraph selection is 'clean' when the focus
52  * node (the end of the selection) is not on a text node.
53  * @param {Selection} sel The paragraph-length selection.
54  * @return {boolean} True if the selection has been cleaned.
55  *     False if the selection cannot be cleaned without invalid extension.
56  */
57 cvox.SelectionUtil.cleanUpParagraphBack = function(sel) {
58   var expand = true;
60   var fnode;
61   var foffset;
63   // nodeType:3 == TEXT_NODE
64   while (sel.focusNode.nodeType == 3) {
65     // Ending with a text node, which is incorrect. Keep extending backward.
66     fnode = sel.focusNode;
67     foffset = sel.focusOffset;
69     sel.modify('extend', 'backward', 'sentence');
71     if ((fnode == sel.focusNode) && (foffset == sel.focusOffset)) {
72       // Nothing more to be done, cannot extend backward further.
73       return true;
74     }
75   }
77   return true;
80 /**
81  * Cleans up a sentence selection by extending forward.
82  * In this context, a sentence selection is 'clean' when the focus
83  * node (the end of the selection) is either:
84  * - not on a text node
85  * - on a text node that ends with a period or a space
86  * @param {Selection} sel The sentence-length selection.
87  * @return {boolean} True if the selection has been cleaned.
88  *     False if the selection cannot be cleaned without invalid extension.
89  */
90 cvox.SelectionUtil.cleanUpSentence = function(sel) {
91   var expand = true;
92   var lastSelection;
93   var lastSelectionOffset;
95   while (expand) {
97     // nodeType:3 == TEXT_NODE
98     if (sel.focusNode.nodeType == 3) {
99       // The focus node is of type text, check end for period
101       var fnode = sel.focusNode;
102       var foffset = sel.focusOffset;
104       if (sel.rangeCount > 0 && sel.getRangeAt(0).endOffset > 0) {
105         if (fnode.substringData(sel.getRangeAt(0).endOffset - 1, 1) == '.') {
106           // Text node ends with period.
107           return true;
108         } else if (fnode.substringData(sel.getRangeAt(0).endOffset - 1, 1) ==
109                    ' ') {
110           // Text node ends with space.
111           return true;
112         } else {
113           // Text node does not end with period or space. Extend forward.
114           sel.modify('extend', 'forward', 'sentence');
116           if ((fnode == sel.focusNode) && (foffset == sel.focusOffset)) {
117             // Nothing more to be done, cannot extend forward any further.
118             return false;
119           }
120         }
121       } else {
122         return true;
123       }
124     } else {
125       // Focus node is not text node, no further cleaning required.
126       return true;
127     }
128   }
130   return true;
134  * Finds the starting position (height from top and left width) of a
135  * selection in a document.
136  * @param {Selection} sel The selection.
137  * @return {Array} The coordinates [top, left] of the selection.
138  */
139 cvox.SelectionUtil.findSelPosition = function(sel) {
140   if (sel.rangeCount == 0) {
141     return [0, 0];
142   }
144   var clientRect = sel.getRangeAt(0).getBoundingClientRect();
146   if (!clientRect) {
147     return [0, 0];
148   }
150   var top = window.pageYOffset + clientRect.top;
151   var left = window.pageXOffset + clientRect.left;
152   return [top, left];
156  * Calculates the horizontal and vertical position of a node
157  * @param {Node} targetNode The node.
158  * @return {Array} The coordinates [top, left] of the node.
159  */
160 cvox.SelectionUtil.findTopLeftPosition = function(targetNode) {
161   var left = 0;
162   var top = 0;
163   var obj = targetNode;
165   if (obj.offsetParent) {
166     left = obj.offsetLeft;
167     top = obj.offsetTop;
168     obj = obj.offsetParent;
170     while (obj !== null) {
171       left += obj.offsetLeft;
172       top += obj.offsetTop;
173       obj = obj.offsetParent;
174     }
175   }
177   return [top, left];
182  * Checks the contents of a selection for meaningful content.
183  * @param {Selection} sel The selection.
184  * @return {boolean} True if the selection is valid.  False if the selection
185  *     contains only whitespace or is an empty string.
186  */
187 cvox.SelectionUtil.isSelectionValid = function(sel) {
188   var regExpWhiteSpace = new RegExp(/^\s+$/);
189   return (! ((regExpWhiteSpace.test(sel.toString())) ||
190              (sel.toString() == '')));
194  * Checks the contents of a range for meaningful content.
195  * @param {Range} range The range.
196  * @return {boolean} True if the range is valid.  False if the range
197  *     contains only whitespace or is an empty string.
198  */
199 cvox.SelectionUtil.isRangeValid = function(range) {
200   var text = range.cloneContents().textContent;
201   var regExpWhiteSpace = new RegExp(/^\s+$/);
202   return (! ((regExpWhiteSpace.test(text)) ||
203              (text == '')));
207  * Returns absolute top and left positions of an element.
209  * @param {!Node} node The element for which to compute the position.
210  * @return {Array<number>} Index 0 is the left; index 1 is the top.
211  * @private
212  */
213 cvox.SelectionUtil.findPos_ = function(node) {
214   var curLeft = 0;
215   var curTop = 0;
216   if (node.offsetParent) {
217     do {
218       curLeft += node.offsetLeft;
219       curTop += node.offsetTop;
220     } while (node = node.offsetParent);
221   }
222   return [curLeft, curTop];
226  * Scrolls node in its parent node such the given node is visible.
227  * @param {Node} focusNode The node.
228  */
229 cvox.SelectionUtil.scrollElementsToView = function(focusNode) {
230   // First, walk up the DOM until we find a node with a bounding rectangle.
231   while (focusNode && !focusNode.getBoundingClientRect) {
232     focusNode = focusNode.parentElement;
233   }
234   if (!focusNode) {
235     return;
236   }
238   // Walk up the DOM, ensuring each element is visible inside its parent.
239   var node = focusNode;
240   var parentNode = node.parentElement;
241   while (node != document.body && parentNode) {
242     node.scrollTop = node.offsetTop;
243     node.scrollLeft = node.offsetLeft;
244     node = parentNode;
245     parentNode = node.parentElement;
246   }
248   // Center the active element on the page once we know it's visible.
249   var pos = cvox.SelectionUtil.findPos_(focusNode);
250   window.scrollTo(pos[0] - window.innerWidth / 2,
251                   pos[1] - window.innerHeight / 2);
255  * Scrolls the selection into view if it is out of view in the current window.
256  * Inspired by workaround for already-on-screen elements @
257  * http://
258  * www.performantdesign.com/2009/08/26/scrollintoview-but-only-if-out-of-view/
259  * @param {Selection} sel The selection to be scrolled into view.
260  */
261 cvox.SelectionUtil.scrollToSelection = function(sel) {
262   if (sel.rangeCount == 0) {
263     return;
264   }
266   // First, scroll all parent elements into view.  Later, move the body
267   // which works slightly differently.
269   cvox.SelectionUtil.scrollElementsToView(sel.focusNode);
271   var pos = cvox.SelectionUtil.findSelPosition(sel);
272   var top = pos[0];
273   var left = pos[1];
275   var scrolledVertically = window.pageYOffset ||
276       document.documentElement.scrollTop ||
277       document.body.scrollTop;
278   var pageHeight = window.innerHeight ||
279       document.documentElement.clientHeight || document.body.clientHeight;
280   var pageWidth = window.innerWidth ||
281       document.documentElement.innerWidth || document.body.clientWidth;
283   if (left < pageWidth) {
284     left = 0;
285   }
287   // window.scroll puts specified pixel in upper left of window
288   if ((scrolledVertically + pageHeight) < top) {
289     // Align with bottom of page
290     var diff = top - pageHeight;
291     window.scroll(left, diff + 100);
292   } else if (top < scrolledVertically) {
293     // Align with top of page
294     window.scroll(left, top - 100);
295   }
299  * This is from  https://developer.mozilla.org/en/Whitespace_in_the_DOM
300  * Determine whether a node's text content is entirely whitespace.
302  * Throughout, whitespace is defined as one of the characters
303  *  "\t" TAB \u0009
304  *  "\n" LF  \u000A
305  *  "\r" CR  \u000D
306  *  " "  SPC \u0020
308  * This does not use Javascript's "\s" because that includes non-breaking
309  * spaces (and also some other characters).
311  * @param {Node} node A node implementing the |CharacterData| interface (i.e.,
312  *             a |Text|, |Comment|, or |CDATASection| node.
313  * @return {boolean} True if all of the text content of |node| is whitespace,
314  *             otherwise false.
315  */
316 cvox.SelectionUtil.isAllWs = function(node) {
317   // Use ECMA-262 Edition 3 String and RegExp features
318   return !(/[^\t\n\r ]/.test(node.data));
323  * This is from  https://developer.mozilla.org/en/Whitespace_in_the_DOM
324  * Determine if a node should be ignored by the iterator functions.
326  * @param {Node} node  An object implementing the DOM1 |Node| interface.
327  * @return {boolean}  True if the node is:
328  *                1) A |Text| node that is all whitespace
329  *                2) A |Comment| node
330  *             and otherwise false.
331  */
333 cvox.SelectionUtil.isIgnorable = function(node) {
334   return (node.nodeType == 8) || // A comment node
335          ((node.nodeType == 3) &&
336           cvox.SelectionUtil.isAllWs(node)); // a text node, all ws
340  * This is from  https://developer.mozilla.org/en/Whitespace_in_the_DOM
341  * Version of |previousSibling| that skips nodes that are entirely
342  * whitespace or comments.  (Normally |previousSibling| is a property
343  * of all DOM nodes that gives the sibling node, the node that is
344  * a child of the same parent, that occurs immediately before the
345  * reference node.)
347  * @param {Node} sib  The reference node.
348  * @return {Node} Either:
349  *               1) The closest previous sibling to |sib| that is not
350  *                  ignorable according to |isIgnorable|, or
351  *               2) null if no such node exists.
352  */
353 cvox.SelectionUtil.nodeBefore = function(sib) {
354   while ((sib = sib.previousSibling)) {
355     if (!cvox.SelectionUtil.isIgnorable(sib)) {
356       return sib;
357     }
358   }
359   return null;
363  * This is from  https://developer.mozilla.org/en/Whitespace_in_the_DOM
364  * Version of |nextSibling| that skips nodes that are entirely
365  * whitespace or comments.
367  * @param {Node} sib  The reference node.
368  * @return {Node} Either:
369  *               1) The closest next sibling to |sib| that is not
370  *                  ignorable according to |isIgnorable|, or
371  *               2) null if no such node exists.
372  */
373 cvox.SelectionUtil.nodeAfter = function(sib) {
374   while ((sib = sib.nextSibling)) {
375     if (!cvox.SelectionUtil.isIgnorable(sib)) {
376       return sib;
377     }
378   }
379   return null;
383  * This is from  https://developer.mozilla.org/en/Whitespace_in_the_DOM
384  * Version of |lastChild| that skips nodes that are entirely
385  * whitespace or comments.  (Normally |lastChild| is a property
386  * of all DOM nodes that gives the last of the nodes contained
387  * directly in the reference node.)
389  * @param {Node} par  The reference node.
390  * @return {Node} Either:
391  *               1) The last child of |sib| that is not
392  *                  ignorable according to |isIgnorable|, or
393  *               2) null if no such node exists.
394  */
395 cvox.SelectionUtil.lastChildNode = function(par) {
396   var res = par.lastChild;
397   while (res) {
398     if (!cvox.SelectionUtil.isIgnorable(res)) {
399       return res;
400     }
401     res = res.previousSibling;
402   }
403   return null;
407  * This is from  https://developer.mozilla.org/en/Whitespace_in_the_DOM
408  * Version of |firstChild| that skips nodes that are entirely
409  * whitespace and comments.
411  * @param {Node} par  The reference node.
412  * @return {Node} Either:
413  *               1) The first child of |sib| that is not
414  *                  ignorable according to |isIgnorable|, or
415  *               2) null if no such node exists.
416  */
417 cvox.SelectionUtil.firstChildNode = function(par) {
418   var res = par.firstChild;
419   while (res) {
420     if (!cvox.SelectionUtil.isIgnorable(res)) {
421       return res;
422     }
423     res = res.nextSibling;
424   }
425   return null;
429  * This is from  https://developer.mozilla.org/en/Whitespace_in_the_DOM
430  * Version of |data| that doesn't include whitespace at the beginning
431  * and end and normalizes all whitespace to a single space.  (Normally
432  * |data| is a property of text nodes that gives the text of the node.)
434  * @param {Node} txt  The text node whose data should be returned.
435  * @return {string} A string giving the contents of the text node with
436  *             whitespace collapsed.
437  */
438 cvox.SelectionUtil.dataOf = function(txt) {
439   var data = txt.data;
440   // Use ECMA-262 Edition 3 String and RegExp features
441   data = data.replace(/[\t\n\r ]+/g, ' ');
442   if (data.charAt(0) == ' ') {
443     data = data.substring(1, data.length);
444   }
445   if (data.charAt(data.length - 1) == ' ') {
446     data = data.substring(0, data.length - 1);
447   }
448   return data;
452  * Returns true if the selection has content from at least one node
453  * that has the specified tagName.
455  * @param {Selection} sel The selection.
456  * @param {string} tagName  Tagname that the selection should be checked for.
457  * @return {boolean} True if the selection has content from at least one node
458  *                   with the specified tagName.
459  */
460 cvox.SelectionUtil.hasContentWithTag = function(sel, tagName) {
461   if (!sel || !sel.anchorNode || !sel.focusNode) {
462     return false;
463   }
464   if (sel.anchorNode.tagName && (sel.anchorNode.tagName == tagName)) {
465     return true;
466   }
467   if (sel.focusNode.tagName && (sel.focusNode.tagName == tagName)) {
468     return true;
469   }
470   if (sel.anchorNode.parentNode.tagName &&
471       (sel.anchorNode.parentNode.tagName == tagName)) {
472     return true;
473   }
474   if (sel.focusNode.parentNode.tagName &&
475       (sel.focusNode.parentNode.tagName == tagName)) {
476     return true;
477   }
478   var docFrag = sel.getRangeAt(0).cloneContents();
479   var span = document.createElement('span');
480   span.appendChild(docFrag);
481   return (span.getElementsByTagName(tagName).length > 0);
485  * Selects text within a text node.
487  * Note that the input node MUST be of type TEXT; otherwise, the offset
488  * count would not mean # of characters - this is because of the way Range
489  * works in JavaScript.
491  * @param {Node} textNode The text node to select text within.
492  * @param {number} start  The start of the selection.
493  * @param {number} end The end of the selection.
494  */
495 cvox.SelectionUtil.selectText = function(textNode, start, end) {
496   var newRange = document.createRange();
497   newRange.setStart(textNode, start);
498   newRange.setEnd(textNode, end);
499   var sel = window.getSelection();
500   sel.removeAllRanges();
501   sel.addRange(newRange);
505  * Selects all the text in a given node.
507  * @param {Node} node The target node.
508  */
509 cvox.SelectionUtil.selectAllTextInNode = function(node) {
510   var newRange = document.createRange();
511   newRange.setStart(node, 0);
512   newRange.setEndAfter(node);
513   var sel = window.getSelection();
514   sel.removeAllRanges();
515   sel.addRange(newRange);
519  * Collapses the selection to the start. If nothing is selected,
520  * selects the beginning of the given node.
522  * @param {Node} node The target node.
523  */
524 cvox.SelectionUtil.collapseToStart = function(node) {
525   var sel = window.getSelection();
526   var cursorNode = sel.anchorNode;
527   var cursorOffset = sel.anchorOffset;
528   if (cursorNode == null) {
529     cursorNode = node;
530     cursorOffset = 0;
531   }
532   var newRange = document.createRange();
533   newRange.setStart(cursorNode, cursorOffset);
534   newRange.setEnd(cursorNode, cursorOffset);
535   sel.removeAllRanges();
536   sel.addRange(newRange);
540  * Collapses the selection to the end. If nothing is selected,
541  * selects the end of the given node.
543  * @param {Node} node The target node.
544  */
545 cvox.SelectionUtil.collapseToEnd = function(node) {
546   var sel = window.getSelection();
547   var cursorNode = sel.focusNode;
548   var cursorOffset = sel.focusOffset;
549   if (cursorNode == null) {
550     cursorNode = node;
551     cursorOffset = 0;
552   }
553   var newRange = document.createRange();
554   newRange.setStart(cursorNode, cursorOffset);
555   newRange.setEnd(cursorNode, cursorOffset);
556   sel.removeAllRanges();
557   sel.addRange(newRange);
561  * Retrieves all the text within a selection.
563  * Note that this can be different than simply using the string from
564  * window.getSelection() as this will account for IMG nodes, etc.
566  * @return {string} The string of text contained in the current selection.
567  */
568 cvox.SelectionUtil.getText = function() {
569   var sel = window.getSelection();
570   if (cvox.SelectionUtil.hasContentWithTag(sel, 'IMG')) {
571     var text = '';
572     var docFrag = sel.getRangeAt(0).cloneContents();
573     var span = document.createElement('span');
574     span.appendChild(docFrag);
575     var leafNodes = cvox.XpathUtil.getLeafNodes(span);
576     for (var i = 0, node; node = leafNodes[i]; i++) {
577       text = text + ' ' + cvox.DomUtil.getName(node);
578     }
579     return text;
580   } else {
581     return this.getSelectionText_();
582   }
586  * Returns the selection as text instead of a selection object. Note that this
587  * function must be used in place of getting text directly from the DOM
588  * if you want i18n tests to pass.
590  * @return {string} The text.
591  * @private
592  */
593 cvox.SelectionUtil.getSelectionText_ = function() {
594   return '' + window.getSelection();
599  * Returns a range as text instead of a selection object. Note that this
600  * function must be used in place of getting text directly from the DOM
601  * if you want i18n tests to pass.
603  * @param {Range} range A range.
604  * @return {string} The text.
605  */
606 cvox.SelectionUtil.getRangeText = function(range) {
607   if (range)
608     return range.cloneContents().textContent.replace(/\s+/g, ' ');
609   else
610     return '';