From dc02ddf4a95b0f252d51367c4b09ba16f0003500 Mon Sep 17 00:00:00 2001 From: Christopher Blum Date: Sun, 18 Dec 2011 19:54:30 +0100 Subject: [PATCH] Upgrade rangy-core lib --- lib/rangy/rangy-core.js | 1938 ++++++++++++++++++++++----------------- 1 file changed, 1094 insertions(+), 844 deletions(-) diff --git a/lib/rangy/rangy-core.js b/lib/rangy/rangy-core.js index 90847beb..d066ff37 100644 --- a/lib/rangy/rangy-core.js +++ b/lib/rangy/rangy-core.js @@ -4,10 +4,10 @@ * * Copyright 2011, Tim Down * Licensed under the MIT license. - * Version: 1.1 - * Build date: 12 March 2011 + * Version: 1.2.2 + * Build date: 13 November 2011 */ -var rangy = (function() { +window['rangy'] = (function() { var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; @@ -23,7 +23,7 @@ var rangy = (function() { // Subset of TextRange's full set of methods that we're interested in var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark", - "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint"]; + "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"]; /*----------------------------------------------------------------------------------------------------------------*/ @@ -60,7 +60,12 @@ var rangy = (function() { var areHostObjects = createMultiplePropertyTest(isHostObject); var areHostProperties = createMultiplePropertyTest(isHostProperty); + function isTextRange(range) { + return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); + } + var api = { + version: "1.2.2", initialized: false, supported: true, @@ -70,14 +75,16 @@ var rangy = (function() { isHostProperty: isHostProperty, areHostMethods: areHostMethods, areHostObjects: areHostObjects, - areHostProperties: areHostProperties + areHostProperties: areHostProperties, + isTextRange: isTextRange }, features: {}, modules: {}, config: { - alertOnWarn: false + alertOnWarn: false, + preferTextRange: false } }; @@ -89,8 +96,8 @@ var rangy = (function() { api.fail = fail; - function warn(reason) { - var warningMessage = "Rangy warning: " + reason; + function warn(msg) { + var warningMessage = "Rangy warning: " + msg; if (api.config.alertOnWarn) { window.alert(warningMessage); } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) { @@ -100,6 +107,21 @@ var rangy = (function() { api.warn = warn; + if ({}.hasOwnProperty) { + api.util.extend = function(o, props) { + for (var i in props) { + if (props.hasOwnProperty(i)) { + o[i] = props[i]; + } + } + }; + } else { + fail("hasOwnProperty not supported"); + } + + var initListeners = []; + var moduleInitializers = []; + // Initialization function init() { if (api.initialized) { @@ -122,7 +144,7 @@ var rangy = (function() { if (body && isHostMethod(body, "createTextRange")) { testRange = body.createTextRange(); - if (areHostMethods(testRange, textRangeMethods) && areHostProperties(testRange, textRangeProperties)) { + if (isTextRange(testRange)) { implementsTextRange = true; } } @@ -144,7 +166,7 @@ var rangy = (function() { allListeners[i](api); } catch (ex) { if (isHostObject(window, "console") && isHostMethod(window.console, "log")) { - console.log("Init listener threw an exception. Continuing.", ex); + window.console.log("Init listener threw an exception. Continuing.", ex); } } @@ -154,9 +176,6 @@ var rangy = (function() { // Allow external scripts to initialize this library in case it's loaded after the document has loaded api.init = init; - var initListeners = []; - var moduleInitializers = []; - // Execute listener immediately if already initialized api.addInitListener = function(listener) { if (api.initialized) { @@ -200,6 +219,10 @@ var rangy = (function() { throw new Error("Module '" + this.name + "' failed to load: " + reason); }; + Module.prototype.warn = function(msg) { + api.warn("Module " + this.name + ": " + msg); + }; + Module.prototype.createError = function(msg) { return new Error("Error in Rangy " + this.name + " module: " + msg); }; @@ -289,6 +312,11 @@ rangy.createModule("DomUtil", function(api, module) { 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"]) || @@ -298,9 +326,9 @@ rangy.createModule("DomUtil", function(api, module) { /*----------------------------------------------------------------------------------------------------------------*/ - // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. Haven't been - // able to replicate it outside of the test. The bug is that indexOf return -1 when called on an Array that contains - // just the document as a single element and the value searched for is the document. + // 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; @@ -316,6 +344,17 @@ rangy.createModule("DomUtil", function(api, module) { 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) ) { @@ -324,6 +363,11 @@ rangy.createModule("DomUtil", function(api, module) { return i; } + function getNodeLength(node) { + var childNodes; + return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0); + } + function getCommonAncestor(node1, node2) { var ancestors = [], n; for (n = node1; n; n = n.parentNode) { @@ -378,16 +422,12 @@ rangy.createModule("DomUtil", function(api, module) { return node; } + // Note that we cannot use splitText() because it is bugridden in IE 9. function splitDataNode(node, index) { - var newNode; - if (node.nodeType == 3) { - newNode = node.splitText(index); - } else { - newNode = node.cloneNode(); - newNode.deleteData(0, index); - node.deleteData(0, node.length - index); - insertAfter(newNode, node); - } + var newNode = node.cloneNode(false); + newNode.deleteData(0, index); + node.deleteData(index, node.length - index); + insertAfter(newNode, node); return newNode; } @@ -440,6 +480,14 @@ rangy.createModule("DomUtil", function(api, module) { return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; } + function getRootContainer(node) { + var parent; + while ( (parent = node.parentNode) ) { + node = parent; + } + return node; + } + function comparePoints(nodeA, offsetA, nodeB, offsetB) { // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing var nodeC, root, childA, childB, n; @@ -481,6 +529,14 @@ rangy.createModule("DomUtil", function(api, module) { } } + function fragmentFromNodeChildren(node) { + var fragment = getDocument(node).createDocumentFragment(), child; + while ( (child = node.firstChild) ) { + fragment.appendChild(child); + } + return fragment; + } + function inspectNode(node) { if (!node) { return "[No node]"; @@ -552,14 +608,7 @@ rangy.createModule("DomUtil", function(api, module) { inspect: function() { return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; - }/*, - - isStartOfElementContent: function() { - var isCharacterData = isCharacterDataNode(this.node); - var el = isCharacterData ? this.node.parentNode : this.node; - return (el && el.nodeType == 1 && (isCharacterData ? - if (isCharacterDataNode(this.node) && !this.node.previousSibling && this.node.parentNode) - }*/ + } }; /** @@ -587,7 +636,10 @@ rangy.createModule("DomUtil", function(api, module) { api.dom = { arrayContains: arrayContains, + isHtmlNamespace: isHtmlNamespace, + parentElement: parentElement, getNodeIndex: getNodeIndex, + getNodeLength: getNodeLength, getCommonAncestor: getCommonAncestor, isAncestorOf: isAncestorOf, getClosestAncestorIn: getClosestAncestorIn, @@ -599,8 +651,10 @@ rangy.createModule("DomUtil", function(api, module) { getIframeWindow: getIframeWindow, getIframeDocument: getIframeDocument, getBody: getBody, + getRootContainer: getRootContainer, comparePoints: comparePoints, inspectNode: inspectNode, + fragmentFromNodeChildren: fragmentFromNodeChildren, createIterator: createIterator, DomPosition: DomPosition }; @@ -613,159 +667,16 @@ rangy.createModule("DomUtil", function(api, module) { var dom = api.dom; var DomPosition = dom.DomPosition; var DOMException = api.DOMException; - - /*----------------------------------------------------------------------------------------------------------------*/ - - // RangeIterator code borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) - - /** - * @constructor - */ - function RangeIterator(range, clonePartiallySelectedTextNodes) { - this.range = range; - this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; - - - - if (!range.collapsed) { - this.sc = range.startContainer; - this.so = range.startOffset; - this.ec = range.endContainer; - this.eo = range.endOffset; - var root = range.commonAncestorContainer; - - if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) { - this.isSingleCharacterDataNode = true; - this._first = this._last = this._next = this.sc; - } else { - this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ? - this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true); - this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ? - this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true); - } - - } - } - - RangeIterator.prototype = { - _current: null, - _next: null, - _first: null, - _last: null, - isSingleCharacterDataNode: false, - - reset: function() { - this._current = null; - this._next = this._first; - }, - - hasNext: function() { - return !!this._next; - }, - - next: function() { - // Move to next node - var current = this._current = this._next; - if (current) { - this._next = (current !== this._last) ? current.nextSibling : null; - - // Check for partially selected text nodes - if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { - if (current === this.ec) { - - (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); - } - if (this._current === this.sc) { - - (current = current.cloneNode(true)).deleteData(0, this.so); - } - } - } - - return current; - }, - - remove: function() { - var current = this._current, start, end; - - if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { - start = (current === this.sc) ? this.so : 0; - end = (current === this.ec) ? this.eo : current.length; - if (start != end) { - current.deleteData(start, end - start); - } - } else { - if (current.parentNode) { - current.parentNode.removeChild(current); - } else { - - } - } - }, - - // Checks if the current node is partially selected - isPartiallySelectedSubtree: function() { - var current = this._current; - return isNonTextPartiallySelected(current, this.range); - }, - - getSubtreeIterator: function() { - var subRange; - if (this.isSingleCharacterDataNode) { - subRange = this.range.cloneRange(); - subRange.collapse(); - } else { - subRange = new Range(getRangeDocument(this.range)); - var current = this._current; - var startContainer = current, startOffset = 0, endContainer = current, endOffset = getEndOffset(current); - - if (dom.isAncestorOf(current, this.sc, true)) { - startContainer = this.sc; - startOffset = this.so; - } - if (dom.isAncestorOf(current, this.ec, true)) { - endContainer = this.ec; - endOffset = this.eo; - } - - updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); - } - return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); - }, - - detach: function(detachRange) { - if (detachRange) { - this.range.detach(); - } - this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; - } - }; - + /*----------------------------------------------------------------------------------------------------------------*/ - // Exceptions + // Utility functions - /** - * @constructor - */ - function RangeException(codeName) { - this.code = this[codeName]; - this.codeName = codeName; - this.message = "RangeException: " + this.codeName; + function isNonTextPartiallySelected(node, range) { + return (node.nodeType != 3) && + (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true)); } - RangeException.prototype = { - BAD_BOUNDARYPOINTS_ERR: 1, - INVALID_NODE_TYPE_ERR: 2 - }; - - RangeException.prototype.toString = function() { - return this.message; - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - function getRangeDocument(range) { return dom.getDocument(range.startContainer); } @@ -787,10 +698,6 @@ rangy.createModule("DomUtil", function(api, module) { return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1); } - function getEndOffset(node) { - return dom.isCharacterDataNode(node) ? node.length : (node.childNodes ? node.childNodes.length : 0); - } - function insertNodeAtPosition(node, n, o) { var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; if (dom.isCharacterDataNode(n)) { @@ -917,43 +824,189 @@ rangy.createModule("DomUtil", function(api, module) { dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; } + /*----------------------------------------------------------------------------------------------------------------*/ + + // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) + /** - * Currently iterates through all nodes in the range on creation until I think of a decent way to do it - * TODO: Look into making this a proper iterator, not requiring preloading everything first * @constructor */ - function RangeNodeIterator(range, nodeTypes, filter) { - this.nodes = getNodesInRange(range, nodeTypes, filter); - this._next = this.nodes[0]; - this._pointer = 0; + function RangeIterator(range, clonePartiallySelectedTextNodes) { + this.range = range; + this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; + + + + if (!range.collapsed) { + this.sc = range.startContainer; + this.so = range.startOffset; + this.ec = range.endContainer; + this.eo = range.endOffset; + var root = range.commonAncestorContainer; + + if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) { + this.isSingleCharacterDataNode = true; + this._first = this._last = this._next = this.sc; + } else { + this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ? + this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true); + this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ? + this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true); + } + + } } - RangeNodeIterator.prototype = { + 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() { - this._current = this._next; - this._next = this.nodes[ ++this._pointer ]; - return this._current; - }, + // Move to next node + var current = this._current = this._next; + if (current) { + this._next = (current !== this._last) ? current.nextSibling : null; - detach: function() { - this._current = this._next = this.nodes = null; - } - }; + // Check for partially selected text nodes + if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { + if (current === this.ec) { - function isNonTextPartiallySelected(node, range) { - return (node.nodeType != 3) && - (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true)); - } + (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); + } + if (this._current === this.sc) { - var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; - var rootContainerNodeTypes = [2, 9, 11]; - var readonlyNodeTypes = [5, 6, 10, 12]; + (current = current.cloneNode(true)).deleteData(0, this.so); + } + } + } + + return current; + }, + + remove: function() { + var current = this._current, start, end; + + if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { + start = (current === this.sc) ? this.so : 0; + end = (current === this.ec) ? this.eo : current.length; + if (start != end) { + current.deleteData(start, end - start); + } + } else { + if (current.parentNode) { + current.parentNode.removeChild(current); + } else { + + } + } + }, + + // Checks if the current node is partially selected + isPartiallySelectedSubtree: function() { + var current = this._current; + return isNonTextPartiallySelected(current, this.range); + }, + + getSubtreeIterator: function() { + var subRange; + if (this.isSingleCharacterDataNode) { + subRange = this.range.cloneRange(); + subRange.collapse(); + } else { + subRange = new Range(getRangeDocument(this.range)); + var current = this._current; + var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current); + + if (dom.isAncestorOf(current, this.sc, true)) { + startContainer = this.sc; + startOffset = this.so; + } + if (dom.isAncestorOf(current, this.ec, true)) { + endContainer = this.ec; + endOffset = this.eo; + } + + updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); + } + return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); + }, + + detach: function(detachRange) { + if (detachRange) { + this.range.detach(); + } + this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; + } + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Exceptions + + /** + * @constructor + */ + function RangeException(codeName) { + this.code = this[codeName]; + this.codeName = codeName; + this.message = "RangeException: " + this.codeName; + } + + RangeException.prototype = { + BAD_BOUNDARYPOINTS_ERR: 1, + INVALID_NODE_TYPE_ERR: 2 + }; + + RangeException.prototype.toString = function() { + return this.message; + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + /** + * Currently iterates through all nodes in the range on creation until I think of a decent way to do it + * TODO: Look into making this a proper iterator, not requiring preloading everything first + * @constructor + */ + function RangeNodeIterator(range, nodeTypes, filter) { + this.nodes = getNodesInRange(range, nodeTypes, filter); + this._next = this.nodes[0]; + this._position = 0; + } + + RangeNodeIterator.prototype = { + _current: null, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + this._current = this._next; + this._next = this.nodes[ ++this._position ]; + return this._current; + }, + + detach: function() { + this._current = this._next = this.nodes = null; + } + }; + + var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; + var rootContainerNodeTypes = [2, 9, 11]; + var readonlyNodeTypes = [5, 6, 10, 12]; var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; @@ -971,14 +1024,7 @@ rangy.createModule("DomUtil", function(api, module) { }; } - function getRootContainer(node) { - var parent; - while ( (parent = node.parentNode) ) { - node = parent; - } - return node; - } - + var getRootContainer = dom.getRootContainer; var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); @@ -1026,7 +1072,7 @@ rangy.createModule("DomUtil", function(api, module) { } function isOrphan(node) { - return !getDocumentOrFragmentContainer(node, true); + return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true); } function isValidOffset(node, offset) { @@ -1034,454 +1080,620 @@ rangy.createModule("DomUtil", function(api, module) { } function assertRangeValid(range) { + assertNotDetached(range); if (isOrphan(range.startContainer) || isOrphan(range.endContainer) || !isValidOffset(range.startContainer, range.startOffset) || !isValidOffset(range.endContainer, range.endOffset)) { - throw new Error("Range Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")"); + throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")"); } } /*----------------------------------------------------------------------------------------------------------------*/ + // Test the browser's innerHTML support to decide how to implement createContextualFragment + var styleEl = document.createElement("style"); + var htmlParsingConforms = false; + try { + styleEl.innerHTML = "x"; + htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node + } catch (e) { + // IE 6 and 7 throw + } + + api.features.htmlParsingConforms = htmlParsingConforms; + + var createContextualFragment = htmlParsingConforms ? + + // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See + // discussion and base code for this implementation at issue 67. + // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface + // Thanks to Aleks Williams. + function(fragmentStr) { + // "Let node the context object's start's node." + var node = this.startContainer; + var doc = dom.getDocument(node); + + // "If the context object's start's node is null, raise an INVALID_STATE_ERR + // exception and abort these steps." + if (!node) { + throw new DOMException("INVALID_STATE_ERR"); + } + + // "Let element be as follows, depending on node's interface:" + // Document, Document Fragment: null + var el = null; + + // "Element: node" + if (node.nodeType == 1) { + el = node; + + // "Text, Comment: node's parentElement" + } else if (dom.isCharacterDataNode(node)) { + el = dom.parentElement(node); + } + + // "If either element is null or element's ownerDocument is an HTML document + // and element's local name is "html" and element's namespace is the HTML + // namespace" + if (el === null || ( + el.nodeName == "HTML" + && dom.isHtmlNamespace(dom.getDocument(el).documentElement) + && dom.isHtmlNamespace(el) + )) { + + // "let element be a new Element with "body" as its local name and the HTML + // namespace as its namespace."" + el = doc.createElement("body"); + } else { + el = el.cloneNode(false); + } + + // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." + // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." + // "In either case, the algorithm must be invoked with fragment as the input + // and element as the context element." + el.innerHTML = fragmentStr; + + // "If this raises an exception, then abort these steps. Otherwise, let new + // children be the nodes returned." + + // "Let fragment be a new DocumentFragment." + // "Append all new children to fragment." + // "Return fragment." + return dom.fragmentFromNodeChildren(el); + } : + + // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that + // previous versions of Rangy used (with the exception of using a body element rather than a div) + function(fragmentStr) { + assertNotDetached(this); + var doc = getRangeDocument(this); + var el = doc.createElement("body"); + el.innerHTML = fragmentStr; + + return dom.fragmentFromNodeChildren(el); + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", "commonAncestorContainer"]; var s2s = 0, s2e = 1, e2e = 2, e2s = 3; var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; - function copyComparisonConstantsToObject(obj) { - obj.START_TO_START = s2s; - obj.START_TO_END = s2e; - obj.END_TO_END = e2e; - obj.END_TO_START = e2s; + function RangePrototype() {} - obj.NODE_BEFORE = n_b; - obj.NODE_AFTER = n_a; - obj.NODE_BEFORE_AND_AFTER = n_b_a; - obj.NODE_INSIDE = n_i; - } + RangePrototype.prototype = { + attachListener: function(type, listener) { + this._listeners[type].push(listener); + }, - function copyComparisonConstants(constructor) { - copyComparisonConstantsToObject(constructor); - copyComparisonConstantsToObject(constructor.prototype); - } + compareBoundaryPoints: function(how, range) { + assertRangeValid(this); + assertSameDocumentOrFragment(this.startContainer, range.startContainer); + + var nodeA, offsetA, nodeB, offsetB; + var prefixA = (how == e2s || how == s2s) ? "start" : "end"; + var prefixB = (how == s2e || how == s2s) ? "start" : "end"; + nodeA = this[prefixA + "Container"]; + offsetA = this[prefixA + "Offset"]; + nodeB = range[prefixB + "Container"]; + offsetB = range[prefixB + "Offset"]; + return dom.comparePoints(nodeA, offsetA, nodeB, offsetB); + }, - function createPrototypeRange(constructor, boundaryUpdater, detacher) { - function createBeforeAfterNodeSetter(isBefore, isStart) { - return function(node) { - assertNotDetached(this); - assertValidNodeType(node, beforeAfterNodeTypes); - assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); + insertNode: function(node) { + assertRangeValid(this); + assertValidNodeType(node, insertableNodeTypes); + assertNodeNotReadOnly(this.startContainer); - var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); - (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); - }; - } + if (dom.isAncestorOf(node, this.startContainer, true)) { + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } - function setRangeStart(range, node, offset) { - var ec = range.endContainer, eo = range.endOffset; - if (node !== range.startContainer || offset !== this.startOffset) { - // Check the root containers of the range and the new boundary, and also check whether the new boundary - // is after the current end. In either case, collapse the range to the new position - if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) { - ec = node; - eo = offset; + // No check for whether the container of the start of the Range is of a type that does not allow + // children of the type of node: the browser's DOM implementation should do this for us when we attempt + // to add the node + + var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); + this.setStartBefore(firstNodeInserted); + }, + + cloneContents: function() { + assertRangeValid(this); + + var clone, frag; + if (this.collapsed) { + return getRangeDocument(this).createDocumentFragment(); + } else { + if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) { + clone = this.startContainer.cloneNode(true); + clone.data = clone.data.slice(this.startOffset, this.endOffset); + frag = getRangeDocument(this).createDocumentFragment(); + frag.appendChild(clone); + return frag; + } else { + var iterator = new RangeIterator(this, true); + clone = cloneSubtree(iterator); + iterator.detach(); } - boundaryUpdater(range, node, offset, ec, eo); + return clone; } - } + }, - function setRangeEnd(range, node, offset) { - var sc = range.startContainer, so = range.startOffset; - if (node !== range.endContainer || offset !== this.endOffset) { - // Check the root containers of the range and the new boundary, and also check whether the new boundary - // is after the current end. In either case, collapse the range to the new position - if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) { - sc = node; - so = offset; + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); + + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects + // no non-text nodes. + var iterator = new RangeIterator(this, true); + var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); + iterator.detach(); + return !boundariesInvalid; + }, + + surroundContents: function(node) { + assertValidNodeType(node, surroundNodeTypes); + + if (!this.canSurroundContents()) { + throw new RangeException("BAD_BOUNDARYPOINTS_ERR"); + } + + // Extract the contents + var content = this.extractContents(); + + // Clear the children of the node + if (node.hasChildNodes()) { + while (node.lastChild) { + node.removeChild(node.lastChild); } - boundaryUpdater(range, sc, so, node, offset); } - } - function setRangeStartAndEnd(range, node, offset) { - if (node !== range.startContainer || offset !== this.startOffset || node !== range.endContainer || offset !== this.endOffset) { - boundaryUpdater(range, node, offset, node, offset); + // 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; + }, - function createRangeContentRemover(remover) { - return function() { - assertNotDetached(this); - assertRangeValid(this); + toString: function() { + assertRangeValid(this); + var sc = this.startContainer; + if (sc === this.endContainer && dom.isCharacterDataNode(sc)) { + return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; + } else { + var textBits = [], iterator = new RangeIterator(this, true); - var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; + iterateSubtree(iterator, function(node) { + // Accept only text or CDATA nodes, not comments - var iterator = new RangeIterator(this, true); + if (node.nodeType == 3 || node.nodeType == 4) { + textBits.push(node.data); + } + }); + iterator.detach(); + return textBits.join(""); + } + }, - // Work out where to position the range after content removal - var node, boundary; - if (sc !== root) { - node = dom.getClosestAncestorIn(sc, root, true); - boundary = getBoundaryAfterNode(node); - sc = boundary.node; - so = boundary.offset; - } + // The methods below are all non-standard. The following batch were introduced by Mozilla but have since + // been removed from Mozilla. - // Check none of the range is read-only - iterateSubtree(iterator, assertNodeNotReadOnly); + compareNode: function(node) { + assertRangeValid(this); - iterator.reset(); + var parent = node.parentNode; + var nodeIndex = dom.getNodeIndex(node); - // Remove the content - var returnValue = remover(iterator); - iterator.detach(); + if (!parent) { + throw new DOMException("NOT_FOUND_ERR"); + } - // Move to the new position - boundaryUpdater(this, sc, so, sc, so); + var startComparison = this.comparePoint(parent, nodeIndex), + endComparison = this.comparePoint(parent, nodeIndex + 1); - return returnValue; - }; - } + if (startComparison < 0) { // Node starts before + return (endComparison > 0) ? n_b_a : n_b; + } else { + return (endComparison > 0) ? n_a : n_i; + } + }, - constructor.prototype = { - attachListener: function(type, listener) { - this._listeners[type].push(listener); - }, + comparePoint: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); - setStart: function(node, offset) { - assertNotDetached(this); - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); + if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { + return -1; + } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { + return 1; + } + return 0; + }, - setRangeStart(this, node, offset); - }, + createContextualFragment: createContextualFragment, - setEnd: function(node, offset) { - assertNotDetached(this); - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); + toHtml: function() { + assertRangeValid(this); + var container = getRangeDocument(this).createElement("div"); + container.appendChild(this.cloneContents()); + return container.innerHTML; + }, - setRangeEnd(this, node, offset); - }, + // touchingIsIntersecting determines whether this method considers a node that borders a range intersects + // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) + intersectsNode: function(node, touchingIsIntersecting) { + assertRangeValid(this); + assertNode(node, "NOT_FOUND_ERR"); + if (dom.getDocument(node) !== getRangeDocument(this)) { + return false; + } - setStartBefore: createBeforeAfterNodeSetter(true, true), - setStartAfter: createBeforeAfterNodeSetter(false, true), - setEndBefore: createBeforeAfterNodeSetter(true, false), - setEndAfter: createBeforeAfterNodeSetter(false, false), + var parent = node.parentNode, offset = dom.getNodeIndex(node); + assertNode(parent, "NOT_FOUND_ERR"); - collapse: function(isStart) { - assertNotDetached(this); - 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); - } - }, + var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset), + endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset); - selectNodeContents: function(node) { - // This doesn't seem well specified: the spec talks only about selecting the node's contents, which - // could be taken to mean only its children. However, browsers implement this the same as selectNode for - // text nodes, so I shall do likewise - assertNotDetached(this); - assertNoDocTypeNotationEntityAncestor(node, true); + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + }, - boundaryUpdater(this, node, 0, node, getEndOffset(node)); - }, - selectNode: function(node) { - assertNotDetached(this); - assertNoDocTypeNotationEntityAncestor(node, false); - assertValidNodeType(node, beforeAfterNodeTypes); + isPointInRange: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); - var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); - boundaryUpdater(this, start.node, start.offset, end.node, end.offset); - }, + return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && + (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); + }, - compareBoundaryPoints: function(how, range) { - assertNotDetached(this); - assertRangeValid(this); - assertSameDocumentOrFragment(this.startContainer, range.startContainer); - - var nodeA, offsetA, nodeB, offsetB; - var prefixA = (how == e2s || how == s2s) ? "start" : "end"; - var prefixB = (how == s2e || how == s2s) ? "start" : "end"; - nodeA = this[prefixA + "Container"]; - offsetA = this[prefixA + "Offset"]; - nodeB = range[prefixB + "Container"]; - offsetB = range[prefixB + "Offset"]; - return dom.comparePoints(nodeA, offsetA, nodeB, offsetB); - }, + // The methods below are non-standard and invented by me. - insertNode: function(node) { - assertNotDetached(this); - assertRangeValid(this); - assertValidNodeType(node, insertableNodeTypes); - assertNodeNotReadOnly(this.startContainer); + // Sharing a boundary start-to-end or end-to-start does not count as intersection. + intersectsRange: function(range, touchingIsIntersecting) { + assertRangeValid(this); - if (dom.isAncestorOf(node, this.startContainer, true)) { - throw new DOMException("HIERARCHY_REQUEST_ERR"); - } + if (getRangeDocument(range) != getRangeDocument(this)) { + throw new DOMException("WRONG_DOCUMENT_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 startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset), + endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset); - var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); - this.setStartBefore(firstNodeInserted); - }, + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + }, - cloneContents: function() { - assertNotDetached(this); - assertRangeValid(this); + intersection: function(range) { + if (this.intersectsRange(range)) { + var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), + endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); - var clone, frag; - if (this.collapsed) { - return getRangeDocument(this).createDocumentFragment(); - } else { - if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) { - clone = this.startContainer.cloneNode(true); - clone.data = clone.data.slice(this.startOffset, this.endOffset); - frag = getRangeDocument(this).createDocumentFragment(); - frag.appendChild(clone); - return frag; - } else { - var iterator = new RangeIterator(this, true); - clone = cloneSubtree(iterator); - iterator.detach(); - } - return clone; + 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; + }, - extractContents: createRangeContentRemover(extractSubtree), + union: function(range) { + if (this.intersectsRange(range, true)) { + var unionRange = this.cloneRange(); + if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { + unionRange.setStart(range.startContainer, range.startOffset); + } + if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { + unionRange.setEnd(range.endContainer, range.endOffset); + } + return unionRange; + } else { + throw new RangeException("Ranges do not intersect"); + } + }, - deleteContents: createRangeContentRemover(deleteSubtree), + containsNode: function(node, allowPartial) { + if (allowPartial) { + return this.intersectsNode(node, false); + } else { + return this.compareNode(node) == n_i; + } + }, - canSurroundContents: function() { - assertNotDetached(this); - assertRangeValid(this); - assertNodeNotReadOnly(this.startContainer); - assertNodeNotReadOnly(this.endContainer); + containsNodeContents: function(node) { + return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0; + }, - // 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; - }, + containsRange: function(range) { + return this.intersection(range).equals(range); + }, - surroundContents: function(node) { - assertValidNodeType(node, surroundNodeTypes); + containsNodeText: function(node) { + var nodeRange = this.cloneRange(); + nodeRange.selectNode(node); + var textNodes = nodeRange.getNodes([3]); + if (textNodes.length > 0) { + nodeRange.setStart(textNodes[0], 0); + var lastTextNode = textNodes.pop(); + nodeRange.setEnd(lastTextNode, lastTextNode.length); + var contains = this.containsRange(nodeRange); + nodeRange.detach(); + return contains; + } else { + return this.containsNodeContents(node); + } + }, - if (!this.canSurroundContents()) { - throw new RangeException("BAD_BOUNDARYPOINTS_ERR"); - } + createNodeIterator: function(nodeTypes, filter) { + assertRangeValid(this); + return new RangeNodeIterator(this, nodeTypes, filter); + }, - // Extract the contents - var content = this.extractContents(); + getNodes: function(nodeTypes, filter) { + assertRangeValid(this); + return getNodesInRange(this, nodeTypes, filter); + }, - // Clear the children of the node - if (node.hasChildNodes()) { - while (node.lastChild) { - node.removeChild(node.lastChild); - } - } + getDocument: function() { + return getRangeDocument(this); + }, - // Insert the new node and add the extracted contents - insertNodeAtPosition(node, this.startContainer, this.startOffset); - node.appendChild(content); + collapseBefore: function(node) { + assertNotDetached(this); - this.selectNode(node); - }, + this.setEndBefore(node); + this.collapse(false); + }, - cloneRange: function() { - assertNotDetached(this); - assertRangeValid(this); - var range = new Range(getRangeDocument(this)); - var i = rangeProperties.length, prop; - while (i--) { - prop = rangeProperties[i]; - range[prop] = this[prop]; - } - return range; - }, + collapseAfter: function(node) { + assertNotDetached(this); - detach: function() { - detacher(this); - }, + this.setStartAfter(node); + this.collapse(true); + }, - toString: function() { - assertNotDetached(this); - assertRangeValid(this); - var sc = this.startContainer; - if (sc === this.endContainer && dom.isCharacterDataNode(sc)) { - return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; - } else { - var textBits = [], iterator = new RangeIterator(this, true); + getName: function() { + return "DomRange"; + }, - iterateSubtree(iterator, function(node) { - // Accept only text or CDATA nodes, not comments + equals: function(range) { + return Range.rangesEqual(this, range); + }, - if (node.nodeType == 3 || node.nodeType == 4) { - textBits.push(node.data); - } - }); - iterator.detach(); - return textBits.join(""); - } - }, + inspect: function() { + return inspect(this); + } + }; - // The methods below are all non-standard. The following batch were introduced by Mozilla but have since - // been removed from Mozilla. + function copyComparisonConstantsToObject(obj) { + obj.START_TO_START = s2s; + obj.START_TO_END = s2e; + obj.END_TO_END = e2e; + obj.END_TO_START = e2s; - compareNode: function(node) { + 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 = dom.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, detacher) { + function createBeforeAfterNodeSetter(isBefore, isStart) { + return function(node) { assertNotDetached(this); - assertRangeValid(this); + assertValidNodeType(node, beforeAfterNodeTypes); + assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); - var parent = node.parentNode; - var nodeIndex = dom.getNodeIndex(node); + var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); + (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); + }; + } - if (!parent) { - throw new DOMException("NOT_FOUND_ERR"); + function setRangeStart(range, node, offset) { + var ec = range.endContainer, eo = range.endOffset; + if (node !== range.startContainer || offset !== range.startOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) { + ec = node; + eo = offset; + } + boundaryUpdater(range, node, offset, ec, eo); + } + } + + function setRangeEnd(range, node, offset) { + var sc = range.startContainer, so = range.startOffset; + if (node !== range.endContainer || offset !== range.endOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) { + sc = node; + so = offset; } + boundaryUpdater(range, sc, so, node, offset); + } + } - var startComparison = this.comparePoint(parent, nodeIndex), - endComparison = this.comparePoint(parent, nodeIndex + 1); + function setRangeStartAndEnd(range, node, offset) { + if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) { + boundaryUpdater(range, node, offset, node, offset); + } + } - if (startComparison < 0) { // Node starts before - return (endComparison > 0) ? n_b_a : n_b; - } else { - return (endComparison > 0) ? n_a : n_i; - } - }, + constructor.prototype = new RangePrototype(); - comparePoint: function(node, offset) { + api.util.extend(constructor.prototype, { + setStart: function(node, offset) { assertNotDetached(this); - assertRangeValid(this); - assertNode(node, "HIERARCHY_REQUEST_ERR"); - assertSameDocumentOrFragment(node, this.startContainer); + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); - if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { - return -1; - } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { - return 1; - } - return 0; + setRangeStart(this, node, offset); }, - createContextualFragment: function(html) { + setEnd: function(node, offset) { assertNotDetached(this); - var doc = getRangeDocument(this); - var container = doc.createElement("div"); - - // The next line is obviously non-standard but will work in all recent browsers - container.innerHTML = html; - - var frag = doc.createDocumentFragment(), n; - - while ( (n = container.firstChild) ) { - frag.appendChild(n); - } + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); - return frag; + setRangeEnd(this, node, offset); }, - // This follows the Mozilla model whereby a node that borders a range is not considered to intersect with it - intersectsNode: function(node, touchingIsIntersecting) { - assertNotDetached(this); + setStartBefore: createBeforeAfterNodeSetter(true, true), + setStartAfter: createBeforeAfterNodeSetter(false, true), + setEndBefore: createBeforeAfterNodeSetter(true, false), + setEndAfter: createBeforeAfterNodeSetter(false, false), + + collapse: function(isStart) { assertRangeValid(this); - assertNode(node, "NOT_FOUND_ERR"); - if (dom.getDocument(node) !== getRangeDocument(this)) { - return false; + if (isStart) { + boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); + } else { + boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); } - - var parent = node.parentNode, offset = dom.getNodeIndex(node); - assertNode(parent, "NOT_FOUND_ERR"); - - var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset), - endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset); - - return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; }, - isPointInRange: function(node, offset) { + selectNodeContents: function(node) { + // This doesn't seem well specified: the spec talks only about selecting the node's contents, which + // could be taken to mean only its children. However, browsers implement this the same as selectNode for + // text nodes, so I shall do likewise assertNotDetached(this); - assertRangeValid(this); - assertNode(node, "HIERARCHY_REQUEST_ERR"); - assertSameDocumentOrFragment(node, this.startContainer); + assertNoDocTypeNotationEntityAncestor(node, true); - return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && - (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); + boundaryUpdater(this, node, 0, node, dom.getNodeLength(node)); }, - // 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) { + selectNode: function(node) { assertNotDetached(this); - assertRangeValid(this); - - if (getRangeDocument(range) != getRangeDocument(this)) { - throw new DOMException("WRONG_DOCUMENT_ERR"); - } + assertNoDocTypeNotationEntityAncestor(node, false); + assertValidNodeType(node, beforeAfterNodeTypes); - return dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset) < 0 && - dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset) > 0; + var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); + boundaryUpdater(this, start.node, start.offset, end.node, end.offset); }, - intersection: function(range) { - if (this.intersectsRange(range)) { - var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), - endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); - - var intersectionRange = this.cloneRange(); + extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), - if (startComparison == -1) { - intersectionRange.setStart(range.startContainer, range.startOffset); - } - if (endComparison == 1) { - intersectionRange.setEnd(range.endContainer, range.endOffset); - } - return intersectionRange; - } - return null; - }, + deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), - containsNode: function(node, allowPartial) { - if (allowPartial) { - return this.intersectsNode(node, false); - } else { - return this.compareNode(node) == n_i; - } + 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; }, - containsNodeContents: function(node) { - return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getEndOffset(node)) <= 0; + detach: function() { + detacher(this); }, splitBoundaries: function() { - assertNotDetached(this); assertRangeValid(this); var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; var startEndSame = (sc === ec); - if (dom.isCharacterDataNode(ec) && eo < ec.length) { + if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { dom.splitDataNode(ec, eo); } - if (dom.isCharacterDataNode(sc) && so > 0) { + if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) { sc = dom.splitDataNode(sc, so); if (startEndSame) { eo -= so; ec = sc; + } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) { + eo++; } so = 0; @@ -1490,7 +1702,6 @@ rangy.createModule("DomUtil", function(api, module) { }, normalizeBoundaries: function() { - assertNotDetached(this); assertRangeValid(this); var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; @@ -1509,12 +1720,21 @@ rangy.createModule("DomUtil", function(api, module) { var sibling = node.previousSibling; if (sibling && sibling.nodeType == node.nodeType) { sc = node; + var nodeLength = node.length; so = sibling.length; node.insertData(0, sibling.data); sibling.parentNode.removeChild(sibling); if (sc == ec) { eo += so; ec = sc; + } else if (ec == node.parentNode) { + var nodeIndex = dom.getNodeIndex(node); + if (eo == nodeIndex) { + ec = node; + eo = nodeLength; + } else if (eo > nodeIndex) { + eo--; + } } } }; @@ -1556,54 +1776,15 @@ rangy.createModule("DomUtil", function(api, module) { boundaryUpdater(this, sc, so, ec, eo); }, - createNodeIterator: function(nodeTypes, filter) { - assertNotDetached(this); - assertRangeValid(this); - return new RangeNodeIterator(this, nodeTypes, filter); - }, - - getNodes: function(nodeTypes, filter) { - assertNotDetached(this); - assertRangeValid(this); - return getNodesInRange(this, nodeTypes, filter); - }, - collapseToPoint: function(node, offset) { assertNotDetached(this); - assertRangeValid(this); assertNoDocTypeNotationEntityAncestor(node, true); assertValidOffset(node, offset); setRangeStartAndEnd(this, node, offset); - }, - - collapseBefore: function(node) { - assertNotDetached(this); - - this.setEndBefore(node); - this.collapse(false); - }, - - collapseAfter: function(node) { - assertNotDetached(this); - - this.setStartAfter(node); - this.collapse(true); - }, - - getName: function() { - return "DomRange"; - }, - - equals: function(range) { - return Range.rangesEqual(this, range); - }, - - inspect: function() { - return inspect(this); } - }; + }); copyComparisonConstants(constructor); } @@ -1655,11 +1836,7 @@ rangy.createModule("DomUtil", function(api, module) { createPrototypeRange(Range, updateBoundaries, detach); - Range.fromRange = function(r) { - var range = new Range(getRangeDocument(r)); - updateBoundaries(range, r.startContainer, r.startOffset, r.endContainer, r.endOffset); - return range; - }; + api.rangePrototype = RangePrototype.prototype; Range.rangeProperties = rangeProperties; Range.RangeIterator = RangeIterator; @@ -1673,7 +1850,6 @@ rangy.createModule("DomUtil", function(api, module) { r1.endContainer === r2.endContainer && r1.endOffset === r2.endOffset; }; - Range.getEndOffset = getEndOffset; api.DomRange = Range; api.RangeException = RangeException; @@ -1767,9 +1943,9 @@ rangy.createModule("DomUtil", function(api, module) { boundaryNode = workingNode.nextSibling; if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) { - // This must be a data node (text, comment, cdata) since we've overshot. 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 + // 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); @@ -1781,11 +1957,11 @@ rangy.createModule("DomUtil", function(api, module) { 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 range's text property - - The text property of the TextRange strips trailing line breaks + - 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 properties return the actual number of characters moved, which is not necessarily + 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). @@ -1793,16 +1969,16 @@ rangy.createModule("DomUtil", function(api, module) { 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 - one character at a time and incrementing a counter with the result of 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 unaffected by the location of the range within the - document). + Another approach that works is to use moveStart() to move the start boundary of the range up to the end + boundary one character at a time and incrementing a counter with the value returned by the moveStart() + call. However, the check for whether the start boundary has reached the end boundary is expensive, so + this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of + the range within the document). - The method below uses the fact that once each \r\n in the range's text property has been converted to a - single \r character (as it is in the text node), we know the offset is at least as long as the range - text's length, so the start of the range is moved that length initially and then a character at a time - to make up for any line breaks that the range text property has stripped. This seems to have good + The method below is a hybrid of the two methods above. It uses the fact that a string containing the + TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the + text of the TextRange, so the start of the range is moved that length initially and then a character at + a time to make up for any trailing line breaks not contained in the 'text' property. This has good performance in most situations compared to the previous two methods. */ var tempRange = workingRange.duplicate(); @@ -1851,9 +2027,6 @@ rangy.createModule("DomUtil", function(api, module) { var workingNode, childNodes, workingRange = doc.body.createTextRange(); var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node); - // There is a shortcut we can take that prevents the need to insert anything into the DOM if the boundary is at - // either end of the contents of an element, which is to use TextRange's moveToElementText method - if (nodeIsDataNode) { boundaryNode = boundaryPosition.node; boundaryParent = boundaryNode.parentNode; @@ -1866,8 +2039,8 @@ rangy.createModule("DomUtil", function(api, module) { // Position the range immediately before the node containing the boundary workingNode = doc.createElement("span"); - // Having a non-empty element persuades IE to consider the TextRange boundary to be within an element - // rather than immediately before or after it, which is what we want + // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the + // element rather than immediately before or after it, which is what we want workingNode.innerHTML = "&#feff;"; // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report @@ -1894,10 +2067,10 @@ rangy.createModule("DomUtil", function(api, module) { /*----------------------------------------------------------------------------------------------------------------*/ - if (api.features.implementsDomRange) { + if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) { // This is a wrapper around the browser's native DOM Range. It has two aims: // - Provide workarounds for specific browser bugs - // - provide convenient extensions, as found in Rangy's DomRange + // - provide convenient extensions, which are inherited from Rangy's DomRange (function() { var rangeProto; @@ -1916,11 +2089,9 @@ rangy.createModule("DomUtil", function(api, module) { var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset); var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset); - if (endMoved) { + // Always set both boundaries for the benefit of IE9 (see issue 35) + if (startMoved || endMoved) { range.setEnd(endContainer, endOffset); - } - - if (startMoved) { range.setStart(startContainer, startOffset); } } @@ -2008,36 +2179,6 @@ rangy.createModule("DomUtil", function(api, module) { dom.getBody(document).appendChild(testTextNode); var range = document.createRange(); - /*--------------------------------------------------------------------------------------------------------*/ - - // Test for Firefox bug (apparently long-standing, still present in 3.6.8) that throws "Index or size is - // negative or greater than the allowed amount" for insertNode in some circumstances, and correct for it - // by using DomRange's insertNode implementation - -/* - var span = dom.getBody(document).insertBefore(document.createElement("span"), testTextNode); - var spanText = span.appendChild(document.createTextNode("span")); - range.setEnd(testTextNode, 2); - range.setStart(spanText, 2); - var nodeToInsert = document.createElement("span"); - nodeToInsert.innerHTML = "OIDUIIU" - var sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - range = sel.getRangeAt(0); - //alert(range) - range.insertNode(nodeToInsert); - - nodeToInsert.parentNode.removeChild(nodeToInsert); - range.setEnd(testTextNode, 2); - range.setStart(spanText, 2); - nodeToInsert = document.createElement("span"); - nodeToInsert.innerHTML = "werw" - range.insertNode(nodeToInsert); - alert(range) -*/ - - /*--------------------------------------------------------------------------------------------------------*/ // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and @@ -2141,7 +2282,8 @@ rangy.createModule("DomUtil", function(api, module) { 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) { + 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 @@ -2162,12 +2304,25 @@ rangy.createModule("DomUtil", function(api, module) { /*--------------------------------------------------------------------------------------------------------*/ + // Test for existence of createContextualFragment and delegate to it if it exists + if (api.util.isHostMethod(range, "createContextualFragment")) { + rangeProto.createContextualFragment = function(fragmentStr) { + return this.nativeRange.createContextualFragment(fragmentStr); + }; + } + + /*--------------------------------------------------------------------------------------------------------*/ + // Clean up dom.getBody(document).removeChild(testTextNode); range.detach(); range2.detach(); })(); + api.createNativeRange = function(doc) { + doc = doc || document; + return doc.createRange(); + }; } else if (api.features.implementsTextRange) { // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a // prototype @@ -2183,7 +2338,6 @@ rangy.createModule("DomUtil", function(api, module) { var start, end; // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that. - // We do that here to avoid doing it twice unnecessarily. var rangeContainerElement = getTextRangeContainerElement(this.textRange); if (textRangeIsCollapsed(this.textRange)) { @@ -2198,26 +2352,39 @@ rangy.createModule("DomUtil", function(api, module) { this.setEnd(end.node, end.offset); }; + DomRange.copyComparisonConstants(WrappedRange); + + // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work + var globalObj = (function() { return this; })(); + if (typeof globalObj.Range == "undefined") { + globalObj.Range = WrappedRange; + } + + api.createNativeRange = function(doc) { + doc = doc || document; + return doc.body.createTextRange(); + }; + } + + if (api.features.implementsTextRange) { WrappedRange.rangeToTextRange = function(range) { if (range.collapsed) { - return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true, true); + var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); + + + + return tr; + + //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); } else { - var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true, false); - var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false, false); + var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); + var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false); var textRange = dom.getDocument(range.startContainer).body.createTextRange(); textRange.setEndPoint("StartToStart", startRange); textRange.setEndPoint("EndToEnd", endRange); return textRange; } }; - - DomRange.copyComparisonConstants(WrappedRange); - - // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work - var globalObj = (function() { return this; })(); - if (typeof globalObj.Range == "undefined") { - globalObj.Range = WrappedRange; - } } WrappedRange.prototype.getName = function() { @@ -2226,15 +2393,6 @@ rangy.createModule("DomUtil", function(api, module) { api.WrappedRange = WrappedRange; - api.createNativeRange = function(doc) { - doc = doc || document; - if (api.features.implementsDomRange) { - return doc.createRange(); - } else if (api.features.implementsTextRange) { - return doc.body.createTextRange(); - } - }; - api.createRange = function(doc) { doc = doc || document; return new WrappedRange(api.createNativeRange(doc)); @@ -2263,38 +2421,57 @@ rangy.createModule("DomUtil", function(api, module) { doc = win = null; }); });rangy.createModule("WrappedSelection", function(api, module) { - // This will create a selection object wrapper that follows the HTML5 draft spec selections section - // (http://dev.w3.org/html5/spec/editing.html#selection) and adds convenience extensions + // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range + // spec (http://html5.org/specs/dom-range.html) api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] ); api.config.checkSelectionRanges = true; - var BOOLEAN = "boolean", windowPropertyName = "_rangySelection"; - var dom = api.dom; - var util = api.util; - var DomRange = api.DomRange; - var WrappedRange = api.WrappedRange; - var DOMException = api.DOMException; - var DomPosition = dom.DomPosition; + var BOOLEAN = "boolean", + windowPropertyName = "_rangySelection", + dom = api.dom, + util = api.util, + DomRange = api.DomRange, + WrappedRange = api.WrappedRange, + DOMException = api.DOMException, + DomPosition = dom.DomPosition, + getSelection, + selectionIsCollapsed, + CONTROL = "Control"; - var getSelection, selectionIsCollapsed; + function getWinSelection(winParam) { + return (winParam || window).getSelection(); + } + function getDocSelection(winParam) { + return (winParam || window).document.selection; + } // Test for the Range/TextRange and Selection features required // Test for ability to retrieve selection - if (api.util.isHostMethod(window, "getSelection")) { - getSelection = function(winParam) { - return (winParam || window).getSelection(); + var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"), + implementsDocSelection = api.util.isHostObject(document, "selection"); + + var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange); + + if (useDocumentSelection) { + getSelection = getDocSelection; + api.isSelectionValid = function(winParam) { + var doc = (winParam || window).document, nativeSel = doc.selection; + + // Check whether the selection TextRange is actually contained within the correct document + return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc); }; - } else if (api.util.isHostObject(document, "selection")) { - getSelection = function(winParam) { - return ((winParam || window).document.selection); + } else if (implementsWinGetSelection) { + getSelection = getWinSelection; + api.isSelectionValid = function() { + return true; }; } else { - module.fail("No means of obtaining a selection object"); + module.fail("Neither document.selection or window.getSelection() detected."); } api.getNativeSelection = getSelection; @@ -2322,33 +2499,41 @@ rangy.createModule("DomUtil", function(api, module) { if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) && typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) { - // Test whether the native selection is capable of supporting multiple ranges (function() { - var textNode1 = body.appendChild(document.createTextNode("One")); - var textNode2 = body.appendChild(document.createTextNode("Two")); - var testRange2 = api.createNativeRange(document); - testRange2.selectNodeContents(textNode1); - var testRange3 = api.createNativeRange(document); - testRange3.selectNodeContents(textNode2); - testSelection.removeAllRanges(); - testSelection.addRange(testRange2); - testSelection.addRange(testRange3); - selectionSupportsMultipleRanges = (testSelection.rangeCount == 2); - testSelection.removeAllRanges(); - textNode1.parentNode.removeChild(textNode1); - textNode2.parentNode.removeChild(textNode2); + var iframe = document.createElement("iframe"); + body.appendChild(iframe); + + var iframeDoc = dom.getIframeDocument(iframe); + iframeDoc.open(); + iframeDoc.write("12"); + iframeDoc.close(); + + var sel = dom.getIframeWindow(iframe).getSelection(); + var docEl = iframeDoc.documentElement; + var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild; // Test whether the native selection will allow a collapsed selection within a non-editable element - var el = document.createElement("p"); - el.contentEditable = false; - var textNode3 = el.appendChild(document.createTextNode("test")); - body.appendChild(el); - var testRange4 = api.createRange(); - testRange4.collapseToPoint(textNode3, 1); - testSelection.addRange(testRange4.nativeRange); - collapsedNonEditableSelectionsSupported = (testSelection.rangeCount == 1); - testSelection.removeAllRanges(); - body.removeChild(el); + var r1 = iframeDoc.createRange(); + r1.setStart(textNode, 1); + r1.collapse(true); + sel.addRange(r1); + collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1); + sel.removeAllRanges(); + + // Test whether the native selection is capable of supporting multiple ranges + var r2 = r1.cloneRange(); + r1.setStart(textNode, 0); + r2.setEnd(textNode, 2); + sel.addRange(r1); + sel.addRange(r2); + + selectionSupportsMultipleRanges = (sel.rangeCount == 2); + + // Clean up + r1.detach(); + r2.detach(); + + body.removeChild(iframe); })(); } @@ -2356,7 +2541,6 @@ rangy.createModule("DomUtil", function(api, module) { api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported; // ControlRanges - var selectionHasType = util.isHostProperty(testSelection, "type"); var implementsControlRange = false, testControlRange; if (body && util.isHostMethod(body, "createControlRange")) { @@ -2418,7 +2602,7 @@ rangy.createModule("DomUtil", function(api, module) { } } else if (range instanceof WrappedRange) { nativeRange = range.nativeRange; - } else if (window.Range && (range instanceof Range)) { + } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) { nativeRange = range; } return nativeRange; @@ -2444,25 +2628,68 @@ rangy.createModule("DomUtil", function(api, module) { return nodes[0]; } - function updateFromControlRange(sel) { + 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.nativeSelection.type == "None") { + if (sel.docSelection.type == "None") { updateEmptySelection(sel); } else { - var controlRange = sel.nativeSelection.createRange(); - sel.rangeCount = controlRange.length; - var range, doc = dom.getDocument(controlRange.item(0)); - for (var i = 0; i < sel.rangeCount; ++i) { - range = api.createRange(doc); - range.selectNode(controlRange.item(i)); - sel._ranges.push(range); + var controlRange = sel.docSelection.createRange(); + if (isTextRange(controlRange)) { + // This case (where the selection type is "Control" and calling createRange() on the selection returns + // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected + // ControlRange have been removed from the ControlRange and removed from the document. + updateFromTextRange(sel, controlRange); + } else { + sel.rangeCount = controlRange.length; + var range, doc = dom.getDocument(controlRange.item(0)); + for (var i = 0; i < sel.rangeCount; ++i) { + range = api.createRange(doc); + range.selectNode(controlRange.item(i)); + sel._ranges.push(range); + } + sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed; + updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false); } - sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed; - updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false); } } + function addRangeToControlSelection(sel, range) { + var controlRange = sel.docSelection.createRange(); + var rangeElement = getSingleElementFromRange(range); + + // Create a new ControlRange containing all the elements in the selected ControlRange plus the element + // contained by the supplied range + var doc = dom.getDocument(controlRange.item(0)); + var newControlRange = dom.getBody(doc).createControlRange(); + for (var i = 0, len = controlRange.length; i < len; ++i) { + newControlRange.add(controlRange.item(i)); + } + try { + newControlRange.add(rangeElement); + } catch (ex) { + throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)"); + } + newControlRange.select(); + + // Update the wrapped selection based on what's now in the native selection + updateControlSelection(sel); + } + var getSelectionRangeAt; if (util.isHostMethod(testSelection, "getRangeAt")) { @@ -2494,20 +2721,24 @@ rangy.createModule("DomUtil", function(api, module) { /** * @constructor */ - function WrappedSelection(selection) { + function WrappedSelection(selection, docSelection, win) { this.nativeSelection = selection; + this.docSelection = docSelection; this._ranges = []; + this.win = win; this.refresh(); } api.getSelection = function(win) { win = win || window; var sel = win[windowPropertyName]; + var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null; if (sel) { - sel.nativeSelection = getSelection(win); - sel.refresh(); + sel.nativeSelection = nativeSel; + sel.docSelection = docSel; + sel.refresh(win); } else { - sel = new WrappedSelection(getSelection(win)); + sel = new WrappedSelection(nativeSel, docSel, win); win[windowPropertyName] = sel; } return sel; @@ -2519,8 +2750,26 @@ rangy.createModule("DomUtil", function(api, module) { var selProto = WrappedSelection.prototype; + function createControlSelection(sel, ranges) { + // Ensure that the selection becomes of type "Control" + var doc = dom.getDocument(ranges[0].startContainer); + var controlRange = dom.getBody(doc).createControlRange(); + for (var i = 0, el; i < rangeCount; ++i) { + el = getSingleElementFromRange(ranges[i]); + try { + controlRange.add(el); + } catch (ex) { + throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)"); + } + } + controlRange.select(); + + // Update the wrapped selection based on what's now in the native selection + updateControlSelection(sel); + } + // Selecting a range - if (selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) { + if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) { selProto.removeAllRanges = function() { this.nativeSelection.removeAllRanges(); updateEmptySelection(this); @@ -2537,39 +2786,43 @@ rangy.createModule("DomUtil", function(api, module) { if (selectionHasRangeCount) { selProto.addRange = function(range, backwards) { - if (backwards && selectionHasExtend) { - addRangeBackwards(this, range); + if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { + addRangeToControlSelection(this, range); } else { - var previousRangeCount; - if (selectionSupportsMultipleRanges) { - previousRangeCount = this.rangeCount; + if (backwards && selectionHasExtend) { + addRangeBackwards(this, range); } else { - this.removeAllRanges(); - previousRangeCount = 0; - } - this.nativeSelection.addRange(getNativeRange(range)); - - // Check whether adding the range was successful - this.rangeCount = this.nativeSelection.rangeCount; - - if (this.rangeCount == previousRangeCount + 1) { - // The range was added successfully - - // Check whether the range that we added to the selection is reflected in the last range extracted from - // the selection - if (api.config.checkSelectionRanges) { - var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1); - if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) { - // Happens in WebKit with, for example, a selection placed at the start of a text node - range = new WrappedRange(nativeRange); + var previousRangeCount; + if (selectionSupportsMultipleRanges) { + previousRangeCount = this.rangeCount; + } else { + this.removeAllRanges(); + previousRangeCount = 0; + } + this.nativeSelection.addRange(getNativeRange(range)); + + // Check whether adding the range was successful + this.rangeCount = this.nativeSelection.rangeCount; + + if (this.rangeCount == previousRangeCount + 1) { + // The range was added successfully + + // Check whether the range that we added to the selection is reflected in the last range extracted from + // the selection + if (api.config.checkSelectionRanges) { + var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1); + if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) { + // Happens in WebKit with, for example, a selection placed at the start of a text node + range = new WrappedRange(nativeRange); + } } + this._ranges[this.rangeCount - 1] = range; + updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection)); + this.isCollapsed = selectionIsCollapsed(this); + } else { + // The range was not added successfully. The simplest thing is to refresh + this.refresh(); } - this._ranges[this.rangeCount - 1] = range; - updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection)); - this.isCollapsed = selectionIsCollapsed(this); - } else { - // The range was not added successfully. The simplest thing is to refresh - this.refresh(); } } }; @@ -2585,28 +2838,32 @@ rangy.createModule("DomUtil", function(api, module) { } selProto.setRanges = function(ranges) { - this.removeAllRanges(); - for (var i = 0, len = ranges.length; i < len; ++i) { - this.addRange(ranges[i]); + if (implementsControlRange && ranges.length > 1) { + createControlSelection(this, ranges); + } else { + this.removeAllRanges(); + for (var i = 0, len = ranges.length; i < len; ++i) { + this.addRange(ranges[i]); + } } }; } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") && - selectionHasType && implementsControlRange) { + implementsControlRange && useDocumentSelection) { selProto.removeAllRanges = function() { // Added try/catch as fix for issue #21 try { - this.nativeSelection.empty(); + this.docSelection.empty(); - // Check for empty() not working (issue 24) - if (this.nativeSelection.type != "None") { + // Check for empty() not working (issue #24) + if (this.docSelection.type != "None") { // Work around failure to empty a control selection by instead selecting a TextRange and then // calling empty() var doc; if (this.anchorNode) { - doc = dom.getDocument(this.anchorNode) - } else if (this.nativeSelection.type == "Control") { - var controlRange = this.nativeSelection.createRange(); + doc = dom.getDocument(this.anchorNode); + } else if (this.docSelection.type == CONTROL) { + var controlRange = this.docSelection.createRange(); if (controlRange.length) { doc = dom.getDocument(controlRange.item(0)).body.createTextRange(); } @@ -2614,7 +2871,7 @@ rangy.createModule("DomUtil", function(api, module) { if (doc) { var textRange = doc.body.createTextRange(); textRange.select(); - this.nativeSelection.empty(); + this.docSelection.empty(); } } } catch(ex) {} @@ -2622,26 +2879,8 @@ rangy.createModule("DomUtil", function(api, module) { }; selProto.addRange = function(range) { - if (this.nativeSelection.type == "Control") { - var controlRange = this.nativeSelection.createRange(); - var rangeElement = getSingleElementFromRange(range); - - // Create a new ControlRange containing all the elements in the selected ControlRange plus the element - // contained by the supplied range - var doc = dom.getDocument(controlRange.item(0)); - var newControlRange = dom.getBody(doc).createControlRange(); - for (var i = 0, len = controlRange.length; i < len; ++i) { - newControlRange.add(controlRange.item(i)); - } - try { - newControlRange.add(rangeElement); - } catch (ex) { - throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)"); - } - newControlRange.select(); - - // Update the wrapped selection based on what's now in the native selection - updateFromControlRange(this); + if (this.docSelection.type == CONTROL) { + addRangeToControlSelection(this, range); } else { WrappedRange.rangeToTextRange(range).select(); this._ranges[0] = range; @@ -2655,21 +2894,7 @@ rangy.createModule("DomUtil", function(api, module) { this.removeAllRanges(); var rangeCount = ranges.length; if (rangeCount > 1) { - // Ensure that the selection becomes of type "Control" - var doc = dom.getDocument(ranges[0].startContainer); - var controlRange = dom.getBody(doc).createControlRange(); - for (var i = 0, el; i < rangeCount; ++i) { - el = getSingleElementFromRange(ranges[i]); - try { - controlRange.add(el); - } catch (ex) { - throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)"); - } - } - controlRange.select(); - - // Update the wrapped selection based on what's now in the native selection - updateFromControlRange(this); + createControlSelection(this, ranges); } else if (rangeCount) { this.addRange(ranges[0]); } @@ -2689,19 +2914,42 @@ rangy.createModule("DomUtil", function(api, module) { var refreshSelection; - if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") { + if (useDocumentSelection) { refreshSelection = function(sel) { - sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount; - if (sel.rangeCount) { - for (var i = 0, len = sel.rangeCount; i < len; ++i) { - sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i)); - } - updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection)); - sel.isCollapsed = selectionIsCollapsed(sel); + var range; + if (api.isSelectionValid(sel.win)) { + range = sel.docSelection.createRange(); + } else { + range = dom.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 (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") { + refreshSelection = function(sel) { + if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) { + updateControlSelection(sel); + } else { + sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount; + if (sel.rangeCount) { + for (var i = 0, len = sel.rangeCount; i < len; ++i) { + sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i)); + } + updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection)); + sel.isCollapsed = selectionIsCollapsed(sel); + } else { + updateEmptySelection(sel); + } + } + }; } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) { refreshSelection = function(sel) { var range, nativeSel = sel.nativeSelection; @@ -2715,25 +2963,6 @@ rangy.createModule("DomUtil", function(api, module) { updateEmptySelection(sel); } }; - } else if (util.isHostMethod(testSelection, "createRange") && api.features.implementsTextRange) { - refreshSelection = function(sel) { - var range = sel.nativeSelection.createRange(), wrappedRange; - - - if (sel.nativeSelection.type == "Control") { - updateFromControlRange(sel); - } else if (range && typeof range.text != "undefined") { - // Create a Range from the selected TextRange - wrappedRange = new WrappedRange(range); - sel._ranges = [wrappedRange]; - - updateAnchorAndFocusFromRange(sel, wrappedRange, false); - sel.rangeCount = 1; - sel.isCollapsed = wrappedRange.collapsed; - } else { - updateEmptySelection(sel); - } - }; } else { module.fail("No means of obtaining a Range or TextRange from the user's selection was found"); return false; @@ -2759,14 +2988,13 @@ rangy.createModule("DomUtil", function(api, module) { // Removal of a single range var removeRangeManually = function(sel, range) { var ranges = sel.getAllRanges(), removed = false; - //console.log("removeRangeManually with " + ranges.length + " ranges (rangeCount " + sel.rangeCount); sel.removeAllRanges(); for (var i = 0, len = ranges.length; i < len; ++i) { if (removed || range !== ranges[i]) { sel.addRange(ranges[i]); } else { - // According to the HTML 5 spec, the same range may be added to the selection multiple times. - // removeRange should only remove the first instance, so the following ensures only the first + // According to the draft WHATWG Range spec, the same range may be added to the selection multiple + // times. removeRange should only remove the first instance, so the following ensures only the first // instance is removed removed = true; } @@ -2774,13 +3002,12 @@ rangy.createModule("DomUtil", function(api, module) { if (!sel.rangeCount) { updateEmptySelection(sel); } - //console.log("removeRangeManually finished with rangeCount " + sel.rangeCount); }; - if (selectionHasType && implementsControlRange) { + if (implementsControlRange) { selProto.removeRange = function(range) { - if (this.nativeSelection.type == "Control") { - var controlRange = this.nativeSelection.createRange(); + 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 @@ -2799,7 +3026,7 @@ rangy.createModule("DomUtil", function(api, module) { newControlRange.select(); // Update the wrapped selection based on what's now in the native selection - updateFromControlRange(this); + updateControlSelection(this); } else { removeRangeManually(this, range); } @@ -2812,7 +3039,7 @@ rangy.createModule("DomUtil", function(api, module) { // Detecting if a selection is backwards var selectionIsBackwards; - if (selectionHasAnchorAndFocus && api.features.implementsDomRange) { + if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) { selectionIsBackwards = function(sel) { var backwards = false; if (sel.anchorNode) { @@ -2831,7 +3058,7 @@ rangy.createModule("DomUtil", function(api, module) { } // Selection text - // This is conformant to the HTML 5 draft spec but differs from WebKit and Mozilla's implementation + // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation selProto.toString = function() { var rangeTexts = []; @@ -2886,7 +3113,17 @@ rangy.createModule("DomUtil", function(api, module) { }; selProto.deleteFromDocument = function() { - if (this.rangeCount) { + // Sepcial behaviour required for Control selections + if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { + var controlRange = this.docSelection.createRange(); + var element; + while (controlRange.length) { + element = controlRange.item(0); + controlRange.remove(element); + element.parentNode.removeChild(element); + } + this.refresh(); + } else if (this.rangeCount) { var ranges = this.getAllRanges(); this.removeAllRanges(); for (var i = 0, len = ranges.length; i < len; ++i) { @@ -2916,6 +3153,18 @@ rangy.createModule("DomUtil", function(api, module) { return false; }; + selProto.toHtml = function() { + var html = ""; + if (this.rangeCount) { + var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div"); + for (var i = 0, len = this._ranges.length; i < len; ++i) { + container.appendChild(this._ranges[i].cloneContents()); + } + html = container.innerHTML; + } + return html; + }; + function inspect(sel) { var rangeInspects = []; var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset); @@ -2941,15 +3190,16 @@ rangy.createModule("DomUtil", function(api, module) { }; selProto.detach = function() { - if (this.anchorNode) { - dom.getWindow(this.anchorNode)[windowPropertyName] = null; - } + this.win[windowPropertyName] = null; + this.win = this.anchorNode = this.focusNode = null; }; WrappedSelection.inspect = inspect; api.Selection = WrappedSelection; + api.selectionPrototype = selProto; + api.addCreateMissingNativeApiListener(function(win) { if (typeof win.getSelection == "undefined") { win.getSelection = function() {