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