mirror of
https://github.com/janickiy/yii2-nomer
synced 2025-03-09 15:39:59 +00:00
1917 lines
73 KiB
JavaScript
1917 lines
73 KiB
JavaScript
/**
|
|
* 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 2015, Tim Down
|
|
* Licensed under the MIT license.
|
|
* Version: 1.3.1-dev
|
|
* Build date: 20 May 2015
|
|
*/
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* NOTE: UMD wrapper removed manually for bundling (Oliver Pulges)
|
|
*/
|
|
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();
|
|
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 = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer);
|
|
|
|
// 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 = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer);
|
|
}
|
|
|
|
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 = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer);
|
|
}
|
|
|
|
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 && !newPos.isTrailingSpace) {
|
|
// 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);
|
|
return !range.expand("word", { wordOptions: wordOptions });
|
|
}
|
|
|
|
function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) {
|
|
var backward = isDirectionBackward(findOptions.direction);
|
|
var it = createCharacterIterator(
|
|
initialPos,
|
|
backward,
|
|
initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward),
|
|
findOptions.characterOptions
|
|
);
|
|
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;
|
|
}
|
|
|
|
if (isRegex) {
|
|
result = searchTerm.exec(text);
|
|
if (result) {
|
|
matchStartIndex = result.index;
|
|
matchEndIndex = matchStartIndex + result[0].length;
|
|
if (insideRegexMatch) {
|
|
// Check whether the match is now over
|
|
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 == UNDEF) {
|
|
count = unit;
|
|
unit = CHARACTER;
|
|
}
|
|
moveOptions = createNestedOptions(moveOptions, defaultMoveOptions);
|
|
|
|
var boundaryIsStart = isStart;
|
|
if (collapse) {
|
|
boundaryIsStart = (count >= 0);
|
|
this.collapse(!boundaryIsStart);
|
|
}
|
|
var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.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 = createOptions(characterOptions, defaultCharacterOptions);
|
|
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 = createNestedOptions(expandOptions, defaultExpandOptions);
|
|
var characterOptions = expandOptions.characterOptions;
|
|
if (!unit) {
|
|
unit = CHARACTER;
|
|
}
|
|
if (unit == WORD) {
|
|
var wordOptions = 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, createOptions(characterOptions, defaultCharacterOptions)).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 = createNestedOptions(findOptions, defaultFindOptions);
|
|
|
|
// Create word options if we're matching whole words only
|
|
if (findOptions.wholeWordsOnly) {
|
|
// 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 = createOptions(options.characterOptions, defaultCaretCharacterOptions);
|
|
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);
|
|
return text;
|
|
};
|
|
|
|
api.createWordIterator = function(startNode, startOffset, iteratorOptions) {
|
|
var session = getSession();
|
|
iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions);
|
|
var startPos = session.getPosition(startNode, startOffset);
|
|
var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.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);
|
|
}
|
|
)
|
|
};
|
|
});
|