| 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 collection of JavaScript utilities used to improve selection |
| 7 * at different granularities. |
| 8 */ |
| 9 |
| 10 |
| 11 goog.provide('cvox.SelectionUtil'); |
| 12 |
| 13 goog.require('cvox.DomUtil'); |
| 14 goog.require('cvox.XpathUtil'); |
| 15 |
| 16 /** |
| 17 * Utilities for improving selection. |
| 18 * @constructor |
| 19 */ |
| 20 cvox.SelectionUtil = function() {}; |
| 21 |
| 22 /** |
| 23 * Checks if a given node is a descendant of another given node. |
| 24 * |
| 25 * @param {Node} nod The candidate descendant. |
| 26 * @param {Node} bound The candidate parent. |
| 27 * @return {boolean} True if nod is a descendant of bound. |
| 28 */ |
| 29 cvox.SelectionUtil.isWithinBound = function(nod, bound) { |
| 30 var parent = nod; |
| 31 while (parent != null) { |
| 32 if (parent.isSameNode(bound)) { |
| 33 return true; |
| 34 } else { |
| 35 parent = parent.parentNode; |
| 36 } |
| 37 } |
| 38 return false; |
| 39 }; |
| 40 |
| 41 /** |
| 42 * Cleans up a paragraph selection acquired by extending forward. |
| 43 * In this context, a paragraph selection is 'clean' when the focus |
| 44 * node (the end of the selection) is not on a text node. |
| 45 * @param {Selection} sel The paragraph-length selection. |
| 46 * @return {boolean} True if the selection has been cleaned. |
| 47 * False if the selection cannot be cleaned without invalid extension. |
| 48 */ |
| 49 cvox.SelectionUtil.cleanUpParagraphForward = function(sel) { |
| 50 var expand = true; |
| 51 |
| 52 // nodeType:3 == TEXT_NODE |
| 53 while (sel.focusNode.nodeType == 3) { |
| 54 // Ending with a text node, which is incorrect. Keep extending forward. |
| 55 var fnode = sel.focusNode; |
| 56 var foffset = sel.focusOffset; |
| 57 |
| 58 sel.modify('extend', 'forward', 'sentence'); |
| 59 if ((fnode == sel.focusNode) && (foffset == sel.focusOffset)) { |
| 60 // Nothing more to be done, cannot extend forward further. |
| 61 return false; |
| 62 } |
| 63 } |
| 64 |
| 65 return true; |
| 66 }; |
| 67 |
| 68 /** |
| 69 * Cleans up a paragraph selection acquired by extending backward. |
| 70 * In this context, a paragraph selection is 'clean' when the focus |
| 71 * node (the end of the selection) is not on a text node. |
| 72 * @param {Selection} sel The paragraph-length selection. |
| 73 * @return {boolean} True if the selection has been cleaned. |
| 74 * False if the selection cannot be cleaned without invalid extension. |
| 75 */ |
| 76 cvox.SelectionUtil.cleanUpParagraphBack = function(sel) { |
| 77 var expand = true; |
| 78 |
| 79 var fnode; |
| 80 var foffset; |
| 81 |
| 82 // nodeType:3 == TEXT_NODE |
| 83 while (sel.focusNode.nodeType == 3) { |
| 84 // Ending with a text node, which is incorrect. Keep extending backward. |
| 85 fnode = sel.focusNode; |
| 86 foffset = sel.focusOffset; |
| 87 |
| 88 sel.modify('extend', 'backward', 'sentence'); |
| 89 |
| 90 if ((fnode == sel.focusNode) && (foffset == sel.focusOffset)) { |
| 91 // Nothing more to be done, cannot extend backward further. |
| 92 return true; |
| 93 } |
| 94 } |
| 95 |
| 96 return true; |
| 97 }; |
| 98 |
| 99 /** |
| 100 * Cleans up a sentence selection by extending forward. |
| 101 * In this context, a sentence selection is 'clean' when the focus |
| 102 * node (the end of the selection) is either: |
| 103 * - not on a text node |
| 104 * - on a text node that ends with a period or a space |
| 105 * @param {Selection} sel The sentence-length selection. |
| 106 * @return {boolean} True if the selection has been cleaned. |
| 107 * False if the selection cannot be cleaned without invalid extension. |
| 108 */ |
| 109 cvox.SelectionUtil.cleanUpSentence = function(sel) { |
| 110 var expand = true; |
| 111 var lastSelection; |
| 112 var lastSelectionOffset; |
| 113 |
| 114 while (expand) { |
| 115 |
| 116 // nodeType:3 == TEXT_NODE |
| 117 if (sel.focusNode.nodeType == 3) { |
| 118 // The focus node is of type text, check end for period |
| 119 |
| 120 var fnode = sel.focusNode; |
| 121 var foffset = sel.focusOffset; |
| 122 |
| 123 if (sel.rangeCount > 0 && sel.getRangeAt(0).endOffset > 0) { |
| 124 if (fnode.substringData(sel.getRangeAt(0).endOffset - 1, 1) == '.') { |
| 125 // Text node ends with period. |
| 126 return true; |
| 127 } else if (fnode.substringData(sel.getRangeAt(0).endOffset - 1, 1) == |
| 128 ' ') { |
| 129 // Text node ends with space. |
| 130 return true; |
| 131 } else { |
| 132 // Text node does not end with period or space. Extend forward. |
| 133 sel.modify('extend', 'forward', 'sentence'); |
| 134 |
| 135 if ((fnode == sel.focusNode) && (foffset == sel.focusOffset)) { |
| 136 // Nothing more to be done, cannot extend forward any further. |
| 137 return false; |
| 138 } |
| 139 } |
| 140 } else { |
| 141 return true; |
| 142 } |
| 143 } else { |
| 144 // Focus node is not text node, no further cleaning required. |
| 145 return true; |
| 146 } |
| 147 } |
| 148 |
| 149 return true; |
| 150 }; |
| 151 |
| 152 /** |
| 153 * Finds the starting position (height from top and left width) of a |
| 154 * selection in a document. |
| 155 * @param {Selection} sel The selection. |
| 156 * @return {Array} The coordinates [top, left] of the selection. |
| 157 */ |
| 158 cvox.SelectionUtil.findSelPosition = function(sel) { |
| 159 if (sel.rangeCount == 0) { |
| 160 return [0, 0]; |
| 161 } |
| 162 |
| 163 var clientRect = sel.getRangeAt(0).getBoundingClientRect(); |
| 164 var top = window.pageYOffset + clientRect.top; |
| 165 var left = window.pageXOffset + clientRect.left; |
| 166 return [top, left]; |
| 167 }; |
| 168 |
| 169 /** |
| 170 * Calculates the horizontal and vertical position of a node |
| 171 * @param {Node} targetNode The node. |
| 172 * @return {Array} The coordinates [top, left] of the node. |
| 173 */ |
| 174 cvox.SelectionUtil.findTopLeftPosition = function(targetNode) { |
| 175 var left = 0; |
| 176 var top = 0; |
| 177 var obj = targetNode; |
| 178 |
| 179 if (obj.offsetParent) { |
| 180 left = obj.offsetLeft; |
| 181 top = obj.offsetTop; |
| 182 obj = obj.offsetParent; |
| 183 |
| 184 while (obj !== null) { |
| 185 left += obj.offsetLeft; |
| 186 top += obj.offsetTop; |
| 187 obj = obj.offsetParent; |
| 188 } |
| 189 } |
| 190 |
| 191 return [top, left]; |
| 192 }; |
| 193 |
| 194 |
| 195 /** |
| 196 * Checks the contents of a selection for meaningful content. |
| 197 * @param {Selection} sel The selection. |
| 198 * @return {boolean} True if the selection is valid. False if the selection |
| 199 * contains only whitespace or is an empty string. |
| 200 */ |
| 201 cvox.SelectionUtil.isSelectionValid = function(sel) { |
| 202 var regExpWhiteSpace = new RegExp(/^\s+$/); |
| 203 return (! ((regExpWhiteSpace.test(sel.toString())) || |
| 204 (sel.toString() == ''))); |
| 205 }; |
| 206 |
| 207 |
| 208 /** |
| 209 * Scrolls the selection into view if it is out of view in the current window. |
| 210 * Inspired by workaround for already-on-screen elements @ |
| 211 * http:// |
| 212 * www.performantdesign.com/2009/08/2/scrollintoview-but-only-if-out-of-view/ |
| 213 * @param {Selection} sel The selection to be scrolled into view. |
| 214 */ |
| 215 cvox.SelectionUtil.scrollToSelection = function(sel) { |
| 216 if (sel.rangeCount == 0) { |
| 217 return; |
| 218 } |
| 219 |
| 220 var pos = cvox.SelectionUtil.findSelPosition(sel); |
| 221 var top = pos[0]; |
| 222 var left = pos[1]; |
| 223 |
| 224 var scrolledVertically = window.pageYOffset || |
| 225 document.documentElement.scrollTop || |
| 226 document.body.scrollTop; |
| 227 var pageHeight = window.innerHeight || |
| 228 document.documentElement.clientHeight || document.body.clientHeight; |
| 229 var pageWidth = window.innerWidth || |
| 230 document.documentElement.innerWidth || document.body.clientWidth; |
| 231 |
| 232 if (left < pageWidth) { |
| 233 left = 0; |
| 234 } |
| 235 |
| 236 // window.scroll puts specified pixel in upper left of window |
| 237 if ((scrolledVertically + pageHeight) < top) { |
| 238 // Align with bottom of page |
| 239 var diff = top - pageHeight; |
| 240 window.scroll(left, diff + 100); |
| 241 } else if (top < scrolledVertically) { |
| 242 // Align with top of page |
| 243 window.scroll(left, top - 100); |
| 244 } |
| 245 }; |
| 246 |
| 247 /** |
| 248 * This is from https://developer.mozilla.org/en/Whitespace_in_the_DOM |
| 249 * Determine whether a node's text content is entirely whitespace. |
| 250 * |
| 251 * Throughout, whitespace is defined as one of the characters |
| 252 * "\t" TAB \u0009 |
| 253 * "\n" LF \u000A |
| 254 * "\r" CR \u000D |
| 255 * " " SPC \u0020 |
| 256 * |
| 257 * This does not use Javascript's "\s" because that includes non-breaking |
| 258 * spaces (and also some other characters). |
| 259 * |
| 260 * @param {Node} node A node implementing the |CharacterData| interface (i.e., |
| 261 * a |Text|, |Comment|, or |CDATASection| node. |
| 262 * @return {boolean} True if all of the text content of |node| is whitespace, |
| 263 * otherwise false. |
| 264 */ |
| 265 cvox.SelectionUtil.isAllWs = function(node) { |
| 266 // Use ECMA-262 Edition 3 String and RegExp features |
| 267 return !(/[^\t\n\r ]/.test(node.data)); |
| 268 }; |
| 269 |
| 270 |
| 271 /** |
| 272 * This is from https://developer.mozilla.org/en/Whitespace_in_the_DOM |
| 273 * Determine if a node should be ignored by the iterator functions. |
| 274 * |
| 275 * @param {Node} node An object implementing the DOM1 |Node| interface. |
| 276 * @return {boolean} True if the node is: |
| 277 * 1) A |Text| node that is all whitespace |
| 278 * 2) A |Comment| node |
| 279 * and otherwise false. |
| 280 */ |
| 281 |
| 282 cvox.SelectionUtil.isIgnorable = function(node) { |
| 283 return (node.nodeType == 8) || // A comment node |
| 284 ((node.nodeType == 3) && |
| 285 cvox.SelectionUtil.isAllWs(node)); // a text node, all ws |
| 286 }; |
| 287 |
| 288 /** |
| 289 * This is from https://developer.mozilla.org/en/Whitespace_in_the_DOM |
| 290 * Version of |previousSibling| that skips nodes that are entirely |
| 291 * whitespace or comments. (Normally |previousSibling| is a property |
| 292 * of all DOM nodes that gives the sibling node, the node that is |
| 293 * a child of the same parent, that occurs immediately before the |
| 294 * reference node.) |
| 295 * |
| 296 * @param {Node} sib The reference node. |
| 297 * @return {Node} Either: |
| 298 * 1) The closest previous sibling to |sib| that is not |
| 299 * ignorable according to |isIgnorable|, or |
| 300 * 2) null if no such node exists. |
| 301 */ |
| 302 cvox.SelectionUtil.nodeBefore = function(sib) { |
| 303 while ((sib = sib.previousSibling)) { |
| 304 if (!cvox.SelectionUtil.isIgnorable(sib)) { |
| 305 return sib; |
| 306 } |
| 307 } |
| 308 return null; |
| 309 }; |
| 310 |
| 311 /** |
| 312 * This is from https://developer.mozilla.org/en/Whitespace_in_the_DOM |
| 313 * Version of |nextSibling| that skips nodes that are entirely |
| 314 * whitespace or comments. |
| 315 * |
| 316 * @param {Node} sib The reference node. |
| 317 * @return {Node} Either: |
| 318 * 1) The closest next sibling to |sib| that is not |
| 319 * ignorable according to |isIgnorable|, or |
| 320 * 2) null if no such node exists. |
| 321 */ |
| 322 cvox.SelectionUtil.nodeAfter = function(sib) { |
| 323 while ((sib = sib.nextSibling)) { |
| 324 if (!cvox.SelectionUtil.isIgnorable(sib)) { |
| 325 return sib; |
| 326 } |
| 327 } |
| 328 return null; |
| 329 }; |
| 330 |
| 331 /** |
| 332 * This is from https://developer.mozilla.org/en/Whitespace_in_the_DOM |
| 333 * Version of |lastChild| that skips nodes that are entirely |
| 334 * whitespace or comments. (Normally |lastChild| is a property |
| 335 * of all DOM nodes that gives the last of the nodes contained |
| 336 * directly in the reference node.) |
| 337 * |
| 338 * @param {Node} par The reference node. |
| 339 * @return {Node} Either: |
| 340 * 1) The last child of |sib| that is not |
| 341 * ignorable according to |isIgnorable|, or |
| 342 * 2) null if no such node exists. |
| 343 */ |
| 344 cvox.SelectionUtil.lastChildNode = function(par) { |
| 345 var res = par.lastChild; |
| 346 while (res) { |
| 347 if (!cvox.SelectionUtil.isIgnorable(res)) { |
| 348 return res; |
| 349 } |
| 350 res = res.previousSibling; |
| 351 } |
| 352 return null; |
| 353 }; |
| 354 |
| 355 /** |
| 356 * This is from https://developer.mozilla.org/en/Whitespace_in_the_DOM |
| 357 * Version of |firstChild| that skips nodes that are entirely |
| 358 * whitespace and comments. |
| 359 * |
| 360 * @param {Node} par The reference node. |
| 361 * @return {Node} Either: |
| 362 * 1) The first child of |sib| that is not |
| 363 * ignorable according to |isIgnorable|, or |
| 364 * 2) null if no such node exists. |
| 365 */ |
| 366 cvox.SelectionUtil.firstChildNode = function(par) { |
| 367 var res = par.firstChild; |
| 368 while (res) { |
| 369 if (!cvox.SelectionUtil.isIgnorable(res)) { |
| 370 return res; |
| 371 } |
| 372 res = res.nextSibling; |
| 373 } |
| 374 return null; |
| 375 }; |
| 376 |
| 377 /** |
| 378 * This is from https://developer.mozilla.org/en/Whitespace_in_the_DOM |
| 379 * Version of |data| that doesn't include whitespace at the beginning |
| 380 * and end and normalizes all whitespace to a single space. (Normally |
| 381 * |data| is a property of text nodes that gives the text of the node.) |
| 382 * |
| 383 * @param {Node} txt The text node whose data should be returned. |
| 384 * @return {string} A string giving the contents of the text node with |
| 385 * whitespace collapsed. |
| 386 */ |
| 387 cvox.SelectionUtil.dataOf = function(txt) { |
| 388 var data = txt.data; |
| 389 // Use ECMA-262 Edition 3 String and RegExp features |
| 390 data = data.replace(/[\t\n\r ]+/g, ' '); |
| 391 if (data.charAt(0) == ' ') { |
| 392 data = data.substring(1, data.length); |
| 393 } |
| 394 if (data.charAt(data.length - 1) == ' ') { |
| 395 data = data.substring(0, data.length - 1); |
| 396 } |
| 397 return data; |
| 398 }; |
| 399 |
| 400 /** |
| 401 * Returns true if the selection has content from at least one node |
| 402 * that has the specified tagName. |
| 403 * |
| 404 * @param {Selection} sel The selection. |
| 405 * @param {string} tagName Tagname that the selection should be checked for. |
| 406 * @return {boolean} True if the selection has content from at least one node |
| 407 * with the specified tagName. |
| 408 */ |
| 409 cvox.SelectionUtil.hasContentWithTag = function(sel, tagName) { |
| 410 if (!sel || !sel.anchorNode || !sel.focusNode) { |
| 411 return false; |
| 412 } |
| 413 if (sel.anchorNode.tagName && (sel.anchorNode.tagName == tagName)) { |
| 414 return true; |
| 415 } |
| 416 if (sel.focusNode.tagName && (sel.focusNode.tagName == tagName)) { |
| 417 return true; |
| 418 } |
| 419 if (sel.anchorNode.parentNode.tagName && |
| 420 (sel.anchorNode.parentNode.tagName == tagName)) { |
| 421 return true; |
| 422 } |
| 423 if (sel.focusNode.parentNode.tagName && |
| 424 (sel.focusNode.parentNode.tagName == tagName)) { |
| 425 return true; |
| 426 } |
| 427 var docFrag = sel.getRangeAt(0).cloneContents(); |
| 428 var span = document.createElement('span'); |
| 429 span.appendChild(docFrag); |
| 430 return (span.getElementsByTagName(tagName).length > 0); |
| 431 }; |
| 432 |
| 433 /** |
| 434 * Selects text within a text node. |
| 435 * |
| 436 * Note that the input node MUST be of type TEXT; otherwise, the offset |
| 437 * count would not mean # of characters - this is because of the way Range |
| 438 * works in JavaScript. |
| 439 * |
| 440 * @param {Node} textNode The text node to select text within. |
| 441 * @param {number} start The start of the selection. |
| 442 * @param {number} end The end of the selection. |
| 443 */ |
| 444 cvox.SelectionUtil.selectText = function(textNode, start, end) { |
| 445 var newRange = document.createRange(); |
| 446 newRange.setStart(textNode, start); |
| 447 newRange.setEnd(textNode, end); |
| 448 var sel = window.getSelection(); |
| 449 sel.removeAllRanges(); |
| 450 sel.addRange(newRange); |
| 451 }; |
| 452 |
| 453 /** |
| 454 * Selects all the text in a given node. |
| 455 * |
| 456 * @param {Node} node The target node. |
| 457 */ |
| 458 cvox.SelectionUtil.selectAllTextInNode = function(node) { |
| 459 var newRange = document.createRange(); |
| 460 newRange.setStart(node, 0); |
| 461 newRange.setEndAfter(node); |
| 462 var sel = window.getSelection(); |
| 463 sel.removeAllRanges(); |
| 464 sel.addRange(newRange); |
| 465 }; |
| 466 |
| 467 /** |
| 468 * Retrieves all the text within a selection. |
| 469 * |
| 470 * Note that this can be different than simply using the string from |
| 471 * window.getSelection() as this will account for IMG nodes, etc. |
| 472 * |
| 473 * @return {string} The string of text contained in the current selection. |
| 474 */ |
| 475 cvox.SelectionUtil.getText = function() { |
| 476 var text = ''; |
| 477 var sel = window.getSelection(); |
| 478 if (cvox.SelectionUtil.hasContentWithTag(sel, 'IMG')) { |
| 479 var docFrag = sel.getRangeAt(0).cloneContents(); |
| 480 var span = document.createElement('span'); |
| 481 span.appendChild(docFrag); |
| 482 var leafNodes = cvox.XpathUtil.getLeafNodes(span); |
| 483 for (var i = 0, node; node = leafNodes[i]; i++) { |
| 484 text = text + ' ' + cvox.DomUtil.getText(node); |
| 485 } |
| 486 } else { |
| 487 text = text + sel; |
| 488 } |
| 489 return text; |
| 490 }; |
| OLD | NEW |