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