4247 lines
172 KiB
JavaScript
4247 lines
172 KiB
JavaScript
/**
|
||
* Rangy, a cross-browser JavaScript range and selection library
|
||
* https://github.com/timdown/rangy
|
||
*
|
||
* Copyright 2015, Tim Down
|
||
* Licensed under the MIT license.
|
||
* Version: 1.3.0
|
||
* Build date: 10 May 2015
|
||
*/
|
||
|
||
(function (factory, root) {
|
||
if (typeof define == "function" && define.amd) {
|
||
// AMD. Register as an anonymous module.
|
||
define(factory);
|
||
} else if (typeof module != "undefined" && typeof exports == "object") {
|
||
// Node/CommonJS style
|
||
module.exports = factory();
|
||
} else {
|
||
// No AMD or CommonJS support so we place Rangy in (probably) the global variable
|
||
root.rangy = factory();
|
||
}
|
||
})(function () {
|
||
|
||
var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
|
||
|
||
// Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START
|
||
// are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.
|
||
var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
|
||
"commonAncestorContainer"];
|
||
|
||
// Minimal set of methods required for DOM Level 2 Range compliance
|
||
var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
|
||
"setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
|
||
"extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
|
||
|
||
var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
|
||
|
||
// Subset of TextRange's full set of methods that we're interested in
|
||
var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
|
||
"setEndPoint", "getBoundingClientRect"];
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Trio of functions taken from Peter Michaux's article:
|
||
// http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
|
||
function isHostMethod(o, p) {
|
||
var t = typeof o[p];
|
||
return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
|
||
}
|
||
|
||
function isHostObject(o, p) {
|
||
return !!(typeof o[p] == OBJECT && o[p]);
|
||
}
|
||
|
||
function isHostProperty(o, p) {
|
||
return typeof o[p] != UNDEFINED;
|
||
}
|
||
|
||
// Creates a convenience function to save verbose repeated calls to tests functions
|
||
function createMultiplePropertyTest(testFunc) {
|
||
return function (o, props) {
|
||
var i = props.length;
|
||
while (i--) {
|
||
if (!testFunc(o, props[i])) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
};
|
||
}
|
||
|
||
// Next trio of functions are a convenience to save verbose repeated calls to previous two functions
|
||
var areHostMethods = createMultiplePropertyTest(isHostMethod);
|
||
var areHostObjects = createMultiplePropertyTest(isHostObject);
|
||
var areHostProperties = createMultiplePropertyTest(isHostProperty);
|
||
|
||
function isTextRange(range) {
|
||
return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
|
||
}
|
||
|
||
function getBody(doc) {
|
||
return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
|
||
}
|
||
|
||
var forEach = [].forEach ?
|
||
function (arr, func) {
|
||
arr.forEach(func);
|
||
} :
|
||
function (arr, func) {
|
||
for (var i = 0, len = arr.length; i < len; ++i) {
|
||
func(arr[i], i);
|
||
}
|
||
};
|
||
|
||
var modules = {};
|
||
|
||
var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);
|
||
|
||
var util = {
|
||
isHostMethod: isHostMethod,
|
||
isHostObject: isHostObject,
|
||
isHostProperty: isHostProperty,
|
||
areHostMethods: areHostMethods,
|
||
areHostObjects: areHostObjects,
|
||
areHostProperties: areHostProperties,
|
||
isTextRange: isTextRange,
|
||
getBody: getBody,
|
||
forEach: forEach
|
||
};
|
||
|
||
var api = {
|
||
version: "1.3.0",
|
||
initialized: false,
|
||
isBrowser: isBrowser,
|
||
supported: true,
|
||
util: util,
|
||
features: {},
|
||
modules: modules,
|
||
config: {
|
||
alertOnFail: false,
|
||
alertOnWarn: false,
|
||
preferTextRange: false,
|
||
autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
|
||
}
|
||
};
|
||
|
||
function consoleLog(msg) {
|
||
if (typeof console != UNDEFINED && isHostMethod(console, "log")) {
|
||
console.log(msg);
|
||
}
|
||
}
|
||
|
||
function alertOrLog(msg, shouldAlert) {
|
||
if (isBrowser && shouldAlert) {
|
||
alert(msg);
|
||
} else {
|
||
consoleLog(msg);
|
||
}
|
||
}
|
||
|
||
function fail(reason) {
|
||
api.initialized = true;
|
||
api.supported = false;
|
||
alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);
|
||
}
|
||
|
||
api.fail = fail;
|
||
|
||
function warn(msg) {
|
||
alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
|
||
}
|
||
|
||
api.warn = warn;
|
||
|
||
// Add utility extend() method
|
||
var extend;
|
||
if ({}.hasOwnProperty) {
|
||
util.extend = extend = function (obj, props, deep) {
|
||
var o, p;
|
||
for (var i in props) {
|
||
if (props.hasOwnProperty(i)) {
|
||
o = obj[i];
|
||
p = props[i];
|
||
if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
|
||
extend(o, p, true);
|
||
}
|
||
obj[i] = p;
|
||
}
|
||
}
|
||
// Special case for toString, which does not show up in for...in loops in IE <= 8
|
||
if (props.hasOwnProperty("toString")) {
|
||
obj.toString = props.toString;
|
||
}
|
||
return obj;
|
||
};
|
||
|
||
util.createOptions = function (optionsParam, defaults) {
|
||
var options = {};
|
||
extend(options, defaults);
|
||
if (optionsParam) {
|
||
extend(options, optionsParam);
|
||
}
|
||
return options;
|
||
};
|
||
} else {
|
||
fail("hasOwnProperty not supported");
|
||
}
|
||
|
||
// Test whether we're in a browser and bail out if not
|
||
if (!isBrowser) {
|
||
fail("Rangy can only run in a browser");
|
||
}
|
||
|
||
// Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not
|
||
(function () {
|
||
var toArray;
|
||
|
||
if (isBrowser) {
|
||
var el = document.createElement("div");
|
||
el.appendChild(document.createElement("span"));
|
||
var slice = [].slice;
|
||
try {
|
||
if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
|
||
toArray = function (arrayLike) {
|
||
return slice.call(arrayLike, 0);
|
||
};
|
||
}
|
||
} catch (e) {
|
||
}
|
||
}
|
||
|
||
if (!toArray) {
|
||
toArray = function (arrayLike) {
|
||
var arr = [];
|
||
for (var i = 0, len = arrayLike.length; i < len; ++i) {
|
||
arr[i] = arrayLike[i];
|
||
}
|
||
return arr;
|
||
};
|
||
}
|
||
|
||
util.toArray = toArray;
|
||
})();
|
||
|
||
// Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or
|
||
// normalization of event properties
|
||
var addListener;
|
||
if (isBrowser) {
|
||
if (isHostMethod(document, "addEventListener")) {
|
||
addListener = function (obj, eventType, listener) {
|
||
obj.addEventListener(eventType, listener, false);
|
||
};
|
||
} else if (isHostMethod(document, "attachEvent")) {
|
||
addListener = function (obj, eventType, listener) {
|
||
obj.attachEvent("on" + eventType, listener);
|
||
};
|
||
} else {
|
||
fail("Document does not have required addEventListener or attachEvent method");
|
||
}
|
||
|
||
util.addListener = addListener;
|
||
}
|
||
|
||
var initListeners = [];
|
||
|
||
function getErrorDesc(ex) {
|
||
return ex.message || ex.description || String(ex);
|
||
}
|
||
|
||
// Initialization
|
||
function init() {
|
||
if (!isBrowser || api.initialized) {
|
||
return;
|
||
}
|
||
var testRange;
|
||
var implementsDomRange = false, implementsTextRange = false;
|
||
|
||
// First, perform basic feature tests
|
||
|
||
if (isHostMethod(document, "createRange")) {
|
||
testRange = document.createRange();
|
||
if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
|
||
implementsDomRange = true;
|
||
}
|
||
}
|
||
|
||
var body = getBody(document);
|
||
if (!body || body.nodeName.toLowerCase() != "body") {
|
||
fail("No body element found");
|
||
return;
|
||
}
|
||
|
||
if (body && isHostMethod(body, "createTextRange")) {
|
||
testRange = body.createTextRange();
|
||
if (isTextRange(testRange)) {
|
||
implementsTextRange = true;
|
||
}
|
||
}
|
||
|
||
if (!implementsDomRange && !implementsTextRange) {
|
||
fail("Neither Range nor TextRange are available");
|
||
return;
|
||
}
|
||
|
||
api.initialized = true;
|
||
api.features = {
|
||
implementsDomRange: implementsDomRange,
|
||
implementsTextRange: implementsTextRange
|
||
};
|
||
|
||
// Initialize modules
|
||
var module, errorMessage;
|
||
for (var moduleName in modules) {
|
||
if ((module = modules[moduleName]) instanceof Module) {
|
||
module.init(module, api);
|
||
}
|
||
}
|
||
|
||
// Call init listeners
|
||
for (var i = 0, len = initListeners.length; i < len; ++i) {
|
||
try {
|
||
initListeners[i](api);
|
||
} catch (ex) {
|
||
errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
|
||
consoleLog(errorMessage);
|
||
}
|
||
}
|
||
}
|
||
|
||
function deprecationNotice(deprecated, replacement, module) {
|
||
if (module) {
|
||
deprecated += " in module " + module.name;
|
||
}
|
||
api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +
|
||
replacement + " instead.");
|
||
}
|
||
|
||
function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {
|
||
owner[deprecated] = function () {
|
||
deprecationNotice(deprecated, replacement, module);
|
||
return owner[replacement].apply(owner, util.toArray(arguments));
|
||
};
|
||
}
|
||
|
||
util.deprecationNotice = deprecationNotice;
|
||
util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;
|
||
|
||
// Allow external scripts to initialize this library in case it's loaded after the document has loaded
|
||
api.init = init;
|
||
|
||
// Execute listener immediately if already initialized
|
||
api.addInitListener = function (listener) {
|
||
if (api.initialized) {
|
||
listener(api);
|
||
} else {
|
||
initListeners.push(listener);
|
||
}
|
||
};
|
||
|
||
var shimListeners = [];
|
||
|
||
api.addShimListener = function (listener) {
|
||
shimListeners.push(listener);
|
||
};
|
||
|
||
function shim(win) {
|
||
win = win || window;
|
||
init();
|
||
|
||
// Notify listeners
|
||
for (var i = 0, len = shimListeners.length; i < len; ++i) {
|
||
shimListeners[i](win);
|
||
}
|
||
}
|
||
|
||
if (isBrowser) {
|
||
api.shim = api.createMissingNativeApi = shim;
|
||
createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");
|
||
}
|
||
|
||
function Module(name, dependencies, initializer) {
|
||
this.name = name;
|
||
this.dependencies = dependencies;
|
||
this.initialized = false;
|
||
this.supported = false;
|
||
this.initializer = initializer;
|
||
}
|
||
|
||
Module.prototype = {
|
||
init: function () {
|
||
var requiredModuleNames = this.dependencies || [];
|
||
for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
|
||
moduleName = requiredModuleNames[i];
|
||
|
||
requiredModule = modules[moduleName];
|
||
if (!requiredModule || !(requiredModule instanceof Module)) {
|
||
throw new Error("required module '" + moduleName + "' not found");
|
||
}
|
||
|
||
requiredModule.init();
|
||
|
||
if (!requiredModule.supported) {
|
||
throw new Error("required module '" + moduleName + "' not supported");
|
||
}
|
||
}
|
||
|
||
// Now run initializer
|
||
this.initializer(this);
|
||
},
|
||
|
||
fail: function (reason) {
|
||
this.initialized = true;
|
||
this.supported = false;
|
||
throw new Error(reason);
|
||
},
|
||
|
||
warn: function (msg) {
|
||
api.warn("Module " + this.name + ": " + msg);
|
||
},
|
||
|
||
deprecationNotice: function (deprecated, replacement) {
|
||
api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +
|
||
replacement + " instead");
|
||
},
|
||
|
||
createError: function (msg) {
|
||
return new Error("Error in Rangy " + this.name + " module: " + msg);
|
||
}
|
||
};
|
||
|
||
function createModule(name, dependencies, initFunc) {
|
||
var newModule = new Module(name, dependencies, function (module) {
|
||
if (!module.initialized) {
|
||
module.initialized = true;
|
||
try {
|
||
initFunc(api, module);
|
||
module.supported = true;
|
||
} catch (ex) {
|
||
var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
|
||
consoleLog(errorMessage);
|
||
if (ex.stack) {
|
||
consoleLog(ex.stack);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
modules[name] = newModule;
|
||
return newModule;
|
||
}
|
||
|
||
api.createModule = function (name) {
|
||
// Allow 2 or 3 arguments (second argument is an optional array of dependencies)
|
||
var initFunc, dependencies;
|
||
if (arguments.length == 2) {
|
||
initFunc = arguments[1];
|
||
dependencies = [];
|
||
} else {
|
||
initFunc = arguments[2];
|
||
dependencies = arguments[1];
|
||
}
|
||
|
||
var module = createModule(name, dependencies, initFunc);
|
||
|
||
// Initialize the module immediately if the core is already initialized
|
||
if (api.initialized && api.supported) {
|
||
module.init();
|
||
}
|
||
};
|
||
|
||
api.createCoreModule = function (name, dependencies, initFunc) {
|
||
createModule(name, dependencies, initFunc);
|
||
};
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately
|
||
|
||
function RangePrototype() {
|
||
}
|
||
|
||
api.RangePrototype = RangePrototype;
|
||
api.rangePrototype = new RangePrototype();
|
||
|
||
function SelectionPrototype() {
|
||
}
|
||
|
||
api.selectionPrototype = new SelectionPrototype();
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// DOM utility methods used by Rangy
|
||
api.createCoreModule("DomUtil", [], function (api, module) {
|
||
var UNDEF = "undefined";
|
||
var util = api.util;
|
||
var getBody = util.getBody;
|
||
|
||
// Perform feature tests
|
||
if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
|
||
module.fail("document missing a Node creation method");
|
||
}
|
||
|
||
if (!util.isHostMethod(document, "getElementsByTagName")) {
|
||
module.fail("document missing getElementsByTagName method");
|
||
}
|
||
|
||
var el = document.createElement("div");
|
||
if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
|
||
!util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
|
||
module.fail("Incomplete Element implementation");
|
||
}
|
||
|
||
// innerHTML is required for Range's createContextualFragment method
|
||
if (!util.isHostProperty(el, "innerHTML")) {
|
||
module.fail("Element is missing innerHTML property");
|
||
}
|
||
|
||
var textNode = document.createTextNode("test");
|
||
if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
|
||
!util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
|
||
!util.areHostProperties(textNode, ["data"]))) {
|
||
module.fail("Incomplete Text Node implementation");
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
|
||
// able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
|
||
// contains just the document as a single element and the value searched for is the document.
|
||
var arrayContains = /*Array.prototype.indexOf ?
|
||
function(arr, val) {
|
||
return arr.indexOf(val) > -1;
|
||
}:*/
|
||
|
||
function (arr, val) {
|
||
var i = arr.length;
|
||
while (i--) {
|
||
if (arr[i] === val) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
|
||
// Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
|
||
function isHtmlNamespace(node) {
|
||
var ns;
|
||
return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
|
||
}
|
||
|
||
function parentElement(node) {
|
||
var parent = node.parentNode;
|
||
return (parent.nodeType == 1) ? parent : null;
|
||
}
|
||
|
||
function getNodeIndex(node) {
|
||
var i = 0;
|
||
while ((node = node.previousSibling)) {
|
||
++i;
|
||
}
|
||
return i;
|
||
}
|
||
|
||
function getNodeLength(node) {
|
||
switch (node.nodeType) {
|
||
case 7:
|
||
case 10:
|
||
return 0;
|
||
case 3:
|
||
case 8:
|
||
return node.length;
|
||
default:
|
||
return node.childNodes.length;
|
||
}
|
||
}
|
||
|
||
function getCommonAncestor(node1, node2) {
|
||
var ancestors = [], n;
|
||
for (n = node1; n; n = n.parentNode) {
|
||
ancestors.push(n);
|
||
}
|
||
|
||
for (n = node2; n; n = n.parentNode) {
|
||
if (arrayContains(ancestors, n)) {
|
||
return n;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function isAncestorOf(ancestor, descendant, selfIsAncestor) {
|
||
var n = selfIsAncestor ? descendant : descendant.parentNode;
|
||
while (n) {
|
||
if (n === ancestor) {
|
||
return true;
|
||
} else {
|
||
n = n.parentNode;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function isOrIsAncestorOf(ancestor, descendant) {
|
||
return isAncestorOf(ancestor, descendant, true);
|
||
}
|
||
|
||
function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
|
||
var p, n = selfIsAncestor ? node : node.parentNode;
|
||
while (n) {
|
||
p = n.parentNode;
|
||
if (p === ancestor) {
|
||
return n;
|
||
}
|
||
n = p;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function isCharacterDataNode(node) {
|
||
var t = node.nodeType;
|
||
return t == 3 || t == 4 || t == 8; // Text, CDataSection or Comment
|
||
}
|
||
|
||
function isTextOrCommentNode(node) {
|
||
if (!node) {
|
||
return false;
|
||
}
|
||
var t = node.nodeType;
|
||
return t == 3 || t == 8; // Text or Comment
|
||
}
|
||
|
||
function insertAfter(node, precedingNode) {
|
||
var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
|
||
if (nextNode) {
|
||
parent.insertBefore(node, nextNode);
|
||
} else {
|
||
parent.appendChild(node);
|
||
}
|
||
return node;
|
||
}
|
||
|
||
// Note that we cannot use splitText() because it is bugridden in IE 9.
|
||
function splitDataNode(node, index, positionsToPreserve) {
|
||
var newNode = node.cloneNode(false);
|
||
newNode.deleteData(0, index);
|
||
node.deleteData(index, node.length - index);
|
||
insertAfter(newNode, node);
|
||
|
||
// Preserve positions
|
||
if (positionsToPreserve) {
|
||
for (var i = 0, position; position = positionsToPreserve[i++];) {
|
||
// Handle case where position was inside the portion of node after the split point
|
||
if (position.node == node && position.offset > index) {
|
||
position.node = newNode;
|
||
position.offset -= index;
|
||
}
|
||
// Handle the case where the position is a node offset within node's parent
|
||
else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
|
||
++position.offset;
|
||
}
|
||
}
|
||
}
|
||
return newNode;
|
||
}
|
||
|
||
function getDocument(node) {
|
||
if (node.nodeType == 9) {
|
||
return node;
|
||
} else if (typeof node.ownerDocument != UNDEF) {
|
||
return node.ownerDocument;
|
||
} else if (typeof node.document != UNDEF) {
|
||
return node.document;
|
||
} else if (node.parentNode) {
|
||
return getDocument(node.parentNode);
|
||
} else {
|
||
throw module.createError("getDocument: no document found for node");
|
||
}
|
||
}
|
||
|
||
function getWindow(node) {
|
||
var doc = getDocument(node);
|
||
if (typeof doc.defaultView != UNDEF) {
|
||
return doc.defaultView;
|
||
} else if (typeof doc.parentWindow != UNDEF) {
|
||
return doc.parentWindow;
|
||
} else {
|
||
throw module.createError("Cannot get a window object for node");
|
||
}
|
||
}
|
||
|
||
function getIframeDocument(iframeEl) {
|
||
if (typeof iframeEl.contentDocument != UNDEF) {
|
||
return iframeEl.contentDocument;
|
||
} else if (typeof iframeEl.contentWindow != UNDEF) {
|
||
return iframeEl.contentWindow.document;
|
||
} else {
|
||
throw module.createError("getIframeDocument: No Document object found for iframe element");
|
||
}
|
||
}
|
||
|
||
function getIframeWindow(iframeEl) {
|
||
if (typeof iframeEl.contentWindow != UNDEF) {
|
||
return iframeEl.contentWindow;
|
||
} else if (typeof iframeEl.contentDocument != UNDEF) {
|
||
return iframeEl.contentDocument.defaultView;
|
||
} else {
|
||
throw module.createError("getIframeWindow: No Window object found for iframe element");
|
||
}
|
||
}
|
||
|
||
// This looks bad. Is it worth it?
|
||
function isWindow(obj) {
|
||
return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
|
||
}
|
||
|
||
function getContentDocument(obj, module, methodName) {
|
||
var doc;
|
||
|
||
if (!obj) {
|
||
doc = document;
|
||
}
|
||
|
||
// Test if a DOM node has been passed and obtain a document object for it if so
|
||
else if (util.isHostProperty(obj, "nodeType")) {
|
||
doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
|
||
getIframeDocument(obj) : getDocument(obj);
|
||
}
|
||
|
||
// Test if the doc parameter appears to be a Window object
|
||
else if (isWindow(obj)) {
|
||
doc = obj.document;
|
||
}
|
||
|
||
if (!doc) {
|
||
throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
|
||
}
|
||
|
||
return doc;
|
||
}
|
||
|
||
function getRootContainer(node) {
|
||
var parent;
|
||
while ((parent = node.parentNode)) {
|
||
node = parent;
|
||
}
|
||
return node;
|
||
}
|
||
|
||
function comparePoints(nodeA, offsetA, nodeB, offsetB) {
|
||
// See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
|
||
var nodeC, root, childA, childB, n;
|
||
if (nodeA == nodeB) {
|
||
// Case 1: nodes are the same
|
||
return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
|
||
} else if ((nodeC = getClosestAncestorIn(nodeB, nodeA, true))) {
|
||
// Case 2: node C (container B or an ancestor) is a child node of A
|
||
return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
|
||
} else if ((nodeC = getClosestAncestorIn(nodeA, nodeB, true))) {
|
||
// Case 3: node C (container A or an ancestor) is a child node of B
|
||
return getNodeIndex(nodeC) < offsetB ? -1 : 1;
|
||
} else {
|
||
root = getCommonAncestor(nodeA, nodeB);
|
||
if (!root) {
|
||
throw new Error("comparePoints error: nodes have no common ancestor");
|
||
}
|
||
|
||
// Case 4: containers are siblings or descendants of siblings
|
||
childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
|
||
childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
|
||
|
||
if (childA === childB) {
|
||
// This shouldn't be possible
|
||
throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
|
||
} else {
|
||
n = root.firstChild;
|
||
while (n) {
|
||
if (n === childA) {
|
||
return -1;
|
||
} else if (n === childB) {
|
||
return 1;
|
||
}
|
||
n = n.nextSibling;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
|
||
var crashyTextNodes = false;
|
||
|
||
function isBrokenNode(node) {
|
||
var n;
|
||
try {
|
||
n = node.parentNode;
|
||
return false;
|
||
} catch (e) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
(function () {
|
||
var el = document.createElement("b");
|
||
el.innerHTML = "1";
|
||
var textNode = el.firstChild;
|
||
el.innerHTML = "<br />";
|
||
crashyTextNodes = isBrokenNode(textNode);
|
||
|
||
api.features.crashyTextNodes = crashyTextNodes;
|
||
})();
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
function inspectNode(node) {
|
||
if (!node) {
|
||
return "[No node]";
|
||
}
|
||
if (crashyTextNodes && isBrokenNode(node)) {
|
||
return "[Broken node]";
|
||
}
|
||
if (isCharacterDataNode(node)) {
|
||
return '"' + node.data + '"';
|
||
}
|
||
if (node.nodeType == 1) {
|
||
var idAttr = node.id ? ' id="' + node.id + '"' : "";
|
||
return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
|
||
}
|
||
return node.nodeName;
|
||
}
|
||
|
||
function fragmentFromNodeChildren(node) {
|
||
var fragment = getDocument(node).createDocumentFragment(), child;
|
||
while ((child = node.firstChild)) {
|
||
fragment.appendChild(child);
|
||
}
|
||
return fragment;
|
||
}
|
||
|
||
var getComputedStyleProperty;
|
||
if (typeof window.getComputedStyle != UNDEF) {
|
||
getComputedStyleProperty = function (el, propName) {
|
||
return getWindow(el).getComputedStyle(el, null)[propName];
|
||
};
|
||
} else if (typeof document.documentElement.currentStyle != UNDEF) {
|
||
getComputedStyleProperty = function (el, propName) {
|
||
return el.currentStyle ? el.currentStyle[propName] : "";
|
||
};
|
||
} else {
|
||
module.fail("No means of obtaining computed style properties found");
|
||
}
|
||
|
||
function createTestElement(doc, html, contentEditable) {
|
||
var body = getBody(doc);
|
||
var el = doc.createElement("div");
|
||
el.contentEditable = "" + !!contentEditable;
|
||
if (html) {
|
||
el.innerHTML = html;
|
||
}
|
||
|
||
// Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
|
||
var bodyFirstChild = body.firstChild;
|
||
if (bodyFirstChild) {
|
||
body.insertBefore(el, bodyFirstChild);
|
||
} else {
|
||
body.appendChild(el);
|
||
}
|
||
|
||
return el;
|
||
}
|
||
|
||
function removeNode(node) {
|
||
return node.parentNode.removeChild(node);
|
||
}
|
||
|
||
function NodeIterator(root) {
|
||
this.root = root;
|
||
this._next = root;
|
||
}
|
||
|
||
NodeIterator.prototype = {
|
||
_current: null,
|
||
|
||
hasNext: function () {
|
||
return !!this._next;
|
||
},
|
||
|
||
next: function () {
|
||
var n = this._current = this._next;
|
||
var child, next;
|
||
if (this._current) {
|
||
child = n.firstChild;
|
||
if (child) {
|
||
this._next = child;
|
||
} else {
|
||
next = null;
|
||
while ((n !== this.root) && !(next = n.nextSibling)) {
|
||
n = n.parentNode;
|
||
}
|
||
this._next = next;
|
||
}
|
||
}
|
||
return this._current;
|
||
},
|
||
|
||
detach: function () {
|
||
this._current = this._next = this.root = null;
|
||
}
|
||
};
|
||
|
||
function createIterator(root) {
|
||
return new NodeIterator(root);
|
||
}
|
||
|
||
function DomPosition(node, offset) {
|
||
this.node = node;
|
||
this.offset = offset;
|
||
}
|
||
|
||
DomPosition.prototype = {
|
||
equals: function (pos) {
|
||
return !!pos && this.node === pos.node && this.offset == pos.offset;
|
||
},
|
||
|
||
inspect: function () {
|
||
return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
|
||
},
|
||
|
||
toString: function () {
|
||
return this.inspect();
|
||
}
|
||
};
|
||
|
||
function DOMException(codeName) {
|
||
this.code = this[codeName];
|
||
this.codeName = codeName;
|
||
this.message = "DOMException: " + this.codeName;
|
||
}
|
||
|
||
DOMException.prototype = {
|
||
INDEX_SIZE_ERR: 1,
|
||
HIERARCHY_REQUEST_ERR: 3,
|
||
WRONG_DOCUMENT_ERR: 4,
|
||
NO_MODIFICATION_ALLOWED_ERR: 7,
|
||
NOT_FOUND_ERR: 8,
|
||
NOT_SUPPORTED_ERR: 9,
|
||
INVALID_STATE_ERR: 11,
|
||
INVALID_NODE_TYPE_ERR: 24
|
||
};
|
||
|
||
DOMException.prototype.toString = function () {
|
||
return this.message;
|
||
};
|
||
|
||
api.dom = {
|
||
arrayContains: arrayContains,
|
||
isHtmlNamespace: isHtmlNamespace,
|
||
parentElement: parentElement,
|
||
getNodeIndex: getNodeIndex,
|
||
getNodeLength: getNodeLength,
|
||
getCommonAncestor: getCommonAncestor,
|
||
isAncestorOf: isAncestorOf,
|
||
isOrIsAncestorOf: isOrIsAncestorOf,
|
||
getClosestAncestorIn: getClosestAncestorIn,
|
||
isCharacterDataNode: isCharacterDataNode,
|
||
isTextOrCommentNode: isTextOrCommentNode,
|
||
insertAfter: insertAfter,
|
||
splitDataNode: splitDataNode,
|
||
getDocument: getDocument,
|
||
getWindow: getWindow,
|
||
getIframeWindow: getIframeWindow,
|
||
getIframeDocument: getIframeDocument,
|
||
getBody: getBody,
|
||
isWindow: isWindow,
|
||
getContentDocument: getContentDocument,
|
||
getRootContainer: getRootContainer,
|
||
comparePoints: comparePoints,
|
||
isBrokenNode: isBrokenNode,
|
||
inspectNode: inspectNode,
|
||
getComputedStyleProperty: getComputedStyleProperty,
|
||
createTestElement: createTestElement,
|
||
removeNode: removeNode,
|
||
fragmentFromNodeChildren: fragmentFromNodeChildren,
|
||
createIterator: createIterator,
|
||
DomPosition: DomPosition
|
||
};
|
||
|
||
api.DOMException = DOMException;
|
||
});
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Pure JavaScript implementation of DOM Range
|
||
api.createCoreModule("DomRange", ["DomUtil"], function (api, module) {
|
||
var dom = api.dom;
|
||
var util = api.util;
|
||
var DomPosition = dom.DomPosition;
|
||
var DOMException = api.DOMException;
|
||
|
||
var isCharacterDataNode = dom.isCharacterDataNode;
|
||
var getNodeIndex = dom.getNodeIndex;
|
||
var isOrIsAncestorOf = dom.isOrIsAncestorOf;
|
||
var getDocument = dom.getDocument;
|
||
var comparePoints = dom.comparePoints;
|
||
var splitDataNode = dom.splitDataNode;
|
||
var getClosestAncestorIn = dom.getClosestAncestorIn;
|
||
var getNodeLength = dom.getNodeLength;
|
||
var arrayContains = dom.arrayContains;
|
||
var getRootContainer = dom.getRootContainer;
|
||
var crashyTextNodes = api.features.crashyTextNodes;
|
||
|
||
var removeNode = dom.removeNode;
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Utility functions
|
||
|
||
function isNonTextPartiallySelected(node, range) {
|
||
return (node.nodeType != 3) &&
|
||
(isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
|
||
}
|
||
|
||
function getRangeDocument(range) {
|
||
return range.document || getDocument(range.startContainer);
|
||
}
|
||
|
||
function getRangeRoot(range) {
|
||
return getRootContainer(range.startContainer);
|
||
}
|
||
|
||
function getBoundaryBeforeNode(node) {
|
||
return new DomPosition(node.parentNode, getNodeIndex(node));
|
||
}
|
||
|
||
function getBoundaryAfterNode(node) {
|
||
return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
|
||
}
|
||
|
||
function insertNodeAtPosition(node, n, o) {
|
||
var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
|
||
if (isCharacterDataNode(n)) {
|
||
if (o == n.length) {
|
||
dom.insertAfter(node, n);
|
||
} else {
|
||
n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
|
||
}
|
||
} else if (o >= n.childNodes.length) {
|
||
n.appendChild(node);
|
||
} else {
|
||
n.insertBefore(node, n.childNodes[o]);
|
||
}
|
||
return firstNodeInserted;
|
||
}
|
||
|
||
function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
|
||
assertRangeValid(rangeA);
|
||
assertRangeValid(rangeB);
|
||
|
||
if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
|
||
throw new DOMException("WRONG_DOCUMENT_ERR");
|
||
}
|
||
|
||
var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
|
||
endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
|
||
|
||
return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
|
||
}
|
||
|
||
function cloneSubtree(iterator) {
|
||
var partiallySelected;
|
||
for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next();) {
|
||
partiallySelected = iterator.isPartiallySelectedSubtree();
|
||
node = node.cloneNode(!partiallySelected);
|
||
if (partiallySelected) {
|
||
subIterator = iterator.getSubtreeIterator();
|
||
node.appendChild(cloneSubtree(subIterator));
|
||
subIterator.detach();
|
||
}
|
||
|
||
if (node.nodeType == 10) { // DocumentType
|
||
throw new DOMException("HIERARCHY_REQUEST_ERR");
|
||
}
|
||
frag.appendChild(node);
|
||
}
|
||
return frag;
|
||
}
|
||
|
||
function iterateSubtree(rangeIterator, func, iteratorState) {
|
||
var it, n;
|
||
iteratorState = iteratorState || {stop: false};
|
||
for (var node, subRangeIterator; node = rangeIterator.next();) {
|
||
if (rangeIterator.isPartiallySelectedSubtree()) {
|
||
if (func(node) === false) {
|
||
iteratorState.stop = true;
|
||
return;
|
||
} else {
|
||
// The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
|
||
// the node selected by the Range.
|
||
subRangeIterator = rangeIterator.getSubtreeIterator();
|
||
iterateSubtree(subRangeIterator, func, iteratorState);
|
||
subRangeIterator.detach();
|
||
if (iteratorState.stop) {
|
||
return;
|
||
}
|
||
}
|
||
} else {
|
||
// The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
|
||
// descendants
|
||
it = dom.createIterator(node);
|
||
while ((n = it.next())) {
|
||
if (func(n) === false) {
|
||
iteratorState.stop = true;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function deleteSubtree(iterator) {
|
||
var subIterator;
|
||
while (iterator.next()) {
|
||
if (iterator.isPartiallySelectedSubtree()) {
|
||
subIterator = iterator.getSubtreeIterator();
|
||
deleteSubtree(subIterator);
|
||
subIterator.detach();
|
||
} else {
|
||
iterator.remove();
|
||
}
|
||
}
|
||
}
|
||
|
||
function extractSubtree(iterator) {
|
||
for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next();) {
|
||
|
||
if (iterator.isPartiallySelectedSubtree()) {
|
||
node = node.cloneNode(false);
|
||
subIterator = iterator.getSubtreeIterator();
|
||
node.appendChild(extractSubtree(subIterator));
|
||
subIterator.detach();
|
||
} else {
|
||
iterator.remove();
|
||
}
|
||
if (node.nodeType == 10) { // DocumentType
|
||
throw new DOMException("HIERARCHY_REQUEST_ERR");
|
||
}
|
||
frag.appendChild(node);
|
||
}
|
||
return frag;
|
||
}
|
||
|
||
function getNodesInRange(range, nodeTypes, filter) {
|
||
var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
|
||
var filterExists = !!filter;
|
||
if (filterNodeTypes) {
|
||
regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
|
||
}
|
||
|
||
var nodes = [];
|
||
iterateSubtree(new RangeIterator(range, false), function (node) {
|
||
if (filterNodeTypes && !regex.test(node.nodeType)) {
|
||
return;
|
||
}
|
||
if (filterExists && !filter(node)) {
|
||
return;
|
||
}
|
||
// Don't include a boundary container if it is a character data node and the range does not contain any
|
||
// of its character data. See issue 190.
|
||
var sc = range.startContainer;
|
||
if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
|
||
return;
|
||
}
|
||
|
||
var ec = range.endContainer;
|
||
if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
|
||
return;
|
||
}
|
||
|
||
nodes.push(node);
|
||
});
|
||
return nodes;
|
||
}
|
||
|
||
function inspect(range) {
|
||
var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
|
||
return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
|
||
dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
|
||
|
||
function RangeIterator(range, clonePartiallySelectedTextNodes) {
|
||
this.range = range;
|
||
this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
|
||
|
||
|
||
if (!range.collapsed) {
|
||
this.sc = range.startContainer;
|
||
this.so = range.startOffset;
|
||
this.ec = range.endContainer;
|
||
this.eo = range.endOffset;
|
||
var root = range.commonAncestorContainer;
|
||
|
||
if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
|
||
this.isSingleCharacterDataNode = true;
|
||
this._first = this._last = this._next = this.sc;
|
||
} else {
|
||
this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
|
||
this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
|
||
this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
|
||
this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
|
||
}
|
||
}
|
||
}
|
||
|
||
RangeIterator.prototype = {
|
||
_current: null,
|
||
_next: null,
|
||
_first: null,
|
||
_last: null,
|
||
isSingleCharacterDataNode: false,
|
||
|
||
reset: function () {
|
||
this._current = null;
|
||
this._next = this._first;
|
||
},
|
||
|
||
hasNext: function () {
|
||
return !!this._next;
|
||
},
|
||
|
||
next: function () {
|
||
// Move to next node
|
||
var current = this._current = this._next;
|
||
if (current) {
|
||
this._next = (current !== this._last) ? current.nextSibling : null;
|
||
|
||
// Check for partially selected text nodes
|
||
if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
|
||
if (current === this.ec) {
|
||
(current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
|
||
}
|
||
if (this._current === this.sc) {
|
||
(current = current.cloneNode(true)).deleteData(0, this.so);
|
||
}
|
||
}
|
||
}
|
||
|
||
return current;
|
||
},
|
||
|
||
remove: function () {
|
||
var current = this._current, start, end;
|
||
|
||
if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
|
||
start = (current === this.sc) ? this.so : 0;
|
||
end = (current === this.ec) ? this.eo : current.length;
|
||
if (start != end) {
|
||
current.deleteData(start, end - start);
|
||
}
|
||
} else {
|
||
if (current.parentNode) {
|
||
removeNode(current);
|
||
} else {
|
||
}
|
||
}
|
||
},
|
||
|
||
// Checks if the current node is partially selected
|
||
isPartiallySelectedSubtree: function () {
|
||
var current = this._current;
|
||
return isNonTextPartiallySelected(current, this.range);
|
||
},
|
||
|
||
getSubtreeIterator: function () {
|
||
var subRange;
|
||
if (this.isSingleCharacterDataNode) {
|
||
subRange = this.range.cloneRange();
|
||
subRange.collapse(false);
|
||
} else {
|
||
subRange = new Range(getRangeDocument(this.range));
|
||
var current = this._current;
|
||
var startContainer = current, startOffset = 0, endContainer = current,
|
||
endOffset = getNodeLength(current);
|
||
|
||
if (isOrIsAncestorOf(current, this.sc)) {
|
||
startContainer = this.sc;
|
||
startOffset = this.so;
|
||
}
|
||
if (isOrIsAncestorOf(current, this.ec)) {
|
||
endContainer = this.ec;
|
||
endOffset = this.eo;
|
||
}
|
||
|
||
updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
|
||
}
|
||
return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
|
||
},
|
||
|
||
detach: function () {
|
||
this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
|
||
}
|
||
};
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
|
||
var rootContainerNodeTypes = [2, 9, 11];
|
||
var readonlyNodeTypes = [5, 6, 10, 12];
|
||
var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
|
||
var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
|
||
|
||
function createAncestorFinder(nodeTypes) {
|
||
return function (node, selfIsAncestor) {
|
||
var t, n = selfIsAncestor ? node : node.parentNode;
|
||
while (n) {
|
||
t = n.nodeType;
|
||
if (arrayContains(nodeTypes, t)) {
|
||
return n;
|
||
}
|
||
n = n.parentNode;
|
||
}
|
||
return null;
|
||
};
|
||
}
|
||
|
||
var getDocumentOrFragmentContainer = createAncestorFinder([9, 11]);
|
||
var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
|
||
var getDocTypeNotationEntityAncestor = createAncestorFinder([6, 10, 12]);
|
||
|
||
function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
|
||
if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
|
||
throw new DOMException("INVALID_NODE_TYPE_ERR");
|
||
}
|
||
}
|
||
|
||
function assertValidNodeType(node, invalidTypes) {
|
||
if (!arrayContains(invalidTypes, node.nodeType)) {
|
||
throw new DOMException("INVALID_NODE_TYPE_ERR");
|
||
}
|
||
}
|
||
|
||
function assertValidOffset(node, offset) {
|
||
if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
|
||
throw new DOMException("INDEX_SIZE_ERR");
|
||
}
|
||
}
|
||
|
||
function assertSameDocumentOrFragment(node1, node2) {
|
||
if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
|
||
throw new DOMException("WRONG_DOCUMENT_ERR");
|
||
}
|
||
}
|
||
|
||
function assertNodeNotReadOnly(node) {
|
||
if (getReadonlyAncestor(node, true)) {
|
||
throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
|
||
}
|
||
}
|
||
|
||
function assertNode(node, codeName) {
|
||
if (!node) {
|
||
throw new DOMException(codeName);
|
||
}
|
||
}
|
||
|
||
function isValidOffset(node, offset) {
|
||
return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
|
||
}
|
||
|
||
function isRangeValid(range) {
|
||
return (!!range.startContainer && !!range.endContainer &&
|
||
!(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
|
||
getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
|
||
isValidOffset(range.startContainer, range.startOffset) &&
|
||
isValidOffset(range.endContainer, range.endOffset));
|
||
}
|
||
|
||
function assertRangeValid(range) {
|
||
if (!isRangeValid(range)) {
|
||
throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
|
||
}
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Test the browser's innerHTML support to decide how to implement createContextualFragment
|
||
var styleEl = document.createElement("style");
|
||
var htmlParsingConforms = false;
|
||
try {
|
||
styleEl.innerHTML = "<b>x</b>";
|
||
htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
|
||
} catch (e) {
|
||
// IE 6 and 7 throw
|
||
}
|
||
|
||
api.features.htmlParsingConforms = htmlParsingConforms;
|
||
|
||
var createContextualFragment = htmlParsingConforms ?
|
||
|
||
// Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
|
||
// discussion and base code for this implementation at issue 67.
|
||
// Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
|
||
// Thanks to Aleks Williams.
|
||
function (fragmentStr) {
|
||
// "Let node the context object's start's node."
|
||
var node = this.startContainer;
|
||
var doc = getDocument(node);
|
||
|
||
// "If the context object's start's node is null, raise an INVALID_STATE_ERR
|
||
// exception and abort these steps."
|
||
if (!node) {
|
||
throw new DOMException("INVALID_STATE_ERR");
|
||
}
|
||
|
||
// "Let element be as follows, depending on node's interface:"
|
||
// Document, Document Fragment: null
|
||
var el = null;
|
||
|
||
// "Element: node"
|
||
if (node.nodeType == 1) {
|
||
el = node;
|
||
|
||
// "Text, Comment: node's parentElement"
|
||
} else if (isCharacterDataNode(node)) {
|
||
el = dom.parentElement(node);
|
||
}
|
||
|
||
// "If either element is null or element's ownerDocument is an HTML document
|
||
// and element's local name is "html" and element's namespace is the HTML
|
||
// namespace"
|
||
if (el === null || (
|
||
el.nodeName == "HTML" &&
|
||
dom.isHtmlNamespace(getDocument(el).documentElement) &&
|
||
dom.isHtmlNamespace(el)
|
||
)) {
|
||
|
||
// "let element be a new Element with "body" as its local name and the HTML
|
||
// namespace as its namespace.""
|
||
el = doc.createElement("body");
|
||
} else {
|
||
el = el.cloneNode(false);
|
||
}
|
||
|
||
// "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
|
||
// "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
|
||
// "In either case, the algorithm must be invoked with fragment as the input
|
||
// and element as the context element."
|
||
el.innerHTML = fragmentStr;
|
||
|
||
// "If this raises an exception, then abort these steps. Otherwise, let new
|
||
// children be the nodes returned."
|
||
|
||
// "Let fragment be a new DocumentFragment."
|
||
// "Append all new children to fragment."
|
||
// "Return fragment."
|
||
return dom.fragmentFromNodeChildren(el);
|
||
} :
|
||
|
||
// In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
|
||
// previous versions of Rangy used (with the exception of using a body element rather than a div)
|
||
function (fragmentStr) {
|
||
var doc = getRangeDocument(this);
|
||
var el = doc.createElement("body");
|
||
el.innerHTML = fragmentStr;
|
||
|
||
return dom.fragmentFromNodeChildren(el);
|
||
};
|
||
|
||
function splitRangeBoundaries(range, positionsToPreserve) {
|
||
assertRangeValid(range);
|
||
|
||
var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
|
||
var startEndSame = (sc === ec);
|
||
|
||
if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
|
||
splitDataNode(ec, eo, positionsToPreserve);
|
||
}
|
||
|
||
if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
|
||
sc = splitDataNode(sc, so, positionsToPreserve);
|
||
if (startEndSame) {
|
||
eo -= so;
|
||
ec = sc;
|
||
} else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
|
||
eo++;
|
||
}
|
||
so = 0;
|
||
}
|
||
range.setStartAndEnd(sc, so, ec, eo);
|
||
}
|
||
|
||
function rangeToHtml(range) {
|
||
assertRangeValid(range);
|
||
var container = range.commonAncestorContainer.parentNode.cloneNode(false);
|
||
container.appendChild(range.cloneContents());
|
||
return container.innerHTML;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
|
||
"commonAncestorContainer"];
|
||
|
||
var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
|
||
var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
|
||
|
||
util.extend(api.rangePrototype, {
|
||
compareBoundaryPoints: function (how, range) {
|
||
assertRangeValid(this);
|
||
assertSameDocumentOrFragment(this.startContainer, range.startContainer);
|
||
|
||
var nodeA, offsetA, nodeB, offsetB;
|
||
var prefixA = (how == e2s || how == s2s) ? "start" : "end";
|
||
var prefixB = (how == s2e || how == s2s) ? "start" : "end";
|
||
nodeA = this[prefixA + "Container"];
|
||
offsetA = this[prefixA + "Offset"];
|
||
nodeB = range[prefixB + "Container"];
|
||
offsetB = range[prefixB + "Offset"];
|
||
return comparePoints(nodeA, offsetA, nodeB, offsetB);
|
||
},
|
||
|
||
insertNode: function (node) {
|
||
assertRangeValid(this);
|
||
assertValidNodeType(node, insertableNodeTypes);
|
||
assertNodeNotReadOnly(this.startContainer);
|
||
|
||
if (isOrIsAncestorOf(node, this.startContainer)) {
|
||
throw new DOMException("HIERARCHY_REQUEST_ERR");
|
||
}
|
||
|
||
// No check for whether the container of the start of the Range is of a type that does not allow
|
||
// children of the type of node: the browser's DOM implementation should do this for us when we attempt
|
||
// to add the node
|
||
|
||
var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
|
||
this.setStartBefore(firstNodeInserted);
|
||
},
|
||
|
||
cloneContents: function () {
|
||
assertRangeValid(this);
|
||
|
||
var clone, frag;
|
||
if (this.collapsed) {
|
||
return getRangeDocument(this).createDocumentFragment();
|
||
} else {
|
||
if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
|
||
clone = this.startContainer.cloneNode(true);
|
||
clone.data = clone.data.slice(this.startOffset, this.endOffset);
|
||
frag = getRangeDocument(this).createDocumentFragment();
|
||
frag.appendChild(clone);
|
||
return frag;
|
||
} else {
|
||
var iterator = new RangeIterator(this, true);
|
||
clone = cloneSubtree(iterator);
|
||
iterator.detach();
|
||
}
|
||
return clone;
|
||
}
|
||
},
|
||
|
||
canSurroundContents: function () {
|
||
assertRangeValid(this);
|
||
assertNodeNotReadOnly(this.startContainer);
|
||
assertNodeNotReadOnly(this.endContainer);
|
||
|
||
// Check if the contents can be surrounded. Specifically, this means whether the range partially selects
|
||
// no non-text nodes.
|
||
var iterator = new RangeIterator(this, true);
|
||
var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
|
||
(iterator._last && isNonTextPartiallySelected(iterator._last, this)));
|
||
iterator.detach();
|
||
return !boundariesInvalid;
|
||
},
|
||
|
||
surroundContents: function (node) {
|
||
assertValidNodeType(node, surroundNodeTypes);
|
||
|
||
if (!this.canSurroundContents()) {
|
||
throw new DOMException("INVALID_STATE_ERR");
|
||
}
|
||
|
||
// Extract the contents
|
||
var content = this.extractContents();
|
||
|
||
// Clear the children of the node
|
||
if (node.hasChildNodes()) {
|
||
while (node.lastChild) {
|
||
node.removeChild(node.lastChild);
|
||
}
|
||
}
|
||
|
||
// Insert the new node and add the extracted contents
|
||
insertNodeAtPosition(node, this.startContainer, this.startOffset);
|
||
node.appendChild(content);
|
||
|
||
this.selectNode(node);
|
||
},
|
||
|
||
cloneRange: function () {
|
||
assertRangeValid(this);
|
||
var range = new Range(getRangeDocument(this));
|
||
var i = rangeProperties.length, prop;
|
||
while (i--) {
|
||
prop = rangeProperties[i];
|
||
range[prop] = this[prop];
|
||
}
|
||
return range;
|
||
},
|
||
|
||
toString: function () {
|
||
assertRangeValid(this);
|
||
var sc = this.startContainer;
|
||
if (sc === this.endContainer && isCharacterDataNode(sc)) {
|
||
return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
|
||
} else {
|
||
var textParts = [], iterator = new RangeIterator(this, true);
|
||
iterateSubtree(iterator, function (node) {
|
||
// Accept only text or CDATA nodes, not comments
|
||
if (node.nodeType == 3 || node.nodeType == 4) {
|
||
textParts.push(node.data);
|
||
}
|
||
});
|
||
iterator.detach();
|
||
return textParts.join("");
|
||
}
|
||
},
|
||
|
||
// The methods below are all non-standard. The following batch were introduced by Mozilla but have since
|
||
// been removed from Mozilla.
|
||
|
||
compareNode: function (node) {
|
||
assertRangeValid(this);
|
||
|
||
var parent = node.parentNode;
|
||
var nodeIndex = getNodeIndex(node);
|
||
|
||
if (!parent) {
|
||
throw new DOMException("NOT_FOUND_ERR");
|
||
}
|
||
|
||
var startComparison = this.comparePoint(parent, nodeIndex),
|
||
endComparison = this.comparePoint(parent, nodeIndex + 1);
|
||
|
||
if (startComparison < 0) { // Node starts before
|
||
return (endComparison > 0) ? n_b_a : n_b;
|
||
} else {
|
||
return (endComparison > 0) ? n_a : n_i;
|
||
}
|
||
},
|
||
|
||
comparePoint: function (node, offset) {
|
||
assertRangeValid(this);
|
||
assertNode(node, "HIERARCHY_REQUEST_ERR");
|
||
assertSameDocumentOrFragment(node, this.startContainer);
|
||
|
||
if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
|
||
return -1;
|
||
} else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
|
||
return 1;
|
||
}
|
||
return 0;
|
||
},
|
||
|
||
createContextualFragment: createContextualFragment,
|
||
|
||
toHtml: function () {
|
||
return rangeToHtml(this);
|
||
},
|
||
|
||
// touchingIsIntersecting determines whether this method considers a node that borders a range intersects
|
||
// with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
|
||
intersectsNode: function (node, touchingIsIntersecting) {
|
||
assertRangeValid(this);
|
||
if (getRootContainer(node) != getRangeRoot(this)) {
|
||
return false;
|
||
}
|
||
|
||
var parent = node.parentNode, offset = getNodeIndex(node);
|
||
if (!parent) {
|
||
return true;
|
||
}
|
||
|
||
var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
|
||
endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
|
||
|
||
return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
|
||
},
|
||
|
||
isPointInRange: function (node, offset) {
|
||
assertRangeValid(this);
|
||
assertNode(node, "HIERARCHY_REQUEST_ERR");
|
||
assertSameDocumentOrFragment(node, this.startContainer);
|
||
|
||
return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
|
||
(comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
|
||
},
|
||
|
||
// The methods below are non-standard and invented by me.
|
||
|
||
// Sharing a boundary start-to-end or end-to-start does not count as intersection.
|
||
intersectsRange: function (range) {
|
||
return rangesIntersect(this, range, false);
|
||
},
|
||
|
||
// Sharing a boundary start-to-end or end-to-start does count as intersection.
|
||
intersectsOrTouchesRange: function (range) {
|
||
return rangesIntersect(this, range, true);
|
||
},
|
||
|
||
intersection: function (range) {
|
||
if (this.intersectsRange(range)) {
|
||
var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
|
||
endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
|
||
|
||
var intersectionRange = this.cloneRange();
|
||
if (startComparison == -1) {
|
||
intersectionRange.setStart(range.startContainer, range.startOffset);
|
||
}
|
||
if (endComparison == 1) {
|
||
intersectionRange.setEnd(range.endContainer, range.endOffset);
|
||
}
|
||
return intersectionRange;
|
||
}
|
||
return null;
|
||
},
|
||
|
||
union: function (range) {
|
||
if (this.intersectsOrTouchesRange(range)) {
|
||
var unionRange = this.cloneRange();
|
||
if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
|
||
unionRange.setStart(range.startContainer, range.startOffset);
|
||
}
|
||
if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
|
||
unionRange.setEnd(range.endContainer, range.endOffset);
|
||
}
|
||
return unionRange;
|
||
} else {
|
||
throw new DOMException("Ranges do not intersect");
|
||
}
|
||
},
|
||
|
||
containsNode: function (node, allowPartial) {
|
||
if (allowPartial) {
|
||
return this.intersectsNode(node, false);
|
||
} else {
|
||
return this.compareNode(node) == n_i;
|
||
}
|
||
},
|
||
|
||
containsNodeContents: function (node) {
|
||
return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
|
||
},
|
||
|
||
containsRange: function (range) {
|
||
var intersection = this.intersection(range);
|
||
return intersection !== null && range.equals(intersection);
|
||
},
|
||
|
||
containsNodeText: function (node) {
|
||
var nodeRange = this.cloneRange();
|
||
nodeRange.selectNode(node);
|
||
var textNodes = nodeRange.getNodes([3]);
|
||
if (textNodes.length > 0) {
|
||
nodeRange.setStart(textNodes[0], 0);
|
||
var lastTextNode = textNodes.pop();
|
||
nodeRange.setEnd(lastTextNode, lastTextNode.length);
|
||
return this.containsRange(nodeRange);
|
||
} else {
|
||
return this.containsNodeContents(node);
|
||
}
|
||
},
|
||
|
||
getNodes: function (nodeTypes, filter) {
|
||
assertRangeValid(this);
|
||
return getNodesInRange(this, nodeTypes, filter);
|
||
},
|
||
|
||
getDocument: function () {
|
||
return getRangeDocument(this);
|
||
},
|
||
|
||
collapseBefore: function (node) {
|
||
this.setEndBefore(node);
|
||
this.collapse(false);
|
||
},
|
||
|
||
collapseAfter: function (node) {
|
||
this.setStartAfter(node);
|
||
this.collapse(true);
|
||
},
|
||
|
||
getBookmark: function (containerNode) {
|
||
var doc = getRangeDocument(this);
|
||
var preSelectionRange = api.createRange(doc);
|
||
containerNode = containerNode || dom.getBody(doc);
|
||
preSelectionRange.selectNodeContents(containerNode);
|
||
var range = this.intersection(preSelectionRange);
|
||
var start = 0, end = 0;
|
||
if (range) {
|
||
preSelectionRange.setEnd(range.startContainer, range.startOffset);
|
||
start = preSelectionRange.toString().length;
|
||
end = start + range.toString().length;
|
||
}
|
||
|
||
return {
|
||
start: start,
|
||
end: end,
|
||
containerNode: containerNode
|
||
};
|
||
},
|
||
|
||
moveToBookmark: function (bookmark) {
|
||
var containerNode = bookmark.containerNode;
|
||
var charIndex = 0;
|
||
this.setStart(containerNode, 0);
|
||
this.collapse(true);
|
||
var nodeStack = [containerNode], node, foundStart = false, stop = false;
|
||
var nextCharIndex, i, childNodes;
|
||
|
||
while (!stop && (node = nodeStack.pop())) {
|
||
if (node.nodeType == 3) {
|
||
nextCharIndex = charIndex + node.length;
|
||
if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
|
||
this.setStart(node, bookmark.start - charIndex);
|
||
foundStart = true;
|
||
}
|
||
if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
|
||
this.setEnd(node, bookmark.end - charIndex);
|
||
stop = true;
|
||
}
|
||
charIndex = nextCharIndex;
|
||
} else {
|
||
childNodes = node.childNodes;
|
||
i = childNodes.length;
|
||
while (i--) {
|
||
nodeStack.push(childNodes[i]);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
getName: function () {
|
||
return "DomRange";
|
||
},
|
||
|
||
equals: function (range) {
|
||
return Range.rangesEqual(this, range);
|
||
},
|
||
|
||
isValid: function () {
|
||
return isRangeValid(this);
|
||
},
|
||
|
||
inspect: function () {
|
||
return inspect(this);
|
||
},
|
||
|
||
detach: function () {
|
||
// In DOM4, detach() is now a no-op.
|
||
}
|
||
});
|
||
|
||
function copyComparisonConstantsToObject(obj) {
|
||
obj.START_TO_START = s2s;
|
||
obj.START_TO_END = s2e;
|
||
obj.END_TO_END = e2e;
|
||
obj.END_TO_START = e2s;
|
||
|
||
obj.NODE_BEFORE = n_b;
|
||
obj.NODE_AFTER = n_a;
|
||
obj.NODE_BEFORE_AND_AFTER = n_b_a;
|
||
obj.NODE_INSIDE = n_i;
|
||
}
|
||
|
||
function copyComparisonConstants(constructor) {
|
||
copyComparisonConstantsToObject(constructor);
|
||
copyComparisonConstantsToObject(constructor.prototype);
|
||
}
|
||
|
||
function createRangeContentRemover(remover, boundaryUpdater) {
|
||
return function () {
|
||
assertRangeValid(this);
|
||
|
||
var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
|
||
|
||
var iterator = new RangeIterator(this, true);
|
||
|
||
// Work out where to position the range after content removal
|
||
var node, boundary;
|
||
if (sc !== root) {
|
||
node = getClosestAncestorIn(sc, root, true);
|
||
boundary = getBoundaryAfterNode(node);
|
||
sc = boundary.node;
|
||
so = boundary.offset;
|
||
}
|
||
|
||
// Check none of the range is read-only
|
||
iterateSubtree(iterator, assertNodeNotReadOnly);
|
||
|
||
iterator.reset();
|
||
|
||
// Remove the content
|
||
var returnValue = remover(iterator);
|
||
iterator.detach();
|
||
|
||
// Move to the new position
|
||
boundaryUpdater(this, sc, so, sc, so);
|
||
|
||
return returnValue;
|
||
};
|
||
}
|
||
|
||
function createPrototypeRange(constructor, boundaryUpdater) {
|
||
function createBeforeAfterNodeSetter(isBefore, isStart) {
|
||
return function (node) {
|
||
assertValidNodeType(node, beforeAfterNodeTypes);
|
||
assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
|
||
|
||
var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
|
||
(isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
|
||
};
|
||
}
|
||
|
||
function setRangeStart(range, node, offset) {
|
||
var ec = range.endContainer, eo = range.endOffset;
|
||
if (node !== range.startContainer || offset !== range.startOffset) {
|
||
// Check the root containers of the range and the new boundary, and also check whether the new boundary
|
||
// is after the current end. In either case, collapse the range to the new position
|
||
if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
|
||
ec = node;
|
||
eo = offset;
|
||
}
|
||
boundaryUpdater(range, node, offset, ec, eo);
|
||
}
|
||
}
|
||
|
||
function setRangeEnd(range, node, offset) {
|
||
var sc = range.startContainer, so = range.startOffset;
|
||
if (node !== range.endContainer || offset !== range.endOffset) {
|
||
// Check the root containers of the range and the new boundary, and also check whether the new boundary
|
||
// is after the current end. In either case, collapse the range to the new position
|
||
if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
|
||
sc = node;
|
||
so = offset;
|
||
}
|
||
boundaryUpdater(range, sc, so, node, offset);
|
||
}
|
||
}
|
||
|
||
// Set up inheritance
|
||
var F = function () {
|
||
};
|
||
F.prototype = api.rangePrototype;
|
||
constructor.prototype = new F();
|
||
|
||
util.extend(constructor.prototype, {
|
||
setStart: function (node, offset) {
|
||
assertNoDocTypeNotationEntityAncestor(node, true);
|
||
assertValidOffset(node, offset);
|
||
|
||
setRangeStart(this, node, offset);
|
||
},
|
||
|
||
setEnd: function (node, offset) {
|
||
assertNoDocTypeNotationEntityAncestor(node, true);
|
||
assertValidOffset(node, offset);
|
||
|
||
setRangeEnd(this, node, offset);
|
||
},
|
||
|
||
/**
|
||
* Convenience method to set a range's start and end boundaries. Overloaded as follows:
|
||
* - Two parameters (node, offset) creates a collapsed range at that position
|
||
* - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
|
||
* startOffset and ending at endOffset
|
||
* - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
|
||
* startNode and ending at endOffset in endNode
|
||
*/
|
||
setStartAndEnd: function () {
|
||
var args = arguments;
|
||
var sc = args[0], so = args[1], ec = sc, eo = so;
|
||
|
||
switch (args.length) {
|
||
case 3:
|
||
eo = args[2];
|
||
break;
|
||
case 4:
|
||
ec = args[2];
|
||
eo = args[3];
|
||
break;
|
||
}
|
||
|
||
boundaryUpdater(this, sc, so, ec, eo);
|
||
},
|
||
|
||
setBoundary: function (node, offset, isStart) {
|
||
this["set" + (isStart ? "Start" : "End")](node, offset);
|
||
},
|
||
|
||
setStartBefore: createBeforeAfterNodeSetter(true, true),
|
||
setStartAfter: createBeforeAfterNodeSetter(false, true),
|
||
setEndBefore: createBeforeAfterNodeSetter(true, false),
|
||
setEndAfter: createBeforeAfterNodeSetter(false, false),
|
||
|
||
collapse: function (isStart) {
|
||
assertRangeValid(this);
|
||
if (isStart) {
|
||
boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
|
||
} else {
|
||
boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
|
||
}
|
||
},
|
||
|
||
selectNodeContents: function (node) {
|
||
assertNoDocTypeNotationEntityAncestor(node, true);
|
||
|
||
boundaryUpdater(this, node, 0, node, getNodeLength(node));
|
||
},
|
||
|
||
selectNode: function (node) {
|
||
assertNoDocTypeNotationEntityAncestor(node, false);
|
||
assertValidNodeType(node, beforeAfterNodeTypes);
|
||
|
||
var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
|
||
boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
|
||
},
|
||
|
||
extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
|
||
|
||
deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
|
||
|
||
canSurroundContents: function () {
|
||
assertRangeValid(this);
|
||
assertNodeNotReadOnly(this.startContainer);
|
||
assertNodeNotReadOnly(this.endContainer);
|
||
|
||
// Check if the contents can be surrounded. Specifically, this means whether the range partially selects
|
||
// no non-text nodes.
|
||
var iterator = new RangeIterator(this, true);
|
||
var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
|
||
(iterator._last && isNonTextPartiallySelected(iterator._last, this)));
|
||
iterator.detach();
|
||
return !boundariesInvalid;
|
||
},
|
||
|
||
splitBoundaries: function () {
|
||
splitRangeBoundaries(this);
|
||
},
|
||
|
||
splitBoundariesPreservingPositions: function (positionsToPreserve) {
|
||
splitRangeBoundaries(this, positionsToPreserve);
|
||
},
|
||
|
||
normalizeBoundaries: function () {
|
||
assertRangeValid(this);
|
||
|
||
var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
|
||
|
||
var mergeForward = function (node) {
|
||
var sibling = node.nextSibling;
|
||
if (sibling && sibling.nodeType == node.nodeType) {
|
||
ec = node;
|
||
eo = node.length;
|
||
node.appendData(sibling.data);
|
||
removeNode(sibling);
|
||
}
|
||
};
|
||
|
||
var mergeBackward = function (node) {
|
||
var sibling = node.previousSibling;
|
||
if (sibling && sibling.nodeType == node.nodeType) {
|
||
sc = node;
|
||
var nodeLength = node.length;
|
||
so = sibling.length;
|
||
node.insertData(0, sibling.data);
|
||
removeNode(sibling);
|
||
if (sc == ec) {
|
||
eo += so;
|
||
ec = sc;
|
||
} else if (ec == node.parentNode) {
|
||
var nodeIndex = getNodeIndex(node);
|
||
if (eo == nodeIndex) {
|
||
ec = node;
|
||
eo = nodeLength;
|
||
} else if (eo > nodeIndex) {
|
||
eo--;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
var normalizeStart = true;
|
||
var sibling;
|
||
|
||
if (isCharacterDataNode(ec)) {
|
||
if (eo == ec.length) {
|
||
mergeForward(ec);
|
||
} else if (eo == 0) {
|
||
sibling = ec.previousSibling;
|
||
if (sibling && sibling.nodeType == ec.nodeType) {
|
||
eo = sibling.length;
|
||
if (sc == ec) {
|
||
normalizeStart = false;
|
||
}
|
||
sibling.appendData(ec.data);
|
||
removeNode(ec);
|
||
ec = sibling;
|
||
}
|
||
}
|
||
} else {
|
||
if (eo > 0) {
|
||
var endNode = ec.childNodes[eo - 1];
|
||
if (endNode && isCharacterDataNode(endNode)) {
|
||
mergeForward(endNode);
|
||
}
|
||
}
|
||
normalizeStart = !this.collapsed;
|
||
}
|
||
|
||
if (normalizeStart) {
|
||
if (isCharacterDataNode(sc)) {
|
||
if (so == 0) {
|
||
mergeBackward(sc);
|
||
} else if (so == sc.length) {
|
||
sibling = sc.nextSibling;
|
||
if (sibling && sibling.nodeType == sc.nodeType) {
|
||
if (ec == sibling) {
|
||
ec = sc;
|
||
eo += sc.length;
|
||
}
|
||
sc.appendData(sibling.data);
|
||
removeNode(sibling);
|
||
}
|
||
}
|
||
} else {
|
||
if (so < sc.childNodes.length) {
|
||
var startNode = sc.childNodes[so];
|
||
if (startNode && isCharacterDataNode(startNode)) {
|
||
mergeBackward(startNode);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
sc = ec;
|
||
so = eo;
|
||
}
|
||
|
||
boundaryUpdater(this, sc, so, ec, eo);
|
||
},
|
||
|
||
collapseToPoint: function (node, offset) {
|
||
assertNoDocTypeNotationEntityAncestor(node, true);
|
||
assertValidOffset(node, offset);
|
||
this.setStartAndEnd(node, offset);
|
||
}
|
||
});
|
||
|
||
copyComparisonConstants(constructor);
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Updates commonAncestorContainer and collapsed after boundary change
|
||
function updateCollapsedAndCommonAncestor(range) {
|
||
range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
|
||
range.commonAncestorContainer = range.collapsed ?
|
||
range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
|
||
}
|
||
|
||
function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
|
||
range.startContainer = startContainer;
|
||
range.startOffset = startOffset;
|
||
range.endContainer = endContainer;
|
||
range.endOffset = endOffset;
|
||
range.document = dom.getDocument(startContainer);
|
||
|
||
updateCollapsedAndCommonAncestor(range);
|
||
}
|
||
|
||
function Range(doc) {
|
||
this.startContainer = doc;
|
||
this.startOffset = 0;
|
||
this.endContainer = doc;
|
||
this.endOffset = 0;
|
||
this.document = doc;
|
||
updateCollapsedAndCommonAncestor(this);
|
||
}
|
||
|
||
createPrototypeRange(Range, updateBoundaries);
|
||
|
||
util.extend(Range, {
|
||
rangeProperties: rangeProperties,
|
||
RangeIterator: RangeIterator,
|
||
copyComparisonConstants: copyComparisonConstants,
|
||
createPrototypeRange: createPrototypeRange,
|
||
inspect: inspect,
|
||
toHtml: rangeToHtml,
|
||
getRangeDocument: getRangeDocument,
|
||
rangesEqual: function (r1, r2) {
|
||
return r1.startContainer === r2.startContainer &&
|
||
r1.startOffset === r2.startOffset &&
|
||
r1.endContainer === r2.endContainer &&
|
||
r1.endOffset === r2.endOffset;
|
||
}
|
||
});
|
||
|
||
api.DomRange = Range;
|
||
});
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Wrappers for the browser's native DOM Range and/or TextRange implementation
|
||
api.createCoreModule("WrappedRange", ["DomRange"], function (api, module) {
|
||
var WrappedRange, WrappedTextRange;
|
||
var dom = api.dom;
|
||
var util = api.util;
|
||
var DomPosition = dom.DomPosition;
|
||
var DomRange = api.DomRange;
|
||
var getBody = dom.getBody;
|
||
var getContentDocument = dom.getContentDocument;
|
||
var isCharacterDataNode = dom.isCharacterDataNode;
|
||
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
if (api.features.implementsDomRange) {
|
||
// This is a wrapper around the browser's native DOM Range. It has two aims:
|
||
// - Provide workarounds for specific browser bugs
|
||
// - provide convenient extensions, which are inherited from Rangy's DomRange
|
||
|
||
(function () {
|
||
var rangeProto;
|
||
var rangeProperties = DomRange.rangeProperties;
|
||
|
||
function updateRangeProperties(range) {
|
||
var i = rangeProperties.length, prop;
|
||
while (i--) {
|
||
prop = rangeProperties[i];
|
||
range[prop] = range.nativeRange[prop];
|
||
}
|
||
// Fix for broken collapsed property in IE 9.
|
||
range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
|
||
}
|
||
|
||
function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
|
||
var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
|
||
var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
|
||
var nativeRangeDifferent = !range.equals(range.nativeRange);
|
||
|
||
// Always set both boundaries for the benefit of IE9 (see issue 35)
|
||
if (startMoved || endMoved || nativeRangeDifferent) {
|
||
range.setEnd(endContainer, endOffset);
|
||
range.setStart(startContainer, startOffset);
|
||
}
|
||
}
|
||
|
||
var createBeforeAfterNodeSetter;
|
||
|
||
WrappedRange = function (range) {
|
||
if (!range) {
|
||
throw module.createError("WrappedRange: Range must be specified");
|
||
}
|
||
this.nativeRange = range;
|
||
updateRangeProperties(this);
|
||
};
|
||
|
||
DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
|
||
|
||
rangeProto = WrappedRange.prototype;
|
||
|
||
rangeProto.selectNode = function (node) {
|
||
this.nativeRange.selectNode(node);
|
||
updateRangeProperties(this);
|
||
};
|
||
|
||
rangeProto.cloneContents = function () {
|
||
return this.nativeRange.cloneContents();
|
||
};
|
||
|
||
// Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
|
||
// insertNode() is never delegated to the native range.
|
||
|
||
rangeProto.surroundContents = function (node) {
|
||
this.nativeRange.surroundContents(node);
|
||
updateRangeProperties(this);
|
||
};
|
||
|
||
rangeProto.collapse = function (isStart) {
|
||
this.nativeRange.collapse(isStart);
|
||
updateRangeProperties(this);
|
||
};
|
||
|
||
rangeProto.cloneRange = function () {
|
||
return new WrappedRange(this.nativeRange.cloneRange());
|
||
};
|
||
|
||
rangeProto.refresh = function () {
|
||
updateRangeProperties(this);
|
||
};
|
||
|
||
rangeProto.toString = function () {
|
||
return this.nativeRange.toString();
|
||
};
|
||
|
||
// Create test range and node for feature detection
|
||
|
||
var testTextNode = document.createTextNode("test");
|
||
getBody(document).appendChild(testTextNode);
|
||
var range = document.createRange();
|
||
|
||
/*--------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
|
||
// correct for it
|
||
|
||
range.setStart(testTextNode, 0);
|
||
range.setEnd(testTextNode, 0);
|
||
|
||
try {
|
||
range.setStart(testTextNode, 1);
|
||
|
||
rangeProto.setStart = function (node, offset) {
|
||
this.nativeRange.setStart(node, offset);
|
||
updateRangeProperties(this);
|
||
};
|
||
|
||
rangeProto.setEnd = function (node, offset) {
|
||
this.nativeRange.setEnd(node, offset);
|
||
updateRangeProperties(this);
|
||
};
|
||
|
||
createBeforeAfterNodeSetter = function (name) {
|
||
return function (node) {
|
||
this.nativeRange[name](node);
|
||
updateRangeProperties(this);
|
||
};
|
||
};
|
||
|
||
} catch (ex) {
|
||
|
||
rangeProto.setStart = function (node, offset) {
|
||
try {
|
||
this.nativeRange.setStart(node, offset);
|
||
} catch (ex) {
|
||
this.nativeRange.setEnd(node, offset);
|
||
this.nativeRange.setStart(node, offset);
|
||
}
|
||
updateRangeProperties(this);
|
||
};
|
||
|
||
rangeProto.setEnd = function (node, offset) {
|
||
try {
|
||
this.nativeRange.setEnd(node, offset);
|
||
} catch (ex) {
|
||
this.nativeRange.setStart(node, offset);
|
||
this.nativeRange.setEnd(node, offset);
|
||
}
|
||
updateRangeProperties(this);
|
||
};
|
||
|
||
createBeforeAfterNodeSetter = function (name, oppositeName) {
|
||
return function (node) {
|
||
try {
|
||
this.nativeRange[name](node);
|
||
} catch (ex) {
|
||
this.nativeRange[oppositeName](node);
|
||
this.nativeRange[name](node);
|
||
}
|
||
updateRangeProperties(this);
|
||
};
|
||
};
|
||
}
|
||
|
||
rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
|
||
rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
|
||
rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
|
||
rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
|
||
|
||
/*--------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
|
||
// whether the native implementation can be trusted
|
||
rangeProto.selectNodeContents = function (node) {
|
||
this.setStartAndEnd(node, 0, dom.getNodeLength(node));
|
||
};
|
||
|
||
/*--------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
|
||
// constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
|
||
|
||
range.selectNodeContents(testTextNode);
|
||
range.setEnd(testTextNode, 3);
|
||
|
||
var range2 = document.createRange();
|
||
range2.selectNodeContents(testTextNode);
|
||
range2.setEnd(testTextNode, 4);
|
||
range2.setStart(testTextNode, 2);
|
||
|
||
if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
|
||
range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
|
||
// This is the wrong way round, so correct for it
|
||
|
||
rangeProto.compareBoundaryPoints = function (type, range) {
|
||
range = range.nativeRange || range;
|
||
if (type == range.START_TO_END) {
|
||
type = range.END_TO_START;
|
||
} else if (type == range.END_TO_START) {
|
||
type = range.START_TO_END;
|
||
}
|
||
return this.nativeRange.compareBoundaryPoints(type, range);
|
||
};
|
||
} else {
|
||
rangeProto.compareBoundaryPoints = function (type, range) {
|
||
return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
|
||
};
|
||
}
|
||
|
||
/*--------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
|
||
|
||
var el = document.createElement("div");
|
||
el.innerHTML = "123";
|
||
var textNode = el.firstChild;
|
||
var body = getBody(document);
|
||
body.appendChild(el);
|
||
|
||
range.setStart(textNode, 1);
|
||
range.setEnd(textNode, 2);
|
||
range.deleteContents();
|
||
|
||
if (textNode.data == "13") {
|
||
// Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
|
||
// extractContents()
|
||
rangeProto.deleteContents = function () {
|
||
this.nativeRange.deleteContents();
|
||
updateRangeProperties(this);
|
||
};
|
||
|
||
rangeProto.extractContents = function () {
|
||
var frag = this.nativeRange.extractContents();
|
||
updateRangeProperties(this);
|
||
return frag;
|
||
};
|
||
} else {
|
||
}
|
||
|
||
body.removeChild(el);
|
||
body = null;
|
||
|
||
/*--------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Test for existence of createContextualFragment and delegate to it if it exists
|
||
if (util.isHostMethod(range, "createContextualFragment")) {
|
||
rangeProto.createContextualFragment = function (fragmentStr) {
|
||
return this.nativeRange.createContextualFragment(fragmentStr);
|
||
};
|
||
}
|
||
|
||
/*--------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Clean up
|
||
getBody(document).removeChild(testTextNode);
|
||
|
||
rangeProto.getName = function () {
|
||
return "WrappedRange";
|
||
};
|
||
|
||
api.WrappedRange = WrappedRange;
|
||
|
||
api.createNativeRange = function (doc) {
|
||
doc = getContentDocument(doc, module, "createNativeRange");
|
||
return doc.createRange();
|
||
};
|
||
})();
|
||
}
|
||
|
||
if (api.features.implementsTextRange) {
|
||
/*
|
||
This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
|
||
method. For example, in the following (where pipes denote the selection boundaries):
|
||
|
||
<ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
|
||
|
||
var range = document.selection.createRange();
|
||
alert(range.parentElement().id); // Should alert "ul" but alerts "b"
|
||
|
||
This method returns the common ancestor node of the following:
|
||
- the parentElement() of the textRange
|
||
- the parentElement() of the textRange after calling collapse(true)
|
||
- the parentElement() of the textRange after calling collapse(false)
|
||
*/
|
||
var getTextRangeContainerElement = function (textRange) {
|
||
var parentEl = textRange.parentElement();
|
||
var range = textRange.duplicate();
|
||
range.collapse(true);
|
||
var startEl = range.parentElement();
|
||
range = textRange.duplicate();
|
||
range.collapse(false);
|
||
var endEl = range.parentElement();
|
||
var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
|
||
|
||
return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
|
||
};
|
||
|
||
var textRangeIsCollapsed = function (textRange) {
|
||
return textRange.compareEndPoints("StartToEnd", textRange) == 0;
|
||
};
|
||
|
||
// Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
|
||
// out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
|
||
// but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
|
||
// bugs, handling for inputs and images, plus optimizations.
|
||
var getTextRangeBoundaryPosition = function (textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
|
||
var workingRange = textRange.duplicate();
|
||
workingRange.collapse(isStart);
|
||
var containerElement = workingRange.parentElement();
|
||
|
||
// Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
|
||
// check for that
|
||
if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
|
||
containerElement = wholeRangeContainerElement;
|
||
}
|
||
|
||
|
||
// Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
|
||
// similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
|
||
if (!containerElement.canHaveHTML) {
|
||
var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
|
||
return {
|
||
boundaryPosition: pos,
|
||
nodeInfo: {
|
||
nodeIndex: pos.offset,
|
||
containerElement: pos.node
|
||
}
|
||
};
|
||
}
|
||
|
||
var workingNode = dom.getDocument(containerElement).createElement("span");
|
||
|
||
// Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
|
||
// Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
|
||
if (workingNode.parentNode) {
|
||
dom.removeNode(workingNode);
|
||
}
|
||
|
||
var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
|
||
var previousNode, nextNode, boundaryPosition, boundaryNode;
|
||
var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
|
||
var childNodeCount = containerElement.childNodes.length;
|
||
var end = childNodeCount;
|
||
|
||
// Check end first. Code within the loop assumes that the endth child node of the container is definitely
|
||
// after the range boundary.
|
||
var nodeIndex = end;
|
||
|
||
while (true) {
|
||
if (nodeIndex == childNodeCount) {
|
||
containerElement.appendChild(workingNode);
|
||
} else {
|
||
containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
|
||
}
|
||
workingRange.moveToElementText(workingNode);
|
||
comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
|
||
if (comparison == 0 || start == end) {
|
||
break;
|
||
} else if (comparison == -1) {
|
||
if (end == start + 1) {
|
||
// We know the endth child node is after the range boundary, so we must be done.
|
||
break;
|
||
} else {
|
||
start = nodeIndex;
|
||
}
|
||
} else {
|
||
end = (end == start + 1) ? start : nodeIndex;
|
||
}
|
||
nodeIndex = Math.floor((start + end) / 2);
|
||
containerElement.removeChild(workingNode);
|
||
}
|
||
|
||
|
||
// We've now reached or gone past the boundary of the text range we're interested in
|
||
// so have identified the node we want
|
||
boundaryNode = workingNode.nextSibling;
|
||
|
||
if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
|
||
// This is a character data node (text, comment, cdata). The working range is collapsed at the start of
|
||
// the node containing the text range's boundary, so we move the end of the working range to the
|
||
// boundary point and measure the length of its text to get the boundary's offset within the node.
|
||
workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
|
||
|
||
var offset;
|
||
|
||
if (/[\r\n]/.test(boundaryNode.data)) {
|
||
/*
|
||
For the particular case of a boundary within a text node containing rendered line breaks (within a
|
||
<pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
|
||
IE. The facts:
|
||
|
||
- Each line break is represented as \r in the text node's data/nodeValue properties
|
||
- Each line break is represented as \r\n in the TextRange's 'text' property
|
||
- The 'text' property of the TextRange does not contain trailing line breaks
|
||
|
||
To get round the problem presented by the final fact above, we can use the fact that TextRange's
|
||
moveStart() and moveEnd() methods return the actual number of characters moved, which is not
|
||
necessarily the same as the number of characters it was instructed to move. The simplest approach is
|
||
to use this to store the characters moved when moving both the start and end of the range to the
|
||
start of the document body and subtracting the start offset from the end offset (the
|
||
"move-negative-gazillion" method). However, this is extremely slow when the document is large and
|
||
the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
|
||
the end of the document) has the same problem.
|
||
|
||
Another approach that works is to use moveStart() to move the start boundary of the range up to the
|
||
end boundary one character at a time and incrementing a counter with the value returned by the
|
||
moveStart() call. However, the check for whether the start boundary has reached the end boundary is
|
||
expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
|
||
by the location of the range within the document).
|
||
|
||
The approach used below is a hybrid of the two methods above. It uses the fact that a string
|
||
containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
|
||
be longer than the text of the TextRange, so the start of the range is moved that length initially
|
||
and then a character at a time to make up for any trailing line breaks not contained in the 'text'
|
||
property. This has good performance in most situations compared to the previous two methods.
|
||
*/
|
||
var tempRange = workingRange.duplicate();
|
||
var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
|
||
|
||
offset = tempRange.moveStart("character", rangeLength);
|
||
while ((comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
|
||
offset++;
|
||
tempRange.moveStart("character", 1);
|
||
}
|
||
} else {
|
||
offset = workingRange.text.length;
|
||
}
|
||
boundaryPosition = new DomPosition(boundaryNode, offset);
|
||
} else {
|
||
|
||
// If the boundary immediately follows a character data node and this is the end boundary, we should favour
|
||
// a position within that, and likewise for a start boundary preceding a character data node
|
||
previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
|
||
nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
|
||
if (nextNode && isCharacterDataNode(nextNode)) {
|
||
boundaryPosition = new DomPosition(nextNode, 0);
|
||
} else if (previousNode && isCharacterDataNode(previousNode)) {
|
||
boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
|
||
} else {
|
||
boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
|
||
}
|
||
}
|
||
|
||
// Clean up
|
||
dom.removeNode(workingNode);
|
||
|
||
return {
|
||
boundaryPosition: boundaryPosition,
|
||
nodeInfo: {
|
||
nodeIndex: nodeIndex,
|
||
containerElement: containerElement
|
||
}
|
||
};
|
||
};
|
||
|
||
// Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
|
||
// node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
|
||
// (http://code.google.com/p/ierange/)
|
||
var createBoundaryTextRange = function (boundaryPosition, isStart) {
|
||
var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
|
||
var doc = dom.getDocument(boundaryPosition.node);
|
||
var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
|
||
var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
|
||
|
||
if (nodeIsDataNode) {
|
||
boundaryNode = boundaryPosition.node;
|
||
boundaryParent = boundaryNode.parentNode;
|
||
} else {
|
||
childNodes = boundaryPosition.node.childNodes;
|
||
boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
|
||
boundaryParent = boundaryPosition.node;
|
||
}
|
||
|
||
// Position the range immediately before the node containing the boundary
|
||
workingNode = doc.createElement("span");
|
||
|
||
// Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
|
||
// the element rather than immediately before or after it
|
||
workingNode.innerHTML = "&#feff;";
|
||
|
||
// insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
|
||
// for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
|
||
if (boundaryNode) {
|
||
boundaryParent.insertBefore(workingNode, boundaryNode);
|
||
} else {
|
||
boundaryParent.appendChild(workingNode);
|
||
}
|
||
|
||
workingRange.moveToElementText(workingNode);
|
||
workingRange.collapse(!isStart);
|
||
|
||
// Clean up
|
||
boundaryParent.removeChild(workingNode);
|
||
|
||
// Move the working range to the text offset, if required
|
||
if (nodeIsDataNode) {
|
||
workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
|
||
}
|
||
|
||
return workingRange;
|
||
};
|
||
|
||
/*------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
|
||
// prototype
|
||
|
||
WrappedTextRange = function (textRange) {
|
||
this.textRange = textRange;
|
||
this.refresh();
|
||
};
|
||
|
||
WrappedTextRange.prototype = new DomRange(document);
|
||
|
||
WrappedTextRange.prototype.refresh = function () {
|
||
var start, end, startBoundary;
|
||
|
||
// TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
|
||
var rangeContainerElement = getTextRangeContainerElement(this.textRange);
|
||
|
||
if (textRangeIsCollapsed(this.textRange)) {
|
||
end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
|
||
true).boundaryPosition;
|
||
} else {
|
||
startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
|
||
start = startBoundary.boundaryPosition;
|
||
|
||
// An optimization used here is that if the start and end boundaries have the same parent element, the
|
||
// search scope for the end boundary can be limited to exclude the portion of the element that precedes
|
||
// the start boundary
|
||
end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
|
||
startBoundary.nodeInfo).boundaryPosition;
|
||
}
|
||
|
||
this.setStart(start.node, start.offset);
|
||
this.setEnd(end.node, end.offset);
|
||
};
|
||
|
||
WrappedTextRange.prototype.getName = function () {
|
||
return "WrappedTextRange";
|
||
};
|
||
|
||
DomRange.copyComparisonConstants(WrappedTextRange);
|
||
|
||
var rangeToTextRange = function (range) {
|
||
if (range.collapsed) {
|
||
return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
|
||
} else {
|
||
var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
|
||
var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
|
||
var textRange = getBody(DomRange.getRangeDocument(range)).createTextRange();
|
||
textRange.setEndPoint("StartToStart", startRange);
|
||
textRange.setEndPoint("EndToEnd", endRange);
|
||
return textRange;
|
||
}
|
||
};
|
||
|
||
WrappedTextRange.rangeToTextRange = rangeToTextRange;
|
||
|
||
WrappedTextRange.prototype.toTextRange = function () {
|
||
return rangeToTextRange(this);
|
||
};
|
||
|
||
api.WrappedTextRange = WrappedTextRange;
|
||
|
||
// IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
|
||
// implementation to use by default.
|
||
if (!api.features.implementsDomRange || api.config.preferTextRange) {
|
||
// Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
|
||
var globalObj = (function (f) {
|
||
return f("return this;")();
|
||
})(Function);
|
||
if (typeof globalObj.Range == "undefined") {
|
||
globalObj.Range = WrappedTextRange;
|
||
}
|
||
|
||
api.createNativeRange = function (doc) {
|
||
doc = getContentDocument(doc, module, "createNativeRange");
|
||
return getBody(doc).createTextRange();
|
||
};
|
||
|
||
api.WrappedRange = WrappedTextRange;
|
||
}
|
||
}
|
||
|
||
api.createRange = function (doc) {
|
||
doc = getContentDocument(doc, module, "createRange");
|
||
return new api.WrappedRange(api.createNativeRange(doc));
|
||
};
|
||
|
||
api.createRangyRange = function (doc) {
|
||
doc = getContentDocument(doc, module, "createRangyRange");
|
||
return new DomRange(doc);
|
||
};
|
||
|
||
util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
|
||
util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
|
||
|
||
api.addShimListener(function (win) {
|
||
var doc = win.document;
|
||
if (typeof doc.createRange == "undefined") {
|
||
doc.createRange = function () {
|
||
return api.createRange(doc);
|
||
};
|
||
}
|
||
doc = win = null;
|
||
});
|
||
});
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
|
||
// in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
|
||
api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function (api, module) {
|
||
api.config.checkSelectionRanges = true;
|
||
|
||
var BOOLEAN = "boolean";
|
||
var NUMBER = "number";
|
||
var dom = api.dom;
|
||
var util = api.util;
|
||
var isHostMethod = util.isHostMethod;
|
||
var DomRange = api.DomRange;
|
||
var WrappedRange = api.WrappedRange;
|
||
var DOMException = api.DOMException;
|
||
var DomPosition = dom.DomPosition;
|
||
var getNativeSelection;
|
||
var selectionIsCollapsed;
|
||
var features = api.features;
|
||
var CONTROL = "Control";
|
||
var getDocument = dom.getDocument;
|
||
var getBody = dom.getBody;
|
||
var rangesEqual = DomRange.rangesEqual;
|
||
|
||
|
||
// Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
|
||
// "forward" or "forwards") or a Boolean (true for backwards).
|
||
function isDirectionBackward(dir) {
|
||
return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
|
||
}
|
||
|
||
function getWindow(win, methodName) {
|
||
if (!win) {
|
||
return window;
|
||
} else if (dom.isWindow(win)) {
|
||
return win;
|
||
} else if (win instanceof WrappedSelection) {
|
||
return win.win;
|
||
} else {
|
||
var doc = dom.getContentDocument(win, module, methodName);
|
||
return dom.getWindow(doc);
|
||
}
|
||
}
|
||
|
||
function getWinSelection(winParam) {
|
||
return getWindow(winParam, "getWinSelection").getSelection();
|
||
}
|
||
|
||
function getDocSelection(winParam) {
|
||
return getWindow(winParam, "getDocSelection").document.selection;
|
||
}
|
||
|
||
function winSelectionIsBackward(sel) {
|
||
var backward = false;
|
||
if (sel.anchorNode) {
|
||
backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
|
||
}
|
||
return backward;
|
||
}
|
||
|
||
// Test for the Range/TextRange and Selection features required
|
||
// Test for ability to retrieve selection
|
||
var implementsWinGetSelection = isHostMethod(window, "getSelection"),
|
||
implementsDocSelection = util.isHostObject(document, "selection");
|
||
|
||
features.implementsWinGetSelection = implementsWinGetSelection;
|
||
features.implementsDocSelection = implementsDocSelection;
|
||
|
||
var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
|
||
|
||
if (useDocumentSelection) {
|
||
getNativeSelection = getDocSelection;
|
||
api.isSelectionValid = function (winParam) {
|
||
var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
|
||
|
||
// Check whether the selection TextRange is actually contained within the correct document
|
||
return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
|
||
};
|
||
} else if (implementsWinGetSelection) {
|
||
getNativeSelection = getWinSelection;
|
||
api.isSelectionValid = function () {
|
||
return true;
|
||
};
|
||
} else {
|
||
module.fail("Neither document.selection or window.getSelection() detected.");
|
||
return false;
|
||
}
|
||
|
||
api.getNativeSelection = getNativeSelection;
|
||
|
||
var testSelection = getNativeSelection();
|
||
|
||
// In Firefox, the selection is null in an iframe with display: none. See issue #138.
|
||
if (!testSelection) {
|
||
module.fail("Native selection was null (possibly issue 138?)");
|
||
return false;
|
||
}
|
||
|
||
var testRange = api.createNativeRange(document);
|
||
var body = getBody(document);
|
||
|
||
// Obtaining a range from a selection
|
||
var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
|
||
["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
|
||
|
||
features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
|
||
|
||
// Test for existence of native selection extend() method
|
||
var selectionHasExtend = isHostMethod(testSelection, "extend");
|
||
features.selectionHasExtend = selectionHasExtend;
|
||
|
||
// Test if rangeCount exists
|
||
var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
|
||
features.selectionHasRangeCount = selectionHasRangeCount;
|
||
|
||
var selectionSupportsMultipleRanges = false;
|
||
var collapsedNonEditableSelectionsSupported = true;
|
||
|
||
var addRangeBackwardToNative = selectionHasExtend ?
|
||
function (nativeSelection, range) {
|
||
var doc = DomRange.getRangeDocument(range);
|
||
var endRange = api.createRange(doc);
|
||
endRange.collapseToPoint(range.endContainer, range.endOffset);
|
||
nativeSelection.addRange(getNativeRange(endRange));
|
||
nativeSelection.extend(range.startContainer, range.startOffset);
|
||
} : null;
|
||
|
||
if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
|
||
typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
|
||
|
||
(function () {
|
||
// Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
|
||
// performed on the current document's selection. See issue 109.
|
||
|
||
// Note also that if a selection previously existed, it is wiped and later restored by these tests. This
|
||
// will result in the selection direction begin reversed if the original selection was backwards and the
|
||
// browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
|
||
var sel = window.getSelection();
|
||
if (sel) {
|
||
// Store the current selection
|
||
var originalSelectionRangeCount = sel.rangeCount;
|
||
var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
|
||
var originalSelectionRanges = [];
|
||
var originalSelectionBackward = winSelectionIsBackward(sel);
|
||
for (var i = 0; i < originalSelectionRangeCount; ++i) {
|
||
originalSelectionRanges[i] = sel.getRangeAt(i);
|
||
}
|
||
|
||
// Create some test elements
|
||
var testEl = dom.createTestElement(document, "", false);
|
||
var textNode = testEl.appendChild(document.createTextNode("\u00a0\u00a0\u00a0"));
|
||
|
||
// Test whether the native selection will allow a collapsed selection within a non-editable element
|
||
var r1 = document.createRange();
|
||
|
||
r1.setStart(textNode, 1);
|
||
r1.collapse(true);
|
||
sel.removeAllRanges();
|
||
sel.addRange(r1);
|
||
collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
|
||
sel.removeAllRanges();
|
||
|
||
// Test whether the native selection is capable of supporting multiple ranges.
|
||
if (!selectionHasMultipleRanges) {
|
||
// Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
|
||
// console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
|
||
// nothing we can do about this while retaining the feature test so we have to resort to a browser
|
||
// sniff. I'm not happy about it. See
|
||
// https://code.google.com/p/chromium/issues/detail?id=399791
|
||
var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
|
||
if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
|
||
selectionSupportsMultipleRanges = false;
|
||
} else {
|
||
var r2 = r1.cloneRange();
|
||
r1.setStart(textNode, 0);
|
||
r2.setEnd(textNode, 3);
|
||
r2.setStart(textNode, 2);
|
||
sel.addRange(r1);
|
||
sel.addRange(r2);
|
||
selectionSupportsMultipleRanges = (sel.rangeCount == 2);
|
||
}
|
||
}
|
||
|
||
// Clean up
|
||
dom.removeNode(testEl);
|
||
sel.removeAllRanges();
|
||
|
||
for (i = 0; i < originalSelectionRangeCount; ++i) {
|
||
if (i == 0 && originalSelectionBackward) {
|
||
if (addRangeBackwardToNative) {
|
||
addRangeBackwardToNative(sel, originalSelectionRanges[i]);
|
||
} else {
|
||
api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
|
||
sel.addRange(originalSelectionRanges[i]);
|
||
}
|
||
} else {
|
||
sel.addRange(originalSelectionRanges[i]);
|
||
}
|
||
}
|
||
}
|
||
})();
|
||
}
|
||
|
||
features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
|
||
features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
|
||
|
||
// ControlRanges
|
||
var implementsControlRange = false, testControlRange;
|
||
|
||
if (body && isHostMethod(body, "createControlRange")) {
|
||
testControlRange = body.createControlRange();
|
||
if (util.areHostProperties(testControlRange, ["item", "add"])) {
|
||
implementsControlRange = true;
|
||
}
|
||
}
|
||
features.implementsControlRange = implementsControlRange;
|
||
|
||
// Selection collapsedness
|
||
if (selectionHasAnchorAndFocus) {
|
||
selectionIsCollapsed = function (sel) {
|
||
return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
|
||
};
|
||
} else {
|
||
selectionIsCollapsed = function (sel) {
|
||
return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
|
||
};
|
||
}
|
||
|
||
function updateAnchorAndFocusFromRange(sel, range, backward) {
|
||
var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
|
||
sel.anchorNode = range[anchorPrefix + "Container"];
|
||
sel.anchorOffset = range[anchorPrefix + "Offset"];
|
||
sel.focusNode = range[focusPrefix + "Container"];
|
||
sel.focusOffset = range[focusPrefix + "Offset"];
|
||
}
|
||
|
||
function updateAnchorAndFocusFromNativeSelection(sel) {
|
||
var nativeSel = sel.nativeSelection;
|
||
sel.anchorNode = nativeSel.anchorNode;
|
||
sel.anchorOffset = nativeSel.anchorOffset;
|
||
sel.focusNode = nativeSel.focusNode;
|
||
sel.focusOffset = nativeSel.focusOffset;
|
||
}
|
||
|
||
function updateEmptySelection(sel) {
|
||
sel.anchorNode = sel.focusNode = null;
|
||
sel.anchorOffset = sel.focusOffset = 0;
|
||
sel.rangeCount = 0;
|
||
sel.isCollapsed = true;
|
||
sel._ranges.length = 0;
|
||
}
|
||
|
||
function getNativeRange(range) {
|
||
var nativeRange;
|
||
if (range instanceof DomRange) {
|
||
nativeRange = api.createNativeRange(range.getDocument());
|
||
nativeRange.setEnd(range.endContainer, range.endOffset);
|
||
nativeRange.setStart(range.startContainer, range.startOffset);
|
||
} else if (range instanceof WrappedRange) {
|
||
nativeRange = range.nativeRange;
|
||
} else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
|
||
nativeRange = range;
|
||
}
|
||
return nativeRange;
|
||
}
|
||
|
||
function rangeContainsSingleElement(rangeNodes) {
|
||
if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
|
||
return false;
|
||
}
|
||
for (var i = 1, len = rangeNodes.length; i < len; ++i) {
|
||
if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function getSingleElementFromRange(range) {
|
||
var nodes = range.getNodes();
|
||
if (!rangeContainsSingleElement(nodes)) {
|
||
throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
|
||
}
|
||
return nodes[0];
|
||
}
|
||
|
||
// Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
|
||
function isTextRange(range) {
|
||
return !!range && typeof range.text != "undefined";
|
||
}
|
||
|
||
function updateFromTextRange(sel, range) {
|
||
// Create a Range from the selected TextRange
|
||
var wrappedRange = new WrappedRange(range);
|
||
sel._ranges = [wrappedRange];
|
||
|
||
updateAnchorAndFocusFromRange(sel, wrappedRange, false);
|
||
sel.rangeCount = 1;
|
||
sel.isCollapsed = wrappedRange.collapsed;
|
||
}
|
||
|
||
function updateControlSelection(sel) {
|
||
// Update the wrapped selection based on what's now in the native selection
|
||
sel._ranges.length = 0;
|
||
if (sel.docSelection.type == "None") {
|
||
updateEmptySelection(sel);
|
||
} else {
|
||
var controlRange = sel.docSelection.createRange();
|
||
if (isTextRange(controlRange)) {
|
||
// This case (where the selection type is "Control" and calling createRange() on the selection returns
|
||
// a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
|
||
// ControlRange have been removed from the ControlRange and removed from the document.
|
||
updateFromTextRange(sel, controlRange);
|
||
} else {
|
||
sel.rangeCount = controlRange.length;
|
||
var range, doc = getDocument(controlRange.item(0));
|
||
for (var i = 0; i < sel.rangeCount; ++i) {
|
||
range = api.createRange(doc);
|
||
range.selectNode(controlRange.item(i));
|
||
sel._ranges.push(range);
|
||
}
|
||
sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
|
||
updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
|
||
}
|
||
}
|
||
}
|
||
|
||
function addRangeToControlSelection(sel, range) {
|
||
var controlRange = sel.docSelection.createRange();
|
||
var rangeElement = getSingleElementFromRange(range);
|
||
|
||
// Create a new ControlRange containing all the elements in the selected ControlRange plus the element
|
||
// contained by the supplied range
|
||
var doc = getDocument(controlRange.item(0));
|
||
var newControlRange = getBody(doc).createControlRange();
|
||
for (var i = 0, len = controlRange.length; i < len; ++i) {
|
||
newControlRange.add(controlRange.item(i));
|
||
}
|
||
try {
|
||
newControlRange.add(rangeElement);
|
||
} catch (ex) {
|
||
throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
|
||
}
|
||
newControlRange.select();
|
||
|
||
// Update the wrapped selection based on what's now in the native selection
|
||
updateControlSelection(sel);
|
||
}
|
||
|
||
var getSelectionRangeAt;
|
||
|
||
if (isHostMethod(testSelection, "getRangeAt")) {
|
||
// try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
|
||
// Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
|
||
// lesson to us all, especially me.
|
||
getSelectionRangeAt = function (sel, index) {
|
||
try {
|
||
return sel.getRangeAt(index);
|
||
} catch (ex) {
|
||
return null;
|
||
}
|
||
};
|
||
} else if (selectionHasAnchorAndFocus) {
|
||
getSelectionRangeAt = function (sel) {
|
||
var doc = getDocument(sel.anchorNode);
|
||
var range = api.createRange(doc);
|
||
range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
|
||
|
||
// Handle the case when the selection was selected backwards (from the end to the start in the
|
||
// document)
|
||
if (range.collapsed !== this.isCollapsed) {
|
||
range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
|
||
}
|
||
|
||
return range;
|
||
};
|
||
}
|
||
|
||
function WrappedSelection(selection, docSelection, win) {
|
||
this.nativeSelection = selection;
|
||
this.docSelection = docSelection;
|
||
this._ranges = [];
|
||
this.win = win;
|
||
this.refresh();
|
||
}
|
||
|
||
WrappedSelection.prototype = api.selectionPrototype;
|
||
|
||
function deleteProperties(sel) {
|
||
sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
|
||
sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
|
||
sel.detached = true;
|
||
}
|
||
|
||
var cachedRangySelections = [];
|
||
|
||
function actOnCachedSelection(win, action) {
|
||
var i = cachedRangySelections.length, cached, sel;
|
||
while (i--) {
|
||
cached = cachedRangySelections[i];
|
||
sel = cached.selection;
|
||
if (action == "deleteAll") {
|
||
deleteProperties(sel);
|
||
} else if (cached.win == win) {
|
||
if (action == "delete") {
|
||
cachedRangySelections.splice(i, 1);
|
||
return true;
|
||
} else {
|
||
return sel;
|
||
}
|
||
}
|
||
}
|
||
if (action == "deleteAll") {
|
||
cachedRangySelections.length = 0;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
var getSelection = function (win) {
|
||
// Check if the parameter is a Rangy Selection object
|
||
if (win && win instanceof WrappedSelection) {
|
||
win.refresh();
|
||
return win;
|
||
}
|
||
|
||
win = getWindow(win, "getNativeSelection");
|
||
|
||
var sel = actOnCachedSelection(win);
|
||
var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
|
||
if (sel) {
|
||
sel.nativeSelection = nativeSel;
|
||
sel.docSelection = docSel;
|
||
sel.refresh();
|
||
} else {
|
||
sel = new WrappedSelection(nativeSel, docSel, win);
|
||
cachedRangySelections.push({win: win, selection: sel});
|
||
}
|
||
return sel;
|
||
};
|
||
|
||
api.getSelection = getSelection;
|
||
|
||
util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
|
||
|
||
var selProto = WrappedSelection.prototype;
|
||
|
||
function createControlSelection(sel, ranges) {
|
||
// Ensure that the selection becomes of type "Control"
|
||
var doc = getDocument(ranges[0].startContainer);
|
||
var controlRange = getBody(doc).createControlRange();
|
||
for (var i = 0, el, len = ranges.length; i < len; ++i) {
|
||
el = getSingleElementFromRange(ranges[i]);
|
||
try {
|
||
controlRange.add(el);
|
||
} catch (ex) {
|
||
throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
|
||
}
|
||
}
|
||
controlRange.select();
|
||
|
||
// Update the wrapped selection based on what's now in the native selection
|
||
updateControlSelection(sel);
|
||
}
|
||
|
||
// Selecting a range
|
||
if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
|
||
selProto.removeAllRanges = function () {
|
||
this.nativeSelection.removeAllRanges();
|
||
updateEmptySelection(this);
|
||
};
|
||
|
||
var addRangeBackward = function (sel, range) {
|
||
addRangeBackwardToNative(sel.nativeSelection, range);
|
||
sel.refresh();
|
||
};
|
||
|
||
if (selectionHasRangeCount) {
|
||
selProto.addRange = function (range, direction) {
|
||
if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
|
||
addRangeToControlSelection(this, range);
|
||
} else {
|
||
if (isDirectionBackward(direction) && selectionHasExtend) {
|
||
addRangeBackward(this, range);
|
||
} else {
|
||
var previousRangeCount;
|
||
if (selectionSupportsMultipleRanges) {
|
||
previousRangeCount = this.rangeCount;
|
||
} else {
|
||
this.removeAllRanges();
|
||
previousRangeCount = 0;
|
||
}
|
||
// Clone the native range so that changing the selected range does not affect the selection.
|
||
// This is contrary to the spec but is the only way to achieve consistency between browsers. See
|
||
// issue 80.
|
||
var clonedNativeRange = getNativeRange(range).cloneRange();
|
||
try {
|
||
this.nativeSelection.addRange(clonedNativeRange);
|
||
} catch (ex) {
|
||
}
|
||
|
||
// Check whether adding the range was successful
|
||
this.rangeCount = this.nativeSelection.rangeCount;
|
||
|
||
if (this.rangeCount == previousRangeCount + 1) {
|
||
// The range was added successfully
|
||
|
||
// Check whether the range that we added to the selection is reflected in the last range extracted from
|
||
// the selection
|
||
if (api.config.checkSelectionRanges) {
|
||
var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
|
||
if (nativeRange && !rangesEqual(nativeRange, range)) {
|
||
// Happens in WebKit with, for example, a selection placed at the start of a text node
|
||
range = new WrappedRange(nativeRange);
|
||
}
|
||
}
|
||
this._ranges[this.rangeCount - 1] = range;
|
||
updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
|
||
this.isCollapsed = selectionIsCollapsed(this);
|
||
} else {
|
||
// The range was not added successfully. The simplest thing is to refresh
|
||
this.refresh();
|
||
}
|
||
}
|
||
}
|
||
};
|
||
} else {
|
||
selProto.addRange = function (range, direction) {
|
||
if (isDirectionBackward(direction) && selectionHasExtend) {
|
||
addRangeBackward(this, range);
|
||
} else {
|
||
this.nativeSelection.addRange(getNativeRange(range));
|
||
this.refresh();
|
||
}
|
||
};
|
||
}
|
||
|
||
selProto.setRanges = function (ranges) {
|
||
if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
|
||
createControlSelection(this, ranges);
|
||
} else {
|
||
this.removeAllRanges();
|
||
for (var i = 0, len = ranges.length; i < len; ++i) {
|
||
this.addRange(ranges[i]);
|
||
}
|
||
}
|
||
};
|
||
} else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
|
||
implementsControlRange && useDocumentSelection) {
|
||
|
||
selProto.removeAllRanges = function () {
|
||
// Added try/catch as fix for issue #21
|
||
try {
|
||
this.docSelection.empty();
|
||
|
||
// Check for empty() not working (issue #24)
|
||
if (this.docSelection.type != "None") {
|
||
// Work around failure to empty a control selection by instead selecting a TextRange and then
|
||
// calling empty()
|
||
var doc;
|
||
if (this.anchorNode) {
|
||
doc = getDocument(this.anchorNode);
|
||
} else if (this.docSelection.type == CONTROL) {
|
||
var controlRange = this.docSelection.createRange();
|
||
if (controlRange.length) {
|
||
doc = getDocument(controlRange.item(0));
|
||
}
|
||
}
|
||
if (doc) {
|
||
var textRange = getBody(doc).createTextRange();
|
||
textRange.select();
|
||
this.docSelection.empty();
|
||
}
|
||
}
|
||
} catch (ex) {
|
||
}
|
||
updateEmptySelection(this);
|
||
};
|
||
|
||
selProto.addRange = function (range) {
|
||
if (this.docSelection.type == CONTROL) {
|
||
addRangeToControlSelection(this, range);
|
||
} else {
|
||
api.WrappedTextRange.rangeToTextRange(range).select();
|
||
this._ranges[0] = range;
|
||
this.rangeCount = 1;
|
||
this.isCollapsed = this._ranges[0].collapsed;
|
||
updateAnchorAndFocusFromRange(this, range, false);
|
||
}
|
||
};
|
||
|
||
selProto.setRanges = function (ranges) {
|
||
this.removeAllRanges();
|
||
var rangeCount = ranges.length;
|
||
if (rangeCount > 1) {
|
||
createControlSelection(this, ranges);
|
||
} else if (rangeCount) {
|
||
this.addRange(ranges[0]);
|
||
}
|
||
};
|
||
} else {
|
||
module.fail("No means of selecting a Range or TextRange was found");
|
||
return false;
|
||
}
|
||
|
||
selProto.getRangeAt = function (index) {
|
||
if (index < 0 || index >= this.rangeCount) {
|
||
throw new DOMException("INDEX_SIZE_ERR");
|
||
} else {
|
||
// Clone the range to preserve selection-range independence. See issue 80.
|
||
return this._ranges[index].cloneRange();
|
||
}
|
||
};
|
||
|
||
var refreshSelection;
|
||
|
||
if (useDocumentSelection) {
|
||
refreshSelection = function (sel) {
|
||
var range;
|
||
if (api.isSelectionValid(sel.win)) {
|
||
range = sel.docSelection.createRange();
|
||
} else {
|
||
range = getBody(sel.win.document).createTextRange();
|
||
range.collapse(true);
|
||
}
|
||
|
||
if (sel.docSelection.type == CONTROL) {
|
||
updateControlSelection(sel);
|
||
} else if (isTextRange(range)) {
|
||
updateFromTextRange(sel, range);
|
||
} else {
|
||
updateEmptySelection(sel);
|
||
}
|
||
};
|
||
} else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
|
||
refreshSelection = function (sel) {
|
||
if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
|
||
updateControlSelection(sel);
|
||
} else {
|
||
sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
|
||
if (sel.rangeCount) {
|
||
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
|
||
sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
|
||
}
|
||
updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
|
||
sel.isCollapsed = selectionIsCollapsed(sel);
|
||
} else {
|
||
updateEmptySelection(sel);
|
||
}
|
||
}
|
||
};
|
||
} else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
|
||
refreshSelection = function (sel) {
|
||
var range, nativeSel = sel.nativeSelection;
|
||
if (nativeSel.anchorNode) {
|
||
range = getSelectionRangeAt(nativeSel, 0);
|
||
sel._ranges = [range];
|
||
sel.rangeCount = 1;
|
||
updateAnchorAndFocusFromNativeSelection(sel);
|
||
sel.isCollapsed = selectionIsCollapsed(sel);
|
||
} else {
|
||
updateEmptySelection(sel);
|
||
}
|
||
};
|
||
} else {
|
||
module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
|
||
return false;
|
||
}
|
||
|
||
selProto.refresh = function (checkForChanges) {
|
||
var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
|
||
var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
|
||
|
||
refreshSelection(this);
|
||
if (checkForChanges) {
|
||
// Check the range count first
|
||
var i = oldRanges.length;
|
||
if (i != this._ranges.length) {
|
||
return true;
|
||
}
|
||
|
||
// Now check the direction. Checking the anchor position is the same is enough since we're checking all the
|
||
// ranges after this
|
||
if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
|
||
return true;
|
||
}
|
||
|
||
// Finally, compare each range in turn
|
||
while (i--) {
|
||
if (!rangesEqual(oldRanges[i], this._ranges[i])) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// Removal of a single range
|
||
var removeRangeManually = function (sel, range) {
|
||
var ranges = sel.getAllRanges();
|
||
sel.removeAllRanges();
|
||
for (var i = 0, len = ranges.length; i < len; ++i) {
|
||
if (!rangesEqual(range, ranges[i])) {
|
||
sel.addRange(ranges[i]);
|
||
}
|
||
}
|
||
if (!sel.rangeCount) {
|
||
updateEmptySelection(sel);
|
||
}
|
||
};
|
||
|
||
if (implementsControlRange && implementsDocSelection) {
|
||
selProto.removeRange = function (range) {
|
||
if (this.docSelection.type == CONTROL) {
|
||
var controlRange = this.docSelection.createRange();
|
||
var rangeElement = getSingleElementFromRange(range);
|
||
|
||
// Create a new ControlRange containing all the elements in the selected ControlRange minus the
|
||
// element contained by the supplied range
|
||
var doc = getDocument(controlRange.item(0));
|
||
var newControlRange = getBody(doc).createControlRange();
|
||
var el, removed = false;
|
||
for (var i = 0, len = controlRange.length; i < len; ++i) {
|
||
el = controlRange.item(i);
|
||
if (el !== rangeElement || removed) {
|
||
newControlRange.add(controlRange.item(i));
|
||
} else {
|
||
removed = true;
|
||
}
|
||
}
|
||
newControlRange.select();
|
||
|
||
// Update the wrapped selection based on what's now in the native selection
|
||
updateControlSelection(this);
|
||
} else {
|
||
removeRangeManually(this, range);
|
||
}
|
||
};
|
||
} else {
|
||
selProto.removeRange = function (range) {
|
||
removeRangeManually(this, range);
|
||
};
|
||
}
|
||
|
||
// Detecting if a selection is backward
|
||
var selectionIsBackward;
|
||
if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
|
||
selectionIsBackward = winSelectionIsBackward;
|
||
|
||
selProto.isBackward = function () {
|
||
return selectionIsBackward(this);
|
||
};
|
||
} else {
|
||
selectionIsBackward = selProto.isBackward = function () {
|
||
return false;
|
||
};
|
||
}
|
||
|
||
// Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
|
||
selProto.isBackwards = selProto.isBackward;
|
||
|
||
// Selection stringifier
|
||
// This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
|
||
// The current spec does not yet define this method.
|
||
selProto.toString = function () {
|
||
var rangeTexts = [];
|
||
for (var i = 0, len = this.rangeCount; i < len; ++i) {
|
||
rangeTexts[i] = "" + this._ranges[i];
|
||
}
|
||
return rangeTexts.join("");
|
||
};
|
||
|
||
function assertNodeInSameDocument(sel, node) {
|
||
if (sel.win.document != getDocument(node)) {
|
||
throw new DOMException("WRONG_DOCUMENT_ERR");
|
||
}
|
||
}
|
||
|
||
// No current browser conforms fully to the spec for this method, so Rangy's own method is always used
|
||
selProto.collapse = function (node, offset) {
|
||
assertNodeInSameDocument(this, node);
|
||
var range = api.createRange(node);
|
||
range.collapseToPoint(node, offset);
|
||
this.setSingleRange(range);
|
||
this.isCollapsed = true;
|
||
};
|
||
|
||
selProto.collapseToStart = function () {
|
||
if (this.rangeCount) {
|
||
var range = this._ranges[0];
|
||
this.collapse(range.startContainer, range.startOffset);
|
||
} else {
|
||
throw new DOMException("INVALID_STATE_ERR");
|
||
}
|
||
};
|
||
|
||
selProto.collapseToEnd = function () {
|
||
if (this.rangeCount) {
|
||
var range = this._ranges[this.rangeCount - 1];
|
||
this.collapse(range.endContainer, range.endOffset);
|
||
} else {
|
||
throw new DOMException("INVALID_STATE_ERR");
|
||
}
|
||
};
|
||
|
||
// The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
|
||
// specified so the native implementation is never used by Rangy.
|
||
selProto.selectAllChildren = function (node) {
|
||
assertNodeInSameDocument(this, node);
|
||
var range = api.createRange(node);
|
||
range.selectNodeContents(node);
|
||
this.setSingleRange(range);
|
||
};
|
||
|
||
selProto.deleteFromDocument = function () {
|
||
// Sepcial behaviour required for IE's control selections
|
||
if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
|
||
var controlRange = this.docSelection.createRange();
|
||
var element;
|
||
while (controlRange.length) {
|
||
element = controlRange.item(0);
|
||
controlRange.remove(element);
|
||
dom.removeNode(element);
|
||
}
|
||
this.refresh();
|
||
} else if (this.rangeCount) {
|
||
var ranges = this.getAllRanges();
|
||
if (ranges.length) {
|
||
this.removeAllRanges();
|
||
for (var i = 0, len = ranges.length; i < len; ++i) {
|
||
ranges[i].deleteContents();
|
||
}
|
||
// The spec says nothing about what the selection should contain after calling deleteContents on each
|
||
// range. Firefox moves the selection to where the final selected range was, so we emulate that
|
||
this.addRange(ranges[len - 1]);
|
||
}
|
||
}
|
||
};
|
||
|
||
// The following are non-standard extensions
|
||
selProto.eachRange = function (func, returnValue) {
|
||
for (var i = 0, len = this._ranges.length; i < len; ++i) {
|
||
if (func(this.getRangeAt(i))) {
|
||
return returnValue;
|
||
}
|
||
}
|
||
};
|
||
|
||
selProto.getAllRanges = function () {
|
||
var ranges = [];
|
||
this.eachRange(function (range) {
|
||
ranges.push(range);
|
||
});
|
||
return ranges;
|
||
};
|
||
|
||
selProto.setSingleRange = function (range, direction) {
|
||
this.removeAllRanges();
|
||
this.addRange(range, direction);
|
||
};
|
||
|
||
selProto.callMethodOnEachRange = function (methodName, params) {
|
||
var results = [];
|
||
this.eachRange(function (range) {
|
||
results.push(range[methodName].apply(range, params || []));
|
||
});
|
||
return results;
|
||
};
|
||
|
||
function createStartOrEndSetter(isStart) {
|
||
return function (node, offset) {
|
||
var range;
|
||
if (this.rangeCount) {
|
||
range = this.getRangeAt(0);
|
||
range["set" + (isStart ? "Start" : "End")](node, offset);
|
||
} else {
|
||
range = api.createRange(this.win.document);
|
||
range.setStartAndEnd(node, offset);
|
||
}
|
||
this.setSingleRange(range, this.isBackward());
|
||
};
|
||
}
|
||
|
||
selProto.setStart = createStartOrEndSetter(true);
|
||
selProto.setEnd = createStartOrEndSetter(false);
|
||
|
||
// Add select() method to Range prototype. Any existing selection will be removed.
|
||
api.rangePrototype.select = function (direction) {
|
||
getSelection(this.getDocument()).setSingleRange(this, direction);
|
||
};
|
||
|
||
selProto.changeEachRange = function (func) {
|
||
var ranges = [];
|
||
var backward = this.isBackward();
|
||
|
||
this.eachRange(function (range) {
|
||
func(range);
|
||
ranges.push(range);
|
||
});
|
||
|
||
this.removeAllRanges();
|
||
if (backward && ranges.length == 1) {
|
||
this.addRange(ranges[0], "backward");
|
||
} else {
|
||
this.setRanges(ranges);
|
||
}
|
||
};
|
||
|
||
selProto.containsNode = function (node, allowPartial) {
|
||
return this.eachRange(function (range) {
|
||
return range.containsNode(node, allowPartial);
|
||
}, true) || false;
|
||
};
|
||
|
||
selProto.getBookmark = function (containerNode) {
|
||
return {
|
||
backward: this.isBackward(),
|
||
rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
|
||
};
|
||
};
|
||
|
||
selProto.moveToBookmark = function (bookmark) {
|
||
var selRanges = [];
|
||
for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++];) {
|
||
range = api.createRange(this.win);
|
||
range.moveToBookmark(rangeBookmark);
|
||
selRanges.push(range);
|
||
}
|
||
if (bookmark.backward) {
|
||
this.setSingleRange(selRanges[0], "backward");
|
||
} else {
|
||
this.setRanges(selRanges);
|
||
}
|
||
};
|
||
|
||
selProto.saveRanges = function () {
|
||
return {
|
||
backward: this.isBackward(),
|
||
ranges: this.callMethodOnEachRange("cloneRange")
|
||
};
|
||
};
|
||
|
||
selProto.restoreRanges = function (selRanges) {
|
||
this.removeAllRanges();
|
||
for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
|
||
this.addRange(range, (selRanges.backward && i == 0));
|
||
}
|
||
};
|
||
|
||
selProto.toHtml = function () {
|
||
var rangeHtmls = [];
|
||
this.eachRange(function (range) {
|
||
rangeHtmls.push(DomRange.toHtml(range));
|
||
});
|
||
return rangeHtmls.join("");
|
||
};
|
||
|
||
if (features.implementsTextRange) {
|
||
selProto.getNativeTextRange = function () {
|
||
var sel, textRange;
|
||
if ((sel = this.docSelection)) {
|
||
var range = sel.createRange();
|
||
if (isTextRange(range)) {
|
||
return range;
|
||
} else {
|
||
throw module.createError("getNativeTextRange: selection is a control selection");
|
||
}
|
||
} else if (this.rangeCount > 0) {
|
||
return api.WrappedTextRange.rangeToTextRange(this.getRangeAt(0));
|
||
} else {
|
||
throw module.createError("getNativeTextRange: selection contains no range");
|
||
}
|
||
};
|
||
}
|
||
|
||
function inspect(sel) {
|
||
var rangeInspects = [];
|
||
var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
|
||
var focus = new DomPosition(sel.focusNode, sel.focusOffset);
|
||
var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
|
||
|
||
if (typeof sel.rangeCount != "undefined") {
|
||
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
|
||
rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
|
||
}
|
||
}
|
||
return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
|
||
")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
|
||
}
|
||
|
||
selProto.getName = function () {
|
||
return "WrappedSelection";
|
||
};
|
||
|
||
selProto.inspect = function () {
|
||
return inspect(this);
|
||
};
|
||
|
||
selProto.detach = function () {
|
||
actOnCachedSelection(this.win, "delete");
|
||
deleteProperties(this);
|
||
};
|
||
|
||
WrappedSelection.detachAll = function () {
|
||
actOnCachedSelection(null, "deleteAll");
|
||
};
|
||
|
||
WrappedSelection.inspect = inspect;
|
||
WrappedSelection.isDirectionBackward = isDirectionBackward;
|
||
|
||
api.Selection = WrappedSelection;
|
||
|
||
api.selectionPrototype = selProto;
|
||
|
||
api.addShimListener(function (win) {
|
||
if (typeof win.getSelection == "undefined") {
|
||
win.getSelection = function () {
|
||
return getSelection(win);
|
||
};
|
||
}
|
||
win = null;
|
||
});
|
||
});
|
||
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------*/
|
||
|
||
// Wait for document to load before initializing
|
||
var docReady = false
|
||
var docLoad = false
|
||
var readyList = []
|
||
var loadList = []; // only run after load finish
|
||
|
||
var readyHandler = function (e) {
|
||
if (!docReady) {
|
||
docReady = true;
|
||
if (!api.initialized && api.config.autoInitialize) {
|
||
init();
|
||
}
|
||
util.forEach(readyList, function (func) {
|
||
func()
|
||
})
|
||
readyList = []
|
||
}
|
||
};
|
||
|
||
var loadHandler = function (e) {
|
||
if (!docLoad) {
|
||
docLoad = true;
|
||
util.forEach(loadList, function (func) {
|
||
func()
|
||
})
|
||
loadList = []
|
||
}
|
||
};
|
||
|
||
if (isBrowser) {
|
||
// Test whether the document has already been loaded and initialize immediately if so
|
||
if (document.readyState === "complete") {
|
||
readyHandler()
|
||
loadHandler();
|
||
} else {
|
||
if (isHostMethod(document, "addEventListener")) {
|
||
document.addEventListener("DOMContentLoaded", readyHandler, false);
|
||
}
|
||
// Add a fallback in case the DOMContentLoaded event isn't supported
|
||
addListener(window, "load", readyHandler);
|
||
addListener(window, "load", loadHandler);
|
||
}
|
||
}
|
||
|
||
api.ready = function (func) {
|
||
if (docReady) {
|
||
func()
|
||
} else {
|
||
readyList.push(func)
|
||
}
|
||
}
|
||
|
||
api.load = function (func) {
|
||
if(docLoad){
|
||
func()
|
||
}else{
|
||
loadList.push(func)
|
||
}
|
||
}
|
||
|
||
// ============ tap ==============
|
||
function mix(target) {
|
||
target = target || {}
|
||
var options, name, src, copy, copyIsArray, clone,
|
||
i = 1,
|
||
length = arguments.length
|
||
|
||
if (typeof target !== 'object' && typeof target !== 'function') {
|
||
target = {}
|
||
}
|
||
|
||
if (i === length) {
|
||
target = this
|
||
i--
|
||
}
|
||
|
||
for (; i < length; i++) {
|
||
//只处理非空参数
|
||
if ((options = arguments[i]) != null) {
|
||
for (name in options) {
|
||
if(options.hasOwnProperty(name)){
|
||
target[name] = options[name]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return target
|
||
}
|
||
var ua = navigator.userAgent.toLowerCase()
|
||
var eventHooks = {}
|
||
|
||
var Recognizer = {
|
||
isAndroid: ua.indexOf('android') > 0,
|
||
isIOS: /iPad|iPhone|iPod/i.test(ua),
|
||
fire: function(elem, type, props) {
|
||
if (elem) {
|
||
var event = document.createEvent('Events')
|
||
event.initEvent(type, true, true)
|
||
mix(event, props)
|
||
elem.dispatchEvent(event)
|
||
}
|
||
},
|
||
|
||
add: function(name, recognizer) {
|
||
function move(event) {
|
||
recognizer.touchmove(event)
|
||
}
|
||
|
||
function end(event) {
|
||
recognizer.touchend(event)
|
||
|
||
document.removeEventListener('touchmove', move)
|
||
|
||
document.removeEventListener('touchend', end)
|
||
|
||
document.removeEventListener('touchcancel', cancel)
|
||
|
||
}
|
||
|
||
function cancel(event) {
|
||
recognizer.touchcancel(event)
|
||
|
||
document.removeEventListener('touchmove', move)
|
||
|
||
document.removeEventListener('touchend', end)
|
||
|
||
document.removeEventListener('touchcancel', cancel)
|
||
|
||
}
|
||
|
||
recognizer.events.forEach(function(eventName) {
|
||
eventHooks[eventName] = {
|
||
fix: function(el, fn) {
|
||
if (!el['touch-' + name]) {
|
||
el['touch-' + name] = 1
|
||
el.addEventListener('touchstart', function(event) {
|
||
recognizer.touchstart(event)
|
||
|
||
document.addEventListener('touchmove', move)
|
||
|
||
document.addEventListener('touchend', end)
|
||
|
||
document.addEventListener('touchcancel', cancel)
|
||
|
||
})
|
||
}
|
||
return fn
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
var tapRecognizer = {
|
||
events: ['tap'],
|
||
touchBoundary: 10,
|
||
tapDelay: 250,
|
||
|
||
fixTarget: function(target) {
|
||
if (target.nodeType === 3) {
|
||
return target.parentNode
|
||
}
|
||
if (window.SVGElementInstance && (target instanceof SVGElementInstance)) {
|
||
return target.correspondingUseElement
|
||
}
|
||
|
||
return target
|
||
},
|
||
|
||
updateScrollParent: function(targetElement) {
|
||
//如果事件源元素位于某一个有滚动条的祖父元素中,那么保持其scrollParent与scrollTop值
|
||
var scrollParent = targetElement.tapScrollParent
|
||
|
||
if (!scrollParent || !scrollParent.contains(targetElement)) {
|
||
var parentElement = targetElement
|
||
do {
|
||
if (parentElement.scrollHeight > parentElement.offsetHeight) {
|
||
scrollParent = parentElement
|
||
targetElement.tapScrollParent = parentElement
|
||
break
|
||
}
|
||
|
||
parentElement = parentElement.parentElement
|
||
} while (parentElement)
|
||
}
|
||
|
||
if (scrollParent) {
|
||
scrollParent.lastScrollTop = scrollParent.scrollTop
|
||
}
|
||
},
|
||
|
||
touchHasMoved: function(event) {
|
||
//判定是否发生移动,其阀值是10px
|
||
var touch = event.changedTouches[0],
|
||
boundary = tapRecognizer.touchBoundary
|
||
return Math.abs(touch.pageX - tapRecognizer.pageX) > boundary ||
|
||
Math.abs(touch.pageY - tapRecognizer.pageY) > boundary
|
||
|
||
},
|
||
|
||
touchstart: function(event) {
|
||
//忽略多点触摸
|
||
if (event.targetTouches.length !== 1) {
|
||
return true
|
||
}
|
||
//修正事件源对象
|
||
var targetElement = tapRecognizer.fixTarget(event.target)
|
||
var touch = event.targetTouches[0]
|
||
if (Recognizer.isIOS) {
|
||
// 判断是否是点击文字,进行选择等操作,如果是,不需要模拟click
|
||
var selection = window.getSelection()
|
||
if (selection.rangeCount && !selection.isCollapsed) {
|
||
return true
|
||
}
|
||
var id = touch.identifier
|
||
//当 alert 或 confirm 时,点击其他地方,会触发touch事件,identifier相同,此事件应该被忽略
|
||
if (id && isFinite(tapRecognizer.lastTouchIdentifier) && tapRecognizer.lastTouchIdentifier === id) {
|
||
event.preventDefault()
|
||
return false
|
||
}
|
||
|
||
tapRecognizer.lastTouchIdentifier = id
|
||
|
||
tapRecognizer.updateScrollParent(targetElement)
|
||
}
|
||
//收集触摸点的信息
|
||
tapRecognizer.status = "tapping"
|
||
tapRecognizer.startTime = Date.now()
|
||
tapRecognizer.element = targetElement
|
||
tapRecognizer.pageX = touch.pageX
|
||
tapRecognizer.pageY = touch.pageY
|
||
// 如果点击太快,阻止双击带来的放大收缩行为
|
||
if ((tapRecognizer.startTime - tapRecognizer.lastTime) < tapRecognizer.tapDelay) {
|
||
event.preventDefault()
|
||
}
|
||
},
|
||
|
||
touchmove: function(event) {
|
||
if (tapRecognizer.status !== "tapping") {
|
||
return true
|
||
}
|
||
// 如果事件源元素发生改变,或者发生了移动,那么就取消触发点击事件
|
||
if (tapRecognizer.element !== tapRecognizer.fixTarget(event.target) ||
|
||
tapRecognizer.touchHasMoved(event)) {
|
||
tapRecognizer.status = tapRecognizer.element = 0
|
||
}
|
||
},
|
||
|
||
touchend: function(event) {
|
||
var targetElement = tapRecognizer.element
|
||
var now = Date.now()
|
||
//如果是touchstart与touchend相隔太久,可以认为是长按,那么就直接返回
|
||
//或者是在touchstart, touchmove阶段,判定其不该触发点击事件,也直接返回
|
||
if (!targetElement || now - tapRecognizer.startTime > tapRecognizer.tapDelay) {
|
||
return true
|
||
}
|
||
|
||
tapRecognizer.lastTime = now
|
||
|
||
var startTime = tapRecognizer.startTime
|
||
tapRecognizer.status = tapRecognizer.startTime = 0
|
||
|
||
var targetTagName = targetElement.tagName.toLowerCase()
|
||
if (targetTagName === 'label') {
|
||
//尝试触发label上可能绑定的tap事件
|
||
Recognizer.fire(targetElement, 'tap', {
|
||
touchEvent: event
|
||
})
|
||
var forElement = tapRecognizer.findControl(targetElement)
|
||
if (forElement) {
|
||
tapRecognizer.focus(targetElement)
|
||
targetElement = forElement
|
||
}
|
||
} else if (tapRecognizer.needFocus(targetElement)) {
|
||
// 如果元素从touchstart到touchend经历时间过长,那么不应该触发点击事
|
||
// 或者此元素是iframe中的input元素,那么它也无法获点焦点
|
||
if ((now - startTime) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
|
||
tapRecognizer.element = 0
|
||
return false
|
||
}
|
||
|
||
tapRecognizer.focus(targetElement)
|
||
Recognizer.isAndroid && tapRecognizer.sendClick(targetElement, event)
|
||
|
||
return false
|
||
}
|
||
|
||
if (Recognizer.isIOS) {
|
||
//如果它的父容器的滚动条发生改变,那么应该识别为划动或拖动事件,不应该触发点击事件
|
||
var scrollParent = targetElement.tapScrollParent
|
||
if (scrollParent && scrollParent.lastScrollTop !== scrollParent.scrollTop) {
|
||
return true
|
||
}
|
||
}
|
||
//如果这不是一个需要使用原生click的元素,则屏蔽原生事件,避免触发两次click
|
||
if (!tapRecognizer.needClick(targetElement)) {
|
||
event.preventDefault()
|
||
// 触发一次模拟的click
|
||
tapRecognizer.sendClick(targetElement, event)
|
||
}
|
||
},
|
||
touchcancel: function() {
|
||
tapRecognizer.startTime = tapRecognizer.element = 0
|
||
},
|
||
|
||
needClick: function(target) {
|
||
//判定是否使用原生的点击事件, 否则使用sendClick方法手动触发一个人工的点击事件
|
||
switch (target.nodeName.toLowerCase()) {
|
||
case 'button':
|
||
case 'select':
|
||
case 'textarea':
|
||
if (target.disabled) {
|
||
return true
|
||
}
|
||
break
|
||
case 'input':
|
||
if ((Recognizer.isIOS && target.type === 'file') || target.disabled) {
|
||
return true
|
||
}
|
||
|
||
break
|
||
case 'label':
|
||
case 'iframe':
|
||
case 'video':
|
||
return true
|
||
}
|
||
|
||
return false
|
||
},
|
||
|
||
needFocus: function(target) {
|
||
switch (target.nodeName.toLowerCase()) {
|
||
case 'textarea':
|
||
case 'select': //实测android下select也需要
|
||
return true
|
||
case 'input':
|
||
switch (target.type) {
|
||
case 'button':
|
||
case 'checkbox':
|
||
case 'file':
|
||
case 'image':
|
||
case 'radio':
|
||
case 'submit':
|
||
return false
|
||
}
|
||
//如果是只读或disabled状态,就无须获得焦点了
|
||
return !target.disabled && !target.readOnly
|
||
default:
|
||
return false
|
||
}
|
||
},
|
||
focus: function(targetElement) {
|
||
var length
|
||
//在iOS7下, 对一些新表单元素(如date, datetime, time, month)调用focus方法会抛错,
|
||
//幸好的是,我们可以改用setSelectionRange获取焦点, 将光标挪到文字的最后
|
||
var type = targetElement.type
|
||
if (Recognizer.isIOS && targetElement.setSelectionRange &&
|
||
type.indexOf('date') !== 0 && type !== 'time' && type !== 'month') {
|
||
length = targetElement.value.length
|
||
targetElement.setSelectionRange(length, length)
|
||
} else {
|
||
targetElement.focus()
|
||
}
|
||
},
|
||
findControl: function(labelElement) {
|
||
// 获取label元素所对应的表单元素
|
||
// 可以能过control属性, getElementById, 或用querySelector直接找其内部第一表单元素实现
|
||
if (labelElement.control !== undefined) {
|
||
return labelElement.control
|
||
}
|
||
|
||
if (labelElement.htmlFor) {
|
||
return document.getElementById(labelElement.htmlFor)
|
||
}
|
||
|
||
return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea')
|
||
},
|
||
|
||
findType: function(targetElement) {
|
||
// 安卓chrome浏览器上,模拟的 click 事件不能让 select 打开,故使用 mousedown 事件
|
||
return Recognizer.isAndroid && targetElement.tagName.toLowerCase() === 'select' ?
|
||
'mousedown' : 'click'
|
||
},
|
||
|
||
sendClick: function(targetElement, event) {
|
||
// 在click之前触发tap事件
|
||
Recognizer.fire(targetElement, 'tap', {
|
||
touchEvent: event
|
||
})
|
||
var clickEvent, touch
|
||
//某些安卓设备必须先移除焦点,之后模拟的click事件才能让新元素获取焦点
|
||
if (document.activeElement && document.activeElement !== targetElement) {
|
||
document.activeElement.blur()
|
||
}
|
||
|
||
touch = event.changedTouches[0]
|
||
// 手动触发点击事件,此时必须使用document.createEvent('MouseEvents')来创建事件
|
||
// 及使用initMouseEvent来初始化它
|
||
clickEvent = document.createEvent('MouseEvents')
|
||
clickEvent.initMouseEvent(tapRecognizer.findType(targetElement), true, true,
|
||
window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY,
|
||
false, false, false, false, 0, null)
|
||
clickEvent.touchEvent = event
|
||
targetElement.dispatchEvent(clickEvent)
|
||
}
|
||
|
||
}
|
||
|
||
Recognizer.add('tap', tapRecognizer)
|
||
|
||
api.attachTouch = function(el, type) {
|
||
if(eventHooks[type]){
|
||
eventHooks[type].fix(el, null)
|
||
}
|
||
}
|
||
|
||
api.Recognizer = Recognizer
|
||
|
||
return api;
|
||
}, this); |