diff --git a/lib/rangy/rangy-core.js b/lib/rangy/rangy-core.js index 8cda5f68..69e95bfa 100644 --- a/lib/rangy/rangy-core.js +++ b/lib/rangy/rangy-core.js @@ -1,20 +1,34 @@ /** - * @license Rangy, a cross-browser JavaScript range and selection library - * http://code.google.com/p/rangy/ + * Rangy, a cross-browser JavaScript range and selection library + * https://github.com/timdown/rangy * - * Copyright 2012, Tim Down + * Copyright 2015, Tim Down * Licensed under the MIT license. - * Version: 1.2.3 - * Build date: 26 February 2012 + * Version: 1.3.0 + * Build date: 10 May 2015 */ -window['rangy'] = (function() { +(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", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"]; + "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"]; @@ -22,8 +36,8 @@ window['rangy'] = (function() { 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", "getBookmark", "moveToBookmark", - "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"]; + var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select", + "setEndPoint", "getBoundingClientRect"]; /*----------------------------------------------------------------------------------------------------------------*/ @@ -64,67 +78,177 @@ window['rangy'] = (function() { 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.2.3", + version: "1.3.0", initialized: false, + isBrowser: isBrowser, supported: true, - - util: { - isHostMethod: isHostMethod, - isHostObject: isHostObject, - isHostProperty: isHostProperty, - areHostMethods: areHostMethods, - areHostObjects: areHostObjects, - areHostProperties: areHostProperties, - isTextRange: isTextRange - }, - + util: util, features: {}, - - modules: {}, + modules: modules, config: { + alertOnFail: false, alertOnWarn: false, - preferTextRange: 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) { - window.alert("Rangy not supported in your browser. Reason: " + 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) { - var warningMessage = "Rangy warning: " + msg; - if (api.config.alertOnWarn) { - window.alert(warningMessage); - } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) { - window.console.log(warningMessage); - } + alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn); } api.warn = warn; + // Add utility extend() method + var extend; if ({}.hasOwnProperty) { - api.util.extend = function(o, props) { + util.extend = extend = function(obj, props, deep) { + var o, p; for (var i in props) { if (props.hasOwnProperty(i)) { - o[i] = props[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 = []; - var moduleInitializers = []; + + function getErrorDesc(ex) { + return ex.message || ex.description || String(ex); + } // Initialization function init() { - if (api.initialized) { + if (!isBrowser || api.initialized) { return; } var testRange; @@ -137,10 +261,13 @@ window['rangy'] = (function() { if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { implementsDomRange = true; } - testRange.detach(); } - var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0]; + var body = getBody(document); + if (!body || body.nodeName.toLowerCase() != "body") { + fail("No body element found"); + return; + } if (body && isHostMethod(body, "createTextRange")) { testRange = body.createTextRange(); @@ -150,7 +277,8 @@ window['rangy'] = (function() { } if (!implementsDomRange && !implementsTextRange) { - fail("Neither Range nor TextRange are implemented"); + fail("Neither Range nor TextRange are available"); + return; } api.initialized = true; @@ -159,20 +287,43 @@ window['rangy'] = (function() { implementsTextRange: implementsTextRange }; - // Initialize modules and call init listeners - var allListeners = moduleInitializers.concat(initListeners); - for (var i = 0, len = allListeners.length; i < len; ++i) { - try { - allListeners[i](api); - } catch (ex) { - if (isHostObject(window, "console") && isHostMethod(window.console, "log")) { - window.console.log("Init listener threw an exception. Continuing.", ex); - } + // 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; @@ -185,1487 +336,1205 @@ window['rangy'] = (function() { } }; - var createMissingNativeApiListeners = []; + var shimListeners = []; - api.addCreateMissingNativeApiListener = function(listener) { - createMissingNativeApiListeners.push(listener); + api.addShimListener = function(listener) { + shimListeners.push(listener); }; - function createMissingNativeApi(win) { + function shim(win) { win = win || window; init(); // Notify listeners - for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) { - createMissingNativeApiListeners[i](win); + for (var i = 0, len = shimListeners.length; i < len; ++i) { + shimListeners[i](win); } } - api.createMissingNativeApi = createMissingNativeApi; + if (isBrowser) { + api.shim = api.createMissingNativeApi = shim; + createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim"); + } - /** - * @constructor - */ - function Module(name) { + function Module(name, dependencies, initializer) { this.name = name; + this.dependencies = dependencies; this.initialized = false; this.supported = false; + this.initializer = initializer; } - Module.prototype.fail = function(reason) { - this.initialized = true; - this.supported = false; + Module.prototype = { + init: function() { + var requiredModuleNames = this.dependencies || []; + for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) { + moduleName = requiredModuleNames[i]; - throw new Error("Module '" + this.name + "' failed to load: " + reason); + 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); + } }; - Module.prototype.warn = function(msg) { - api.warn("Module " + this.name + ": " + msg); - }; - - Module.prototype.createError = function(msg) { - return new Error("Error in Rangy " + this.name + " module: " + msg); - }; - - api.createModule = function(name, initFunc) { - var module = new Module(name); - api.modules[name] = module; - - moduleInitializers.push(function(api) { - initFunc(api, module); - module.initialized = true; - module.supported = true; + 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.requireModules = function(modules) { - for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) { - moduleName = modules[i]; - module = api.modules[moduleName]; - if (!module || !(module instanceof Module)) { - throw new Error("Module '" + moduleName + "' not found"); - } - if (!module.supported) { - throw new Error("Module '" + moduleName + "' not supported"); - } - } + api.createCoreModule = function(name, dependencies, initFunc) { + createModule(name, dependencies, initFunc); }; /*----------------------------------------------------------------------------------------------------------------*/ - // Wait for document to load before running tests + // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately - var docReady = false; + function RangePrototype() {} + api.RangePrototype = RangePrototype; + api.rangePrototype = new RangePrototype(); - var loadHandler = function(e) { - - if (!docReady) { - docReady = true; - if (!api.initialized) { - init(); - } - } - }; - - // Test whether we have window and document objects that we will need - if (typeof window == UNDEFINED) { - fail("No window found"); - return; - } - if (typeof document == UNDEFINED) { - fail("No document found"); - return; - } - - if (isHostMethod(document, "addEventListener")) { - document.addEventListener("DOMContentLoaded", loadHandler, false); - } - - // Add a fallback in case the DOMContentLoaded event isn't supported - if (isHostMethod(window, "addEventListener")) { - window.addEventListener("load", loadHandler, false); - } else if (isHostMethod(window, "attachEvent")) { - window.attachEvent("onload", loadHandler); - } else { - fail("Window does not have required addEventListener or attachEvent method"); - } - - return api; -})(); -rangy.createModule("DomUtil", function(api, module) { - - var UNDEF = "undefined"; - var util = api.util; - - // 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"); - } + function SelectionPrototype() {} + api.selectionPrototype = new SelectionPrototype(); /*----------------------------------------------------------------------------------------------------------------*/ - // 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; - }:*/ + // DOM utility methods used by Rangy + api.createCoreModule("DomUtil", [], function(api, module) { + var UNDEF = "undefined"; + var util = api.util; + var getBody = util.getBody; - function(arr, val) { - var i = arr.length; - while (i--) { - if (arr[i] === val) { + // 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; - }; - - // 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) { - var childNodes; - return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0); - } - - 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; - } + function isOrIsAncestorOf(ancestor, descendant) { + return isAncestorOf(ancestor, descendant, true); } - 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 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 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) { - var newNode = node.cloneNode(false); - newNode.deleteData(0, index); - node.deleteData(index, node.length - index); - insertAfter(newNode, node); - 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 new Error("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 new Error("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 new Error("getIframeWindow: 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 new Error("getIframeWindow: No Window object found for iframe element"); - } - } - - function getBody(doc) { - return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; - } - - 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 { - - // Case 4: containers are siblings or descendants of siblings - root = getCommonAncestor(nodeA, nodeB); - 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 new Error("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; - } - throw new Error("Should not be here!"); - } - } - } - - function fragmentFromNodeChildren(node) { - var fragment = getDocument(node).createDocumentFragment(), child; - while ( (child = node.firstChild) ) { - fragment.appendChild(child); - } - return fragment; - } - - function inspectNode(node) { - if (!node) { - return "[No node]"; - } - if (isCharacterDataNode(node)) { - return '"' + node.data + '"'; - } else if (node.nodeType == 1) { - var idAttr = node.id ? ' id="' + node.id + '"' : ""; - return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]"; - } else { - return node.nodeName; - } - } - - /** - * @constructor - */ - 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); - } - - /** - * @constructor - */ - function DomPosition(node, offset) { - this.node = node; - this.offset = offset; - } - - DomPosition.prototype = { - equals: function(pos) { - return this.node === pos.node & this.offset == pos.offset; - }, - - inspect: function() { - return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; - } - }; - - /** - * @constructor - */ - 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 - }; - - DOMException.prototype.toString = function() { - return this.message; - }; - - api.dom = { - arrayContains: arrayContains, - isHtmlNamespace: isHtmlNamespace, - parentElement: parentElement, - getNodeIndex: getNodeIndex, - getNodeLength: getNodeLength, - getCommonAncestor: getCommonAncestor, - isAncestorOf: isAncestorOf, - getClosestAncestorIn: getClosestAncestorIn, - isCharacterDataNode: isCharacterDataNode, - insertAfter: insertAfter, - splitDataNode: splitDataNode, - getDocument: getDocument, - getWindow: getWindow, - getIframeWindow: getIframeWindow, - getIframeDocument: getIframeDocument, - getBody: getBody, - getRootContainer: getRootContainer, - comparePoints: comparePoints, - inspectNode: inspectNode, - fragmentFromNodeChildren: fragmentFromNodeChildren, - createIterator: createIterator, - DomPosition: DomPosition - }; - - api.DOMException = DOMException; -});rangy.createModule("DomRange", function(api, module) { - api.requireModules( ["DomUtil"] ); - - - var dom = api.dom; - var DomPosition = dom.DomPosition; - var DOMException = api.DOMException; - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Utility functions - - function isNonTextPartiallySelected(node, range) { - return (node.nodeType != 3) && - (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true)); - } - - function getRangeDocument(range) { - return dom.getDocument(range.startContainer); - } - - function dispatchEvent(range, type, args) { - var listeners = range._listeners[type]; - if (listeners) { - for (var i = 0, len = listeners.length; i < len; ++i) { - listeners[i].call(range, {target: range, args: args}); - } - } - } - - function getBoundaryBeforeNode(node) { - return new DomPosition(node.parentNode, dom.getNodeIndex(node)); - } - - function getBoundaryAfterNode(node) { - return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1); - } - - function insertNodeAtPosition(node, n, o) { - var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; - if (dom.isCharacterDataNode(n)) { - if (o == n.length) { - dom.insertAfter(node, n); - } else { - n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o)); - } - } else if (o >= n.childNodes.length) { - n.appendChild(node); - } else { - n.insertBefore(node, n.childNodes[o]); - } - return firstNodeInserted; - } - - 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(true); - } - - 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(); ) { - //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node)); - if (rangeIterator.isPartiallySelectedSubtree()) { - // 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. - if (func(node) === false) { - iteratorState.stop = true; - return; - } else { - subRangeIterator = rangeIterator.getSubtreeIterator(); - iterateSubtree(subRangeIterator, func, iteratorState); - subRangeIterator.detach(true); - if (iteratorState.stop) { - return; - } - } - } else { - // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its - // descendant - 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(true); - } 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(true); - } else { - iterator.remove(); - } - if (node.nodeType == 10) { // DocumentType - throw new DOMException("HIERARCHY_REQUEST_ERR"); - } - frag.appendChild(node); - } - return frag; - } - - function getNodesInRange(range, nodeTypes, filter) { - //log.info("getNodesInRange, " + nodeTypes.join(",")); - 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)) && (!filterExists || filter(node))) { - 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) - - /** - * @constructor - */ - 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 && dom.isCharacterDataNode(this.sc)) { - this.isSingleCharacterDataNode = true; - this._first = this._last = this._next = this.sc; - } else { - this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ? - this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true); - this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ? - this.ec.childNodes[this.eo - 1] : dom.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 (dom.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 (dom.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) { - current.parentNode.removeChild(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(); - } else { - subRange = new Range(getRangeDocument(this.range)); - var current = this._current; - var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current); - - if (dom.isAncestorOf(current, this.sc, true)) { - startContainer = this.sc; - startOffset = this.so; - } - if (dom.isAncestorOf(current, this.ec, true)) { - endContainer = this.ec; - endOffset = this.eo; - } - - updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); - } - return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); - }, - - detach: function(detachRange) { - if (detachRange) { - this.range.detach(); - } - this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; - } - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Exceptions - - /** - * @constructor - */ - function RangeException(codeName) { - this.code = this[codeName]; - this.codeName = codeName; - this.message = "RangeException: " + this.codeName; - } - - RangeException.prototype = { - BAD_BOUNDARYPOINTS_ERR: 1, - INVALID_NODE_TYPE_ERR: 2 - }; - - RangeException.prototype.toString = function() { - return this.message; - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - /** - * Currently iterates through all nodes in the range on creation until I think of a decent way to do it - * TODO: Look into making this a proper iterator, not requiring preloading everything first - * @constructor - */ - function RangeNodeIterator(range, nodeTypes, filter) { - this.nodes = getNodesInRange(range, nodeTypes, filter); - this._next = this.nodes[0]; - this._position = 0; - } - - RangeNodeIterator.prototype = { - _current: null, - - hasNext: function() { - return !!this._next; - }, - - next: function() { - this._current = this._next; - this._next = this.nodes[ ++this._position ]; - return this._current; - }, - - detach: function() { - this._current = this._next = this.nodes = 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; + function getClosestAncestorIn(node, ancestor, selfIsAncestor) { + var p, n = selfIsAncestor ? node : node.parentNode; while (n) { - t = n.nodeType; - if (dom.arrayContains(nodeTypes, t)) { + p = n.parentNode; + if (p === ancestor) { return n; } - n = n.parentNode; + n = p; } return null; - }; - } - - var getRootContainer = dom.getRootContainer; - 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 RangeException("INVALID_NODE_TYPE_ERR"); } - } - function assertNotDetached(range) { - if (!range.startContainer) { - throw new DOMException("INVALID_STATE_ERR"); + function isCharacterDataNode(node) { + var t = node.nodeType; + return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment } - } - function assertValidNodeType(node, invalidTypes) { - if (!dom.arrayContains(invalidTypes, node.nodeType)) { - throw new RangeException("INVALID_NODE_TYPE_ERR"); - } - } - - function assertValidOffset(node, offset) { - if (offset < 0 || offset > (dom.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 isOrphan(node) { - return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true); - } - - function isValidOffset(node, offset) { - return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length); - } - - function isRangeValid(range) { - return (!!range.startContainer && !!range.endContainer - && !isOrphan(range.startContainer) - && !isOrphan(range.endContainer) - && isValidOffset(range.startContainer, range.startOffset) - && isValidOffset(range.endContainer, range.endOffset)); - } - - function assertRangeValid(range) { - assertNotDetached(range); - if (!isRangeValid(range)) { - throw new Error("Range error: Range is no longer valid after DOM mutation (" + 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 = "x"; - 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 = dom.getDocument(node); - - // "If the context object's start's node is null, raise an INVALID_STATE_ERR - // exception and abort these steps." + function isTextOrCommentNode(node) { 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 (dom.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(dom.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) { - assertNotDetached(this); - var doc = getRangeDocument(this); - var el = doc.createElement("body"); - el.innerHTML = fragmentStr; - - return dom.fragmentFromNodeChildren(el); - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - 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; - - function RangePrototype() {} - - RangePrototype.prototype = { - attachListener: function(type, listener) { - this._listeners[type].push(listener); - }, - - 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 dom.comparePoints(nodeA, offsetA, nodeB, offsetB); - }, - - insertNode: function(node) { - assertRangeValid(this); - assertValidNodeType(node, insertableNodeTypes); - assertNodeNotReadOnly(this.startContainer); - - if (dom.isAncestorOf(node, this.startContainer, true)) { - 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 && dom.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 RangeException("BAD_BOUNDARYPOINTS_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 && dom.isCharacterDataNode(sc)) { - return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; - } else { - var textBits = [], iterator = new RangeIterator(this, true); - - iterateSubtree(iterator, function(node) { - // Accept only text or CDATA nodes, not comments - - if (node.nodeType == 3 || node.nodeType == 4) { - textBits.push(node.data); - } - }); - iterator.detach(); - return textBits.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 = dom.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 (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { - return -1; - } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { - return 1; - } - return 0; - }, - - createContextualFragment: createContextualFragment, - - toHtml: function() { - assertRangeValid(this); - var container = getRangeDocument(this).createElement("div"); - container.appendChild(this.cloneContents()); - return container.innerHTML; - }, - - // 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); - assertNode(node, "NOT_FOUND_ERR"); - if (dom.getDocument(node) !== getRangeDocument(this)) { return false; } + var t = node.nodeType; + return t == 3 || t == 8 ; // Text or Comment + } - var parent = node.parentNode, offset = dom.getNodeIndex(node); - assertNode(parent, "NOT_FOUND_ERR"); + function insertAfter(node, precedingNode) { + var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; + if (nextNode) { + parent.insertBefore(node, nextNode); + } else { + parent.appendChild(node); + } + return node; + } - var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset), - endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset); + // 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); - return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; - }, + // 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"); + } + } - isPointInRange: function(node, offset) { - assertRangeValid(this); - assertNode(node, "HIERARCHY_REQUEST_ERR"); - assertSameDocumentOrFragment(node, this.startContainer); + 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"); + } + } - return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && - (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); - }, + 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"); + } + } - // The methods below are non-standard and invented by me. + 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"); + } + } - // Sharing a boundary start-to-end or end-to-start does not count as intersection. - intersectsRange: function(range, touchingIsIntersecting) { - assertRangeValid(this); + // This looks bad. Is it worth it? + function isWindow(obj) { + return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document"); + } - if (getRangeDocument(range) != getRangeDocument(this)) { + 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 = "
"; + 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 = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset), - endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset); + 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; - }, - - intersection: function(range) { - if (this.intersectsRange(range)) { - var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), - endComparison = dom.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.intersectsRange(range, true)) { - var unionRange = this.cloneRange(); - if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { - unionRange.setStart(range.startContainer, range.startOffset); - } - if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { - unionRange.setEnd(range.endContainer, range.endOffset); - } - return unionRange; - } else { - throw new RangeException("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, dom.getNodeLength(node)) <= 0; - }, - - containsRange: function(range) { - return this.intersection(range).equals(range); - }, - - 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); - var contains = this.containsRange(nodeRange); - nodeRange.detach(); - return contains; - } else { - return this.containsNodeContents(node); - } - }, - - createNodeIterator: function(nodeTypes, filter) { - assertRangeValid(this); - return new RangeNodeIterator(this, nodeTypes, filter); - }, - - getNodes: function(nodeTypes, filter) { - assertRangeValid(this); - return getNodesInRange(this, nodeTypes, filter); - }, - - getDocument: function() { - return getRangeDocument(this); - }, - - collapseBefore: function(node) { - assertNotDetached(this); - - this.setEndBefore(node); - this.collapse(false); - }, - - collapseAfter: function(node) { - assertNotDetached(this); - - this.setStartAfter(node); - this.collapse(true); - }, - - getName: function() { - return "DomRange"; - }, - - equals: function(range) { - return Range.rangesEqual(this, range); - }, - - isValid: function() { - return isRangeValid(this); - }, - - inspect: function() { - return inspect(this); } - }; - function copyComparisonConstantsToObject(obj) { - obj.START_TO_START = s2s; - obj.START_TO_END = s2e; - obj.END_TO_END = e2e; - obj.END_TO_START = e2s; + 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(); + } - obj.NODE_BEFORE = n_b; - obj.NODE_AFTER = n_a; - obj.NODE_BEFORE_AND_AFTER = n_b_a; - obj.NODE_INSIDE = n_i; - } + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } - function copyComparisonConstants(constructor) { - copyComparisonConstantsToObject(constructor); - copyComparisonConstantsToObject(constructor.prototype); - } + 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 createRangeContentRemover(remover, boundaryUpdater) { - return function() { - assertRangeValid(this); + function deleteSubtree(iterator) { + var subIterator; + while (iterator.next()) { + if (iterator.isPartiallySelectedSubtree()) { + subIterator = iterator.getSubtreeIterator(); + deleteSubtree(subIterator); + subIterator.detach(); + } else { + iterator.remove(); + } + } + } - var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; + function extractSubtree(iterator) { + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { - var iterator = new RangeIterator(this, true); + 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; + } - // Work out where to position the range after content removal - var node, boundary; - if (sc !== root) { - node = dom.getClosestAncestorIn(sc, root, true); - boundary = getBoundaryAfterNode(node); - sc = boundary.node; - so = boundary.offset; + function getNodesInRange(range, nodeTypes, filter) { + var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; + var filterExists = !!filter; + if (filterNodeTypes) { + regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); } - // Check none of the range is read-only - iterateSubtree(iterator, assertNodeNotReadOnly); + 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; + } - iterator.reset(); + var ec = range.endContainer; + if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) { + return; + } - // Remove the content - var returnValue = remover(iterator); - iterator.detach(); + nodes.push(node); + }); + return nodes; + } - // Move to the new position - boundaryUpdater(this, sc, so, sc, so); + 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 + ")]"; + } - return returnValue; + /*----------------------------------------------------------------------------------------------------------------*/ + + // 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; + } }; - } - function createPrototypeRange(constructor, boundaryUpdater, detacher) { - function createBeforeAfterNodeSetter(isBefore, isStart) { - return function(node) { - assertNotDetached(this); - assertValidNodeType(node, beforeAfterNodeTypes); - assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); + /*----------------------------------------------------------------------------------------------------------------*/ - var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); - (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); + 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; }; } - 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) || dom.comparePoints(node, offset, ec, eo) == 1) { - ec = node; - eo = offset; + 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 = "x"; + 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"); } - 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) || dom.comparePoints(node, offset, sc, so) == -1) { - sc = node; - so = offset; + // "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); } - boundaryUpdater(range, sc, so, node, offset); - } - } - function setRangeStartAndEnd(range, node, offset) { - if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) { - boundaryUpdater(range, node, offset, node, offset); - } - } + // "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) + )) { - constructor.prototype = new RangePrototype(); - - api.util.extend(constructor.prototype, { - setStart: function(node, offset) { - assertNotDetached(this); - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); - - setRangeStart(this, node, offset); - }, - - setEnd: function(node, offset) { - assertNotDetached(this); - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); - - setRangeEnd(this, 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); + // "let element be a new Element with "body" as its local name and the HTML + // namespace as its namespace."" + el = doc.createElement("body"); } else { - boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); + 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; } }, - selectNodeContents: function(node) { - // This doesn't seem well specified: the spec talks only about selecting the node's contents, which - // could be taken to mean only its children. However, browsers implement this the same as selectNode for - // text nodes, so I shall do likewise - assertNotDetached(this); - assertNoDocTypeNotationEntityAncestor(node, true); - - boundaryUpdater(this, node, 0, node, dom.getNodeLength(node)); - }, - - selectNode: function(node) { - assertNotDetached(this); - 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); @@ -1680,1545 +1549,2297 @@ rangy.createModule("DomUtil", function(api, module) { return !boundariesInvalid; }, - detach: function() { - detacher(this); - }, - - splitBoundaries: function() { - assertRangeValid(this); - - - var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; - var startEndSame = (sc === ec); - - if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { - dom.splitDataNode(ec, eo); + surroundContents: function(node) { + assertValidNodeType(node, surroundNodeTypes); + if (!this.canSurroundContents()) { + throw new DOMException("INVALID_STATE_ERR"); } - if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) { + // Extract the contents + var content = this.extractContents(); - sc = dom.splitDataNode(sc, so); - if (startEndSame) { - eo -= so; - ec = sc; - } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) { - eo++; + // Clear the children of the node + if (node.hasChildNodes()) { + while (node.lastChild) { + node.removeChild(node.lastChild); } - so = 0; - } - boundaryUpdater(this, sc, so, ec, eo); + + // Insert the new node and add the extracted contents + insertNodeAtPosition(node, this.startContainer, this.startOffset); + node.appendChild(content); + + this.selectNode(node); }, - normalizeBoundaries: function() { + 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; + }, - 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); - sibling.parentNode.removeChild(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); - sibling.parentNode.removeChild(sibling); - if (sc == ec) { - eo += so; - ec = sc; - } else if (ec == node.parentNode) { - var nodeIndex = dom.getNodeIndex(node); - if (eo == nodeIndex) { - ec = node; - eo = nodeLength; - } else if (eo > nodeIndex) { - eo--; - } - } - } - }; - - var normalizeStart = true; - - if (dom.isCharacterDataNode(ec)) { - if (ec.length == eo) { - mergeForward(ec); - } + 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 { - if (eo > 0) { - var endNode = ec.childNodes[eo - 1]; - if (endNode && dom.isCharacterDataNode(endNode)) { - mergeForward(endNode); + 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); } - } - normalizeStart = !this.collapsed; + }); + 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"); } - if (normalizeStart) { - if (dom.isCharacterDataNode(sc)) { - if (so == 0) { - mergeBackward(sc); + 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 { - if (so < sc.childNodes.length) { - var startNode = sc.childNodes[so]; - if (startNode && dom.isCharacterDataNode(startNode)) { - mergeBackward(startNode); - } + childNodes = node.childNodes; + i = childNodes.length; + while (i--) { + nodeStack.push(childNodes[i]); } } - } else { - sc = ec; - so = eo; } - - boundaryUpdater(this, sc, so, ec, eo); }, - collapseToPoint: function(node, offset) { - assertNotDetached(this); + getName: function() { + return "DomRange"; + }, - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); + equals: function(range) { + return Range.rangesEqual(this, range); + }, - setRangeStartAndEnd(this, node, offset); + isValid: function() { + return isRangeValid(this); + }, + + inspect: function() { + return inspect(this); + }, + + detach: function() { + // In DOM4, detach() is now a no-op. } }); - 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) { - var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset); - var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset); - - range.startContainer = startContainer; - range.startOffset = startOffset; - range.endContainer = endContainer; - range.endOffset = endOffset; - - updateCollapsedAndCommonAncestor(range); - dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved}); - } - - function detach(range) { - assertNotDetached(range); - range.startContainer = range.startOffset = range.endContainer = range.endOffset = null; - range.collapsed = range.commonAncestorContainer = null; - dispatchEvent(range, "detach", null); - range._listeners = null; - } - - /** - * @constructor - */ - function Range(doc) { - this.startContainer = doc; - this.startOffset = 0; - this.endContainer = doc; - this.endOffset = 0; - this._listeners = { - boundarychange: [], - detach: [] - }; - updateCollapsedAndCommonAncestor(this); - } - - createPrototypeRange(Range, updateBoundaries, detach); - - api.rangePrototype = RangePrototype.prototype; - - Range.rangeProperties = rangeProperties; - Range.RangeIterator = RangeIterator; - Range.copyComparisonConstants = copyComparisonConstants; - Range.createPrototypeRange = createPrototypeRange; - Range.inspect = inspect; - Range.getRangeDocument = getRangeDocument; - Range.rangesEqual = function(r1, r2) { - return r1.startContainer === r2.startContainer && - r1.startOffset === r2.startOffset && - r1.endContainer === r2.endContainer && - r1.endOffset === r2.endOffset; - }; - - api.DomRange = Range; - api.RangeException = RangeException; -});rangy.createModule("WrappedRange", function(api, module) { - api.requireModules( ["DomUtil", "DomRange"] ); - - /** - * @constructor - */ - var WrappedRange; - var dom = api.dom; - var DomPosition = dom.DomPosition; - var DomRange = api.DomRange; - - - - /*----------------------------------------------------------------------------------------------------------------*/ - - /* - 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): - - - - 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) - */ - function getTextRangeContainerElement(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); - } - - function textRangeIsCollapsed(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. - function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) { - 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 - // TODO: Find out when. Workaround for wholeRangeContainerElement may break this - if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) { - containerElement = wholeRangeContainerElement; + 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; } - - - // 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) { - return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); + function copyComparisonConstants(constructor) { + copyComparisonConstantsToObject(constructor); + copyComparisonConstantsToObject(constructor.prototype); } - var workingNode = dom.getDocument(containerElement).createElement("span"); - var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; - var previousNode, nextNode, boundaryPosition, boundaryNode; + function createRangeContentRemover(remover, boundaryUpdater) { + return function() { + assertRangeValid(this); - // Move the working range through the container's children, starting at the end and working backwards, until the - // working range reaches or goes past the boundary we're interested in - do { - containerElement.insertBefore(workingNode, workingNode.previousSibling); - workingRange.moveToElementText(workingNode); - } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 && - workingNode.previousSibling); + var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; - // 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; + var iterator = new RangeIterator(this, true); - if (comparison == -1 && boundaryNode && dom.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 line breaks (within a
 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 method 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);
+                // 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;
                 }
-            } else {
-                offset = workingRange.text.length;
-            }
-            boundaryPosition = new DomPosition(boundaryNode, offset);
-        } else {
 
+                // Check none of the range is read-only
+                iterateSubtree(iterator, assertNodeNotReadOnly);
 
-            // 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;
+                iterator.reset();
 
+                // Remove the content
+                var returnValue = remover(iterator);
+                iterator.detach();
 
+                // Move to the new position
+                boundaryUpdater(this, sc, so, sc, so);
 
-            if (nextNode && dom.isCharacterDataNode(nextNode)) {
-                boundaryPosition = new DomPosition(nextNode, 0);
-            } else if (previousNode && dom.isCharacterDataNode(previousNode)) {
-                boundaryPosition = new DomPosition(previousNode, previousNode.length);
-            } else {
-                boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
-            }
+                return returnValue;
+            };
         }
 
-        // Clean up
-        workingNode.parentNode.removeChild(workingNode);
+        function createPrototypeRange(constructor, boundaryUpdater) {
+            function createBeforeAfterNodeSetter(isBefore, isStart) {
+                return function(node) {
+                    assertValidNodeType(node, beforeAfterNodeTypes);
+                    assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
 
-        return boundaryPosition;
-    }
-
-    // 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/)
-    function createBoundaryTextRange(boundaryPosition, isStart) {
-        var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
-        var doc = dom.getDocument(boundaryPosition.node);
-        var workingNode, childNodes, workingRange = doc.body.createTextRange();
-        var nodeIsDataNode = dom.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, which is what we want
-        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;
-    }
-
-    /*----------------------------------------------------------------------------------------------------------------*/
-
-    if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {
-        // 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;
-            var canSetRangeStartAfterEnd;
-
-            function updateRangeProperties(range) {
-                var i = rangeProperties.length, prop;
-                while (i--) {
-                    prop = rangeProperties[i];
-                    range[prop] = range.nativeRange[prop];
-                }
-            }
-
-            function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {
-                var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
-                var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
-
-                // Always set both boundaries for the benefit of IE9 (see issue 35)
-                if (startMoved || endMoved) {
-                    range.setEnd(endContainer, endOffset);
-                    range.setStart(startContainer, startOffset);
-                }
-            }
-
-            function detach(range) {
-                range.nativeRange.detach();
-                range.detached = true;
-                var i = rangeProperties.length, prop;
-                while (i--) {
-                    prop = rangeProperties[i];
-                    range[prop] = null;
-                }
-            }
-
-            var createBeforeAfterNodeSetter;
-
-            WrappedRange = function(range) {
-                if (!range) {
-                    throw new Error("Range must be specified");
-                }
-                this.nativeRange = range;
-                updateRangeProperties(this);
-            };
-
-            DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);
-
-            rangeProto = WrappedRange.prototype;
-
-            rangeProto.selectNode = function(node) {
-                this.nativeRange.selectNode(node);
-                updateRangeProperties(this);
-            };
-
-            rangeProto.deleteContents = function() {
-                this.nativeRange.deleteContents();
-                updateRangeProperties(this);
-            };
-
-            rangeProto.extractContents = function() {
-                var frag = this.nativeRange.extractContents();
-                updateRangeProperties(this);
-                return frag;
-            };
-
-            rangeProto.cloneContents = function() {
-                return this.nativeRange.cloneContents();
-            };
-
-            // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still
-            // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for
-            // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of
-            // insertNode, which works but is almost certainly slower than the native implementation.
-/*
-            rangeProto.insertNode = function(node) {
-                this.nativeRange.insertNode(node);
-                updateRangeProperties(this);
-            };
-*/
-
-            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");
-            dom.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);
-                canSetRangeStartAfterEnd = true;
-
-                rangeProto.setStart = function(node, offset) {
-                    this.nativeRange.setStart(node, offset);
-                    updateRangeProperties(this);
+                    var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
+                    (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
                 };
+            }
 
-                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) {
-
-
-                canSetRangeStartAfterEnd = false;
-
-                rangeProto.setStart = function(node, offset) {
-                    try {
-                        this.nativeRange.setStart(node, offset);
-                    } catch (ex) {
-                        this.nativeRange.setEnd(node, offset);
-                        this.nativeRange.setStart(node, 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;
                     }
-                    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");
-
-            /*--------------------------------------------------------------------------------------------------------*/
-
-            // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to
-            // the 0th character of the text node
-            range.selectNodeContents(testTextNode);
-            if (range.startContainer == testTextNode && range.endContainer == testTextNode &&
-                    range.startOffset == 0 && range.endOffset == testTextNode.length) {
-                rangeProto.selectNodeContents = function(node) {
-                    this.nativeRange.selectNodeContents(node);
-                    updateRangeProperties(this);
-                };
-            } else {
-                rangeProto.selectNodeContents = function(node) {
-                    this.setStart(node, 0);
-                    this.setEnd(node, DomRange.getEndOffset(node));
-                };
-            }
-
-            /*--------------------------------------------------------------------------------------------------------*/
-
-            // Test for WebKit bug that has the beahviour 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 existence of createContextualFragment and delegate to it if it exists
-            if (api.util.isHostMethod(range, "createContextualFragment")) {
-                rangeProto.createContextualFragment = function(fragmentStr) {
-                    return this.nativeRange.createContextualFragment(fragmentStr);
-                };
-            }
-
-            /*--------------------------------------------------------------------------------------------------------*/
-
-            // Clean up
-            dom.getBody(document).removeChild(testTextNode);
-            range.detach();
-            range2.detach();
-        })();
-
-        api.createNativeRange = function(doc) {
-            doc = doc || document;
-            return doc.createRange();
-        };
-    } else if (api.features.implementsTextRange) {
-        // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
-        // prototype
-
-        WrappedRange = function(textRange) {
-            this.textRange = textRange;
-            this.refresh();
-        };
-
-        WrappedRange.prototype = new DomRange(document);
-
-        WrappedRange.prototype.refresh = function() {
-            var start, end;
-
-            // 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);
-            } else {
-
-                start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
-                end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);
-            }
-
-            this.setStart(start.node, start.offset);
-            this.setEnd(end.node, end.offset);
-        };
-
-        DomRange.copyComparisonConstants(WrappedRange);
-
-        // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work
-        var globalObj = (function() { return this; })();
-        if (typeof globalObj.Range == "undefined") {
-            globalObj.Range = WrappedRange;
-        }
-
-        api.createNativeRange = function(doc) {
-            doc = doc || document;
-            return doc.body.createTextRange();
-        };
-    }
-
-    if (api.features.implementsTextRange) {
-        WrappedRange.rangeToTextRange = function(range) {
-            if (range.collapsed) {
-                var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
-
-
-
-                return tr;
-
-                //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 = dom.getDocument(range.startContainer).body.createTextRange();
-                textRange.setEndPoint("StartToStart", startRange);
-                textRange.setEndPoint("EndToEnd", endRange);
-                return textRange;
-            }
-        };
-    }
-
-    WrappedRange.prototype.getName = function() {
-        return "WrappedRange";
-    };
-
-    api.WrappedRange = WrappedRange;
-
-    api.createRange = function(doc) {
-        doc = doc || document;
-        return new WrappedRange(api.createNativeRange(doc));
-    };
-
-    api.createRangyRange = function(doc) {
-        doc = doc || document;
-        return new DomRange(doc);
-    };
-
-    api.createIframeRange = function(iframeEl) {
-        return api.createRange(dom.getIframeDocument(iframeEl));
-    };
-
-    api.createIframeRangyRange = function(iframeEl) {
-        return api.createRangyRange(dom.getIframeDocument(iframeEl));
-    };
-
-    api.addCreateMissingNativeApiListener(function(win) {
-        var doc = win.document;
-        if (typeof doc.createRange == "undefined") {
-            doc.createRange = function() {
-                return api.createRange(this);
-            };
-        }
-        doc = win = null;
-    });
-});rangy.createModule("WrappedSelection", function(api, module) {
-    // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range
-    // spec (http://html5.org/specs/dom-range.html)
-
-    api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
-
-    api.config.checkSelectionRanges = true;
-
-    var BOOLEAN = "boolean",
-        windowPropertyName = "_rangySelection",
-        dom = api.dom,
-        util = api.util,
-        DomRange = api.DomRange,
-        WrappedRange = api.WrappedRange,
-        DOMException = api.DOMException,
-        DomPosition = dom.DomPosition,
-        getSelection,
-        selectionIsCollapsed,
-        CONTROL = "Control";
-
-
-
-    function getWinSelection(winParam) {
-        return (winParam || window).getSelection();
-    }
-
-    function getDocSelection(winParam) {
-        return (winParam || window).document.selection;
-    }
-
-    // Test for the Range/TextRange and Selection features required
-    // Test for ability to retrieve selection
-    var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),
-        implementsDocSelection = api.util.isHostObject(document, "selection");
-
-    var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
-
-    if (useDocumentSelection) {
-        getSelection = getDocSelection;
-        api.isSelectionValid = function(winParam) {
-            var doc = (winParam || window).document, nativeSel = doc.selection;
-
-            // Check whether the selection TextRange is actually contained within the correct document
-            return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);
-        };
-    } else if (implementsWinGetSelection) {
-        getSelection = getWinSelection;
-        api.isSelectionValid = function() {
-            return true;
-        };
-    } else {
-        module.fail("Neither document.selection or window.getSelection() detected.");
-    }
-
-    api.getNativeSelection = getSelection;
-
-    var testSelection = getSelection();
-    var testRange = api.createNativeRange(document);
-    var body = dom.getBody(document);
-
-    // Obtaining a range from a selection
-    var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&
-                                     util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));
-    api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
-
-    // Test for existence of native selection extend() method
-    var selectionHasExtend = util.isHostMethod(testSelection, "extend");
-    api.features.selectionHasExtend = selectionHasExtend;
-
-    // Test if rangeCount exists
-    var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
-    api.features.selectionHasRangeCount = selectionHasRangeCount;
-
-    var selectionSupportsMultipleRanges = false;
-    var collapsedNonEditableSelectionsSupported = true;
-
-    if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
-            typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {
-
-        (function() {
-            var iframe = document.createElement("iframe");
-            iframe.frameBorder = 0;
-            iframe.style.position = "absolute";
-            iframe.style.left = "-10000px";
-            body.appendChild(iframe);
-
-            var iframeDoc = dom.getIframeDocument(iframe);
-            iframeDoc.open();
-            iframeDoc.write("12");
-            iframeDoc.close();
-
-            var sel = dom.getIframeWindow(iframe).getSelection();
-            var docEl = iframeDoc.documentElement;
-            var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;
-
-            // Test whether the native selection will allow a collapsed selection within a non-editable element
-            var r1 = iframeDoc.createRange();
-            r1.setStart(textNode, 1);
-            r1.collapse(true);
-            sel.addRange(r1);
-            collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
-            sel.removeAllRanges();
-
-            // Test whether the native selection is capable of supporting multiple ranges
-            var r2 = r1.cloneRange();
-            r1.setStart(textNode, 0);
-            r2.setEnd(textNode, 2);
-            sel.addRange(r1);
-            sel.addRange(r2);
-
-            selectionSupportsMultipleRanges = (sel.rangeCount == 2);
-
-            // Clean up
-            r1.detach();
-            r2.detach();
-
-            body.removeChild(iframe);
-        })();
-    }
-
-    api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
-    api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
-
-    // ControlRanges
-    var implementsControlRange = false, testControlRange;
-
-    if (body && util.isHostMethod(body, "createControlRange")) {
-        testControlRange = body.createControlRange();
-        if (util.areHostProperties(testControlRange, ["item", "add"])) {
-            implementsControlRange = true;
-        }
-    }
-    api.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, backwards) {
-        var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "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 = range._selectionNativeRange;
-            if (!nativeRange) {
-                nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));
-                nativeRange.setEnd(range.endContainer, range.endOffset);
-                nativeRange.setStart(range.startContainer, range.startOffset);
-                range._selectionNativeRange = nativeRange;
-                range.attachListener("detach", function() {
-
-                    this._selectionNativeRange = null;
-                });
-            }
-        } else if (range instanceof WrappedRange) {
-            nativeRange = range.nativeRange;
-        } else if (api.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 new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
-        }
-        return nodes[0];
-    }
-
-    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 = dom.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);
+                    boundaryUpdater(range, node, offset, ec, eo);
                 }
-                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 = dom.getDocument(controlRange.item(0));
-        var newControlRange = dom.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 new Error("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 (util.isHostMethod(testSelection,  "getRangeAt")) {
-        getSelectionRangeAt = function(sel, index) {
-            try {
-                return sel.getRangeAt(index);
-            } catch(ex) {
-                return null;
-            }
-        };
-    } else if (selectionHasAnchorAndFocus) {
-        getSelectionRangeAt = function(sel) {
-            var doc = dom.getDocument(sel.anchorNode);
-            var range = api.createRange(doc);
-            range.setStart(sel.anchorNode, sel.anchorOffset);
-            range.setEnd(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.setStart(sel.focusNode, sel.focusOffset);
-                range.setEnd(sel.anchorNode, sel.anchorOffset);
             }
 
-            return range;
-        };
-    }
-
-    /**
-     * @constructor
-     */
-    function WrappedSelection(selection, docSelection, win) {
-        this.nativeSelection = selection;
-        this.docSelection = docSelection;
-        this._ranges = [];
-        this.win = win;
-        this.refresh();
-    }
-
-    api.getSelection = function(win) {
-        win = win || window;
-        var sel = win[windowPropertyName];
-        var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
-        if (sel) {
-            sel.nativeSelection = nativeSel;
-            sel.docSelection = docSel;
-            sel.refresh(win);
-        } else {
-            sel = new WrappedSelection(nativeSel, docSel, win);
-            win[windowPropertyName] = sel;
-        }
-        return sel;
-    };
-
-    api.getIframeSelection = function(iframeEl) {
-        return api.getSelection(dom.getIframeWindow(iframeEl));
-    };
-
-    var selProto = WrappedSelection.prototype;
-
-    function createControlSelection(sel, ranges) {
-        // Ensure that the selection becomes of type "Control"
-        var doc = dom.getDocument(ranges[0].startContainer);
-        var controlRange = dom.getBody(doc).createControlRange();
-        for (var i = 0, el; i < rangeCount; ++i) {
-            el = getSingleElementFromRange(ranges[i]);
-            try {
-                controlRange.add(el);
-            } catch (ex) {
-                throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");
+            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);
+                }
             }
-        }
-        controlRange.select();
 
-        // Update the wrapped selection based on what's now in the native selection
-        updateControlSelection(sel);
-    }
+            // Set up inheritance
+            var F = function() {};
+            F.prototype = api.rangePrototype;
+            constructor.prototype = new F();
 
-    // Selecting a range
-    if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
-        selProto.removeAllRanges = function() {
-            this.nativeSelection.removeAllRanges();
-            updateEmptySelection(this);
-        };
+            util.extend(constructor.prototype, {
+                setStart: function(node, offset) {
+                    assertNoDocTypeNotationEntityAncestor(node, true);
+                    assertValidOffset(node, offset);
 
-        var addRangeBackwards = function(sel, range) {
-            var doc = DomRange.getRangeDocument(range);
-            var endRange = api.createRange(doc);
-            endRange.collapseToPoint(range.endContainer, range.endOffset);
-            sel.nativeSelection.addRange(getNativeRange(endRange));
-            sel.nativeSelection.extend(range.startContainer, range.startOffset);
-            sel.refresh();
-        };
+                    setRangeStart(this, node, offset);
+                },
 
-        if (selectionHasRangeCount) {
-            selProto.addRange = function(range, backwards) {
-                if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
-                    addRangeToControlSelection(this, range);
-                } else {
-                    if (backwards && selectionHasExtend) {
-                        addRangeBackwards(this, range);
+                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 {
-                        var previousRangeCount;
-                        if (selectionSupportsMultipleRanges) {
-                            previousRangeCount = this.rangeCount;
-                        } else {
-                            this.removeAllRanges();
-                            previousRangeCount = 0;
+                        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);
                         }
-                        this.nativeSelection.addRange(getNativeRange(range));
+                    };
 
-                        // 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 && !DomRange.rangesEqual(nativeRange, range)) {
-                                    // Happens in WebKit with, for example, a selection placed at the start of a text node
-                                    range = new WrappedRange(nativeRange);
+                    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);
                                 }
                             }
-                            this._ranges[this.rangeCount - 1] = range;
-                            updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection));
-                            this.isCollapsed = selectionIsCollapsed(this);
                         } else {
-                            // The range was not added successfully. The simplest thing is to refresh
-                            this.refresh();
+                            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);
                     }
                 }
-            };
-        } else {
-            selProto.addRange = function(range, backwards) {
-                if (backwards && selectionHasExtend) {
-                    addRangeBackwards(this, range);
+
+                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 {
-                    this.nativeSelection.addRange(getNativeRange(range));
-                    this.refresh();
+                    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();
+                };
+            })();
         }
 
-        selProto.setRanges = function(ranges) {
-            if (implementsControlRange && 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 (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&
-               implementsControlRange && useDocumentSelection) {
+        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):
 
-        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 = dom.getDocument(this.anchorNode);
-                    } else if (this.docSelection.type == CONTROL) {
-                        var controlRange = this.docSelection.createRange();
-                        if (controlRange.length) {
-                            doc = dom.getDocument(controlRange.item(0)).body.createTextRange();
-                        }
-                    }
-                    if (doc) {
-                        var textRange = doc.body.createTextRange();
-                        textRange.select();
-                        this.docSelection.empty();
-                    }
-                }
-            } catch(ex) {}
-            updateEmptySelection(this);
-        };
+            var range = document.selection.createRange();
+            alert(range.parentElement().id); // Should alert "ul" but alerts "b"
 
-        selProto.addRange = function(range) {
-            if (this.docSelection.type == CONTROL) {
-                addRangeToControlSelection(this, range);
-            } else {
-                WrappedRange.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 {
-            return this._ranges[index];
-        }
-    };
-
-    var refreshSelection;
-
-    if (useDocumentSelection) {
-        refreshSelection = function(sel) {
-            var range;
-            if (api.isSelectionValid(sel.win)) {
-                range = sel.docSelection.createRange();
-            } else {
-                range = dom.getBody(sel.win.document).createTextRange();
+            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);
+            };
 
-            if (sel.docSelection.type == CONTROL) {
-                updateControlSelection(sel);
-            } else if (isTextRange(range)) {
-                updateFromTextRange(sel, range);
-            } else {
-                updateEmptySelection(sel);
-            }
-        };
-    } else if (util.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], selectionIsBackwards(sel.nativeSelection));
-                    sel.isCollapsed = selectionIsCollapsed(sel);
-                } else {
-                    updateEmptySelection(sel);
+            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;
                 }
-            }
-        };
-    } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.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;
-        refreshSelection(this);
-        if (checkForChanges) {
-            var i = oldRanges.length;
-            if (i != this._ranges.length) {
+
+                // 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
+                        
 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;
             }
-            while (i--) {
-                if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {
+            for (var i = 1, len = rangeNodes.length; i < len; ++i) {
+                if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
                     return false;
                 }
             }
             return true;
         }
-    };
 
-    // Removal of a single range
-    var removeRangeManually = function(sel, range) {
-        var ranges = sel.getAllRanges(), removed = false;
-        sel.removeAllRanges();
-        for (var i = 0, len = ranges.length; i < len; ++i) {
-            if (removed || range !== ranges[i]) {
-                sel.addRange(ranges[i]);
-            } else {
-                // According to the draft WHATWG Range spec, the same range may be added to the selection multiple
-                // times. removeRange should only remove the first instance, so the following ensures only the first
-                // instance is removed
-                removed = 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];
         }
-        if (!sel.rangeCount) {
-            updateEmptySelection(sel);
+
+        // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
+        function isTextRange(range) {
+            return !!range && typeof range.text != "undefined";
         }
-    };
 
-    if (implementsControlRange) {
-        selProto.removeRange = function(range) {
-            if (this.docSelection.type == CONTROL) {
-                var controlRange = this.docSelection.createRange();
-                var rangeElement = getSingleElementFromRange(range);
+        function updateFromTextRange(sel, range) {
+            // Create a Range from the selected TextRange
+            var wrappedRange = new WrappedRange(range);
+            sel._ranges = [wrappedRange];
 
-                // Create a new ControlRange containing all the elements in the selected ControlRange minus the
-                // element contained by the supplied range
-                var doc = dom.getDocument(controlRange.item(0));
-                var newControlRange = dom.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;
+            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);
                 }
-                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 backwards
-    var selectionIsBackwards;
-    if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {
-        selectionIsBackwards = function(sel) {
-            var backwards = false;
-            if (sel.anchorNode) {
-                backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
-            }
-            return backwards;
-        };
-
-        selProto.isBackwards = function() {
-            return selectionIsBackwards(this);
-        };
-    } else {
-        selectionIsBackwards = selProto.isBackwards = function() {
-            return false;
-        };
-    }
-
-    // Selection text
-    // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation
-    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.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {
-            throw new DOMException("WRONG_DOCUMENT_ERR");
-        }
-    }
-
-    // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used
-    selProto.collapse = function(node, offset) {
-        assertNodeInSameDocument(this, node);
-        var range = api.createRange(dom.getDocument(node));
-        range.collapseToPoint(node, offset);
-        this.removeAllRanges();
-        this.addRange(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 HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is
-    // never used by Rangy.
-    selProto.selectAllChildren = function(node) {
-        assertNodeInSameDocument(this, node);
-        var range = api.createRange(dom.getDocument(node));
-        range.selectNodeContents(node);
-        this.removeAllRanges();
-        this.addRange(range);
-    };
-
-    selProto.deleteFromDocument = function() {
-        // Sepcial behaviour required for 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);
-                element.parentNode.removeChild(element);
-            }
-            this.refresh();
-        } else if (this.rangeCount) {
-            var ranges = this.getAllRanges();
-            this.removeAllRanges();
-            for (var i = 0, len = ranges.length; i < len; ++i) {
-                ranges[i].deleteContents();
-            }
-            // The HTML5 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.getAllRanges = function() {
-        return this._ranges.slice(0);
-    };
-
-    selProto.setSingleRange = function(range) {
-        this.setRanges( [range] );
-    };
-
-    selProto.containsNode = function(node, allowPartial) {
-        for (var i = 0, len = this._ranges.length; i < len; ++i) {
-            if (this._ranges[i].containsNode(node, allowPartial)) {
-                return true;
             }
         }
-        return false;
-    };
 
-    selProto.toHtml = function() {
-        var html = "";
-        if (this.rangeCount) {
-            var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div");
-            for (var i = 0, len = this._ranges.length; i < len; ++i) {
-                container.appendChild(this._ranges[i].cloneContents());
+        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));
             }
-            html = container.innerHTML;
-        }
-        return html;
-    };
-
-    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));
+            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);
         }
-        return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
-                ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
 
-    }
+        var getSelectionRangeAt;
 
-    selProto.getName = function() {
-        return "WrappedSelection";
-    };
+        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);
 
-    selProto.inspect = function() {
-        return inspect(this);
-    };
+                // 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);
+                }
 
-    selProto.detach = function() {
-        this.win[windowPropertyName] = null;
-        this.win = this.anchorNode = this.focusNode = null;
-    };
-
-    WrappedSelection.inspect = inspect;
-
-    api.Selection = WrappedSelection;
-
-    api.selectionPrototype = selProto;
-
-    api.addCreateMissingNativeApiListener(function(win) {
-        if (typeof win.getSelection == "undefined") {
-            win.getSelection = function() {
-                return api.getSelection(this);
+                return range;
             };
         }
-        win = null;
+
+        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 loadHandler = function(e) {
+        if (!docReady) {
+            docReady = true;
+            if (!api.initialized && api.config.autoInitialize) {
+                init();
+            }
+        }
+    };
+
+    if (isBrowser) {
+        // Test whether the document has already been loaded and initialize immediately if so
+        if (document.readyState == "complete") {
+            loadHandler();
+        } else {
+            if (isHostMethod(document, "addEventListener")) {
+                document.addEventListener("DOMContentLoaded", loadHandler, false);
+            }
+
+            // Add a fallback in case the DOMContentLoaded event isn't supported
+            addListener(window, "load", loadHandler);
+        }
+    }
+
+    return api;
+}, this);
\ No newline at end of file