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 * @fileoverview A collection of JavaScript utilities used to improve selection
7 * at different granularities.
11 goog.provide('cvox.SelectionUtil');
13 goog.require('cvox.DomUtil');
14 goog.require('cvox.XpathUtil');
17 * Utilities for improving selection.
20 cvox.SelectionUtil = function() {};
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.
30 cvox.SelectionUtil.cleanUpParagraphForward = function(sel) {
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.
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.
57 cvox.SelectionUtil.cleanUpParagraphBack = function(sel) {
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.
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.
90 cvox.SelectionUtil.cleanUpSentence = function(sel) {
93 var lastSelectionOffset;
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.
108 } else if (fnode.substringData(sel.getRangeAt(0).endOffset - 1, 1) ==
110 // Text node ends with space.
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.
125 // Focus node is not text node, no further cleaning required.
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.
139 cvox.SelectionUtil.findSelPosition = function(sel) {
140 if (sel.rangeCount == 0) {
144 var clientRect = sel.getRangeAt(0).getBoundingClientRect();
150 var top = window.pageYOffset + clientRect.top;
151 var left = window.pageXOffset + clientRect.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.
160 cvox.SelectionUtil.findTopLeftPosition = function(targetNode) {
163 var obj = targetNode;
165 if (obj.offsetParent) {
166 left = obj.offsetLeft;
168 obj = obj.offsetParent;
170 while (obj !== null) {
171 left += obj.offsetLeft;
172 top += obj.offsetTop;
173 obj = obj.offsetParent;
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.
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.
199 cvox.SelectionUtil.isRangeValid = function(range) {
200 var text = range.cloneContents().textContent;
201 var regExpWhiteSpace = new RegExp(/^\s+$/);
202 return (! ((regExpWhiteSpace.test(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.
213 cvox.SelectionUtil.findPos_ = function(node) {
216 if (node.offsetParent) {
218 curLeft += node.offsetLeft;
219 curTop += node.offsetTop;
220 } while (node = node.offsetParent);
222 return [curLeft, curTop];
226 * Scrolls node in its parent node such the given node is visible.
227 * @param {Node} focusNode The node.
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;
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;
245 parentNode = node.parentElement;
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 @
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.
261 cvox.SelectionUtil.scrollToSelection = function(sel) {
262 if (sel.rangeCount == 0) {
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);
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) {
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);
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
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,
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.
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
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.
353 cvox.SelectionUtil.nodeBefore = function(sib) {
354 while ((sib = sib.previousSibling)) {
355 if (!cvox.SelectionUtil.isIgnorable(sib)) {
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.
373 cvox.SelectionUtil.nodeAfter = function(sib) {
374 while ((sib = sib.nextSibling)) {
375 if (!cvox.SelectionUtil.isIgnorable(sib)) {
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.
395 cvox.SelectionUtil.lastChildNode = function(par) {
396 var res = par.lastChild;
398 if (!cvox.SelectionUtil.isIgnorable(res)) {
401 res = res.previousSibling;
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.
417 cvox.SelectionUtil.firstChildNode = function(par) {
418 var res = par.firstChild;
420 if (!cvox.SelectionUtil.isIgnorable(res)) {
423 res = res.nextSibling;
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.
438 cvox.SelectionUtil.dataOf = function(txt) {
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);
445 if (data.charAt(data.length - 1) == ' ') {
446 data = data.substring(0, data.length - 1);
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.
460 cvox.SelectionUtil.hasContentWithTag = function(sel, tagName) {
461 if (!sel || !sel.anchorNode || !sel.focusNode) {
464 if (sel.anchorNode.tagName && (sel.anchorNode.tagName == tagName)) {
467 if (sel.focusNode.tagName && (sel.focusNode.tagName == tagName)) {
470 if (sel.anchorNode.parentNode.tagName &&
471 (sel.anchorNode.parentNode.tagName == tagName)) {
474 if (sel.focusNode.parentNode.tagName &&
475 (sel.focusNode.parentNode.tagName == tagName)) {
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.
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.
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.
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) {
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.
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) {
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.
568 cvox.SelectionUtil.getText = function() {
569 var sel = window.getSelection();
570 if (cvox.SelectionUtil.hasContentWithTag(sel, 'IMG')) {
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);
581 return this.getSelectionText_();
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.
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.
606 cvox.SelectionUtil.getRangeText = function(range) {
608 return range.cloneContents().textContent.replace(/\s+/g, ' ');