rangy-textrange.js 80 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930
  1. /**
  2. * Text range module for Rangy.
  3. * Text-based manipulation and searching of ranges and selections.
  4. *
  5. * Features
  6. *
  7. * - Ability to move range boundaries by character or word offsets
  8. * - Customizable word tokenizer
  9. * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties
  10. * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case
  11. * sensitivity
  12. * - Selection and range save/restore as text offsets within a node
  13. * - Methods to return visible text within a range or selection
  14. * - innerText method for elements
  15. *
  16. * References
  17. *
  18. * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145
  19. * http://aryeh.name/spec/innertext/innertext.html
  20. * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html
  21. *
  22. * Part of Rangy, a cross-browser JavaScript range and selection library
  23. * https://github.com/timdown/rangy
  24. *
  25. * Depends on Rangy core.
  26. *
  27. * Copyright 2015, Tim Down
  28. * Licensed under the MIT license.
  29. * Version: 1.3.0
  30. * Build date: 10 May 2015
  31. */
  32. /**
  33. * Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers.
  34. *
  35. * First, a <br>: this is relatively simple. For the following HTML:
  36. *
  37. * 1 <br>2
  38. *
  39. * - IE and WebKit render the space, include it in the selection (i.e. when the content is selected and pasted into a
  40. * textarea, the space is present) and allow the caret to be placed after it.
  41. * - Firefox does not acknowledge the space in the selection but it is possible to place the caret after it.
  42. * - Opera does not render the space but has two separate caret positions on either side of the space (left and right
  43. * arrow keys show this) and includes the space in the selection.
  44. *
  45. * The other case is the line break or breaks implied by block elements. For the following HTML:
  46. *
  47. * <p>1 </p><p>2<p>
  48. *
  49. * - WebKit does not acknowledge the space in any way
  50. * - Firefox, IE and Opera as per <br>
  51. *
  52. * One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML:
  53. *
  54. * <p style="white-space: pre-line">1
  55. * 2</p>
  56. *
  57. * - Firefox and WebKit include the space in caret positions
  58. * - IE does not support pre-line up to and including version 9
  59. * - Opera ignores the space
  60. * - Trailing space only renders if there is a non-collapsed character in the line
  61. *
  62. * Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be
  63. * feature-tested
  64. */
  65. (function(factory, root) {
  66. if (typeof define == "function" && define.amd) {
  67. // AMD. Register as an anonymous module with a dependency on Rangy.
  68. define(["./rangy-core"], factory);
  69. } else if (typeof module != "undefined" && typeof exports == "object") {
  70. // Node/CommonJS style
  71. module.exports = factory( require("rangy") );
  72. } else {
  73. // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
  74. factory(root.rangy);
  75. }
  76. })(function(rangy) {
  77. rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) {
  78. var UNDEF = "undefined";
  79. var CHARACTER = "character", WORD = "word";
  80. var dom = api.dom, util = api.util;
  81. var extend = util.extend;
  82. var createOptions = util.createOptions;
  83. var getBody = dom.getBody;
  84. var spacesRegex = /^[ \t\f\r\n]+$/;
  85. var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/;
  86. var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/;
  87. var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/;
  88. var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/;
  89. var defaultLanguage = "en";
  90. var isDirectionBackward = api.Selection.isDirectionBackward;
  91. // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit,
  92. // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed.
  93. var trailingSpaceInBlockCollapses = false;
  94. var trailingSpaceBeforeBrCollapses = false;
  95. var trailingSpaceBeforeBlockCollapses = false;
  96. var trailingSpaceBeforeLineBreakInPreLineCollapses = true;
  97. (function() {
  98. var el = dom.createTestElement(document, "<p>1 </p><p></p>", true);
  99. var p = el.firstChild;
  100. var sel = api.getSelection();
  101. sel.collapse(p.lastChild, 2);
  102. sel.setStart(p.firstChild, 0);
  103. trailingSpaceInBlockCollapses = ("" + sel).length == 1;
  104. el.innerHTML = "1 <br />";
  105. sel.collapse(el, 2);
  106. sel.setStart(el.firstChild, 0);
  107. trailingSpaceBeforeBrCollapses = ("" + sel).length == 1;
  108. el.innerHTML = "1 <p>1</p>";
  109. sel.collapse(el, 2);
  110. sel.setStart(el.firstChild, 0);
  111. trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1;
  112. dom.removeNode(el);
  113. sel.removeAllRanges();
  114. })();
  115. /*----------------------------------------------------------------------------------------------------------------*/
  116. // This function must create word and non-word tokens for the whole of the text supplied to it
  117. function defaultTokenizer(chars, wordOptions) {
  118. var word = chars.join(""), result, tokenRanges = [];
  119. function createTokenRange(start, end, isWord) {
  120. tokenRanges.push( { start: start, end: end, isWord: isWord } );
  121. }
  122. // Match words and mark characters
  123. var lastWordEnd = 0, wordStart, wordEnd;
  124. while ( (result = wordOptions.wordRegex.exec(word)) ) {
  125. wordStart = result.index;
  126. wordEnd = wordStart + result[0].length;
  127. // Create token for non-word characters preceding this word
  128. if (wordStart > lastWordEnd) {
  129. createTokenRange(lastWordEnd, wordStart, false);
  130. }
  131. // Get trailing space characters for word
  132. if (wordOptions.includeTrailingSpace) {
  133. while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) {
  134. ++wordEnd;
  135. }
  136. }
  137. createTokenRange(wordStart, wordEnd, true);
  138. lastWordEnd = wordEnd;
  139. }
  140. // Create token for trailing non-word characters, if any exist
  141. if (lastWordEnd < chars.length) {
  142. createTokenRange(lastWordEnd, chars.length, false);
  143. }
  144. return tokenRanges;
  145. }
  146. function convertCharRangeToToken(chars, tokenRange) {
  147. var tokenChars = chars.slice(tokenRange.start, tokenRange.end);
  148. var token = {
  149. isWord: tokenRange.isWord,
  150. chars: tokenChars,
  151. toString: function() {
  152. return tokenChars.join("");
  153. }
  154. };
  155. for (var i = 0, len = tokenChars.length; i < len; ++i) {
  156. tokenChars[i].token = token;
  157. }
  158. return token;
  159. }
  160. function tokenize(chars, wordOptions, tokenizer) {
  161. var tokenRanges = tokenizer(chars, wordOptions);
  162. var tokens = [];
  163. for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) {
  164. tokens.push( convertCharRangeToToken(chars, tokenRange) );
  165. }
  166. return tokens;
  167. }
  168. var defaultCharacterOptions = {
  169. includeBlockContentTrailingSpace: true,
  170. includeSpaceBeforeBr: true,
  171. includeSpaceBeforeBlock: true,
  172. includePreLineTrailingSpace: true,
  173. ignoreCharacters: ""
  174. };
  175. function normalizeIgnoredCharacters(ignoredCharacters) {
  176. // Check if character is ignored
  177. var ignoredChars = ignoredCharacters || "";
  178. // Normalize ignored characters into a string consisting of characters in ascending order of character code
  179. var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars;
  180. ignoredCharsArray.sort(function(char1, char2) {
  181. return char1.charCodeAt(0) - char2.charCodeAt(0);
  182. });
  183. /// Convert back to a string and remove duplicates
  184. return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1");
  185. }
  186. var defaultCaretCharacterOptions = {
  187. includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses,
  188. includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses,
  189. includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses,
  190. includePreLineTrailingSpace: true
  191. };
  192. var defaultWordOptions = {
  193. "en": {
  194. wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi,
  195. includeTrailingSpace: false,
  196. tokenizer: defaultTokenizer
  197. }
  198. };
  199. var defaultFindOptions = {
  200. caseSensitive: false,
  201. withinRange: null,
  202. wholeWordsOnly: false,
  203. wrap: false,
  204. direction: "forward",
  205. wordOptions: null,
  206. characterOptions: null
  207. };
  208. var defaultMoveOptions = {
  209. wordOptions: null,
  210. characterOptions: null
  211. };
  212. var defaultExpandOptions = {
  213. wordOptions: null,
  214. characterOptions: null,
  215. trim: false,
  216. trimStart: true,
  217. trimEnd: true
  218. };
  219. var defaultWordIteratorOptions = {
  220. wordOptions: null,
  221. characterOptions: null,
  222. direction: "forward"
  223. };
  224. function createWordOptions(options) {
  225. var lang, defaults;
  226. if (!options) {
  227. return defaultWordOptions[defaultLanguage];
  228. } else {
  229. lang = options.language || defaultLanguage;
  230. defaults = {};
  231. extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]);
  232. extend(defaults, options);
  233. return defaults;
  234. }
  235. }
  236. function createNestedOptions(optionsParam, defaults) {
  237. var options = createOptions(optionsParam, defaults);
  238. if (defaults.hasOwnProperty("wordOptions")) {
  239. options.wordOptions = createWordOptions(options.wordOptions);
  240. }
  241. if (defaults.hasOwnProperty("characterOptions")) {
  242. options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions);
  243. }
  244. return options;
  245. }
  246. /*----------------------------------------------------------------------------------------------------------------*/
  247. /* DOM utility functions */
  248. var getComputedStyleProperty = dom.getComputedStyleProperty;
  249. // Create cachable versions of DOM functions
  250. // Test for old IE's incorrect display properties
  251. var tableCssDisplayBlock;
  252. (function() {
  253. var table = document.createElement("table");
  254. var body = getBody(document);
  255. body.appendChild(table);
  256. tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block");
  257. body.removeChild(table);
  258. })();
  259. var defaultDisplayValueForTag = {
  260. table: "table",
  261. caption: "table-caption",
  262. colgroup: "table-column-group",
  263. col: "table-column",
  264. thead: "table-header-group",
  265. tbody: "table-row-group",
  266. tfoot: "table-footer-group",
  267. tr: "table-row",
  268. td: "table-cell",
  269. th: "table-cell"
  270. };
  271. // Corrects IE's "block" value for table-related elements
  272. function getComputedDisplay(el, win) {
  273. var display = getComputedStyleProperty(el, "display", win);
  274. var tagName = el.tagName.toLowerCase();
  275. return (display == "block" &&
  276. tableCssDisplayBlock &&
  277. defaultDisplayValueForTag.hasOwnProperty(tagName)) ?
  278. defaultDisplayValueForTag[tagName] : display;
  279. }
  280. function isHidden(node) {
  281. var ancestors = getAncestorsAndSelf(node);
  282. for (var i = 0, len = ancestors.length; i < len; ++i) {
  283. if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") {
  284. return true;
  285. }
  286. }
  287. return false;
  288. }
  289. function isVisibilityHiddenTextNode(textNode) {
  290. var el;
  291. return textNode.nodeType == 3 &&
  292. (el = textNode.parentNode) &&
  293. getComputedStyleProperty(el, "visibility") == "hidden";
  294. }
  295. /*----------------------------------------------------------------------------------------------------------------*/
  296. // "A block node is either an Element whose "display" property does not have
  297. // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
  298. // Document, or a DocumentFragment."
  299. function isBlockNode(node) {
  300. return node &&
  301. ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) ||
  302. node.nodeType == 9 || node.nodeType == 11);
  303. }
  304. function getLastDescendantOrSelf(node) {
  305. var lastChild = node.lastChild;
  306. return lastChild ? getLastDescendantOrSelf(lastChild) : node;
  307. }
  308. function containsPositions(node) {
  309. return dom.isCharacterDataNode(node) ||
  310. !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName);
  311. }
  312. function getAncestors(node) {
  313. var ancestors = [];
  314. while (node.parentNode) {
  315. ancestors.unshift(node.parentNode);
  316. node = node.parentNode;
  317. }
  318. return ancestors;
  319. }
  320. function getAncestorsAndSelf(node) {
  321. return getAncestors(node).concat([node]);
  322. }
  323. function nextNodeDescendants(node) {
  324. while (node && !node.nextSibling) {
  325. node = node.parentNode;
  326. }
  327. if (!node) {
  328. return null;
  329. }
  330. return node.nextSibling;
  331. }
  332. function nextNode(node, excludeChildren) {
  333. if (!excludeChildren && node.hasChildNodes()) {
  334. return node.firstChild;
  335. }
  336. return nextNodeDescendants(node);
  337. }
  338. function previousNode(node) {
  339. var previous = node.previousSibling;
  340. if (previous) {
  341. node = previous;
  342. while (node.hasChildNodes()) {
  343. node = node.lastChild;
  344. }
  345. return node;
  346. }
  347. var parent = node.parentNode;
  348. if (parent && parent.nodeType == 1) {
  349. return parent;
  350. }
  351. return null;
  352. }
  353. // Adpated from Aryeh's code.
  354. // "A whitespace node is either a Text node whose data is the empty string; or
  355. // a Text node whose data consists only of one or more tabs (0x0009), line
  356. // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
  357. // parent is an Element whose resolved value for "white-space" is "normal" or
  358. // "nowrap"; or a Text node whose data consists only of one or more tabs
  359. // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
  360. // parent is an Element whose resolved value for "white-space" is "pre-line"."
  361. function isWhitespaceNode(node) {
  362. if (!node || node.nodeType != 3) {
  363. return false;
  364. }
  365. var text = node.data;
  366. if (text === "") {
  367. return true;
  368. }
  369. var parent = node.parentNode;
  370. if (!parent || parent.nodeType != 1) {
  371. return false;
  372. }
  373. var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
  374. return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) ||
  375. (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line");
  376. }
  377. // Adpated from Aryeh's code.
  378. // "node is a collapsed whitespace node if the following algorithm returns
  379. // true:"
  380. function isCollapsedWhitespaceNode(node) {
  381. // "If node's data is the empty string, return true."
  382. if (node.data === "") {
  383. return true;
  384. }
  385. // "If node is not a whitespace node, return false."
  386. if (!isWhitespaceNode(node)) {
  387. return false;
  388. }
  389. // "Let ancestor be node's parent."
  390. var ancestor = node.parentNode;
  391. // "If ancestor is null, return true."
  392. if (!ancestor) {
  393. return true;
  394. }
  395. // "If the "display" property of some ancestor of node has resolved value "none", return true."
  396. if (isHidden(node)) {
  397. return true;
  398. }
  399. return false;
  400. }
  401. function isCollapsedNode(node) {
  402. var type = node.nodeType;
  403. return type == 7 /* PROCESSING_INSTRUCTION */ ||
  404. type == 8 /* COMMENT */ ||
  405. isHidden(node) ||
  406. /^(script|style)$/i.test(node.nodeName) ||
  407. isVisibilityHiddenTextNode(node) ||
  408. isCollapsedWhitespaceNode(node);
  409. }
  410. function isIgnoredNode(node, win) {
  411. var type = node.nodeType;
  412. return type == 7 /* PROCESSING_INSTRUCTION */ ||
  413. type == 8 /* COMMENT */ ||
  414. (type == 1 && getComputedDisplay(node, win) == "none");
  415. }
  416. /*----------------------------------------------------------------------------------------------------------------*/
  417. // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down
  418. function Cache() {
  419. this.store = {};
  420. }
  421. Cache.prototype = {
  422. get: function(key) {
  423. return this.store.hasOwnProperty(key) ? this.store[key] : null;
  424. },
  425. set: function(key, value) {
  426. return this.store[key] = value;
  427. }
  428. };
  429. var cachedCount = 0, uncachedCount = 0;
  430. function createCachingGetter(methodName, func, objProperty) {
  431. return function(args) {
  432. var cache = this.cache;
  433. if (cache.hasOwnProperty(methodName)) {
  434. cachedCount++;
  435. return cache[methodName];
  436. } else {
  437. uncachedCount++;
  438. var value = func.call(this, objProperty ? this[objProperty] : this, args);
  439. cache[methodName] = value;
  440. return value;
  441. }
  442. };
  443. }
  444. /*----------------------------------------------------------------------------------------------------------------*/
  445. function NodeWrapper(node, session) {
  446. this.node = node;
  447. this.session = session;
  448. this.cache = new Cache();
  449. this.positions = new Cache();
  450. }
  451. var nodeProto = {
  452. getPosition: function(offset) {
  453. var positions = this.positions;
  454. return positions.get(offset) || positions.set(offset, new Position(this, offset));
  455. },
  456. toString: function() {
  457. return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]";
  458. }
  459. };
  460. NodeWrapper.prototype = nodeProto;
  461. var EMPTY = "EMPTY",
  462. NON_SPACE = "NON_SPACE",
  463. UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE",
  464. COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE",
  465. TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK",
  466. TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK",
  467. TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR",
  468. PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK",
  469. TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR",
  470. INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR";
  471. extend(nodeProto, {
  472. isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"),
  473. getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"),
  474. getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"),
  475. containsPositions: createCachingGetter("containsPositions", containsPositions, "node"),
  476. isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"),
  477. isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"),
  478. getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"),
  479. isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"),
  480. isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"),
  481. next: createCachingGetter("nextPos", nextNode, "node"),
  482. previous: createCachingGetter("previous", previousNode, "node"),
  483. getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) {
  484. var spaceRegex = null, collapseSpaces = false;
  485. var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace");
  486. var preLine = (cssWhitespace == "pre-line");
  487. if (preLine) {
  488. spaceRegex = spacesMinusLineBreaksRegex;
  489. collapseSpaces = true;
  490. } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") {
  491. spaceRegex = spacesRegex;
  492. collapseSpaces = true;
  493. }
  494. return {
  495. node: textNode,
  496. text: textNode.data,
  497. spaceRegex: spaceRegex,
  498. collapseSpaces: collapseSpaces,
  499. preLine: preLine
  500. };
  501. }, "node"),
  502. hasInnerText: createCachingGetter("hasInnerText", function(el, backward) {
  503. var session = this.session;
  504. var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1);
  505. var firstPosInEl = session.getPosition(el, 0);
  506. var pos = backward ? posAfterEl : firstPosInEl;
  507. var endPos = backward ? firstPosInEl : posAfterEl;
  508. /*
  509. <body><p>X </p><p>Y</p></body>
  510. Positions:
  511. body:0:""
  512. p:0:""
  513. text:0:""
  514. text:1:"X"
  515. text:2:TRAILING_SPACE_IN_BLOCK
  516. text:3:COLLAPSED_SPACE
  517. p:1:""
  518. body:1:"\n"
  519. p:0:""
  520. text:0:""
  521. text:1:"Y"
  522. A character is a TRAILING_SPACE_IN_BLOCK iff:
  523. - There is no uncollapsed character after it within the visible containing block element
  524. A character is a TRAILING_SPACE_BEFORE_BR iff:
  525. - There is no uncollapsed character after it preceding a <br> element
  526. An element has inner text iff
  527. - It is not hidden
  528. - It contains an uncollapsed character
  529. All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render.
  530. */
  531. while (pos !== endPos) {
  532. pos.prepopulateChar();
  533. if (pos.isDefinitelyNonEmpty()) {
  534. return true;
  535. }
  536. pos = backward ? pos.previousVisible() : pos.nextVisible();
  537. }
  538. return false;
  539. }, "node"),
  540. isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) {
  541. // Ensure that a block element containing a <br> is considered to have inner text
  542. var brs = el.getElementsByTagName("br");
  543. for (var i = 0, len = brs.length; i < len; ++i) {
  544. if (!isCollapsedNode(brs[i])) {
  545. return true;
  546. }
  547. }
  548. return this.hasInnerText();
  549. }, "node"),
  550. getTrailingSpace: createCachingGetter("trailingSpace", function(el) {
  551. if (el.tagName.toLowerCase() == "br") {
  552. return "";
  553. } else {
  554. switch (this.getComputedDisplay()) {
  555. case "inline":
  556. var child = el.lastChild;
  557. while (child) {
  558. if (!isIgnoredNode(child)) {
  559. return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : "";
  560. }
  561. child = child.previousSibling;
  562. }
  563. break;
  564. case "inline-block":
  565. case "inline-table":
  566. case "none":
  567. case "table-column":
  568. case "table-column-group":
  569. break;
  570. case "table-cell":
  571. return "\t";
  572. default:
  573. return this.isRenderedBlock(true) ? "\n" : "";
  574. }
  575. }
  576. return "";
  577. }, "node"),
  578. getLeadingSpace: createCachingGetter("leadingSpace", function(el) {
  579. switch (this.getComputedDisplay()) {
  580. case "inline":
  581. case "inline-block":
  582. case "inline-table":
  583. case "none":
  584. case "table-column":
  585. case "table-column-group":
  586. case "table-cell":
  587. break;
  588. default:
  589. return this.isRenderedBlock(false) ? "\n" : "";
  590. }
  591. return "";
  592. }, "node")
  593. });
  594. /*----------------------------------------------------------------------------------------------------------------*/
  595. function Position(nodeWrapper, offset) {
  596. this.offset = offset;
  597. this.nodeWrapper = nodeWrapper;
  598. this.node = nodeWrapper.node;
  599. this.session = nodeWrapper.session;
  600. this.cache = new Cache();
  601. }
  602. function inspectPosition() {
  603. return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]";
  604. }
  605. var positionProto = {
  606. character: "",
  607. characterType: EMPTY,
  608. isBr: false,
  609. /*
  610. This method:
  611. - Fully populates positions that have characters that can be determined independently of any other characters.
  612. - Populates most types of space positions with a provisional character. The character is finalized later.
  613. */
  614. prepopulateChar: function() {
  615. var pos = this;
  616. if (!pos.prepopulatedChar) {
  617. var node = pos.node, offset = pos.offset;
  618. var visibleChar = "", charType = EMPTY;
  619. var finalizedChar = false;
  620. if (offset > 0) {
  621. if (node.nodeType == 3) {
  622. var text = node.data;
  623. var textChar = text.charAt(offset - 1);
  624. var nodeInfo = pos.nodeWrapper.getTextNodeInfo();
  625. var spaceRegex = nodeInfo.spaceRegex;
  626. if (nodeInfo.collapseSpaces) {
  627. if (spaceRegex.test(textChar)) {
  628. // "If the character at position is from set, append a single space (U+0020) to newdata and advance
  629. // position until the character at position is not from set."
  630. // We also need to check for the case where we're in a pre-line and we have a space preceding a
  631. // line break, because such spaces are collapsed in some browsers
  632. if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) {
  633. } else if (nodeInfo.preLine && text.charAt(offset) === "\n") {
  634. visibleChar = " ";
  635. charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK;
  636. } else {
  637. visibleChar = " ";
  638. //pos.checkForFollowingLineBreak = true;
  639. charType = COLLAPSIBLE_SPACE;
  640. }
  641. } else {
  642. visibleChar = textChar;
  643. charType = NON_SPACE;
  644. finalizedChar = true;
  645. }
  646. } else {
  647. visibleChar = textChar;
  648. charType = UNCOLLAPSIBLE_SPACE;
  649. finalizedChar = true;
  650. }
  651. } else {
  652. var nodePassed = node.childNodes[offset - 1];
  653. if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) {
  654. if (nodePassed.tagName.toLowerCase() == "br") {
  655. visibleChar = "\n";
  656. pos.isBr = true;
  657. charType = COLLAPSIBLE_SPACE;
  658. finalizedChar = false;
  659. } else {
  660. pos.checkForTrailingSpace = true;
  661. }
  662. }
  663. // Check the leading space of the next node for the case when a block element follows an inline
  664. // element or text node. In that case, there is an implied line break between the two nodes.
  665. if (!visibleChar) {
  666. var nextNode = node.childNodes[offset];
  667. if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) {
  668. pos.checkForLeadingSpace = true;
  669. }
  670. }
  671. }
  672. }
  673. pos.prepopulatedChar = true;
  674. pos.character = visibleChar;
  675. pos.characterType = charType;
  676. pos.isCharInvariant = finalizedChar;
  677. }
  678. },
  679. isDefinitelyNonEmpty: function() {
  680. var charType = this.characterType;
  681. return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE;
  682. },
  683. // Resolve leading and trailing spaces, which may involve prepopulating other positions
  684. resolveLeadingAndTrailingSpaces: function() {
  685. if (!this.prepopulatedChar) {
  686. this.prepopulateChar();
  687. }
  688. if (this.checkForTrailingSpace) {
  689. var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace();
  690. if (trailingSpace) {
  691. this.isTrailingSpace = true;
  692. this.character = trailingSpace;
  693. this.characterType = COLLAPSIBLE_SPACE;
  694. }
  695. this.checkForTrailingSpace = false;
  696. }
  697. if (this.checkForLeadingSpace) {
  698. var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace();
  699. if (leadingSpace) {
  700. this.isLeadingSpace = true;
  701. this.character = leadingSpace;
  702. this.characterType = COLLAPSIBLE_SPACE;
  703. }
  704. this.checkForLeadingSpace = false;
  705. }
  706. },
  707. getPrecedingUncollapsedPosition: function(characterOptions) {
  708. var pos = this, character;
  709. while ( (pos = pos.previousVisible()) ) {
  710. character = pos.getCharacter(characterOptions);
  711. if (character !== "") {
  712. return pos;
  713. }
  714. }
  715. return null;
  716. },
  717. getCharacter: function(characterOptions) {
  718. this.resolveLeadingAndTrailingSpaces();
  719. var thisChar = this.character, returnChar;
  720. // Check if character is ignored
  721. var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters);
  722. var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1);
  723. // Check if this position's character is invariant (i.e. not dependent on character options) and return it
  724. // if so
  725. if (this.isCharInvariant) {
  726. returnChar = isIgnoredCharacter ? "" : thisChar;
  727. return returnChar;
  728. }
  729. var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_");
  730. var cachedChar = this.cache.get(cacheKey);
  731. if (cachedChar !== null) {
  732. return cachedChar;
  733. }
  734. // We need to actually get the character now
  735. var character = "";
  736. var collapsible = (this.characterType == COLLAPSIBLE_SPACE);
  737. var nextPos, previousPos;
  738. var gotPreviousPos = false;
  739. var pos = this;
  740. function getPreviousPos() {
  741. if (!gotPreviousPos) {
  742. previousPos = pos.getPrecedingUncollapsedPosition(characterOptions);
  743. gotPreviousPos = true;
  744. }
  745. return previousPos;
  746. }
  747. // Disallow a collapsible space that is followed by a line break or is the last character
  748. if (collapsible) {
  749. // Allow a trailing space that we've previously determined should be included
  750. if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) {
  751. character = "\n";
  752. }
  753. // Disallow a collapsible space that follows a trailing space or line break, or is the first character,
  754. // or follows a collapsible included space
  755. else if (thisChar == " " &&
  756. (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) {
  757. }
  758. // Allow a leading line break unless it follows a line break
  759. else if (thisChar == "\n" && this.isLeadingSpace) {
  760. if (getPreviousPos() && previousPos.character != "\n") {
  761. character = "\n";
  762. } else {
  763. }
  764. } else {
  765. nextPos = this.nextUncollapsed();
  766. if (nextPos) {
  767. if (nextPos.isBr) {
  768. this.type = TRAILING_SPACE_BEFORE_BR;
  769. } else if (nextPos.isTrailingSpace && nextPos.character == "\n") {
  770. this.type = TRAILING_SPACE_IN_BLOCK;
  771. } else if (nextPos.isLeadingSpace && nextPos.character == "\n") {
  772. this.type = TRAILING_SPACE_BEFORE_BLOCK;
  773. }
  774. if (nextPos.character == "\n") {
  775. if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) {
  776. } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) {
  777. } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) {
  778. } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) {
  779. } else if (thisChar == "\n") {
  780. if (nextPos.isTrailingSpace) {
  781. if (this.isTrailingSpace) {
  782. } else if (this.isBr) {
  783. nextPos.type = TRAILING_LINE_BREAK_AFTER_BR;
  784. if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") {
  785. nextPos.character = "";
  786. } else {
  787. nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR;
  788. }
  789. }
  790. } else {
  791. character = "\n";
  792. }
  793. } else if (thisChar == " ") {
  794. character = " ";
  795. } else {
  796. }
  797. } else {
  798. character = thisChar;
  799. }
  800. } else {
  801. }
  802. }
  803. }
  804. if (ignoredChars.indexOf(character) > -1) {
  805. character = "";
  806. }
  807. this.cache.set(cacheKey, character);
  808. return character;
  809. },
  810. equals: function(pos) {
  811. return !!pos && this.node === pos.node && this.offset === pos.offset;
  812. },
  813. inspect: inspectPosition,
  814. toString: function() {
  815. return this.character;
  816. }
  817. };
  818. Position.prototype = positionProto;
  819. extend(positionProto, {
  820. next: createCachingGetter("nextPos", function(pos) {
  821. var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
  822. if (!node) {
  823. return null;
  824. }
  825. var nextNode, nextOffset, child;
  826. if (offset == nodeWrapper.getLength()) {
  827. // Move onto the next node
  828. nextNode = node.parentNode;
  829. nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0;
  830. } else {
  831. if (nodeWrapper.isCharacterDataNode()) {
  832. nextNode = node;
  833. nextOffset = offset + 1;
  834. } else {
  835. child = node.childNodes[offset];
  836. // Go into the children next, if children there are
  837. if (session.getNodeWrapper(child).containsPositions()) {
  838. nextNode = child;
  839. nextOffset = 0;
  840. } else {
  841. nextNode = node;
  842. nextOffset = offset + 1;
  843. }
  844. }
  845. }
  846. return nextNode ? session.getPosition(nextNode, nextOffset) : null;
  847. }),
  848. previous: createCachingGetter("previous", function(pos) {
  849. var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
  850. var previousNode, previousOffset, child;
  851. if (offset == 0) {
  852. previousNode = node.parentNode;
  853. previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0;
  854. } else {
  855. if (nodeWrapper.isCharacterDataNode()) {
  856. previousNode = node;
  857. previousOffset = offset - 1;
  858. } else {
  859. child = node.childNodes[offset - 1];
  860. // Go into the children next, if children there are
  861. if (session.getNodeWrapper(child).containsPositions()) {
  862. previousNode = child;
  863. previousOffset = dom.getNodeLength(child);
  864. } else {
  865. previousNode = node;
  866. previousOffset = offset - 1;
  867. }
  868. }
  869. }
  870. return previousNode ? session.getPosition(previousNode, previousOffset) : null;
  871. }),
  872. /*
  873. Next and previous position moving functions that filter out
  874. - Hidden (CSS visibility/display) elements
  875. - Script and style elements
  876. */
  877. nextVisible: createCachingGetter("nextVisible", function(pos) {
  878. var next = pos.next();
  879. if (!next) {
  880. return null;
  881. }
  882. var nodeWrapper = next.nodeWrapper, node = next.node;
  883. var newPos = next;
  884. if (nodeWrapper.isCollapsed()) {
  885. // We're skipping this node and all its descendants
  886. newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1);
  887. }
  888. return newPos;
  889. }),
  890. nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) {
  891. var nextPos = pos;
  892. while ( (nextPos = nextPos.nextVisible()) ) {
  893. nextPos.resolveLeadingAndTrailingSpaces();
  894. if (nextPos.character !== "") {
  895. return nextPos;
  896. }
  897. }
  898. return null;
  899. }),
  900. previousVisible: createCachingGetter("previousVisible", function(pos) {
  901. var previous = pos.previous();
  902. if (!previous) {
  903. return null;
  904. }
  905. var nodeWrapper = previous.nodeWrapper, node = previous.node;
  906. var newPos = previous;
  907. if (nodeWrapper.isCollapsed()) {
  908. // We're skipping this node and all its descendants
  909. newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex());
  910. }
  911. return newPos;
  912. })
  913. });
  914. /*----------------------------------------------------------------------------------------------------------------*/
  915. var currentSession = null;
  916. var Session = (function() {
  917. function createWrapperCache(nodeProperty) {
  918. var cache = new Cache();
  919. return {
  920. get: function(node) {
  921. var wrappersByProperty = cache.get(node[nodeProperty]);
  922. if (wrappersByProperty) {
  923. for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) {
  924. if (wrapper.node === node) {
  925. return wrapper;
  926. }
  927. }
  928. }
  929. return null;
  930. },
  931. set: function(nodeWrapper) {
  932. var property = nodeWrapper.node[nodeProperty];
  933. var wrappersByProperty = cache.get(property) || cache.set(property, []);
  934. wrappersByProperty.push(nodeWrapper);
  935. }
  936. };
  937. }
  938. var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID");
  939. function Session() {
  940. this.initCaches();
  941. }
  942. Session.prototype = {
  943. initCaches: function() {
  944. this.elementCache = uniqueIDSupported ? (function() {
  945. var elementsCache = new Cache();
  946. return {
  947. get: function(el) {
  948. return elementsCache.get(el.uniqueID);
  949. },
  950. set: function(elWrapper) {
  951. elementsCache.set(elWrapper.node.uniqueID, elWrapper);
  952. }
  953. };
  954. })() : createWrapperCache("tagName");
  955. // Store text nodes keyed by data, although we may need to truncate this
  956. this.textNodeCache = createWrapperCache("data");
  957. this.otherNodeCache = createWrapperCache("nodeName");
  958. },
  959. getNodeWrapper: function(node) {
  960. var wrapperCache;
  961. switch (node.nodeType) {
  962. case 1:
  963. wrapperCache = this.elementCache;
  964. break;
  965. case 3:
  966. wrapperCache = this.textNodeCache;
  967. break;
  968. default:
  969. wrapperCache = this.otherNodeCache;
  970. break;
  971. }
  972. var wrapper = wrapperCache.get(node);
  973. if (!wrapper) {
  974. wrapper = new NodeWrapper(node, this);
  975. wrapperCache.set(wrapper);
  976. }
  977. return wrapper;
  978. },
  979. getPosition: function(node, offset) {
  980. return this.getNodeWrapper(node).getPosition(offset);
  981. },
  982. getRangeBoundaryPosition: function(range, isStart) {
  983. var prefix = isStart ? "start" : "end";
  984. return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]);
  985. },
  986. detach: function() {
  987. this.elementCache = this.textNodeCache = this.otherNodeCache = null;
  988. }
  989. };
  990. return Session;
  991. })();
  992. /*----------------------------------------------------------------------------------------------------------------*/
  993. function startSession() {
  994. endSession();
  995. return (currentSession = new Session());
  996. }
  997. function getSession() {
  998. return currentSession || startSession();
  999. }
  1000. function endSession() {
  1001. if (currentSession) {
  1002. currentSession.detach();
  1003. }
  1004. currentSession = null;
  1005. }
  1006. /*----------------------------------------------------------------------------------------------------------------*/
  1007. // Extensions to the rangy.dom utility object
  1008. extend(dom, {
  1009. nextNode: nextNode,
  1010. previousNode: previousNode
  1011. });
  1012. /*----------------------------------------------------------------------------------------------------------------*/
  1013. function createCharacterIterator(startPos, backward, endPos, characterOptions) {
  1014. // Adjust the end position to ensure that it is actually reached
  1015. if (endPos) {
  1016. if (backward) {
  1017. if (isCollapsedNode(endPos.node)) {
  1018. endPos = startPos.previousVisible();
  1019. }
  1020. } else {
  1021. if (isCollapsedNode(endPos.node)) {
  1022. endPos = endPos.nextVisible();
  1023. }
  1024. }
  1025. }
  1026. var pos = startPos, finished = false;
  1027. function next() {
  1028. var charPos = null;
  1029. if (backward) {
  1030. charPos = pos;
  1031. if (!finished) {
  1032. pos = pos.previousVisible();
  1033. finished = !pos || (endPos && pos.equals(endPos));
  1034. }
  1035. } else {
  1036. if (!finished) {
  1037. charPos = pos = pos.nextVisible();
  1038. finished = !pos || (endPos && pos.equals(endPos));
  1039. }
  1040. }
  1041. if (finished) {
  1042. pos = null;
  1043. }
  1044. return charPos;
  1045. }
  1046. var previousTextPos, returnPreviousTextPos = false;
  1047. return {
  1048. next: function() {
  1049. if (returnPreviousTextPos) {
  1050. returnPreviousTextPos = false;
  1051. return previousTextPos;
  1052. } else {
  1053. var pos, character;
  1054. while ( (pos = next()) ) {
  1055. character = pos.getCharacter(characterOptions);
  1056. if (character) {
  1057. previousTextPos = pos;
  1058. return pos;
  1059. }
  1060. }
  1061. return null;
  1062. }
  1063. },
  1064. rewind: function() {
  1065. if (previousTextPos) {
  1066. returnPreviousTextPos = true;
  1067. } else {
  1068. throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound.");
  1069. }
  1070. },
  1071. dispose: function() {
  1072. startPos = endPos = null;
  1073. }
  1074. };
  1075. }
  1076. var arrayIndexOf = Array.prototype.indexOf ?
  1077. function(arr, val) {
  1078. return arr.indexOf(val);
  1079. } :
  1080. function(arr, val) {
  1081. for (var i = 0, len = arr.length; i < len; ++i) {
  1082. if (arr[i] === val) {
  1083. return i;
  1084. }
  1085. }
  1086. return -1;
  1087. };
  1088. // Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next()
  1089. // is called and there is no more tokenized text
  1090. function createTokenizedTextProvider(pos, characterOptions, wordOptions) {
  1091. var forwardIterator = createCharacterIterator(pos, false, null, characterOptions);
  1092. var backwardIterator = createCharacterIterator(pos, true, null, characterOptions);
  1093. var tokenizer = wordOptions.tokenizer;
  1094. // Consumes a word and the whitespace beyond it
  1095. function consumeWord(forward) {
  1096. var pos, textChar;
  1097. var newChars = [], it = forward ? forwardIterator : backwardIterator;
  1098. var passedWordBoundary = false, insideWord = false;
  1099. while ( (pos = it.next()) ) {
  1100. textChar = pos.character;
  1101. if (allWhiteSpaceRegex.test(textChar)) {
  1102. if (insideWord) {
  1103. insideWord = false;
  1104. passedWordBoundary = true;
  1105. }
  1106. } else {
  1107. if (passedWordBoundary) {
  1108. it.rewind();
  1109. break;
  1110. } else {
  1111. insideWord = true;
  1112. }
  1113. }
  1114. newChars.push(pos);
  1115. }
  1116. return newChars;
  1117. }
  1118. // Get initial word surrounding initial position and tokenize it
  1119. var forwardChars = consumeWord(true);
  1120. var backwardChars = consumeWord(false).reverse();
  1121. var tokens = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer);
  1122. // Create initial token buffers
  1123. var forwardTokensBuffer = forwardChars.length ?
  1124. tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : [];
  1125. var backwardTokensBuffer = backwardChars.length ?
  1126. tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : [];
  1127. function inspectBuffer(buffer) {
  1128. var textPositions = ["[" + buffer.length + "]"];
  1129. for (var i = 0; i < buffer.length; ++i) {
  1130. textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")");
  1131. }
  1132. return textPositions;
  1133. }
  1134. return {
  1135. nextEndToken: function() {
  1136. var lastToken, forwardChars;
  1137. // If we're down to the last token, consume character chunks until we have a word or run out of
  1138. // characters to consume
  1139. while ( forwardTokensBuffer.length == 1 &&
  1140. !(lastToken = forwardTokensBuffer[0]).isWord &&
  1141. (forwardChars = consumeWord(true)).length > 0) {
  1142. // Merge trailing non-word into next word and tokenize
  1143. forwardTokensBuffer = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer);
  1144. }
  1145. return forwardTokensBuffer.shift();
  1146. },
  1147. previousStartToken: function() {
  1148. var lastToken, backwardChars;
  1149. // If we're down to the last token, consume character chunks until we have a word or run out of
  1150. // characters to consume
  1151. while ( backwardTokensBuffer.length == 1 &&
  1152. !(lastToken = backwardTokensBuffer[0]).isWord &&
  1153. (backwardChars = consumeWord(false)).length > 0) {
  1154. // Merge leading non-word into next word and tokenize
  1155. backwardTokensBuffer = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer);
  1156. }
  1157. return backwardTokensBuffer.pop();
  1158. },
  1159. dispose: function() {
  1160. forwardIterator.dispose();
  1161. backwardIterator.dispose();
  1162. forwardTokensBuffer = backwardTokensBuffer = null;
  1163. }
  1164. };
  1165. }
  1166. function movePositionBy(pos, unit, count, characterOptions, wordOptions) {
  1167. var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token;
  1168. if (count !== 0) {
  1169. var backward = (count < 0);
  1170. switch (unit) {
  1171. case CHARACTER:
  1172. charIterator = createCharacterIterator(pos, backward, null, characterOptions);
  1173. while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) {
  1174. ++unitsMoved;
  1175. newPos = currentPos;
  1176. }
  1177. nextPos = currentPos;
  1178. charIterator.dispose();
  1179. break;
  1180. case WORD:
  1181. var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions);
  1182. var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken;
  1183. while ( (token = next()) && unitsMoved < absCount ) {
  1184. if (token.isWord) {
  1185. ++unitsMoved;
  1186. newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1];
  1187. }
  1188. }
  1189. break;
  1190. default:
  1191. throw new Error("movePositionBy: unit '" + unit + "' not implemented");
  1192. }
  1193. // Perform any necessary position tweaks
  1194. if (backward) {
  1195. newPos = newPos.previousVisible();
  1196. unitsMoved = -unitsMoved;
  1197. } else if (newPos && newPos.isLeadingSpace && !newPos.isTrailingSpace) {
  1198. // Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space
  1199. // before a block element (for example, the line break between "1" and "2" in the following HTML:
  1200. // "1<p>2</p>") is considered to be attached to the position immediately before the block element, which
  1201. // corresponds with a different selection position in most browsers from the one we want (i.e. at the
  1202. // start of the contents of the block element). We get round this by advancing the position returned to
  1203. // the last possible equivalent visible position.
  1204. if (unit == WORD) {
  1205. charIterator = createCharacterIterator(pos, false, null, characterOptions);
  1206. nextPos = charIterator.next();
  1207. charIterator.dispose();
  1208. }
  1209. if (nextPos) {
  1210. newPos = nextPos.previousVisible();
  1211. }
  1212. }
  1213. }
  1214. return {
  1215. position: newPos,
  1216. unitsMoved: unitsMoved
  1217. };
  1218. }
  1219. function createRangeCharacterIterator(session, range, characterOptions, backward) {
  1220. var rangeStart = session.getRangeBoundaryPosition(range, true);
  1221. var rangeEnd = session.getRangeBoundaryPosition(range, false);
  1222. var itStart = backward ? rangeEnd : rangeStart;
  1223. var itEnd = backward ? rangeStart : rangeEnd;
  1224. return createCharacterIterator(itStart, !!backward, itEnd, characterOptions);
  1225. }
  1226. function getRangeCharacters(session, range, characterOptions) {
  1227. var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos;
  1228. while ( (pos = it.next()) ) {
  1229. chars.push(pos);
  1230. }
  1231. it.dispose();
  1232. return chars;
  1233. }
  1234. function isWholeWord(startPos, endPos, wordOptions) {
  1235. var range = api.createRange(startPos.node);
  1236. range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset);
  1237. return !range.expand("word", { wordOptions: wordOptions });
  1238. }
  1239. function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) {
  1240. var backward = isDirectionBackward(findOptions.direction);
  1241. var it = createCharacterIterator(
  1242. initialPos,
  1243. backward,
  1244. initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward),
  1245. findOptions.characterOptions
  1246. );
  1247. var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex;
  1248. var result, insideRegexMatch;
  1249. var returnValue = null;
  1250. function handleMatch(startIndex, endIndex) {
  1251. var startPos = chars[startIndex].previousVisible();
  1252. var endPos = chars[endIndex - 1];
  1253. var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions));
  1254. return {
  1255. startPos: startPos,
  1256. endPos: endPos,
  1257. valid: valid
  1258. };
  1259. }
  1260. while ( (pos = it.next()) ) {
  1261. currentChar = pos.character;
  1262. if (!isRegex && !findOptions.caseSensitive) {
  1263. currentChar = currentChar.toLowerCase();
  1264. }
  1265. if (backward) {
  1266. chars.unshift(pos);
  1267. text = currentChar + text;
  1268. } else {
  1269. chars.push(pos);
  1270. text += currentChar;
  1271. }
  1272. if (isRegex) {
  1273. result = searchTerm.exec(text);
  1274. if (result) {
  1275. matchStartIndex = result.index;
  1276. matchEndIndex = matchStartIndex + result[0].length;
  1277. if (insideRegexMatch) {
  1278. // Check whether the match is now over
  1279. if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) {
  1280. returnValue = handleMatch(matchStartIndex, matchEndIndex);
  1281. break;
  1282. }
  1283. } else {
  1284. insideRegexMatch = true;
  1285. }
  1286. }
  1287. } else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) {
  1288. returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length);
  1289. break;
  1290. }
  1291. }
  1292. // Check whether regex match extends to the end of the range
  1293. if (insideRegexMatch) {
  1294. returnValue = handleMatch(matchStartIndex, matchEndIndex);
  1295. }
  1296. it.dispose();
  1297. return returnValue;
  1298. }
  1299. function createEntryPointFunction(func) {
  1300. return function() {
  1301. var sessionRunning = !!currentSession;
  1302. var session = getSession();
  1303. var args = [session].concat( util.toArray(arguments) );
  1304. var returnValue = func.apply(this, args);
  1305. if (!sessionRunning) {
  1306. endSession();
  1307. }
  1308. return returnValue;
  1309. };
  1310. }
  1311. /*----------------------------------------------------------------------------------------------------------------*/
  1312. // Extensions to the Rangy Range object
  1313. function createRangeBoundaryMover(isStart, collapse) {
  1314. /*
  1315. Unit can be "character" or "word"
  1316. Options:
  1317. - includeTrailingSpace
  1318. - wordRegex
  1319. - tokenizer
  1320. - collapseSpaceBeforeLineBreak
  1321. */
  1322. return createEntryPointFunction(
  1323. function(session, unit, count, moveOptions) {
  1324. if (typeof count == UNDEF) {
  1325. count = unit;
  1326. unit = CHARACTER;
  1327. }
  1328. moveOptions = createNestedOptions(moveOptions, defaultMoveOptions);
  1329. var boundaryIsStart = isStart;
  1330. if (collapse) {
  1331. boundaryIsStart = (count >= 0);
  1332. this.collapse(!boundaryIsStart);
  1333. }
  1334. var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.wordOptions);
  1335. var newPos = moveResult.position;
  1336. this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset);
  1337. return moveResult.unitsMoved;
  1338. }
  1339. );
  1340. }
  1341. function createRangeTrimmer(isStart) {
  1342. return createEntryPointFunction(
  1343. function(session, characterOptions) {
  1344. characterOptions = createOptions(characterOptions, defaultCharacterOptions);
  1345. var pos;
  1346. var it = createRangeCharacterIterator(session, this, characterOptions, !isStart);
  1347. var trimCharCount = 0;
  1348. while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) {
  1349. ++trimCharCount;
  1350. }
  1351. it.dispose();
  1352. var trimmed = (trimCharCount > 0);
  1353. if (trimmed) {
  1354. this[isStart ? "moveStart" : "moveEnd"](
  1355. "character",
  1356. isStart ? trimCharCount : -trimCharCount,
  1357. { characterOptions: characterOptions }
  1358. );
  1359. }
  1360. return trimmed;
  1361. }
  1362. );
  1363. }
  1364. extend(api.rangePrototype, {
  1365. moveStart: createRangeBoundaryMover(true, false),
  1366. moveEnd: createRangeBoundaryMover(false, false),
  1367. move: createRangeBoundaryMover(true, true),
  1368. trimStart: createRangeTrimmer(true),
  1369. trimEnd: createRangeTrimmer(false),
  1370. trim: createEntryPointFunction(
  1371. function(session, characterOptions) {
  1372. var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions);
  1373. return startTrimmed || endTrimmed;
  1374. }
  1375. ),
  1376. expand: createEntryPointFunction(
  1377. function(session, unit, expandOptions) {
  1378. var moved = false;
  1379. expandOptions = createNestedOptions(expandOptions, defaultExpandOptions);
  1380. var characterOptions = expandOptions.characterOptions;
  1381. if (!unit) {
  1382. unit = CHARACTER;
  1383. }
  1384. if (unit == WORD) {
  1385. var wordOptions = expandOptions.wordOptions;
  1386. var startPos = session.getRangeBoundaryPosition(this, true);
  1387. var endPos = session.getRangeBoundaryPosition(this, false);
  1388. var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions);
  1389. var startToken = startTokenizedTextProvider.nextEndToken();
  1390. var newStartPos = startToken.chars[0].previousVisible();
  1391. var endToken, newEndPos;
  1392. if (this.collapsed) {
  1393. endToken = startToken;
  1394. } else {
  1395. var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions);
  1396. endToken = endTokenizedTextProvider.previousStartToken();
  1397. }
  1398. newEndPos = endToken.chars[endToken.chars.length - 1];
  1399. if (!newStartPos.equals(startPos)) {
  1400. this.setStart(newStartPos.node, newStartPos.offset);
  1401. moved = true;
  1402. }
  1403. if (newEndPos && !newEndPos.equals(endPos)) {
  1404. this.setEnd(newEndPos.node, newEndPos.offset);
  1405. moved = true;
  1406. }
  1407. if (expandOptions.trim) {
  1408. if (expandOptions.trimStart) {
  1409. moved = this.trimStart(characterOptions) || moved;
  1410. }
  1411. if (expandOptions.trimEnd) {
  1412. moved = this.trimEnd(characterOptions) || moved;
  1413. }
  1414. }
  1415. return moved;
  1416. } else {
  1417. return this.moveEnd(CHARACTER, 1, expandOptions);
  1418. }
  1419. }
  1420. ),
  1421. text: createEntryPointFunction(
  1422. function(session, characterOptions) {
  1423. return this.collapsed ?
  1424. "" : getRangeCharacters(session, this, createOptions(characterOptions, defaultCharacterOptions)).join("");
  1425. }
  1426. ),
  1427. selectCharacters: createEntryPointFunction(
  1428. function(session, containerNode, startIndex, endIndex, characterOptions) {
  1429. var moveOptions = { characterOptions: characterOptions };
  1430. if (!containerNode) {
  1431. containerNode = getBody( this.getDocument() );
  1432. }
  1433. this.selectNodeContents(containerNode);
  1434. this.collapse(true);
  1435. this.moveStart("character", startIndex, moveOptions);
  1436. this.collapse(true);
  1437. this.moveEnd("character", endIndex - startIndex, moveOptions);
  1438. }
  1439. ),
  1440. // Character indexes are relative to the start of node
  1441. toCharacterRange: createEntryPointFunction(
  1442. function(session, containerNode, characterOptions) {
  1443. if (!containerNode) {
  1444. containerNode = getBody( this.getDocument() );
  1445. }
  1446. var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode);
  1447. var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1);
  1448. var rangeBetween = this.cloneRange();
  1449. var startIndex, endIndex;
  1450. if (rangeStartsBeforeNode) {
  1451. rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex);
  1452. startIndex = -rangeBetween.text(characterOptions).length;
  1453. } else {
  1454. rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset);
  1455. startIndex = rangeBetween.text(characterOptions).length;
  1456. }
  1457. endIndex = startIndex + this.text(characterOptions).length;
  1458. return {
  1459. start: startIndex,
  1460. end: endIndex
  1461. };
  1462. }
  1463. ),
  1464. findText: createEntryPointFunction(
  1465. function(session, searchTermParam, findOptions) {
  1466. // Set up options
  1467. findOptions = createNestedOptions(findOptions, defaultFindOptions);
  1468. // Create word options if we're matching whole words only
  1469. if (findOptions.wholeWordsOnly) {
  1470. // We don't ever want trailing spaces for search results
  1471. findOptions.wordOptions.includeTrailingSpace = false;
  1472. }
  1473. var backward = isDirectionBackward(findOptions.direction);
  1474. // Create a range representing the search scope if none was provided
  1475. var searchScopeRange = findOptions.withinRange;
  1476. if (!searchScopeRange) {
  1477. searchScopeRange = api.createRange();
  1478. searchScopeRange.selectNodeContents(this.getDocument());
  1479. }
  1480. // Examine and prepare the search term
  1481. var searchTerm = searchTermParam, isRegex = false;
  1482. if (typeof searchTerm == "string") {
  1483. if (!findOptions.caseSensitive) {
  1484. searchTerm = searchTerm.toLowerCase();
  1485. }
  1486. } else {
  1487. isRegex = true;
  1488. }
  1489. var initialPos = session.getRangeBoundaryPosition(this, !backward);
  1490. // Adjust initial position if it lies outside the search scope
  1491. var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset);
  1492. if (comparison === -1) {
  1493. initialPos = session.getRangeBoundaryPosition(searchScopeRange, true);
  1494. } else if (comparison === 1) {
  1495. initialPos = session.getRangeBoundaryPosition(searchScopeRange, false);
  1496. }
  1497. var pos = initialPos;
  1498. var wrappedAround = false;
  1499. // Try to find a match and ignore invalid ones
  1500. var findResult;
  1501. while (true) {
  1502. findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions);
  1503. if (findResult) {
  1504. if (findResult.valid) {
  1505. this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset);
  1506. return true;
  1507. } else {
  1508. // We've found a match that is not a whole word, so we carry on searching from the point immediately
  1509. // after the match
  1510. pos = backward ? findResult.startPos : findResult.endPos;
  1511. }
  1512. } else if (findOptions.wrap && !wrappedAround) {
  1513. // No result found but we're wrapping around and limiting the scope to the unsearched part of the range
  1514. searchScopeRange = searchScopeRange.cloneRange();
  1515. pos = session.getRangeBoundaryPosition(searchScopeRange, !backward);
  1516. searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward);
  1517. wrappedAround = true;
  1518. } else {
  1519. // Nothing found and we can't wrap around, so we're done
  1520. return false;
  1521. }
  1522. }
  1523. }
  1524. ),
  1525. pasteHtml: function(html) {
  1526. this.deleteContents();
  1527. if (html) {
  1528. var frag = this.createContextualFragment(html);
  1529. var lastChild = frag.lastChild;
  1530. this.insertNode(frag);
  1531. this.collapseAfter(lastChild);
  1532. }
  1533. }
  1534. });
  1535. /*----------------------------------------------------------------------------------------------------------------*/
  1536. // Extensions to the Rangy Selection object
  1537. function createSelectionTrimmer(methodName) {
  1538. return createEntryPointFunction(
  1539. function(session, characterOptions) {
  1540. var trimmed = false;
  1541. this.changeEachRange(function(range) {
  1542. trimmed = range[methodName](characterOptions) || trimmed;
  1543. });
  1544. return trimmed;
  1545. }
  1546. );
  1547. }
  1548. extend(api.selectionPrototype, {
  1549. expand: createEntryPointFunction(
  1550. function(session, unit, expandOptions) {
  1551. this.changeEachRange(function(range) {
  1552. range.expand(unit, expandOptions);
  1553. });
  1554. }
  1555. ),
  1556. move: createEntryPointFunction(
  1557. function(session, unit, count, options) {
  1558. var unitsMoved = 0;
  1559. if (this.focusNode) {
  1560. this.collapse(this.focusNode, this.focusOffset);
  1561. var range = this.getRangeAt(0);
  1562. if (!options) {
  1563. options = {};
  1564. }
  1565. options.characterOptions = createOptions(options.characterOptions, defaultCaretCharacterOptions);
  1566. unitsMoved = range.move(unit, count, options);
  1567. this.setSingleRange(range);
  1568. }
  1569. return unitsMoved;
  1570. }
  1571. ),
  1572. trimStart: createSelectionTrimmer("trimStart"),
  1573. trimEnd: createSelectionTrimmer("trimEnd"),
  1574. trim: createSelectionTrimmer("trim"),
  1575. selectCharacters: createEntryPointFunction(
  1576. function(session, containerNode, startIndex, endIndex, direction, characterOptions) {
  1577. var range = api.createRange(containerNode);
  1578. range.selectCharacters(containerNode, startIndex, endIndex, characterOptions);
  1579. this.setSingleRange(range, direction);
  1580. }
  1581. ),
  1582. saveCharacterRanges: createEntryPointFunction(
  1583. function(session, containerNode, characterOptions) {
  1584. var ranges = this.getAllRanges(), rangeCount = ranges.length;
  1585. var rangeInfos = [];
  1586. var backward = rangeCount == 1 && this.isBackward();
  1587. for (var i = 0, len = ranges.length; i < len; ++i) {
  1588. rangeInfos[i] = {
  1589. characterRange: ranges[i].toCharacterRange(containerNode, characterOptions),
  1590. backward: backward,
  1591. characterOptions: characterOptions
  1592. };
  1593. }
  1594. return rangeInfos;
  1595. }
  1596. ),
  1597. restoreCharacterRanges: createEntryPointFunction(
  1598. function(session, containerNode, saved) {
  1599. this.removeAllRanges();
  1600. for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) {
  1601. rangeInfo = saved[i];
  1602. characterRange = rangeInfo.characterRange;
  1603. range = api.createRange(containerNode);
  1604. range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions);
  1605. this.addRange(range, rangeInfo.backward);
  1606. }
  1607. }
  1608. ),
  1609. text: createEntryPointFunction(
  1610. function(session, characterOptions) {
  1611. var rangeTexts = [];
  1612. for (var i = 0, len = this.rangeCount; i < len; ++i) {
  1613. rangeTexts[i] = this.getRangeAt(i).text(characterOptions);
  1614. }
  1615. return rangeTexts.join("");
  1616. }
  1617. )
  1618. });
  1619. /*----------------------------------------------------------------------------------------------------------------*/
  1620. // Extensions to the core rangy object
  1621. api.innerText = function(el, characterOptions) {
  1622. var range = api.createRange(el);
  1623. range.selectNodeContents(el);
  1624. var text = range.text(characterOptions);
  1625. return text;
  1626. };
  1627. api.createWordIterator = function(startNode, startOffset, iteratorOptions) {
  1628. var session = getSession();
  1629. iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions);
  1630. var startPos = session.getPosition(startNode, startOffset);
  1631. var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.wordOptions);
  1632. var backward = isDirectionBackward(iteratorOptions.direction);
  1633. return {
  1634. next: function() {
  1635. return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken();
  1636. },
  1637. dispose: function() {
  1638. tokenizedTextProvider.dispose();
  1639. this.next = function() {};
  1640. }
  1641. };
  1642. };
  1643. /*----------------------------------------------------------------------------------------------------------------*/
  1644. api.noMutation = function(func) {
  1645. var session = getSession();
  1646. func(session);
  1647. endSession();
  1648. };
  1649. api.noMutation.createEntryPointFunction = createEntryPointFunction;
  1650. api.textRange = {
  1651. isBlockNode: isBlockNode,
  1652. isCollapsedWhitespaceNode: isCollapsedWhitespaceNode,
  1653. createPosition: createEntryPointFunction(
  1654. function(session, node, offset) {
  1655. return session.getPosition(node, offset);
  1656. }
  1657. )
  1658. };
  1659. });
  1660. return rangy;
  1661. }, this);