rangy-rails 1.3alpha.772.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,237 @@
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.772
13
+ * Build date: 26 February 2013
14
+ */
15
+ rangy.createModule("SaveRestore", function(api, module) {
16
+ api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
17
+
18
+ var dom = api.dom;
19
+
20
+ var markerTextChar = "\ufeff";
21
+
22
+ function gEBI(id, doc) {
23
+ return (doc || document).getElementById(id);
24
+ }
25
+
26
+ function insertRangeBoundaryMarker(range, atStart) {
27
+ var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
28
+ var markerEl;
29
+ var doc = dom.getDocument(range.startContainer);
30
+
31
+ // Clone the Range and collapse to the appropriate boundary point
32
+ var boundaryRange = range.cloneRange();
33
+ boundaryRange.collapse(atStart);
34
+
35
+ // Create the marker element containing a single invisible character using DOM methods and insert it
36
+ markerEl = doc.createElement("span");
37
+ markerEl.id = markerId;
38
+ markerEl.style.lineHeight = "0";
39
+ markerEl.style.display = "none";
40
+ markerEl.className = "rangySelectionBoundary";
41
+ markerEl.appendChild(doc.createTextNode(markerTextChar));
42
+
43
+ boundaryRange.insertNode(markerEl);
44
+ boundaryRange.detach();
45
+ return markerEl;
46
+ }
47
+
48
+ function setRangeBoundary(doc, range, markerId, atStart) {
49
+ var markerEl = gEBI(markerId, doc);
50
+ if (markerEl) {
51
+ range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
52
+ markerEl.parentNode.removeChild(markerEl);
53
+ } else {
54
+ module.warn("Marker element has been removed. Cannot restore selection.");
55
+ }
56
+ }
57
+
58
+ function compareRanges(r1, r2) {
59
+ return r2.compareBoundaryPoints(r1.START_TO_START, r1);
60
+ }
61
+
62
+ function saveRange(range, backward) {
63
+ var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
64
+
65
+ if (range.collapsed) {
66
+ endEl = insertRangeBoundaryMarker(range, false);
67
+ return {
68
+ document: doc,
69
+ markerId: endEl.id,
70
+ collapsed: true
71
+ };
72
+ } else {
73
+ endEl = insertRangeBoundaryMarker(range, false);
74
+ startEl = insertRangeBoundaryMarker(range, true);
75
+
76
+ return {
77
+ document: doc,
78
+ startMarkerId: startEl.id,
79
+ endMarkerId: endEl.id,
80
+ collapsed: false,
81
+ backward: backward,
82
+ toString: function() {
83
+ return "original text: '" + text + "', new text: '" + range.toString() + "'";
84
+ }
85
+ };
86
+ }
87
+ }
88
+
89
+ function restoreRange(rangeInfo, normalize) {
90
+ var doc = rangeInfo.document;
91
+ if (typeof normalize == "undefined") {
92
+ normalize = true;
93
+ }
94
+ var range = api.createRange(doc);
95
+ if (rangeInfo.collapsed) {
96
+ var markerEl = gEBI(rangeInfo.markerId, doc);
97
+ if (markerEl) {
98
+ markerEl.style.display = "inline";
99
+ var previousNode = markerEl.previousSibling;
100
+
101
+ // Workaround for issue 17
102
+ if (previousNode && previousNode.nodeType == 3) {
103
+ markerEl.parentNode.removeChild(markerEl);
104
+ range.collapseToPoint(previousNode, previousNode.length);
105
+ } else {
106
+ range.collapseBefore(markerEl);
107
+ markerEl.parentNode.removeChild(markerEl);
108
+ }
109
+ } else {
110
+ module.warn("Marker element has been removed. Cannot restore selection.");
111
+ }
112
+ } else {
113
+ setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
114
+ setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
115
+ }
116
+
117
+ if (normalize) {
118
+ range.normalizeBoundaries();
119
+ }
120
+
121
+ return range;
122
+ }
123
+
124
+ function saveRanges(ranges, backward) {
125
+ var rangeInfos = [], range, doc;
126
+
127
+ // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
128
+ ranges = ranges.slice(0);
129
+ ranges.sort(compareRanges);
130
+
131
+ for (var i = 0, len = ranges.length; i < len; ++i) {
132
+ rangeInfos[i] = saveRange(ranges[i], backward);
133
+ }
134
+
135
+ // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
136
+ // between its markers
137
+ for (i = len - 1; i >= 0; --i) {
138
+ range = ranges[i];
139
+ doc = api.DomRange.getRangeDocument(range);
140
+ if (range.collapsed) {
141
+ range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
142
+ } else {
143
+ range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
144
+ range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
145
+ }
146
+ }
147
+
148
+ return rangeInfos;
149
+ }
150
+
151
+ function saveSelection(win) {
152
+ if (!api.isSelectionValid(win)) {
153
+ module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
154
+ return null;
155
+ }
156
+ var sel = api.getSelection(win);
157
+ var ranges = sel.getAllRanges();
158
+ var backward = (ranges.length == 1 && sel.isBackward());
159
+
160
+ var rangeInfos = saveRanges(ranges, backward);
161
+
162
+ // Ensure current selection is unaffected
163
+ if (backward) {
164
+ sel.setSingleRange(ranges[0], "backward");
165
+ } else {
166
+ sel.setRanges(ranges);
167
+ }
168
+
169
+ return {
170
+ win: win,
171
+ rangeInfos: rangeInfos,
172
+ restored: false
173
+ };
174
+ }
175
+
176
+ function restoreRanges(rangeInfos) {
177
+ var ranges = [];
178
+
179
+ // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
180
+ // normalization affecting previously restored ranges.
181
+ var rangeCount = rangeInfos.length;
182
+
183
+ for (var i = rangeCount - 1; i >= 0; i--) {
184
+ ranges[i] = restoreRange(rangeInfos[i], true);
185
+ }
186
+
187
+ return ranges;
188
+ }
189
+
190
+ function restoreSelection(savedSelection, preserveDirection) {
191
+ if (!savedSelection.restored) {
192
+ var rangeInfos = savedSelection.rangeInfos;
193
+ var sel = api.getSelection(savedSelection.win);
194
+ var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
195
+
196
+ if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
197
+ sel.removeAllRanges();
198
+ sel.addRange(ranges[0], true);
199
+ } else {
200
+ sel.setRanges(ranges);
201
+ }
202
+
203
+ savedSelection.restored = true;
204
+ }
205
+ }
206
+
207
+ function removeMarkerElement(doc, markerId) {
208
+ var markerEl = gEBI(markerId, doc);
209
+ if (markerEl) {
210
+ markerEl.parentNode.removeChild(markerEl);
211
+ }
212
+ }
213
+
214
+ function removeMarkers(savedSelection) {
215
+ var rangeInfos = savedSelection.rangeInfos;
216
+ for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
217
+ rangeInfo = rangeInfos[i];
218
+ if (rangeInfo.collapsed) {
219
+ removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
220
+ } else {
221
+ removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
222
+ removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
223
+ }
224
+ }
225
+ }
226
+
227
+ api.util.extend(api, {
228
+ saveRange: saveRange,
229
+ restoreRange: restoreRange,
230
+ saveRanges: saveRanges,
231
+ restoreRanges: restoreRanges,
232
+ saveSelection: saveSelection,
233
+ restoreSelection: restoreSelection,
234
+ removeMarkerElement: removeMarkerElement,
235
+ removeMarkers: removeMarkers
236
+ });
237
+ });
@@ -0,0 +1,294 @@
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
+ * http://code.google.com/p/rangy/
8
+ *
9
+ * Depends on Rangy core.
10
+ *
11
+ * Copyright 2013, Tim Down
12
+ * Licensed under the MIT license.
13
+ * Version: 1.3alpha.772
14
+ * Build date: 26 February 2013
15
+ */
16
+ rangy.createModule("Serializer", function(api, module) {
17
+ api.requireModules( ["WrappedSelection", "WrappedRange"] );
18
+ var UNDEF = "undefined";
19
+
20
+ // encodeURIComponent and decodeURIComponent are required for cookie handling
21
+ if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
22
+ module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method");
23
+ }
24
+
25
+ // Checksum for checking whether range can be serialized
26
+ var crc32 = (function() {
27
+ function utf8encode(str) {
28
+ var utf8CharCodes = [];
29
+
30
+ for (var i = 0, len = str.length, c; i < len; ++i) {
31
+ c = str.charCodeAt(i);
32
+ if (c < 128) {
33
+ utf8CharCodes.push(c);
34
+ } else if (c < 2048) {
35
+ utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
36
+ } else {
37
+ utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
38
+ }
39
+ }
40
+ return utf8CharCodes;
41
+ }
42
+
43
+ var cachedCrcTable = null;
44
+
45
+ function buildCRCTable() {
46
+ var table = [];
47
+ for (var i = 0, j, crc; i < 256; ++i) {
48
+ crc = i;
49
+ j = 8;
50
+ while (j--) {
51
+ if ((crc & 1) == 1) {
52
+ crc = (crc >>> 1) ^ 0xEDB88320;
53
+ } else {
54
+ crc >>>= 1;
55
+ }
56
+ }
57
+ table[i] = crc >>> 0;
58
+ }
59
+ return table;
60
+ }
61
+
62
+ function getCrcTable() {
63
+ if (!cachedCrcTable) {
64
+ cachedCrcTable = buildCRCTable();
65
+ }
66
+ return cachedCrcTable;
67
+ }
68
+
69
+ return function(str) {
70
+ var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
71
+ for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
72
+ y = (crc ^ utf8CharCodes[i]) & 0xFF;
73
+ crc = (crc >>> 8) ^ crcTable[y];
74
+ }
75
+ return (crc ^ -1) >>> 0;
76
+ };
77
+ })();
78
+
79
+ var dom = api.dom;
80
+
81
+ function escapeTextForHtml(str) {
82
+ return str.replace(/</g, "&lt;").replace(/>/g, "&gt;");
83
+ }
84
+
85
+ function nodeToInfoString(node, infoParts) {
86
+ infoParts = infoParts || [];
87
+ var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
88
+ var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
89
+ var start = "", end = "";
90
+ switch (nodeType) {
91
+ case 3: // Text node
92
+ start = escapeTextForHtml(node.nodeValue);
93
+ break;
94
+ case 8: // Comment
95
+ start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
96
+ break;
97
+ default:
98
+ start = "<" + nodeInfo + ">";
99
+ end = "</>";
100
+ break;
101
+ }
102
+ if (start) {
103
+ infoParts.push(start);
104
+ }
105
+ for (var i = 0; i < childCount; ++i) {
106
+ nodeToInfoString(children[i], infoParts);
107
+ }
108
+ if (end) {
109
+ infoParts.push(end);
110
+ }
111
+ return infoParts;
112
+ }
113
+
114
+ // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
115
+ // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
116
+ // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
117
+ // innerHTML whenever the user changes an input within the element.
118
+ function getElementChecksum(el) {
119
+ var info = nodeToInfoString(el).join("");
120
+ return crc32(info).toString(16);
121
+ }
122
+
123
+ function serializePosition(node, offset, rootNode) {
124
+ var pathBits = [], n = node;
125
+ rootNode = rootNode || dom.getDocument(node).documentElement;
126
+ while (n && n != rootNode) {
127
+ pathBits.push(dom.getNodeIndex(n, true));
128
+ n = n.parentNode;
129
+ }
130
+ return pathBits.join("/") + ":" + offset;
131
+ }
132
+
133
+ function deserializePosition(serialized, rootNode, doc) {
134
+ if (!rootNode) {
135
+ rootNode = (doc || document).documentElement;
136
+ }
137
+ var bits = serialized.split(":");
138
+ var node = rootNode;
139
+ var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;
140
+
141
+ while (i--) {
142
+ nodeIndex = parseInt(nodeIndices[i], 10);
143
+ if (nodeIndex < node.childNodes.length) {
144
+ node = node.childNodes[nodeIndex];
145
+ } else {
146
+ throw module.createError("deserializePosition() failed: node " + dom.inspectNode(node) +
147
+ " has no child with index " + nodeIndex + ", " + i);
148
+ }
149
+ }
150
+
151
+ return new dom.DomPosition(node, parseInt(bits[1], 10));
152
+ }
153
+
154
+ function serializeRange(range, omitChecksum, rootNode) {
155
+ rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
156
+ if (!dom.isOrIsAncestorOf(rootNode, range.commonAncestorContainer)) {
157
+ throw module.createError("serializeRange(): range " + range.inspect() +
158
+ " is not wholly contained within specified root node " + dom.inspectNode(rootNode));
159
+ }
160
+ var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
161
+ serializePosition(range.endContainer, range.endOffset, rootNode);
162
+ if (!omitChecksum) {
163
+ serialized += "{" + getElementChecksum(rootNode) + "}";
164
+ }
165
+ return serialized;
166
+ }
167
+
168
+ function deserializeRange(serialized, rootNode, doc) {
169
+ if (rootNode) {
170
+ doc = doc || dom.getDocument(rootNode);
171
+ } else {
172
+ doc = doc || document;
173
+ rootNode = doc.documentElement;
174
+ }
175
+ var result = /^([^,]+),([^,\{]+)(\{([^}]+)\})?$/.exec(serialized);
176
+ var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);
177
+ if (checksum && checksum !== getElementChecksum(rootNode)) {
178
+ throw module.createError("deserializeRange(): checksums of serialized range root node (" + checksum +
179
+ ") and target root node (" + rootNodeChecksum + ") do not match");
180
+ }
181
+ var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
182
+ var range = api.createRange(doc);
183
+ range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
184
+ return range;
185
+ }
186
+
187
+ function canDeserializeRange(serialized, rootNode, doc) {
188
+ if (!rootNode) {
189
+ rootNode = (doc || document).documentElement;
190
+ }
191
+ var result = /^([^,]+),([^,]+)(\{([^}]+)\})?$/.exec(serialized);
192
+ var checksum = result[3];
193
+ return !checksum || checksum === getElementChecksum(rootNode);
194
+ }
195
+
196
+ function serializeSelection(selection, omitChecksum, rootNode) {
197
+ selection = api.getSelection(selection);
198
+ var ranges = selection.getAllRanges(), serializedRanges = [];
199
+ for (var i = 0, len = ranges.length; i < len; ++i) {
200
+ serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
201
+ }
202
+ return serializedRanges.join("|");
203
+ }
204
+
205
+ function deserializeSelection(serialized, rootNode, win) {
206
+ if (rootNode) {
207
+ win = win || dom.getWindow(rootNode);
208
+ } else {
209
+ win = win || window;
210
+ rootNode = win.document.documentElement;
211
+ }
212
+ var serializedRanges = serialized.split("|");
213
+ var sel = api.getSelection(win);
214
+ var ranges = [];
215
+
216
+ for (var i = 0, len = serializedRanges.length; i < len; ++i) {
217
+ ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
218
+ }
219
+ sel.setRanges(ranges);
220
+
221
+ return sel;
222
+ }
223
+
224
+ function canDeserializeSelection(serialized, rootNode, win) {
225
+ var doc;
226
+ if (rootNode) {
227
+ doc = win ? win.document : dom.getDocument(rootNode);
228
+ } else {
229
+ win = win || window;
230
+ rootNode = win.document.documentElement;
231
+ }
232
+ var serializedRanges = serialized.split("|");
233
+
234
+ for (var i = 0, len = serializedRanges.length; i < len; ++i) {
235
+ if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
236
+ return false;
237
+ }
238
+ }
239
+
240
+ return true;
241
+ }
242
+
243
+ var cookieName = "rangySerializedSelection";
244
+
245
+ function getSerializedSelectionFromCookie(cookie) {
246
+ var parts = cookie.split(/[;,]/);
247
+ for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
248
+ nameVal = parts[i].split("=");
249
+ if (nameVal[0].replace(/^\s+/, "") == cookieName) {
250
+ val = nameVal[1];
251
+ if (val) {
252
+ return decodeURIComponent(val.replace(/\s+$/, ""));
253
+ }
254
+ }
255
+ }
256
+ return null;
257
+ }
258
+
259
+ function restoreSelectionFromCookie(win) {
260
+ win = win || window;
261
+ var serialized = getSerializedSelectionFromCookie(win.document.cookie);
262
+ if (serialized) {
263
+ deserializeSelection(serialized, win.doc);
264
+ }
265
+ }
266
+
267
+ function saveSelectionCookie(win, props) {
268
+ win = win || window;
269
+ props = (typeof props == "object") ? props : {};
270
+ var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
271
+ var path = props.path ? ";path=" + props.path : "";
272
+ var domain = props.domain ? ";domain=" + props.domain : "";
273
+ var secure = props.secure ? ";secure" : "";
274
+ var serialized = serializeSelection(api.getSelection(win));
275
+ win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
276
+ }
277
+
278
+ api.serializePosition = serializePosition;
279
+ api.deserializePosition = deserializePosition;
280
+
281
+ api.serializeRange = serializeRange;
282
+ api.deserializeRange = deserializeRange;
283
+ api.canDeserializeRange = canDeserializeRange;
284
+
285
+ api.serializeSelection = serializeSelection;
286
+ api.deserializeSelection = deserializeSelection;
287
+ api.canDeserializeSelection = canDeserializeSelection;
288
+
289
+ api.restoreSelectionFromCookie = restoreSelectionFromCookie;
290
+ api.saveSelectionCookie = saveSelectionCookie;
291
+
292
+ api.getElementChecksum = getElementChecksum;
293
+ api.nodeToInfoString = nodeToInfoString;
294
+ });