rangy
Version:
A cross-browser DOM range and selection library
1,354 lines (1,154 loc) • 164 kB
JavaScript
/**
* Rangy, a cross-browser JavaScript range and selection library
* https://github.com/timdown/rangy
*
* Copyright 2024, Tim Down
* Licensed under the MIT license.
* Version: 1.3.2
* Build date: 2 November 2024
*/
(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.2",
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 (i === "__proto__" || i === "constructor" || i === "prototype") {
continue;
}
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 because we don't need this.
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] );
var getElementAncestor = createAncestorFinder( [1] );
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.endCont