mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-24 12:03:01 +00:00
301 lines
11 KiB
JavaScript
301 lines
11 KiB
JavaScript
/**
|
|
* @license Serializer module for Rangy.
|
|
* Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
|
|
* cookie or local storage and restore it on the user's next visit to the same page.
|
|
*
|
|
* Part of Rangy, a cross-browser JavaScript range and selection library
|
|
* http://code.google.com/p/rangy/
|
|
*
|
|
* Depends on Rangy core.
|
|
*
|
|
* Copyright 2011, Tim Down
|
|
* Licensed under the MIT license.
|
|
* Version: 1.2.2
|
|
* Build date: 13 November 2011
|
|
*/
|
|
rangy.createModule("Serializer", function(api, module) {
|
|
api.requireModules( ["WrappedSelection", "WrappedRange"] );
|
|
var UNDEF = "undefined";
|
|
|
|
// encodeURIComponent and decodeURIComponent are required for cookie handling
|
|
if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
|
|
module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method");
|
|
}
|
|
|
|
// Checksum for checking whether range can be serialized
|
|
var crc32 = (function() {
|
|
function utf8encode(str) {
|
|
var utf8CharCodes = [];
|
|
|
|
for (var i = 0, len = str.length, c; i < len; ++i) {
|
|
c = str.charCodeAt(i);
|
|
if (c < 128) {
|
|
utf8CharCodes.push(c);
|
|
} else if (c < 2048) {
|
|
utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
|
|
} else {
|
|
utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
|
|
}
|
|
}
|
|
return utf8CharCodes;
|
|
}
|
|
|
|
var cachedCrcTable = null;
|
|
|
|
function buildCRCTable() {
|
|
var table = [];
|
|
for (var i = 0, j, crc; i < 256; ++i) {
|
|
crc = i;
|
|
j = 8;
|
|
while (j--) {
|
|
if ((crc & 1) == 1) {
|
|
crc = (crc >>> 1) ^ 0xEDB88320;
|
|
} else {
|
|
crc >>>= 1;
|
|
}
|
|
}
|
|
table[i] = crc >>> 0;
|
|
}
|
|
return table;
|
|
}
|
|
|
|
function getCrcTable() {
|
|
if (!cachedCrcTable) {
|
|
cachedCrcTable = buildCRCTable();
|
|
}
|
|
return cachedCrcTable;
|
|
}
|
|
|
|
return function(str) {
|
|
var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
|
|
for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
|
|
y = (crc ^ utf8CharCodes[i]) & 0xFF;
|
|
crc = (crc >>> 8) ^ crcTable[y];
|
|
}
|
|
return (crc ^ -1) >>> 0;
|
|
};
|
|
})();
|
|
|
|
var dom = api.dom;
|
|
|
|
function escapeTextForHtml(str) {
|
|
return str.replace(/</g, "<").replace(/>/g, ">");
|
|
}
|
|
|
|
function nodeToInfoString(node, infoParts) {
|
|
infoParts = infoParts || [];
|
|
var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
|
|
var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
|
|
var start = "", end = "";
|
|
switch (nodeType) {
|
|
case 3: // Text node
|
|
start = escapeTextForHtml(node.nodeValue);
|
|
break;
|
|
case 8: // Comment
|
|
start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
|
|
break;
|
|
default:
|
|
start = "<" + nodeInfo + ">";
|
|
end = "</>";
|
|
break;
|
|
}
|
|
if (start) {
|
|
infoParts.push(start);
|
|
}
|
|
for (var i = 0; i < childCount; ++i) {
|
|
nodeToInfoString(children[i], infoParts);
|
|
}
|
|
if (end) {
|
|
infoParts.push(end);
|
|
}
|
|
return infoParts;
|
|
}
|
|
|
|
// Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
|
|
// attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
|
|
// IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
|
|
// innerHTML whenever the user changes an input within the element.
|
|
function getElementChecksum(el) {
|
|
var info = nodeToInfoString(el).join("");
|
|
return crc32(info).toString(16);
|
|
}
|
|
|
|
function serializePosition(node, offset, rootNode) {
|
|
var pathBits = [], n = node;
|
|
rootNode = rootNode || dom.getDocument(node).documentElement;
|
|
while (n && n != rootNode) {
|
|
pathBits.push(dom.getNodeIndex(n, true));
|
|
n = n.parentNode;
|
|
}
|
|
return pathBits.join("/") + ":" + offset;
|
|
}
|
|
|
|
function deserializePosition(serialized, rootNode, doc) {
|
|
if (rootNode) {
|
|
doc = doc || dom.getDocument(rootNode);
|
|
} else {
|
|
doc = doc || document;
|
|
rootNode = doc.documentElement;
|
|
}
|
|
var bits = serialized.split(":");
|
|
var node = rootNode;
|
|
var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;
|
|
|
|
while (i--) {
|
|
nodeIndex = parseInt(nodeIndices[i], 10);
|
|
if (nodeIndex < node.childNodes.length) {
|
|
node = node.childNodes[parseInt(nodeIndices[i], 10)];
|
|
} else {
|
|
throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) +
|
|
" has no child with index " + nodeIndex + ", " + i);
|
|
}
|
|
}
|
|
|
|
return new dom.DomPosition(node, parseInt(bits[1], 10));
|
|
}
|
|
|
|
function serializeRange(range, omitChecksum, rootNode) {
|
|
rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
|
|
if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) {
|
|
throw new Error("serializeRange: range is not wholly contained within specified root node");
|
|
}
|
|
var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
|
|
serializePosition(range.endContainer, range.endOffset, rootNode);
|
|
if (!omitChecksum) {
|
|
serialized += "{" + getElementChecksum(rootNode) + "}";
|
|
}
|
|
return serialized;
|
|
}
|
|
|
|
function deserializeRange(serialized, rootNode, doc) {
|
|
if (rootNode) {
|
|
doc = doc || dom.getDocument(rootNode);
|
|
} else {
|
|
doc = doc || document;
|
|
rootNode = doc.documentElement;
|
|
}
|
|
var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized);
|
|
var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);
|
|
if (checksum && checksum !== getElementChecksum(rootNode)) {
|
|
throw new Error("deserializeRange: checksums of serialized range root node (" + checksum +
|
|
") and target root node (" + rootNodeChecksum + ") do not match");
|
|
}
|
|
var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
|
|
var range = api.createRange(doc);
|
|
range.setStart(start.node, start.offset);
|
|
range.setEnd(end.node, end.offset);
|
|
return range;
|
|
}
|
|
|
|
function canDeserializeRange(serialized, rootNode, doc) {
|
|
if (rootNode) {
|
|
doc = doc || dom.getDocument(rootNode);
|
|
} else {
|
|
doc = doc || document;
|
|
rootNode = doc.documentElement;
|
|
}
|
|
var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized);
|
|
var checksum = result[3];
|
|
return !checksum || checksum === getElementChecksum(rootNode);
|
|
}
|
|
|
|
function serializeSelection(selection, omitChecksum, rootNode) {
|
|
selection = selection || api.getSelection();
|
|
var ranges = selection.getAllRanges(), serializedRanges = [];
|
|
for (var i = 0, len = ranges.length; i < len; ++i) {
|
|
serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
|
|
}
|
|
return serializedRanges.join("|");
|
|
}
|
|
|
|
function deserializeSelection(serialized, rootNode, win) {
|
|
if (rootNode) {
|
|
win = win || dom.getWindow(rootNode);
|
|
} else {
|
|
win = win || window;
|
|
rootNode = win.document.documentElement;
|
|
}
|
|
var serializedRanges = serialized.split("|");
|
|
var sel = api.getSelection(win);
|
|
var ranges = [];
|
|
|
|
for (var i = 0, len = serializedRanges.length; i < len; ++i) {
|
|
ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
|
|
}
|
|
sel.setRanges(ranges);
|
|
|
|
return sel;
|
|
}
|
|
|
|
function canDeserializeSelection(serialized, rootNode, win) {
|
|
var doc;
|
|
if (rootNode) {
|
|
doc = win ? win.document : dom.getDocument(rootNode);
|
|
} else {
|
|
win = win || window;
|
|
rootNode = win.document.documentElement;
|
|
}
|
|
var serializedRanges = serialized.split("|");
|
|
|
|
for (var i = 0, len = serializedRanges.length; i < len; ++i) {
|
|
if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
var cookieName = "rangySerializedSelection";
|
|
|
|
function getSerializedSelectionFromCookie(cookie) {
|
|
var parts = cookie.split(/[;,]/);
|
|
for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
|
|
nameVal = parts[i].split("=");
|
|
if (nameVal[0].replace(/^\s+/, "") == cookieName) {
|
|
val = nameVal[1];
|
|
if (val) {
|
|
return decodeURIComponent(val.replace(/\s+$/, ""));
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function restoreSelectionFromCookie(win) {
|
|
win = win || window;
|
|
var serialized = getSerializedSelectionFromCookie(win.document.cookie);
|
|
if (serialized) {
|
|
deserializeSelection(serialized, win.doc)
|
|
}
|
|
}
|
|
|
|
function saveSelectionCookie(win, props) {
|
|
win = win || window;
|
|
props = (typeof props == "object") ? props : {};
|
|
var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
|
|
var path = props.path ? ";path=" + props.path : "";
|
|
var domain = props.domain ? ";domain=" + props.domain : "";
|
|
var secure = props.secure ? ";secure" : "";
|
|
var serialized = serializeSelection(api.getSelection(win));
|
|
win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
|
|
}
|
|
|
|
api.serializePosition = serializePosition;
|
|
api.deserializePosition = deserializePosition;
|
|
|
|
api.serializeRange = serializeRange;
|
|
api.deserializeRange = deserializeRange;
|
|
api.canDeserializeRange = canDeserializeRange;
|
|
|
|
api.serializeSelection = serializeSelection;
|
|
api.deserializeSelection = deserializeSelection;
|
|
api.canDeserializeSelection = canDeserializeSelection;
|
|
|
|
api.restoreSelectionFromCookie = restoreSelectionFromCookie;
|
|
api.saveSelectionCookie = saveSelectionCookie;
|
|
|
|
api.getElementChecksum = getElementChecksum;
|
|
});
|