Merge branch 'MDL-51605-master' of git://github.com/damyon/moodle

This commit is contained in:
Dan Poltawski
2015-10-12 09:46:00 +01:00
26 changed files with 23139 additions and 12376 deletions
@@ -62,8 +62,8 @@ Y.namespace('M.atto_subscript').Button = Y.Base.create('button', Y.M.editor_atto
// Watch the following tags and add/remove highlighting as appropriate:
tags: 'sub'
});
this._subscriptApplier = window.rangy.createCssClassApplier("editor-subscript");
this._superscriptApplier = window.rangy.createCssClassApplier("editor-superscript");
this._subscriptApplier = window.rangy.createClassApplier("editor-subscript");
this._superscriptApplier = window.rangy.createClassApplier("editor-superscript");
},
/**
@@ -1 +1 @@
YUI.add("moodle-atto_subscript-button",function(e,t){e.namespace("M.atto_subscript").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{_subscriptApplier:null,_superscriptApplier:null,initializer:function(){this.addButton({buttonName:"subscript",callback:this.toggleSubscript,icon:"e/subscript",inlineFormat:!0,tags:"sub"}),this._subscriptApplier=window.rangy.createCssClassApplier("editor-subscript"),this._superscriptApplier=window.rangy.createCssClassApplier("editor-superscript")},toggleSubscript:function(){this.get("host").changeToCSS("sup","editor-superscript"),this.get("host").changeToCSS("sub","editor-subscript"),this._superscriptApplier.undoToSelection(),this._subscriptApplier.toggleSelection(),this.get("host").changeToTags("editor-subscript","sub"),this.get("host").changeToTags("editor-superscript","sup")}})},"@VERSION@",{requires:["moodle-editor_atto-plugin"]});
YUI.add("moodle-atto_subscript-button",function(e,t){e.namespace("M.atto_subscript").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{_subscriptApplier:null,_superscriptApplier:null,initializer:function(){this.addButton({buttonName:"subscript",callback:this.toggleSubscript,icon:"e/subscript",inlineFormat:!0,tags:"sub"}),this._subscriptApplier=window.rangy.createClassApplier("editor-subscript"),this._superscriptApplier=window.rangy.createClassApplier("editor-superscript")},toggleSubscript:function(){this.get("host").changeToCSS("sup","editor-superscript"),this.get("host").changeToCSS("sub","editor-subscript"),this._superscriptApplier.undoToSelection(),this._subscriptApplier.toggleSelection(),this.get("host").changeToTags("editor-subscript","sub"),this.get("host").changeToTags("editor-superscript","sup")}})},"@VERSION@",{requires:["moodle-editor_atto-plugin"]});
@@ -62,8 +62,8 @@ Y.namespace('M.atto_subscript').Button = Y.Base.create('button', Y.M.editor_atto
// Watch the following tags and add/remove highlighting as appropriate:
tags: 'sub'
});
this._subscriptApplier = window.rangy.createCssClassApplier("editor-subscript");
this._superscriptApplier = window.rangy.createCssClassApplier("editor-superscript");
this._subscriptApplier = window.rangy.createClassApplier("editor-subscript");
this._superscriptApplier = window.rangy.createClassApplier("editor-superscript");
},
/**
@@ -60,8 +60,8 @@ Y.namespace('M.atto_subscript').Button = Y.Base.create('button', Y.M.editor_atto
// Watch the following tags and add/remove highlighting as appropriate:
tags: 'sub'
});
this._subscriptApplier = window.rangy.createCssClassApplier("editor-subscript");
this._superscriptApplier = window.rangy.createCssClassApplier("editor-superscript");
this._subscriptApplier = window.rangy.createClassApplier("editor-subscript");
this._superscriptApplier = window.rangy.createClassApplier("editor-superscript");
},
/**
@@ -62,8 +62,8 @@ Y.namespace('M.atto_superscript').Button = Y.Base.create('button', Y.M.editor_at
// Watch the following tags and add/remove highlighting as appropriate:
tags: 'sup'
});
this._subscriptApplier = window.rangy.createCssClassApplier("editor-subscript");
this._superscriptApplier = window.rangy.createCssClassApplier("editor-superscript");
this._subscriptApplier = window.rangy.createClassApplier("editor-subscript");
this._superscriptApplier = window.rangy.createClassApplier("editor-superscript");
},
/**
@@ -1 +1 @@
YUI.add("moodle-atto_superscript-button",function(e,t){e.namespace("M.atto_superscript").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{_subscriptApplier:null,_superscriptApplier:null,initializer:function(){this.addButton({buttonName:"superscript",callback:this.toggleSuperscript,icon:"e/superscript",inlineFormat:!0,tags:"sup"}),this._subscriptApplier=window.rangy.createCssClassApplier("editor-subscript"),this._superscriptApplier=window.rangy.createCssClassApplier("editor-superscript")},toggleSuperscript:function(){this.get("host").changeToCSS("sub","editor-subscript"),this.get("host").changeToCSS("sup","editor-superscript"),this._superscriptApplier.toggleSelection(),this._subscriptApplier.undoToSelection(),this.get("host").changeToTags("editor-superscript","sup"),this.get("host").changeToTags("editor-subscript","sub")}})},"@VERSION@",{requires:["moodle-editor_atto-plugin"]});
YUI.add("moodle-atto_superscript-button",function(e,t){e.namespace("M.atto_superscript").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{_subscriptApplier:null,_superscriptApplier:null,initializer:function(){this.addButton({buttonName:"superscript",callback:this.toggleSuperscript,icon:"e/superscript",inlineFormat:!0,tags:"sup"}),this._subscriptApplier=window.rangy.createClassApplier("editor-subscript"),this._superscriptApplier=window.rangy.createClassApplier("editor-superscript")},toggleSuperscript:function(){this.get("host").changeToCSS("sub","editor-subscript"),this.get("host").changeToCSS("sup","editor-superscript"),this._superscriptApplier.toggleSelection(),this._subscriptApplier.undoToSelection(),this.get("host").changeToTags("editor-superscript","sup"),this.get("host").changeToTags("editor-subscript","sub")}})},"@VERSION@",{requires:["moodle-editor_atto-plugin"]});
@@ -62,8 +62,8 @@ Y.namespace('M.atto_superscript').Button = Y.Base.create('button', Y.M.editor_at
// Watch the following tags and add/remove highlighting as appropriate:
tags: 'sup'
});
this._subscriptApplier = window.rangy.createCssClassApplier("editor-subscript");
this._superscriptApplier = window.rangy.createCssClassApplier("editor-superscript");
this._subscriptApplier = window.rangy.createClassApplier("editor-subscript");
this._superscriptApplier = window.rangy.createClassApplier("editor-superscript");
},
/**
@@ -60,8 +60,8 @@ Y.namespace('M.atto_superscript').Button = Y.Base.create('button', Y.M.editor_at
// Watch the following tags and add/remove highlighting as appropriate:
tags: 'sup'
});
this._subscriptApplier = window.rangy.createCssClassApplier("editor-subscript");
this._superscriptApplier = window.rangy.createCssClassApplier("editor-superscript");
this._subscriptApplier = window.rangy.createClassApplier("editor-subscript");
this._superscriptApplier = window.rangy.createClassApplier("editor-superscript");
},
/**
+10
View File
@@ -2,9 +2,19 @@ Description of the import of libraries associated with the Atto editor.
1) Rangy (version 1.2.3)
* Download the latest stable release;
* Delete all files in yui/src/rangy/js
* Copy the content of the 'currentrelease/uncompressed' folder into yui/src/rangy/js
* Patch out the AMD / module support from rangy (because we are loading it with YUI)
To do this - change the code start of each js file to look like (just delete the other lines):
(function(factory, root) {
// No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
factory(root.rangy);
})(function(rangy) {
* Run shifter against yui/src/rangy
Notes:
* We have patched 1.2.3 with a backport fix from the next release of Rangy which addresses an incompatibility
between Rangy and HTML5Shiv which is used in the bootstrapclean theme. See MDL-44798 for further information.
+1 -1
View File
@@ -4,7 +4,7 @@
<location>yui/src/rangy/js/*.*</location>
<name>Rangy</name>
<license>MIT</license>
<version>1.2.3</version>
<version>1.3.0</version>
<licenseversion></licenseversion>
</library>
</libraries>
+3
View File
@@ -1,4 +1,7 @@
This files describes API changes in the editor_atto code.
=== 3.0 ===
* Rangy updated to 1.3.0.
Rangy has deprecated the createCssClassApplier function in this version (it is now createClassApplier).
=== 2.9 ===
@@ -2341,7 +2341,7 @@ EditorStyling.prototype = {
*/
toggleInlineSelectionClass: function(toggleclasses) {
var classname = toggleclasses.join(" ");
var cssApplier = rangy.createCssClassApplier(classname, {normalize: true});
var cssApplier = rangy.createClassApplier(classname, {normalize: true});
cssApplier.toggleSelection();
},
@@ -2356,7 +2356,7 @@ EditorStyling.prototype = {
*/
formatSelectionInlineStyle: function(styles) {
var classname = this.PLACEHOLDER_CLASS;
var cssApplier = rangy.createCssClassApplier(classname, {normalize: true});
var cssApplier = rangy.createClassApplier(classname, {normalize: true});
cssApplier.applyToSelection();
File diff suppressed because one or more lines are too long
@@ -2328,7 +2328,7 @@ EditorStyling.prototype = {
*/
toggleInlineSelectionClass: function(toggleclasses) {
var classname = toggleclasses.join(" ");
var cssApplier = rangy.createCssClassApplier(classname, {normalize: true});
var cssApplier = rangy.createClassApplier(classname, {normalize: true});
cssApplier.toggleSelection();
},
@@ -2343,7 +2343,7 @@ EditorStyling.prototype = {
*/
formatSelectionInlineStyle: function(styles) {
var classname = this.PLACEHOLDER_CLASS;
var cssApplier = rangy.createCssClassApplier(classname, {normalize: true});
var cssApplier = rangy.createClassApplier(classname, {normalize: true});
cssApplier.applyToSelection();
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -87,7 +87,7 @@ EditorStyling.prototype = {
*/
toggleInlineSelectionClass: function(toggleclasses) {
var classname = toggleclasses.join(" ");
var cssApplier = rangy.createCssClassApplier(classname, {normalize: true});
var cssApplier = rangy.createClassApplier(classname, {normalize: true});
cssApplier.toggleSelection();
},
@@ -102,7 +102,7 @@ EditorStyling.prototype = {
*/
formatSelectionInlineStyle: function(styles) {
var classname = this.PLACEHOLDER_CLASS;
var cssApplier = rangy.createCssClassApplier(classname, {normalize: true});
var cssApplier = rangy.createClassApplier(classname, {normalize: true});
cssApplier.applyToSelection();
+4 -2
View File
@@ -4,9 +4,11 @@
"moodle-editor_atto-rangy": {
"prependfiles": [
"rangy-core.js",
"rangy-cssclassapplier.js",
"rangy-selectionsaverestore.js",
"rangy-serializer.js"
"rangy-serializer.js",
"rangy-classapplier.js",
"rangy-highlighter.js",
"rangy-textrange.js"
],
"jsfiles": [
"init.js"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,713 +0,0 @@
/**
* @license CSS Class Applier module for Rangy.
* Adds, removes and toggles CSS classes on Ranges and Selections
*
* Part of Rangy, a cross-browser JavaScript range and selection library
* http://code.google.com/p/rangy/
*
* Depends on Rangy core.
*
* Copyright 2012, Tim Down
* Licensed under the MIT license.
* Version: 1.2.3
* Build date: 26 February 2012
*/
rangy.createModule("CssClassApplier", function(api, module) {
api.requireModules( ["WrappedSelection", "WrappedRange"] );
var dom = api.dom;
var defaultTagName = "span";
function trim(str) {
return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
}
function hasClass(el, cssClass) {
return el.className && new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)").test(el.className);
}
function addClass(el, cssClass) {
if (el.className) {
if (!hasClass(el, cssClass)) {
el.className += " " + cssClass;
}
} else {
el.className = cssClass;
}
}
var removeClass = (function() {
function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
}
return function(el, cssClass) {
if (el.className) {
el.className = el.className.replace(new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)"), replacer);
}
};
})();
function sortClassName(className) {
return className.split(/\s+/).sort().join(" ");
}
function getSortedClassName(el) {
return sortClassName(el.className);
}
function haveSameClasses(el1, el2) {
return getSortedClassName(el1) == getSortedClassName(el2);
}
function replaceWithOwnChildren(el) {
var parent = el.parentNode;
while (el.hasChildNodes()) {
parent.insertBefore(el.firstChild, el);
}
parent.removeChild(el);
}
function rangeSelectsAnyText(range, textNode) {
var textRange = range.cloneRange();
textRange.selectNodeContents(textNode);
var intersectionRange = textRange.intersection(range);
var text = intersectionRange ? intersectionRange.toString() : "";
textRange.detach();
return text != "";
}
function getEffectiveTextNodes(range) {
return range.getNodes([3], function(textNode) {
return rangeSelectsAnyText(range, textNode);
});
}
function elementsHaveSameNonClassAttributes(el1, el2) {
if (el1.attributes.length != el2.attributes.length) return false;
for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
attr1 = el1.attributes[i];
name = attr1.name;
if (name != "class") {
attr2 = el2.attributes.getNamedItem(name);
if (attr1.specified != attr2.specified) return false;
if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
}
}
return true;
}
function elementHasNonClassAttributes(el, exceptions) {
for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
attrName = el.attributes[i].name;
if ( !(exceptions && dom.arrayContains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
return true;
}
}
return false;
}
function elementHasProps(el, props) {
for (var p in props) {
if (props.hasOwnProperty(p) && el[p] !== props[p]) {
return false;
}
}
return true;
}
var getComputedStyleProperty;
if (typeof window.getComputedStyle != "undefined") {
getComputedStyleProperty = function(el, propName) {
return dom.getWindow(el).getComputedStyle(el, null)[propName];
};
} else if (typeof document.documentElement.currentStyle != "undefined") {
getComputedStyleProperty = function(el, propName) {
return el.currentStyle[propName];
};
} else {
module.fail("No means of obtaining computed style properties found");
}
var isEditableElement;
(function() {
var testEl = document.createElement("div");
if (typeof testEl.isContentEditable == "boolean") {
isEditableElement = function(node) {
return node && node.nodeType == 1 && node.isContentEditable;
};
} else {
isEditableElement = function(node) {
if (!node || node.nodeType != 1 || node.contentEditable == "false") {
return false;
}
return node.contentEditable == "true" || isEditableElement(node.parentNode);
};
}
})();
function isEditingHost(node) {
var parent;
return node && node.nodeType == 1
&& (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on")
|| (isEditableElement(node) && !isEditableElement(node.parentNode)));
}
function isEditable(node) {
return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
}
var inlineDisplayRegex = /^inline(-block|-table)?$/i;
function isNonInlineElement(node) {
return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
}
// White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
function isUnrenderedWhiteSpaceNode(node) {
if (node.data.length == 0) {
return true;
}
if (htmlNonWhiteSpaceRegex.test(node.data)) {
return false;
}
var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
switch (cssWhiteSpace) {
case "pre":
case "pre-wrap":
case "-moz-pre-wrap":
return false;
case "pre-line":
if (/[\r\n]/.test(node.data)) {
return false;
}
}
// We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
// non-inline element, it will not be rendered. This seems to be a good enough definition.
return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
}
function isSplitPoint(node, offset) {
if (dom.isCharacterDataNode(node)) {
if (offset == 0) {
return !!node.previousSibling;
} else if (offset == node.length) {
return !!node.nextSibling;
} else {
return true;
}
}
return offset > 0 && offset < node.childNodes.length;
}
function splitNodeAt(node, descendantNode, descendantOffset, rangesToPreserve) {
var newNode;
var splitAtStart = (descendantOffset == 0);
if (dom.isAncestorOf(descendantNode, node)) {
return node;
}
if (dom.isCharacterDataNode(descendantNode)) {
if (descendantOffset == 0) {
descendantOffset = dom.getNodeIndex(descendantNode);
descendantNode = descendantNode.parentNode;
} else if (descendantOffset == descendantNode.length) {
descendantOffset = dom.getNodeIndex(descendantNode) + 1;
descendantNode = descendantNode.parentNode;
} else {
throw module.createError("splitNodeAt should not be called with offset in the middle of a data node ("
+ descendantOffset + " in " + descendantNode.data);
}
}
if (isSplitPoint(descendantNode, descendantOffset)) {
if (!newNode) {
newNode = descendantNode.cloneNode(false);
if (newNode.id) {
newNode.removeAttribute("id");
}
var child;
while ((child = descendantNode.childNodes[descendantOffset])) {
newNode.appendChild(child);
}
dom.insertAfter(newNode, descendantNode);
}
return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, dom.getNodeIndex(newNode), rangesToPreserve);
} else if (node != descendantNode) {
newNode = descendantNode.parentNode;
// Work out a new split point in the parent node
var newNodeIndex = dom.getNodeIndex(descendantNode);
if (!splitAtStart) {
newNodeIndex++;
}
return splitNodeAt(node, newNode, newNodeIndex, rangesToPreserve);
}
return node;
}
function areElementsMergeable(el1, el2) {
return el1.tagName == el2.tagName && haveSameClasses(el1, el2) && elementsHaveSameNonClassAttributes(el1, el2);
}
function createAdjacentMergeableTextNodeGetter(forward) {
var propName = forward ? "nextSibling" : "previousSibling";
return function(textNode, checkParentElement) {
var el = textNode.parentNode;
var adjacentNode = textNode[propName];
if (adjacentNode) {
// Can merge if the node's previous/next sibling is a text node
if (adjacentNode && adjacentNode.nodeType == 3) {
return adjacentNode;
}
} else if (checkParentElement) {
// Compare text node parent element with its sibling
adjacentNode = el[propName];
if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
return adjacentNode[forward ? "firstChild" : "lastChild"];
}
}
return null;
}
}
var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
function Merge(firstNode) {
this.isElementMerge = (firstNode.nodeType == 1);
this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
this.textNodes = [this.firstTextNode];
}
Merge.prototype = {
doMerge: function() {
var textBits = [], textNode, parent, text;
for (var i = 0, len = this.textNodes.length; i < len; ++i) {
textNode = this.textNodes[i];
parent = textNode.parentNode;
textBits[i] = textNode.data;
if (i) {
parent.removeChild(textNode);
if (!parent.hasChildNodes()) {
parent.parentNode.removeChild(parent);
}
}
}
this.firstTextNode.data = text = textBits.join("");
return text;
},
getLength: function() {
var i = this.textNodes.length, len = 0;
while (i--) {
len += this.textNodes[i].length;
}
return len;
},
toString: function() {
var textBits = [];
for (var i = 0, len = this.textNodes.length; i < len; ++i) {
textBits[i] = "'" + this.textNodes[i].data + "'";
}
return "[Merge(" + textBits.join(",") + ")]";
}
};
var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly"];
// Allow "class" as a property name in object properties
var mappedPropertyNames = {"class" : "className"};
function CssClassApplier(cssClass, options, tagNames) {
this.cssClass = cssClass;
var normalize, i, len, propName;
var elementPropertiesFromOptions = null;
// Initialize from options object
if (typeof options == "object" && options !== null) {
tagNames = options.tagNames;
elementPropertiesFromOptions = options.elementProperties;
for (i = 0; propName = optionProperties[i++]; ) {
if (options.hasOwnProperty(propName)) {
this[propName] = options[propName];
}
}
normalize = options.normalize;
} else {
normalize = options;
}
// Backwards compatibility: the second parameter can also be a Boolean indicating whether normalization
this.normalize = (typeof normalize == "undefined") ? true : normalize;
// Initialize element properties and attribute exceptions
this.attrExceptions = [];
var el = document.createElement(this.elementTagName);
this.elementProperties = {};
for (var p in elementPropertiesFromOptions) {
if (elementPropertiesFromOptions.hasOwnProperty(p)) {
// Map "class" to "className"
if (mappedPropertyNames.hasOwnProperty(p)) {
p = mappedPropertyNames[p];
}
el[p] = elementPropertiesFromOptions[p];
// Copy the property back from the dummy element so that later comparisons to check whether elements
// may be removed are checking against the right value. For example, the href property of an element
// returns a fully qualified URL even if it was previously assigned a relative URL.
this.elementProperties[p] = el[p];
this.attrExceptions.push(p);
}
}
this.elementSortedClassName = this.elementProperties.hasOwnProperty("className") ?
sortClassName(this.elementProperties.className + " " + cssClass) : cssClass;
// Initialize tag names
this.applyToAnyTagName = false;
var type = typeof tagNames;
if (type == "string") {
if (tagNames == "*") {
this.applyToAnyTagName = true;
} else {
this.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
}
} else if (type == "object" && typeof tagNames.length == "number") {
this.tagNames = [];
for (i = 0, len = tagNames.length; i < len; ++i) {
if (tagNames[i] == "*") {
this.applyToAnyTagName = true;
} else {
this.tagNames.push(tagNames[i].toLowerCase());
}
}
} else {
this.tagNames = [this.elementTagName];
}
}
CssClassApplier.prototype = {
elementTagName: defaultTagName,
elementProperties: {},
ignoreWhiteSpace: true,
applyToEditableOnly: false,
hasClass: function(node) {
return node.nodeType == 1 && dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && hasClass(node, this.cssClass);
},
getSelfOrAncestorWithClass: function(node) {
while (node) {
if (this.hasClass(node, this.cssClass)) {
return node;
}
node = node.parentNode;
}
return null;
},
isModifiable: function(node) {
return !this.applyToEditableOnly || isEditable(node);
},
// White space adjacent to an unwrappable node can be ignored for wrapping
isIgnorableWhiteSpaceNode: function(node) {
return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
},
// Normalizes nodes after applying a CSS class to a Range.
postApply: function(textNodes, range, isUndo) {
var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
var merges = [], currentMerge;
var rangeStartNode = firstNode, rangeEndNode = lastNode;
var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
var textNode, precedingTextNode;
for (var i = 0, len = textNodes.length; i < len; ++i) {
textNode = textNodes[i];
precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
if (precedingTextNode) {
if (!currentMerge) {
currentMerge = new Merge(precedingTextNode);
merges.push(currentMerge);
}
currentMerge.textNodes.push(textNode);
if (textNode === firstNode) {
rangeStartNode = currentMerge.firstTextNode;
rangeStartOffset = rangeStartNode.length;
}
if (textNode === lastNode) {
rangeEndNode = currentMerge.firstTextNode;
rangeEndOffset = currentMerge.getLength();
}
} else {
currentMerge = null;
}
}
// Test whether the first node after the range needs merging
var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
if (nextTextNode) {
if (!currentMerge) {
currentMerge = new Merge(lastNode);
merges.push(currentMerge);
}
currentMerge.textNodes.push(nextTextNode);
}
// Do the merges
if (merges.length) {
for (i = 0, len = merges.length; i < len; ++i) {
merges[i].doMerge();
}
// Set the range boundaries
range.setStart(rangeStartNode, rangeStartOffset);
range.setEnd(rangeEndNode, rangeEndOffset);
}
},
createContainer: function(doc) {
var el = doc.createElement(this.elementTagName);
api.util.extend(el, this.elementProperties);
addClass(el, this.cssClass);
return el;
},
applyToTextNode: function(textNode) {
var parent = textNode.parentNode;
if (parent.childNodes.length == 1 && dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
addClass(parent, this.cssClass);
} else {
var el = this.createContainer(dom.getDocument(textNode));
textNode.parentNode.insertBefore(el, textNode);
el.appendChild(textNode);
}
},
isRemovable: function(el) {
return el.tagName.toLowerCase() == this.elementTagName
&& getSortedClassName(el) == this.elementSortedClassName
&& elementHasProps(el, this.elementProperties)
&& !elementHasNonClassAttributes(el, this.attrExceptions)
&& this.isModifiable(el);
},
undoToTextNode: function(textNode, range, ancestorWithClass) {
if (!range.containsNode(ancestorWithClass)) {
// Split out the portion of the ancestor from which we can remove the CSS class
//var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
var ancestorRange = range.cloneRange();
ancestorRange.selectNode(ancestorWithClass);
if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)/* && isSplitPoint(range.endContainer, range.endOffset)*/) {
splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, [range]);
range.setEndAfter(ancestorWithClass);
}
if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)/* && isSplitPoint(range.startContainer, range.startOffset)*/) {
ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, [range]);
}
}
if (this.isRemovable(ancestorWithClass)) {
replaceWithOwnChildren(ancestorWithClass);
} else {
removeClass(ancestorWithClass, this.cssClass);
}
},
applyToRange: function(range) {
range.splitBoundaries();
var textNodes = getEffectiveTextNodes(range);
if (textNodes.length) {
var textNode;
for (var i = 0, len = textNodes.length; i < len; ++i) {
textNode = textNodes[i];
if (!this.isIgnorableWhiteSpaceNode(textNode) && !this.getSelfOrAncestorWithClass(textNode)
&& this.isModifiable(textNode)) {
this.applyToTextNode(textNode);
}
}
range.setStart(textNodes[0], 0);
textNode = textNodes[textNodes.length - 1];
range.setEnd(textNode, textNode.length);
if (this.normalize) {
this.postApply(textNodes, range, false);
}
}
},
applyToSelection: function(win) {
win = win || window;
var sel = api.getSelection(win);
var range, ranges = sel.getAllRanges();
sel.removeAllRanges();
var i = ranges.length;
while (i--) {
range = ranges[i];
this.applyToRange(range);
sel.addRange(range);
}
},
undoToRange: function(range) {
range.splitBoundaries();
var textNodes = getEffectiveTextNodes(range);
var textNode, ancestorWithClass;
var lastTextNode = textNodes[textNodes.length - 1];
if (textNodes.length) {
for (var i = 0, len = textNodes.length; i < len; ++i) {
textNode = textNodes[i];
ancestorWithClass = this.getSelfOrAncestorWithClass(textNode);
if (ancestorWithClass && this.isModifiable(textNode)) {
this.undoToTextNode(textNode, range, ancestorWithClass);
}
// Ensure the range is still valid
range.setStart(textNodes[0], 0);
range.setEnd(lastTextNode, lastTextNode.length);
}
if (this.normalize) {
this.postApply(textNodes, range, true);
}
}
},
undoToSelection: function(win) {
win = win || window;
var sel = api.getSelection(win);
var ranges = sel.getAllRanges(), range;
sel.removeAllRanges();
for (var i = 0, len = ranges.length; i < len; ++i) {
range = ranges[i];
this.undoToRange(range);
sel.addRange(range);
}
},
getTextSelectedByRange: function(textNode, range) {
var textRange = range.cloneRange();
textRange.selectNodeContents(textNode);
var intersectionRange = textRange.intersection(range);
var text = intersectionRange ? intersectionRange.toString() : "";
textRange.detach();
return text;
},
isAppliedToRange: function(range) {
if (range.collapsed) {
return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
} else {
var textNodes = range.getNodes( [3] );
for (var i = 0, textNode; textNode = textNodes[i++]; ) {
if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode)
&& this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
return false;
}
}
return true;
}
},
isAppliedToSelection: function(win) {
win = win || window;
var sel = api.getSelection(win);
var ranges = sel.getAllRanges();
var i = ranges.length;
while (i--) {
if (!this.isAppliedToRange(ranges[i])) {
return false;
}
}
return true;
},
toggleRange: function(range) {
if (this.isAppliedToRange(range)) {
this.undoToRange(range);
} else {
this.applyToRange(range);
}
},
toggleSelection: function(win) {
if (this.isAppliedToSelection(win)) {
this.undoToSelection(win);
} else {
this.applyToSelection(win);
}
},
detach: function() {}
};
function createCssClassApplier(cssClass, options, tagNames) {
return new CssClassApplier(cssClass, options, tagNames);
}
CssClassApplier.util = {
hasClass: hasClass,
addClass: addClass,
removeClass: removeClass,
hasSameClasses: haveSameClasses,
replaceWithOwnChildren: replaceWithOwnChildren,
elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
elementHasNonClassAttributes: elementHasNonClassAttributes,
splitNodeAt: splitNodeAt,
isEditableElement: isEditableElement,
isEditingHost: isEditingHost,
isEditable: isEditable
};
api.CssClassApplier = CssClassApplier;
api.createCssClassApplier = createCssClassApplier;
});
+612
View File
@@ -0,0 +1,612 @@
/**
* Highlighter module for Rangy, a cross-browser JavaScript range and selection library
* https://github.com/timdown/rangy
*
* Depends on Rangy core, ClassApplier and optionally TextRange modules.
*
* Copyright 2015, Tim Down
* Licensed under the MIT license.
* Version: 1.3.0
* Build date: 10 May 2015
*/
(function(factory, root) {
// No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
factory(root.rangy);
})(function(rangy) {
rangy.createModule("Highlighter", ["ClassApplier"], function(api, module) {
var dom = api.dom;
var contains = dom.arrayContains;
var getBody = dom.getBody;
var createOptions = api.util.createOptions;
var forEach = api.util.forEach;
var nextHighlightId = 1;
// Puts highlights in order, last in document first.
function compareHighlights(h1, h2) {
return h1.characterRange.start - h2.characterRange.start;
}
function getContainerElement(doc, id) {
return id ? doc.getElementById(id) : getBody(doc);
}
/*----------------------------------------------------------------------------------------------------------------*/
var highlighterTypes = {};
function HighlighterType(type, converterCreator) {
this.type = type;
this.converterCreator = converterCreator;
}
HighlighterType.prototype.create = function() {
var converter = this.converterCreator();
converter.type = this.type;
return converter;
};
function registerHighlighterType(type, converterCreator) {
highlighterTypes[type] = new HighlighterType(type, converterCreator);
}
function getConverter(type) {
var highlighterType = highlighterTypes[type];
if (highlighterType instanceof HighlighterType) {
return highlighterType.create();
} else {
throw new Error("Highlighter type '" + type + "' is not valid");
}
}
api.registerHighlighterType = registerHighlighterType;
/*----------------------------------------------------------------------------------------------------------------*/
function CharacterRange(start, end) {
this.start = start;
this.end = end;
}
CharacterRange.prototype = {
intersects: function(charRange) {
return this.start < charRange.end && this.end > charRange.start;
},
isContiguousWith: function(charRange) {
return this.start == charRange.end || this.end == charRange.start;
},
union: function(charRange) {
return new CharacterRange(Math.min(this.start, charRange.start), Math.max(this.end, charRange.end));
},
intersection: function(charRange) {
return new CharacterRange(Math.max(this.start, charRange.start), Math.min(this.end, charRange.end));
},
getComplements: function(charRange) {
var ranges = [];
if (this.start >= charRange.start) {
if (this.end <= charRange.end) {
return [];
}
ranges.push(new CharacterRange(charRange.end, this.end));
} else {
ranges.push(new CharacterRange(this.start, Math.min(this.end, charRange.start)));
if (this.end > charRange.end) {
ranges.push(new CharacterRange(charRange.end, this.end));
}
}
return ranges;
},
toString: function() {
return "[CharacterRange(" + this.start + ", " + this.end + ")]";
}
};
CharacterRange.fromCharacterRange = function(charRange) {
return new CharacterRange(charRange.start, charRange.end);
};
/*----------------------------------------------------------------------------------------------------------------*/
var textContentConverter = {
rangeToCharacterRange: function(range, containerNode) {
var bookmark = range.getBookmark(containerNode);
return new CharacterRange(bookmark.start, bookmark.end);
},
characterRangeToRange: function(doc, characterRange, containerNode) {
var range = api.createRange(doc);
range.moveToBookmark({
start: characterRange.start,
end: characterRange.end,
containerNode: containerNode
});
return range;
},
serializeSelection: function(selection, containerNode) {
var ranges = selection.getAllRanges(), rangeCount = ranges.length;
var rangeInfos = [];
var backward = rangeCount == 1 && selection.isBackward();
for (var i = 0, len = ranges.length; i < len; ++i) {
rangeInfos[i] = {
characterRange: this.rangeToCharacterRange(ranges[i], containerNode),
backward: backward
};
}
return rangeInfos;
},
restoreSelection: function(selection, savedSelection, containerNode) {
selection.removeAllRanges();
var doc = selection.win.document;
for (var i = 0, len = savedSelection.length, range, rangeInfo, characterRange; i < len; ++i) {
rangeInfo = savedSelection[i];
characterRange = rangeInfo.characterRange;
range = this.characterRangeToRange(doc, rangeInfo.characterRange, containerNode);
selection.addRange(range, rangeInfo.backward);
}
}
};
registerHighlighterType("textContent", function() {
return textContentConverter;
});
/*----------------------------------------------------------------------------------------------------------------*/
// Lazily load the TextRange-based converter so that the dependency is only checked when required.
registerHighlighterType("TextRange", (function() {
var converter;
return function() {
if (!converter) {
// Test that textRangeModule exists and is supported
var textRangeModule = api.modules.TextRange;
if (!textRangeModule) {
throw new Error("TextRange module is missing.");
} else if (!textRangeModule.supported) {
throw new Error("TextRange module is present but not supported.");
}
converter = {
rangeToCharacterRange: function(range, containerNode) {
return CharacterRange.fromCharacterRange( range.toCharacterRange(containerNode) );
},
characterRangeToRange: function(doc, characterRange, containerNode) {
var range = api.createRange(doc);
range.selectCharacters(containerNode, characterRange.start, characterRange.end);
return range;
},
serializeSelection: function(selection, containerNode) {
return selection.saveCharacterRanges(containerNode);
},
restoreSelection: function(selection, savedSelection, containerNode) {
selection.restoreCharacterRanges(containerNode, savedSelection);
}
};
}
return converter;
};
})());
/*----------------------------------------------------------------------------------------------------------------*/
function Highlight(doc, characterRange, classApplier, converter, id, containerElementId) {
if (id) {
this.id = id;
nextHighlightId = Math.max(nextHighlightId, id + 1);
} else {
this.id = nextHighlightId++;
}
this.characterRange = characterRange;
this.doc = doc;
this.classApplier = classApplier;
this.converter = converter;
this.containerElementId = containerElementId || null;
this.applied = false;
}
Highlight.prototype = {
getContainerElement: function() {
return getContainerElement(this.doc, this.containerElementId);
},
getRange: function() {
return this.converter.characterRangeToRange(this.doc, this.characterRange, this.getContainerElement());
},
fromRange: function(range) {
this.characterRange = this.converter.rangeToCharacterRange(range, this.getContainerElement());
},
getText: function() {
return this.getRange().toString();
},
containsElement: function(el) {
return this.getRange().containsNodeContents(el.firstChild);
},
unapply: function() {
this.classApplier.undoToRange(this.getRange());
this.applied = false;
},
apply: function() {
this.classApplier.applyToRange(this.getRange());
this.applied = true;
},
getHighlightElements: function() {
return this.classApplier.getElementsWithClassIntersectingRange(this.getRange());
},
toString: function() {
return "[Highlight(ID: " + this.id + ", class: " + this.classApplier.className + ", character range: " +
this.characterRange.start + " - " + this.characterRange.end + ")]";
}
};
/*----------------------------------------------------------------------------------------------------------------*/
function Highlighter(doc, type) {
type = type || "textContent";
this.doc = doc || document;
this.classAppliers = {};
this.highlights = [];
this.converter = getConverter(type);
}
Highlighter.prototype = {
addClassApplier: function(classApplier) {
this.classAppliers[classApplier.className] = classApplier;
},
getHighlightForElement: function(el) {
var highlights = this.highlights;
for (var i = 0, len = highlights.length; i < len; ++i) {
if (highlights[i].containsElement(el)) {
return highlights[i];
}
}
return null;
},
removeHighlights: function(highlights) {
for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) {
highlight = this.highlights[i];
if (contains(highlights, highlight)) {
highlight.unapply();
this.highlights.splice(i--, 1);
}
}
},
removeAllHighlights: function() {
this.removeHighlights(this.highlights);
},
getIntersectingHighlights: function(ranges) {
// Test each range against each of the highlighted ranges to see whether they overlap
var intersectingHighlights = [], highlights = this.highlights;
forEach(ranges, function(range) {
//var selCharRange = converter.rangeToCharacterRange(range);
forEach(highlights, function(highlight) {
if (range.intersectsRange( highlight.getRange() ) && !contains(intersectingHighlights, highlight)) {
intersectingHighlights.push(highlight);
}
});
});
return intersectingHighlights;
},
highlightCharacterRanges: function(className, charRanges, options) {
var i, len, j;
var highlights = this.highlights;
var converter = this.converter;
var doc = this.doc;
var highlightsToRemove = [];
var classApplier = className ? this.classAppliers[className] : null;
options = createOptions(options, {
containerElementId: null,
exclusive: true
});
var containerElementId = options.containerElementId;
var exclusive = options.exclusive;
var containerElement, containerElementRange, containerElementCharRange;
if (containerElementId) {
containerElement = this.doc.getElementById(containerElementId);
if (containerElement) {
containerElementRange = api.createRange(this.doc);
containerElementRange.selectNodeContents(containerElement);
containerElementCharRange = new CharacterRange(0, containerElementRange.toString().length);
}
}
var charRange, highlightCharRange, removeHighlight, isSameClassApplier, highlightsToKeep, splitHighlight;
for (i = 0, len = charRanges.length; i < len; ++i) {
charRange = charRanges[i];
highlightsToKeep = [];
// Restrict character range to container element, if it exists
if (containerElementCharRange) {
charRange = charRange.intersection(containerElementCharRange);
}
// Ignore empty ranges
if (charRange.start == charRange.end) {
continue;
}
// Check for intersection with existing highlights. For each intersection, create a new highlight
// which is the union of the highlight range and the selected range
for (j = 0; j < highlights.length; ++j) {
removeHighlight = false;
if (containerElementId == highlights[j].containerElementId) {
highlightCharRange = highlights[j].characterRange;
isSameClassApplier = (classApplier == highlights[j].classApplier);
splitHighlight = !isSameClassApplier && exclusive;
// Replace the existing highlight if it needs to be:
// 1. merged (isSameClassApplier)
// 2. partially or entirely erased (className === null)
// 3. partially or entirely replaced (isSameClassApplier == false && exclusive == true)
if ( (highlightCharRange.intersects(charRange) || highlightCharRange.isContiguousWith(charRange)) &&
(isSameClassApplier || splitHighlight) ) {
// Remove existing highlights, keeping the unselected parts
if (splitHighlight) {
forEach(highlightCharRange.getComplements(charRange), function(rangeToAdd) {
highlightsToKeep.push( new Highlight(doc, rangeToAdd, highlights[j].classApplier, converter, null, containerElementId) );
});
}
removeHighlight = true;
if (isSameClassApplier) {
charRange = highlightCharRange.union(charRange);
}
}
}
if (removeHighlight) {
highlightsToRemove.push(highlights[j]);
highlights[j] = new Highlight(doc, highlightCharRange.union(charRange), classApplier, converter, null, containerElementId);
} else {
highlightsToKeep.push(highlights[j]);
}
}
// Add new range
if (classApplier) {
highlightsToKeep.push(new Highlight(doc, charRange, classApplier, converter, null, containerElementId));
}
this.highlights = highlights = highlightsToKeep;
}
// Remove the old highlights
forEach(highlightsToRemove, function(highlightToRemove) {
highlightToRemove.unapply();
});
// Apply new highlights
var newHighlights = [];
forEach(highlights, function(highlight) {
if (!highlight.applied) {
highlight.apply();
newHighlights.push(highlight);
}
});
return newHighlights;
},
highlightRanges: function(className, ranges, options) {
var selCharRanges = [];
var converter = this.converter;
options = createOptions(options, {
containerElement: null,
exclusive: true
});
var containerElement = options.containerElement;
var containerElementId = containerElement ? containerElement.id : null;
var containerElementRange;
if (containerElement) {
containerElementRange = api.createRange(containerElement);
containerElementRange.selectNodeContents(containerElement);
}
forEach(ranges, function(range) {
var scopedRange = containerElement ? containerElementRange.intersection(range) : range;
selCharRanges.push( converter.rangeToCharacterRange(scopedRange, containerElement || getBody(range.getDocument())) );
});
return this.highlightCharacterRanges(className, selCharRanges, {
containerElementId: containerElementId,
exclusive: options.exclusive
});
},
highlightSelection: function(className, options) {
var converter = this.converter;
var classApplier = className ? this.classAppliers[className] : false;
options = createOptions(options, {
containerElementId: null,
selection: api.getSelection(this.doc),
exclusive: true
});
var containerElementId = options.containerElementId;
var exclusive = options.exclusive;
var selection = options.selection;
var doc = selection.win.document;
var containerElement = getContainerElement(doc, containerElementId);
if (!classApplier && className !== false) {
throw new Error("No class applier found for class '" + className + "'");
}
// Store the existing selection as character ranges
var serializedSelection = converter.serializeSelection(selection, containerElement);
// Create an array of selected character ranges
var selCharRanges = [];
forEach(serializedSelection, function(rangeInfo) {
selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) );
});
var newHighlights = this.highlightCharacterRanges(className, selCharRanges, {
containerElementId: containerElementId,
exclusive: exclusive
});
// Restore selection
converter.restoreSelection(selection, serializedSelection, containerElement);
return newHighlights;
},
unhighlightSelection: function(selection) {
selection = selection || api.getSelection(this.doc);
var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() );
this.removeHighlights(intersectingHighlights);
selection.removeAllRanges();
return intersectingHighlights;
},
getHighlightsInSelection: function(selection) {
selection = selection || api.getSelection(this.doc);
return this.getIntersectingHighlights(selection.getAllRanges());
},
selectionOverlapsHighlight: function(selection) {
return this.getHighlightsInSelection(selection).length > 0;
},
serialize: function(options) {
var highlighter = this;
var highlights = highlighter.highlights;
var serializedType, serializedHighlights, convertType, serializationConverter;
highlights.sort(compareHighlights);
options = createOptions(options, {
serializeHighlightText: false,
type: highlighter.converter.type
});
serializedType = options.type;
convertType = (serializedType != highlighter.converter.type);
if (convertType) {
serializationConverter = getConverter(serializedType);
}
serializedHighlights = ["type:" + serializedType];
forEach(highlights, function(highlight) {
var characterRange = highlight.characterRange;
var containerElement;
// Convert to the current Highlighter's type, if different from the serialization type
if (convertType) {
containerElement = highlight.getContainerElement();
characterRange = serializationConverter.rangeToCharacterRange(
highlighter.converter.characterRangeToRange(highlighter.doc, characterRange, containerElement),
containerElement
);
}
var parts = [
characterRange.start,
characterRange.end,
highlight.id,
highlight.classApplier.className,
highlight.containerElementId
];
if (options.serializeHighlightText) {
parts.push(highlight.getText());
}
serializedHighlights.push( parts.join("$") );
});
return serializedHighlights.join("|");
},
deserialize: function(serialized) {
var serializedHighlights = serialized.split("|");
var highlights = [];
var firstHighlight = serializedHighlights[0];
var regexResult;
var serializationType, serializationConverter, convertType = false;
if ( firstHighlight && (regexResult = /^type:(\w+)$/.exec(firstHighlight)) ) {
serializationType = regexResult[1];
if (serializationType != this.converter.type) {
serializationConverter = getConverter(serializationType);
convertType = true;
}
serializedHighlights.shift();
} else {
throw new Error("Serialized highlights are invalid.");
}
var classApplier, highlight, characterRange, containerElementId, containerElement;
for (var i = serializedHighlights.length, parts; i-- > 0; ) {
parts = serializedHighlights[i].split("$");
characterRange = new CharacterRange(+parts[0], +parts[1]);
containerElementId = parts[4] || null;
// Convert to the current Highlighter's type, if different from the serialization type
if (convertType) {
containerElement = getContainerElement(this.doc, containerElementId);
characterRange = this.converter.rangeToCharacterRange(
serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement),
containerElement
);
}
classApplier = this.classAppliers[ parts[3] ];
if (!classApplier) {
throw new Error("No class applier found for class '" + parts[3] + "'");
}
highlight = new Highlight(this.doc, characterRange, classApplier, this.converter, parseInt(parts[2]), containerElementId);
highlight.apply();
highlights.push(highlight);
}
this.highlights = highlights;
}
};
api.Highlighter = Highlighter;
api.createHighlighter = function(doc, rangeCharacterOffsetConverterType) {
return new Highlighter(doc, rangeCharacterOffsetConverterType);
};
});
return rangy;
}, this);
+235 -186
View File
@@ -1,195 +1,244 @@
/**
* @license Selection save and restore module for Rangy.
* Selection save and restore module for Rangy.
* Saves and restores user selections using marker invisible elements in the DOM.
*
* Part of Rangy, a cross-browser JavaScript range and selection library
* http://code.google.com/p/rangy/
* https://github.com/timdown/rangy
*
* Depends on Rangy core.
*
* Copyright 2012, Tim Down
* Copyright 2015, Tim Down
* Licensed under the MIT license.
* Version: 1.2.3
* Build date: 26 February 2012
* Version: 1.3.0
* Build date: 10 May 2015
*/
rangy.createModule("SaveRestore", function(api, module) {
api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
var dom = api.dom;
var markerTextChar = "\ufeff";
function gEBI(id, doc) {
return (doc || document).getElementById(id);
}
function insertRangeBoundaryMarker(range, atStart) {
var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
var markerEl;
var doc = dom.getDocument(range.startContainer);
// Clone the Range and collapse to the appropriate boundary point
var boundaryRange = range.cloneRange();
boundaryRange.collapse(atStart);
// Create the marker element containing a single invisible character using DOM methods and insert it
markerEl = doc.createElement("span");
markerEl.id = markerId;
markerEl.style.lineHeight = "0";
markerEl.style.display = "none";
markerEl.className = "rangySelectionBoundary";
markerEl.appendChild(doc.createTextNode(markerTextChar));
boundaryRange.insertNode(markerEl);
boundaryRange.detach();
return markerEl;
}
function setRangeBoundary(doc, range, markerId, atStart) {
var markerEl = gEBI(markerId, doc);
if (markerEl) {
range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
markerEl.parentNode.removeChild(markerEl);
} else {
module.warn("Marker element has been removed. Cannot restore selection.");
}
}
function compareRanges(r1, r2) {
return r2.compareBoundaryPoints(r1.START_TO_START, r1);
}
function saveSelection(win) {
win = win || window;
var doc = win.document;
if (!api.isSelectionValid(win)) {
module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
return;
}
var sel = api.getSelection(win);
var ranges = sel.getAllRanges();
var rangeInfos = [], startEl, endEl, range;
// Order the ranges by position within the DOM, latest first
ranges.sort(compareRanges);
for (var i = 0, len = ranges.length; i < len; ++i) {
range = ranges[i];
if (range.collapsed) {
endEl = insertRangeBoundaryMarker(range, false);
rangeInfos.push({
markerId: endEl.id,
collapsed: true
});
} else {
endEl = insertRangeBoundaryMarker(range, false);
startEl = insertRangeBoundaryMarker(range, true);
rangeInfos[i] = {
startMarkerId: startEl.id,
endMarkerId: endEl.id,
collapsed: false,
backwards: ranges.length == 1 && sel.isBackwards()
};
}
}
// Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
// between its markers
for (i = len - 1; i >= 0; --i) {
range = ranges[i];
if (range.collapsed) {
range.collapseBefore(gEBI(rangeInfos[i].markerId, doc));
} else {
range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
}
}
// Ensure current selection is unaffected
sel.setRanges(ranges);
return {
win: win,
doc: doc,
rangeInfos: rangeInfos,
restored: false
};
}
function restoreSelection(savedSelection, preserveDirection) {
if (!savedSelection.restored) {
var rangeInfos = savedSelection.rangeInfos;
var sel = api.getSelection(savedSelection.win);
var ranges = [];
// Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
// normalization affecting previously restored ranges.
for (var len = rangeInfos.length, i = len - 1, rangeInfo, range; i >= 0; --i) {
rangeInfo = rangeInfos[i];
range = api.createRange(savedSelection.doc);
if (rangeInfo.collapsed) {
var markerEl = gEBI(rangeInfo.markerId, savedSelection.doc);
if (markerEl) {
markerEl.style.display = "inline";
var previousNode = markerEl.previousSibling;
// Workaround for issue 17
if (previousNode && previousNode.nodeType == 3) {
markerEl.parentNode.removeChild(markerEl);
range.collapseToPoint(previousNode, previousNode.length);
} else {
range.collapseBefore(markerEl);
markerEl.parentNode.removeChild(markerEl);
}
} else {
module.warn("Marker element has been removed. Cannot restore selection.");
}
} else {
setRangeBoundary(savedSelection.doc, range, rangeInfo.startMarkerId, true);
setRangeBoundary(savedSelection.doc, range, rangeInfo.endMarkerId, false);
}
// Normalizing range boundaries is only viable if the selection contains only one range. For example,
// if the selection contained two ranges that were both contained within the same single text node,
// both would alter the same text node when restoring and break the other range.
if (len == 1) {
range.normalizeBoundaries();
}
ranges[i] = range;
}
if (len == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backwards) {
sel.removeAllRanges();
sel.addRange(ranges[0], true);
} else {
sel.setRanges(ranges);
}
savedSelection.restored = true;
}
}
function removeMarkerElement(doc, markerId) {
var markerEl = gEBI(markerId, doc);
if (markerEl) {
markerEl.parentNode.removeChild(markerEl);
}
}
function removeMarkers(savedSelection) {
var rangeInfos = savedSelection.rangeInfos;
for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
rangeInfo = rangeInfos[i];
if (rangeInfo.collapsed) {
removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
} else {
removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
}
}
}
api.saveSelection = saveSelection;
api.restoreSelection = restoreSelection;
api.removeMarkerElement = removeMarkerElement;
api.removeMarkers = removeMarkers;
});
(function(factory, root) {
// No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
factory(root.rangy);
})(function(rangy) {
rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) {
var dom = api.dom;
var removeNode = dom.removeNode;
var isDirectionBackward = api.Selection.isDirectionBackward;
var markerTextChar = "\ufeff";
function gEBI(id, doc) {
return (doc || document).getElementById(id);
}
function insertRangeBoundaryMarker(range, atStart) {
var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
var markerEl;
var doc = dom.getDocument(range.startContainer);
// Clone the Range and collapse to the appropriate boundary point
var boundaryRange = range.cloneRange();
boundaryRange.collapse(atStart);
// Create the marker element containing a single invisible character using DOM methods and insert it
markerEl = doc.createElement("span");
markerEl.id = markerId;
markerEl.style.lineHeight = "0";
markerEl.style.display = "none";
markerEl.className = "rangySelectionBoundary";
markerEl.appendChild(doc.createTextNode(markerTextChar));
boundaryRange.insertNode(markerEl);
return markerEl;
}
function setRangeBoundary(doc, range, markerId, atStart) {
var markerEl = gEBI(markerId, doc);
if (markerEl) {
range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
removeNode(markerEl);
} else {
module.warn("Marker element has been removed. Cannot restore selection.");
}
}
function compareRanges(r1, r2) {
return r2.compareBoundaryPoints(r1.START_TO_START, r1);
}
function saveRange(range, direction) {
var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
var backward = isDirectionBackward(direction);
if (range.collapsed) {
endEl = insertRangeBoundaryMarker(range, false);
return {
document: doc,
markerId: endEl.id,
collapsed: true
};
} else {
endEl = insertRangeBoundaryMarker(range, false);
startEl = insertRangeBoundaryMarker(range, true);
return {
document: doc,
startMarkerId: startEl.id,
endMarkerId: endEl.id,
collapsed: false,
backward: backward,
toString: function() {
return "original text: '" + text + "', new text: '" + range.toString() + "'";
}
};
}
}
function restoreRange(rangeInfo, normalize) {
var doc = rangeInfo.document;
if (typeof normalize == "undefined") {
normalize = true;
}
var range = api.createRange(doc);
if (rangeInfo.collapsed) {
var markerEl = gEBI(rangeInfo.markerId, doc);
if (markerEl) {
markerEl.style.display = "inline";
var previousNode = markerEl.previousSibling;
// Workaround for issue 17
if (previousNode && previousNode.nodeType == 3) {
removeNode(markerEl);
range.collapseToPoint(previousNode, previousNode.length);
} else {
range.collapseBefore(markerEl);
removeNode(markerEl);
}
} else {
module.warn("Marker element has been removed. Cannot restore selection.");
}
} else {
setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
}
if (normalize) {
range.normalizeBoundaries();
}
return range;
}
function saveRanges(ranges, direction) {
var rangeInfos = [], range, doc;
var backward = isDirectionBackward(direction);
// Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
ranges = ranges.slice(0);
ranges.sort(compareRanges);
for (var i = 0, len = ranges.length; i < len; ++i) {
rangeInfos[i] = saveRange(ranges[i], backward);
}
// Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
// between its markers
for (i = len - 1; i >= 0; --i) {
range = ranges[i];
doc = api.DomRange.getRangeDocument(range);
if (range.collapsed) {
range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
} else {
range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
}
}
return rangeInfos;
}
function saveSelection(win) {
if (!api.isSelectionValid(win)) {
module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
return null;
}
var sel = api.getSelection(win);
var ranges = sel.getAllRanges();
var backward = (ranges.length == 1 && sel.isBackward());
var rangeInfos = saveRanges(ranges, backward);
// Ensure current selection is unaffected
if (backward) {
sel.setSingleRange(ranges[0], backward);
} else {
sel.setRanges(ranges);
}
return {
win: win,
rangeInfos: rangeInfos,
restored: false
};
}
function restoreRanges(rangeInfos) {
var ranges = [];
// Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
// normalization affecting previously restored ranges.
var rangeCount = rangeInfos.length;
for (var i = rangeCount - 1; i >= 0; i--) {
ranges[i] = restoreRange(rangeInfos[i], true);
}
return ranges;
}
function restoreSelection(savedSelection, preserveDirection) {
if (!savedSelection.restored) {
var rangeInfos = savedSelection.rangeInfos;
var sel = api.getSelection(savedSelection.win);
var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
sel.removeAllRanges();
sel.addRange(ranges[0], true);
} else {
sel.setRanges(ranges);
}
savedSelection.restored = true;
}
}
function removeMarkerElement(doc, markerId) {
var markerEl = gEBI(markerId, doc);
if (markerEl) {
removeNode(markerEl);
}
}
function removeMarkers(savedSelection) {
var rangeInfos = savedSelection.rangeInfos;
for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
rangeInfo = rangeInfos[i];
if (rangeInfo.collapsed) {
removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
} else {
removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
}
}
}
api.util.extend(api, {
saveRange: saveRange,
restoreRange: restoreRange,
saveRanges: saveRanges,
restoreRanges: restoreRanges,
saveSelection: saveSelection,
restoreSelection: restoreSelection,
removeMarkerElement: removeMarkerElement,
removeMarkers: removeMarkers
});
});
return rangy;
}, this);
+296 -290
View File
@@ -1,300 +1,306 @@
/**
* @license Serializer module for Rangy.
* 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/
* https://github.com/timdown/rangy
*
* Depends on Rangy core.
*
* Copyright 2012, Tim Down
* Copyright 2015, Tim Down
* Licensed under the MIT license.
* Version: 1.2.3
* Build date: 26 February 2012
* Version: 1.3.0
* Build date: 10 May 2015
*/
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, "&lt;").replace(/>/g, "&gt;");
}
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;
});
(function(factory, root) {
// No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
factory(root.rangy);
})(function(rangy) {
rangy.createModule("Serializer", ["WrappedSelection"], function(api, module) {
var UNDEF = "undefined";
var util = api.util;
// encodeURIComponent and decodeURIComponent are required for cookie handling
if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
module.fail("encodeURIComponent and/or decodeURIComponent method is missing");
}
// 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, "&lt;").replace(/>/g, "&gt;");
}
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 pathParts = [], n = node;
rootNode = rootNode || dom.getDocument(node).documentElement;
while (n && n != rootNode) {
pathParts.push(dom.getNodeIndex(n, true));
n = n.parentNode;
}
return pathParts.join("/") + ":" + offset;
}
function deserializePosition(serialized, rootNode, doc) {
if (!rootNode) {
rootNode = (doc || document).documentElement;
}
var parts = serialized.split(":");
var node = rootNode;
var nodeIndices = parts[0] ? parts[0].split("/") : [], i = nodeIndices.length, nodeIndex;
while (i--) {
nodeIndex = parseInt(nodeIndices[i], 10);
if (nodeIndex < node.childNodes.length) {
node = node.childNodes[nodeIndex];
} else {
throw module.createError("deserializePosition() failed: node " + dom.inspectNode(node) +
" has no child with index " + nodeIndex + ", " + i);
}
}
return new dom.DomPosition(node, parseInt(parts[1], 10));
}
function serializeRange(range, omitChecksum, rootNode) {
rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
if (!dom.isOrIsAncestorOf(rootNode, range.commonAncestorContainer)) {
throw module.createError("serializeRange(): range " + range.inspect() +
" is not wholly contained within specified root node " + dom.inspectNode(rootNode));
}
var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
serializePosition(range.endContainer, range.endOffset, rootNode);
if (!omitChecksum) {
serialized += "{" + getElementChecksum(rootNode) + "}";
}
return serialized;
}
var deserializeRegex = /^([^,]+),([^,\{]+)(\{([^}]+)\})?$/;
function deserializeRange(serialized, rootNode, doc) {
if (rootNode) {
doc = doc || dom.getDocument(rootNode);
} else {
doc = doc || document;
rootNode = doc.documentElement;
}
var result = deserializeRegex.exec(serialized);
var checksum = result[4];
if (checksum) {
var rootNodeChecksum = getElementChecksum(rootNode);
if (checksum !== rootNodeChecksum) {
throw module.createError("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.setStartAndEnd(start.node, start.offset, end.node, end.offset);
return range;
}
function canDeserializeRange(serialized, rootNode, doc) {
if (!rootNode) {
rootNode = (doc || document).documentElement;
}
var result = deserializeRegex.exec(serialized);
var checksum = result[3];
return !checksum || checksum === getElementChecksum(rootNode);
}
function serializeSelection(selection, omitChecksum, rootNode) {
selection = api.getSelection(selection);
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;
}
util.extend(api, {
serializePosition: serializePosition,
deserializePosition: deserializePosition,
serializeRange: serializeRange,
deserializeRange: deserializeRange,
canDeserializeRange: canDeserializeRange,
serializeSelection: serializeSelection,
deserializeSelection: deserializeSelection,
canDeserializeSelection: canDeserializeSelection,
restoreSelectionFromCookie: restoreSelectionFromCookie,
saveSelectionCookie: saveSelectionCookie,
getElementChecksum: getElementChecksum,
nodeToInfoString: nodeToInfoString
});
util.crc32 = crc32;
});
return rangy;
}, this);
File diff suppressed because it is too large Load Diff