Mini Shell
/**
* 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
* http://code.google.com/p/rangy/
*
* Depends on Rangy core.
*
* Copyright 2013, Tim Down
* Licensed under the MIT license.
* Version: 1.3alpha.804
* Build date: 8 December 2013
*/
/**
* 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
*/
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 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 = document.createElement("div");
el.contentEditable = "true";
el.innerHTML = "<p>1 </p><p></p>";
var body = getBody(document);
var p = el.firstChild;
var sel = api.getSelection();
body.appendChild(el);
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;
body.removeChild(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, tokens = [];
function createTokenFromRange(start, end, isWord) {
var tokenChars = chars.slice(start, end);
var token = {
isWord: isWord,
chars: tokenChars,
toString: function() {
return tokenChars.join("");
}
};
for (var i = 0, len = tokenChars.length; i < len; ++i) {
tokenChars[i].token = token;
}
tokens.push(token);
}
// 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) {
createTokenFromRange(lastWordEnd, wordStart, false);
}
// Get trailing space characters for word
if (wordOptions.includeTrailingSpace) {
while (nonLineBreakWhiteSpaceRegex.test(chars[wordEnd])) {
++wordEnd;
}
}
createTokenFromRange(wordStart, wordEnd, true);
lastWordEnd = wordEnd;
}
// Create token for trailing non-word characters, if any exist
if (lastWordEnd < chars.length) {
createTokenFromRange(lastWordEnd, chars.length, false);
}
return tokens;
}
var defaultCharacterOptions = {
includeBlockContentTrailingSpace: true,
includeSpaceBeforeBr: true,
includeSpaceBeforeBlock: true,
includePreLineTrailingSpace: true
};
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
}
};
function createOptions(optionsParam, defaults) {
if (!optionsParam) {
return defaults;
} else {
var options = {};
extend(options, defaults);
extend(options, optionsParam);
return options;
}
}
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 createCharacterOptions(options) {
return createOptions(options, defaultCharacterOptions);
}
function createCaretCharacterOptions(options) {
return createOptions(options, defaultCaretCharacterOptions);
}
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"
};
/*----------------------------------------------------------------------------------------------------------------*/
/* 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);
})();
api.features.tableCssDisplayBlock = tableCssDisplayBlock;
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;
}
};
}
/*
api.report = function() {
console.log("Cached: " + cachedCount + ", uncached: " + uncachedCount);
};
*/
/*----------------------------------------------------------------------------------------------------------------*/
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";
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();
// Check if this position's character is invariant (i.e. not dependent on character options) and return it
// if so
if (this.isCharInvariant) {
return this.character;
}
var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace].join("_");
var cachedChar = this.cache.get(cacheKey);
if (cachedChar !== null) {
return cachedChar;
}
// We need to actually get the character
var character = "";
var collapsible = (this.characterType == COLLAPSIBLE_SPACE);
var nextPos, previousPos/* = this.getPrecedingUncollapsedPosition(characterOptions)*/;
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) {
// Disallow a collapsible space that follows a trailing space or line break, or is the first character
if (this.character == " " &&
(!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n")) {
}
// Allow a leading line break unless it follows a line break
else if (this.character == "\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 (this.character === "\n") {
if (nextPos.isTrailingSpace) {
if (this.isTrailingSpace) {
} else if (this.isBr) {
nextPos.type = TRAILING_LINE_BREAK_AFTER_BR;
if (getPreviousPos() && previousPos.isLeadingSpace && previousPos.character == "\n") {
nextPos.character = "";
} else {
//character = "\n";
//nextPos
/*
nextPos.character = "";
character = "\n";
*/
}
}
} else {
character = "\n";
}
} else if (this.character === " ") {
character = " ";
} else {
}
} else {
character = this.character;
}
} else {
}
}
}
// Collapse a br element that is followed by a trailing space
else if (this.character === "\n" &&
(!(nextPos = this.nextUncollapsed()) || nextPos.isTrailingSpace)) {
}
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 newPos = null, charPos = null;
if (backward) {
charPos = pos;
if (!finished) {
pos = pos.previousVisible();
finished = !pos || (endPos && pos.equals(endPos));
}
} else {
if (!finished) {
charPos = pos = pos.nextVisible();
finished = !pos || (endPos && pos.equals(endPos));
}
}
if (finished) {
pos = null;
}
return charPos;
}
var previousTextPos, returnPreviousTextPos = false;
return {
next: function() {
if (returnPreviousTextPos) {
returnPreviousTextPos = false;
return previousTextPos;
} else {
var pos, character;
while ( (pos = next()) ) {
character = pos.getCharacter(characterOptions);
if (character) {
previousTextPos = pos;
return pos;
}
}
return null;
}
},
rewind: function() {
if (previousTextPos) {
returnPreviousTextPos = true;
} else {
throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound.");
}
},
dispose: function() {
startPos = endPos = null;
}
};
}
var arrayIndexOf = Array.prototype.indexOf ?
function(arr, val) {
return arr.indexOf(val);
} :
function(arr, val) {
for (var i = 0, len = arr.length; i < len; ++i) {
if (arr[i] === val) {
return i;
}
}
return -1;
};
// Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next()
// is called and there is no more tokenized text
function createTokenizedTextProvider(pos, characterOptions, wordOptions) {
var forwardIterator = createCharacterIterator(pos, false, null, characterOptions);
var backwardIterator = createCharacterIterator(pos, true, null, characterOptions);
var tokenizer = wordOptions.tokenizer;
// Consumes a word and the whitespace beyond it
function consumeWord(forward) {
var pos, textChar;
var newChars = [], it = forward ? forwardIterator : backwardIterator;
var passedWordBoundary = false, insideWord = false;
while ( (pos = it.next()) ) {
textChar = pos.character;
if (allWhiteSpaceRegex.test(textChar)) {
if (insideWord) {
insideWord = false;
passedWordBoundary = true;
}
} else {
if (passedWordBoundary) {
it.rewind();
break;
} else {
insideWord = true;
}
}
newChars.push(pos);
}
return newChars;
}
// Get initial word surrounding initial position and tokenize it
var forwardChars = consumeWord(true);
var backwardChars = consumeWord(false).reverse();
var tokens = tokenizer(backwardChars.concat(forwardChars), wordOptions);
// Create initial token buffers
var forwardTokensBuffer = forwardChars.length ?
tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : [];
var backwardTokensBuffer = backwardChars.length ?
tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : [];
function inspectBuffer(buffer) {
var textPositions = ["[" + buffer.length + "]"];
for (var i = 0; i < buffer.length; ++i) {
textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")");
}
return textPositions;
}
return {
nextEndToken: function() {
var lastToken, forwardChars;
// If we're down to the last token, consume character chunks until we have a word or run out of
// characters to consume
while ( forwardTokensBuffer.length == 1 &&
!(lastToken = forwardTokensBuffer[0]).isWord &&
(forwardChars = consumeWord(true)).length > 0) {
// Merge trailing non-word into next word and tokenize
forwardTokensBuffer = tokenizer(lastToken.chars.concat(forwardChars), wordOptions);
}
return forwardTokensBuffer.shift();
},
previousStartToken: function() {
var lastToken, backwardChars;
// If we're down to the last token, consume character chunks until we have a word or run out of
// characters to consume
while ( backwardTokensBuffer.length == 1 &&
!(lastToken = backwardTokensBuffer[0]).isWord &&
(backwardChars = consumeWord(false)).length > 0) {
// Merge leading non-word into next word and tokenize
backwardTokensBuffer = tokenizer(backwardChars.reverse().concat(lastToken.chars), wordOptions);
}
return backwardTokensBuffer.pop();
},
dispose: function() {
forwardIterator.dispose();
backwardIterator.dispose();
forwardTokensBuffer = backwardTokensBuffer = null;
}
};
}
function movePositionBy(pos, unit, count, characterOptions, wordOptions) {
var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token;
if (count !== 0) {
var backward = (count < 0);
switch (unit) {
case CHARACTER:
charIterator = createCharacterIterator(pos, backward, null, characterOptions);
while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) {
++unitsMoved;
newPos = currentPos;
}
nextPos = currentPos;
charIterator.dispose();
break;
case WORD:
var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions);
var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken;
while ( (token = next()) && unitsMoved < absCount ) {
if (token.isWord) {
++unitsMoved;
newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1];
}
}
break;
default:
throw new Error("movePositionBy: unit '" + unit + "' not implemented");
}
// Perform any necessary position tweaks
if (backward) {
newPos = newPos.previousVisible();
unitsMoved = -unitsMoved;
} else if (newPos && newPos.isLeadingSpace) {
// Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space
// before a block element (for example, the line break between "1" and "2" in the following HTML:
// "1<p>2</p>") is considered to be attached to the position immediately before the block element, which
// corresponds with a different selection position in most browsers from the one we want (i.e. at the
// start of the contents of the block element). We get round this by advancing the position returned to
// the last possible equivalent visible position.
if (unit == WORD) {
charIterator = createCharacterIterator(pos, false, null, characterOptions);
nextPos = charIterator.next();
charIterator.dispose();
}
if (nextPos) {
newPos = nextPos.previousVisible();
}
}
}
return {
position: newPos,
unitsMoved: unitsMoved
};
}
function createRangeCharacterIterator(session, range, characterOptions, backward) {
var rangeStart = session.getRangeBoundaryPosition(range, true);
var rangeEnd = session.getRangeBoundaryPosition(range, false);
var itStart = backward ? rangeEnd : rangeStart;
var itEnd = backward ? rangeStart : rangeEnd;
return createCharacterIterator(itStart, !!backward, itEnd, characterOptions);
}
function getRangeCharacters(session, range, characterOptions) {
var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos;
while ( (pos = it.next()) ) {
chars.push(pos);
}
it.dispose();
return chars;
}
function isWholeWord(startPos, endPos, wordOptions) {
var range = api.createRange(startPos.node);
range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset);
var returnVal = !range.expand("word", wordOptions);
range.detach();
return returnVal;
}
function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) {
var backward = isDirectionBackward(findOptions.direction);
var it = createCharacterIterator(
initialPos,
backward,
initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward),
findOptions
);
var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex;
var result, insideRegexMatch;
var returnValue = null;
function handleMatch(startIndex, endIndex) {
var startPos = chars[startIndex].previousVisible();
var endPos = chars[endIndex - 1];
var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions));
return {
startPos: startPos,
endPos: endPos,
valid: valid
};
}
while ( (pos = it.next()) ) {
currentChar = pos.character;
if (!isRegex && !findOptions.caseSensitive) {
currentChar = currentChar.toLowerCase();
}
if (backward) {
chars.unshift(pos);
text = currentChar + text;
} else {
chars.push(pos);
text += currentChar;
}
//console.log("text " + text)
if (isRegex) {
result = searchTerm.exec(text);
if (result) {
if (insideRegexMatch) {
// Check whether the match is now over
matchStartIndex = result.index;
matchEndIndex = matchStartIndex + result[0].length;
if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) {
returnValue = handleMatch(matchStartIndex, matchEndIndex);
break;
}
} else {
insideRegexMatch = true;
}
}
} else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) {
returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length);
break;
}
}
// Check whether regex match extends to the end of the range
if (insideRegexMatch) {
returnValue = handleMatch(matchStartIndex, matchEndIndex);
}
it.dispose();
return returnValue;
}
function createEntryPointFunction(func) {
return function() {
var sessionRunning = !!currentSession;
var session = getSession();
var args = [session].concat( util.toArray(arguments) );
var returnValue = func.apply(this, args);
if (!sessionRunning) {
endSession();
}
return returnValue;
};
}
/*----------------------------------------------------------------------------------------------------------------*/
// Extensions to the Rangy Range object
function createRangeBoundaryMover(isStart, collapse) {
/*
Unit can be "character" or "word"
Options:
- includeTrailingSpace
- wordRegex
- tokenizer
- collapseSpaceBeforeLineBreak
*/
return createEntryPointFunction(
function(session, unit, count, moveOptions) {
if (typeof count == "undefined") {
count = unit;
unit = CHARACTER;
}
moveOptions = createOptions(moveOptions, defaultMoveOptions);
var characterOptions = createCharacterOptions(moveOptions.characterOptions);
var wordOptions = createWordOptions(moveOptions.wordOptions);
var boundaryIsStart = isStart;
if (collapse) {
boundaryIsStart = (count >= 0);
this.collapse(!boundaryIsStart);
}
var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, characterOptions, wordOptions);
var newPos = moveResult.position;
this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset);
return moveResult.unitsMoved;
}
);
}
function createRangeTrimmer(isStart) {
return createEntryPointFunction(
function(session, characterOptions) {
characterOptions = createCharacterOptions(characterOptions);
var pos;
var it = createRangeCharacterIterator(session, this, characterOptions, !isStart);
var trimCharCount = 0;
while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) {
++trimCharCount;
}
it.dispose();
var trimmed = (trimCharCount > 0);
if (trimmed) {
this[isStart ? "moveStart" : "moveEnd"](
"character",
isStart ? trimCharCount : -trimCharCount,
{ characterOptions: characterOptions }
);
}
return trimmed;
}
);
}
extend(api.rangePrototype, {
moveStart: createRangeBoundaryMover(true, false),
moveEnd: createRangeBoundaryMover(false, false),
move: createRangeBoundaryMover(true, true),
trimStart: createRangeTrimmer(true),
trimEnd: createRangeTrimmer(false),
trim: createEntryPointFunction(
function(session, characterOptions) {
var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions);
return startTrimmed || endTrimmed;
}
),
expand: createEntryPointFunction(
function(session, unit, expandOptions) {
var moved = false;
expandOptions = createOptions(expandOptions, defaultExpandOptions);
var characterOptions = createCharacterOptions(expandOptions.characterOptions);
if (!unit) {
unit = CHARACTER;
}
if (unit == WORD) {
var wordOptions = createWordOptions(expandOptions.wordOptions);
var startPos = session.getRangeBoundaryPosition(this, true);
var endPos = session.getRangeBoundaryPosition(this, false);
var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions);
var startToken = startTokenizedTextProvider.nextEndToken();
var newStartPos = startToken.chars[0].previousVisible();
var endToken, newEndPos;
if (this.collapsed) {
endToken = startToken;
} else {
var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions);
endToken = endTokenizedTextProvider.previousStartToken();
}
newEndPos = endToken.chars[endToken.chars.length - 1];
if (!newStartPos.equals(startPos)) {
this.setStart(newStartPos.node, newStartPos.offset);
moved = true;
}
if (newEndPos && !newEndPos.equals(endPos)) {
this.setEnd(newEndPos.node, newEndPos.offset);
moved = true;
}
if (expandOptions.trim) {
if (expandOptions.trimStart) {
moved = this.trimStart(characterOptions) || moved;
}
if (expandOptions.trimEnd) {
moved = this.trimEnd(characterOptions) || moved;
}
}
return moved;
} else {
return this.moveEnd(CHARACTER, 1, expandOptions);
}
}
),
text: createEntryPointFunction(
function(session, characterOptions) {
return this.collapsed ?
"" : getRangeCharacters(session, this, createCharacterOptions(characterOptions)).join("");
}
),
selectCharacters: createEntryPointFunction(
function(session, containerNode, startIndex, endIndex, characterOptions) {
var moveOptions = { characterOptions: characterOptions };
if (!containerNode) {
containerNode = getBody( this.getDocument() );
}
this.selectNodeContents(containerNode);
this.collapse(true);
this.moveStart("character", startIndex, moveOptions);
this.collapse(true);
this.moveEnd("character", endIndex - startIndex, moveOptions);
}
),
// Character indexes are relative to the start of node
toCharacterRange: createEntryPointFunction(
function(session, containerNode, characterOptions) {
if (!containerNode) {
containerNode = getBody( this.getDocument() );
}
var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode);
var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1);
var rangeBetween = this.cloneRange();
var startIndex, endIndex;
if (rangeStartsBeforeNode) {
rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex);
startIndex = -rangeBetween.text(characterOptions).length;
} else {
rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset);
startIndex = rangeBetween.text(characterOptions).length;
}
endIndex = startIndex + this.text(characterOptions).length;
return {
start: startIndex,
end: endIndex
};
}
),
findText: createEntryPointFunction(
function(session, searchTermParam, findOptions) {
// Set up options
findOptions = createOptions(findOptions, defaultFindOptions);
// Create word options if we're matching whole words only
if (findOptions.wholeWordsOnly) {
findOptions.wordOptions = createWordOptions(findOptions.wordOptions);
// We don't ever want trailing spaces for search results
findOptions.wordOptions.includeTrailingSpace = false;
}
var backward = isDirectionBackward(findOptions.direction);
// Create a range representing the search scope if none was provided
var searchScopeRange = findOptions.withinRange;
if (!searchScopeRange) {
searchScopeRange = api.createRange();
searchScopeRange.selectNodeContents(this.getDocument());
}
// Examine and prepare the search term
var searchTerm = searchTermParam, isRegex = false;
if (typeof searchTerm == "string") {
if (!findOptions.caseSensitive) {
searchTerm = searchTerm.toLowerCase();
}
} else {
isRegex = true;
}
var initialPos = session.getRangeBoundaryPosition(this, !backward);
// Adjust initial position if it lies outside the search scope
var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset);
if (comparison === -1) {
initialPos = session.getRangeBoundaryPosition(searchScopeRange, true);
} else if (comparison === 1) {
initialPos = session.getRangeBoundaryPosition(searchScopeRange, false);
}
var pos = initialPos;
var wrappedAround = false;
// Try to find a match and ignore invalid ones
var findResult;
while (true) {
findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions);
if (findResult) {
if (findResult.valid) {
this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset);
return true;
} else {
// We've found a match that is not a whole word, so we carry on searching from the point immediately
// after the match
pos = backward ? findResult.startPos : findResult.endPos;
}
} else if (findOptions.wrap && !wrappedAround) {
// No result found but we're wrapping around and limiting the scope to the unsearched part of the range
searchScopeRange = searchScopeRange.cloneRange();
pos = session.getRangeBoundaryPosition(searchScopeRange, !backward);
searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward);
wrappedAround = true;
} else {
// Nothing found and we can't wrap around, so we're done
return false;
}
}
}
),
pasteHtml: function(html) {
this.deleteContents();
if (html) {
var frag = this.createContextualFragment(html);
var lastChild = frag.lastChild;
this.insertNode(frag);
this.collapseAfter(lastChild);
}
}
});
/*----------------------------------------------------------------------------------------------------------------*/
// Extensions to the Rangy Selection object
function createSelectionTrimmer(methodName) {
return createEntryPointFunction(
function(session, characterOptions) {
var trimmed = false;
this.changeEachRange(function(range) {
trimmed = range[methodName](characterOptions) || trimmed;
});
return trimmed;
}
);
}
extend(api.selectionPrototype, {
expand: createEntryPointFunction(
function(session, unit, expandOptions) {
this.changeEachRange(function(range) {
range.expand(unit, expandOptions);
});
}
),
move: createEntryPointFunction(
function(session, unit, count, options) {
var unitsMoved = 0;
if (this.focusNode) {
this.collapse(this.focusNode, this.focusOffset);
var range = this.getRangeAt(0);
if (!options) {
options = {};
}
options.characterOptions = createCaretCharacterOptions(options.characterOptions);
unitsMoved = range.move(unit, count, options);
this.setSingleRange(range);
}
return unitsMoved;
}
),
trimStart: createSelectionTrimmer("trimStart"),
trimEnd: createSelectionTrimmer("trimEnd"),
trim: createSelectionTrimmer("trim"),
selectCharacters: createEntryPointFunction(
function(session, containerNode, startIndex, endIndex, direction, characterOptions) {
var range = api.createRange(containerNode);
range.selectCharacters(containerNode, startIndex, endIndex, characterOptions);
this.setSingleRange(range, direction);
}
),
saveCharacterRanges: createEntryPointFunction(
function(session, containerNode, characterOptions) {
var ranges = this.getAllRanges(), rangeCount = ranges.length;
var rangeInfos = [];
var backward = rangeCount == 1 && this.isBackward();
for (var i = 0, len = ranges.length; i < len; ++i) {
rangeInfos[i] = {
characterRange: ranges[i].toCharacterRange(containerNode, characterOptions),
backward: backward,
characterOptions: characterOptions
};
}
return rangeInfos;
}
),
restoreCharacterRanges: createEntryPointFunction(
function(session, containerNode, saved) {
this.removeAllRanges();
for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) {
rangeInfo = saved[i];
characterRange = rangeInfo.characterRange;
range = api.createRange(containerNode);
range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions);
this.addRange(range, rangeInfo.backward);
}
}
),
text: createEntryPointFunction(
function(session, characterOptions) {
var rangeTexts = [];
for (var i = 0, len = this.rangeCount; i < len; ++i) {
rangeTexts[i] = this.getRangeAt(i).text(characterOptions);
}
return rangeTexts.join("");
}
)
});
/*----------------------------------------------------------------------------------------------------------------*/
// Extensions to the core rangy object
api.innerText = function(el, characterOptions) {
var range = api.createRange(el);
range.selectNodeContents(el);
var text = range.text(characterOptions);
range.detach();
return text;
};
api.createWordIterator = function(startNode, startOffset, iteratorOptions) {
var session = getSession();
iteratorOptions = createOptions(iteratorOptions, defaultWordIteratorOptions);
var characterOptions = createCharacterOptions(iteratorOptions.characterOptions);
var wordOptions = createWordOptions(iteratorOptions.wordOptions);
var startPos = session.getPosition(startNode, startOffset);
var tokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions);
var backward = isDirectionBackward(iteratorOptions.direction);
return {
next: function() {
return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken();
},
dispose: function() {
tokenizedTextProvider.dispose();
this.next = function() {};
}
};
};
/*----------------------------------------------------------------------------------------------------------------*/
api.noMutation = function(func) {
var session = getSession();
func(session);
endSession();
};
api.noMutation.createEntryPointFunction = createEntryPointFunction;
api.textRange = {
isBlockNode: isBlockNode,
isCollapsedWhitespaceNode: isCollapsedWhitespaceNode,
createPosition: createEntryPointFunction(
function(session, node, offset) {
return session.getPosition(node, offset);
}
)
};
});
Zerion Mini Shell 1.0