rangy-serializer.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. /**
  2. * Serializer module for Rangy.
  3. * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
  4. * cookie or local storage and restore it on the user's next visit to the same page.
  5. *
  6. * Part of Rangy, a cross-browser JavaScript range and selection library
  7. * https://github.com/timdown/rangy
  8. *
  9. * Depends on Rangy core.
  10. *
  11. * Copyright 2015, Tim Down
  12. * Licensed under the MIT license.
  13. * Version: 1.3.0
  14. * Build date: 10 May 2015
  15. */
  16. (function(factory, root) {
  17. if (typeof define == "function" && define.amd) {
  18. // AMD. Register as an anonymous module with a dependency on Rangy.
  19. define(["./rangy-core"], factory);
  20. } else if (typeof module != "undefined" && typeof exports == "object") {
  21. // Node/CommonJS style
  22. module.exports = factory( require("rangy") );
  23. } else {
  24. // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
  25. factory(root.rangy);
  26. }
  27. })(function(rangy) {
  28. rangy.createModule("Serializer", ["WrappedSelection"], function(api, module) {
  29. var UNDEF = "undefined";
  30. var util = api.util;
  31. // encodeURIComponent and decodeURIComponent are required for cookie handling
  32. if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
  33. module.fail("encodeURIComponent and/or decodeURIComponent method is missing");
  34. }
  35. // Checksum for checking whether range can be serialized
  36. var crc32 = (function() {
  37. function utf8encode(str) {
  38. var utf8CharCodes = [];
  39. for (var i = 0, len = str.length, c; i < len; ++i) {
  40. c = str.charCodeAt(i);
  41. if (c < 128) {
  42. utf8CharCodes.push(c);
  43. } else if (c < 2048) {
  44. utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
  45. } else {
  46. utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
  47. }
  48. }
  49. return utf8CharCodes;
  50. }
  51. var cachedCrcTable = null;
  52. function buildCRCTable() {
  53. var table = [];
  54. for (var i = 0, j, crc; i < 256; ++i) {
  55. crc = i;
  56. j = 8;
  57. while (j--) {
  58. if ((crc & 1) == 1) {
  59. crc = (crc >>> 1) ^ 0xEDB88320;
  60. } else {
  61. crc >>>= 1;
  62. }
  63. }
  64. table[i] = crc >>> 0;
  65. }
  66. return table;
  67. }
  68. function getCrcTable() {
  69. if (!cachedCrcTable) {
  70. cachedCrcTable = buildCRCTable();
  71. }
  72. return cachedCrcTable;
  73. }
  74. return function(str) {
  75. var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
  76. for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
  77. y = (crc ^ utf8CharCodes[i]) & 0xFF;
  78. crc = (crc >>> 8) ^ crcTable[y];
  79. }
  80. return (crc ^ -1) >>> 0;
  81. };
  82. })();
  83. var dom = api.dom;
  84. function escapeTextForHtml(str) {
  85. return str.replace(/</g, "&lt;").replace(/>/g, "&gt;");
  86. }
  87. function nodeToInfoString(node, infoParts) {
  88. infoParts = infoParts || [];
  89. var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
  90. var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
  91. var start = "", end = "";
  92. switch (nodeType) {
  93. case 3: // Text node
  94. start = escapeTextForHtml(node.nodeValue);
  95. break;
  96. case 8: // Comment
  97. start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
  98. break;
  99. default:
  100. start = "<" + nodeInfo + ">";
  101. end = "</>";
  102. break;
  103. }
  104. if (start) {
  105. infoParts.push(start);
  106. }
  107. for (var i = 0; i < childCount; ++i) {
  108. nodeToInfoString(children[i], infoParts);
  109. }
  110. if (end) {
  111. infoParts.push(end);
  112. }
  113. return infoParts;
  114. }
  115. // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
  116. // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
  117. // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
  118. // innerHTML whenever the user changes an input within the element.
  119. function getElementChecksum(el) {
  120. var info = nodeToInfoString(el).join("");
  121. return crc32(info).toString(16);
  122. }
  123. function serializePosition(node, offset, rootNode) {
  124. var pathParts = [], n = node;
  125. rootNode = rootNode || dom.getDocument(node).documentElement;
  126. while (n && n != rootNode) {
  127. pathParts.push(dom.getNodeIndex(n, true));
  128. n = n.parentNode;
  129. }
  130. return pathParts.join("/") + ":" + offset;
  131. }
  132. function deserializePosition(serialized, rootNode, doc) {
  133. if (!rootNode) {
  134. rootNode = (doc || document).documentElement;
  135. }
  136. var parts = serialized.split(":");
  137. var node = rootNode;
  138. var nodeIndices = parts[0] ? parts[0].split("/") : [], i = nodeIndices.length, nodeIndex;
  139. while (i--) {
  140. nodeIndex = parseInt(nodeIndices[i], 10);
  141. if (nodeIndex < node.childNodes.length) {
  142. node = node.childNodes[nodeIndex];
  143. } else {
  144. throw module.createError("deserializePosition() failed: node " + dom.inspectNode(node) +
  145. " has no child with index " + nodeIndex + ", " + i);
  146. }
  147. }
  148. return new dom.DomPosition(node, parseInt(parts[1], 10));
  149. }
  150. function serializeRange(range, omitChecksum, rootNode) {
  151. rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
  152. if (!dom.isOrIsAncestorOf(rootNode, range.commonAncestorContainer)) {
  153. throw module.createError("serializeRange(): range " + range.inspect() +
  154. " is not wholly contained within specified root node " + dom.inspectNode(rootNode));
  155. }
  156. var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
  157. serializePosition(range.endContainer, range.endOffset, rootNode);
  158. if (!omitChecksum) {
  159. serialized += "{" + getElementChecksum(rootNode) + "}";
  160. }
  161. return serialized;
  162. }
  163. var deserializeRegex = /^([^,]+),([^,\{]+)(\{([^}]+)\})?$/;
  164. function deserializeRange(serialized, rootNode, doc) {
  165. if (rootNode) {
  166. doc = doc || dom.getDocument(rootNode);
  167. } else {
  168. doc = doc || document;
  169. rootNode = doc.documentElement;
  170. }
  171. var result = deserializeRegex.exec(serialized);
  172. var checksum = result[4];
  173. if (checksum) {
  174. var rootNodeChecksum = getElementChecksum(rootNode);
  175. if (checksum !== rootNodeChecksum) {
  176. throw module.createError("deserializeRange(): checksums of serialized range root node (" + checksum +
  177. ") and target root node (" + rootNodeChecksum + ") do not match");
  178. }
  179. }
  180. var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
  181. var range = api.createRange(doc);
  182. range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
  183. return range;
  184. }
  185. function canDeserializeRange(serialized, rootNode, doc) {
  186. if (!rootNode) {
  187. rootNode = (doc || document).documentElement;
  188. }
  189. var result = deserializeRegex.exec(serialized);
  190. var checksum = result[3];
  191. return !checksum || checksum === getElementChecksum(rootNode);
  192. }
  193. function serializeSelection(selection, omitChecksum, rootNode) {
  194. selection = api.getSelection(selection);
  195. var ranges = selection.getAllRanges(), serializedRanges = [];
  196. for (var i = 0, len = ranges.length; i < len; ++i) {
  197. serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
  198. }
  199. return serializedRanges.join("|");
  200. }
  201. function deserializeSelection(serialized, rootNode, win) {
  202. if (rootNode) {
  203. win = win || dom.getWindow(rootNode);
  204. } else {
  205. win = win || window;
  206. rootNode = win.document.documentElement;
  207. }
  208. var serializedRanges = serialized.split("|");
  209. var sel = api.getSelection(win);
  210. var ranges = [];
  211. for (var i = 0, len = serializedRanges.length; i < len; ++i) {
  212. ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
  213. }
  214. sel.setRanges(ranges);
  215. return sel;
  216. }
  217. function canDeserializeSelection(serialized, rootNode, win) {
  218. var doc;
  219. if (rootNode) {
  220. doc = win ? win.document : dom.getDocument(rootNode);
  221. } else {
  222. win = win || window;
  223. rootNode = win.document.documentElement;
  224. }
  225. var serializedRanges = serialized.split("|");
  226. for (var i = 0, len = serializedRanges.length; i < len; ++i) {
  227. if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
  228. return false;
  229. }
  230. }
  231. return true;
  232. }
  233. var cookieName = "rangySerializedSelection";
  234. function getSerializedSelectionFromCookie(cookie) {
  235. var parts = cookie.split(/[;,]/);
  236. for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
  237. nameVal = parts[i].split("=");
  238. if (nameVal[0].replace(/^\s+/, "") == cookieName) {
  239. val = nameVal[1];
  240. if (val) {
  241. return decodeURIComponent(val.replace(/\s+$/, ""));
  242. }
  243. }
  244. }
  245. return null;
  246. }
  247. function restoreSelectionFromCookie(win) {
  248. win = win || window;
  249. var serialized = getSerializedSelectionFromCookie(win.document.cookie);
  250. if (serialized) {
  251. deserializeSelection(serialized, win.doc);
  252. }
  253. }
  254. function saveSelectionCookie(win, props) {
  255. win = win || window;
  256. props = (typeof props == "object") ? props : {};
  257. var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
  258. var path = props.path ? ";path=" + props.path : "";
  259. var domain = props.domain ? ";domain=" + props.domain : "";
  260. var secure = props.secure ? ";secure" : "";
  261. var serialized = serializeSelection(api.getSelection(win));
  262. win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
  263. }
  264. util.extend(api, {
  265. serializePosition: serializePosition,
  266. deserializePosition: deserializePosition,
  267. serializeRange: serializeRange,
  268. deserializeRange: deserializeRange,
  269. canDeserializeRange: canDeserializeRange,
  270. serializeSelection: serializeSelection,
  271. deserializeSelection: deserializeSelection,
  272. canDeserializeSelection: canDeserializeSelection,
  273. restoreSelectionFromCookie: restoreSelectionFromCookie,
  274. saveSelectionCookie: saveSelectionCookie,
  275. getElementChecksum: getElementChecksum,
  276. nodeToInfoString: nodeToInfoString
  277. });
  278. util.crc32 = crc32;
  279. });
  280. return rangy;
  281. }, this);