2 * Text range module for Rangy.
3 * Text-based manipulation and searching of ranges and selections.
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
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
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
22 * Part of Rangy, a cross-browser JavaScript range and selection library
23 * https://github.com/timdown/rangy
25 * Depends on Rangy core.
27 * Copyright 2015, Tim Down
28 * Licensed under the MIT license.
30 * Build date: 10 May 2015
34 * Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers.
36 * First, a <br>: this is relatively simple. For the following HTML:
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.
46 * The other case is the line break or breaks implied by block elements. For the following HTML:
50 * - WebKit does not acknowledge the space in any way
51 * - Firefox, IE and Opera as per <br>
53 * One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML:
55 * <p style="white-space: pre-line">1
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
63 * Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be
66 (function(factory, root) {
67 if (typeof define == "function" && define.amd) {
68 // AMD. Register as an anonymous module with a dependency on Rangy.
69 define(["./rangy-core"], factory);
70 } else if (typeof module != "undefined" && typeof exports == "object") {
71 // Node/CommonJS style
72 module.exports = factory( require("rangy") );
74 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
78 rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) {
79 var UNDEF = "undefined";
80 var CHARACTER = "character", WORD = "word";
81 var dom = api.dom, util = api.util;
82 var extend = util.extend;
83 var createOptions = util.createOptions;
84 var getBody = dom.getBody;
87 var spacesRegex = /^[ \t\f\r\n]+$/;
88 var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/;
89 var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/;
90 var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/;
91 var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/;
93 var defaultLanguage = "en";
95 var isDirectionBackward = api.Selection.isDirectionBackward;
97 // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit,
98 // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed.
99 var trailingSpaceInBlockCollapses = false;
100 var trailingSpaceBeforeBrCollapses = false;
101 var trailingSpaceBeforeBlockCollapses = false;
102 var trailingSpaceBeforeLineBreakInPreLineCollapses = true;
105 var el = dom.createTestElement(document, "<p>1 </p><p></p>", true);
106 var p = el.firstChild;
107 var sel = api.getSelection();
108 sel.collapse(p.lastChild, 2);
109 sel.setStart(p.firstChild, 0);
110 trailingSpaceInBlockCollapses = ("" + sel).length == 1;
112 el.innerHTML = "1 <br />";
114 sel.setStart(el.firstChild, 0);
115 trailingSpaceBeforeBrCollapses = ("" + sel).length == 1;
117 el.innerHTML = "1 <p>1</p>";
119 sel.setStart(el.firstChild, 0);
120 trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1;
123 sel.removeAllRanges();
126 /*----------------------------------------------------------------------------------------------------------------*/
128 // This function must create word and non-word tokens for the whole of the text supplied to it
129 function defaultTokenizer(chars, wordOptions) {
130 var word = chars.join(""), result, tokenRanges = [];
132 function createTokenRange(start, end, isWord) {
133 tokenRanges.push( { start: start, end: end, isWord: isWord } );
136 // Match words and mark characters
137 var lastWordEnd = 0, wordStart, wordEnd;
138 while ( (result = wordOptions.wordRegex.exec(word)) ) {
139 wordStart = result.index;
140 wordEnd = wordStart + result[0].length;
142 // Create token for non-word characters preceding this word
143 if (wordStart > lastWordEnd) {
144 createTokenRange(lastWordEnd, wordStart, false);
147 // Get trailing space characters for word
148 if (wordOptions.includeTrailingSpace) {
149 while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) {
153 createTokenRange(wordStart, wordEnd, true);
154 lastWordEnd = wordEnd;
157 // Create token for trailing non-word characters, if any exist
158 if (lastWordEnd < chars.length) {
159 createTokenRange(lastWordEnd, chars.length, false);
165 function convertCharRangeToToken(chars, tokenRange) {
166 var tokenChars = chars.slice(tokenRange.start, tokenRange.end);
168 isWord: tokenRange.isWord,
170 toString: function() {
171 return tokenChars.join("");
174 for (var i = 0, len = tokenChars.length; i < len; ++i) {
175 tokenChars[i].token = token;
180 function tokenize(chars, wordOptions, tokenizer) {
181 var tokenRanges = tokenizer(chars, wordOptions);
183 for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) {
184 tokens.push( convertCharRangeToToken(chars, tokenRange) );
189 var defaultCharacterOptions = {
190 includeBlockContentTrailingSpace: true,
191 includeSpaceBeforeBr: true,
192 includeSpaceBeforeBlock: true,
193 includePreLineTrailingSpace: true,
197 function normalizeIgnoredCharacters(ignoredCharacters) {
198 // Check if character is ignored
199 var ignoredChars = ignoredCharacters || "";
201 // Normalize ignored characters into a string consisting of characters in ascending order of character code
202 var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars;
203 ignoredCharsArray.sort(function(char1, char2) {
204 return char1.charCodeAt(0) - char2.charCodeAt(0);
207 /// Convert back to a string and remove duplicates
208 return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1");
211 var defaultCaretCharacterOptions = {
212 includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses,
213 includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses,
214 includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses,
215 includePreLineTrailingSpace: true
218 var defaultWordOptions = {
220 wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi,
221 includeTrailingSpace: false,
222 tokenizer: defaultTokenizer
226 var defaultFindOptions = {
227 caseSensitive: false,
229 wholeWordsOnly: false,
231 direction: "forward",
233 characterOptions: null
236 var defaultMoveOptions = {
238 characterOptions: null
241 var defaultExpandOptions = {
243 characterOptions: null,
249 var defaultWordIteratorOptions = {
251 characterOptions: null,
255 function createWordOptions(options) {
258 return defaultWordOptions[defaultLanguage];
260 lang = options.language || defaultLanguage;
262 extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]);
263 extend(defaults, options);
268 function createNestedOptions(optionsParam, defaults) {
269 var options = createOptions(optionsParam, defaults);
270 if (defaults.hasOwnProperty("wordOptions")) {
271 options.wordOptions = createWordOptions(options.wordOptions);
273 if (defaults.hasOwnProperty("characterOptions")) {
274 options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions);
279 /*----------------------------------------------------------------------------------------------------------------*/
281 /* DOM utility functions */
282 var getComputedStyleProperty = dom.getComputedStyleProperty;
284 // Create cachable versions of DOM functions
286 // Test for old IE's incorrect display properties
287 var tableCssDisplayBlock;
289 var table = document.createElement("table");
290 var body = getBody(document);
291 body.appendChild(table);
292 tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block");
293 body.removeChild(table);
296 var defaultDisplayValueForTag = {
298 caption: "table-caption",
299 colgroup: "table-column-group",
301 thead: "table-header-group",
302 tbody: "table-row-group",
303 tfoot: "table-footer-group",
309 // Corrects IE's "block" value for table-related elements
310 function getComputedDisplay(el, win) {
311 var display = getComputedStyleProperty(el, "display", win);
312 var tagName = el.tagName.toLowerCase();
313 return (display == "block" &&
314 tableCssDisplayBlock &&
315 defaultDisplayValueForTag.hasOwnProperty(tagName)) ?
316 defaultDisplayValueForTag[tagName] : display;
319 function isHidden(node) {
320 var ancestors = getAncestorsAndSelf(node);
321 for (var i = 0, len = ancestors.length; i < len; ++i) {
322 if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") {
330 function isVisibilityHiddenTextNode(textNode) {
332 return textNode.nodeType == 3 &&
333 (el = textNode.parentNode) &&
334 getComputedStyleProperty(el, "visibility") == "hidden";
337 /*----------------------------------------------------------------------------------------------------------------*/
340 // "A block node is either an Element whose "display" property does not have
341 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
342 // Document, or a DocumentFragment."
343 function isBlockNode(node) {
345 ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) ||
346 node.nodeType == 9 || node.nodeType == 11);
349 function getLastDescendantOrSelf(node) {
350 var lastChild = node.lastChild;
351 return lastChild ? getLastDescendantOrSelf(lastChild) : node;
354 function containsPositions(node) {
355 return dom.isCharacterDataNode(node) ||
356 !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName);
359 function getAncestors(node) {
361 while (node.parentNode) {
362 ancestors.unshift(node.parentNode);
363 node = node.parentNode;
368 function getAncestorsAndSelf(node) {
369 return getAncestors(node).concat([node]);
372 function nextNodeDescendants(node) {
373 while (node && !node.nextSibling) {
374 node = node.parentNode;
379 return node.nextSibling;
382 function nextNode(node, excludeChildren) {
383 if (!excludeChildren && node.hasChildNodes()) {
384 return node.firstChild;
386 return nextNodeDescendants(node);
389 function previousNode(node) {
390 var previous = node.previousSibling;
393 while (node.hasChildNodes()) {
394 node = node.lastChild;
398 var parent = node.parentNode;
399 if (parent && parent.nodeType == 1) {
405 // Adpated from Aryeh's code.
406 // "A whitespace node is either a Text node whose data is the empty string; or
407 // a Text node whose data consists only of one or more tabs (0x0009), line
408 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
409 // parent is an Element whose resolved value for "white-space" is "normal" or
410 // "nowrap"; or a Text node whose data consists only of one or more tabs
411 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
412 // parent is an Element whose resolved value for "white-space" is "pre-line"."
413 function isWhitespaceNode(node) {
414 if (!node || node.nodeType != 3) {
417 var text = node.data;
421 var parent = node.parentNode;
422 if (!parent || parent.nodeType != 1) {
425 var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
427 return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) ||
428 (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line");
431 // Adpated from Aryeh's code.
432 // "node is a collapsed whitespace node if the following algorithm returns
434 function isCollapsedWhitespaceNode(node) {
435 // "If node's data is the empty string, return true."
436 if (node.data === "") {
440 // "If node is not a whitespace node, return false."
441 if (!isWhitespaceNode(node)) {
445 // "Let ancestor be node's parent."
446 var ancestor = node.parentNode;
448 // "If ancestor is null, return true."
453 // "If the "display" property of some ancestor of node has resolved value "none", return true."
454 if (isHidden(node)) {
461 function isCollapsedNode(node) {
462 var type = node.nodeType;
463 return type == 7 /* PROCESSING_INSTRUCTION */ ||
464 type == 8 /* COMMENT */ ||
466 /^(script|style)$/i.test(node.nodeName) ||
467 isVisibilityHiddenTextNode(node) ||
468 isCollapsedWhitespaceNode(node);
471 function isIgnoredNode(node, win) {
472 var type = node.nodeType;
473 return type == 7 /* PROCESSING_INSTRUCTION */ ||
474 type == 8 /* COMMENT */ ||
475 (type == 1 && getComputedDisplay(node, win) == "none");
478 /*----------------------------------------------------------------------------------------------------------------*/
480 // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down
488 return this.store.hasOwnProperty(key) ? this.store[key] : null;
491 set: function(key, value) {
492 return this.store[key] = value;
496 var cachedCount = 0, uncachedCount = 0;
498 function createCachingGetter(methodName, func, objProperty) {
499 return function(args) {
500 var cache = this.cache;
501 if (cache.hasOwnProperty(methodName)) {
503 return cache[methodName];
506 var value = func.call(this, objProperty ? this[objProperty] : this, args);
507 cache[methodName] = value;
513 /*----------------------------------------------------------------------------------------------------------------*/
515 function NodeWrapper(node, session) {
517 this.session = session;
518 this.cache = new Cache();
519 this.positions = new Cache();
523 getPosition: function(offset) {
524 var positions = this.positions;
525 return positions.get(offset) || positions.set(offset, new Position(this, offset));
528 toString: function() {
529 return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]";
533 NodeWrapper.prototype = nodeProto;
536 NON_SPACE = "NON_SPACE",
537 UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE",
538 COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE",
539 TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK",
540 TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK",
541 TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR",
542 PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK",
543 TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR",
544 INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR";
547 isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"),
548 getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"),
549 getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"),
550 containsPositions: createCachingGetter("containsPositions", containsPositions, "node"),
551 isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"),
552 isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"),
553 getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"),
554 isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"),
555 isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"),
556 next: createCachingGetter("nextPos", nextNode, "node"),
557 previous: createCachingGetter("previous", previousNode, "node"),
559 getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) {
560 var spaceRegex = null, collapseSpaces = false;
561 var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace");
562 var preLine = (cssWhitespace == "pre-line");
564 spaceRegex = spacesMinusLineBreaksRegex;
565 collapseSpaces = true;
566 } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") {
567 spaceRegex = spacesRegex;
568 collapseSpaces = true;
574 spaceRegex: spaceRegex,
575 collapseSpaces: collapseSpaces,
580 hasInnerText: createCachingGetter("hasInnerText", function(el, backward) {
581 var session = this.session;
582 var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1);
583 var firstPosInEl = session.getPosition(el, 0);
585 var pos = backward ? posAfterEl : firstPosInEl;
586 var endPos = backward ? firstPosInEl : posAfterEl;
589 <body><p>X </p><p>Y</p></body>
597 text:2:TRAILING_SPACE_IN_BLOCK
598 text:3:COLLAPSED_SPACE
605 A character is a TRAILING_SPACE_IN_BLOCK iff:
607 - There is no uncollapsed character after it within the visible containing block element
609 A character is a TRAILING_SPACE_BEFORE_BR iff:
611 - There is no uncollapsed character after it preceding a <br> element
613 An element has inner text iff
616 - It contains an uncollapsed character
618 All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render.
621 while (pos !== endPos) {
622 pos.prepopulateChar();
623 if (pos.isDefinitelyNonEmpty()) {
626 pos = backward ? pos.previousVisible() : pos.nextVisible();
632 isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) {
633 // Ensure that a block element containing a <br> is considered to have inner text
634 var brs = el.getElementsByTagName("br");
635 for (var i = 0, len = brs.length; i < len; ++i) {
636 if (!isCollapsedNode(brs[i])) {
640 return this.hasInnerText();
643 getTrailingSpace: createCachingGetter("trailingSpace", function(el) {
644 if (el.tagName.toLowerCase() == "br") {
647 switch (this.getComputedDisplay()) {
649 var child = el.lastChild;
651 if (!isIgnoredNode(child)) {
652 return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : "";
654 child = child.previousSibling;
661 case "table-column-group":
666 return this.isRenderedBlock(true) ? "\n" : "";
672 getLeadingSpace: createCachingGetter("leadingSpace", function(el) {
673 switch (this.getComputedDisplay()) {
679 case "table-column-group":
683 return this.isRenderedBlock(false) ? "\n" : "";
689 /*----------------------------------------------------------------------------------------------------------------*/
691 function Position(nodeWrapper, offset) {
692 this.offset = offset;
693 this.nodeWrapper = nodeWrapper;
694 this.node = nodeWrapper.node;
695 this.session = nodeWrapper.session;
696 this.cache = new Cache();
699 function inspectPosition() {
700 return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]";
703 var positionProto = {
705 characterType: EMPTY,
710 - Fully populates positions that have characters that can be determined independently of any other characters.
711 - Populates most types of space positions with a provisional character. The character is finalized later.
713 prepopulateChar: function() {
715 if (!pos.prepopulatedChar) {
716 var node = pos.node, offset = pos.offset;
717 var visibleChar = "", charType = EMPTY;
718 var finalizedChar = false;
720 if (node.nodeType == 3) {
721 var text = node.data;
722 var textChar = text.charAt(offset - 1);
724 var nodeInfo = pos.nodeWrapper.getTextNodeInfo();
725 var spaceRegex = nodeInfo.spaceRegex;
726 if (nodeInfo.collapseSpaces) {
727 if (spaceRegex.test(textChar)) {
728 // "If the character at position is from set, append a single space (U+0020) to newdata and advance
729 // position until the character at position is not from set."
731 // We also need to check for the case where we're in a pre-line and we have a space preceding a
732 // line break, because such spaces are collapsed in some browsers
733 if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) {
734 } else if (nodeInfo.preLine && text.charAt(offset) === "\n") {
736 charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK;
739 //pos.checkForFollowingLineBreak = true;
740 charType = COLLAPSIBLE_SPACE;
743 visibleChar = textChar;
744 charType = NON_SPACE;
745 finalizedChar = true;
748 visibleChar = textChar;
749 charType = UNCOLLAPSIBLE_SPACE;
750 finalizedChar = true;
753 var nodePassed = node.childNodes[offset - 1];
754 if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) {
755 if (nodePassed.tagName.toLowerCase() == "br") {
758 charType = COLLAPSIBLE_SPACE;
759 finalizedChar = false;
761 pos.checkForTrailingSpace = true;
765 // Check the leading space of the next node for the case when a block element follows an inline
766 // element or text node. In that case, there is an implied line break between the two nodes.
768 var nextNode = node.childNodes[offset];
769 if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) {
770 pos.checkForLeadingSpace = true;
776 pos.prepopulatedChar = true;
777 pos.character = visibleChar;
778 pos.characterType = charType;
779 pos.isCharInvariant = finalizedChar;
783 isDefinitelyNonEmpty: function() {
784 var charType = this.characterType;
785 return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE;
788 // Resolve leading and trailing spaces, which may involve prepopulating other positions
789 resolveLeadingAndTrailingSpaces: function() {
790 if (!this.prepopulatedChar) {
791 this.prepopulateChar();
793 if (this.checkForTrailingSpace) {
794 var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace();
796 this.isTrailingSpace = true;
797 this.character = trailingSpace;
798 this.characterType = COLLAPSIBLE_SPACE;
800 this.checkForTrailingSpace = false;
802 if (this.checkForLeadingSpace) {
803 var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace();
805 this.isLeadingSpace = true;
806 this.character = leadingSpace;
807 this.characterType = COLLAPSIBLE_SPACE;
809 this.checkForLeadingSpace = false;
813 getPrecedingUncollapsedPosition: function(characterOptions) {
814 var pos = this, character;
815 while ( (pos = pos.previousVisible()) ) {
816 character = pos.getCharacter(characterOptions);
817 if (character !== "") {
825 getCharacter: function(characterOptions) {
826 this.resolveLeadingAndTrailingSpaces();
828 var thisChar = this.character, returnChar;
830 // Check if character is ignored
831 var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters);
832 var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1);
834 // Check if this position's character is invariant (i.e. not dependent on character options) and return it
836 if (this.isCharInvariant) {
837 returnChar = isIgnoredCharacter ? "" : thisChar;
841 var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_");
842 var cachedChar = this.cache.get(cacheKey);
843 if (cachedChar !== null) {
847 // We need to actually get the character now
849 var collapsible = (this.characterType == COLLAPSIBLE_SPACE);
851 var nextPos, previousPos;
852 var gotPreviousPos = false;
855 function getPreviousPos() {
856 if (!gotPreviousPos) {
857 previousPos = pos.getPrecedingUncollapsedPosition(characterOptions);
858 gotPreviousPos = true;
863 // Disallow a collapsible space that is followed by a line break or is the last character
865 // Allow a trailing space that we've previously determined should be included
866 if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) {
869 // Disallow a collapsible space that follows a trailing space or line break, or is the first character,
870 // or follows a collapsible included space
871 else if (thisChar == " " &&
872 (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) {
874 // Allow a leading line break unless it follows a line break
875 else if (thisChar == "\n" && this.isLeadingSpace) {
876 if (getPreviousPos() && previousPos.character != "\n") {
881 nextPos = this.nextUncollapsed();
884 this.type = TRAILING_SPACE_BEFORE_BR;
885 } else if (nextPos.isTrailingSpace && nextPos.character == "\n") {
886 this.type = TRAILING_SPACE_IN_BLOCK;
887 } else if (nextPos.isLeadingSpace && nextPos.character == "\n") {
888 this.type = TRAILING_SPACE_BEFORE_BLOCK;
891 if (nextPos.character == "\n") {
892 if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) {
893 } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) {
894 } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) {
895 } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) {
896 } else if (thisChar == "\n") {
897 if (nextPos.isTrailingSpace) {
898 if (this.isTrailingSpace) {
899 } else if (this.isBr) {
900 nextPos.type = TRAILING_LINE_BREAK_AFTER_BR;
902 if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") {
903 nextPos.character = "";
905 nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR;
911 } else if (thisChar == " ") {
916 character = thisChar;
923 if (ignoredChars.indexOf(character) > -1) {
928 this.cache.set(cacheKey, character);
933 equals: function(pos) {
934 return !!pos && this.node === pos.node && this.offset === pos.offset;
937 inspect: inspectPosition,
939 toString: function() {
940 return this.character;
944 Position.prototype = positionProto;
946 extend(positionProto, {
947 next: createCachingGetter("nextPos", function(pos) {
948 var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
952 var nextNode, nextOffset, child;
953 if (offset == nodeWrapper.getLength()) {
954 // Move onto the next node
955 nextNode = node.parentNode;
956 nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0;
958 if (nodeWrapper.isCharacterDataNode()) {
960 nextOffset = offset + 1;
962 child = node.childNodes[offset];
963 // Go into the children next, if children there are
964 if (session.getNodeWrapper(child).containsPositions()) {
969 nextOffset = offset + 1;
974 return nextNode ? session.getPosition(nextNode, nextOffset) : null;
977 previous: createCachingGetter("previous", function(pos) {
978 var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
979 var previousNode, previousOffset, child;
981 previousNode = node.parentNode;
982 previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0;
984 if (nodeWrapper.isCharacterDataNode()) {
986 previousOffset = offset - 1;
988 child = node.childNodes[offset - 1];
989 // Go into the children next, if children there are
990 if (session.getNodeWrapper(child).containsPositions()) {
991 previousNode = child;
992 previousOffset = dom.getNodeLength(child);
995 previousOffset = offset - 1;
999 return previousNode ? session.getPosition(previousNode, previousOffset) : null;
1003 Next and previous position moving functions that filter out
1005 - Hidden (CSS visibility/display) elements
1006 - Script and style elements
1008 nextVisible: createCachingGetter("nextVisible", function(pos) {
1009 var next = pos.next();
1013 var nodeWrapper = next.nodeWrapper, node = next.node;
1015 if (nodeWrapper.isCollapsed()) {
1016 // We're skipping this node and all its descendants
1017 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1);
1022 nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) {
1024 while ( (nextPos = nextPos.nextVisible()) ) {
1025 nextPos.resolveLeadingAndTrailingSpaces();
1026 if (nextPos.character !== "") {
1033 previousVisible: createCachingGetter("previousVisible", function(pos) {
1034 var previous = pos.previous();
1038 var nodeWrapper = previous.nodeWrapper, node = previous.node;
1039 var newPos = previous;
1040 if (nodeWrapper.isCollapsed()) {
1041 // We're skipping this node and all its descendants
1042 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex());
1048 /*----------------------------------------------------------------------------------------------------------------*/
1050 var currentSession = null;
1052 var Session = (function() {
1053 function createWrapperCache(nodeProperty) {
1054 var cache = new Cache();
1057 get: function(node) {
1058 var wrappersByProperty = cache.get(node[nodeProperty]);
1059 if (wrappersByProperty) {
1060 for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) {
1061 if (wrapper.node === node) {
1069 set: function(nodeWrapper) {
1070 var property = nodeWrapper.node[nodeProperty];
1071 var wrappersByProperty = cache.get(property) || cache.set(property, []);
1072 wrappersByProperty.push(nodeWrapper);
1077 var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID");
1079 function Session() {
1083 Session.prototype = {
1084 initCaches: function() {
1085 this.elementCache = uniqueIDSupported ? (function() {
1086 var elementsCache = new Cache();
1090 return elementsCache.get(el.uniqueID);
1093 set: function(elWrapper) {
1094 elementsCache.set(elWrapper.node.uniqueID, elWrapper);
1097 })() : createWrapperCache("tagName");
1099 // Store text nodes keyed by data, although we may need to truncate this
1100 this.textNodeCache = createWrapperCache("data");
1101 this.otherNodeCache = createWrapperCache("nodeName");
1104 getNodeWrapper: function(node) {
1106 switch (node.nodeType) {
1108 wrapperCache = this.elementCache;
1111 wrapperCache = this.textNodeCache;
1114 wrapperCache = this.otherNodeCache;
1118 var wrapper = wrapperCache.get(node);
1120 wrapper = new NodeWrapper(node, this);
1121 wrapperCache.set(wrapper);
1126 getPosition: function(node, offset) {
1127 return this.getNodeWrapper(node).getPosition(offset);
1130 getRangeBoundaryPosition: function(range, isStart) {
1131 var prefix = isStart ? "start" : "end";
1132 return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]);
1135 detach: function() {
1136 this.elementCache = this.textNodeCache = this.otherNodeCache = null;
1143 /*----------------------------------------------------------------------------------------------------------------*/
1145 function startSession() {
1147 return (currentSession = new Session());
1150 function getSession() {
1151 return currentSession || startSession();
1154 function endSession() {
1155 if (currentSession) {
1156 currentSession.detach();
1158 currentSession = null;
1161 /*----------------------------------------------------------------------------------------------------------------*/
1163 // Extensions to the rangy.dom utility object
1167 previousNode: previousNode
1170 /*----------------------------------------------------------------------------------------------------------------*/
1172 function createCharacterIterator(startPos, backward, endPos, characterOptions) {
1174 // Adjust the end position to ensure that it is actually reached
1177 if (isCollapsedNode(endPos.node)) {
1178 endPos = startPos.previousVisible();
1181 if (isCollapsedNode(endPos.node)) {
1182 endPos = endPos.nextVisible();
1187 var pos = startPos, finished = false;
1194 pos = pos.previousVisible();
1195 finished = !pos || (endPos && pos.equals(endPos));
1199 charPos = pos = pos.nextVisible();
1200 finished = !pos || (endPos && pos.equals(endPos));
1209 var previousTextPos, returnPreviousTextPos = false;
1213 if (returnPreviousTextPos) {
1214 returnPreviousTextPos = false;
1215 return previousTextPos;
1218 while ( (pos = next()) ) {
1219 character = pos.getCharacter(characterOptions);
1221 previousTextPos = pos;
1229 rewind: function() {
1230 if (previousTextPos) {
1231 returnPreviousTextPos = true;
1233 throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound.");
1237 dispose: function() {
1238 startPos = endPos = null;
1243 var arrayIndexOf = Array.prototype.indexOf ?
1244 function(arr, val) {
1245 return arr.indexOf(val);
1247 function(arr, val) {
1248 for (var i = 0, len = arr.length; i < len; ++i) {
1249 if (arr[i] === val) {
1256 // Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next()
1257 // is called and there is no more tokenized text
1258 function createTokenizedTextProvider(pos, characterOptions, wordOptions) {
1259 var forwardIterator = createCharacterIterator(pos, false, null, characterOptions);
1260 var backwardIterator = createCharacterIterator(pos, true, null, characterOptions);
1261 var tokenizer = wordOptions.tokenizer;
1263 // Consumes a word and the whitespace beyond it
1264 function consumeWord(forward) {
1266 var newChars = [], it = forward ? forwardIterator : backwardIterator;
1268 var passedWordBoundary = false, insideWord = false;
1270 while ( (pos = it.next()) ) {
1271 textChar = pos.character;
1274 if (allWhiteSpaceRegex.test(textChar)) {
1277 passedWordBoundary = true;
1280 if (passedWordBoundary) {
1294 // Get initial word surrounding initial position and tokenize it
1295 var forwardChars = consumeWord(true);
1296 var backwardChars = consumeWord(false).reverse();
1297 var tokens = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer);
1299 // Create initial token buffers
1300 var forwardTokensBuffer = forwardChars.length ?
1301 tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : [];
1303 var backwardTokensBuffer = backwardChars.length ?
1304 tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : [];
1306 function inspectBuffer(buffer) {
1307 var textPositions = ["[" + buffer.length + "]"];
1308 for (var i = 0; i < buffer.length; ++i) {
1309 textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")");
1311 return textPositions;
1316 nextEndToken: function() {
1317 var lastToken, forwardChars;
1319 // If we're down to the last token, consume character chunks until we have a word or run out of
1320 // characters to consume
1321 while ( forwardTokensBuffer.length == 1 &&
1322 !(lastToken = forwardTokensBuffer[0]).isWord &&
1323 (forwardChars = consumeWord(true)).length > 0) {
1325 // Merge trailing non-word into next word and tokenize
1326 forwardTokensBuffer = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer);
1329 return forwardTokensBuffer.shift();
1332 previousStartToken: function() {
1333 var lastToken, backwardChars;
1335 // If we're down to the last token, consume character chunks until we have a word or run out of
1336 // characters to consume
1337 while ( backwardTokensBuffer.length == 1 &&
1338 !(lastToken = backwardTokensBuffer[0]).isWord &&
1339 (backwardChars = consumeWord(false)).length > 0) {
1341 // Merge leading non-word into next word and tokenize
1342 backwardTokensBuffer = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer);
1345 return backwardTokensBuffer.pop();
1348 dispose: function() {
1349 forwardIterator.dispose();
1350 backwardIterator.dispose();
1351 forwardTokensBuffer = backwardTokensBuffer = null;
1356 function movePositionBy(pos, unit, count, characterOptions, wordOptions) {
1357 var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token;
1359 var backward = (count < 0);
1363 charIterator = createCharacterIterator(pos, backward, null, characterOptions);
1364 while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) {
1366 newPos = currentPos;
1368 nextPos = currentPos;
1369 charIterator.dispose();
1372 var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions);
1373 var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken;
1375 while ( (token = next()) && unitsMoved < absCount ) {
1378 newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1];
1383 throw new Error("movePositionBy: unit '" + unit + "' not implemented");
1386 // Perform any necessary position tweaks
1388 newPos = newPos.previousVisible();
1389 unitsMoved = -unitsMoved;
1390 } else if (newPos && newPos.isLeadingSpace && !newPos.isTrailingSpace) {
1391 // Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space
1392 // before a block element (for example, the line break between "1" and "2" in the following HTML:
1393 // "1<p>2</p>") is considered to be attached to the position immediately before the block element, which
1394 // corresponds with a different selection position in most browsers from the one we want (i.e. at the
1395 // start of the contents of the block element). We get round this by advancing the position returned to
1396 // the last possible equivalent visible position.
1398 charIterator = createCharacterIterator(pos, false, null, characterOptions);
1399 nextPos = charIterator.next();
1400 charIterator.dispose();
1403 newPos = nextPos.previousVisible();
1411 unitsMoved: unitsMoved
1415 function createRangeCharacterIterator(session, range, characterOptions, backward) {
1416 var rangeStart = session.getRangeBoundaryPosition(range, true);
1417 var rangeEnd = session.getRangeBoundaryPosition(range, false);
1418 var itStart = backward ? rangeEnd : rangeStart;
1419 var itEnd = backward ? rangeStart : rangeEnd;
1421 return createCharacterIterator(itStart, !!backward, itEnd, characterOptions);
1424 function getRangeCharacters(session, range, characterOptions) {
1426 var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos;
1427 while ( (pos = it.next()) ) {
1435 function isWholeWord(startPos, endPos, wordOptions) {
1436 var range = api.createRange(startPos.node);
1437 range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset);
1438 return !range.expand("word", { wordOptions: wordOptions });
1441 function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) {
1442 var backward = isDirectionBackward(findOptions.direction);
1443 var it = createCharacterIterator(
1446 initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward),
1447 findOptions.characterOptions
1449 var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex;
1450 var result, insideRegexMatch;
1451 var returnValue = null;
1453 function handleMatch(startIndex, endIndex) {
1454 var startPos = chars[startIndex].previousVisible();
1455 var endPos = chars[endIndex - 1];
1456 var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions));
1465 while ( (pos = it.next()) ) {
1466 currentChar = pos.character;
1467 if (!isRegex && !findOptions.caseSensitive) {
1468 currentChar = currentChar.toLowerCase();
1473 text = currentChar + text;
1476 text += currentChar;
1480 result = searchTerm.exec(text);
1482 matchStartIndex = result.index;
1483 matchEndIndex = matchStartIndex + result[0].length;
1484 if (insideRegexMatch) {
1485 // Check whether the match is now over
1486 if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) {
1487 returnValue = handleMatch(matchStartIndex, matchEndIndex);
1491 insideRegexMatch = true;
1494 } else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) {
1495 returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length);
1500 // Check whether regex match extends to the end of the range
1501 if (insideRegexMatch) {
1502 returnValue = handleMatch(matchStartIndex, matchEndIndex);
1509 function createEntryPointFunction(func) {
1511 var sessionRunning = !!currentSession;
1512 var session = getSession();
1513 var args = [session].concat( util.toArray(arguments) );
1514 var returnValue = func.apply(this, args);
1515 if (!sessionRunning) {
1522 /*----------------------------------------------------------------------------------------------------------------*/
1524 // Extensions to the Rangy Range object
1526 function createRangeBoundaryMover(isStart, collapse) {
1528 Unit can be "character" or "word"
1531 - includeTrailingSpace
1534 - collapseSpaceBeforeLineBreak
1536 return createEntryPointFunction(
1537 function(session, unit, count, moveOptions) {
1538 if (typeof count == UNDEF) {
1542 moveOptions = createNestedOptions(moveOptions, defaultMoveOptions);
1544 var boundaryIsStart = isStart;
1546 boundaryIsStart = (count >= 0);
1547 this.collapse(!boundaryIsStart);
1549 var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.wordOptions);
1550 var newPos = moveResult.position;
1551 this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset);
1552 return moveResult.unitsMoved;
1557 function createRangeTrimmer(isStart) {
1558 return createEntryPointFunction(
1559 function(session, characterOptions) {
1560 characterOptions = createOptions(characterOptions, defaultCharacterOptions);
1562 var it = createRangeCharacterIterator(session, this, characterOptions, !isStart);
1563 var trimCharCount = 0;
1564 while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) {
1568 var trimmed = (trimCharCount > 0);
1570 this[isStart ? "moveStart" : "moveEnd"](
1572 isStart ? trimCharCount : -trimCharCount,
1573 { characterOptions: characterOptions }
1581 extend(api.rangePrototype, {
1582 moveStart: createRangeBoundaryMover(true, false),
1584 moveEnd: createRangeBoundaryMover(false, false),
1586 move: createRangeBoundaryMover(true, true),
1588 trimStart: createRangeTrimmer(true),
1590 trimEnd: createRangeTrimmer(false),
1592 trim: createEntryPointFunction(
1593 function(session, characterOptions) {
1594 var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions);
1595 return startTrimmed || endTrimmed;
1599 expand: createEntryPointFunction(
1600 function(session, unit, expandOptions) {
1602 expandOptions = createNestedOptions(expandOptions, defaultExpandOptions);
1603 var characterOptions = expandOptions.characterOptions;
1608 var wordOptions = expandOptions.wordOptions;
1609 var startPos = session.getRangeBoundaryPosition(this, true);
1610 var endPos = session.getRangeBoundaryPosition(this, false);
1612 var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions);
1613 var startToken = startTokenizedTextProvider.nextEndToken();
1614 var newStartPos = startToken.chars[0].previousVisible();
1615 var endToken, newEndPos;
1617 if (this.collapsed) {
1618 endToken = startToken;
1620 var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions);
1621 endToken = endTokenizedTextProvider.previousStartToken();
1623 newEndPos = endToken.chars[endToken.chars.length - 1];
1625 if (!newStartPos.equals(startPos)) {
1626 this.setStart(newStartPos.node, newStartPos.offset);
1629 if (newEndPos && !newEndPos.equals(endPos)) {
1630 this.setEnd(newEndPos.node, newEndPos.offset);
1634 if (expandOptions.trim) {
1635 if (expandOptions.trimStart) {
1636 moved = this.trimStart(characterOptions) || moved;
1638 if (expandOptions.trimEnd) {
1639 moved = this.trimEnd(characterOptions) || moved;
1645 return this.moveEnd(CHARACTER, 1, expandOptions);
1650 text: createEntryPointFunction(
1651 function(session, characterOptions) {
1652 return this.collapsed ?
1653 "" : getRangeCharacters(session, this, createOptions(characterOptions, defaultCharacterOptions)).join("");
1657 selectCharacters: createEntryPointFunction(
1658 function(session, containerNode, startIndex, endIndex, characterOptions) {
1659 var moveOptions = { characterOptions: characterOptions };
1660 if (!containerNode) {
1661 containerNode = getBody( this.getDocument() );
1663 this.selectNodeContents(containerNode);
1664 this.collapse(true);
1665 this.moveStart("character", startIndex, moveOptions);
1666 this.collapse(true);
1667 this.moveEnd("character", endIndex - startIndex, moveOptions);
1671 // Character indexes are relative to the start of node
1672 toCharacterRange: createEntryPointFunction(
1673 function(session, containerNode, characterOptions) {
1674 if (!containerNode) {
1675 containerNode = getBody( this.getDocument() );
1677 var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode);
1678 var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1);
1679 var rangeBetween = this.cloneRange();
1680 var startIndex, endIndex;
1681 if (rangeStartsBeforeNode) {
1682 rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex);
1683 startIndex = -rangeBetween.text(characterOptions).length;
1685 rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset);
1686 startIndex = rangeBetween.text(characterOptions).length;
1688 endIndex = startIndex + this.text(characterOptions).length;
1697 findText: createEntryPointFunction(
1698 function(session, searchTermParam, findOptions) {
1700 findOptions = createNestedOptions(findOptions, defaultFindOptions);
1702 // Create word options if we're matching whole words only
1703 if (findOptions.wholeWordsOnly) {
1704 // We don't ever want trailing spaces for search results
1705 findOptions.wordOptions.includeTrailingSpace = false;
1708 var backward = isDirectionBackward(findOptions.direction);
1710 // Create a range representing the search scope if none was provided
1711 var searchScopeRange = findOptions.withinRange;
1712 if (!searchScopeRange) {
1713 searchScopeRange = api.createRange();
1714 searchScopeRange.selectNodeContents(this.getDocument());
1717 // Examine and prepare the search term
1718 var searchTerm = searchTermParam, isRegex = false;
1719 if (typeof searchTerm == "string") {
1720 if (!findOptions.caseSensitive) {
1721 searchTerm = searchTerm.toLowerCase();
1727 var initialPos = session.getRangeBoundaryPosition(this, !backward);
1729 // Adjust initial position if it lies outside the search scope
1730 var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset);
1732 if (comparison === -1) {
1733 initialPos = session.getRangeBoundaryPosition(searchScopeRange, true);
1734 } else if (comparison === 1) {
1735 initialPos = session.getRangeBoundaryPosition(searchScopeRange, false);
1738 var pos = initialPos;
1739 var wrappedAround = false;
1741 // Try to find a match and ignore invalid ones
1744 findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions);
1747 if (findResult.valid) {
1748 this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset);
1751 // We've found a match that is not a whole word, so we carry on searching from the point immediately
1753 pos = backward ? findResult.startPos : findResult.endPos;
1755 } else if (findOptions.wrap && !wrappedAround) {
1756 // No result found but we're wrapping around and limiting the scope to the unsearched part of the range
1757 searchScopeRange = searchScopeRange.cloneRange();
1758 pos = session.getRangeBoundaryPosition(searchScopeRange, !backward);
1759 searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward);
1760 wrappedAround = true;
1762 // Nothing found and we can't wrap around, so we're done
1769 pasteHtml: function(html) {
1770 this.deleteContents();
1772 var frag = this.createContextualFragment(html);
1773 var lastChild = frag.lastChild;
1774 this.insertNode(frag);
1775 this.collapseAfter(lastChild);
1780 /*----------------------------------------------------------------------------------------------------------------*/
1782 // Extensions to the Rangy Selection object
1784 function createSelectionTrimmer(methodName) {
1785 return createEntryPointFunction(
1786 function(session, characterOptions) {
1787 var trimmed = false;
1788 this.changeEachRange(function(range) {
1789 trimmed = range[methodName](characterOptions) || trimmed;
1796 extend(api.selectionPrototype, {
1797 expand: createEntryPointFunction(
1798 function(session, unit, expandOptions) {
1799 this.changeEachRange(function(range) {
1800 range.expand(unit, expandOptions);
1805 move: createEntryPointFunction(
1806 function(session, unit, count, options) {
1808 if (this.focusNode) {
1809 this.collapse(this.focusNode, this.focusOffset);
1810 var range = this.getRangeAt(0);
1814 options.characterOptions = createOptions(options.characterOptions, defaultCaretCharacterOptions);
1815 unitsMoved = range.move(unit, count, options);
1816 this.setSingleRange(range);
1822 trimStart: createSelectionTrimmer("trimStart"),
1823 trimEnd: createSelectionTrimmer("trimEnd"),
1824 trim: createSelectionTrimmer("trim"),
1826 selectCharacters: createEntryPointFunction(
1827 function(session, containerNode, startIndex, endIndex, direction, characterOptions) {
1828 var range = api.createRange(containerNode);
1829 range.selectCharacters(containerNode, startIndex, endIndex, characterOptions);
1830 this.setSingleRange(range, direction);
1834 saveCharacterRanges: createEntryPointFunction(
1835 function(session, containerNode, characterOptions) {
1836 var ranges = this.getAllRanges(), rangeCount = ranges.length;
1837 var rangeInfos = [];
1839 var backward = rangeCount == 1 && this.isBackward();
1841 for (var i = 0, len = ranges.length; i < len; ++i) {
1843 characterRange: ranges[i].toCharacterRange(containerNode, characterOptions),
1845 characterOptions: characterOptions
1853 restoreCharacterRanges: createEntryPointFunction(
1854 function(session, containerNode, saved) {
1855 this.removeAllRanges();
1856 for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) {
1857 rangeInfo = saved[i];
1858 characterRange = rangeInfo.characterRange;
1859 range = api.createRange(containerNode);
1860 range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions);
1861 this.addRange(range, rangeInfo.backward);
1866 text: createEntryPointFunction(
1867 function(session, characterOptions) {
1868 var rangeTexts = [];
1869 for (var i = 0, len = this.rangeCount; i < len; ++i) {
1870 rangeTexts[i] = this.getRangeAt(i).text(characterOptions);
1872 return rangeTexts.join("");
1877 /*----------------------------------------------------------------------------------------------------------------*/
1879 // Extensions to the core rangy object
1881 api.innerText = function(el, characterOptions) {
1882 var range = api.createRange(el);
1883 range.selectNodeContents(el);
1884 var text = range.text(characterOptions);
1888 api.createWordIterator = function(startNode, startOffset, iteratorOptions) {
1889 var session = getSession();
1890 iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions);
1891 var startPos = session.getPosition(startNode, startOffset);
1892 var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.wordOptions);
1893 var backward = isDirectionBackward(iteratorOptions.direction);
1897 return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken();
1900 dispose: function() {
1901 tokenizedTextProvider.dispose();
1902 this.next = function() {};
1907 /*----------------------------------------------------------------------------------------------------------------*/
1909 api.noMutation = function(func) {
1910 var session = getSession();
1915 api.noMutation.createEntryPointFunction = createEntryPointFunction;
1918 isBlockNode: isBlockNode,
1919 isCollapsedWhitespaceNode: isCollapsedWhitespaceNode,
1921 createPosition: createEntryPointFunction(
1922 function(session, node, offset) {
1923 return session.getPosition(node, offset);