UNPKG

rangy

Version:

A cross-browser DOM range and selection library

1,200 lines (1,037 loc) 82 kB
/** * Text range module for Rangy. * Text-based manipulation and searching of ranges and selections. * * Features * * - Ability to move range boundaries by character or word offsets * - Customizable word tokenizer * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case * sensitivity * - Selection and range save/restore as text offsets within a node * - Methods to return visible text within a range or selection * - innerText method for elements * * References * * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145 * http://aryeh.name/spec/innertext/innertext.html * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html * * Part of Rangy, a cross-browser JavaScript range and selection library * https://github.com/timdown/rangy * * Depends on Rangy core. * * Copyright 2024, Tim Down * Licensed under the MIT license. * Version: 1.3.2 * Build date: 2 November 2024 */ /** * Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers. * * First, a <br>: this is relatively simple. For the following HTML: * * 1 <br>2 * * - IE and WebKit render the space, include it in the selection (i.e. when the content is selected and pasted into a * textarea, the space is present) and allow the caret to be placed after it. * - Firefox does not acknowledge the space in the selection but it is possible to place the caret after it. * - Opera does not render the space but has two separate caret positions on either side of the space (left and right * arrow keys show this) and includes the space in the selection. * * The other case is the line break or breaks implied by block elements. For the following HTML: * * <p>1 </p><p>2<p> * * - WebKit does not acknowledge the space in any way * - Firefox, IE and Opera as per <br> * * One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML: * * <p style="white-space: pre-line">1 * 2</p> * * - Firefox and WebKit include the space in caret positions * - IE does not support pre-line up to and including version 9 * - Opera ignores the space * - Trailing space only renders if there is a non-collapsed character in the line * * Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be * feature-tested */ (function(factory, root) { if (typeof define == "function" && define.amd) { // AMD. Register as an anonymous module with a dependency on Rangy. define(["./rangy-core"], factory); } else if (typeof module != "undefined" && typeof exports == "object") { // Node/CommonJS style module.exports = factory( require("rangy") ); } else { // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) factory(root.rangy); } })(function(rangy) { rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) { var UNDEF = "undefined"; var CHARACTER = "character", WORD = "word"; var dom = api.dom, util = api.util; var extend = util.extend; var createOptions = util.createOptions; var getBody = dom.getBody; var spacesRegex = /^[ \t\f\r\n]+$/; var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/; var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/; var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/; var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/; var defaultLanguage = "en"; var isDirectionBackward = api.Selection.isDirectionBackward; // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit, // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed. var trailingSpaceInBlockCollapses = false; var trailingSpaceBeforeBrCollapses = false; var trailingSpaceBeforeBlockCollapses = false; var trailingSpaceBeforeLineBreakInPreLineCollapses = true; (function() { var el = dom.createTestElement(document, "<p>1 </p><p></p>", true); var p = el.firstChild; var sel = api.getSelection(); sel.collapse(p.lastChild, 2); sel.setStart(p.firstChild, 0); trailingSpaceInBlockCollapses = ("" + sel).length == 1; el.innerHTML = "1 <br />"; sel.collapse(el, 2); sel.setStart(el.firstChild, 0); trailingSpaceBeforeBrCollapses = ("" + sel).length == 1; el.innerHTML = "1 <p>1</p>"; sel.collapse(el, 2); sel.setStart(el.firstChild, 0); trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1; dom.removeNode(el); sel.removeAllRanges(); })(); /*----------------------------------------------------------------------------------------------------------------*/ // This function must create word and non-word tokens for the whole of the text supplied to it function defaultTokenizer(chars, wordOptions) { var word = chars.join(""), result, tokenRanges = []; function createTokenRange(start, end, isWord) { tokenRanges.push( { start: start, end: end, isWord: isWord } ); } // Match words and mark characters var lastWordEnd = 0, wordStart, wordEnd; while ( (result = wordOptions.wordRegex.exec(word)) ) { wordStart = result.index; wordEnd = wordStart + result[0].length; // Create token for non-word characters preceding this word if (wordStart > lastWordEnd) { createTokenRange(lastWordEnd, wordStart, false); } // Get trailing space characters for word if (wordOptions.includeTrailingSpace) { while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) { ++wordEnd; } } createTokenRange(wordStart, wordEnd, true); lastWordEnd = wordEnd; } // Create token for trailing non-word characters, if any exist if (lastWordEnd < chars.length) { createTokenRange(lastWordEnd, chars.length, false); } return tokenRanges; } function convertCharRangeToToken(chars, tokenRange) { var tokenChars = chars.slice(tokenRange.start, tokenRange.end); var token = { isWord: tokenRange.isWord, chars: tokenChars, toString: function() { return tokenChars.join(""); } }; for (var i = 0, len = tokenChars.length; i < len; ++i) { tokenChars[i].token = token; } return token; } function tokenize(chars, wordOptions, tokenizer) { var tokenRanges = tokenizer(chars, wordOptions); var tokens = []; for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) { tokens.push( convertCharRangeToToken(chars, tokenRange) ); } return tokens; } var defaultCharacterOptions = { includeBlockContentTrailingSpace: true, includeSpaceBeforeBr: true, includeSpaceBeforeBlock: true, includePreLineTrailingSpace: true, ignoreCharacters: "" }; function normalizeIgnoredCharacters(ignoredCharacters) { // Check if character is ignored var ignoredChars = ignoredCharacters || ""; // Normalize ignored characters into a string consisting of characters in ascending order of character code var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars; ignoredCharsArray.sort(function(char1, char2) { return char1.charCodeAt(0) - char2.charCodeAt(0); }); /// Convert back to a string and remove duplicates return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1"); } var defaultCaretCharacterOptions = { includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses, includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses, includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses, includePreLineTrailingSpace: true }; var defaultWordOptions = { "en": { wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi, includeTrailingSpace: false, tokenizer: defaultTokenizer } }; var defaultFindOptions = { caseSensitive: false, withinRange: null, wholeWordsOnly: false, wrap: false, direction: "forward", wordOptions: null, characterOptions: null }; var defaultMoveOptions = { wordOptions: null, characterOptions: null }; var defaultExpandOptions = { wordOptions: null, characterOptions: null, trim: false, trimStart: true, trimEnd: true }; var defaultWordIteratorOptions = { wordOptions: null, characterOptions: null, direction: "forward" }; function createWordOptions(options) { var lang, defaults; if (!options) { return defaultWordOptions[defaultLanguage]; } else { lang = options.language || defaultLanguage; defaults = {}; extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]); extend(defaults, options); return defaults; } } function createNestedOptions(optionsParam, defaults) { var options = createOptions(optionsParam, defaults); if (defaults.hasOwnProperty("wordOptions")) { options.wordOptions = createWordOptions(options.wordOptions); } if (defaults.hasOwnProperty("characterOptions")) { options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions); } return options; } /*----------------------------------------------------------------------------------------------------------------*/ /* DOM utility functions */ var getComputedStyleProperty = dom.getComputedStyleProperty; // Create cachable versions of DOM functions // Test for old IE's incorrect display properties var tableCssDisplayBlock; (function() { var table = document.createElement("table"); var body = getBody(document); body.appendChild(table); tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block"); body.removeChild(table); })(); var defaultDisplayValueForTag = { table: "table", caption: "table-caption", colgroup: "table-column-group", col: "table-column", thead: "table-header-group", tbody: "table-row-group", tfoot: "table-footer-group", tr: "table-row", td: "table-cell", th: "table-cell" }; // Corrects IE's "block" value for table-related elements function getComputedDisplay(el, win) { var display = getComputedStyleProperty(el, "display", win); var tagName = el.tagName.toLowerCase(); return (display == "block" && tableCssDisplayBlock && defaultDisplayValueForTag.hasOwnProperty(tagName)) ? defaultDisplayValueForTag[tagName] : display; } function isHidden(node) { var ancestors = getAncestorsAndSelf(node); for (var i = 0, len = ancestors.length; i < len; ++i) { if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") { return true; } } return false; } function isVisibilityHiddenTextNode(textNode) { var el; return textNode.nodeType == 3 && (el = textNode.parentNode) && getComputedStyleProperty(el, "visibility") == "hidden"; } /*----------------------------------------------------------------------------------------------------------------*/ // "A block node is either an Element whose "display" property does not have // resolved value "inline" or "inline-block" or "inline-table" or "none", or a // Document, or a DocumentFragment." function isBlockNode(node) { return node && ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) || node.nodeType == 9 || node.nodeType == 11); } function getLastDescendantOrSelf(node) { var lastChild = node.lastChild; return lastChild ? getLastDescendantOrSelf(lastChild) : node; } function containsPositions(node) { return dom.isCharacterDataNode(node) || !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName); } function getAncestors(node) { var ancestors = []; while (node.parentNode) { ancestors.unshift(node.parentNode); node = node.parentNode; } return ancestors; } function getAncestorsAndSelf(node) { return getAncestors(node).concat([node]); } function nextNodeDescendants(node) { while (node && !node.nextSibling) { node = node.parentNode; } if (!node) { return null; } return node.nextSibling; } function nextNode(node, excludeChildren) { if (!excludeChildren && node.hasChildNodes()) { return node.firstChild; } return nextNodeDescendants(node); } function previousNode(node) { var previous = node.previousSibling; if (previous) { node = previous; while (node.hasChildNodes()) { node = node.lastChild; } return node; } var parent = node.parentNode; if (parent && parent.nodeType == 1) { return parent; } return null; } // Adpated from Aryeh's code. // "A whitespace node is either a Text node whose data is the empty string; or // a Text node whose data consists only of one or more tabs (0x0009), line // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose // parent is an Element whose resolved value for "white-space" is "normal" or // "nowrap"; or a Text node whose data consists only of one or more tabs // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose // parent is an Element whose resolved value for "white-space" is "pre-line"." function isWhitespaceNode(node) { if (!node || node.nodeType != 3) { return false; } var text = node.data; if (text === "") { return true; } var parent = node.parentNode; if (!parent || parent.nodeType != 1) { return false; } var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace"); return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) || (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line"); } // Adpated from Aryeh's code. // "node is a collapsed whitespace node if the following algorithm returns // true:" function isCollapsedWhitespaceNode(node) { // "If node's data is the empty string, return true." if (node.data === "") { return true; } // "If node is not a whitespace node, return false." if (!isWhitespaceNode(node)) { return false; } // "Let ancestor be node's parent." var ancestor = node.parentNode; // "If ancestor is null, return true." if (!ancestor) { return true; } // "If the "display" property of some ancestor of node has resolved value "none", return true." if (isHidden(node)) { return true; } return false; } function isCollapsedNode(node) { var type = node.nodeType; return type == 7 /* PROCESSING_INSTRUCTION */ || type == 8 /* COMMENT */ || isHidden(node) || /^(script|style)$/i.test(node.nodeName) || isVisibilityHiddenTextNode(node) || isCollapsedWhitespaceNode(node); } function isIgnoredNode(node, win) { var type = node.nodeType; return type == 7 /* PROCESSING_INSTRUCTION */ || type == 8 /* COMMENT */ || (type == 1 && getComputedDisplay(node, win) == "none"); } /*----------------------------------------------------------------------------------------------------------------*/ // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down function Cache() { this.store = {}; } Cache.prototype = { get: function(key) { return this.store.hasOwnProperty(key) ? this.store[key] : null; }, set: function(key, value) { return this.store[key] = value; } }; var cachedCount = 0, uncachedCount = 0; function createCachingGetter(methodName, func, objProperty) { return function(args) { var cache = this.cache; if (cache.hasOwnProperty(methodName)) { cachedCount++; return cache[methodName]; } else { uncachedCount++; var value = func.call(this, objProperty ? this[objProperty] : this, args); cache[methodName] = value; return value; } }; } /*----------------------------------------------------------------------------------------------------------------*/ function NodeWrapper(node, session) { this.node = node; this.session = session; this.cache = new Cache(); this.positions = new Cache(); } var nodeProto = { getPosition: function(offset) { var positions = this.positions; return positions.get(offset) || positions.set(offset, new Position(this, offset)); }, toString: function() { return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]"; } }; NodeWrapper.prototype = nodeProto; var EMPTY = "EMPTY", NON_SPACE = "NON_SPACE", UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE", COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE", TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK", TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK", TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR", PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK", TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR", INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR"; extend(nodeProto, { isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"), getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"), getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"), containsPositions: createCachingGetter("containsPositions", containsPositions, "node"), isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"), isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"), getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"), isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"), isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"), next: createCachingGetter("nextPos", nextNode, "node"), previous: createCachingGetter("previous", previousNode, "node"), getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) { var spaceRegex = null, collapseSpaces = false; var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace"); var preLine = (cssWhitespace == "pre-line"); if (preLine) { spaceRegex = spacesMinusLineBreaksRegex; collapseSpaces = true; } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") { spaceRegex = spacesRegex; collapseSpaces = true; } return { node: textNode, text: textNode.data, spaceRegex: spaceRegex, collapseSpaces: collapseSpaces, preLine: preLine }; }, "node"), hasInnerText: createCachingGetter("hasInnerText", function(el, backward) { var session = this.session; var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1); var firstPosInEl = session.getPosition(el, 0); var pos = backward ? posAfterEl : firstPosInEl; var endPos = backward ? firstPosInEl : posAfterEl; /* <body><p>X </p><p>Y</p></body> Positions: body:0:"" p:0:"" text:0:"" text:1:"X" text:2:TRAILING_SPACE_IN_BLOCK text:3:COLLAPSED_SPACE p:1:"" body:1:"\n" p:0:"" text:0:"" text:1:"Y" A character is a TRAILING_SPACE_IN_BLOCK iff: - There is no uncollapsed character after it within the visible containing block element A character is a TRAILING_SPACE_BEFORE_BR iff: - There is no uncollapsed character after it preceding a <br> element An element has inner text iff - It is not hidden - It contains an uncollapsed character All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render. */ while (pos !== endPos) { pos.prepopulateChar(); if (pos.isDefinitelyNonEmpty()) { return true; } pos = backward ? pos.previousVisible() : pos.nextVisible(); } return false; }, "node"), isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) { // Ensure that a block element containing a <br> is considered to have inner text var brs = el.getElementsByTagName("br"); for (var i = 0, len = brs.length; i < len; ++i) { if (!isCollapsedNode(brs[i])) { return true; } } return this.hasInnerText(); }, "node"), getTrailingSpace: createCachingGetter("trailingSpace", function(el) { if (el.tagName.toLowerCase() == "br") { return ""; } else { switch (this.getComputedDisplay()) { case "inline": var child = el.lastChild; while (child) { if (!isIgnoredNode(child)) { return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : ""; } child = child.previousSibling; } break; case "inline-block": case "inline-table": case "none": case "table-column": case "table-column-group": break; case "table-cell": return "\t"; default: return this.isRenderedBlock(true) ? "\n" : ""; } } return ""; }, "node"), getLeadingSpace: createCachingGetter("leadingSpace", function(el) { switch (this.getComputedDisplay()) { case "inline": case "inline-block": case "inline-table": case "none": case "table-column": case "table-column-group": case "table-cell": break; default: return this.isRenderedBlock(false) ? "\n" : ""; } return ""; }, "node") }); /*----------------------------------------------------------------------------------------------------------------*/ function Position(nodeWrapper, offset) { this.offset = offset; this.nodeWrapper = nodeWrapper; this.node = nodeWrapper.node; this.session = nodeWrapper.session; this.cache = new Cache(); } function inspectPosition() { return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]"; } var positionProto = { character: "", characterType: EMPTY, isBr: false, /* This method: - Fully populates positions that have characters that can be determined independently of any other characters. - Populates most types of space positions with a provisional character. The character is finalized later. */ prepopulateChar: function() { var pos = this; if (!pos.prepopulatedChar) { var node = pos.node, offset = pos.offset; var visibleChar = "", charType = EMPTY; var finalizedChar = false; if (offset > 0) { if (node.nodeType == 3) { var text = node.data; var textChar = text.charAt(offset - 1); var nodeInfo = pos.nodeWrapper.getTextNodeInfo(); var spaceRegex = nodeInfo.spaceRegex; if (nodeInfo.collapseSpaces) { if (spaceRegex.test(textChar)) { // "If the character at position is from set, append a single space (U+0020) to newdata and advance // position until the character at position is not from set." // We also need to check for the case where we're in a pre-line and we have a space preceding a // line break, because such spaces are collapsed in some browsers if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) { } else if (nodeInfo.preLine && text.charAt(offset) === "\n") { visibleChar = " "; charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK; } else { visibleChar = " "; //pos.checkForFollowingLineBreak = true; charType = COLLAPSIBLE_SPACE; } } else { visibleChar = textChar; charType = NON_SPACE; finalizedChar = true; } } else { visibleChar = textChar; charType = UNCOLLAPSIBLE_SPACE; finalizedChar = true; } } else { var nodePassed = node.childNodes[offset - 1]; if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) { if (nodePassed.tagName.toLowerCase() == "br") { visibleChar = "\n"; pos.isBr = true; charType = COLLAPSIBLE_SPACE; finalizedChar = false; } else { pos.checkForTrailingSpace = true; } } // Check the leading space of the next node for the case when a block element follows an inline // element or text node. In that case, there is an implied line break between the two nodes. if (!visibleChar) { var nextNode = node.childNodes[offset]; if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) { pos.checkForLeadingSpace = true; } } } } pos.prepopulatedChar = true; pos.character = visibleChar; pos.characterType = charType; pos.isCharInvariant = finalizedChar; } }, isDefinitelyNonEmpty: function() { var charType = this.characterType; return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE; }, // Resolve leading and trailing spaces, which may involve prepopulating other positions resolveLeadingAndTrailingSpaces: function() { if (!this.prepopulatedChar) { this.prepopulateChar(); } if (this.checkForTrailingSpace) { var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace(); if (trailingSpace) { this.isTrailingSpace = true; this.character = trailingSpace; this.characterType = COLLAPSIBLE_SPACE; } this.checkForTrailingSpace = false; } if (this.checkForLeadingSpace) { var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace(); if (leadingSpace) { this.isLeadingSpace = true; this.character = leadingSpace; this.characterType = COLLAPSIBLE_SPACE; } this.checkForLeadingSpace = false; } }, getPrecedingUncollapsedPosition: function(characterOptions) { var pos = this, character; while ( (pos = pos.previousVisible()) ) { character = pos.getCharacter(characterOptions); if (character !== "") { return pos; } } return null; }, getCharacter: function(characterOptions) { this.resolveLeadingAndTrailingSpaces(); var thisChar = this.character, returnChar; // Check if character is ignored var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters); var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1); // Check if this position's character is invariant (i.e. not dependent on character options) and return it // if so if (this.isCharInvariant) { returnChar = isIgnoredCharacter ? "" : thisChar; return returnChar; } var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_"); var cachedChar = this.cache.get(cacheKey); if (cachedChar !== null) { return cachedChar; } // We need to actually get the character now var character = ""; var collapsible = (this.characterType == COLLAPSIBLE_SPACE); var nextPos, previousPos; var gotPreviousPos = false; var pos = this; function getPreviousPos() { if (!gotPreviousPos) { previousPos = pos.getPrecedingUncollapsedPosition(characterOptions); gotPreviousPos = true; } return previousPos; } // Disallow a collapsible space that is followed by a line break or is the last character if (collapsible) { // Allow a trailing space that we've previously determined should be included if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) { character = "\n"; } // Disallow a collapsible space that follows a trailing space or line break, or is the first character, // or follows a collapsible included space else if (thisChar == " " && (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) { } // Allow a leading line break unless it follows a line break else if (thisChar == "\n" && this.isLeadingSpace) { if (getPreviousPos() && previousPos.character != "\n") { character = "\n"; } else { } } else { nextPos = this.nextUncollapsed(); if (nextPos) { if (nextPos.isBr) { this.type = TRAILING_SPACE_BEFORE_BR; } else if (nextPos.isTrailingSpace && nextPos.character == "\n") { this.type = TRAILING_SPACE_IN_BLOCK; } else if (nextPos.isLeadingSpace && nextPos.character == "\n") { this.type = TRAILING_SPACE_BEFORE_BLOCK; } if (nextPos.character == "\n") { if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) { } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) { } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) { } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) { } else if (thisChar == "\n") { if (nextPos.isTrailingSpace) { if (this.isTrailingSpace) { } else if (this.isBr) { nextPos.type = TRAILING_LINE_BREAK_AFTER_BR; if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") { nextPos.character = ""; } else { nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR; } } } else { character = "\n"; } } else if (thisChar == " ") { character = " "; } else { } } else { character = thisChar; } } else { } } } if (ignoredChars.indexOf(character) > -1) { character = ""; } this.cache.set(cacheKey, character); return character; }, equals: function(pos) { return !!pos && this.node === pos.node && this.offset === pos.offset; }, inspect: inspectPosition, toString: function() { return this.character; } }; Position.prototype = positionProto; extend(positionProto, { next: createCachingGetter("nextPos", function(pos) { var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session; if (!node) { return null; } var nextNode, nextOffset, child; if (offset == nodeWrapper.getLength()) { // Move onto the next node nextNode = node.parentNode; nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0; } else { if (nodeWrapper.isCharacterDataNode()) { nextNode = node; nextOffset = offset + 1; } else { child = node.childNodes[offset]; // Go into the children next, if children there are if (session.getNodeWrapper(child).containsPositions()) { nextNode = child; nextOffset = 0; } else { nextNode = node; nextOffset = offset + 1; } } } return nextNode ? session.getPosition(nextNode, nextOffset) : null; }), previous: createCachingGetter("previous", function(pos) { var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session; var previousNode, previousOffset, child; if (offset == 0) { previousNode = node.parentNode; previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0; } else { if (nodeWrapper.isCharacterDataNode()) { previousNode = node; previousOffset = offset - 1; } else { child = node.childNodes[offset - 1]; // Go into the children next, if children there are if (session.getNodeWrapper(child).containsPositions()) { previousNode = child; previousOffset = dom.getNodeLength(child); } else { previousNode = node; previousOffset = offset - 1; } } } return previousNode ? session.getPosition(previousNode, previousOffset) : null; }), /* Next and previous position moving functions that filter out - Hidden (CSS visibility/display) elements - Script and style elements */ nextVisible: createCachingGetter("nextVisible", function(pos) { var next = pos.next(); if (!next) { return null; } var nodeWrapper = next.nodeWrapper, node = next.node; var newPos = next; if (nodeWrapper.isCollapsed()) { // We're skipping this node and all its descendants newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1); } return newPos; }), nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) { var nextPos = pos; while ( (nextPos = nextPos.nextVisible()) ) { nextPos.resolveLeadingAndTrailingSpaces(); if (nextPos.character !== "") { return nextPos; } } return null; }), previousVisible: createCachingGetter("previousVisible", function(pos) { var previous = pos.previous(); if (!previous) { return null; } var nodeWrapper = previous.nodeWrapper, node = previous.node; var newPos = previous; if (nodeWrapper.isCollapsed()) { // We're skipping this node and all its descendants newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex()); } return newPos; }) }); /*----------------------------------------------------------------------------------------------------------------*/ var currentSession = null; var Session = (function() { function createWrapperCache(nodeProperty) { var cache = new Cache(); return { get: function(node) { var wrappersByProperty = cache.get(node[nodeProperty]); if (wrappersByProperty) { for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) { if (wrapper.node === node) { return wrapper; } } } return null; }, set: function(nodeWrapper) { var property = nodeWrapper.node[nodeProperty]; var wrappersByProperty = cache.get(property) || cache.set(property, []); wrappersByProperty.push(nodeWrapper); } }; } var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID"); function Session() { this.initCaches(); } Session.prototype = { initCaches: function() { this.elementCache = uniqueIDSupported ? (function() { var elementsCache = new Cache(); return { get: function(el) { return elementsCache.get(el.uniqueID); }, set: function(elWrapper) { elementsCache.set(elWrapper.node.uniqueID, elWrapper); } }; })() : createWrapperCache("tagName"); // Store text nodes keyed by data, although we may need to truncate this this.textNodeCache = createWrapperCache("data"); this.otherNodeCache = createWrapperCache("nodeName"); }, getNodeWrapper: function(node) { var wrapperCache; switch (node.nodeType) { case 1: wrapperCache = this.elementCache; break; case 3: wrapperCache = this.textNodeCache; break; default: wrapperCache = this.otherNodeCache; break; } var wrapper = wrapperCache.get(node); if (!wrapper) { wrapper = new NodeWrapper(node, this); wrapperCache.set(wrapper); } return wrapper; }, getPosition: function(node, offset) { return this.getNodeWrapper(node).getPosition(offset); }, getRangeBoundaryPosition: function(range, isStart) { var prefix = isStart ? "start" : "end"; return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]); }, detach: function() { this.elementCache = this.textNodeCache = this.otherNodeCache = null; } }; return Session; })(); /*----------------------------------------------------------------------------------------------------------------*/ function startSession() { endSession(); return (currentSession = new Session()); } function getSession() { return currentSession || startSession(); } function endSession() { if (currentSession) { currentSession.detach(); } currentSession = null; } /*----------------------------------------------------------------------------------------------------------------*/ // Extensions to the rangy.dom utility object extend(dom, { nextNode: nextNode, previousNode: previousNode }); /*----------------------------------------------------------------------------------------------------------------*/ function createCharacterIterator(startPos, backward, endPos, characterOptions) { // Adjust the end position to ensure that it is actually reached if (endPos) { if (backward) { if (isCollapsedNode(endPos.node)) { endPos = startPos.previousVisible(); } } else { if (isCollapsedNode(endPos.node)) { endPos = endPos.nextVisible(); } } } var pos = startPos, finished = false; function next() { var charPos = null; if (backward) { charPos = pos; if (!finished) { pos = pos.previousVisible(); finished = !pos || (endPos && pos.equals(endPos)); } } else { if (!finished) { charPos = pos = pos.nextVisible();