Merge branch 'MDL-51605-master' of git://github.com/damyon/moodle
This commit is contained in:
+2
-2
@@ -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
@@ -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"]});
|
||||
|
||||
+2
-2
@@ -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");
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -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
@@ -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"]});
|
||||
|
||||
+2
-2
@@ -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");
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
@@ -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();
|
||||
|
||||
|
||||
+7695
-4114
File diff suppressed because it is too large
Load Diff
+17
-10
File diff suppressed because one or more lines are too long
+7695
-4114
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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,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"
|
||||
|
||||
+1095
File diff suppressed because it is too large
Load Diff
+3533
-2925
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;
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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, "<").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;
|
||||
});
|
||||
(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, "<").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 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);
|
||||
|
||||
+1922
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user