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