rangy-selectionsaverestore.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. /**
  2. * Selection save and restore module for Rangy.
  3. * Saves and restores user selections using marker invisible elements in the DOM.
  4. *
  5. * Part of Rangy, a cross-browser JavaScript range and selection library
  6. * https://github.com/timdown/rangy
  7. *
  8. * Depends on Rangy core.
  9. *
  10. * Copyright 2015, Tim Down
  11. * Licensed under the MIT license.
  12. * Version: 1.3.0
  13. * Build date: 10 May 2015
  14. */
  15. (function(factory, root) {
  16. if (typeof define == "function" && define.amd) {
  17. // AMD. Register as an anonymous module with a dependency on Rangy.
  18. define(["./rangy-core"], factory);
  19. } else if (typeof module != "undefined" && typeof exports == "object") {
  20. // Node/CommonJS style
  21. module.exports = factory( require("rangy") );
  22. } else {
  23. // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
  24. factory(root.rangy);
  25. }
  26. })(function(rangy) {
  27. rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) {
  28. var dom = api.dom;
  29. var removeNode = dom.removeNode;
  30. var isDirectionBackward = api.Selection.isDirectionBackward;
  31. var markerTextChar = "\ufeff";
  32. function gEBI(id, doc) {
  33. return (doc || document).getElementById(id);
  34. }
  35. function insertRangeBoundaryMarker(range, atStart) {
  36. var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
  37. var markerEl;
  38. var doc = dom.getDocument(range.startContainer);
  39. // Clone the Range and collapse to the appropriate boundary point
  40. var boundaryRange = range.cloneRange();
  41. boundaryRange.collapse(atStart);
  42. // Create the marker element containing a single invisible character using DOM methods and insert it
  43. markerEl = doc.createElement("span");
  44. markerEl.id = markerId;
  45. markerEl.style.lineHeight = "0";
  46. markerEl.style.display = "none";
  47. markerEl.className = "rangySelectionBoundary";
  48. markerEl.appendChild(doc.createTextNode(markerTextChar));
  49. boundaryRange.insertNode(markerEl);
  50. return markerEl;
  51. }
  52. function setRangeBoundary(doc, range, markerId, atStart) {
  53. var markerEl = gEBI(markerId, doc);
  54. if (markerEl) {
  55. range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
  56. removeNode(markerEl);
  57. } else {
  58. module.warn("Marker element has been removed. Cannot restore selection.");
  59. }
  60. }
  61. function compareRanges(r1, r2) {
  62. return r2.compareBoundaryPoints(r1.START_TO_START, r1);
  63. }
  64. function saveRange(range, direction) {
  65. var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
  66. var backward = isDirectionBackward(direction);
  67. if (range.collapsed) {
  68. endEl = insertRangeBoundaryMarker(range, false);
  69. return {
  70. document: doc,
  71. markerId: endEl.id,
  72. collapsed: true
  73. };
  74. } else {
  75. endEl = insertRangeBoundaryMarker(range, false);
  76. startEl = insertRangeBoundaryMarker(range, true);
  77. return {
  78. document: doc,
  79. startMarkerId: startEl.id,
  80. endMarkerId: endEl.id,
  81. collapsed: false,
  82. backward: backward,
  83. toString: function() {
  84. return "original text: '" + text + "', new text: '" + range.toString() + "'";
  85. }
  86. };
  87. }
  88. }
  89. function restoreRange(rangeInfo, normalize) {
  90. var doc = rangeInfo.document;
  91. if (typeof normalize == "undefined") {
  92. normalize = true;
  93. }
  94. var range = api.createRange(doc);
  95. if (rangeInfo.collapsed) {
  96. var markerEl = gEBI(rangeInfo.markerId, doc);
  97. if (markerEl) {
  98. markerEl.style.display = "inline";
  99. var previousNode = markerEl.previousSibling;
  100. // Workaround for issue 17
  101. if (previousNode && previousNode.nodeType == 3) {
  102. removeNode(markerEl);
  103. range.collapseToPoint(previousNode, previousNode.length);
  104. } else {
  105. range.collapseBefore(markerEl);
  106. removeNode(markerEl);
  107. }
  108. } else {
  109. module.warn("Marker element has been removed. Cannot restore selection.");
  110. }
  111. } else {
  112. setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
  113. setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
  114. }
  115. if (normalize) {
  116. range.normalizeBoundaries();
  117. }
  118. return range;
  119. }
  120. function saveRanges(ranges, direction) {
  121. var rangeInfos = [], range, doc;
  122. var backward = isDirectionBackward(direction);
  123. // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
  124. ranges = ranges.slice(0);
  125. ranges.sort(compareRanges);
  126. for (var i = 0, len = ranges.length; i < len; ++i) {
  127. rangeInfos[i] = saveRange(ranges[i], backward);
  128. }
  129. // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
  130. // between its markers
  131. for (i = len - 1; i >= 0; --i) {
  132. range = ranges[i];
  133. doc = api.DomRange.getRangeDocument(range);
  134. if (range.collapsed) {
  135. range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
  136. } else {
  137. range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
  138. range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
  139. }
  140. }
  141. return rangeInfos;
  142. }
  143. function saveSelection(win) {
  144. if (!api.isSelectionValid(win)) {
  145. module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
  146. return null;
  147. }
  148. var sel = api.getSelection(win);
  149. var ranges = sel.getAllRanges();
  150. var backward = (ranges.length == 1 && sel.isBackward());
  151. var rangeInfos = saveRanges(ranges, backward);
  152. // Ensure current selection is unaffected
  153. if (backward) {
  154. sel.setSingleRange(ranges[0], backward);
  155. } else {
  156. sel.setRanges(ranges);
  157. }
  158. return {
  159. win: win,
  160. rangeInfos: rangeInfos,
  161. restored: false
  162. };
  163. }
  164. function restoreRanges(rangeInfos) {
  165. var ranges = [];
  166. // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
  167. // normalization affecting previously restored ranges.
  168. var rangeCount = rangeInfos.length;
  169. for (var i = rangeCount - 1; i >= 0; i--) {
  170. ranges[i] = restoreRange(rangeInfos[i], true);
  171. }
  172. return ranges;
  173. }
  174. function restoreSelection(savedSelection, preserveDirection) {
  175. if (!savedSelection.restored) {
  176. var rangeInfos = savedSelection.rangeInfos;
  177. var sel = api.getSelection(savedSelection.win);
  178. var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
  179. if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
  180. sel.removeAllRanges();
  181. sel.addRange(ranges[0], true);
  182. } else {
  183. sel.setRanges(ranges);
  184. }
  185. savedSelection.restored = true;
  186. }
  187. }
  188. function removeMarkerElement(doc, markerId) {
  189. var markerEl = gEBI(markerId, doc);
  190. if (markerEl) {
  191. removeNode(markerEl);
  192. }
  193. }
  194. function removeMarkers(savedSelection) {
  195. var rangeInfos = savedSelection.rangeInfos;
  196. for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
  197. rangeInfo = rangeInfos[i];
  198. if (rangeInfo.collapsed) {
  199. removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
  200. } else {
  201. removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
  202. removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
  203. }
  204. }
  205. }
  206. api.util.extend(api, {
  207. saveRange: saveRange,
  208. restoreRange: restoreRange,
  209. saveRanges: saveRanges,
  210. restoreRanges: restoreRanges,
  211. saveSelection: saveSelection,
  212. restoreSelection: restoreSelection,
  213. removeMarkerElement: removeMarkerElement,
  214. removeMarkers: removeMarkers
  215. });
  216. });
  217. return rangy;
  218. }, this);