mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-09-25 03:08:42 +00:00
Merge "Updated rangy from 1.2.2 to 1.3alpha.772"
This commit is contained in:
commit
5e70e26e77
|
@ -66,6 +66,7 @@ $wgResourceModules += array(
|
|||
'scripts' => array(
|
||||
'rangy/rangy-core.js',
|
||||
'rangy/rangy-position.js',
|
||||
'rangy/rangy-export.js',
|
||||
),
|
||||
),
|
||||
'jquery.visibleText' => $wgVisualEditorResourceTemplate + array(
|
||||
|
|
3935
modules/rangy/rangy-core.js
Normal file → Executable file
3935
modules/rangy/rangy-core.js
Normal file → Executable file
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 2011, Tim Down
|
||||
* Licensed under the MIT license.
|
||||
* Version: 1.2.2
|
||||
* Build date: 13 November 2011
|
||||
*/
|
||||
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;
|
||||
});
|
1
modules/rangy/rangy-export.js
Normal file
1
modules/rangy/rangy-export.js
Normal file
|
@ -0,0 +1 @@
|
|||
window.rangy = rangy;
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* @license Position module for Rangy.
|
||||
* Position module for Rangy.
|
||||
* Extensions to Range and Selection objects to provide access to pixel positions relative to the viewport or document.
|
||||
*
|
||||
* Part of Rangy, a cross-browser JavaScript range and selection library
|
||||
|
@ -7,21 +7,27 @@
|
|||
*
|
||||
* Depends on Rangy core.
|
||||
*
|
||||
* Copyright %%build:year%%, Tim Down
|
||||
* Copyright 2013, Tim Down
|
||||
* Licensed under the MIT license.
|
||||
* Version: %%build:version%%
|
||||
* Build date: %%build:date%%
|
||||
* Version: 1.3alpha.772
|
||||
* Build date: 26 February 2013
|
||||
*/
|
||||
rangy.createModule("Coordinates", function(api, module) {
|
||||
rangy.createModule("Position", function(api, module) {
|
||||
api.requireModules( ["WrappedSelection", "WrappedRange"] );
|
||||
|
||||
var NUMBER = "number";
|
||||
var WrappedRange = api.WrappedRange;
|
||||
var dom = api.dom, util = api.util;
|
||||
//var log = log4javascript.getLogger("rangy.position");
|
||||
|
||||
// Since Rangy can deal with multiple documents, we have to do the checks every time, unless we cache a
|
||||
// getScrollPosition function in each document. This would necessarily pollute the document's global
|
||||
// namespace, which I'm choosing to view as a greater evil than a slight performance hit.
|
||||
var NUMBER = "number", UNDEF = "undefined";
|
||||
var WrappedRange = api.WrappedRange;
|
||||
var dom = api.dom, util = api.util, DomPosition = dom.DomPosition;
|
||||
|
||||
// Feature detection
|
||||
|
||||
//var caretPositionFromPointSupported = (typeof document.caretPositionFromPoint != UNDEF);
|
||||
|
||||
// Since Rangy can deal with multiple documents which could be in different modes, we have to do the checks every
|
||||
// time, unless we cache a getScrollPosition function in each document. This would necessarily pollute the
|
||||
// document's global namespace, which I'm choosing to view as a greater evil than a slight performance hit.
|
||||
function getScrollPosition(win) {
|
||||
var x = 0, y = 0;
|
||||
if (typeof win.pageXOffset == NUMBER && typeof win.pageYOffset == NUMBER) {
|
||||
|
@ -101,264 +107,433 @@ rangy.createModule("Coordinates", function(api, module) {
|
|||
Math.min.apply(Math, lefts)
|
||||
);
|
||||
}
|
||||
|
||||
function getTextRangePosition(doc, x, y) {
|
||||
var textRange = dom.getBody(doc).createTextRange();
|
||||
textRange.moveToPoint(x, y);
|
||||
var range = new api.WrappedTextRange(textRange);
|
||||
return new DomPosition(range.startContainer, range.startOffset);
|
||||
}
|
||||
|
||||
(function() {
|
||||
function caretPositionFromPoint(doc, x, y) {
|
||||
var pos = doc.caretPositionFromPoint(x, y);
|
||||
return new DomPosition(pos.offsetNode, pos.offset);
|
||||
}
|
||||
|
||||
// Test that <span> elements support getBoundingClientRect
|
||||
var span = document.createElement("span");
|
||||
var elementSupportsGetBoundingClientRect = util.isHostMethod(span, "getBoundingClientRect");
|
||||
span = null;
|
||||
function caretRangeFromPoint(doc, x, y) {
|
||||
var range = doc.caretRangeFromPoint(x, y);
|
||||
return new DomPosition(range.startContainer, range.startOffset);
|
||||
}
|
||||
|
||||
// Test for getBoundingClientRect support in Range
|
||||
var rangeSupportsGetClientRects = false, rangeSupportsGetBoundingClientRect = false;
|
||||
if (api.features.implementsDomRange) {
|
||||
var testRange = api.createNativeRange();
|
||||
rangeSupportsGetClientRects = util.isHostMethod(testRange, "getClientRects");
|
||||
rangeSupportsGetBoundingClientRect = util.isHostMethod(testRange, "getBoundingClientRect");
|
||||
testRange.detach();
|
||||
}
|
||||
function getLastRangeRect(range) {
|
||||
var rects = (range.nativeRange || range).getClientRects();
|
||||
return (rects.length > 0) ? rects[rects.length - 1] : null;
|
||||
}
|
||||
|
||||
util.extend(api.features, {
|
||||
rangeSupportsGetBoundingClientRect: rangeSupportsGetBoundingClientRect,
|
||||
rangeSupportsGetClientRects: rangeSupportsGetClientRects,
|
||||
elementSupportsGetBoundingClientRect: elementSupportsGetBoundingClientRect
|
||||
});
|
||||
function pointIsInOrAboveRect(x, y, rect) {
|
||||
console.log("pointIsInOrAboveRect", x, y, Math.floor(rect.top), Math.floor(rect.right), Math.floor(rect.bottom), Math.floor(rect.left))
|
||||
return y < rect.bottom && x >= rect.left && x <= rect.right;
|
||||
}
|
||||
|
||||
var createClientBoundaryPosGetter = function(isStart) {
|
||||
return function() {
|
||||
var boundaryRange = this.cloneRange();
|
||||
boundaryRange.collapse(isStart);
|
||||
var rect = boundaryRange.getBoundingClientRect();
|
||||
return { x: rect[isStart ? "left" : "right"], y: rect[isStart ? "top" : "bottom"] };
|
||||
};
|
||||
};
|
||||
function positionFromPoint(doc, x, y, favourPrecedingPosition) {
|
||||
var el = doc.elementFromPoint(x, y);
|
||||
|
||||
console.log("elementFromPoint is ", el);
|
||||
|
||||
var rangeProto = api.rangePrototype;
|
||||
var range = api.createRange(doc);
|
||||
range.selectNodeContents(el);
|
||||
range.collapse(true);
|
||||
|
||||
if (api.features.implementsTextRange && elementSupportsGetBoundingClientRect) {
|
||||
rangeProto.getBoundingClientRect = function() {
|
||||
// We need a TextRange
|
||||
var textRange = WrappedRange.rangeToTextRange(this);
|
||||
var node = el.firstChild, offset, rect, textLen;
|
||||
|
||||
// Work around table problems (table cell bounding rects seem not to count if TextRange spans cells)
|
||||
var cells = this.getNodes([1], function(el) {
|
||||
return /^t[dh]$/i.test(el.tagName);
|
||||
});
|
||||
|
||||
// Merge rects for each cell selected by the range into overall rect
|
||||
var rect, rects = [];
|
||||
if (cells.length > 0) {
|
||||
var lastTable = getAncestorElement(this.startContainer, "table");
|
||||
|
||||
for (var i = 0, cell, tempTextRange, table, subRange, subRect; cell = cells[i]; ++i) {
|
||||
// Handle non-table sections of the range
|
||||
table = getAncestorElement(cell, "table");
|
||||
if (!lastTable || table != lastTable) {
|
||||
// There is a section of the range prior to the current table, or lying between tables.
|
||||
// Merge in its rect
|
||||
subRange = this.cloneRange();
|
||||
if (lastTable) {
|
||||
subRange.setStartAfter(lastTable);
|
||||
if (!node) {
|
||||
node = el.parentNode;
|
||||
offset = dom.getNodeIndex(el);
|
||||
if (!favourPrecedingPosition) {
|
||||
++offset;
|
||||
}
|
||||
} else {
|
||||
// Search through the text node children of el
|
||||
main: while (node) {
|
||||
console.log(node);
|
||||
if (node.nodeType == 3) {
|
||||
// Go through the text node character by character
|
||||
for (offset = 0, textLen = node.length; offset <= textLen; ++offset) {
|
||||
range.setEnd(node, offset);
|
||||
rect = getLastRangeRect(range);
|
||||
if (rect && pointIsInOrAboveRect(x, y, rect)) {
|
||||
// We've gone past the point. Now we check which side (left or right) of the character the point is nearer to
|
||||
if (rect.right - x > x - rect.left) {
|
||||
--offset;
|
||||
}
|
||||
subRange.setEndBefore(table);
|
||||
rects.push(WrappedRange.rangeToTextRange(subRange).getBoundingClientRect());
|
||||
break main;
|
||||
}
|
||||
|
||||
if (this.containsNode(cell)) {
|
||||
rects.push(cell.getBoundingClientRect());
|
||||
} else {
|
||||
tempTextRange = textRange.duplicate();
|
||||
tempTextRange.moveToElementText(cell);
|
||||
if (tempTextRange.compareEndPoints("StartToStart", textRange) == -1) {
|
||||
tempTextRange.setEndPoint("StartToStart", textRange);
|
||||
} else if (tempTextRange.compareEndPoints("EndToEnd", textRange) == 1) {
|
||||
tempTextRange.setEndPoint("EndToEnd", textRange);
|
||||
}
|
||||
rects.push(tempTextRange.getBoundingClientRect());
|
||||
}
|
||||
lastTable = table;
|
||||
}
|
||||
|
||||
// Merge in the rect for any content lying after the final table
|
||||
var endTable = getAncestorElement(this.endContainer, "table");
|
||||
if (!endTable && lastTable) {
|
||||
subRange = this.cloneRange();
|
||||
subRange.setStartAfter(lastTable);
|
||||
rects.push(WrappedRange.rangeToTextRange(subRange).getBoundingClientRect());
|
||||
}
|
||||
rect = mergeRects(rects);
|
||||
} else {
|
||||
rect = textRange.getBoundingClientRect();
|
||||
// Handle elements
|
||||
range.setEndAfter(node);
|
||||
rect = getLastRangeRect(range);
|
||||
if (rect && pointIsInOrAboveRect(x, y, rect)) {
|
||||
offset = dom.getNodeIndex(node);
|
||||
node = el.parentNode;
|
||||
if (!favourPrecedingPosition) {
|
||||
++offset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
node = node.nextSibling;
|
||||
}
|
||||
if (!node) {
|
||||
node = el;
|
||||
offset = el.childNodes.length;
|
||||
}
|
||||
}
|
||||
|
||||
return new DomPosition(node, offset);
|
||||
}
|
||||
|
||||
function createCaretPositionFromPointGetter(doc) {
|
||||
if (api.features.implementsTextRange) {
|
||||
return getTextRangePosition;
|
||||
} else if (typeof doc.caretPositionFromPoint != UNDEF) {
|
||||
return caretPositionFromPoint;
|
||||
} else if (typeof doc.caretRangeFromPoint != UNDEF) {
|
||||
return caretRangeFromPoint;
|
||||
} else if (typeof doc.elementFromPoint != UNDEF && rangeSupportsGetClientRects) {
|
||||
return positionFromPoint;
|
||||
} else {
|
||||
throw module.createError("createCaretPositionFromPointGetter(): Browser does not provide a recognised method to create a selection from pixel coordinates");
|
||||
}
|
||||
}
|
||||
|
||||
function createRangeFromPoints(startX, startY, endX, endY, doc) {
|
||||
doc = dom.getContentDocument(doc, module, "createRangeFromPoints");
|
||||
var positionFinder = createCaretPositionFromPointGetter(doc);
|
||||
var startPos = positionFinder(doc, startX, startY, false);
|
||||
var endPos = positionFinder(doc, endX, endY, true);
|
||||
console.log(startPos.node, startPos.offset, endPos.node, endPos.offset);
|
||||
var range = api.createRange(doc);
|
||||
range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset);
|
||||
return range;
|
||||
}
|
||||
|
||||
function moveSelectionToPoints(anchorX, anchorY, focusX, focusY, doc) {
|
||||
var startX, startY, endX, endY;
|
||||
|
||||
// Detect backward selection for coordinates and flip start and end coordinates if necessary
|
||||
var backward = focusY < anchorY || (anchorY == focusY && focusX < anchorX);
|
||||
|
||||
if (backward) {
|
||||
startX = focusX;
|
||||
startY = focusY;
|
||||
endX = anchorX;
|
||||
endY = anchorY;
|
||||
} else {
|
||||
startX = anchorX;
|
||||
startY = anchorY;
|
||||
endX = focusX;
|
||||
endY = focusY;
|
||||
}
|
||||
|
||||
var sel = rangy.getSelection(doc);
|
||||
var range = createRangeFromPoints(startX, startY, endX, endY, doc);
|
||||
sel.setSingleRange(range);
|
||||
return sel;
|
||||
}
|
||||
|
||||
// Test that <span> elements support getBoundingClientRect
|
||||
var span = document.createElement("span");
|
||||
var elementSupportsGetBoundingClientRect = util.isHostMethod(span, "getBoundingClientRect");
|
||||
span = null;
|
||||
|
||||
// Test for getBoundingClientRect support in Range
|
||||
var rangeSupportsGetClientRects = false, rangeSupportsGetBoundingClientRect = false;
|
||||
if (api.features.implementsDomRange) {
|
||||
var testRange = api.createNativeRange();
|
||||
rangeSupportsGetClientRects = util.isHostMethod(testRange, "getClientRects");
|
||||
rangeSupportsGetBoundingClientRect = util.isHostMethod(testRange, "getBoundingClientRect");
|
||||
testRange.detach();
|
||||
}
|
||||
|
||||
util.extend(api.features, {
|
||||
rangeSupportsGetBoundingClientRect: rangeSupportsGetBoundingClientRect,
|
||||
rangeSupportsGetClientRects: rangeSupportsGetClientRects,
|
||||
elementSupportsGetBoundingClientRect: elementSupportsGetBoundingClientRect
|
||||
});
|
||||
|
||||
var createClientBoundaryPosGetter = function(isStart) {
|
||||
return function() {
|
||||
var boundaryRange = this.cloneRange();
|
||||
boundaryRange.collapse(isStart);
|
||||
var rect = boundaryRange.getBoundingClientRect();
|
||||
return {
|
||||
x: rect[isStart ? "left" : "right"],
|
||||
y: rect[isStart ? "top" : "bottom"]
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
var rangeProto = api.rangePrototype;
|
||||
|
||||
if (api.features.implementsTextRange && elementSupportsGetBoundingClientRect) {
|
||||
rangeProto.getBoundingClientRect = function() {
|
||||
// We need a TextRange
|
||||
var textRange = WrappedRange.rangeToTextRange(this);
|
||||
|
||||
// Work around table problems (table cell bounding rects seem not to count if TextRange spans cells)
|
||||
var cells = this.getNodes([1], function(el) {
|
||||
return /^t[dh]$/i.test(el.tagName);
|
||||
});
|
||||
|
||||
// Merge rects for each cell selected by the range into overall rect
|
||||
var rect, rects = [];
|
||||
if (cells.length > 0) {
|
||||
var lastTable = getAncestorElement(this.startContainer, "table");
|
||||
|
||||
for (var i = 0, cell, tempTextRange, table, subRange, subRect; cell = cells[i]; ++i) {
|
||||
// Handle non-table sections of the range
|
||||
table = getAncestorElement(cell, "table");
|
||||
if (!lastTable || table != lastTable) {
|
||||
// There is a section of the range prior to the current table, or lying between tables.
|
||||
// Merge in its rect
|
||||
subRange = this.cloneRange();
|
||||
if (lastTable) {
|
||||
subRange.setStartAfter(lastTable);
|
||||
}
|
||||
subRange.setEndBefore(table);
|
||||
rects.push(WrappedRange.rangeToTextRange(subRange).getBoundingClientRect());
|
||||
}
|
||||
|
||||
if (this.containsNode(cell)) {
|
||||
rects.push(cell.getBoundingClientRect());
|
||||
} else {
|
||||
tempTextRange = textRange.duplicate();
|
||||
tempTextRange.moveToElementText(cell);
|
||||
if (tempTextRange.compareEndPoints("StartToStart", textRange) == -1) {
|
||||
tempTextRange.setEndPoint("StartToStart", textRange);
|
||||
} else if (tempTextRange.compareEndPoints("EndToEnd", textRange) == 1) {
|
||||
tempTextRange.setEndPoint("EndToEnd", textRange);
|
||||
}
|
||||
rects.push(tempTextRange.getBoundingClientRect());
|
||||
}
|
||||
lastTable = table;
|
||||
}
|
||||
|
||||
// Merge in the rect for any content lying after the final table
|
||||
var endTable = getAncestorElement(this.endContainer, "table");
|
||||
if (!endTable && lastTable) {
|
||||
subRange = this.cloneRange();
|
||||
subRange.setStartAfter(lastTable);
|
||||
rects.push(WrappedRange.rangeToTextRange(subRange).getBoundingClientRect());
|
||||
}
|
||||
rect = mergeRects(rects);
|
||||
} else {
|
||||
rect = textRange.getBoundingClientRect();
|
||||
}
|
||||
|
||||
return adjustClientRect(rect, dom.getDocument(this.startContainer));
|
||||
};
|
||||
} else if (api.features.implementsDomRange) {
|
||||
var createWrappedRange = function(range) {
|
||||
return (range instanceof WrappedRange) ? range : new WrappedRange(range);
|
||||
};
|
||||
|
||||
if (rangeSupportsGetBoundingClientRect) {
|
||||
rangeProto.getBoundingClientRect = function() {
|
||||
var nativeRange = createWrappedRange(this).nativeRange;
|
||||
// Test for WebKit getBoundingClientRect bug (https://bugs.webkit.org/show_bug.cgi?id=65324)
|
||||
var rect = nativeRange.getBoundingClientRect() || nativeRange.getClientRects()[0];
|
||||
return adjustClientRect(rect, dom.getDocument(this.startContainer));
|
||||
};
|
||||
} else if (api.features.implementsDomRange) {
|
||||
var createWrappedRange = function(range) {
|
||||
return (range instanceof WrappedRange) ? range : new WrappedRange(range);
|
||||
};
|
||||
|
||||
if (rangeSupportsGetBoundingClientRect) {
|
||||
rangeProto.getBoundingClientRect = function() {
|
||||
var nativeRange = createWrappedRange(this).nativeRange;
|
||||
// Test for WebKit getBoundingClientRect bug (https://bugs.webkit.org/show_bug.cgi?id=65324)
|
||||
var rect = nativeRange.getBoundingClientRect() || nativeRange.getClientRects()[0];
|
||||
return adjustClientRect(rect, dom.getDocument(this.startContainer));
|
||||
if (rangeSupportsGetClientRects) {
|
||||
var getElementRectsForPosition = function(node, offset) {
|
||||
var children = node.childNodes;
|
||||
//if (offset < children.length)
|
||||
};
|
||||
|
||||
if (rangeSupportsGetClientRects) {
|
||||
createClientBoundaryPosGetter = function(isStart) {
|
||||
return function() {
|
||||
var rect, nativeRange = createWrappedRange(this).nativeRange;
|
||||
createClientBoundaryPosGetter = function(isStart) {
|
||||
return function() {
|
||||
var rect, nativeRange = createWrappedRange(this).nativeRange;
|
||||
var rects = nativeRange.getClientRects();
|
||||
|
||||
if (rects.length == 0 && elementSupportsGetBoundingClientRect) {
|
||||
if (isStart) {
|
||||
rect = nativeRange.getClientRects()[0];
|
||||
|
||||
|
||||
}
|
||||
|
||||
console.log(nativeRange, nativeRange.getClientRects(), nativeRange.getBoundingClientRect());
|
||||
if (this.collapsed
|
||||
&& this.startContainer.nodeType == 1
|
||||
&& this.startOffset < this.startContainer.childNodes.length) {
|
||||
var n = this.startContainer.childNodes[this.startOffset];
|
||||
if (n.getClientRects) {
|
||||
console.log(n, n.getClientRects(), this.startContainer.getClientRects())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (rects.length > 0) {
|
||||
if (isStart) {
|
||||
rect = rects[0];
|
||||
return { x: rect.left, y: rect.top };
|
||||
} else {
|
||||
var rects = nativeRange.getClientRects();
|
||||
rect = rects[rects.length - 1];
|
||||
return { x: rect.right, y: rect.bottom };
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var getElementBoundingClientRect = elementSupportsGetBoundingClientRect ?
|
||||
function(el) {
|
||||
return adjustClientRect(el.getBoundingClientRect(), dom.getDocument(el));
|
||||
} :
|
||||
|
||||
// This implementation is very naive. There are many browser quirks that make it extremely
|
||||
// difficult to get accurate element coordinates in all situations
|
||||
function(el) {
|
||||
var x = 0, y = 0, offsetEl = el, width = el.offsetWidth, height = el.offsetHeight;
|
||||
while (offsetEl) {
|
||||
x += offsetEl.offsetLeft;
|
||||
y += offsetEl.offsetTop;
|
||||
offsetEl = offsetEl.offsetParent;
|
||||
} else {
|
||||
throw module.createError("Cannot get position for range " + this.inspect());
|
||||
}
|
||||
|
||||
return adjustClientRect(new Rect(y, x + width, y + height, x), dom.getDocument(el));
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var getElementBoundingClientRect = elementSupportsGetBoundingClientRect ?
|
||||
function(el) {
|
||||
return adjustClientRect(el.getBoundingClientRect(), dom.getDocument(el));
|
||||
} :
|
||||
|
||||
var getRectFromBoundaries = function(range) {
|
||||
var rect;
|
||||
range.splitBoundaries();
|
||||
var span = document.createElement("span");
|
||||
|
||||
if (range.collapsed) {
|
||||
range.insertNode(span);
|
||||
rect = getElementBoundingClientRect(span);
|
||||
span.parentNode.removeChild(span);
|
||||
} else {
|
||||
// TODO: This isn't right. I'm not sure it can be made right sensibly. Consider what to do.
|
||||
// This doesn't consider all the line boxes it needs to consider.
|
||||
var workingRange = range.cloneRange();
|
||||
|
||||
// Get the start rectangle
|
||||
workingRange.collapse(true);
|
||||
workingRange.insertNode(span);
|
||||
var startRect = getElementBoundingClientRect(span);
|
||||
span.parentNode.removeChild(span);
|
||||
|
||||
// Get the end rectangle
|
||||
workingRange.collapseToPoint(range.endContainer, range.endOffset);
|
||||
workingRange.insertNode(span);
|
||||
var endRect = getElementBoundingClientRect(span);
|
||||
span.parentNode.removeChild(span);
|
||||
|
||||
// Merge the start and end rects
|
||||
var rects = [startRect, endRect];
|
||||
|
||||
// Merge in rectangles for all elements in the range
|
||||
var elements = range.getNodes([1], function(el) {
|
||||
return range.containsNode(el);
|
||||
});
|
||||
|
||||
for (var i = 0, len = elements.length; i < len; ++i) {
|
||||
rects.push(getElementBoundingClientRect(elements[i]));
|
||||
}
|
||||
rect = mergeRects(rects)
|
||||
// This implementation is very naive. There are many browser quirks that make it extremely
|
||||
// difficult to get accurate element coordinates in all situations
|
||||
function(el) {
|
||||
var x = 0, y = 0, offsetEl = el, width = el.offsetWidth, height = el.offsetHeight;
|
||||
while (offsetEl) {
|
||||
x += offsetEl.offsetLeft;
|
||||
y += offsetEl.offsetTop;
|
||||
offsetEl = offsetEl.offsetParent;
|
||||
}
|
||||
|
||||
// Clean up
|
||||
range.normalizeBoundaries();
|
||||
return rect;
|
||||
return adjustClientRect(new Rect(y, x + width, y + height, x), dom.getDocument(el));
|
||||
};
|
||||
|
||||
rangeProto.getBoundingClientRect = function(range) {
|
||||
return getRectFromBoundaries(createWrappedRange(range));
|
||||
};
|
||||
}
|
||||
var getRectFromBoundaries = function(range) {
|
||||
var rect;
|
||||
range.splitBoundaries();
|
||||
var span = document.createElement("span");
|
||||
|
||||
function createDocumentBoundaryPosGetter(isStart) {
|
||||
return function() {
|
||||
var pos = this["get" + (isStart ? "Start" : "End") + "ClientPos"]();
|
||||
var scrollPos = getScrollPosition( dom.getWindow(this.startContainer) );
|
||||
return { x: pos.x + scrollPos.x, y: pos.y + scrollPos.y };
|
||||
};
|
||||
}
|
||||
if (range.collapsed) {
|
||||
range.insertNode(span);
|
||||
rect = getElementBoundingClientRect(span);
|
||||
span.parentNode.removeChild(span);
|
||||
} else {
|
||||
// TODO: This isn't right. I'm not sure it can be made right sensibly. Consider what to do.
|
||||
// This doesn't consider all the line boxes it needs to consider.
|
||||
var workingRange = range.cloneRange();
|
||||
|
||||
// Get the start rectangle
|
||||
workingRange.collapse(true);
|
||||
workingRange.insertNode(span);
|
||||
var startRect = getElementBoundingClientRect(span);
|
||||
span.parentNode.removeChild(span);
|
||||
|
||||
// Get the end rectangle
|
||||
workingRange.collapseToPoint(range.endContainer, range.endOffset);
|
||||
workingRange.insertNode(span);
|
||||
var endRect = getElementBoundingClientRect(span);
|
||||
span.parentNode.removeChild(span);
|
||||
|
||||
// Merge the start and end rects
|
||||
var rects = [startRect, endRect];
|
||||
|
||||
// Merge in rectangles for all elements in the range
|
||||
var elements = range.getNodes([1], function(el) {
|
||||
return range.containsNode(el);
|
||||
});
|
||||
|
||||
for (var i = 0, len = elements.length; i < len; ++i) {
|
||||
rects.push(getElementBoundingClientRect(elements[i]));
|
||||
}
|
||||
rect = mergeRects(rects)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
range.normalizeBoundaries();
|
||||
return rect;
|
||||
};
|
||||
|
||||
rangeProto.getBoundingClientRect = function(range) {
|
||||
return getRectFromBoundaries(createWrappedRange(range));
|
||||
};
|
||||
}
|
||||
|
||||
util.extend(rangeProto, {
|
||||
getBoundingDocumentRect: function() {
|
||||
function createDocumentBoundaryPosGetter(isStart) {
|
||||
return function() {
|
||||
var pos = this["get" + (isStart ? "Start" : "End") + "ClientPos"]();
|
||||
var scrollPos = getScrollPosition( dom.getWindow(this.startContainer) );
|
||||
return createRelativeRect(this.getBoundingClientRect(), scrollPos.x, scrollPos.y);
|
||||
},
|
||||
return { x: pos.x + scrollPos.x, y: pos.y + scrollPos.y };
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getStartClientPos: createClientBoundaryPosGetter(true),
|
||||
getEndClientPos: createClientBoundaryPosGetter(false),
|
||||
util.extend(rangeProto, {
|
||||
getBoundingDocumentRect: function() {
|
||||
var scrollPos = getScrollPosition( dom.getWindow(this.startContainer) );
|
||||
return createRelativeRect(this.getBoundingClientRect(), scrollPos.x, scrollPos.y);
|
||||
},
|
||||
|
||||
getStartDocumentPos: createDocumentBoundaryPosGetter(true),
|
||||
getEndDocumentPos: createDocumentBoundaryPosGetter(false)
|
||||
});
|
||||
})();
|
||||
getStartClientPos: createClientBoundaryPosGetter(true),
|
||||
getEndClientPos: createClientBoundaryPosGetter(false),
|
||||
|
||||
getStartDocumentPos: createDocumentBoundaryPosGetter(true),
|
||||
getEndDocumentPos: createDocumentBoundaryPosGetter(false)
|
||||
});
|
||||
|
||||
// Add Selection methods
|
||||
(function() {
|
||||
function compareRanges(r1, r2) {
|
||||
return r1.compareBoundaryPoints(r2.START_TO_START, r2);
|
||||
}
|
||||
function compareRanges(r1, r2) {
|
||||
return r1.compareBoundaryPoints(r2.START_TO_START, r2);
|
||||
}
|
||||
|
||||
function createSelectionRectGetter(isDocument) {
|
||||
return function() {
|
||||
var rangeMethodName = "getBounding" + (isDocument ? "Document" : "Client") + "Rect";
|
||||
var rects = [];
|
||||
for (var i = 0, rect = null, rangeRect; i < this.rangeCount; ++i) {
|
||||
rects.push(this.getRangeAt(i)[rangeMethodName]());
|
||||
}
|
||||
return mergeRects(rects);
|
||||
};
|
||||
}
|
||||
function createSelectionRectGetter(isDocument) {
|
||||
return function() {
|
||||
var rangeMethodName = "getBounding" + (isDocument ? "Document" : "Client") + "Rect";
|
||||
var rects = [];
|
||||
for (var i = 0, rect = null, rangeRect; i < this.rangeCount; ++i) {
|
||||
rects.push(this.getRangeAt(i)[rangeMethodName]());
|
||||
}
|
||||
return mergeRects(rects);
|
||||
};
|
||||
}
|
||||
|
||||
function createSelectionBoundaryPosGetter(isStart, isDocument) {
|
||||
return function() {
|
||||
if (this.rangeCount == 0) {
|
||||
return null;
|
||||
}
|
||||
function createSelectionBoundaryPosGetter(isStart, isDocument) {
|
||||
return function() {
|
||||
if (this.rangeCount == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var posType = isDocument ? "Document" : "Client";
|
||||
var posType = isDocument ? "Document" : "Client";
|
||||
|
||||
var ranges = this.getAllRanges();
|
||||
if (ranges.length > 1) {
|
||||
// Order the ranges by position within the DOM
|
||||
ranges.sort(compareRanges);
|
||||
}
|
||||
var ranges = this.getAllRanges();
|
||||
if (ranges.length > 1) {
|
||||
// Order the ranges by position within the DOM
|
||||
ranges.sort(compareRanges);
|
||||
}
|
||||
|
||||
return isStart ?
|
||||
ranges[0]["getStart" + posType + "Pos"]() :
|
||||
ranges[ranges.length - 1]["getEnd" + posType + "Pos"]();
|
||||
};
|
||||
}
|
||||
return isStart ?
|
||||
ranges[0]["getStart" + posType + "Pos"]() :
|
||||
ranges[ranges.length - 1]["getEnd" + posType + "Pos"]();
|
||||
};
|
||||
}
|
||||
|
||||
util.extend(api.selectionPrototype, {
|
||||
getBoundingClientRect: createSelectionRectGetter(false),
|
||||
getBoundingDocumentRect: createSelectionRectGetter(true),
|
||||
util.extend(api.selectionPrototype, {
|
||||
getBoundingClientRect: createSelectionRectGetter(false),
|
||||
getBoundingDocumentRect: createSelectionRectGetter(true),
|
||||
|
||||
getStartClientPos: createSelectionBoundaryPosGetter(true, false),
|
||||
getEndClientPos: createSelectionBoundaryPosGetter(false, false),
|
||||
getStartClientPos: createSelectionBoundaryPosGetter(true, false),
|
||||
getEndClientPos: createSelectionBoundaryPosGetter(false, false),
|
||||
|
||||
getStartDocumentPos: createSelectionBoundaryPosGetter(true, true),
|
||||
getEndDocumentPos: createSelectionBoundaryPosGetter(false, true)
|
||||
});
|
||||
})();
|
||||
getStartDocumentPos: createSelectionBoundaryPosGetter(true, true),
|
||||
getEndDocumentPos: createSelectionBoundaryPosGetter(false, true)
|
||||
});
|
||||
|
||||
api.positionFromPoint = function(x, y, doc) {
|
||||
doc = dom.getContentDocument(doc, module, "positionFromPoint");
|
||||
return createCaretPositionFromPointGetter(doc)(doc, x, y);
|
||||
};
|
||||
|
||||
api.createRangeFromPoints = createRangeFromPoints;
|
||||
api.moveSelectionToPoints = moveSelectionToPoints;
|
||||
});
|
||||
|
|
|
@ -1,195 +0,0 @@
|
|||
/**
|
||||
* @license 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/
|
||||
*
|
||||
* Depends on Rangy core.
|
||||
*
|
||||
* Copyright 2011, Tim Down
|
||||
* Licensed under the MIT license.
|
||||
* Version: 1.2.2
|
||||
* Build date: 13 November 2011
|
||||
*/
|
||||
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;
|
||||
});
|
|
@ -1,300 +0,0 @@
|
|||
/**
|
||||
* @license Serializer module for Rangy.
|
||||
* Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
|
||||
* cookie or local storage and restore it on the user's next visit to the same page.
|
||||
*
|
||||
* Part of Rangy, a cross-browser JavaScript range and selection library
|
||||
* http://code.google.com/p/rangy/
|
||||
*
|
||||
* Depends on Rangy core.
|
||||
*
|
||||
* Copyright 2011, Tim Down
|
||||
* Licensed under the MIT license.
|
||||
* Version: 1.2.2
|
||||
* Build date: 13 November 2011
|
||||
*/
|
||||
rangy.createModule("Serializer", function(api, module) {
|
||||
api.requireModules( ["WrappedSelection", "WrappedRange"] );
|
||||
var UNDEF = "undefined";
|
||||
|
||||
// encodeURIComponent and decodeURIComponent are required for cookie handling
|
||||
if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
|
||||
module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method");
|
||||
}
|
||||
|
||||
// Checksum for checking whether range can be serialized
|
||||
var crc32 = (function() {
|
||||
function utf8encode(str) {
|
||||
var utf8CharCodes = [];
|
||||
|
||||
for (var i = 0, len = str.length, c; i < len; ++i) {
|
||||
c = str.charCodeAt(i);
|
||||
if (c < 128) {
|
||||
utf8CharCodes.push(c);
|
||||
} else if (c < 2048) {
|
||||
utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
|
||||
} else {
|
||||
utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
|
||||
}
|
||||
}
|
||||
return utf8CharCodes;
|
||||
}
|
||||
|
||||
var cachedCrcTable = null;
|
||||
|
||||
function buildCRCTable() {
|
||||
var table = [];
|
||||
for (var i = 0, j, crc; i < 256; ++i) {
|
||||
crc = i;
|
||||
j = 8;
|
||||
while (j--) {
|
||||
if ((crc & 1) == 1) {
|
||||
crc = (crc >>> 1) ^ 0xEDB88320;
|
||||
} else {
|
||||
crc >>>= 1;
|
||||
}
|
||||
}
|
||||
table[i] = crc >>> 0;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
function getCrcTable() {
|
||||
if (!cachedCrcTable) {
|
||||
cachedCrcTable = buildCRCTable();
|
||||
}
|
||||
return cachedCrcTable;
|
||||
}
|
||||
|
||||
return function(str) {
|
||||
var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
|
||||
for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
|
||||
y = (crc ^ utf8CharCodes[i]) & 0xFF;
|
||||
crc = (crc >>> 8) ^ crcTable[y];
|
||||
}
|
||||
return (crc ^ -1) >>> 0;
|
||||
};
|
||||
})();
|
||||
|
||||
var dom = api.dom;
|
||||
|
||||
function escapeTextForHtml(str) {
|
||||
return str.replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function nodeToInfoString(node, infoParts) {
|
||||
infoParts = infoParts || [];
|
||||
var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
|
||||
var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
|
||||
var start = "", end = "";
|
||||
switch (nodeType) {
|
||||
case 3: // Text node
|
||||
start = escapeTextForHtml(node.nodeValue);
|
||||
break;
|
||||
case 8: // Comment
|
||||
start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
|
||||
break;
|
||||
default:
|
||||
start = "<" + nodeInfo + ">";
|
||||
end = "</>";
|
||||
break;
|
||||
}
|
||||
if (start) {
|
||||
infoParts.push(start);
|
||||
}
|
||||
for (var i = 0; i < childCount; ++i) {
|
||||
nodeToInfoString(children[i], infoParts);
|
||||
}
|
||||
if (end) {
|
||||
infoParts.push(end);
|
||||
}
|
||||
return infoParts;
|
||||
}
|
||||
|
||||
// Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
|
||||
// attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
|
||||
// IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
|
||||
// innerHTML whenever the user changes an input within the element.
|
||||
function getElementChecksum(el) {
|
||||
var info = nodeToInfoString(el).join("");
|
||||
return crc32(info).toString(16);
|
||||
}
|
||||
|
||||
function serializePosition(node, offset, rootNode) {
|
||||
var pathBits = [], n = node;
|
||||
rootNode = rootNode || dom.getDocument(node).documentElement;
|
||||
while (n && n != rootNode) {
|
||||
pathBits.push(dom.getNodeIndex(n, true));
|
||||
n = n.parentNode;
|
||||
}
|
||||
return pathBits.join("/") + ":" + offset;
|
||||
}
|
||||
|
||||
function deserializePosition(serialized, rootNode, doc) {
|
||||
if (rootNode) {
|
||||
doc = doc || dom.getDocument(rootNode);
|
||||
} else {
|
||||
doc = doc || document;
|
||||
rootNode = doc.documentElement;
|
||||
}
|
||||
var bits = serialized.split(":");
|
||||
var node = rootNode;
|
||||
var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;
|
||||
|
||||
while (i--) {
|
||||
nodeIndex = parseInt(nodeIndices[i], 10);
|
||||
if (nodeIndex < node.childNodes.length) {
|
||||
node = node.childNodes[parseInt(nodeIndices[i], 10)];
|
||||
} else {
|
||||
throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) +
|
||||
" has no child with index " + nodeIndex + ", " + i);
|
||||
}
|
||||
}
|
||||
|
||||
return new dom.DomPosition(node, parseInt(bits[1], 10));
|
||||
}
|
||||
|
||||
function serializeRange(range, omitChecksum, rootNode) {
|
||||
rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
|
||||
if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) {
|
||||
throw new Error("serializeRange: range is not wholly contained within specified root node");
|
||||
}
|
||||
var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
|
||||
serializePosition(range.endContainer, range.endOffset, rootNode);
|
||||
if (!omitChecksum) {
|
||||
serialized += "{" + getElementChecksum(rootNode) + "}";
|
||||
}
|
||||
return serialized;
|
||||
}
|
||||
|
||||
function deserializeRange(serialized, rootNode, doc) {
|
||||
if (rootNode) {
|
||||
doc = doc || dom.getDocument(rootNode);
|
||||
} else {
|
||||
doc = doc || document;
|
||||
rootNode = doc.documentElement;
|
||||
}
|
||||
var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized);
|
||||
var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);
|
||||
if (checksum && checksum !== getElementChecksum(rootNode)) {
|
||||
throw new Error("deserializeRange: checksums of serialized range root node (" + checksum +
|
||||
") and target root node (" + rootNodeChecksum + ") do not match");
|
||||
}
|
||||
var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
|
||||
var range = api.createRange(doc);
|
||||
range.setStart(start.node, start.offset);
|
||||
range.setEnd(end.node, end.offset);
|
||||
return range;
|
||||
}
|
||||
|
||||
function canDeserializeRange(serialized, rootNode, doc) {
|
||||
if (rootNode) {
|
||||
doc = doc || dom.getDocument(rootNode);
|
||||
} else {
|
||||
doc = doc || document;
|
||||
rootNode = doc.documentElement;
|
||||
}
|
||||
var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized);
|
||||
var checksum = result[3];
|
||||
return !checksum || checksum === getElementChecksum(rootNode);
|
||||
}
|
||||
|
||||
function serializeSelection(selection, omitChecksum, rootNode) {
|
||||
selection = selection || api.getSelection();
|
||||
var ranges = selection.getAllRanges(), serializedRanges = [];
|
||||
for (var i = 0, len = ranges.length; i < len; ++i) {
|
||||
serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
|
||||
}
|
||||
return serializedRanges.join("|");
|
||||
}
|
||||
|
||||
function deserializeSelection(serialized, rootNode, win) {
|
||||
if (rootNode) {
|
||||
win = win || dom.getWindow(rootNode);
|
||||
} else {
|
||||
win = win || window;
|
||||
rootNode = win.document.documentElement;
|
||||
}
|
||||
var serializedRanges = serialized.split("|");
|
||||
var sel = api.getSelection(win);
|
||||
var ranges = [];
|
||||
|
||||
for (var i = 0, len = serializedRanges.length; i < len; ++i) {
|
||||
ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
|
||||
}
|
||||
sel.setRanges(ranges);
|
||||
|
||||
return sel;
|
||||
}
|
||||
|
||||
function canDeserializeSelection(serialized, rootNode, win) {
|
||||
var doc;
|
||||
if (rootNode) {
|
||||
doc = win ? win.document : dom.getDocument(rootNode);
|
||||
} else {
|
||||
win = win || window;
|
||||
rootNode = win.document.documentElement;
|
||||
}
|
||||
var serializedRanges = serialized.split("|");
|
||||
|
||||
for (var i = 0, len = serializedRanges.length; i < len; ++i) {
|
||||
if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
var cookieName = "rangySerializedSelection";
|
||||
|
||||
function getSerializedSelectionFromCookie(cookie) {
|
||||
var parts = cookie.split(/[;,]/);
|
||||
for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
|
||||
nameVal = parts[i].split("=");
|
||||
if (nameVal[0].replace(/^\s+/, "") == cookieName) {
|
||||
val = nameVal[1];
|
||||
if (val) {
|
||||
return decodeURIComponent(val.replace(/\s+$/, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function restoreSelectionFromCookie(win) {
|
||||
win = win || window;
|
||||
var serialized = getSerializedSelectionFromCookie(win.document.cookie);
|
||||
if (serialized) {
|
||||
deserializeSelection(serialized, win.doc)
|
||||
}
|
||||
}
|
||||
|
||||
function saveSelectionCookie(win, props) {
|
||||
win = win || window;
|
||||
props = (typeof props == "object") ? props : {};
|
||||
var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
|
||||
var path = props.path ? ";path=" + props.path : "";
|
||||
var domain = props.domain ? ";domain=" + props.domain : "";
|
||||
var secure = props.secure ? ";secure" : "";
|
||||
var serialized = serializeSelection(api.getSelection(win));
|
||||
win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
|
||||
}
|
||||
|
||||
api.serializePosition = serializePosition;
|
||||
api.deserializePosition = deserializePosition;
|
||||
|
||||
api.serializeRange = serializeRange;
|
||||
api.deserializeRange = deserializeRange;
|
||||
api.canDeserializeRange = canDeserializeRange;
|
||||
|
||||
api.serializeSelection = serializeSelection;
|
||||
api.deserializeSelection = deserializeSelection;
|
||||
api.canDeserializeSelection = canDeserializeSelection;
|
||||
|
||||
api.restoreSelectionFromCookie = restoreSelectionFromCookie;
|
||||
api.saveSelectionCookie = saveSelectionCookie;
|
||||
|
||||
api.getElementChecksum = getElementChecksum;
|
||||
});
|
Loading…
Reference in a new issue