rangy-selectionsaverestore.js 7.9 KB

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