2 * Rangy, a cross-browser JavaScript range and selection library
\r
3 * https://github.com/timdown/rangy
\r
5 * Copyright 2015, Tim Down
\r
6 * Licensed under the MIT license.
\r
8 * Build date: 10 May 2015
\r
11 (function(factory, root) {
\r
12 // No AMD or CommonJS support so we place Rangy in (probably) the global variable
\r
13 root.rangy = factory();
\r
16 var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
\r
18 // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START
\r
19 // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.
\r
20 var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
\r
21 "commonAncestorContainer"];
\r
23 // Minimal set of methods required for DOM Level 2 Range compliance
\r
24 var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
\r
25 "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
\r
26 "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
\r
28 var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
\r
30 // Subset of TextRange's full set of methods that we're interested in
\r
31 var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
\r
32 "setEndPoint", "getBoundingClientRect"];
\r
34 /*----------------------------------------------------------------------------------------------------------------*/
\r
36 // Trio of functions taken from Peter Michaux's article:
\r
37 // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
\r
38 function isHostMethod(o, p) {
\r
39 var t = typeof o[p];
\r
40 return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
\r
43 function isHostObject(o, p) {
\r
44 return !!(typeof o[p] == OBJECT && o[p]);
\r
47 function isHostProperty(o, p) {
\r
48 return typeof o[p] != UNDEFINED;
\r
51 // Creates a convenience function to save verbose repeated calls to tests functions
\r
52 function createMultiplePropertyTest(testFunc) {
\r
53 return function(o, props) {
\r
54 var i = props.length;
\r
56 if (!testFunc(o, props[i])) {
\r
64 // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
\r
65 var areHostMethods = createMultiplePropertyTest(isHostMethod);
\r
66 var areHostObjects = createMultiplePropertyTest(isHostObject);
\r
67 var areHostProperties = createMultiplePropertyTest(isHostProperty);
\r
69 function isTextRange(range) {
\r
70 return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
\r
73 function getBody(doc) {
\r
74 return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
\r
77 var forEach = [].forEach ?
\r
78 function(arr, func) {
\r
81 function(arr, func) {
\r
82 for (var i = 0, len = arr.length; i < len; ++i) {
\r
89 var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);
\r
92 isHostMethod: isHostMethod,
\r
93 isHostObject: isHostObject,
\r
94 isHostProperty: isHostProperty,
\r
95 areHostMethods: areHostMethods,
\r
96 areHostObjects: areHostObjects,
\r
97 areHostProperties: areHostProperties,
\r
98 isTextRange: isTextRange,
\r
105 initialized: false,
\r
106 isBrowser: isBrowser,
\r
112 alertOnFail: false,
\r
113 alertOnWarn: false,
\r
114 preferTextRange: false,
\r
115 autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
\r
119 function consoleLog(msg) {
\r
120 if (typeof console != UNDEFINED && isHostMethod(console, "log")) {
\r
125 function alertOrLog(msg, shouldAlert) {
\r
126 if (isBrowser && shouldAlert) {
\r
133 function fail(reason) {
\r
134 api.initialized = true;
\r
135 api.supported = false;
\r
136 alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);
\r
141 function warn(msg) {
\r
142 alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
\r
147 // Add utility extend() method
\r
149 if ({}.hasOwnProperty) {
\r
150 util.extend = extend = function(obj, props, deep) {
\r
152 for (var i in props) {
\r
153 if (props.hasOwnProperty(i)) {
\r
156 if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
\r
157 extend(o, p, true);
\r
162 // Special case for toString, which does not show up in for...in loops in IE <= 8
\r
163 if (props.hasOwnProperty("toString")) {
\r
164 obj.toString = props.toString;
\r
169 util.createOptions = function(optionsParam, defaults) {
\r
171 extend(options, defaults);
\r
172 if (optionsParam) {
\r
173 extend(options, optionsParam);
\r
178 fail("hasOwnProperty not supported");
\r
181 // Test whether we're in a browser and bail out if not
\r
183 fail("Rangy can only run in a browser");
\r
186 // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not
\r
191 var el = document.createElement("div");
\r
192 el.appendChild(document.createElement("span"));
\r
193 var slice = [].slice;
\r
195 if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
\r
196 toArray = function(arrayLike) {
\r
197 return slice.call(arrayLike, 0);
\r
204 toArray = function(arrayLike) {
\r
206 for (var i = 0, len = arrayLike.length; i < len; ++i) {
\r
207 arr[i] = arrayLike[i];
\r
213 util.toArray = toArray;
\r
216 // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or
\r
217 // normalization of event properties
\r
220 if (isHostMethod(document, "addEventListener")) {
\r
221 addListener = function(obj, eventType, listener) {
\r
222 obj.addEventListener(eventType, listener, false);
\r
224 } else if (isHostMethod(document, "attachEvent")) {
\r
225 addListener = function(obj, eventType, listener) {
\r
226 obj.attachEvent("on" + eventType, listener);
\r
229 fail("Document does not have required addEventListener or attachEvent method");
\r
232 util.addListener = addListener;
\r
235 var initListeners = [];
\r
237 function getErrorDesc(ex) {
\r
238 return ex.message || ex.description || String(ex);
\r
243 if (!isBrowser || api.initialized) {
\r
247 var implementsDomRange = false, implementsTextRange = false;
\r
249 // First, perform basic feature tests
\r
251 if (isHostMethod(document, "createRange")) {
\r
252 testRange = document.createRange();
\r
253 if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
\r
254 implementsDomRange = true;
\r
258 var body = getBody(document);
\r
259 if (!body || body.nodeName.toLowerCase() != "body") {
\r
260 fail("No body element found");
\r
264 if (body && isHostMethod(body, "createTextRange")) {
\r
265 testRange = body.createTextRange();
\r
266 if (isTextRange(testRange)) {
\r
267 implementsTextRange = true;
\r
271 if (!implementsDomRange && !implementsTextRange) {
\r
272 fail("Neither Range nor TextRange are available");
\r
276 api.initialized = true;
\r
278 implementsDomRange: implementsDomRange,
\r
279 implementsTextRange: implementsTextRange
\r
282 // Initialize modules
\r
283 var module, errorMessage;
\r
284 for (var moduleName in modules) {
\r
285 if ( (module = modules[moduleName]) instanceof Module ) {
\r
286 module.init(module, api);
\r
290 // Call init listeners
\r
291 for (var i = 0, len = initListeners.length; i < len; ++i) {
\r
293 initListeners[i](api);
\r
295 errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
\r
296 consoleLog(errorMessage);
\r
301 function deprecationNotice(deprecated, replacement, module) {
\r
303 deprecated += " in module " + module.name;
\r
305 api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +
\r
306 replacement + " instead.");
\r
309 function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {
\r
310 owner[deprecated] = function() {
\r
311 deprecationNotice(deprecated, replacement, module);
\r
312 return owner[replacement].apply(owner, util.toArray(arguments));
\r
316 util.deprecationNotice = deprecationNotice;
\r
317 util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;
\r
319 // Allow external scripts to initialize this library in case it's loaded after the document has loaded
\r
322 // Execute listener immediately if already initialized
\r
323 api.addInitListener = function(listener) {
\r
324 if (api.initialized) {
\r
327 initListeners.push(listener);
\r
331 var shimListeners = [];
\r
333 api.addShimListener = function(listener) {
\r
334 shimListeners.push(listener);
\r
337 function shim(win) {
\r
338 win = win || window;
\r
341 // Notify listeners
\r
342 for (var i = 0, len = shimListeners.length; i < len; ++i) {
\r
343 shimListeners[i](win);
\r
348 api.shim = api.createMissingNativeApi = shim;
\r
349 createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");
\r
352 function Module(name, dependencies, initializer) {
\r
354 this.dependencies = dependencies;
\r
355 this.initialized = false;
\r
356 this.supported = false;
\r
357 this.initializer = initializer;
\r
360 Module.prototype = {
\r
362 var requiredModuleNames = this.dependencies || [];
\r
363 for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
\r
364 moduleName = requiredModuleNames[i];
\r
366 requiredModule = modules[moduleName];
\r
367 if (!requiredModule || !(requiredModule instanceof Module)) {
\r
368 throw new Error("required module '" + moduleName + "' not found");
\r
371 requiredModule.init();
\r
373 if (!requiredModule.supported) {
\r
374 throw new Error("required module '" + moduleName + "' not supported");
\r
378 // Now run initializer
\r
379 this.initializer(this);
\r
382 fail: function(reason) {
\r
383 this.initialized = true;
\r
384 this.supported = false;
\r
385 throw new Error(reason);
\r
388 warn: function(msg) {
\r
389 api.warn("Module " + this.name + ": " + msg);
\r
392 deprecationNotice: function(deprecated, replacement) {
\r
393 api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +
\r
394 replacement + " instead");
\r
397 createError: function(msg) {
\r
398 return new Error("Error in Rangy " + this.name + " module: " + msg);
\r
402 function createModule(name, dependencies, initFunc) {
\r
403 var newModule = new Module(name, dependencies, function(module) {
\r
404 if (!module.initialized) {
\r
405 module.initialized = true;
\r
407 initFunc(api, module);
\r
408 module.supported = true;
\r
410 var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
\r
411 consoleLog(errorMessage);
\r
413 consoleLog(ex.stack);
\r
418 modules[name] = newModule;
\r
422 api.createModule = function(name) {
\r
423 // Allow 2 or 3 arguments (second argument is an optional array of dependencies)
\r
424 var initFunc, dependencies;
\r
425 if (arguments.length == 2) {
\r
426 initFunc = arguments[1];
\r
429 initFunc = arguments[2];
\r
430 dependencies = arguments[1];
\r
433 var module = createModule(name, dependencies, initFunc);
\r
435 // Initialize the module immediately if the core is already initialized
\r
436 if (api.initialized && api.supported) {
\r
441 api.createCoreModule = function(name, dependencies, initFunc) {
\r
442 createModule(name, dependencies, initFunc);
\r
445 /*----------------------------------------------------------------------------------------------------------------*/
\r
447 // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately
\r
449 function RangePrototype() {}
\r
450 api.RangePrototype = RangePrototype;
\r
451 api.rangePrototype = new RangePrototype();
\r
453 function SelectionPrototype() {}
\r
454 api.selectionPrototype = new SelectionPrototype();
\r
456 /*----------------------------------------------------------------------------------------------------------------*/
\r
458 // DOM utility methods used by Rangy
459 api.createCoreModule("DomUtil", [], function(api, module) {
460 var UNDEF = "undefined";
462 var getBody = util.getBody;
464 // Perform feature tests
465 if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
466 module.fail("document missing a Node creation method");
469 if (!util.isHostMethod(document, "getElementsByTagName")) {
470 module.fail("document missing getElementsByTagName method");
473 var el = document.createElement("div");
474 if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
475 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
476 module.fail("Incomplete Element implementation");
479 // innerHTML is required for Range's createContextualFragment method
480 if (!util.isHostProperty(el, "innerHTML")) {
481 module.fail("Element is missing innerHTML property");
484 var textNode = document.createTextNode("test");
485 if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
486 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
487 !util.areHostProperties(textNode, ["data"]))) {
488 module.fail("Incomplete Text Node implementation");
491 /*----------------------------------------------------------------------------------------------------------------*/
493 // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
494 // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
495 // contains just the document as a single element and the value searched for is the document.
496 var arrayContains = /*Array.prototype.indexOf ?
498 return arr.indexOf(val) > -1;
504 if (arr[i] === val) {
511 // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
512 function isHtmlNamespace(node) {
514 return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
517 function parentElement(node) {
518 var parent = node.parentNode;
519 return (parent.nodeType == 1) ? parent : null;
522 function getNodeIndex(node) {
524 while( (node = node.previousSibling) ) {
530 function getNodeLength(node) {
531 switch (node.nodeType) {
539 return node.childNodes.length;
543 function getCommonAncestor(node1, node2) {
544 var ancestors = [], n;
545 for (n = node1; n; n = n.parentNode) {
549 for (n = node2; n; n = n.parentNode) {
550 if (arrayContains(ancestors, n)) {
558 function isAncestorOf(ancestor, descendant, selfIsAncestor) {
559 var n = selfIsAncestor ? descendant : descendant.parentNode;
561 if (n === ancestor) {
570 function isOrIsAncestorOf(ancestor, descendant) {
571 return isAncestorOf(ancestor, descendant, true);
574 function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
575 var p, n = selfIsAncestor ? node : node.parentNode;
578 if (p === ancestor) {
586 function isCharacterDataNode(node) {
587 var t = node.nodeType;
588 return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
591 function isTextOrCommentNode(node) {
595 var t = node.nodeType;
596 return t == 3 || t == 8 ; // Text or Comment
599 function insertAfter(node, precedingNode) {
600 var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
602 parent.insertBefore(node, nextNode);
604 parent.appendChild(node);
609 // Note that we cannot use splitText() because it is bugridden in IE 9.
610 function splitDataNode(node, index, positionsToPreserve) {
611 var newNode = node.cloneNode(false);
612 newNode.deleteData(0, index);
613 node.deleteData(index, node.length - index);
614 insertAfter(newNode, node);
616 // Preserve positions
617 if (positionsToPreserve) {
618 for (var i = 0, position; position = positionsToPreserve[i++]; ) {
619 // Handle case where position was inside the portion of node after the split point
620 if (position.node == node && position.offset > index) {
621 position.node = newNode;
622 position.offset -= index;
624 // Handle the case where the position is a node offset within node's parent
625 else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
633 function getDocument(node) {
634 if (node.nodeType == 9) {
636 } else if (typeof node.ownerDocument != UNDEF) {
637 return node.ownerDocument;
638 } else if (typeof node.document != UNDEF) {
639 return node.document;
640 } else if (node.parentNode) {
641 return getDocument(node.parentNode);
643 throw module.createError("getDocument: no document found for node");
647 function getWindow(node) {
648 var doc = getDocument(node);
649 if (typeof doc.defaultView != UNDEF) {
650 return doc.defaultView;
651 } else if (typeof doc.parentWindow != UNDEF) {
652 return doc.parentWindow;
654 throw module.createError("Cannot get a window object for node");
658 function getIframeDocument(iframeEl) {
659 if (typeof iframeEl.contentDocument != UNDEF) {
660 return iframeEl.contentDocument;
661 } else if (typeof iframeEl.contentWindow != UNDEF) {
662 return iframeEl.contentWindow.document;
664 throw module.createError("getIframeDocument: No Document object found for iframe element");
668 function getIframeWindow(iframeEl) {
669 if (typeof iframeEl.contentWindow != UNDEF) {
670 return iframeEl.contentWindow;
671 } else if (typeof iframeEl.contentDocument != UNDEF) {
672 return iframeEl.contentDocument.defaultView;
674 throw module.createError("getIframeWindow: No Window object found for iframe element");
678 // This looks bad. Is it worth it?
679 function isWindow(obj) {
680 return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
683 function getContentDocument(obj, module, methodName) {
690 // Test if a DOM node has been passed and obtain a document object for it if so
691 else if (util.isHostProperty(obj, "nodeType")) {
692 doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
693 getIframeDocument(obj) : getDocument(obj);
696 // Test if the doc parameter appears to be a Window object
697 else if (isWindow(obj)) {
702 throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
708 function getRootContainer(node) {
710 while ( (parent = node.parentNode) ) {
716 function comparePoints(nodeA, offsetA, nodeB, offsetB) {
717 // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
718 var nodeC, root, childA, childB, n;
719 if (nodeA == nodeB) {
720 // Case 1: nodes are the same
721 return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
722 } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
723 // Case 2: node C (container B or an ancestor) is a child node of A
724 return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
725 } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
726 // Case 3: node C (container A or an ancestor) is a child node of B
727 return getNodeIndex(nodeC) < offsetB ? -1 : 1;
729 root = getCommonAncestor(nodeA, nodeB);
731 throw new Error("comparePoints error: nodes have no common ancestor");
734 // Case 4: containers are siblings or descendants of siblings
735 childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
736 childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
738 if (childA === childB) {
739 // This shouldn't be possible
740 throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
746 } else if (n === childB) {
755 /*----------------------------------------------------------------------------------------------------------------*/
757 // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
758 var crashyTextNodes = false;
760 function isBrokenNode(node) {
771 var el = document.createElement("b");
773 var textNode = el.firstChild;
774 el.innerHTML = "<br />";
775 crashyTextNodes = isBrokenNode(textNode);
777 api.features.crashyTextNodes = crashyTextNodes;
780 /*----------------------------------------------------------------------------------------------------------------*/
782 function inspectNode(node) {
786 if (crashyTextNodes && isBrokenNode(node)) {
787 return "[Broken node]";
789 if (isCharacterDataNode(node)) {
790 return '"' + node.data + '"';
792 if (node.nodeType == 1) {
793 var idAttr = node.id ? ' id="' + node.id + '"' : "";
794 return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
796 return node.nodeName;
799 function fragmentFromNodeChildren(node) {
800 var fragment = getDocument(node).createDocumentFragment(), child;
801 while ( (child = node.firstChild) ) {
802 fragment.appendChild(child);
807 var getComputedStyleProperty;
808 if (typeof window.getComputedStyle != UNDEF) {
809 getComputedStyleProperty = function(el, propName) {
810 return getWindow(el).getComputedStyle(el, null)[propName];
812 } else if (typeof document.documentElement.currentStyle != UNDEF) {
813 getComputedStyleProperty = function(el, propName) {
814 return el.currentStyle ? el.currentStyle[propName] : "";
817 module.fail("No means of obtaining computed style properties found");
820 function createTestElement(doc, html, contentEditable) {
821 var body = getBody(doc);
822 var el = doc.createElement("div");
823 el.contentEditable = "" + !!contentEditable;
828 // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
829 var bodyFirstChild = body.firstChild;
830 if (bodyFirstChild) {
831 body.insertBefore(el, bodyFirstChild);
833 body.appendChild(el);
839 function removeNode(node) {
840 return node.parentNode.removeChild(node);
843 function NodeIterator(root) {
848 NodeIterator.prototype = {
851 hasNext: function() {
856 var n = this._current = this._next;
859 child = n.firstChild;
864 while ((n !== this.root) && !(next = n.nextSibling)) {
870 return this._current;
874 this._current = this._next = this.root = null;
878 function createIterator(root) {
879 return new NodeIterator(root);
882 function DomPosition(node, offset) {
884 this.offset = offset;
887 DomPosition.prototype = {
888 equals: function(pos) {
889 return !!pos && this.node === pos.node && this.offset == pos.offset;
892 inspect: function() {
893 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
896 toString: function() {
897 return this.inspect();
901 function DOMException(codeName) {
902 this.code = this[codeName];
903 this.codeName = codeName;
904 this.message = "DOMException: " + this.codeName;
907 DOMException.prototype = {
909 HIERARCHY_REQUEST_ERR: 3,
910 WRONG_DOCUMENT_ERR: 4,
911 NO_MODIFICATION_ALLOWED_ERR: 7,
913 NOT_SUPPORTED_ERR: 9,
914 INVALID_STATE_ERR: 11,
915 INVALID_NODE_TYPE_ERR: 24
918 DOMException.prototype.toString = function() {
923 arrayContains: arrayContains,
924 isHtmlNamespace: isHtmlNamespace,
925 parentElement: parentElement,
926 getNodeIndex: getNodeIndex,
927 getNodeLength: getNodeLength,
928 getCommonAncestor: getCommonAncestor,
929 isAncestorOf: isAncestorOf,
930 isOrIsAncestorOf: isOrIsAncestorOf,
931 getClosestAncestorIn: getClosestAncestorIn,
932 isCharacterDataNode: isCharacterDataNode,
933 isTextOrCommentNode: isTextOrCommentNode,
934 insertAfter: insertAfter,
935 splitDataNode: splitDataNode,
936 getDocument: getDocument,
937 getWindow: getWindow,
938 getIframeWindow: getIframeWindow,
939 getIframeDocument: getIframeDocument,
942 getContentDocument: getContentDocument,
943 getRootContainer: getRootContainer,
944 comparePoints: comparePoints,
945 isBrokenNode: isBrokenNode,
946 inspectNode: inspectNode,
947 getComputedStyleProperty: getComputedStyleProperty,
948 createTestElement: createTestElement,
949 removeNode: removeNode,
950 fragmentFromNodeChildren: fragmentFromNodeChildren,
951 createIterator: createIterator,
952 DomPosition: DomPosition
955 api.DOMException = DOMException;
958 /*----------------------------------------------------------------------------------------------------------------*/
\r
960 // Pure JavaScript implementation of DOM Range
961 api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
964 var DomPosition = dom.DomPosition;
965 var DOMException = api.DOMException;
967 var isCharacterDataNode = dom.isCharacterDataNode;
968 var getNodeIndex = dom.getNodeIndex;
969 var isOrIsAncestorOf = dom.isOrIsAncestorOf;
970 var getDocument = dom.getDocument;
971 var comparePoints = dom.comparePoints;
972 var splitDataNode = dom.splitDataNode;
973 var getClosestAncestorIn = dom.getClosestAncestorIn;
974 var getNodeLength = dom.getNodeLength;
975 var arrayContains = dom.arrayContains;
976 var getRootContainer = dom.getRootContainer;
977 var crashyTextNodes = api.features.crashyTextNodes;
979 var removeNode = dom.removeNode;
981 /*----------------------------------------------------------------------------------------------------------------*/
985 function isNonTextPartiallySelected(node, range) {
986 return (node.nodeType != 3) &&
987 (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
990 function getRangeDocument(range) {
991 return range.document || getDocument(range.startContainer);
994 function getRangeRoot(range) {
995 return getRootContainer(range.startContainer);
998 function getBoundaryBeforeNode(node) {
999 return new DomPosition(node.parentNode, getNodeIndex(node));
1002 function getBoundaryAfterNode(node) {
1003 return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
1006 function insertNodeAtPosition(node, n, o) {
1007 var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
1008 if (isCharacterDataNode(n)) {
1009 if (o == n.length) {
1010 dom.insertAfter(node, n);
1012 n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
1014 } else if (o >= n.childNodes.length) {
1015 n.appendChild(node);
1017 n.insertBefore(node, n.childNodes[o]);
1019 return firstNodeInserted;
1022 function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
1023 assertRangeValid(rangeA);
1024 assertRangeValid(rangeB);
1026 if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
1027 throw new DOMException("WRONG_DOCUMENT_ERR");
1030 var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
1031 endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
1033 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1036 function cloneSubtree(iterator) {
1037 var partiallySelected;
1038 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1039 partiallySelected = iterator.isPartiallySelectedSubtree();
1040 node = node.cloneNode(!partiallySelected);
1041 if (partiallySelected) {
1042 subIterator = iterator.getSubtreeIterator();
1043 node.appendChild(cloneSubtree(subIterator));
1044 subIterator.detach();
1047 if (node.nodeType == 10) { // DocumentType
1048 throw new DOMException("HIERARCHY_REQUEST_ERR");
1050 frag.appendChild(node);
1055 function iterateSubtree(rangeIterator, func, iteratorState) {
1057 iteratorState = iteratorState || { stop: false };
1058 for (var node, subRangeIterator; node = rangeIterator.next(); ) {
1059 if (rangeIterator.isPartiallySelectedSubtree()) {
1060 if (func(node) === false) {
1061 iteratorState.stop = true;
1064 // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
1065 // the node selected by the Range.
1066 subRangeIterator = rangeIterator.getSubtreeIterator();
1067 iterateSubtree(subRangeIterator, func, iteratorState);
1068 subRangeIterator.detach();
1069 if (iteratorState.stop) {
1074 // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
1076 it = dom.createIterator(node);
1077 while ( (n = it.next()) ) {
1078 if (func(n) === false) {
1079 iteratorState.stop = true;
1087 function deleteSubtree(iterator) {
1089 while (iterator.next()) {
1090 if (iterator.isPartiallySelectedSubtree()) {
1091 subIterator = iterator.getSubtreeIterator();
1092 deleteSubtree(subIterator);
1093 subIterator.detach();
1100 function extractSubtree(iterator) {
1101 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1103 if (iterator.isPartiallySelectedSubtree()) {
1104 node = node.cloneNode(false);
1105 subIterator = iterator.getSubtreeIterator();
1106 node.appendChild(extractSubtree(subIterator));
1107 subIterator.detach();
1111 if (node.nodeType == 10) { // DocumentType
1112 throw new DOMException("HIERARCHY_REQUEST_ERR");
1114 frag.appendChild(node);
1119 function getNodesInRange(range, nodeTypes, filter) {
1120 var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
1121 var filterExists = !!filter;
1122 if (filterNodeTypes) {
1123 regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
1127 iterateSubtree(new RangeIterator(range, false), function(node) {
1128 if (filterNodeTypes && !regex.test(node.nodeType)) {
1131 if (filterExists && !filter(node)) {
1134 // Don't include a boundary container if it is a character data node and the range does not contain any
1135 // of its character data. See issue 190.
1136 var sc = range.startContainer;
1137 if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
1141 var ec = range.endContainer;
1142 if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
1151 function inspect(range) {
1152 var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
1153 return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
1154 dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
1157 /*----------------------------------------------------------------------------------------------------------------*/
1159 // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
1161 function RangeIterator(range, clonePartiallySelectedTextNodes) {
1163 this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
1166 if (!range.collapsed) {
1167 this.sc = range.startContainer;
1168 this.so = range.startOffset;
1169 this.ec = range.endContainer;
1170 this.eo = range.endOffset;
1171 var root = range.commonAncestorContainer;
1173 if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
1174 this.isSingleCharacterDataNode = true;
1175 this._first = this._last = this._next = this.sc;
1177 this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
1178 this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
1179 this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
1180 this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
1185 RangeIterator.prototype = {
1190 isSingleCharacterDataNode: false,
1193 this._current = null;
1194 this._next = this._first;
1197 hasNext: function() {
1198 return !!this._next;
1202 // Move to next node
1203 var current = this._current = this._next;
1205 this._next = (current !== this._last) ? current.nextSibling : null;
1207 // Check for partially selected text nodes
1208 if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
1209 if (current === this.ec) {
1210 (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
1212 if (this._current === this.sc) {
1213 (current = current.cloneNode(true)).deleteData(0, this.so);
1221 remove: function() {
1222 var current = this._current, start, end;
1224 if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
1225 start = (current === this.sc) ? this.so : 0;
1226 end = (current === this.ec) ? this.eo : current.length;
1228 current.deleteData(start, end - start);
1231 if (current.parentNode) {
1232 removeNode(current);
1238 // Checks if the current node is partially selected
1239 isPartiallySelectedSubtree: function() {
1240 var current = this._current;
1241 return isNonTextPartiallySelected(current, this.range);
1244 getSubtreeIterator: function() {
1246 if (this.isSingleCharacterDataNode) {
1247 subRange = this.range.cloneRange();
1248 subRange.collapse(false);
1250 subRange = new Range(getRangeDocument(this.range));
1251 var current = this._current;
1252 var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
1254 if (isOrIsAncestorOf(current, this.sc)) {
1255 startContainer = this.sc;
1256 startOffset = this.so;
1258 if (isOrIsAncestorOf(current, this.ec)) {
1259 endContainer = this.ec;
1260 endOffset = this.eo;
1263 updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
1265 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
1268 detach: function() {
1269 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
1273 /*----------------------------------------------------------------------------------------------------------------*/
1275 var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
1276 var rootContainerNodeTypes = [2, 9, 11];
1277 var readonlyNodeTypes = [5, 6, 10, 12];
1278 var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
1279 var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
1281 function createAncestorFinder(nodeTypes) {
1282 return function(node, selfIsAncestor) {
1283 var t, n = selfIsAncestor ? node : node.parentNode;
1286 if (arrayContains(nodeTypes, t)) {
1295 var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1296 var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1297 var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
1299 function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1300 if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1301 throw new DOMException("INVALID_NODE_TYPE_ERR");
1305 function assertValidNodeType(node, invalidTypes) {
1306 if (!arrayContains(invalidTypes, node.nodeType)) {
1307 throw new DOMException("INVALID_NODE_TYPE_ERR");
1311 function assertValidOffset(node, offset) {
1312 if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1313 throw new DOMException("INDEX_SIZE_ERR");
1317 function assertSameDocumentOrFragment(node1, node2) {
1318 if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1319 throw new DOMException("WRONG_DOCUMENT_ERR");
1323 function assertNodeNotReadOnly(node) {
1324 if (getReadonlyAncestor(node, true)) {
1325 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1329 function assertNode(node, codeName) {
1331 throw new DOMException(codeName);
1335 function isValidOffset(node, offset) {
1336 return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
1339 function isRangeValid(range) {
1340 return (!!range.startContainer && !!range.endContainer &&
1341 !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
1342 getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
1343 isValidOffset(range.startContainer, range.startOffset) &&
1344 isValidOffset(range.endContainer, range.endOffset));
1347 function assertRangeValid(range) {
1348 if (!isRangeValid(range)) {
1349 throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
1353 /*----------------------------------------------------------------------------------------------------------------*/
1355 // Test the browser's innerHTML support to decide how to implement createContextualFragment
1356 var styleEl = document.createElement("style");
1357 var htmlParsingConforms = false;
1359 styleEl.innerHTML = "<b>x</b>";
1360 htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
1365 api.features.htmlParsingConforms = htmlParsingConforms;
1367 var createContextualFragment = htmlParsingConforms ?
1369 // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
1370 // discussion and base code for this implementation at issue 67.
1371 // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
1372 // Thanks to Aleks Williams.
1373 function(fragmentStr) {
1374 // "Let node the context object's start's node."
1375 var node = this.startContainer;
1376 var doc = getDocument(node);
1378 // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1379 // exception and abort these steps."
1381 throw new DOMException("INVALID_STATE_ERR");
1384 // "Let element be as follows, depending on node's interface:"
1385 // Document, Document Fragment: null
1389 if (node.nodeType == 1) {
1392 // "Text, Comment: node's parentElement"
1393 } else if (isCharacterDataNode(node)) {
1394 el = dom.parentElement(node);
1397 // "If either element is null or element's ownerDocument is an HTML document
1398 // and element's local name is "html" and element's namespace is the HTML
1400 if (el === null || (
1401 el.nodeName == "HTML" &&
1402 dom.isHtmlNamespace(getDocument(el).documentElement) &&
1403 dom.isHtmlNamespace(el)
1406 // "let element be a new Element with "body" as its local name and the HTML
1407 // namespace as its namespace.""
1408 el = doc.createElement("body");
1410 el = el.cloneNode(false);
1413 // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
1414 // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
1415 // "In either case, the algorithm must be invoked with fragment as the input
1416 // and element as the context element."
1417 el.innerHTML = fragmentStr;
1419 // "If this raises an exception, then abort these steps. Otherwise, let new
1420 // children be the nodes returned."
1422 // "Let fragment be a new DocumentFragment."
1423 // "Append all new children to fragment."
1424 // "Return fragment."
1425 return dom.fragmentFromNodeChildren(el);
1428 // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
1429 // previous versions of Rangy used (with the exception of using a body element rather than a div)
1430 function(fragmentStr) {
1431 var doc = getRangeDocument(this);
1432 var el = doc.createElement("body");
1433 el.innerHTML = fragmentStr;
1435 return dom.fragmentFromNodeChildren(el);
1438 function splitRangeBoundaries(range, positionsToPreserve) {
1439 assertRangeValid(range);
1441 var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
1442 var startEndSame = (sc === ec);
1444 if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1445 splitDataNode(ec, eo, positionsToPreserve);
1448 if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1449 sc = splitDataNode(sc, so, positionsToPreserve);
1453 } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
1458 range.setStartAndEnd(sc, so, ec, eo);
1461 function rangeToHtml(range) {
1462 assertRangeValid(range);
1463 var container = range.commonAncestorContainer.parentNode.cloneNode(false);
1464 container.appendChild( range.cloneContents() );
1465 return container.innerHTML;
1468 /*----------------------------------------------------------------------------------------------------------------*/
1470 var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1471 "commonAncestorContainer"];
1473 var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
1474 var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
1476 util.extend(api.rangePrototype, {
1477 compareBoundaryPoints: function(how, range) {
1478 assertRangeValid(this);
1479 assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1481 var nodeA, offsetA, nodeB, offsetB;
1482 var prefixA = (how == e2s || how == s2s) ? "start" : "end";
1483 var prefixB = (how == s2e || how == s2s) ? "start" : "end";
1484 nodeA = this[prefixA + "Container"];
1485 offsetA = this[prefixA + "Offset"];
1486 nodeB = range[prefixB + "Container"];
1487 offsetB = range[prefixB + "Offset"];
1488 return comparePoints(nodeA, offsetA, nodeB, offsetB);
1491 insertNode: function(node) {
1492 assertRangeValid(this);
1493 assertValidNodeType(node, insertableNodeTypes);
1494 assertNodeNotReadOnly(this.startContainer);
1496 if (isOrIsAncestorOf(node, this.startContainer)) {
1497 throw new DOMException("HIERARCHY_REQUEST_ERR");
1500 // No check for whether the container of the start of the Range is of a type that does not allow
1501 // children of the type of node: the browser's DOM implementation should do this for us when we attempt
1504 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1505 this.setStartBefore(firstNodeInserted);
1508 cloneContents: function() {
1509 assertRangeValid(this);
1512 if (this.collapsed) {
1513 return getRangeDocument(this).createDocumentFragment();
1515 if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
1516 clone = this.startContainer.cloneNode(true);
1517 clone.data = clone.data.slice(this.startOffset, this.endOffset);
1518 frag = getRangeDocument(this).createDocumentFragment();
1519 frag.appendChild(clone);
1522 var iterator = new RangeIterator(this, true);
1523 clone = cloneSubtree(iterator);
1530 canSurroundContents: function() {
1531 assertRangeValid(this);
1532 assertNodeNotReadOnly(this.startContainer);
1533 assertNodeNotReadOnly(this.endContainer);
1535 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1536 // no non-text nodes.
1537 var iterator = new RangeIterator(this, true);
1538 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1539 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1541 return !boundariesInvalid;
1544 surroundContents: function(node) {
1545 assertValidNodeType(node, surroundNodeTypes);
1547 if (!this.canSurroundContents()) {
1548 throw new DOMException("INVALID_STATE_ERR");
1551 // Extract the contents
1552 var content = this.extractContents();
1554 // Clear the children of the node
1555 if (node.hasChildNodes()) {
1556 while (node.lastChild) {
1557 node.removeChild(node.lastChild);
1561 // Insert the new node and add the extracted contents
1562 insertNodeAtPosition(node, this.startContainer, this.startOffset);
1563 node.appendChild(content);
1565 this.selectNode(node);
1568 cloneRange: function() {
1569 assertRangeValid(this);
1570 var range = new Range(getRangeDocument(this));
1571 var i = rangeProperties.length, prop;
1573 prop = rangeProperties[i];
1574 range[prop] = this[prop];
1579 toString: function() {
1580 assertRangeValid(this);
1581 var sc = this.startContainer;
1582 if (sc === this.endContainer && isCharacterDataNode(sc)) {
1583 return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
1585 var textParts = [], iterator = new RangeIterator(this, true);
1586 iterateSubtree(iterator, function(node) {
1587 // Accept only text or CDATA nodes, not comments
1588 if (node.nodeType == 3 || node.nodeType == 4) {
1589 textParts.push(node.data);
1593 return textParts.join("");
1597 // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
1598 // been removed from Mozilla.
1600 compareNode: function(node) {
1601 assertRangeValid(this);
1603 var parent = node.parentNode;
1604 var nodeIndex = getNodeIndex(node);
1607 throw new DOMException("NOT_FOUND_ERR");
1610 var startComparison = this.comparePoint(parent, nodeIndex),
1611 endComparison = this.comparePoint(parent, nodeIndex + 1);
1613 if (startComparison < 0) { // Node starts before
1614 return (endComparison > 0) ? n_b_a : n_b;
1616 return (endComparison > 0) ? n_a : n_i;
1620 comparePoint: function(node, offset) {
1621 assertRangeValid(this);
1622 assertNode(node, "HIERARCHY_REQUEST_ERR");
1623 assertSameDocumentOrFragment(node, this.startContainer);
1625 if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
1627 } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
1633 createContextualFragment: createContextualFragment,
1635 toHtml: function() {
1636 return rangeToHtml(this);
1639 // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
1640 // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
1641 intersectsNode: function(node, touchingIsIntersecting) {
1642 assertRangeValid(this);
1643 if (getRootContainer(node) != getRangeRoot(this)) {
1647 var parent = node.parentNode, offset = getNodeIndex(node);
1652 var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
1653 endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
1655 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1658 isPointInRange: function(node, offset) {
1659 assertRangeValid(this);
1660 assertNode(node, "HIERARCHY_REQUEST_ERR");
1661 assertSameDocumentOrFragment(node, this.startContainer);
1663 return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
1664 (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
1667 // The methods below are non-standard and invented by me.
1669 // Sharing a boundary start-to-end or end-to-start does not count as intersection.
1670 intersectsRange: function(range) {
1671 return rangesIntersect(this, range, false);
1674 // Sharing a boundary start-to-end or end-to-start does count as intersection.
1675 intersectsOrTouchesRange: function(range) {
1676 return rangesIntersect(this, range, true);
1679 intersection: function(range) {
1680 if (this.intersectsRange(range)) {
1681 var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
1682 endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
1684 var intersectionRange = this.cloneRange();
1685 if (startComparison == -1) {
1686 intersectionRange.setStart(range.startContainer, range.startOffset);
1688 if (endComparison == 1) {
1689 intersectionRange.setEnd(range.endContainer, range.endOffset);
1691 return intersectionRange;
1696 union: function(range) {
1697 if (this.intersectsOrTouchesRange(range)) {
1698 var unionRange = this.cloneRange();
1699 if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
1700 unionRange.setStart(range.startContainer, range.startOffset);
1702 if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
1703 unionRange.setEnd(range.endContainer, range.endOffset);
1707 throw new DOMException("Ranges do not intersect");
1711 containsNode: function(node, allowPartial) {
1713 return this.intersectsNode(node, false);
1715 return this.compareNode(node) == n_i;
1719 containsNodeContents: function(node) {
1720 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
1723 containsRange: function(range) {
1724 var intersection = this.intersection(range);
1725 return intersection !== null && range.equals(intersection);
1728 containsNodeText: function(node) {
1729 var nodeRange = this.cloneRange();
1730 nodeRange.selectNode(node);
1731 var textNodes = nodeRange.getNodes([3]);
1732 if (textNodes.length > 0) {
1733 nodeRange.setStart(textNodes[0], 0);
1734 var lastTextNode = textNodes.pop();
1735 nodeRange.setEnd(lastTextNode, lastTextNode.length);
1736 return this.containsRange(nodeRange);
1738 return this.containsNodeContents(node);
1742 getNodes: function(nodeTypes, filter) {
1743 assertRangeValid(this);
1744 return getNodesInRange(this, nodeTypes, filter);
1747 getDocument: function() {
1748 return getRangeDocument(this);
1751 collapseBefore: function(node) {
1752 this.setEndBefore(node);
1753 this.collapse(false);
1756 collapseAfter: function(node) {
1757 this.setStartAfter(node);
1758 this.collapse(true);
1761 getBookmark: function(containerNode) {
1762 var doc = getRangeDocument(this);
1763 var preSelectionRange = api.createRange(doc);
1764 containerNode = containerNode || dom.getBody(doc);
1765 preSelectionRange.selectNodeContents(containerNode);
1766 var range = this.intersection(preSelectionRange);
1767 var start = 0, end = 0;
1769 preSelectionRange.setEnd(range.startContainer, range.startOffset);
1770 start = preSelectionRange.toString().length;
1771 end = start + range.toString().length;
1777 containerNode: containerNode
1781 moveToBookmark: function(bookmark) {
1782 var containerNode = bookmark.containerNode;
1784 this.setStart(containerNode, 0);
1785 this.collapse(true);
1786 var nodeStack = [containerNode], node, foundStart = false, stop = false;
1787 var nextCharIndex, i, childNodes;
1789 while (!stop && (node = nodeStack.pop())) {
1790 if (node.nodeType == 3) {
1791 nextCharIndex = charIndex + node.length;
1792 if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
1793 this.setStart(node, bookmark.start - charIndex);
1796 if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
1797 this.setEnd(node, bookmark.end - charIndex);
1800 charIndex = nextCharIndex;
1802 childNodes = node.childNodes;
1803 i = childNodes.length;
1805 nodeStack.push(childNodes[i]);
1811 getName: function() {
1815 equals: function(range) {
1816 return Range.rangesEqual(this, range);
1819 isValid: function() {
1820 return isRangeValid(this);
1823 inspect: function() {
1824 return inspect(this);
1827 detach: function() {
1828 // In DOM4, detach() is now a no-op.
1832 function copyComparisonConstantsToObject(obj) {
1833 obj.START_TO_START = s2s;
1834 obj.START_TO_END = s2e;
1835 obj.END_TO_END = e2e;
1836 obj.END_TO_START = e2s;
1838 obj.NODE_BEFORE = n_b;
1839 obj.NODE_AFTER = n_a;
1840 obj.NODE_BEFORE_AND_AFTER = n_b_a;
1841 obj.NODE_INSIDE = n_i;
1844 function copyComparisonConstants(constructor) {
1845 copyComparisonConstantsToObject(constructor);
1846 copyComparisonConstantsToObject(constructor.prototype);
1849 function createRangeContentRemover(remover, boundaryUpdater) {
1851 assertRangeValid(this);
1853 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
1855 var iterator = new RangeIterator(this, true);
1857 // Work out where to position the range after content removal
1860 node = getClosestAncestorIn(sc, root, true);
1861 boundary = getBoundaryAfterNode(node);
1863 so = boundary.offset;
1866 // Check none of the range is read-only
1867 iterateSubtree(iterator, assertNodeNotReadOnly);
1871 // Remove the content
1872 var returnValue = remover(iterator);
1875 // Move to the new position
1876 boundaryUpdater(this, sc, so, sc, so);
1882 function createPrototypeRange(constructor, boundaryUpdater) {
1883 function createBeforeAfterNodeSetter(isBefore, isStart) {
1884 return function(node) {
1885 assertValidNodeType(node, beforeAfterNodeTypes);
1886 assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
1888 var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
1889 (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
1893 function setRangeStart(range, node, offset) {
1894 var ec = range.endContainer, eo = range.endOffset;
1895 if (node !== range.startContainer || offset !== range.startOffset) {
1896 // Check the root containers of the range and the new boundary, and also check whether the new boundary
1897 // is after the current end. In either case, collapse the range to the new position
1898 if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
1902 boundaryUpdater(range, node, offset, ec, eo);
1906 function setRangeEnd(range, node, offset) {
1907 var sc = range.startContainer, so = range.startOffset;
1908 if (node !== range.endContainer || offset !== range.endOffset) {
1909 // Check the root containers of the range and the new boundary, and also check whether the new boundary
1910 // is after the current end. In either case, collapse the range to the new position
1911 if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
1915 boundaryUpdater(range, sc, so, node, offset);
1919 // Set up inheritance
1920 var F = function() {};
1921 F.prototype = api.rangePrototype;
1922 constructor.prototype = new F();
1924 util.extend(constructor.prototype, {
1925 setStart: function(node, offset) {
1926 assertNoDocTypeNotationEntityAncestor(node, true);
1927 assertValidOffset(node, offset);
1929 setRangeStart(this, node, offset);
1932 setEnd: function(node, offset) {
1933 assertNoDocTypeNotationEntityAncestor(node, true);
1934 assertValidOffset(node, offset);
1936 setRangeEnd(this, node, offset);
1940 * Convenience method to set a range's start and end boundaries. Overloaded as follows:
1941 * - Two parameters (node, offset) creates a collapsed range at that position
1942 * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
1943 * startOffset and ending at endOffset
1944 * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
1945 * startNode and ending at endOffset in endNode
1947 setStartAndEnd: function() {
1948 var args = arguments;
1949 var sc = args[0], so = args[1], ec = sc, eo = so;
1951 switch (args.length) {
1961 boundaryUpdater(this, sc, so, ec, eo);
1964 setBoundary: function(node, offset, isStart) {
1965 this["set" + (isStart ? "Start" : "End")](node, offset);
1968 setStartBefore: createBeforeAfterNodeSetter(true, true),
1969 setStartAfter: createBeforeAfterNodeSetter(false, true),
1970 setEndBefore: createBeforeAfterNodeSetter(true, false),
1971 setEndAfter: createBeforeAfterNodeSetter(false, false),
1973 collapse: function(isStart) {
1974 assertRangeValid(this);
1976 boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
1978 boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
1982 selectNodeContents: function(node) {
1983 assertNoDocTypeNotationEntityAncestor(node, true);
1985 boundaryUpdater(this, node, 0, node, getNodeLength(node));
1988 selectNode: function(node) {
1989 assertNoDocTypeNotationEntityAncestor(node, false);
1990 assertValidNodeType(node, beforeAfterNodeTypes);
1992 var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
1993 boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
1996 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
1998 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
2000 canSurroundContents: function() {
2001 assertRangeValid(this);
2002 assertNodeNotReadOnly(this.startContainer);
2003 assertNodeNotReadOnly(this.endContainer);
2005 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
2006 // no non-text nodes.
2007 var iterator = new RangeIterator(this, true);
2008 var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
2009 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
2011 return !boundariesInvalid;
2014 splitBoundaries: function() {
2015 splitRangeBoundaries(this);
2018 splitBoundariesPreservingPositions: function(positionsToPreserve) {
2019 splitRangeBoundaries(this, positionsToPreserve);
2022 normalizeBoundaries: function() {
2023 assertRangeValid(this);
2025 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
2027 var mergeForward = function(node) {
2028 var sibling = node.nextSibling;
2029 if (sibling && sibling.nodeType == node.nodeType) {
2032 node.appendData(sibling.data);
2033 removeNode(sibling);
2037 var mergeBackward = function(node) {
2038 var sibling = node.previousSibling;
2039 if (sibling && sibling.nodeType == node.nodeType) {
2041 var nodeLength = node.length;
2042 so = sibling.length;
2043 node.insertData(0, sibling.data);
2044 removeNode(sibling);
2048 } else if (ec == node.parentNode) {
2049 var nodeIndex = getNodeIndex(node);
2050 if (eo == nodeIndex) {
2053 } else if (eo > nodeIndex) {
2060 var normalizeStart = true;
2063 if (isCharacterDataNode(ec)) {
2064 if (eo == ec.length) {
2066 } else if (eo == 0) {
2067 sibling = ec.previousSibling;
2068 if (sibling && sibling.nodeType == ec.nodeType) {
2069 eo = sibling.length;
2071 normalizeStart = false;
2073 sibling.appendData(ec.data);
2080 var endNode = ec.childNodes[eo - 1];
2081 if (endNode && isCharacterDataNode(endNode)) {
2082 mergeForward(endNode);
2085 normalizeStart = !this.collapsed;
2088 if (normalizeStart) {
2089 if (isCharacterDataNode(sc)) {
2092 } else if (so == sc.length) {
2093 sibling = sc.nextSibling;
2094 if (sibling && sibling.nodeType == sc.nodeType) {
2095 if (ec == sibling) {
2099 sc.appendData(sibling.data);
2100 removeNode(sibling);
2104 if (so < sc.childNodes.length) {
2105 var startNode = sc.childNodes[so];
2106 if (startNode && isCharacterDataNode(startNode)) {
2107 mergeBackward(startNode);
2116 boundaryUpdater(this, sc, so, ec, eo);
2119 collapseToPoint: function(node, offset) {
2120 assertNoDocTypeNotationEntityAncestor(node, true);
2121 assertValidOffset(node, offset);
2122 this.setStartAndEnd(node, offset);
2126 copyComparisonConstants(constructor);
2129 /*----------------------------------------------------------------------------------------------------------------*/
2131 // Updates commonAncestorContainer and collapsed after boundary change
2132 function updateCollapsedAndCommonAncestor(range) {
2133 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2134 range.commonAncestorContainer = range.collapsed ?
2135 range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
2138 function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
2139 range.startContainer = startContainer;
2140 range.startOffset = startOffset;
2141 range.endContainer = endContainer;
2142 range.endOffset = endOffset;
2143 range.document = dom.getDocument(startContainer);
2145 updateCollapsedAndCommonAncestor(range);
2148 function Range(doc) {
2149 this.startContainer = doc;
2150 this.startOffset = 0;
2151 this.endContainer = doc;
2153 this.document = doc;
2154 updateCollapsedAndCommonAncestor(this);
2157 createPrototypeRange(Range, updateBoundaries);
2159 util.extend(Range, {
2160 rangeProperties: rangeProperties,
2161 RangeIterator: RangeIterator,
2162 copyComparisonConstants: copyComparisonConstants,
2163 createPrototypeRange: createPrototypeRange,
2165 toHtml: rangeToHtml,
2166 getRangeDocument: getRangeDocument,
2167 rangesEqual: function(r1, r2) {
2168 return r1.startContainer === r2.startContainer &&
2169 r1.startOffset === r2.startOffset &&
2170 r1.endContainer === r2.endContainer &&
2171 r1.endOffset === r2.endOffset;
2175 api.DomRange = Range;
2178 /*----------------------------------------------------------------------------------------------------------------*/
\r
2180 // Wrappers for the browser's native DOM Range and/or TextRange implementation
2181 api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
2182 var WrappedRange, WrappedTextRange;
2184 var util = api.util;
2185 var DomPosition = dom.DomPosition;
2186 var DomRange = api.DomRange;
2187 var getBody = dom.getBody;
2188 var getContentDocument = dom.getContentDocument;
2189 var isCharacterDataNode = dom.isCharacterDataNode;
2192 /*----------------------------------------------------------------------------------------------------------------*/
2194 if (api.features.implementsDomRange) {
2195 // This is a wrapper around the browser's native DOM Range. It has two aims:
2196 // - Provide workarounds for specific browser bugs
2197 // - provide convenient extensions, which are inherited from Rangy's DomRange
2201 var rangeProperties = DomRange.rangeProperties;
2203 function updateRangeProperties(range) {
2204 var i = rangeProperties.length, prop;
2206 prop = rangeProperties[i];
2207 range[prop] = range.nativeRange[prop];
2209 // Fix for broken collapsed property in IE 9.
2210 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2213 function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
2214 var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
2215 var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
2216 var nativeRangeDifferent = !range.equals(range.nativeRange);
2218 // Always set both boundaries for the benefit of IE9 (see issue 35)
2219 if (startMoved || endMoved || nativeRangeDifferent) {
2220 range.setEnd(endContainer, endOffset);
2221 range.setStart(startContainer, startOffset);
2225 var createBeforeAfterNodeSetter;
2227 WrappedRange = function(range) {
2229 throw module.createError("WrappedRange: Range must be specified");
2231 this.nativeRange = range;
2232 updateRangeProperties(this);
2235 DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
2237 rangeProto = WrappedRange.prototype;
2239 rangeProto.selectNode = function(node) {
2240 this.nativeRange.selectNode(node);
2241 updateRangeProperties(this);
2244 rangeProto.cloneContents = function() {
2245 return this.nativeRange.cloneContents();
2248 // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
2249 // insertNode() is never delegated to the native range.
2251 rangeProto.surroundContents = function(node) {
2252 this.nativeRange.surroundContents(node);
2253 updateRangeProperties(this);
2256 rangeProto.collapse = function(isStart) {
2257 this.nativeRange.collapse(isStart);
2258 updateRangeProperties(this);
2261 rangeProto.cloneRange = function() {
2262 return new WrappedRange(this.nativeRange.cloneRange());
2265 rangeProto.refresh = function() {
2266 updateRangeProperties(this);
2269 rangeProto.toString = function() {
2270 return this.nativeRange.toString();
2273 // Create test range and node for feature detection
2275 var testTextNode = document.createTextNode("test");
2276 getBody(document).appendChild(testTextNode);
2277 var range = document.createRange();
2279 /*--------------------------------------------------------------------------------------------------------*/
2281 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
2284 range.setStart(testTextNode, 0);
2285 range.setEnd(testTextNode, 0);
2288 range.setStart(testTextNode, 1);
2290 rangeProto.setStart = function(node, offset) {
2291 this.nativeRange.setStart(node, offset);
2292 updateRangeProperties(this);
2295 rangeProto.setEnd = function(node, offset) {
2296 this.nativeRange.setEnd(node, offset);
2297 updateRangeProperties(this);
2300 createBeforeAfterNodeSetter = function(name) {
2301 return function(node) {
2302 this.nativeRange[name](node);
2303 updateRangeProperties(this);
2309 rangeProto.setStart = function(node, offset) {
2311 this.nativeRange.setStart(node, offset);
2313 this.nativeRange.setEnd(node, offset);
2314 this.nativeRange.setStart(node, offset);
2316 updateRangeProperties(this);
2319 rangeProto.setEnd = function(node, offset) {
2321 this.nativeRange.setEnd(node, offset);
2323 this.nativeRange.setStart(node, offset);
2324 this.nativeRange.setEnd(node, offset);
2326 updateRangeProperties(this);
2329 createBeforeAfterNodeSetter = function(name, oppositeName) {
2330 return function(node) {
2332 this.nativeRange[name](node);
2334 this.nativeRange[oppositeName](node);
2335 this.nativeRange[name](node);
2337 updateRangeProperties(this);
2342 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
2343 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
2344 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
2345 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
2347 /*--------------------------------------------------------------------------------------------------------*/
2349 // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
2350 // whether the native implementation can be trusted
2351 rangeProto.selectNodeContents = function(node) {
2352 this.setStartAndEnd(node, 0, dom.getNodeLength(node));
2355 /*--------------------------------------------------------------------------------------------------------*/
2357 // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
2358 // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
2360 range.selectNodeContents(testTextNode);
2361 range.setEnd(testTextNode, 3);
2363 var range2 = document.createRange();
2364 range2.selectNodeContents(testTextNode);
2365 range2.setEnd(testTextNode, 4);
2366 range2.setStart(testTextNode, 2);
2368 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
2369 range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
2370 // This is the wrong way round, so correct for it
2372 rangeProto.compareBoundaryPoints = function(type, range) {
2373 range = range.nativeRange || range;
2374 if (type == range.START_TO_END) {
2375 type = range.END_TO_START;
2376 } else if (type == range.END_TO_START) {
2377 type = range.START_TO_END;
2379 return this.nativeRange.compareBoundaryPoints(type, range);
2382 rangeProto.compareBoundaryPoints = function(type, range) {
2383 return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
2387 /*--------------------------------------------------------------------------------------------------------*/
2389 // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
2391 var el = document.createElement("div");
2392 el.innerHTML = "123";
2393 var textNode = el.firstChild;
2394 var body = getBody(document);
2395 body.appendChild(el);
2397 range.setStart(textNode, 1);
2398 range.setEnd(textNode, 2);
2399 range.deleteContents();
2401 if (textNode.data == "13") {
2402 // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
2403 // extractContents()
2404 rangeProto.deleteContents = function() {
2405 this.nativeRange.deleteContents();
2406 updateRangeProperties(this);
2409 rangeProto.extractContents = function() {
2410 var frag = this.nativeRange.extractContents();
2411 updateRangeProperties(this);
2417 body.removeChild(el);
2420 /*--------------------------------------------------------------------------------------------------------*/
2422 // Test for existence of createContextualFragment and delegate to it if it exists
2423 if (util.isHostMethod(range, "createContextualFragment")) {
2424 rangeProto.createContextualFragment = function(fragmentStr) {
2425 return this.nativeRange.createContextualFragment(fragmentStr);
2429 /*--------------------------------------------------------------------------------------------------------*/
2432 getBody(document).removeChild(testTextNode);
2434 rangeProto.getName = function() {
2435 return "WrappedRange";
2438 api.WrappedRange = WrappedRange;
2440 api.createNativeRange = function(doc) {
2441 doc = getContentDocument(doc, module, "createNativeRange");
2442 return doc.createRange();
2447 if (api.features.implementsTextRange) {
2449 This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
2450 method. For example, in the following (where pipes denote the selection boundaries):
2452 <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
2454 var range = document.selection.createRange();
2455 alert(range.parentElement().id); // Should alert "ul" but alerts "b"
2457 This method returns the common ancestor node of the following:
2458 - the parentElement() of the textRange
2459 - the parentElement() of the textRange after calling collapse(true)
2460 - the parentElement() of the textRange after calling collapse(false)
2462 var getTextRangeContainerElement = function(textRange) {
2463 var parentEl = textRange.parentElement();
2464 var range = textRange.duplicate();
2465 range.collapse(true);
2466 var startEl = range.parentElement();
2467 range = textRange.duplicate();
2468 range.collapse(false);
2469 var endEl = range.parentElement();
2470 var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
2472 return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
2475 var textRangeIsCollapsed = function(textRange) {
2476 return textRange.compareEndPoints("StartToEnd", textRange) == 0;
2479 // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
2480 // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
2481 // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
2482 // bugs, handling for inputs and images, plus optimizations.
2483 var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
2484 var workingRange = textRange.duplicate();
2485 workingRange.collapse(isStart);
2486 var containerElement = workingRange.parentElement();
2488 // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
2490 if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
2491 containerElement = wholeRangeContainerElement;
2495 // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
2496 // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
2497 if (!containerElement.canHaveHTML) {
2498 var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
2500 boundaryPosition: pos,
2502 nodeIndex: pos.offset,
2503 containerElement: pos.node
2508 var workingNode = dom.getDocument(containerElement).createElement("span");
2510 // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
2511 // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
2512 if (workingNode.parentNode) {
2513 dom.removeNode(workingNode);
2516 var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
2517 var previousNode, nextNode, boundaryPosition, boundaryNode;
2518 var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
2519 var childNodeCount = containerElement.childNodes.length;
2520 var end = childNodeCount;
2522 // Check end first. Code within the loop assumes that the endth child node of the container is definitely
2523 // after the range boundary.
2524 var nodeIndex = end;
2527 if (nodeIndex == childNodeCount) {
2528 containerElement.appendChild(workingNode);
2530 containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
2532 workingRange.moveToElementText(workingNode);
2533 comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
2534 if (comparison == 0 || start == end) {
2536 } else if (comparison == -1) {
2537 if (end == start + 1) {
2538 // We know the endth child node is after the range boundary, so we must be done.
2544 end = (end == start + 1) ? start : nodeIndex;
2546 nodeIndex = Math.floor((start + end) / 2);
2547 containerElement.removeChild(workingNode);
2551 // We've now reached or gone past the boundary of the text range we're interested in
2552 // so have identified the node we want
2553 boundaryNode = workingNode.nextSibling;
2555 if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
2556 // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
2557 // the node containing the text range's boundary, so we move the end of the working range to the
2558 // boundary point and measure the length of its text to get the boundary's offset within the node.
2559 workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
2563 if (/[\r\n]/.test(boundaryNode.data)) {
2565 For the particular case of a boundary within a text node containing rendered line breaks (within a
2566 <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
2569 - Each line break is represented as \r in the text node's data/nodeValue properties
2570 - Each line break is represented as \r\n in the TextRange's 'text' property
2571 - The 'text' property of the TextRange does not contain trailing line breaks
2573 To get round the problem presented by the final fact above, we can use the fact that TextRange's
2574 moveStart() and moveEnd() methods return the actual number of characters moved, which is not
2575 necessarily the same as the number of characters it was instructed to move. The simplest approach is
2576 to use this to store the characters moved when moving both the start and end of the range to the
2577 start of the document body and subtracting the start offset from the end offset (the
2578 "move-negative-gazillion" method). However, this is extremely slow when the document is large and
2579 the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
2580 the end of the document) has the same problem.
2582 Another approach that works is to use moveStart() to move the start boundary of the range up to the
2583 end boundary one character at a time and incrementing a counter with the value returned by the
2584 moveStart() call. However, the check for whether the start boundary has reached the end boundary is
2585 expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
2586 by the location of the range within the document).
2588 The approach used below is a hybrid of the two methods above. It uses the fact that a string
2589 containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
2590 be longer than the text of the TextRange, so the start of the range is moved that length initially
2591 and then a character at a time to make up for any trailing line breaks not contained in the 'text'
2592 property. This has good performance in most situations compared to the previous two methods.
2594 var tempRange = workingRange.duplicate();
2595 var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
2597 offset = tempRange.moveStart("character", rangeLength);
2598 while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
2600 tempRange.moveStart("character", 1);
2603 offset = workingRange.text.length;
2605 boundaryPosition = new DomPosition(boundaryNode, offset);
2608 // If the boundary immediately follows a character data node and this is the end boundary, we should favour
2609 // a position within that, and likewise for a start boundary preceding a character data node
2610 previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
2611 nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
2612 if (nextNode && isCharacterDataNode(nextNode)) {
2613 boundaryPosition = new DomPosition(nextNode, 0);
2614 } else if (previousNode && isCharacterDataNode(previousNode)) {
2615 boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
2617 boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
2622 dom.removeNode(workingNode);
2625 boundaryPosition: boundaryPosition,
2627 nodeIndex: nodeIndex,
2628 containerElement: containerElement
2633 // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
2634 // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
2635 // (http://code.google.com/p/ierange/)
2636 var createBoundaryTextRange = function(boundaryPosition, isStart) {
2637 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
2638 var doc = dom.getDocument(boundaryPosition.node);
2639 var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
2640 var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);