[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 /** 2 * Text range module for Rangy. 3 * Text-based manipulation and searching of ranges and selections. 4 * 5 * Features 6 * 7 * - Ability to move range boundaries by character or word offsets 8 * - Customizable word tokenizer 9 * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties 10 * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case 11 * sensitivity 12 * - Selection and range save/restore as text offsets within a node 13 * - Methods to return visible text within a range or selection 14 * - innerText method for elements 15 * 16 * References 17 * 18 * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145 19 * http://aryeh.name/spec/innertext/innertext.html 20 * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html 21 * 22 * Part of Rangy, a cross-browser JavaScript range and selection library 23 * https://github.com/timdown/rangy 24 * 25 * Depends on Rangy core. 26 * 27 * Copyright 2015, Tim Down 28 * Licensed under the MIT license. 29 * Version: 1.3.0 30 * Build date: 10 May 2015 31 */ 32 33 /** 34 * Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers. 35 * 36 * First, a <br>: this is relatively simple. For the following HTML: 37 * 38 * 1 <br>2 39 * 40 * - IE and WebKit render the space, include it in the selection (i.e. when the content is selected and pasted into a 41 * textarea, the space is present) and allow the caret to be placed after it. 42 * - Firefox does not acknowledge the space in the selection but it is possible to place the caret after it. 43 * - Opera does not render the space but has two separate caret positions on either side of the space (left and right 44 * arrow keys show this) and includes the space in the selection. 45 * 46 * The other case is the line break or breaks implied by block elements. For the following HTML: 47 * 48 * <p>1 </p><p>2<p> 49 * 50 * - WebKit does not acknowledge the space in any way 51 * - Firefox, IE and Opera as per <br> 52 * 53 * One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML: 54 * 55 * <p style="white-space: pre-line">1 56 * 2</p> 57 * 58 * - Firefox and WebKit include the space in caret positions 59 * - IE does not support pre-line up to and including version 9 60 * - Opera ignores the space 61 * - Trailing space only renders if there is a non-collapsed character in the line 62 * 63 * Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be 64 * feature-tested 65 */ 66 (function(factory, root) { 67 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 68 factory(root.rangy); 69 })(function(rangy) { 70 rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) { 71 var UNDEF = "undefined"; 72 var CHARACTER = "character", WORD = "word"; 73 var dom = api.dom, util = api.util; 74 var extend = util.extend; 75 var createOptions = util.createOptions; 76 var getBody = dom.getBody; 77 78 79 var spacesRegex = /^[ \t\f\r\n]+$/; 80 var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/; 81 var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/; 82 var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/; 83 var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/; 84 85 var defaultLanguage = "en"; 86 87 var isDirectionBackward = api.Selection.isDirectionBackward; 88 89 // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit, 90 // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed. 91 var trailingSpaceInBlockCollapses = false; 92 var trailingSpaceBeforeBrCollapses = false; 93 var trailingSpaceBeforeBlockCollapses = false; 94 var trailingSpaceBeforeLineBreakInPreLineCollapses = true; 95 96 (function() { 97 var el = dom.createTestElement(document, "<p>1 </p><p></p>", true); 98 var p = el.firstChild; 99 var sel = api.getSelection(); 100 sel.collapse(p.lastChild, 2); 101 sel.setStart(p.firstChild, 0); 102 trailingSpaceInBlockCollapses = ("" + sel).length == 1; 103 104 el.innerHTML = "1 <br />"; 105 sel.collapse(el, 2); 106 sel.setStart(el.firstChild, 0); 107 trailingSpaceBeforeBrCollapses = ("" + sel).length == 1; 108 109 el.innerHTML = "1 <p>1</p>"; 110 sel.collapse(el, 2); 111 sel.setStart(el.firstChild, 0); 112 trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1; 113 114 dom.removeNode(el); 115 sel.removeAllRanges(); 116 })(); 117 118 /*----------------------------------------------------------------------------------------------------------------*/ 119 120 // This function must create word and non-word tokens for the whole of the text supplied to it 121 function defaultTokenizer(chars, wordOptions) { 122 var word = chars.join(""), result, tokenRanges = []; 123 124 function createTokenRange(start, end, isWord) { 125 tokenRanges.push( { start: start, end: end, isWord: isWord } ); 126 } 127 128 // Match words and mark characters 129 var lastWordEnd = 0, wordStart, wordEnd; 130 while ( (result = wordOptions.wordRegex.exec(word)) ) { 131 wordStart = result.index; 132 wordEnd = wordStart + result[0].length; 133 134 // Create token for non-word characters preceding this word 135 if (wordStart > lastWordEnd) { 136 createTokenRange(lastWordEnd, wordStart, false); 137 } 138 139 // Get trailing space characters for word 140 if (wordOptions.includeTrailingSpace) { 141 while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) { 142 ++wordEnd; 143 } 144 } 145 createTokenRange(wordStart, wordEnd, true); 146 lastWordEnd = wordEnd; 147 } 148 149 // Create token for trailing non-word characters, if any exist 150 if (lastWordEnd < chars.length) { 151 createTokenRange(lastWordEnd, chars.length, false); 152 } 153 154 return tokenRanges; 155 } 156 157 function convertCharRangeToToken(chars, tokenRange) { 158 var tokenChars = chars.slice(tokenRange.start, tokenRange.end); 159 var token = { 160 isWord: tokenRange.isWord, 161 chars: tokenChars, 162 toString: function() { 163 return tokenChars.join(""); 164 } 165 }; 166 for (var i = 0, len = tokenChars.length; i < len; ++i) { 167 tokenChars[i].token = token; 168 } 169 return token; 170 } 171 172 function tokenize(chars, wordOptions, tokenizer) { 173 var tokenRanges = tokenizer(chars, wordOptions); 174 var tokens = []; 175 for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) { 176 tokens.push( convertCharRangeToToken(chars, tokenRange) ); 177 } 178 return tokens; 179 } 180 181 var defaultCharacterOptions = { 182 includeBlockContentTrailingSpace: true, 183 includeSpaceBeforeBr: true, 184 includeSpaceBeforeBlock: true, 185 includePreLineTrailingSpace: true, 186 ignoreCharacters: "" 187 }; 188 189 function normalizeIgnoredCharacters(ignoredCharacters) { 190 // Check if character is ignored 191 var ignoredChars = ignoredCharacters || ""; 192 193 // Normalize ignored characters into a string consisting of characters in ascending order of character code 194 var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars; 195 ignoredCharsArray.sort(function(char1, char2) { 196 return char1.charCodeAt(0) - char2.charCodeAt(0); 197 }); 198 199 /// Convert back to a string and remove duplicates 200 return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1"); 201 } 202 203 var defaultCaretCharacterOptions = { 204 includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses, 205 includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses, 206 includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses, 207 includePreLineTrailingSpace: true 208 }; 209 210 var defaultWordOptions = { 211 "en": { 212 wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi, 213 includeTrailingSpace: false, 214 tokenizer: defaultTokenizer 215 } 216 }; 217 218 var defaultFindOptions = { 219 caseSensitive: false, 220 withinRange: null, 221 wholeWordsOnly: false, 222 wrap: false, 223 direction: "forward", 224 wordOptions: null, 225 characterOptions: null 226 }; 227 228 var defaultMoveOptions = { 229 wordOptions: null, 230 characterOptions: null 231 }; 232 233 var defaultExpandOptions = { 234 wordOptions: null, 235 characterOptions: null, 236 trim: false, 237 trimStart: true, 238 trimEnd: true 239 }; 240 241 var defaultWordIteratorOptions = { 242 wordOptions: null, 243 characterOptions: null, 244 direction: "forward" 245 }; 246 247 function createWordOptions(options) { 248 var lang, defaults; 249 if (!options) { 250 return defaultWordOptions[defaultLanguage]; 251 } else { 252 lang = options.language || defaultLanguage; 253 defaults = {}; 254 extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]); 255 extend(defaults, options); 256 return defaults; 257 } 258 } 259 260 function createNestedOptions(optionsParam, defaults) { 261 var options = createOptions(optionsParam, defaults); 262 if (defaults.hasOwnProperty("wordOptions")) { 263 options.wordOptions = createWordOptions(options.wordOptions); 264 } 265 if (defaults.hasOwnProperty("characterOptions")) { 266 options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions); 267 } 268 return options; 269 } 270 271 /*----------------------------------------------------------------------------------------------------------------*/ 272 273 /* DOM utility functions */ 274 var getComputedStyleProperty = dom.getComputedStyleProperty; 275 276 // Create cachable versions of DOM functions 277 278 // Test for old IE's incorrect display properties 279 var tableCssDisplayBlock; 280 (function() { 281 var table = document.createElement("table"); 282 var body = getBody(document); 283 body.appendChild(table); 284 tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block"); 285 body.removeChild(table); 286 })(); 287 288 var defaultDisplayValueForTag = { 289 table: "table", 290 caption: "table-caption", 291 colgroup: "table-column-group", 292 col: "table-column", 293 thead: "table-header-group", 294 tbody: "table-row-group", 295 tfoot: "table-footer-group", 296 tr: "table-row", 297 td: "table-cell", 298 th: "table-cell" 299 }; 300 301 // Corrects IE's "block" value for table-related elements 302 function getComputedDisplay(el, win) { 303 var display = getComputedStyleProperty(el, "display", win); 304 var tagName = el.tagName.toLowerCase(); 305 return (display == "block" && 306 tableCssDisplayBlock && 307 defaultDisplayValueForTag.hasOwnProperty(tagName)) ? 308 defaultDisplayValueForTag[tagName] : display; 309 } 310 311 function isHidden(node) { 312 var ancestors = getAncestorsAndSelf(node); 313 for (var i = 0, len = ancestors.length; i < len; ++i) { 314 if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") { 315 return true; 316 } 317 } 318 319 return false; 320 } 321 322 function isVisibilityHiddenTextNode(textNode) { 323 var el; 324 return textNode.nodeType == 3 && 325 (el = textNode.parentNode) && 326 getComputedStyleProperty(el, "visibility") == "hidden"; 327 } 328 329 /*----------------------------------------------------------------------------------------------------------------*/ 330 331 332 // "A block node is either an Element whose "display" property does not have 333 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a 334 // Document, or a DocumentFragment." 335 function isBlockNode(node) { 336 return node && 337 ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) || 338 node.nodeType == 9 || node.nodeType == 11); 339 } 340 341 function getLastDescendantOrSelf(node) { 342 var lastChild = node.lastChild; 343 return lastChild ? getLastDescendantOrSelf(lastChild) : node; 344 } 345 346 function containsPositions(node) { 347 return dom.isCharacterDataNode(node) || 348 !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName); 349 } 350 351 function getAncestors(node) { 352 var ancestors = []; 353 while (node.parentNode) { 354 ancestors.unshift(node.parentNode); 355 node = node.parentNode; 356 } 357 return ancestors; 358 } 359 360 function getAncestorsAndSelf(node) { 361 return getAncestors(node).concat([node]); 362 } 363 364 function nextNodeDescendants(node) { 365 while (node && !node.nextSibling) { 366 node = node.parentNode; 367 } 368 if (!node) { 369 return null; 370 } 371 return node.nextSibling; 372 } 373 374 function nextNode(node, excludeChildren) { 375 if (!excludeChildren && node.hasChildNodes()) { 376 return node.firstChild; 377 } 378 return nextNodeDescendants(node); 379 } 380 381 function previousNode(node) { 382 var previous = node.previousSibling; 383 if (previous) { 384 node = previous; 385 while (node.hasChildNodes()) { 386 node = node.lastChild; 387 } 388 return node; 389 } 390 var parent = node.parentNode; 391 if (parent && parent.nodeType == 1) { 392 return parent; 393 } 394 return null; 395 } 396 397 // Adpated from Aryeh's code. 398 // "A whitespace node is either a Text node whose data is the empty string; or 399 // a Text node whose data consists only of one or more tabs (0x0009), line 400 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose 401 // parent is an Element whose resolved value for "white-space" is "normal" or 402 // "nowrap"; or a Text node whose data consists only of one or more tabs 403 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose 404 // parent is an Element whose resolved value for "white-space" is "pre-line"." 405 function isWhitespaceNode(node) { 406 if (!node || node.nodeType != 3) { 407 return false; 408 } 409 var text = node.data; 410 if (text === "") { 411 return true; 412 } 413 var parent = node.parentNode; 414 if (!parent || parent.nodeType != 1) { 415 return false; 416 } 417 var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace"); 418 419 return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) || 420 (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line"); 421 } 422 423 // Adpated from Aryeh's code. 424 // "node is a collapsed whitespace node if the following algorithm returns 425 // true:" 426 function isCollapsedWhitespaceNode(node) { 427 // "If node's data is the empty string, return true." 428 if (node.data === "") { 429 return true; 430 } 431 432 // "If node is not a whitespace node, return false." 433 if (!isWhitespaceNode(node)) { 434 return false; 435 } 436 437 // "Let ancestor be node's parent." 438 var ancestor = node.parentNode; 439 440 // "If ancestor is null, return true." 441 if (!ancestor) { 442 return true; 443 } 444 445 // "If the "display" property of some ancestor of node has resolved value "none", return true." 446 if (isHidden(node)) { 447 return true; 448 } 449 450 return false; 451 } 452 453 function isCollapsedNode(node) { 454 var type = node.nodeType; 455 return type == 7 /* PROCESSING_INSTRUCTION */ || 456 type == 8 /* COMMENT */ || 457 isHidden(node) || 458 /^(script|style)$/i.test(node.nodeName) || 459 isVisibilityHiddenTextNode(node) || 460 isCollapsedWhitespaceNode(node); 461 } 462 463 function isIgnoredNode(node, win) { 464 var type = node.nodeType; 465 return type == 7 /* PROCESSING_INSTRUCTION */ || 466 type == 8 /* COMMENT */ || 467 (type == 1 && getComputedDisplay(node, win) == "none"); 468 } 469 470 /*----------------------------------------------------------------------------------------------------------------*/ 471 472 // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down 473 474 function Cache() { 475 this.store = {}; 476 } 477 478 Cache.prototype = { 479 get: function(key) { 480 return this.store.hasOwnProperty(key) ? this.store[key] : null; 481 }, 482 483 set: function(key, value) { 484 return this.store[key] = value; 485 } 486 }; 487 488 var cachedCount = 0, uncachedCount = 0; 489 490 function createCachingGetter(methodName, func, objProperty) { 491 return function(args) { 492 var cache = this.cache; 493 if (cache.hasOwnProperty(methodName)) { 494 cachedCount++; 495 return cache[methodName]; 496 } else { 497 uncachedCount++; 498 var value = func.call(this, objProperty ? this[objProperty] : this, args); 499 cache[methodName] = value; 500 return value; 501 } 502 }; 503 } 504 505 /*----------------------------------------------------------------------------------------------------------------*/ 506 507 function NodeWrapper(node, session) { 508 this.node = node; 509 this.session = session; 510 this.cache = new Cache(); 511 this.positions = new Cache(); 512 } 513 514 var nodeProto = { 515 getPosition: function(offset) { 516 var positions = this.positions; 517 return positions.get(offset) || positions.set(offset, new Position(this, offset)); 518 }, 519 520 toString: function() { 521 return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]"; 522 } 523 }; 524 525 NodeWrapper.prototype = nodeProto; 526 527 var EMPTY = "EMPTY", 528 NON_SPACE = "NON_SPACE", 529 UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE", 530 COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE", 531 TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK", 532 TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK", 533 TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR", 534 PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK", 535 TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR", 536 INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR"; 537 538 extend(nodeProto, { 539 isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"), 540 getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"), 541 getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"), 542 containsPositions: createCachingGetter("containsPositions", containsPositions, "node"), 543 isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"), 544 isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"), 545 getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"), 546 isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"), 547 isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"), 548 next: createCachingGetter("nextPos", nextNode, "node"), 549 previous: createCachingGetter("previous", previousNode, "node"), 550 551 getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) { 552 var spaceRegex = null, collapseSpaces = false; 553 var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace"); 554 var preLine = (cssWhitespace == "pre-line"); 555 if (preLine) { 556 spaceRegex = spacesMinusLineBreaksRegex; 557 collapseSpaces = true; 558 } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") { 559 spaceRegex = spacesRegex; 560 collapseSpaces = true; 561 } 562 563 return { 564 node: textNode, 565 text: textNode.data, 566 spaceRegex: spaceRegex, 567 collapseSpaces: collapseSpaces, 568 preLine: preLine 569 }; 570 }, "node"), 571 572 hasInnerText: createCachingGetter("hasInnerText", function(el, backward) { 573 var session = this.session; 574 var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1); 575 var firstPosInEl = session.getPosition(el, 0); 576 577 var pos = backward ? posAfterEl : firstPosInEl; 578 var endPos = backward ? firstPosInEl : posAfterEl; 579 580 /* 581 <body><p>X </p><p>Y</p></body> 582 583 Positions: 584 585 body:0:"" 586 p:0:"" 587 text:0:"" 588 text:1:"X" 589 text:2:TRAILING_SPACE_IN_BLOCK 590 text:3:COLLAPSED_SPACE 591 p:1:"" 592 body:1:"\n" 593 p:0:"" 594 text:0:"" 595 text:1:"Y" 596 597 A character is a TRAILING_SPACE_IN_BLOCK iff: 598 599 - There is no uncollapsed character after it within the visible containing block element 600 601 A character is a TRAILING_SPACE_BEFORE_BR iff: 602 603 - There is no uncollapsed character after it preceding a <br> element 604 605 An element has inner text iff 606 607 - It is not hidden 608 - It contains an uncollapsed character 609 610 All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render. 611 */ 612 613 while (pos !== endPos) { 614 pos.prepopulateChar(); 615 if (pos.isDefinitelyNonEmpty()) { 616 return true; 617 } 618 pos = backward ? pos.previousVisible() : pos.nextVisible(); 619 } 620 621 return false; 622 }, "node"), 623 624 isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) { 625 // Ensure that a block element containing a <br> is considered to have inner text 626 var brs = el.getElementsByTagName("br"); 627 for (var i = 0, len = brs.length; i < len; ++i) { 628 if (!isCollapsedNode(brs[i])) { 629 return true; 630 } 631 } 632 return this.hasInnerText(); 633 }, "node"), 634 635 getTrailingSpace: createCachingGetter("trailingSpace", function(el) { 636 if (el.tagName.toLowerCase() == "br") { 637 return ""; 638 } else { 639 switch (this.getComputedDisplay()) { 640 case "inline": 641 var child = el.lastChild; 642 while (child) { 643 if (!isIgnoredNode(child)) { 644 return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : ""; 645 } 646 child = child.previousSibling; 647 } 648 break; 649 case "inline-block": 650 case "inline-table": 651 case "none": 652 case "table-column": 653 case "table-column-group": 654 break; 655 case "table-cell": 656 return "\t"; 657 default: 658 return this.isRenderedBlock(true) ? "\n" : ""; 659 } 660 } 661 return ""; 662 }, "node"), 663 664 getLeadingSpace: createCachingGetter("leadingSpace", function(el) { 665 switch (this.getComputedDisplay()) { 666 case "inline": 667 case "inline-block": 668 case "inline-table": 669 case "none": 670 case "table-column": 671 case "table-column-group": 672 case "table-cell": 673 break; 674 default: 675 return this.isRenderedBlock(false) ? "\n" : ""; 676 } 677 return ""; 678 }, "node") 679 }); 680 681 /*----------------------------------------------------------------------------------------------------------------*/ 682 683 function Position(nodeWrapper, offset) { 684 this.offset = offset; 685 this.nodeWrapper = nodeWrapper; 686 this.node = nodeWrapper.node; 687 this.session = nodeWrapper.session; 688 this.cache = new Cache(); 689 } 690 691 function inspectPosition() { 692 return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]"; 693 } 694 695 var positionProto = { 696 character: "", 697 characterType: EMPTY, 698 isBr: false, 699 700 /* 701 This method: 702 - Fully populates positions that have characters that can be determined independently of any other characters. 703 - Populates most types of space positions with a provisional character. The character is finalized later. 704 */ 705 prepopulateChar: function() { 706 var pos = this; 707 if (!pos.prepopulatedChar) { 708 var node = pos.node, offset = pos.offset; 709 var visibleChar = "", charType = EMPTY; 710 var finalizedChar = false; 711 if (offset > 0) { 712 if (node.nodeType == 3) { 713 var text = node.data; 714 var textChar = text.charAt(offset - 1); 715 716 var nodeInfo = pos.nodeWrapper.getTextNodeInfo(); 717 var spaceRegex = nodeInfo.spaceRegex; 718 if (nodeInfo.collapseSpaces) { 719 if (spaceRegex.test(textChar)) { 720 // "If the character at position is from set, append a single space (U+0020) to newdata and advance 721 // position until the character at position is not from set." 722 723 // We also need to check for the case where we're in a pre-line and we have a space preceding a 724 // line break, because such spaces are collapsed in some browsers 725 if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) { 726 } else if (nodeInfo.preLine && text.charAt(offset) === "\n") { 727 visibleChar = " "; 728 charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK; 729 } else { 730 visibleChar = " "; 731 //pos.checkForFollowingLineBreak = true; 732 charType = COLLAPSIBLE_SPACE; 733 } 734 } else { 735 visibleChar = textChar; 736 charType = NON_SPACE; 737 finalizedChar = true; 738 } 739 } else { 740 visibleChar = textChar; 741 charType = UNCOLLAPSIBLE_SPACE; 742 finalizedChar = true; 743 } 744 } else { 745 var nodePassed = node.childNodes[offset - 1]; 746 if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) { 747 if (nodePassed.tagName.toLowerCase() == "br") { 748 visibleChar = "\n"; 749 pos.isBr = true; 750 charType = COLLAPSIBLE_SPACE; 751 finalizedChar = false; 752 } else { 753 pos.checkForTrailingSpace = true; 754 } 755 } 756 757 // Check the leading space of the next node for the case when a block element follows an inline 758 // element or text node. In that case, there is an implied line break between the two nodes. 759 if (!visibleChar) { 760 var nextNode = node.childNodes[offset]; 761 if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) { 762 pos.checkForLeadingSpace = true; 763 } 764 } 765 } 766 } 767 768 pos.prepopulatedChar = true; 769 pos.character = visibleChar; 770 pos.characterType = charType; 771 pos.isCharInvariant = finalizedChar; 772 } 773 }, 774 775 isDefinitelyNonEmpty: function() { 776 var charType = this.characterType; 777 return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE; 778 }, 779 780 // Resolve leading and trailing spaces, which may involve prepopulating other positions 781 resolveLeadingAndTrailingSpaces: function() { 782 if (!this.prepopulatedChar) { 783 this.prepopulateChar(); 784 } 785 if (this.checkForTrailingSpace) { 786 var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace(); 787 if (trailingSpace) { 788 this.isTrailingSpace = true; 789 this.character = trailingSpace; 790 this.characterType = COLLAPSIBLE_SPACE; 791 } 792 this.checkForTrailingSpace = false; 793 } 794 if (this.checkForLeadingSpace) { 795 var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace(); 796 if (leadingSpace) { 797 this.isLeadingSpace = true; 798 this.character = leadingSpace; 799 this.characterType = COLLAPSIBLE_SPACE; 800 } 801 this.checkForLeadingSpace = false; 802 } 803 }, 804 805 getPrecedingUncollapsedPosition: function(characterOptions) { 806 var pos = this, character; 807 while ( (pos = pos.previousVisible()) ) { 808 character = pos.getCharacter(characterOptions); 809 if (character !== "") { 810 return pos; 811 } 812 } 813 814 return null; 815 }, 816 817 getCharacter: function(characterOptions) { 818 this.resolveLeadingAndTrailingSpaces(); 819 820 var thisChar = this.character, returnChar; 821 822 // Check if character is ignored 823 var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters); 824 var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1); 825 826 // Check if this position's character is invariant (i.e. not dependent on character options) and return it 827 // if so 828 if (this.isCharInvariant) { 829 returnChar = isIgnoredCharacter ? "" : thisChar; 830 return returnChar; 831 } 832 833 var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_"); 834 var cachedChar = this.cache.get(cacheKey); 835 if (cachedChar !== null) { 836 return cachedChar; 837 } 838 839 // We need to actually get the character now 840 var character = ""; 841 var collapsible = (this.characterType == COLLAPSIBLE_SPACE); 842 843 var nextPos, previousPos; 844 var gotPreviousPos = false; 845 var pos = this; 846 847 function getPreviousPos() { 848 if (!gotPreviousPos) { 849 previousPos = pos.getPrecedingUncollapsedPosition(characterOptions); 850 gotPreviousPos = true; 851 } 852 return previousPos; 853 } 854 855 // Disallow a collapsible space that is followed by a line break or is the last character 856 if (collapsible) { 857 // Allow a trailing space that we've previously determined should be included 858 if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) { 859 character = "\n"; 860 } 861 // Disallow a collapsible space that follows a trailing space or line break, or is the first character, 862 // or follows a collapsible included space 863 else if (thisChar == " " && 864 (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) { 865 } 866 // Allow a leading line break unless it follows a line break 867 else if (thisChar == "\n" && this.isLeadingSpace) { 868 if (getPreviousPos() && previousPos.character != "\n") { 869 character = "\n"; 870 } else { 871 } 872 } else { 873 nextPos = this.nextUncollapsed(); 874 if (nextPos) { 875 if (nextPos.isBr) { 876 this.type = TRAILING_SPACE_BEFORE_BR; 877 } else if (nextPos.isTrailingSpace && nextPos.character == "\n") { 878 this.type = TRAILING_SPACE_IN_BLOCK; 879 } else if (nextPos.isLeadingSpace && nextPos.character == "\n") { 880 this.type = TRAILING_SPACE_BEFORE_BLOCK; 881 } 882 883 if (nextPos.character == "\n") { 884 if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) { 885 } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) { 886 } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) { 887 } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) { 888 } else if (thisChar == "\n") { 889 if (nextPos.isTrailingSpace) { 890 if (this.isTrailingSpace) { 891 } else if (this.isBr) { 892 nextPos.type = TRAILING_LINE_BREAK_AFTER_BR; 893 894 if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") { 895 nextPos.character = ""; 896 } else { 897 nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR; 898 } 899 } 900 } else { 901 character = "\n"; 902 } 903 } else if (thisChar == " ") { 904 character = " "; 905 } else { 906 } 907 } else { 908 character = thisChar; 909 } 910 } else { 911 } 912 } 913 } 914 915 if (ignoredChars.indexOf(character) > -1) { 916 character = ""; 917 } 918 919 920 this.cache.set(cacheKey, character); 921 922 return character; 923 }, 924 925 equals: function(pos) { 926 return !!pos && this.node === pos.node && this.offset === pos.offset; 927 }, 928 929 inspect: inspectPosition, 930 931 toString: function() { 932 return this.character; 933 } 934 }; 935 936 Position.prototype = positionProto; 937 938 extend(positionProto, { 939 next: createCachingGetter("nextPos", function(pos) { 940 var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session; 941 if (!node) { 942 return null; 943 } 944 var nextNode, nextOffset, child; 945 if (offset == nodeWrapper.getLength()) { 946 // Move onto the next node 947 nextNode = node.parentNode; 948 nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0; 949 } else { 950 if (nodeWrapper.isCharacterDataNode()) { 951 nextNode = node; 952 nextOffset = offset + 1; 953 } else { 954 child = node.childNodes[offset]; 955 // Go into the children next, if children there are 956 if (session.getNodeWrapper(child).containsPositions()) { 957 nextNode = child; 958 nextOffset = 0; 959 } else { 960 nextNode = node; 961 nextOffset = offset + 1; 962 } 963 } 964 } 965 966 return nextNode ? session.getPosition(nextNode, nextOffset) : null; 967 }), 968 969 previous: createCachingGetter("previous", function(pos) { 970 var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session; 971 var previousNode, previousOffset, child; 972 if (offset == 0) { 973 previousNode = node.parentNode; 974 previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0; 975 } else { 976 if (nodeWrapper.isCharacterDataNode()) { 977 previousNode = node; 978 previousOffset = offset - 1; 979 } else { 980 child = node.childNodes[offset - 1]; 981 // Go into the children next, if children there are 982 if (session.getNodeWrapper(child).containsPositions()) { 983 previousNode = child; 984 previousOffset = dom.getNodeLength(child); 985 } else { 986 previousNode = node; 987 previousOffset = offset - 1; 988 } 989 } 990 } 991 return previousNode ? session.getPosition(previousNode, previousOffset) : null; 992 }), 993 994 /* 995 Next and previous position moving functions that filter out 996 997 - Hidden (CSS visibility/display) elements 998 - Script and style elements 999 */ 1000 nextVisible: createCachingGetter("nextVisible", function(pos) { 1001 var next = pos.next(); 1002 if (!next) { 1003 return null; 1004 } 1005 var nodeWrapper = next.nodeWrapper, node = next.node; 1006 var newPos = next; 1007 if (nodeWrapper.isCollapsed()) { 1008 // We're skipping this node and all its descendants 1009 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1); 1010 } 1011 return newPos; 1012 }), 1013 1014 nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) { 1015 var nextPos = pos; 1016 while ( (nextPos = nextPos.nextVisible()) ) { 1017 nextPos.resolveLeadingAndTrailingSpaces(); 1018 if (nextPos.character !== "") { 1019 return nextPos; 1020 } 1021 } 1022 return null; 1023 }), 1024 1025 previousVisible: createCachingGetter("previousVisible", function(pos) { 1026 var previous = pos.previous(); 1027 if (!previous) { 1028 return null; 1029 } 1030 var nodeWrapper = previous.nodeWrapper, node = previous.node; 1031 var newPos = previous; 1032 if (nodeWrapper.isCollapsed()) { 1033 // We're skipping this node and all its descendants 1034 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex()); 1035 } 1036 return newPos; 1037 }) 1038 }); 1039 1040 /*----------------------------------------------------------------------------------------------------------------*/ 1041 1042 var currentSession = null; 1043 1044 var Session = (function() { 1045 function createWrapperCache(nodeProperty) { 1046 var cache = new Cache(); 1047 1048 return { 1049 get: function(node) { 1050 var wrappersByProperty = cache.get(node[nodeProperty]); 1051 if (wrappersByProperty) { 1052 for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) { 1053 if (wrapper.node === node) { 1054 return wrapper; 1055 } 1056 } 1057 } 1058 return null; 1059 }, 1060 1061 set: function(nodeWrapper) { 1062 var property = nodeWrapper.node[nodeProperty]; 1063 var wrappersByProperty = cache.get(property) || cache.set(property, []); 1064 wrappersByProperty.push(nodeWrapper); 1065 } 1066 }; 1067 } 1068 1069 var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID"); 1070 1071 function Session() { 1072 this.initCaches(); 1073 } 1074 1075 Session.prototype = { 1076 initCaches: function() { 1077 this.elementCache = uniqueIDSupported ? (function() { 1078 var elementsCache = new Cache(); 1079 1080 return { 1081 get: function(el) { 1082 return elementsCache.get(el.uniqueID); 1083 }, 1084 1085 set: function(elWrapper) { 1086 elementsCache.set(elWrapper.node.uniqueID, elWrapper); 1087 } 1088 }; 1089 })() : createWrapperCache("tagName"); 1090 1091 // Store text nodes keyed by data, although we may need to truncate this 1092 this.textNodeCache = createWrapperCache("data"); 1093 this.otherNodeCache = createWrapperCache("nodeName"); 1094 }, 1095 1096 getNodeWrapper: function(node) { 1097 var wrapperCache; 1098 switch (node.nodeType) { 1099 case 1: 1100 wrapperCache = this.elementCache; 1101 break; 1102 case 3: 1103 wrapperCache = this.textNodeCache; 1104 break; 1105 default: 1106 wrapperCache = this.otherNodeCache; 1107 break; 1108 } 1109 1110 var wrapper = wrapperCache.get(node); 1111 if (!wrapper) { 1112 wrapper = new NodeWrapper(node, this); 1113 wrapperCache.set(wrapper); 1114 } 1115 return wrapper; 1116 }, 1117 1118 getPosition: function(node, offset) { 1119 return this.getNodeWrapper(node).getPosition(offset); 1120 }, 1121 1122 getRangeBoundaryPosition: function(range, isStart) { 1123 var prefix = isStart ? "start" : "end"; 1124 return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]); 1125 }, 1126 1127 detach: function() { 1128 this.elementCache = this.textNodeCache = this.otherNodeCache = null; 1129 } 1130 }; 1131 1132 return Session; 1133 })(); 1134 1135 /*----------------------------------------------------------------------------------------------------------------*/ 1136 1137 function startSession() { 1138 endSession(); 1139 return (currentSession = new Session()); 1140 } 1141 1142 function getSession() { 1143 return currentSession || startSession(); 1144 } 1145 1146 function endSession() { 1147 if (currentSession) { 1148 currentSession.detach(); 1149 } 1150 currentSession = null; 1151 } 1152 1153 /*----------------------------------------------------------------------------------------------------------------*/ 1154 1155 // Extensions to the rangy.dom utility object 1156 1157 extend(dom, { 1158 nextNode: nextNode, 1159 previousNode: previousNode 1160 }); 1161 1162 /*----------------------------------------------------------------------------------------------------------------*/ 1163 1164 function createCharacterIterator(startPos, backward, endPos, characterOptions) { 1165 1166 // Adjust the end position to ensure that it is actually reached 1167 if (endPos) { 1168 if (backward) { 1169 if (isCollapsedNode(endPos.node)) { 1170 endPos = startPos.previousVisible(); 1171 } 1172 } else { 1173 if (isCollapsedNode(endPos.node)) { 1174 endPos = endPos.nextVisible(); 1175 } 1176 } 1177 } 1178 1179 var pos = startPos, finished = false; 1180 1181 function next() { 1182 var charPos = null; 1183 if (backward) { 1184 charPos = pos; 1185 if (!finished) { 1186 pos = pos.previousVisible(); 1187 finished = !pos || (endPos && pos.equals(endPos)); 1188 } 1189 } else { 1190 if (!finished) { 1191 charPos = pos = pos.nextVisible(); 1192 finished = !pos || (endPos && pos.equals(endPos)); 1193 } 1194 } 1195 if (finished) { 1196 pos = null; 1197 } 1198 return charPos; 1199 } 1200 1201 var previousTextPos, returnPreviousTextPos = false; 1202 1203 return { 1204 next: function() { 1205 if (returnPreviousTextPos) { 1206 returnPreviousTextPos = false; 1207 return previousTextPos; 1208 } else { 1209 var pos, character; 1210 while ( (pos = next()) ) { 1211 character = pos.getCharacter(characterOptions); 1212 if (character) { 1213 previousTextPos = pos; 1214 return pos; 1215 } 1216 } 1217 return null; 1218 } 1219 }, 1220 1221 rewind: function() { 1222 if (previousTextPos) { 1223 returnPreviousTextPos = true; 1224 } else { 1225 throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound."); 1226 } 1227 }, 1228 1229 dispose: function() { 1230 startPos = endPos = null; 1231 } 1232 }; 1233 } 1234 1235 var arrayIndexOf = Array.prototype.indexOf ? 1236 function(arr, val) { 1237 return arr.indexOf(val); 1238 } : 1239 function(arr, val) { 1240 for (var i = 0, len = arr.length; i < len; ++i) { 1241 if (arr[i] === val) { 1242 return i; 1243 } 1244 } 1245 return -1; 1246 }; 1247 1248 // Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next() 1249 // is called and there is no more tokenized text 1250 function createTokenizedTextProvider(pos, characterOptions, wordOptions) { 1251 var forwardIterator = createCharacterIterator(pos, false, null, characterOptions); 1252 var backwardIterator = createCharacterIterator(pos, true, null, characterOptions); 1253 var tokenizer = wordOptions.tokenizer; 1254 1255 // Consumes a word and the whitespace beyond it 1256 function consumeWord(forward) { 1257 var pos, textChar; 1258 var newChars = [], it = forward ? forwardIterator : backwardIterator; 1259 1260 var passedWordBoundary = false, insideWord = false; 1261 1262 while ( (pos = it.next()) ) { 1263 textChar = pos.character; 1264 1265 1266 if (allWhiteSpaceRegex.test(textChar)) { 1267 if (insideWord) { 1268 insideWord = false; 1269 passedWordBoundary = true; 1270 } 1271 } else { 1272 if (passedWordBoundary) { 1273 it.rewind(); 1274 break; 1275 } else { 1276 insideWord = true; 1277 } 1278 } 1279 newChars.push(pos); 1280 } 1281 1282 1283 return newChars; 1284 } 1285 1286 // Get initial word surrounding initial position and tokenize it 1287 var forwardChars = consumeWord(true); 1288 var backwardChars = consumeWord(false).reverse(); 1289 var tokens = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer); 1290 1291 // Create initial token buffers 1292 var forwardTokensBuffer = forwardChars.length ? 1293 tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : []; 1294 1295 var backwardTokensBuffer = backwardChars.length ? 1296 tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : []; 1297 1298 function inspectBuffer(buffer) { 1299 var textPositions = ["[" + buffer.length + "]"]; 1300 for (var i = 0; i < buffer.length; ++i) { 1301 textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")"); 1302 } 1303 return textPositions; 1304 } 1305 1306 1307 return { 1308 nextEndToken: function() { 1309 var lastToken, forwardChars; 1310 1311 // If we're down to the last token, consume character chunks until we have a word or run out of 1312 // characters to consume 1313 while ( forwardTokensBuffer.length == 1 && 1314 !(lastToken = forwardTokensBuffer[0]).isWord && 1315 (forwardChars = consumeWord(true)).length > 0) { 1316 1317 // Merge trailing non-word into next word and tokenize 1318 forwardTokensBuffer = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer); 1319 } 1320 1321 return forwardTokensBuffer.shift(); 1322 }, 1323 1324 previousStartToken: function() { 1325 var lastToken, backwardChars; 1326 1327 // If we're down to the last token, consume character chunks until we have a word or run out of 1328 // characters to consume 1329 while ( backwardTokensBuffer.length == 1 && 1330 !(lastToken = backwardTokensBuffer[0]).isWord && 1331 (backwardChars = consumeWord(false)).length > 0) { 1332 1333 // Merge leading non-word into next word and tokenize 1334 backwardTokensBuffer = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer); 1335 } 1336 1337 return backwardTokensBuffer.pop(); 1338 }, 1339 1340 dispose: function() { 1341 forwardIterator.dispose(); 1342 backwardIterator.dispose(); 1343 forwardTokensBuffer = backwardTokensBuffer = null; 1344 } 1345 }; 1346 } 1347 1348 function movePositionBy(pos, unit, count, characterOptions, wordOptions) { 1349 var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token; 1350 if (count !== 0) { 1351 var backward = (count < 0); 1352 1353 switch (unit) { 1354 case CHARACTER: 1355 charIterator = createCharacterIterator(pos, backward, null, characterOptions); 1356 while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) { 1357 ++unitsMoved; 1358 newPos = currentPos; 1359 } 1360 nextPos = currentPos; 1361 charIterator.dispose(); 1362 break; 1363 case WORD: 1364 var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions); 1365 var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken; 1366 1367 while ( (token = next()) && unitsMoved < absCount ) { 1368 if (token.isWord) { 1369 ++unitsMoved; 1370 newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1]; 1371 } 1372 } 1373 break; 1374 default: 1375 throw new Error("movePositionBy: unit '" + unit + "' not implemented"); 1376 } 1377 1378 // Perform any necessary position tweaks 1379 if (backward) { 1380 newPos = newPos.previousVisible(); 1381 unitsMoved = -unitsMoved; 1382 } else if (newPos && newPos.isLeadingSpace && !newPos.isTrailingSpace) { 1383 // Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space 1384 // before a block element (for example, the line break between "1" and "2" in the following HTML: 1385 // "1<p>2</p>") is considered to be attached to the position immediately before the block element, which 1386 // corresponds with a different selection position in most browsers from the one we want (i.e. at the 1387 // start of the contents of the block element). We get round this by advancing the position returned to 1388 // the last possible equivalent visible position. 1389 if (unit == WORD) { 1390 charIterator = createCharacterIterator(pos, false, null, characterOptions); 1391 nextPos = charIterator.next(); 1392 charIterator.dispose(); 1393 } 1394 if (nextPos) { 1395 newPos = nextPos.previousVisible(); 1396 } 1397 } 1398 } 1399 1400 1401 return { 1402 position: newPos, 1403 unitsMoved: unitsMoved 1404 }; 1405 } 1406 1407 function createRangeCharacterIterator(session, range, characterOptions, backward) { 1408 var rangeStart = session.getRangeBoundaryPosition(range, true); 1409 var rangeEnd = session.getRangeBoundaryPosition(range, false); 1410 var itStart = backward ? rangeEnd : rangeStart; 1411 var itEnd = backward ? rangeStart : rangeEnd; 1412 1413 return createCharacterIterator(itStart, !!backward, itEnd, characterOptions); 1414 } 1415 1416 function getRangeCharacters(session, range, characterOptions) { 1417 1418 var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos; 1419 while ( (pos = it.next()) ) { 1420 chars.push(pos); 1421 } 1422 1423 it.dispose(); 1424 return chars; 1425 } 1426 1427 function isWholeWord(startPos, endPos, wordOptions) { 1428 var range = api.createRange(startPos.node); 1429 range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset); 1430 return !range.expand("word", { wordOptions: wordOptions }); 1431 } 1432 1433 function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) { 1434 var backward = isDirectionBackward(findOptions.direction); 1435 var it = createCharacterIterator( 1436 initialPos, 1437 backward, 1438 initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward), 1439 findOptions.characterOptions 1440 ); 1441 var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex; 1442 var result, insideRegexMatch; 1443 var returnValue = null; 1444 1445 function handleMatch(startIndex, endIndex) { 1446 var startPos = chars[startIndex].previousVisible(); 1447 var endPos = chars[endIndex - 1]; 1448 var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions)); 1449 1450 return { 1451 startPos: startPos, 1452 endPos: endPos, 1453 valid: valid 1454 }; 1455 } 1456 1457 while ( (pos = it.next()) ) { 1458 currentChar = pos.character; 1459 if (!isRegex && !findOptions.caseSensitive) { 1460 currentChar = currentChar.toLowerCase(); 1461 } 1462 1463 if (backward) { 1464 chars.unshift(pos); 1465 text = currentChar + text; 1466 } else { 1467 chars.push(pos); 1468 text += currentChar; 1469 } 1470 1471 if (isRegex) { 1472 result = searchTerm.exec(text); 1473 if (result) { 1474 matchStartIndex = result.index; 1475 matchEndIndex = matchStartIndex + result[0].length; 1476 if (insideRegexMatch) { 1477 // Check whether the match is now over 1478 if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) { 1479 returnValue = handleMatch(matchStartIndex, matchEndIndex); 1480 break; 1481 } 1482 } else { 1483 insideRegexMatch = true; 1484 } 1485 } 1486 } else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) { 1487 returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length); 1488 break; 1489 } 1490 } 1491 1492 // Check whether regex match extends to the end of the range 1493 if (insideRegexMatch) { 1494 returnValue = handleMatch(matchStartIndex, matchEndIndex); 1495 } 1496 it.dispose(); 1497 1498 return returnValue; 1499 } 1500 1501 function createEntryPointFunction(func) { 1502 return function() { 1503 var sessionRunning = !!currentSession; 1504 var session = getSession(); 1505 var args = [session].concat( util.toArray(arguments) ); 1506 var returnValue = func.apply(this, args); 1507 if (!sessionRunning) { 1508 endSession(); 1509 } 1510 return returnValue; 1511 }; 1512 } 1513 1514 /*----------------------------------------------------------------------------------------------------------------*/ 1515 1516 // Extensions to the Rangy Range object 1517 1518 function createRangeBoundaryMover(isStart, collapse) { 1519 /* 1520 Unit can be "character" or "word" 1521 Options: 1522 1523 - includeTrailingSpace 1524 - wordRegex 1525 - tokenizer 1526 - collapseSpaceBeforeLineBreak 1527 */ 1528 return createEntryPointFunction( 1529 function(session, unit, count, moveOptions) { 1530 if (typeof count == UNDEF) { 1531 count = unit; 1532 unit = CHARACTER; 1533 } 1534 moveOptions = createNestedOptions(moveOptions, defaultMoveOptions); 1535 1536 var boundaryIsStart = isStart; 1537 if (collapse) { 1538 boundaryIsStart = (count >= 0); 1539 this.collapse(!boundaryIsStart); 1540 } 1541 var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.wordOptions); 1542 var newPos = moveResult.position; 1543 this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset); 1544 return moveResult.unitsMoved; 1545 } 1546 ); 1547 } 1548 1549 function createRangeTrimmer(isStart) { 1550 return createEntryPointFunction( 1551 function(session, characterOptions) { 1552 characterOptions = createOptions(characterOptions, defaultCharacterOptions); 1553 var pos; 1554 var it = createRangeCharacterIterator(session, this, characterOptions, !isStart); 1555 var trimCharCount = 0; 1556 while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) { 1557 ++trimCharCount; 1558 } 1559 it.dispose(); 1560 var trimmed = (trimCharCount > 0); 1561 if (trimmed) { 1562 this[isStart ? "moveStart" : "moveEnd"]( 1563 "character", 1564 isStart ? trimCharCount : -trimCharCount, 1565 { characterOptions: characterOptions } 1566 ); 1567 } 1568 return trimmed; 1569 } 1570 ); 1571 } 1572 1573 extend(api.rangePrototype, { 1574 moveStart: createRangeBoundaryMover(true, false), 1575 1576 moveEnd: createRangeBoundaryMover(false, false), 1577 1578 move: createRangeBoundaryMover(true, true), 1579 1580 trimStart: createRangeTrimmer(true), 1581 1582 trimEnd: createRangeTrimmer(false), 1583 1584 trim: createEntryPointFunction( 1585 function(session, characterOptions) { 1586 var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions); 1587 return startTrimmed || endTrimmed; 1588 } 1589 ), 1590 1591 expand: createEntryPointFunction( 1592 function(session, unit, expandOptions) { 1593 var moved = false; 1594 expandOptions = createNestedOptions(expandOptions, defaultExpandOptions); 1595 var characterOptions = expandOptions.characterOptions; 1596 if (!unit) { 1597 unit = CHARACTER; 1598 } 1599 if (unit == WORD) { 1600 var wordOptions = expandOptions.wordOptions; 1601 var startPos = session.getRangeBoundaryPosition(this, true); 1602 var endPos = session.getRangeBoundaryPosition(this, false); 1603 1604 var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions); 1605 var startToken = startTokenizedTextProvider.nextEndToken(); 1606 var newStartPos = startToken.chars[0].previousVisible(); 1607 var endToken, newEndPos; 1608 1609 if (this.collapsed) { 1610 endToken = startToken; 1611 } else { 1612 var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions); 1613 endToken = endTokenizedTextProvider.previousStartToken(); 1614 } 1615 newEndPos = endToken.chars[endToken.chars.length - 1]; 1616 1617 if (!newStartPos.equals(startPos)) { 1618 this.setStart(newStartPos.node, newStartPos.offset); 1619 moved = true; 1620 } 1621 if (newEndPos && !newEndPos.equals(endPos)) { 1622 this.setEnd(newEndPos.node, newEndPos.offset); 1623 moved = true; 1624 } 1625 1626 if (expandOptions.trim) { 1627 if (expandOptions.trimStart) { 1628 moved = this.trimStart(characterOptions) || moved; 1629 } 1630 if (expandOptions.trimEnd) { 1631 moved = this.trimEnd(characterOptions) || moved; 1632 } 1633 } 1634 1635 return moved; 1636 } else { 1637 return this.moveEnd(CHARACTER, 1, expandOptions); 1638 } 1639 } 1640 ), 1641 1642 text: createEntryPointFunction( 1643 function(session, characterOptions) { 1644 return this.collapsed ? 1645 "" : getRangeCharacters(session, this, createOptions(characterOptions, defaultCharacterOptions)).join(""); 1646 } 1647 ), 1648 1649 selectCharacters: createEntryPointFunction( 1650 function(session, containerNode, startIndex, endIndex, characterOptions) { 1651 var moveOptions = { characterOptions: characterOptions }; 1652 if (!containerNode) { 1653 containerNode = getBody( this.getDocument() ); 1654 } 1655 this.selectNodeContents(containerNode); 1656 this.collapse(true); 1657 this.moveStart("character", startIndex, moveOptions); 1658 this.collapse(true); 1659 this.moveEnd("character", endIndex - startIndex, moveOptions); 1660 } 1661 ), 1662 1663 // Character indexes are relative to the start of node 1664 toCharacterRange: createEntryPointFunction( 1665 function(session, containerNode, characterOptions) { 1666 if (!containerNode) { 1667 containerNode = getBody( this.getDocument() ); 1668 } 1669 var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode); 1670 var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1); 1671 var rangeBetween = this.cloneRange(); 1672 var startIndex, endIndex; 1673 if (rangeStartsBeforeNode) { 1674 rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex); 1675 startIndex = -rangeBetween.text(characterOptions).length; 1676 } else { 1677 rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset); 1678 startIndex = rangeBetween.text(characterOptions).length; 1679 } 1680 endIndex = startIndex + this.text(characterOptions).length; 1681 1682 return { 1683 start: startIndex, 1684 end: endIndex 1685 }; 1686 } 1687 ), 1688 1689 findText: createEntryPointFunction( 1690 function(session, searchTermParam, findOptions) { 1691 // Set up options 1692 findOptions = createNestedOptions(findOptions, defaultFindOptions); 1693 1694 // Create word options if we're matching whole words only 1695 if (findOptions.wholeWordsOnly) { 1696 // We don't ever want trailing spaces for search results 1697 findOptions.wordOptions.includeTrailingSpace = false; 1698 } 1699 1700 var backward = isDirectionBackward(findOptions.direction); 1701 1702 // Create a range representing the search scope if none was provided 1703 var searchScopeRange = findOptions.withinRange; 1704 if (!searchScopeRange) { 1705 searchScopeRange = api.createRange(); 1706 searchScopeRange.selectNodeContents(this.getDocument()); 1707 } 1708 1709 // Examine and prepare the search term 1710 var searchTerm = searchTermParam, isRegex = false; 1711 if (typeof searchTerm == "string") { 1712 if (!findOptions.caseSensitive) { 1713 searchTerm = searchTerm.toLowerCase(); 1714 } 1715 } else { 1716 isRegex = true; 1717 } 1718 1719 var initialPos = session.getRangeBoundaryPosition(this, !backward); 1720 1721 // Adjust initial position if it lies outside the search scope 1722 var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset); 1723 1724 if (comparison === -1) { 1725 initialPos = session.getRangeBoundaryPosition(searchScopeRange, true); 1726 } else if (comparison === 1) { 1727 initialPos = session.getRangeBoundaryPosition(searchScopeRange, false); 1728 } 1729 1730 var pos = initialPos; 1731 var wrappedAround = false; 1732 1733 // Try to find a match and ignore invalid ones 1734 var findResult; 1735 while (true) { 1736 findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions); 1737 1738 if (findResult) { 1739 if (findResult.valid) { 1740 this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset); 1741 return true; 1742 } else { 1743 // We've found a match that is not a whole word, so we carry on searching from the point immediately 1744 // after the match 1745 pos = backward ? findResult.startPos : findResult.endPos; 1746 } 1747 } else if (findOptions.wrap && !wrappedAround) { 1748 // No result found but we're wrapping around and limiting the scope to the unsearched part of the range 1749 searchScopeRange = searchScopeRange.cloneRange(); 1750 pos = session.getRangeBoundaryPosition(searchScopeRange, !backward); 1751 searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward); 1752 wrappedAround = true; 1753 } else { 1754 // Nothing found and we can't wrap around, so we're done 1755 return false; 1756 } 1757 } 1758 } 1759 ), 1760 1761 pasteHtml: function(html) { 1762 this.deleteContents(); 1763 if (html) { 1764 var frag = this.createContextualFragment(html); 1765 var lastChild = frag.lastChild; 1766 this.insertNode(frag); 1767 this.collapseAfter(lastChild); 1768 } 1769 } 1770 }); 1771 1772 /*----------------------------------------------------------------------------------------------------------------*/ 1773 1774 // Extensions to the Rangy Selection object 1775 1776 function createSelectionTrimmer(methodName) { 1777 return createEntryPointFunction( 1778 function(session, characterOptions) { 1779 var trimmed = false; 1780 this.changeEachRange(function(range) { 1781 trimmed = range[methodName](characterOptions) || trimmed; 1782 }); 1783 return trimmed; 1784 } 1785 ); 1786 } 1787 1788 extend(api.selectionPrototype, { 1789 expand: createEntryPointFunction( 1790 function(session, unit, expandOptions) { 1791 this.changeEachRange(function(range) { 1792 range.expand(unit, expandOptions); 1793 }); 1794 } 1795 ), 1796 1797 move: createEntryPointFunction( 1798 function(session, unit, count, options) { 1799 var unitsMoved = 0; 1800 if (this.focusNode) { 1801 this.collapse(this.focusNode, this.focusOffset); 1802 var range = this.getRangeAt(0); 1803 if (!options) { 1804 options = {}; 1805 } 1806 options.characterOptions = createOptions(options.characterOptions, defaultCaretCharacterOptions); 1807 unitsMoved = range.move(unit, count, options); 1808 this.setSingleRange(range); 1809 } 1810 return unitsMoved; 1811 } 1812 ), 1813 1814 trimStart: createSelectionTrimmer("trimStart"), 1815 trimEnd: createSelectionTrimmer("trimEnd"), 1816 trim: createSelectionTrimmer("trim"), 1817 1818 selectCharacters: createEntryPointFunction( 1819 function(session, containerNode, startIndex, endIndex, direction, characterOptions) { 1820 var range = api.createRange(containerNode); 1821 range.selectCharacters(containerNode, startIndex, endIndex, characterOptions); 1822 this.setSingleRange(range, direction); 1823 } 1824 ), 1825 1826 saveCharacterRanges: createEntryPointFunction( 1827 function(session, containerNode, characterOptions) { 1828 var ranges = this.getAllRanges(), rangeCount = ranges.length; 1829 var rangeInfos = []; 1830 1831 var backward = rangeCount == 1 && this.isBackward(); 1832 1833 for (var i = 0, len = ranges.length; i < len; ++i) { 1834 rangeInfos[i] = { 1835 characterRange: ranges[i].toCharacterRange(containerNode, characterOptions), 1836 backward: backward, 1837 characterOptions: characterOptions 1838 }; 1839 } 1840 1841 return rangeInfos; 1842 } 1843 ), 1844 1845 restoreCharacterRanges: createEntryPointFunction( 1846 function(session, containerNode, saved) { 1847 this.removeAllRanges(); 1848 for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) { 1849 rangeInfo = saved[i]; 1850 characterRange = rangeInfo.characterRange; 1851 range = api.createRange(containerNode); 1852 range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions); 1853 this.addRange(range, rangeInfo.backward); 1854 } 1855 } 1856 ), 1857 1858 text: createEntryPointFunction( 1859 function(session, characterOptions) { 1860 var rangeTexts = []; 1861 for (var i = 0, len = this.rangeCount; i < len; ++i) { 1862 rangeTexts[i] = this.getRangeAt(i).text(characterOptions); 1863 } 1864 return rangeTexts.join(""); 1865 } 1866 ) 1867 }); 1868 1869 /*----------------------------------------------------------------------------------------------------------------*/ 1870 1871 // Extensions to the core rangy object 1872 1873 api.innerText = function(el, characterOptions) { 1874 var range = api.createRange(el); 1875 range.selectNodeContents(el); 1876 var text = range.text(characterOptions); 1877 return text; 1878 }; 1879 1880 api.createWordIterator = function(startNode, startOffset, iteratorOptions) { 1881 var session = getSession(); 1882 iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions); 1883 var startPos = session.getPosition(startNode, startOffset); 1884 var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.wordOptions); 1885 var backward = isDirectionBackward(iteratorOptions.direction); 1886 1887 return { 1888 next: function() { 1889 return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken(); 1890 }, 1891 1892 dispose: function() { 1893 tokenizedTextProvider.dispose(); 1894 this.next = function() {}; 1895 } 1896 }; 1897 }; 1898 1899 /*----------------------------------------------------------------------------------------------------------------*/ 1900 1901 api.noMutation = function(func) { 1902 var session = getSession(); 1903 func(session); 1904 endSession(); 1905 }; 1906 1907 api.noMutation.createEntryPointFunction = createEntryPointFunction; 1908 1909 api.textRange = { 1910 isBlockNode: isBlockNode, 1911 isCollapsedWhitespaceNode: isCollapsedWhitespaceNode, 1912 1913 createPosition: createEntryPointFunction( 1914 function(session, node, offset) { 1915 return session.getPosition(node, offset); 1916 } 1917 ) 1918 }; 1919 }); 1920 1921 return rangy; 1922 }, this);
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |