Merge "Updated rangy from 1.2.2 to 1.3alpha.772"

This commit is contained in:
jenkins-bot 2013-04-18 22:47:03 +00:00 committed by Gerrit Code Review
commit 5e70e26e77
7 changed files with 2584 additions and 3182 deletions

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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;
});

View file

@ -0,0 +1 @@
window.rangy = rangy;

View file

@ -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;
});

View file

@ -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;
});

View file

@ -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, "&lt;").replace(/>/g, "&gt;");
}
function nodeToInfoString(node, infoParts) {
infoParts = infoParts || [];
var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
var start = "", end = "";
switch (nodeType) {
case 3: // Text node
start = escapeTextForHtml(node.nodeValue);
break;
case 8: // Comment
start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
break;
default:
start = "<" + nodeInfo + ">";
end = "</>";
break;
}
if (start) {
infoParts.push(start);
}
for (var i = 0; i < childCount; ++i) {
nodeToInfoString(children[i], infoParts);
}
if (end) {
infoParts.push(end);
}
return infoParts;
}
// Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
// attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
// IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
// innerHTML whenever the user changes an input within the element.
function getElementChecksum(el) {
var info = nodeToInfoString(el).join("");
return crc32(info).toString(16);
}
function serializePosition(node, offset, rootNode) {
var pathBits = [], n = node;
rootNode = rootNode || dom.getDocument(node).documentElement;
while (n && n != rootNode) {
pathBits.push(dom.getNodeIndex(n, true));
n = n.parentNode;
}
return pathBits.join("/") + ":" + offset;
}
function deserializePosition(serialized, rootNode, doc) {
if (rootNode) {
doc = doc || dom.getDocument(rootNode);
} else {
doc = doc || document;
rootNode = doc.documentElement;
}
var bits = serialized.split(":");
var node = rootNode;
var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;
while (i--) {
nodeIndex = parseInt(nodeIndices[i], 10);
if (nodeIndex < node.childNodes.length) {
node = node.childNodes[parseInt(nodeIndices[i], 10)];
} else {
throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) +
" has no child with index " + nodeIndex + ", " + i);
}
}
return new dom.DomPosition(node, parseInt(bits[1], 10));
}
function serializeRange(range, omitChecksum, rootNode) {
rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) {
throw new Error("serializeRange: range is not wholly contained within specified root node");
}
var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
serializePosition(range.endContainer, range.endOffset, rootNode);
if (!omitChecksum) {
serialized += "{" + getElementChecksum(rootNode) + "}";
}
return serialized;
}
function deserializeRange(serialized, rootNode, doc) {
if (rootNode) {
doc = doc || dom.getDocument(rootNode);
} else {
doc = doc || document;
rootNode = doc.documentElement;
}
var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized);
var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);
if (checksum && checksum !== getElementChecksum(rootNode)) {
throw new Error("deserializeRange: checksums of serialized range root node (" + checksum +
") and target root node (" + rootNodeChecksum + ") do not match");
}
var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
var range = api.createRange(doc);
range.setStart(start.node, start.offset);
range.setEnd(end.node, end.offset);
return range;
}
function canDeserializeRange(serialized, rootNode, doc) {
if (rootNode) {
doc = doc || dom.getDocument(rootNode);
} else {
doc = doc || document;
rootNode = doc.documentElement;
}
var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized);
var checksum = result[3];
return !checksum || checksum === getElementChecksum(rootNode);
}
function serializeSelection(selection, omitChecksum, rootNode) {
selection = selection || api.getSelection();
var ranges = selection.getAllRanges(), serializedRanges = [];
for (var i = 0, len = ranges.length; i < len; ++i) {
serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
}
return serializedRanges.join("|");
}
function deserializeSelection(serialized, rootNode, win) {
if (rootNode) {
win = win || dom.getWindow(rootNode);
} else {
win = win || window;
rootNode = win.document.documentElement;
}
var serializedRanges = serialized.split("|");
var sel = api.getSelection(win);
var ranges = [];
for (var i = 0, len = serializedRanges.length; i < len; ++i) {
ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
}
sel.setRanges(ranges);
return sel;
}
function canDeserializeSelection(serialized, rootNode, win) {
var doc;
if (rootNode) {
doc = win ? win.document : dom.getDocument(rootNode);
} else {
win = win || window;
rootNode = win.document.documentElement;
}
var serializedRanges = serialized.split("|");
for (var i = 0, len = serializedRanges.length; i < len; ++i) {
if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
return false;
}
}
return true;
}
var cookieName = "rangySerializedSelection";
function getSerializedSelectionFromCookie(cookie) {
var parts = cookie.split(/[;,]/);
for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
nameVal = parts[i].split("=");
if (nameVal[0].replace(/^\s+/, "") == cookieName) {
val = nameVal[1];
if (val) {
return decodeURIComponent(val.replace(/\s+$/, ""));
}
}
}
return null;
}
function restoreSelectionFromCookie(win) {
win = win || window;
var serialized = getSerializedSelectionFromCookie(win.document.cookie);
if (serialized) {
deserializeSelection(serialized, win.doc)
}
}
function saveSelectionCookie(win, props) {
win = win || window;
props = (typeof props == "object") ? props : {};
var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
var path = props.path ? ";path=" + props.path : "";
var domain = props.domain ? ";domain=" + props.domain : "";
var secure = props.secure ? ";secure" : "";
var serialized = serializeSelection(api.getSelection(win));
win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
}
api.serializePosition = serializePosition;
api.deserializePosition = deserializePosition;
api.serializeRange = serializeRange;
api.deserializeRange = deserializeRange;
api.canDeserializeRange = canDeserializeRange;
api.serializeSelection = serializeSelection;
api.deserializeSelection = deserializeSelection;
api.canDeserializeSelection = canDeserializeSelection;
api.restoreSelectionFromCookie = restoreSelectionFromCookie;
api.saveSelectionCookie = saveSelectionCookie;
api.getElementChecksum = getElementChecksum;
});