2 * @license Serializer module for Rangy.
\r
3 * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
\r
4 * cookie or local storage and restore it on the user's next visit to the same page.
\r
6 * Part of Rangy, a cross-browser JavaScript range and selection library
\r
7 * http://code.google.com/p/rangy/
\r
9 * Depends on Rangy core.
\r
11 * Copyright 2012, Tim Down
\r
12 * Licensed under the MIT license.
\r
14 * Build date: 26 February 2012
\r
16 rangy.createModule("Serializer", function(api, module) {
\r
17 api.requireModules( ["WrappedSelection", "WrappedRange"] );
\r
18 var UNDEF = "undefined";
\r
20 // encodeURIComponent and decodeURIComponent are required for cookie handling
\r
21 if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
\r
22 module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method");
\r
25 // Checksum for checking whether range can be serialized
\r
26 var crc32 = (function() {
\r
27 function utf8encode(str) {
\r
28 var utf8CharCodes = [];
\r
30 for (var i = 0, len = str.length, c; i < len; ++i) {
\r
31 c = str.charCodeAt(i);
\r
33 utf8CharCodes.push(c);
\r
34 } else if (c < 2048) {
\r
35 utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
\r
37 utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
\r
40 return utf8CharCodes;
\r
43 var cachedCrcTable = null;
\r
45 function buildCRCTable() {
\r
47 for (var i = 0, j, crc; i < 256; ++i) {
\r
51 if ((crc & 1) == 1) {
\r
52 crc = (crc >>> 1) ^ 0xEDB88320;
\r
57 table[i] = crc >>> 0;
\r
62 function getCrcTable() {
\r
63 if (!cachedCrcTable) {
\r
64 cachedCrcTable = buildCRCTable();
\r
66 return cachedCrcTable;
\r
69 return function(str) {
\r
70 var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
\r
71 for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
\r
72 y = (crc ^ utf8CharCodes[i]) & 0xFF;
\r
73 crc = (crc >>> 8) ^ crcTable[y];
\r
75 return (crc ^ -1) >>> 0;
\r
81 function escapeTextForHtml(str) {
\r
82 return str.replace(/</g, "<").replace(/>/g, ">");
\r
85 function nodeToInfoString(node, infoParts) {
\r
86 infoParts = infoParts || [];
\r
87 var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
\r
88 var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
\r
89 var start = "", end = "";
\r
91 case 3: // Text node
\r
92 start = escapeTextForHtml(node.nodeValue);
\r
95 start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
\r
98 start = "<" + nodeInfo + ">";
\r
103 infoParts.push(start);
\r
105 for (var i = 0; i < childCount; ++i) {
\r
106 nodeToInfoString(children[i], infoParts);
\r
109 infoParts.push(end);
\r
114 // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
\r
115 // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
\r
116 // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
\r
117 // innerHTML whenever the user changes an input within the element.
\r
118 function getElementChecksum(el) {
\r
119 var info = nodeToInfoString(el).join("");
\r
120 return crc32(info).toString(16);
\r
123 function serializePosition(node, offset, rootNode) {
\r
124 var pathBits = [], n = node;
\r
125 rootNode = rootNode || dom.getDocument(node).documentElement;
\r
126 while (n && n != rootNode) {
\r
127 pathBits.push(dom.getNodeIndex(n, true));
\r
130 return pathBits.join("/") + ":" + offset;
\r
133 function deserializePosition(serialized, rootNode, doc) {
\r
135 doc = doc || dom.getDocument(rootNode);
\r
137 doc = doc || document;
\r
138 rootNode = doc.documentElement;
\r
140 var bits = serialized.split(":");
\r
141 var node = rootNode;
\r
142 var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;
\r
145 nodeIndex = parseInt(nodeIndices[i], 10);
\r
146 if (nodeIndex < node.childNodes.length) {
\r
147 node = node.childNodes[parseInt(nodeIndices[i], 10)];
\r
149 throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) +
\r
150 " has no child with index " + nodeIndex + ", " + i);
\r
154 return new dom.DomPosition(node, parseInt(bits[1], 10));
\r
157 function serializeRange(range, omitChecksum, rootNode) {
\r
158 rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
\r
159 if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) {
\r
160 throw new Error("serializeRange: range is not wholly contained within specified root node");
\r
162 var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
\r
163 serializePosition(range.endContainer, range.endOffset, rootNode);
\r
164 if (!omitChecksum) {
\r
165 serialized += "{" + getElementChecksum(rootNode) + "}";
\r
170 function deserializeRange(serialized, rootNode, doc) {
\r
172 doc = doc || dom.getDocument(rootNode);
\r
174 doc = doc || document;
\r
175 rootNode = doc.documentElement;
\r
177 var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized);
\r
178 var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);
\r
179 if (checksum && checksum !== getElementChecksum(rootNode)) {
\r
180 throw new Error("deserializeRange: checksums of serialized range root node (" + checksum +
\r
181 ") and target root node (" + rootNodeChecksum + ") do not match");
\r
183 var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
\r
184 var range = api.createRange(doc);
\r
185 range.setStart(start.node, start.offset);
\r
186 range.setEnd(end.node, end.offset);
\r
190 function canDeserializeRange(serialized, rootNode, doc) {
\r
192 doc = doc || dom.getDocument(rootNode);
\r
194 doc = doc || document;
\r
195 rootNode = doc.documentElement;
\r
197 var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized);
\r
198 var checksum = result[3];
\r
199 return !checksum || checksum === getElementChecksum(rootNode);
\r
202 function serializeSelection(selection, omitChecksum, rootNode) {
\r
203 selection = selection || api.getSelection();
\r
204 var ranges = selection.getAllRanges(), serializedRanges = [];
\r
205 for (var i = 0, len = ranges.length; i < len; ++i) {
\r
206 serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
\r
208 return serializedRanges.join("|");
\r
211 function deserializeSelection(serialized, rootNode, win) {
\r
213 win = win || dom.getWindow(rootNode);
\r
215 win = win || window;
\r
216 rootNode = win.document.documentElement;
\r
218 var serializedRanges = serialized.split("|");
\r
219 var sel = api.getSelection(win);
\r
222 for (var i = 0, len = serializedRanges.length; i < len; ++i) {
\r
223 ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
\r
225 sel.setRanges(ranges);
\r
230 function canDeserializeSelection(serialized, rootNode, win) {
\r
233 doc = win ? win.document : dom.getDocument(rootNode);
\r
235 win = win || window;
\r
236 rootNode = win.document.documentElement;
\r
238 var serializedRanges = serialized.split("|");
\r
240 for (var i = 0, len = serializedRanges.length; i < len; ++i) {
\r
241 if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
\r
250 var cookieName = "rangySerializedSelection";
\r
252 function getSerializedSelectionFromCookie(cookie) {
\r
253 var parts = cookie.split(/[;,]/);
\r
254 for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
\r
255 nameVal = parts[i].split("=");
\r
256 if (nameVal[0].replace(/^\s+/, "") == cookieName) {
\r
259 return decodeURIComponent(val.replace(/\s+$/, ""));
\r
266 function restoreSelectionFromCookie(win) {
\r
267 win = win || window;
\r
268 var serialized = getSerializedSelectionFromCookie(win.document.cookie);
\r
270 deserializeSelection(serialized, win.doc)
\r
274 function saveSelectionCookie(win, props) {
\r
275 win = win || window;
\r
276 props = (typeof props == "object") ? props : {};
\r
277 var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
\r
278 var path = props.path ? ";path=" + props.path : "";
\r
279 var domain = props.domain ? ";domain=" + props.domain : "";
\r
280 var secure = props.secure ? ";secure" : "";
\r
281 var serialized = serializeSelection(api.getSelection(win));
\r
282 win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
\r
285 api.serializePosition = serializePosition;
\r
286 api.deserializePosition = deserializePosition;
\r
288 api.serializeRange = serializeRange;
\r
289 api.deserializeRange = deserializeRange;
\r
290 api.canDeserializeRange = canDeserializeRange;
\r
292 api.serializeSelection = serializeSelection;
\r
293 api.deserializeSelection = deserializeSelection;
\r
294 api.canDeserializeSelection = canDeserializeSelection;
\r
296 api.restoreSelectionFromCookie = restoreSelectionFromCookie;
\r
297 api.saveSelectionCookie = saveSelectionCookie;
\r
299 api.getElementChecksum = getElementChecksum;
\r