rangy-textrange.js 73 KB

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