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.
6 * Part of Rangy, a cross-browser JavaScript range and selection library
7 * https://github.com/timdown/rangy
9 * Depends on Rangy core.
11 * Copyright 2015, Tim Down
12 * Licensed under the MIT license.
14 * Build date: 10 May 2015
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") );
24 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
28 rangy.createModule("Serializer", ["WrappedSelection"], function(api, module) {
29 var UNDEF = "undefined";
32 // encodeURIComponent and decodeURIComponent are required for cookie handling
33 if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
34 module.fail("encodeURIComponent and/or decodeURIComponent method is missing");
37 // Checksum for checking whether range can be serialized
38 var crc32 = (function() {
39 function utf8encode(str) {
40 var utf8CharCodes = [];
42 for (var i = 0, len = str.length, c; i < len; ++i) {
43 c = str.charCodeAt(i);
45 utf8CharCodes.push(c);
46 } else if (c < 2048) {
47 utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
49 utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
55 var cachedCrcTable = null;
57 function buildCRCTable() {
59 for (var i = 0, j, crc; i < 256; ++i) {
64 crc = (crc >>> 1) ^ 0xEDB88320;
74 function getCrcTable() {
75 if (!cachedCrcTable) {
76 cachedCrcTable = buildCRCTable();
78 return cachedCrcTable;
81 return function(str) {
82 var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
83 for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
84 y = (crc ^ utf8CharCodes[i]) & 0xFF;
85 crc = (crc >>> 8) ^ crcTable[y];
87 return (crc ^ -1) >>> 0;
93 function escapeTextForHtml(str) {
94 return str.replace(/</g, "<").replace(/>/g, ">");
97 function nodeToInfoString(node, infoParts) {
98 infoParts = infoParts || [];
99 var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
100 var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
101 var start = "", end = "";
104 start = escapeTextForHtml(node.nodeValue);
107 start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
110 start = "<" + nodeInfo + ">";
115 infoParts.push(start);
117 for (var i = 0; i < childCount; ++i) {
118 nodeToInfoString(children[i], infoParts);
126 // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
127 // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
128 // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
129 // innerHTML whenever the user changes an input within the element.
130 function getElementChecksum(el) {
131 var info = nodeToInfoString(el).join("");
132 return crc32(info).toString(16);
135 function serializePosition(node, offset, rootNode) {
136 var pathParts = [], n = node;
137 rootNode = rootNode || dom.getDocument(node).documentElement;
138 while (n && n != rootNode) {
139 pathParts.push(dom.getNodeIndex(n, true));
142 return pathParts.join("/") + ":" + offset;
145 function deserializePosition(serialized, rootNode, doc) {
147 rootNode = (doc || document).documentElement;
149 var parts = serialized.split(":");
151 var nodeIndices = parts[0] ? parts[0].split("/") : [], i = nodeIndices.length, nodeIndex;
154 nodeIndex = parseInt(nodeIndices[i], 10);
155 if (nodeIndex < node.childNodes.length) {
156 node = node.childNodes[nodeIndex];
158 throw module.createError("deserializePosition() failed: node " + dom.inspectNode(node) +
159 " has no child with index " + nodeIndex + ", " + i);
163 return new dom.DomPosition(node, parseInt(parts[1], 10));
166 function serializeRange(range, omitChecksum, rootNode) {
167 rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
168 if (!dom.isOrIsAncestorOf(rootNode, range.commonAncestorContainer)) {
169 throw module.createError("serializeRange(): range " + range.inspect() +
170 " is not wholly contained within specified root node " + dom.inspectNode(rootNode));
172 var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
173 serializePosition(range.endContainer, range.endOffset, rootNode);
175 serialized += "{" + getElementChecksum(rootNode) + "}";
180 var deserializeRegex = /^([^,]+),([^,\{]+)(\{([^}]+)\})?$/;
182 function deserializeRange(serialized, rootNode, doc) {
184 doc = doc || dom.getDocument(rootNode);
186 doc = doc || document;
187 rootNode = doc.documentElement;
189 var result = deserializeRegex.exec(serialized);
190 var checksum = result[4];
192 var rootNodeChecksum = getElementChecksum(rootNode);
193 if (checksum !== rootNodeChecksum) {
194 throw module.createError("deserializeRange(): checksums of serialized range root node (" + checksum +
195 ") and target root node (" + rootNodeChecksum + ") do not match");
198 var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
199 var range = api.createRange(doc);
200 range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
204 function canDeserializeRange(serialized, rootNode, doc) {
206 rootNode = (doc || document).documentElement;
208 var result = deserializeRegex.exec(serialized);
209 var checksum = result[3];
210 return !checksum || checksum === getElementChecksum(rootNode);
213 function serializeSelection(selection, omitChecksum, rootNode) {
214 selection = api.getSelection(selection);
215 var ranges = selection.getAllRanges(), serializedRanges = [];
216 for (var i = 0, len = ranges.length; i < len; ++i) {
217 serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
219 return serializedRanges.join("|");
222 function deserializeSelection(serialized, rootNode, win) {
224 win = win || dom.getWindow(rootNode);
227 rootNode = win.document.documentElement;
229 var serializedRanges = serialized.split("|");
230 var sel = api.getSelection(win);
233 for (var i = 0, len = serializedRanges.length; i < len; ++i) {
234 ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
236 sel.setRanges(ranges);
241 function canDeserializeSelection(serialized, rootNode, win) {
244 doc = win ? win.document : dom.getDocument(rootNode);
247 rootNode = win.document.documentElement;
249 var serializedRanges = serialized.split("|");
251 for (var i = 0, len = serializedRanges.length; i < len; ++i) {
252 if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
260 var cookieName = "rangySerializedSelection";
262 function getSerializedSelectionFromCookie(cookie) {
263 var parts = cookie.split(/[;,]/);
264 for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
265 nameVal = parts[i].split("=");
266 if (nameVal[0].replace(/^\s+/, "") == cookieName) {
269 return decodeURIComponent(val.replace(/\s+$/, ""));
276 function restoreSelectionFromCookie(win) {
278 var serialized = getSerializedSelectionFromCookie(win.document.cookie);
280 deserializeSelection(serialized, win.doc);
284 function saveSelectionCookie(win, props) {
286 props = (typeof props == "object") ? props : {};
287 var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
288 var path = props.path ? ";path=" + props.path : "";
289 var domain = props.domain ? ";domain=" + props.domain : "";
290 var secure = props.secure ? ";secure" : "";
291 var serialized = serializeSelection(api.getSelection(win));
292 win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
296 serializePosition: serializePosition,
297 deserializePosition: deserializePosition,
298 serializeRange: serializeRange,
299 deserializeRange: deserializeRange,
300 canDeserializeRange: canDeserializeRange,
301 serializeSelection: serializeSelection,
302 deserializeSelection: deserializeSelection,
303 canDeserializeSelection: canDeserializeSelection,
304 restoreSelectionFromCookie: restoreSelectionFromCookie,
305 saveSelectionCookie: saveSelectionCookie,
306 getElementChecksum: getElementChecksum,
307 nodeToInfoString: nodeToInfoString