2 * Rangy, a cross-browser JavaScript range and selection library
3 * https://github.com/timdown/rangy
5 * Copyright 2015, Tim Down
6 * Licensed under the MIT license.
8 * Build date: 10 May 2015
11 (function(factory, root) {
12 if (typeof define == "function" && define.amd) {
13 // AMD. Register as an anonymous module.
15 } else if (typeof module != "undefined" && typeof exports == "object") {
16 // Node/CommonJS style
17 module.exports = factory();
19 // No AMD or CommonJS support so we place Rangy in (probably) the global variable
20 root.rangy = factory();
24 var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
26 // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START
27 // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.
28 var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
29 "commonAncestorContainer"];
31 // Minimal set of methods required for DOM Level 2 Range compliance
32 var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
33 "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
34 "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
36 var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
38 // Subset of TextRange's full set of methods that we're interested in
39 var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
40 "setEndPoint", "getBoundingClientRect"];
42 /*----------------------------------------------------------------------------------------------------------------*/
44 // Trio of functions taken from Peter Michaux's article:
45 // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
46 function isHostMethod(o, p) {
48 return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
51 function isHostObject(o, p) {
52 return !!(typeof o[p] == OBJECT && o[p]);
55 function isHostProperty(o, p) {
56 return typeof o[p] != UNDEFINED;
59 // Creates a convenience function to save verbose repeated calls to tests functions
60 function createMultiplePropertyTest(testFunc) {
61 return function(o, props) {
64 if (!testFunc(o, props[i])) {
72 // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
73 var areHostMethods = createMultiplePropertyTest(isHostMethod);
74 var areHostObjects = createMultiplePropertyTest(isHostObject);
75 var areHostProperties = createMultiplePropertyTest(isHostProperty);
77 function isTextRange(range) {
78 return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
81 function getBody(doc) {
82 return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
85 var forEach = [].forEach ?
90 for (var i = 0, len = arr.length; i < len; ++i) {
97 var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);
100 isHostMethod: isHostMethod,
101 isHostObject: isHostObject,
102 isHostProperty: isHostProperty,
103 areHostMethods: areHostMethods,
104 areHostObjects: areHostObjects,
105 areHostProperties: areHostProperties,
106 isTextRange: isTextRange,
114 isBrowser: isBrowser,
122 preferTextRange: false,
123 autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
127 function consoleLog(msg) {
128 if (typeof console != UNDEFINED && isHostMethod(console, "log")) {
133 function alertOrLog(msg, shouldAlert) {
134 if (isBrowser && shouldAlert) {
141 function fail(reason) {
142 api.initialized = true;
143 api.supported = false;
144 alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);
150 alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
155 // Add utility extend() method
157 if ({}.hasOwnProperty) {
158 util.extend = extend = function(obj, props, deep) {
160 for (var i in props) {
161 if (props.hasOwnProperty(i)) {
164 if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
170 // Special case for toString, which does not show up in for...in loops in IE <= 8
171 if (props.hasOwnProperty("toString")) {
172 obj.toString = props.toString;
177 util.createOptions = function(optionsParam, defaults) {
179 extend(options, defaults);
181 extend(options, optionsParam);
186 fail("hasOwnProperty not supported");
189 // Test whether we're in a browser and bail out if not
191 fail("Rangy can only run in a browser");
194 // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not
199 var el = document.createElement("div");
200 el.appendChild(document.createElement("span"));
201 var slice = [].slice;
203 if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
204 toArray = function(arrayLike) {
205 return slice.call(arrayLike, 0);
212 toArray = function(arrayLike) {
214 for (var i = 0, len = arrayLike.length; i < len; ++i) {
215 arr[i] = arrayLike[i];
221 util.toArray = toArray;
224 // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or
225 // normalization of event properties
228 if (isHostMethod(document, "addEventListener")) {
229 addListener = function(obj, eventType, listener) {
230 obj.addEventListener(eventType, listener, false);
232 } else if (isHostMethod(document, "attachEvent")) {
233 addListener = function(obj, eventType, listener) {
234 obj.attachEvent("on" + eventType, listener);
237 fail("Document does not have required addEventListener or attachEvent method");
240 util.addListener = addListener;
243 var initListeners = [];
245 function getErrorDesc(ex) {
246 return ex.message || ex.description || String(ex);
251 if (!isBrowser || api.initialized) {
255 var implementsDomRange = false, implementsTextRange = false;
257 // First, perform basic feature tests
259 if (isHostMethod(document, "createRange")) {
260 testRange = document.createRange();
261 if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
262 implementsDomRange = true;
266 var body = getBody(document);
267 if (!body || body.nodeName.toLowerCase() != "body") {
268 fail("No body element found");
272 if (body && isHostMethod(body, "createTextRange")) {
273 testRange = body.createTextRange();
274 if (isTextRange(testRange)) {
275 implementsTextRange = true;
279 if (!implementsDomRange && !implementsTextRange) {
280 fail("Neither Range nor TextRange are available");
284 api.initialized = true;
286 implementsDomRange: implementsDomRange,
287 implementsTextRange: implementsTextRange
290 // Initialize modules
291 var module, errorMessage;
292 for (var moduleName in modules) {
293 if ( (module = modules[moduleName]) instanceof Module ) {
294 module.init(module, api);
298 // Call init listeners
299 for (var i = 0, len = initListeners.length; i < len; ++i) {
301 initListeners[i](api);
303 errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
304 consoleLog(errorMessage);
309 function deprecationNotice(deprecated, replacement, module) {
311 deprecated += " in module " + module.name;
313 api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +
314 replacement + " instead.");
317 function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {
318 owner[deprecated] = function() {
319 deprecationNotice(deprecated, replacement, module);
320 return owner[replacement].apply(owner, util.toArray(arguments));
324 util.deprecationNotice = deprecationNotice;
325 util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;
327 // Allow external scripts to initialize this library in case it's loaded after the document has loaded
330 // Execute listener immediately if already initialized
331 api.addInitListener = function(listener) {
332 if (api.initialized) {
335 initListeners.push(listener);
339 var shimListeners = [];
341 api.addShimListener = function(listener) {
342 shimListeners.push(listener);
350 for (var i = 0, len = shimListeners.length; i < len; ++i) {
351 shimListeners[i](win);
356 api.shim = api.createMissingNativeApi = shim;
357 createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");
360 function Module(name, dependencies, initializer) {
362 this.dependencies = dependencies;
363 this.initialized = false;
364 this.supported = false;
365 this.initializer = initializer;
370 var requiredModuleNames = this.dependencies || [];
371 for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
372 moduleName = requiredModuleNames[i];
374 requiredModule = modules[moduleName];
375 if (!requiredModule || !(requiredModule instanceof Module)) {
376 throw new Error("required module '" + moduleName + "' not found");
379 requiredModule.init();
381 if (!requiredModule.supported) {
382 throw new Error("required module '" + moduleName + "' not supported");
386 // Now run initializer
387 this.initializer(this);
390 fail: function(reason) {
391 this.initialized = true;
392 this.supported = false;
393 throw new Error(reason);
396 warn: function(msg) {
397 api.warn("Module " + this.name + ": " + msg);
400 deprecationNotice: function(deprecated, replacement) {
401 api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +
402 replacement + " instead");
405 createError: function(msg) {
406 return new Error("Error in Rangy " + this.name + " module: " + msg);
410 function createModule(name, dependencies, initFunc) {
411 var newModule = new Module(name, dependencies, function(module) {
412 if (!module.initialized) {
413 module.initialized = true;
415 initFunc(api, module);
416 module.supported = true;
418 var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
419 consoleLog(errorMessage);
421 consoleLog(ex.stack);
426 modules[name] = newModule;
430 api.createModule = function(name) {
431 // Allow 2 or 3 arguments (second argument is an optional array of dependencies)
432 var initFunc, dependencies;
433 if (arguments.length == 2) {
434 initFunc = arguments[1];
437 initFunc = arguments[2];
438 dependencies = arguments[1];
441 var module = createModule(name, dependencies, initFunc);
443 // Initialize the module immediately if the core is already initialized
444 if (api.initialized && api.supported) {
449 api.createCoreModule = function(name, dependencies, initFunc) {
450 createModule(name, dependencies, initFunc);
453 /*----------------------------------------------------------------------------------------------------------------*/
455 // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately
457 function RangePrototype() {}
458 api.RangePrototype = RangePrototype;
459 api.rangePrototype = new RangePrototype();
461 function SelectionPrototype() {}
462 api.selectionPrototype = new SelectionPrototype();
464 /*----------------------------------------------------------------------------------------------------------------*/
466 // DOM utility methods used by Rangy
467 api.createCoreModule("DomUtil", [], function(api, module) {
468 var UNDEF = "undefined";
470 var getBody = util.getBody;
472 // Perform feature tests
473 if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
474 module.fail("document missing a Node creation method");
477 if (!util.isHostMethod(document, "getElementsByTagName")) {
478 module.fail("document missing getElementsByTagName method");
481 var el = document.createElement("div");
482 if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
483 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
484 module.fail("Incomplete Element implementation");
487 // innerHTML is required for Range's createContextualFragment method
488 if (!util.isHostProperty(el, "innerHTML")) {
489 module.fail("Element is missing innerHTML property");
492 var textNode = document.createTextNode("test");
493 if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
494 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
495 !util.areHostProperties(textNode, ["data"]))) {
496 module.fail("Incomplete Text Node implementation");
499 /*----------------------------------------------------------------------------------------------------------------*/
501 // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
502 // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
503 // contains just the document as a single element and the value searched for is the document.
504 var arrayContains = /*Array.prototype.indexOf ?
506 return arr.indexOf(val) > -1;
512 if (arr[i] === val) {
519 // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
520 function isHtmlNamespace(node) {
522 return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
525 function parentElement(node) {
526 var parent = node.parentNode;
527 return (parent.nodeType == 1) ? parent : null;
530 function getNodeIndex(node) {
532 while( (node = node.previousSibling) ) {
538 function getNodeLength(node) {
539 switch (node.nodeType) {
547 return node.childNodes.length;
551 function getCommonAncestor(node1, node2) {
552 var ancestors = [], n;
553 for (n = node1; n; n = n.parentNode) {
557 for (n = node2; n; n = n.parentNode) {
558 if (arrayContains(ancestors, n)) {
566 function isAncestorOf(ancestor, descendant, selfIsAncestor) {
567 var n = selfIsAncestor ? descendant : descendant.parentNode;
569 if (n === ancestor) {
578 function isOrIsAncestorOf(ancestor, descendant) {
579 return isAncestorOf(ancestor, descendant, true);
582 function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
583 var p, n = selfIsAncestor ? node : node.parentNode;
586 if (p === ancestor) {
594 function isCharacterDataNode(node) {
595 var t = node.nodeType;
596 return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
599 function isTextOrCommentNode(node) {
603 var t = node.nodeType;
604 return t == 3 || t == 8 ; // Text or Comment
607 function insertAfter(node, precedingNode) {
608 var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
610 parent.insertBefore(node, nextNode);
612 parent.appendChild(node);
617 // Note that we cannot use splitText() because it is bugridden in IE 9.
618 function splitDataNode(node, index, positionsToPreserve) {
619 var newNode = node.cloneNode(false);
620 newNode.deleteData(0, index);
621 node.deleteData(index, node.length - index);
622 insertAfter(newNode, node);
624 // Preserve positions
625 if (positionsToPreserve) {
626 for (var i = 0, position; position = positionsToPreserve[i++]; ) {
627 // Handle case where position was inside the portion of node after the split point
628 if (position.node == node && position.offset > index) {
629 position.node = newNode;
630 position.offset -= index;
632 // Handle the case where the position is a node offset within node's parent
633 else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
641 function getDocument(node) {
642 if (node.nodeType == 9) {
644 } else if (typeof node.ownerDocument != UNDEF) {
645 return node.ownerDocument;
646 } else if (typeof node.document != UNDEF) {
647 return node.document;
648 } else if (node.parentNode) {
649 return getDocument(node.parentNode);
651 throw module.createError("getDocument: no document found for node");
655 function getWindow(node) {
656 var doc = getDocument(node);
657 if (typeof doc.defaultView != UNDEF) {
658 return doc.defaultView;
659 } else if (typeof doc.parentWindow != UNDEF) {
660 return doc.parentWindow;
662 throw module.createError("Cannot get a window object for node");
666 function getIframeDocument(iframeEl) {
667 if (typeof iframeEl.contentDocument != UNDEF) {
668 return iframeEl.contentDocument;
669 } else if (typeof iframeEl.contentWindow != UNDEF) {
670 return iframeEl.contentWindow.document;
672 throw module.createError("getIframeDocument: No Document object found for iframe element");
676 function getIframeWindow(iframeEl) {
677 if (typeof iframeEl.contentWindow != UNDEF) {
678 return iframeEl.contentWindow;
679 } else if (typeof iframeEl.contentDocument != UNDEF) {
680 return iframeEl.contentDocument.defaultView;
682 throw module.createError("getIframeWindow: No Window object found for iframe element");
686 // This looks bad. Is it worth it?
687 function isWindow(obj) {
688 return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
691 function getContentDocument(obj, module, methodName) {
698 // Test if a DOM node has been passed and obtain a document object for it if so
699 else if (util.isHostProperty(obj, "nodeType")) {
700 doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
701 getIframeDocument(obj) : getDocument(obj);
704 // Test if the doc parameter appears to be a Window object
705 else if (isWindow(obj)) {
710 throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
716 function getRootContainer(node) {
718 while ( (parent = node.parentNode) ) {
724 function comparePoints(nodeA, offsetA, nodeB, offsetB) {
725 // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
726 var nodeC, root, childA, childB, n;
727 if (nodeA == nodeB) {
728 // Case 1: nodes are the same
729 return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
730 } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
731 // Case 2: node C (container B or an ancestor) is a child node of A
732 return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
733 } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
734 // Case 3: node C (container A or an ancestor) is a child node of B
735 return getNodeIndex(nodeC) < offsetB ? -1 : 1;
737 root = getCommonAncestor(nodeA, nodeB);
739 throw new Error("comparePoints error: nodes have no common ancestor");
742 // Case 4: containers are siblings or descendants of siblings
743 childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
744 childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
746 if (childA === childB) {
747 // This shouldn't be possible
748 throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
754 } else if (n === childB) {
763 /*----------------------------------------------------------------------------------------------------------------*/
765 // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
766 var crashyTextNodes = false;
768 function isBrokenNode(node) {
779 var el = document.createElement("b");
781 var textNode = el.firstChild;
782 el.innerHTML = "<br />";
783 crashyTextNodes = isBrokenNode(textNode);
785 api.features.crashyTextNodes = crashyTextNodes;
788 /*----------------------------------------------------------------------------------------------------------------*/
790 function inspectNode(node) {
794 if (crashyTextNodes && isBrokenNode(node)) {
795 return "[Broken node]";
797 if (isCharacterDataNode(node)) {
798 return '"' + node.data + '"';
800 if (node.nodeType == 1) {
801 var idAttr = node.id ? ' id="' + node.id + '"' : "";
802 return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
804 return node.nodeName;
807 function fragmentFromNodeChildren(node) {
808 var fragment = getDocument(node).createDocumentFragment(), child;
809 while ( (child = node.firstChild) ) {
810 fragment.appendChild(child);
815 var getComputedStyleProperty;
816 if (typeof window.getComputedStyle != UNDEF) {
817 getComputedStyleProperty = function(el, propName) {
818 return getWindow(el).getComputedStyle(el, null)[propName];
820 } else if (typeof document.documentElement.currentStyle != UNDEF) {
821 getComputedStyleProperty = function(el, propName) {
822 return el.currentStyle ? el.currentStyle[propName] : "";
825 module.fail("No means of obtaining computed style properties found");
828 function createTestElement(doc, html, contentEditable) {
829 var body = getBody(doc);
830 var el = doc.createElement("div");
831 el.contentEditable = "" + !!contentEditable;
836 // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
837 var bodyFirstChild = body.firstChild;
838 if (bodyFirstChild) {
839 body.insertBefore(el, bodyFirstChild);
841 body.appendChild(el);
847 function removeNode(node) {
848 return node.parentNode.removeChild(node);
851 function NodeIterator(root) {
856 NodeIterator.prototype = {
859 hasNext: function() {
864 var n = this._current = this._next;
867 child = n.firstChild;
872 while ((n !== this.root) && !(next = n.nextSibling)) {
878 return this._current;
882 this._current = this._next = this.root = null;
886 function createIterator(root) {
887 return new NodeIterator(root);
890 function DomPosition(node, offset) {
892 this.offset = offset;
895 DomPosition.prototype = {
896 equals: function(pos) {
897 return !!pos && this.node === pos.node && this.offset == pos.offset;
900 inspect: function() {
901 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
904 toString: function() {
905 return this.inspect();
909 function DOMException(codeName) {
910 this.code = this[codeName];
911 this.codeName = codeName;
912 this.message = "DOMException: " + this.codeName;
915 DOMException.prototype = {
917 HIERARCHY_REQUEST_ERR: 3,
918 WRONG_DOCUMENT_ERR: 4,
919 NO_MODIFICATION_ALLOWED_ERR: 7,
921 NOT_SUPPORTED_ERR: 9,
922 INVALID_STATE_ERR: 11,
923 INVALID_NODE_TYPE_ERR: 24
926 DOMException.prototype.toString = function() {
931 arrayContains: arrayContains,
932 isHtmlNamespace: isHtmlNamespace,
933 parentElement: parentElement,
934 getNodeIndex: getNodeIndex,
935 getNodeLength: getNodeLength,
936 getCommonAncestor: getCommonAncestor,
937 isAncestorOf: isAncestorOf,
938 isOrIsAncestorOf: isOrIsAncestorOf,
939 getClosestAncestorIn: getClosestAncestorIn,
940 isCharacterDataNode: isCharacterDataNode,
941 isTextOrCommentNode: isTextOrCommentNode,
942 insertAfter: insertAfter,
943 splitDataNode: splitDataNode,
944 getDocument: getDocument,
945 getWindow: getWindow,
946 getIframeWindow: getIframeWindow,
947 getIframeDocument: getIframeDocument,
950 getContentDocument: getContentDocument,
951 getRootContainer: getRootContainer,
952 comparePoints: comparePoints,
953 isBrokenNode: isBrokenNode,
954 inspectNode: inspectNode,
955 getComputedStyleProperty: getComputedStyleProperty,
956 createTestElement: createTestElement,
957 removeNode: removeNode,
958 fragmentFromNodeChildren: fragmentFromNodeChildren,
959 createIterator: createIterator,
960 DomPosition: DomPosition
963 api.DOMException = DOMException;
966 /*----------------------------------------------------------------------------------------------------------------*/
968 // Pure JavaScript implementation of DOM Range
969 api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
972 var DomPosition = dom.DomPosition;
973 var DOMException = api.DOMException;
975 var isCharacterDataNode = dom.isCharacterDataNode;
976 var getNodeIndex = dom.getNodeIndex;
977 var isOrIsAncestorOf = dom.isOrIsAncestorOf;
978 var getDocument = dom.getDocument;
979 var comparePoints = dom.comparePoints;
980 var splitDataNode = dom.splitDataNode;
981 var getClosestAncestorIn = dom.getClosestAncestorIn;
982 var getNodeLength = dom.getNodeLength;
983 var arrayContains = dom.arrayContains;
984 var getRootContainer = dom.getRootContainer;
985 var crashyTextNodes = api.features.crashyTextNodes;
987 var removeNode = dom.removeNode;
989 /*----------------------------------------------------------------------------------------------------------------*/
993 function isNonTextPartiallySelected(node, range) {
994 return (node.nodeType != 3) &&
995 (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
998 function getRangeDocument(range) {
999 return range.document || getDocument(range.startContainer);
1002 function getRangeRoot(range) {
1003 return getRootContainer(range.startContainer);
1006 function getBoundaryBeforeNode(node) {
1007 return new DomPosition(node.parentNode, getNodeIndex(node));
1010 function getBoundaryAfterNode(node) {
1011 return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
1014 function insertNodeAtPosition(node, n, o) {
1015 var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
1016 if (isCharacterDataNode(n)) {
1017 if (o == n.length) {
1018 dom.insertAfter(node, n);
1020 n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
1022 } else if (o >= n.childNodes.length) {
1023 n.appendChild(node);
1025 n.insertBefore(node, n.childNodes[o]);
1027 return firstNodeInserted;
1030 function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
1031 assertRangeValid(rangeA);
1032 assertRangeValid(rangeB);
1034 if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
1035 throw new DOMException("WRONG_DOCUMENT_ERR");
1038 var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
1039 endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
1041 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1044 function cloneSubtree(iterator) {
1045 var partiallySelected;
1046 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1047 partiallySelected = iterator.isPartiallySelectedSubtree();
1048 node = node.cloneNode(!partiallySelected);
1049 if (partiallySelected) {
1050 subIterator = iterator.getSubtreeIterator();
1051 node.appendChild(cloneSubtree(subIterator));
1052 subIterator.detach();
1055 if (node.nodeType == 10) { // DocumentType
1056 throw new DOMException("HIERARCHY_REQUEST_ERR");
1058 frag.appendChild(node);
1063 function iterateSubtree(rangeIterator, func, iteratorState) {
1065 iteratorState = iteratorState || { stop: false };
1066 for (var node, subRangeIterator; node = rangeIterator.next(); ) {
1067 if (rangeIterator.isPartiallySelectedSubtree()) {
1068 if (func(node) === false) {
1069 iteratorState.stop = true;
1072 // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
1073 // the node selected by the Range.
1074 subRangeIterator = rangeIterator.getSubtreeIterator();
1075 iterateSubtree(subRangeIterator, func, iteratorState);
1076 subRangeIterator.detach();
1077 if (iteratorState.stop) {
1082 // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
1084 it = dom.createIterator(node);
1085 while ( (n = it.next()) ) {
1086 if (func(n) === false) {
1087 iteratorState.stop = true;
1095 function deleteSubtree(iterator) {
1097 while (iterator.next()) {
1098 if (iterator.isPartiallySelectedSubtree()) {
1099 subIterator = iterator.getSubtreeIterator();
1100 deleteSubtree(subIterator);
1101 subIterator.detach();
1108 function extractSubtree(iterator) {
1109 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1111 if (iterator.isPartiallySelectedSubtree()) {
1112 node = node.cloneNode(false);
1113 subIterator = iterator.getSubtreeIterator();
1114 node.appendChild(extractSubtree(subIterator));
1115 subIterator.detach();
1119 if (node.nodeType == 10) { // DocumentType
1120 throw new DOMException("HIERARCHY_REQUEST_ERR");
1122 frag.appendChild(node);
1127 function getNodesInRange(range, nodeTypes, filter) {
1128 var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
1129 var filterExists = !!filter;
1130 if (filterNodeTypes) {
1131 regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
1135 iterateSubtree(new RangeIterator(range, false), function(node) {
1136 if (filterNodeTypes && !regex.test(node.nodeType)) {
1139 if (filterExists && !filter(node)) {
1142 // Don't include a boundary container if it is a character data node and the range does not contain any
1143 // of its character data. See issue 190.
1144 var sc = range.startContainer;
1145 if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
1149 var ec = range.endContainer;
1150 if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
1159 function inspect(range) {
1160 var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
1161 return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
1162 dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
1165 /*----------------------------------------------------------------------------------------------------------------*/
1167 // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
1169 function RangeIterator(range, clonePartiallySelectedTextNodes) {
1171 this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
1174 if (!range.collapsed) {
1175 this.sc = range.startContainer;
1176 this.so = range.startOffset;
1177 this.ec = range.endContainer;
1178 this.eo = range.endOffset;
1179 var root = range.commonAncestorContainer;
1181 if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
1182 this.isSingleCharacterDataNode = true;
1183 this._first = this._last = this._next = this.sc;
1185 this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
1186 this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
1187 this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
1188 this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
1193 RangeIterator.prototype = {
1198 isSingleCharacterDataNode: false,
1201 this._current = null;
1202 this._next = this._first;
1205 hasNext: function() {
1206 return !!this._next;
1210 // Move to next node
1211 var current = this._current = this._next;
1213 this._next = (current !== this._last) ? current.nextSibling : null;
1215 // Check for partially selected text nodes
1216 if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
1217 if (current === this.ec) {
1218 (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
1220 if (this._current === this.sc) {
1221 (current = current.cloneNode(true)).deleteData(0, this.so);
1229 remove: function() {
1230 var current = this._current, start, end;
1232 if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
1233 start = (current === this.sc) ? this.so : 0;
1234 end = (current === this.ec) ? this.eo : current.length;
1236 current.deleteData(start, end - start);
1239 if (current.parentNode) {
1240 removeNode(current);
1246 // Checks if the current node is partially selected
1247 isPartiallySelectedSubtree: function() {
1248 var current = this._current;
1249 return isNonTextPartiallySelected(current, this.range);
1252 getSubtreeIterator: function() {
1254 if (this.isSingleCharacterDataNode) {
1255 subRange = this.range.cloneRange();
1256 subRange.collapse(false);
1258 subRange = new Range(getRangeDocument(this.range));
1259 var current = this._current;
1260 var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
1262 if (isOrIsAncestorOf(current, this.sc)) {
1263 startContainer = this.sc;
1264 startOffset = this.so;
1266 if (isOrIsAncestorOf(current, this.ec)) {
1267 endContainer = this.ec;
1268 endOffset = this.eo;
1271 updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
1273 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
1276 detach: function() {
1277 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
1281 /*----------------------------------------------------------------------------------------------------------------*/
1283 var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
1284 var rootContainerNodeTypes = [2, 9, 11];
1285 var readonlyNodeTypes = [5, 6, 10, 12];
1286 var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
1287 var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
1289 function createAncestorFinder(nodeTypes) {
1290 return function(node, selfIsAncestor) {
1291 var t, n = selfIsAncestor ? node : node.parentNode;
1294 if (arrayContains(nodeTypes, t)) {
1303 var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1304 var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1305 var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
1307 function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1308 if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1309 throw new DOMException("INVALID_NODE_TYPE_ERR");
1313 function assertValidNodeType(node, invalidTypes) {
1314 if (!arrayContains(invalidTypes, node.nodeType)) {
1315 throw new DOMException("INVALID_NODE_TYPE_ERR");
1319 function assertValidOffset(node, offset) {
1320 if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1321 throw new DOMException("INDEX_SIZE_ERR");
1325 function assertSameDocumentOrFragment(node1, node2) {
1326 if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1327 throw new DOMException("WRONG_DOCUMENT_ERR");
1331 function assertNodeNotReadOnly(node) {
1332 if (getReadonlyAncestor(node, true)) {
1333 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1337 function assertNode(node, codeName) {
1339 throw new DOMException(codeName);
1343 function isValidOffset(node, offset) {
1344 return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
1347 function isRangeValid(range) {
1348 return (!!range.startContainer && !!range.endContainer &&
1349 !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
1350 getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
1351 isValidOffset(range.startContainer, range.startOffset) &&
1352 isValidOffset(range.endContainer, range.endOffset));
1355 function assertRangeValid(range) {
1356 if (!isRangeValid(range)) {
1357 throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
1361 /*----------------------------------------------------------------------------------------------------------------*/
1363 // Test the browser's innerHTML support to decide how to implement createContextualFragment
1364 var styleEl = document.createElement("style");
1365 var htmlParsingConforms = false;
1367 styleEl.innerHTML = "<b>x</b>";
1368 htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
1373 api.features.htmlParsingConforms = htmlParsingConforms;
1375 var createContextualFragment = htmlParsingConforms ?
1377 // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
1378 // discussion and base code for this implementation at issue 67.
1379 // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
1380 // Thanks to Aleks Williams.
1381 function(fragmentStr) {
1382 // "Let node the context object's start's node."
1383 var node = this.startContainer;
1384 var doc = getDocument(node);
1386 // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1387 // exception and abort these steps."
1389 throw new DOMException("INVALID_STATE_ERR");
1392 // "Let element be as follows, depending on node's interface:"
1393 // Document, Document Fragment: null
1397 if (node.nodeType == 1) {
1400 // "Text, Comment: node's parentElement"
1401 } else if (isCharacterDataNode(node)) {
1402 el = dom.parentElement(node);
1405 // "If either element is null or element's ownerDocument is an HTML document
1406 // and element's local name is "html" and element's namespace is the HTML
1408 if (el === null || (
1409 el.nodeName == "HTML" &&
1410 dom.isHtmlNamespace(getDocument(el).documentElement) &&
1411 dom.isHtmlNamespace(el)
1414 // "let element be a new Element with "body" as its local name and the HTML
1415 // namespace as its namespace.""
1416 el = doc.createElement("body");
1418 el = el.cloneNode(false);
1421 // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
1422 // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
1423 // "In either case, the algorithm must be invoked with fragment as the input
1424 // and element as the context element."
1425 el.innerHTML = fragmentStr;
1427 // "If this raises an exception, then abort these steps. Otherwise, let new
1428 // children be the nodes returned."
1430 // "Let fragment be a new DocumentFragment."
1431 // "Append all new children to fragment."
1432 // "Return fragment."
1433 return dom.fragmentFromNodeChildren(el);
1436 // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
1437 // previous versions of Rangy used (with the exception of using a body element rather than a div)
1438 function(fragmentStr) {
1439 var doc = getRangeDocument(this);
1440 var el = doc.createElement("body");
1441 el.innerHTML = fragmentStr;
1443 return dom.fragmentFromNodeChildren(el);
1446 function splitRangeBoundaries(range, positionsToPreserve) {
1447 assertRangeValid(range);
1449 var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
1450 var startEndSame = (sc === ec);
1452 if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1453 splitDataNode(ec, eo, positionsToPreserve);
1456 if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1457 sc = splitDataNode(sc, so, positionsToPreserve);
1461 } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
1466 range.setStartAndEnd(sc, so, ec, eo);
1469 function rangeToHtml(range) {
1470 assertRangeValid(range);
1471 var container = range.commonAncestorContainer.parentNode.cloneNode(false);
1472 container.appendChild( range.cloneContents() );
1473 return container.innerHTML;
1476 /*----------------------------------------------------------------------------------------------------------------*/
1478 var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1479 "commonAncestorContainer"];
1481 var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
1482 var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
1484 util.extend(api.rangePrototype, {
1485 compareBoundaryPoints: function(how, range) {
1486 assertRangeValid(this);
1487 assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1489 var nodeA, offsetA, nodeB, offsetB;
1490 var prefixA = (how == e2s || how == s2s) ? "start" : "end";
1491 var prefixB = (how == s2e || how == s2s) ? "start" : "end";
1492 nodeA = this[prefixA + "Container"];
1493 offsetA = this[prefixA + "Offset"];
1494 nodeB = range[prefixB + "Container"];
1495 offsetB = range[prefixB + "Offset"];
1496 return comparePoints(nodeA, offsetA, nodeB, offsetB);
1499 insertNode: function(node) {
1500 assertRangeValid(this);
1501 assertValidNodeType(node, insertableNodeTypes);
1502 assertNodeNotReadOnly(this.startContainer);
1504 if (isOrIsAncestorOf(node, this.startContainer)) {
1505 throw new DOMException("HIERARCHY_REQUEST_ERR");
1508 // No check for whether the container of the start of the Range is of a type that does not allow
1509 // children of the type of node: the browser's DOM implementation should do this for us when we attempt
1512 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1513 this.setStartBefore(firstNodeInserted);
1516 cloneContents: function() {
1517 assertRangeValid(this);
1520 if (this.collapsed) {
1521 return getRangeDocument(this).createDocumentFragment();
1523 if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
1524 clone = this.startContainer.cloneNode(true);
1525 clone.data = clone.data.slice(this.startOffset, this.endOffset);
1526 frag = getRangeDocument(this).createDocumentFragment();
1527 frag.appendChild(clone);
1530 var iterator = new RangeIterator(this, true);
1531 clone = cloneSubtree(iterator);
1538 canSurroundContents: function() {
1539 assertRangeValid(this);
1540 assertNodeNotReadOnly(this.startContainer);
1541 assertNodeNotReadOnly(this.endContainer);
1543 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1544 // no non-text nodes.
1545 var iterator = new RangeIterator(this, true);
1546 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1547 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1549 return !boundariesInvalid;
1552 surroundContents: function(node) {
1553 assertValidNodeType(node, surroundNodeTypes);
1555 if (!this.canSurroundContents()) {
1556 throw new DOMException("INVALID_STATE_ERR");
1559 // Extract the contents
1560 var content = this.extractContents();
1562 // Clear the children of the node
1563 if (node.hasChildNodes()) {
1564 while (node.lastChild) {
1565 node.removeChild(node.lastChild);
1569 // Insert the new node and add the extracted contents
1570 insertNodeAtPosition(node, this.startContainer, this.startOffset);
1571 node.appendChild(content);
1573 this.selectNode(node);
1576 cloneRange: function() {
1577 assertRangeValid(this);
1578 var range = new Range(getRangeDocument(this));
1579 var i = rangeProperties.length, prop;
1581 prop = rangeProperties[i];
1582 range[prop] = this[prop];
1587 toString: function() {
1588 assertRangeValid(this);
1589 var sc = this.startContainer;
1590 if (sc === this.endContainer && isCharacterDataNode(sc)) {
1591 return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
1593 var textParts = [], iterator = new RangeIterator(this, true);
1594 iterateSubtree(iterator, function(node) {
1595 // Accept only text or CDATA nodes, not comments
1596 if (node.nodeType == 3 || node.nodeType == 4) {
1597 textParts.push(node.data);
1601 return textParts.join("");
1605 // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
1606 // been removed from Mozilla.
1608 compareNode: function(node) {
1609 assertRangeValid(this);
1611 var parent = node.parentNode;
1612 var nodeIndex = getNodeIndex(node);
1615 throw new DOMException("NOT_FOUND_ERR");
1618 var startComparison = this.comparePoint(parent, nodeIndex),
1619 endComparison = this.comparePoint(parent, nodeIndex + 1);
1621 if (startComparison < 0) { // Node starts before
1622 return (endComparison > 0) ? n_b_a : n_b;
1624 return (endComparison > 0) ? n_a : n_i;
1628 comparePoint: function(node, offset) {
1629 assertRangeValid(this);
1630 assertNode(node, "HIERARCHY_REQUEST_ERR");
1631 assertSameDocumentOrFragment(node, this.startContainer);
1633 if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
1635 } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
1641 createContextualFragment: createContextualFragment,
1643 toHtml: function() {
1644 return rangeToHtml(this);
1647 // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
1648 // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
1649 intersectsNode: function(node, touchingIsIntersecting) {
1650 assertRangeValid(this);
1651 if (getRootContainer(node) != getRangeRoot(this)) {
1655 var parent = node.parentNode, offset = getNodeIndex(node);
1660 var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
1661 endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
1663 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1666 isPointInRange: function(node, offset) {
1667 assertRangeValid(this);
1668 assertNode(node, "HIERARCHY_REQUEST_ERR");
1669 assertSameDocumentOrFragment(node, this.startContainer);
1671 return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
1672 (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
1675 // The methods below are non-standard and invented by me.
1677 // Sharing a boundary start-to-end or end-to-start does not count as intersection.
1678 intersectsRange: function(range) {
1679 return rangesIntersect(this, range, false);
1682 // Sharing a boundary start-to-end or end-to-start does count as intersection.
1683 intersectsOrTouchesRange: function(range) {
1684 return rangesIntersect(this, range, true);
1687 intersection: function(range) {
1688 if (this.intersectsRange(range)) {
1689 var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
1690 endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
1692 var intersectionRange = this.cloneRange();
1693 if (startComparison == -1) {
1694 intersectionRange.setStart(range.startContainer, range.startOffset);
1696 if (endComparison == 1) {
1697 intersectionRange.setEnd(range.endContainer, range.endOffset);
1699 return intersectionRange;
1704 union: function(range) {
1705 if (this.intersectsOrTouchesRange(range)) {
1706 var unionRange = this.cloneRange();
1707 if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
1708 unionRange.setStart(range.startContainer, range.startOffset);
1710 if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
1711 unionRange.setEnd(range.endContainer, range.endOffset);
1715 throw new DOMException("Ranges do not intersect");
1719 containsNode: function(node, allowPartial) {
1721 return this.intersectsNode(node, false);
1723 return this.compareNode(node) == n_i;
1727 containsNodeContents: function(node) {
1728 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
1731 containsRange: function(range) {
1732 var intersection = this.intersection(range);
1733 return intersection !== null && range.equals(intersection);
1736 containsNodeText: function(node) {
1737 var nodeRange = this.cloneRange();
1738 nodeRange.selectNode(node);
1739 var textNodes = nodeRange.getNodes([3]);
1740 if (textNodes.length > 0) {
1741 nodeRange.setStart(textNodes[0], 0);
1742 var lastTextNode = textNodes.pop();
1743 nodeRange.setEnd(lastTextNode, lastTextNode.length);
1744 return this.containsRange(nodeRange);
1746 return this.containsNodeContents(node);
1750 getNodes: function(nodeTypes, filter) {
1751 assertRangeValid(this);
1752 return getNodesInRange(this, nodeTypes, filter);
1755 getDocument: function() {
1756 return getRangeDocument(this);
1759 collapseBefore: function(node) {
1760 this.setEndBefore(node);
1761 this.collapse(false);
1764 collapseAfter: function(node) {
1765 this.setStartAfter(node);
1766 this.collapse(true);
1769 getBookmark: function(containerNode) {
1770 var doc = getRangeDocument(this);
1771 var preSelectionRange = api.createRange(doc);
1772 containerNode = containerNode || dom.getBody(doc);
1773 preSelectionRange.selectNodeContents(containerNode);
1774 var range = this.intersection(preSelectionRange);
1775 var start = 0, end = 0;
1777 preSelectionRange.setEnd(range.startContainer, range.startOffset);
1778 start = preSelectionRange.toString().length;
1779 end = start + range.toString().length;
1785 containerNode: containerNode
1789 moveToBookmark: function(bookmark) {
1790 var containerNode = bookmark.containerNode;
1792 this.setStart(containerNode, 0);
1793 this.collapse(true);
1794 var nodeStack = [containerNode], node, foundStart = false, stop = false;
1795 var nextCharIndex, i, childNodes;
1797 while (!stop && (node = nodeStack.pop())) {
1798 if (node.nodeType == 3) {
1799 nextCharIndex = charIndex + node.length;
1800 if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
1801 this.setStart(node, bookmark.start - charIndex);
1804 if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
1805 this.setEnd(node, bookmark.end - charIndex);
1808 charIndex = nextCharIndex;
1810 childNodes = node.childNodes;
1811 i = childNodes.length;
1813 nodeStack.push(childNodes[i]);
1819 getName: function() {
1823 equals: function(range) {
1824 return Range.rangesEqual(this, range);
1827 isValid: function() {
1828 return isRangeValid(this);
1831 inspect: function() {
1832 return inspect(this);
1835 detach: function() {
1836 // In DOM4, detach() is now a no-op.
1840 function copyComparisonConstantsToObject(obj) {
1841 obj.START_TO_START = s2s;
1842 obj.START_TO_END = s2e;
1843 obj.END_TO_END = e2e;
1844 obj.END_TO_START = e2s;
1846 obj.NODE_BEFORE = n_b;
1847 obj.NODE_AFTER = n_a;
1848 obj.NODE_BEFORE_AND_AFTER = n_b_a;
1849 obj.NODE_INSIDE = n_i;
1852 function copyComparisonConstants(constructor) {
1853 copyComparisonConstantsToObject(constructor);
1854 copyComparisonConstantsToObject(constructor.prototype);
1857 function createRangeContentRemover(remover, boundaryUpdater) {
1859 assertRangeValid(this);
1861 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
1863 var iterator = new RangeIterator(this, true);
1865 // Work out where to position the range after content removal
1868 node = getClosestAncestorIn(sc, root, true);
1869 boundary = getBoundaryAfterNode(node);
1871 so = boundary.offset;
1874 // Check none of the range is read-only
1875 iterateSubtree(iterator, assertNodeNotReadOnly);
1879 // Remove the content
1880 var returnValue = remover(iterator);
1883 // Move to the new position
1884 boundaryUpdater(this, sc, so, sc, so);
1890 function createPrototypeRange(constructor, boundaryUpdater) {
1891 function createBeforeAfterNodeSetter(isBefore, isStart) {
1892 return function(node) {
1893 assertValidNodeType(node, beforeAfterNodeTypes);
1894 assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
1896 var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
1897 (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
1901 function setRangeStart(range, node, offset) {
1902 var ec = range.endContainer, eo = range.endOffset;
1903 if (node !== range.startContainer || offset !== range.startOffset) {
1904 // Check the root containers of the range and the new boundary, and also check whether the new boundary
1905 // is after the current end. In either case, collapse the range to the new position
1906 if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
1910 boundaryUpdater(range, node, offset, ec, eo);
1914 function setRangeEnd(range, node, offset) {
1915 var sc = range.startContainer, so = range.startOffset;
1916 if (node !== range.endContainer || offset !== range.endOffset) {
1917 // Check the root containers of the range and the new boundary, and also check whether the new boundary
1918 // is after the current end. In either case, collapse the range to the new position
1919 if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
1923 boundaryUpdater(range, sc, so, node, offset);
1927 // Set up inheritance
1928 var F = function() {};
1929 F.prototype = api.rangePrototype;
1930 constructor.prototype = new F();
1932 util.extend(constructor.prototype, {
1933 setStart: function(node, offset) {
1934 assertNoDocTypeNotationEntityAncestor(node, true);
1935 assertValidOffset(node, offset);
1937 setRangeStart(this, node, offset);
1940 setEnd: function(node, offset) {
1941 assertNoDocTypeNotationEntityAncestor(node, true);
1942 assertValidOffset(node, offset);
1944 setRangeEnd(this, node, offset);
1948 * Convenience method to set a range's start and end boundaries. Overloaded as follows:
1949 * - Two parameters (node, offset) creates a collapsed range at that position
1950 * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
1951 * startOffset and ending at endOffset
1952 * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
1953 * startNode and ending at endOffset in endNode
1955 setStartAndEnd: function() {
1956 var args = arguments;
1957 var sc = args[0], so = args[1], ec = sc, eo = so;
1959 switch (args.length) {
1969 boundaryUpdater(this, sc, so, ec, eo);
1972 setBoundary: function(node, offset, isStart) {
1973 this["set" + (isStart ? "Start" : "End")](node, offset);
1976 setStartBefore: createBeforeAfterNodeSetter(true, true),
1977 setStartAfter: createBeforeAfterNodeSetter(false, true),
1978 setEndBefore: createBeforeAfterNodeSetter(true, false),
1979 setEndAfter: createBeforeAfterNodeSetter(false, false),
1981 collapse: function(isStart) {
1982 assertRangeValid(this);
1984 boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
1986 boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
1990 selectNodeContents: function(node) {
1991 assertNoDocTypeNotationEntityAncestor(node, true);
1993 boundaryUpdater(this, node, 0, node, getNodeLength(node));
1996 selectNode: function(node) {
1997 assertNoDocTypeNotationEntityAncestor(node, false);
1998 assertValidNodeType(node, beforeAfterNodeTypes);
2000 var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
2001 boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
2004 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
2006 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
2008 canSurroundContents: function() {
2009 assertRangeValid(this);
2010 assertNodeNotReadOnly(this.startContainer);
2011 assertNodeNotReadOnly(this.endContainer);
2013 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
2014 // no non-text nodes.
2015 var iterator = new RangeIterator(this, true);
2016 var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
2017 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
2019 return !boundariesInvalid;
2022 splitBoundaries: function() {
2023 splitRangeBoundaries(this);
2026 splitBoundariesPreservingPositions: function(positionsToPreserve) {
2027 splitRangeBoundaries(this, positionsToPreserve);
2030 normalizeBoundaries: function() {
2031 assertRangeValid(this);
2033 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
2035 var mergeForward = function(node) {
2036 var sibling = node.nextSibling;
2037 if (sibling && sibling.nodeType == node.nodeType) {
2040 node.appendData(sibling.data);
2041 removeNode(sibling);
2045 var mergeBackward = function(node) {
2046 var sibling = node.previousSibling;
2047 if (sibling && sibling.nodeType == node.nodeType) {
2049 var nodeLength = node.length;
2050 so = sibling.length;
2051 node.insertData(0, sibling.data);
2052 removeNode(sibling);
2056 } else if (ec == node.parentNode) {
2057 var nodeIndex = getNodeIndex(node);
2058 if (eo == nodeIndex) {
2061 } else if (eo > nodeIndex) {
2068 var normalizeStart = true;
2071 if (isCharacterDataNode(ec)) {
2072 if (eo == ec.length) {
2074 } else if (eo == 0) {
2075 sibling = ec.previousSibling;
2076 if (sibling && sibling.nodeType == ec.nodeType) {
2077 eo = sibling.length;
2079 normalizeStart = false;
2081 sibling.appendData(ec.data);
2088 var endNode = ec.childNodes[eo - 1];
2089 if (endNode && isCharacterDataNode(endNode)) {
2090 mergeForward(endNode);
2093 normalizeStart = !this.collapsed;
2096 if (normalizeStart) {
2097 if (isCharacterDataNode(sc)) {
2100 } else if (so == sc.length) {
2101 sibling = sc.nextSibling;
2102 if (sibling && sibling.nodeType == sc.nodeType) {
2103 if (ec == sibling) {
2107 sc.appendData(sibling.data);
2108 removeNode(sibling);
2112 if (so < sc.childNodes.length) {
2113 var startNode = sc.childNodes[so];
2114 if (startNode && isCharacterDataNode(startNode)) {
2115 mergeBackward(startNode);
2124 boundaryUpdater(this, sc, so, ec, eo);
2127 collapseToPoint: function(node, offset) {
2128 assertNoDocTypeNotationEntityAncestor(node, true);
2129 assertValidOffset(node, offset);
2130 this.setStartAndEnd(node, offset);
2134 copyComparisonConstants(constructor);
2137 /*----------------------------------------------------------------------------------------------------------------*/
2139 // Updates commonAncestorContainer and collapsed after boundary change
2140 function updateCollapsedAndCommonAncestor(range) {
2141 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2142 range.commonAncestorContainer = range.collapsed ?
2143 range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
2146 function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
2147 range.startContainer = startContainer;
2148 range.startOffset = startOffset;
2149 range.endContainer = endContainer;
2150 range.endOffset = endOffset;
2151 range.document = dom.getDocument(startContainer);
2153 updateCollapsedAndCommonAncestor(range);
2156 function Range(doc) {
2157 this.startContainer = doc;
2158 this.startOffset = 0;
2159 this.endContainer = doc;
2161 this.document = doc;
2162 updateCollapsedAndCommonAncestor(this);
2165 createPrototypeRange(Range, updateBoundaries);
2167 util.extend(Range, {
2168 rangeProperties: rangeProperties,
2169 RangeIterator: RangeIterator,
2170 copyComparisonConstants: copyComparisonConstants,
2171 createPrototypeRange: createPrototypeRange,
2173 toHtml: rangeToHtml,
2174 getRangeDocument: getRangeDocument,
2175 rangesEqual: function(r1, r2) {
2176 return r1.startContainer === r2.startContainer &&
2177 r1.startOffset === r2.startOffset &&
2178 r1.endContainer === r2.endContainer &&
2179 r1.endOffset === r2.endOffset;
2183 api.DomRange = Range;
2186 /*----------------------------------------------------------------------------------------------------------------*/
2188 // Wrappers for the browser's native DOM Range and/or TextRange implementation
2189 api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
2190 var WrappedRange, WrappedTextRange;
2192 var util = api.util;
2193 var DomPosition = dom.DomPosition;
2194 var DomRange = api.DomRange;
2195 var getBody = dom.getBody;
2196 var getContentDocument = dom.getContentDocument;
2197 var isCharacterDataNode = dom.isCharacterDataNode;
2200 /*----------------------------------------------------------------------------------------------------------------*/
2202 if (api.features.implementsDomRange) {
2203 // This is a wrapper around the browser's native DOM Range. It has two aims:
2204 // - Provide workarounds for specific browser bugs
2205 // - provide convenient extensions, which are inherited from Rangy's DomRange
2209 var rangeProperties = DomRange.rangeProperties;
2211 function updateRangeProperties(range) {
2212 var i = rangeProperties.length, prop;
2214 prop = rangeProperties[i];
2215 range[prop] = range.nativeRange[prop];
2217 // Fix for broken collapsed property in IE 9.
2218 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2221 function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
2222 var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
2223 var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
2224 var nativeRangeDifferent = !range.equals(range.nativeRange);
2226 // Always set both boundaries for the benefit of IE9 (see issue 35)
2227 if (startMoved || endMoved || nativeRangeDifferent) {
2228 range.setEnd(endContainer, endOffset);
2229 range.setStart(startContainer, startOffset);
2233 var createBeforeAfterNodeSetter;
2235 WrappedRange = function(range) {
2237 throw module.createError("WrappedRange: Range must be specified");
2239 this.nativeRange = range;
2240 updateRangeProperties(this);
2243 DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
2245 rangeProto = WrappedRange.prototype;
2247 rangeProto.selectNode = function(node) {
2248 this.nativeRange.selectNode(node);
2249 updateRangeProperties(this);
2252 rangeProto.cloneContents = function() {
2253 return this.nativeRange.cloneContents();
2256 // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
2257 // insertNode() is never delegated to the native range.
2259 rangeProto.surroundContents = function(node) {
2260 this.nativeRange.surroundContents(node);
2261 updateRangeProperties(this);
2264 rangeProto.collapse = function(isStart) {
2265 this.nativeRange.collapse(isStart);
2266 updateRangeProperties(this);
2269 rangeProto.cloneRange = function() {
2270 return new WrappedRange(this.nativeRange.cloneRange());
2273 rangeProto.refresh = function() {
2274 updateRangeProperties(this);
2277 rangeProto.toString = function() {
2278 return this.nativeRange.toString();
2281 // Create test range and node for feature detection
2283 var testTextNode = document.createTextNode("test");
2284 getBody(document).appendChild(testTextNode);
2285 var range = document.createRange();
2287 /*--------------------------------------------------------------------------------------------------------*/
2289 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
2292 range.setStart(testTextNode, 0);
2293 range.setEnd(testTextNode, 0);
2296 range.setStart(testTextNode, 1);
2298 rangeProto.setStart = function(node, offset) {
2299 this.nativeRange.setStart(node, offset);
2300 updateRangeProperties(this);
2303 rangeProto.setEnd = function(node, offset) {
2304 this.nativeRange.setEnd(node, offset);
2305 updateRangeProperties(this);
2308 createBeforeAfterNodeSetter = function(name) {
2309 return function(node) {
2310 this.nativeRange[name](node);
2311 updateRangeProperties(this);
2317 rangeProto.setStart = function(node, offset) {
2319 this.nativeRange.setStart(node, offset);
2321 this.nativeRange.setEnd(node, offset);
2322 this.nativeRange.setStart(node, offset);
2324 updateRangeProperties(this);
2327 rangeProto.setEnd = function(node, offset) {
2329 this.nativeRange.setEnd(node, offset);
2331 this.nativeRange.setStart(node, offset);
2332 this.nativeRange.setEnd(node, offset);
2334 updateRangeProperties(this);
2337 createBeforeAfterNodeSetter = function(name, oppositeName) {
2338 return function(node) {
2340 this.nativeRange[name](node);
2342 this.nativeRange[oppositeName](node);
2343 this.nativeRange[name](node);
2345 updateRangeProperties(this);
2350 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
2351 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
2352 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
2353 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
2355 /*--------------------------------------------------------------------------------------------------------*/
2357 // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
2358 // whether the native implementation can be trusted
2359 rangeProto.selectNodeContents = function(node) {
2360 this.setStartAndEnd(node, 0, dom.getNodeLength(node));
2363 /*--------------------------------------------------------------------------------------------------------*/
2365 // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
2366 // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
2368 range.selectNodeContents(testTextNode);
2369 range.setEnd(testTextNode, 3);
2371 var range2 = document.createRange();
2372 range2.selectNodeContents(testTextNode);
2373 range2.setEnd(testTextNode, 4);
2374 range2.setStart(testTextNode, 2);
2376 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
2377 range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
2378 // This is the wrong way round, so correct for it
2380 rangeProto.compareBoundaryPoints = function(type, range) {
2381 range = range.nativeRange || range;
2382 if (type == range.START_TO_END) {
2383 type = range.END_TO_START;
2384 } else if (type == range.END_TO_START) {
2385 type = range.START_TO_END;
2387 return this.nativeRange.compareBoundaryPoints(type, range);
2390 rangeProto.compareBoundaryPoints = function(type, range) {
2391 return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
2395 /*--------------------------------------------------------------------------------------------------------*/
2397 // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
2399 var el = document.createElement("div");
2400 el.innerHTML = "123";
2401 var textNode = el.firstChild;
2402 var body = getBody(document);
2403 body.appendChild(el);
2405 range.setStart(textNode, 1);
2406 range.setEnd(textNode, 2);
2407 range.deleteContents();
2409 if (textNode.data == "13") {
2410 // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
2411 // extractContents()
2412 rangeProto.deleteContents = function() {
2413 this.nativeRange.deleteContents();
2414 updateRangeProperties(this);
2417 rangeProto.extractContents = function() {
2418 var frag = this.nativeRange.extractContents();
2419 updateRangeProperties(this);
2425 body.removeChild(el);
2428 /*--------------------------------------------------------------------------------------------------------*/
2430 // Test for existence of createContextualFragment and delegate to it if it exists
2431 if (util.isHostMethod(range, "createContextualFragment")) {
2432 rangeProto.createContextualFragment = function(fragmentStr) {
2433 return this.nativeRange.createContextualFragment(fragmentStr);
2437 /*--------------------------------------------------------------------------------------------------------*/
2440 getBody(document).removeChild(testTextNode);
2442 rangeProto.getName = function() {
2443 return "WrappedRange";
2446 api.WrappedRange = WrappedRange;
2448 api.createNativeRange = function(doc) {
2449 doc = getContentDocument(doc, module, "createNativeRange");
2450 return doc.createRange();
2455 if (api.features.implementsTextRange) {
2457 This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
2458 method. For example, in the following (where pipes denote the selection boundaries):
2460 <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
2462 var range = document.selection.createRange();
2463 alert(range.parentElement().id); // Should alert "ul" but alerts "b"
2465 This method returns the common ancestor node of the following:
2466 - the parentElement() of the textRange
2467 - the parentElement() of the textRange after calling collapse(true)
2468 - the parentElement() of the textRange after calling collapse(false)
2470 var getTextRangeContainerElement = function(textRange) {
2471 var parentEl = textRange.parentElement();
2472 var range = textRange.duplicate();
2473 range.collapse(true);
2474 var startEl = range.parentElement();
2475 range = textRange.duplicate();
2476 range.collapse(false);
2477 var endEl = range.parentElement();
2478 var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
2480 return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
2483 var textRangeIsCollapsed = function(textRange) {
2484 return textRange.compareEndPoints("StartToEnd", textRange) == 0;
2487 // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
2488 // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
2489 // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
2490 // bugs, handling for inputs and images, plus optimizations.
2491 var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
2492 var workingRange = textRange.duplicate();
2493 workingRange.collapse(isStart);
2494 var containerElement = workingRange.parentElement();
2496 // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
2498 if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
2499 containerElement = wholeRangeContainerElement;
2503 // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
2504 // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
2505 if (!containerElement.canHaveHTML) {
2506 var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
2508 boundaryPosition: pos,
2510 nodeIndex: pos.offset,
2511 containerElement: pos.node
2516 var workingNode = dom.getDocument(containerElement).createElement("span");
2518 // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
2519 // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
2520 if (workingNode.parentNode) {
2521 dom.removeNode(workingNode);
2524 var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
2525 var previousNode, nextNode, boundaryPosition, boundaryNode;
2526 var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
2527 var childNodeCount = containerElement.childNodes.length;
2528 var end = childNodeCount;
2530 // Check end first. Code within the loop assumes that the endth child node of the container is definitely
2531 // after the range boundary.
2532 var nodeIndex = end;
2535 if (nodeIndex == childNodeCount) {
2536 containerElement.appendChild(workingNode);
2538 containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
2540 workingRange.moveToElementText(workingNode);
2541 comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
2542 if (comparison == 0 || start == end) {
2544 } else if (comparison == -1) {
2545 if (end == start + 1) {
2546 // We know the endth child node is after the range boundary, so we must be done.
2552 end = (end == start + 1) ? start : nodeIndex;
2554 nodeIndex = Math.floor((start + end) / 2);
2555 containerElement.removeChild(workingNode);
2559 // We've now reached or gone past the boundary of the text range we're interested in
2560 // so have identified the node we want
2561 boundaryNode = workingNode.nextSibling;
2563 if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
2564 // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
2565 // the node containing the text range's boundary, so we move the end of the working range to the
2566 // boundary point and measure the length of its text to get the boundary's offset within the node.
2567 workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
2571 if (/[\r\n]/.test(boundaryNode.data)) {
2573 For the particular case of a boundary within a text node containing rendered line breaks (within a
2574 <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
2577 - Each line break is represented as \r in the text node's data/nodeValue properties
2578 - Each line break is represented as \r\n in the TextRange's 'text' property
2579 - The 'text' property of the TextRange does not contain trailing line breaks
2581 To get round the problem presented by the final fact above, we can use the fact that TextRange's
2582 moveStart() and moveEnd() methods return the actual number of characters moved, which is not
2583 necessarily the same as the number of characters it was instructed to move. The simplest approach is
2584 to use this to store the characters moved when moving both the start and end of the range to the
2585 start of the document body and subtracting the start offset from the end offset (the
2586 "move-negative-gazillion" method). However, this is extremely slow when the document is large and
2587 the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
2588 the end of the document) has the same problem.
2590 Another approach that works is to use moveStart() to move the start boundary of the range up to the
2591 end boundary one character at a time and incrementing a counter with the value returned by the
2592 moveStart() call. However, the check for whether the start boundary has reached the end boundary is
2593 expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
2594 by the location of the range within the document).
2596 The approach used below is a hybrid of the two methods above. It uses the fact that a string
2597 containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
2598 be longer than the text of the TextRange, so the start of the range is moved that length initially
2599 and then a character at a time to make up for any trailing line breaks not contained in the 'text'
2600 property. This has good performance in most situations compared to the previous two methods.
2602 var tempRange = workingRange.duplicate();
2603 var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
2605 offset = tempRange.moveStart("character", rangeLength);
2606 while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
2608 tempRange.moveStart("character", 1);
2611 offset = workingRange.text.length;
2613 boundaryPosition = new DomPosition(boundaryNode, offset);
2616 // If the boundary immediately follows a character data node and this is the end boundary, we should favour
2617 // a position within that, and likewise for a start boundary preceding a character data node
2618 previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
2619 nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
2620 if (nextNode && isCharacterDataNode(nextNode)) {
2621 boundaryPosition = new DomPosition(nextNode, 0);
2622 } else if (previousNode && isCharacterDataNode(previousNode)) {
2623 boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
2625 boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
2630 dom.removeNode(workingNode);
2633 boundaryPosition: boundaryPosition,
2635 nodeIndex: nodeIndex,
2636 containerElement: containerElement
2641 // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
2642 // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
2643 // (http://code.google.com/p/ierange/)
2644 var createBoundaryTextRange = function(boundaryPosition, isStart) {
2645 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
2646 var doc = dom.getDocument(boundaryPosition.node);
2647 var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
2648 var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
2650 if (nodeIsDataNode) {
2651 boundaryNode = boundaryPosition.node;
2652 boundaryParent = boundaryNode.parentNode;
2654 childNodes = boundaryPosition.node.childNodes;
2655 boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
2656 boundaryParent = boundaryPosition.node;
2659 // Position the range immediately before the node containing the boundary
2660 workingNode = doc.createElement("span");
2662 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
2663 // the element rather than immediately before or after it
2664 workingNode.innerHTML = "&#feff;";
2666 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
2667 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
2669 boundaryParent.insertBefore(workingNode, boundaryNode);
2671 boundaryParent.appendChild(workingNode);
2674 workingRange.moveToElementText(workingNode);
2675 workingRange.collapse(!isStart);
2678 boundaryParent.removeChild(workingNode);
2680 // Move the working range to the text offset, if required
2681 if (nodeIsDataNode) {
2682 workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
2685 return workingRange;
2688 /*------------------------------------------------------------------------------------------------------------*/
2690 // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
2693 WrappedTextRange = function(textRange) {
2694 this.textRange = textRange;
2698 WrappedTextRange.prototype = new DomRange(document);
2700 WrappedTextRange.prototype.refresh = function() {
2701 var start, end, startBoundary;
2703 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
2704 var rangeContainerElement = getTextRangeContainerElement(this.textRange);
2706 if (textRangeIsCollapsed(this.textRange)) {
2707 end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
2708 true).boundaryPosition;
2710 startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
2711 start = startBoundary.boundaryPosition;
2713 // An optimization used here is that if the start and end boundaries have the same parent element, the
2714 // search scope for the end boundary can be limited to exclude the portion of the element that precedes
2715 // the start boundary
2716 end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
2717 startBoundary.nodeInfo).boundaryPosition;
2720 this.setStart(start.node, start.offset);
2721 this.setEnd(end.node, end.offset);
2724 WrappedTextRange.prototype.getName = function() {
2725 return "WrappedTextRange";
2728 DomRange.copyComparisonConstants(WrappedTextRange);
2730 var rangeToTextRange = function(range) {
2731 if (range.collapsed) {
2732 return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2734 var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2735 var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
2736 var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
2737 textRange.setEndPoint("StartToStart", startRange);
2738 textRange.setEndPoint("EndToEnd", endRange);
2743 WrappedTextRange.rangeToTextRange = rangeToTextRange;
2745 WrappedTextRange.prototype.toTextRange = function() {
2746 return rangeToTextRange(this);
2749 api.WrappedTextRange = WrappedTextRange;
2751 // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
2752 // implementation to use by default.
2753 if (!api.features.implementsDomRange || api.config.preferTextRange) {
2754 // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
2755 var globalObj = (function(f) { return f("return this;")(); })(Function);
2756 if (typeof globalObj.Range == "undefined") {
2757 globalObj.Range = WrappedTextRange;
2760 api.createNativeRange = function(doc) {
2761 doc = getContentDocument(doc, module, "createNativeRange");
2762 return getBody(doc).createTextRange();
2765 api.WrappedRange = WrappedTextRange;
2769 api.createRange = function(doc) {
2770 doc = getContentDocument(doc, module, "createRange");
2771 return new api.WrappedRange(api.createNativeRange(doc));
2774 api.createRangyRange = function(doc) {
2775 doc = getContentDocument(doc, module, "createRangyRange");
2776 return new DomRange(doc);
2779 util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
2780 util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
2782 api.addShimListener(function(win) {
2783 var doc = win.document;
2784 if (typeof doc.createRange == "undefined") {
2785 doc.createRange = function() {
2786 return api.createRange(doc);
2793 /*----------------------------------------------------------------------------------------------------------------*/
2795 // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
2796 // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
2797 api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
2798 api.config.checkSelectionRanges = true;
2800 var BOOLEAN = "boolean";
2801 var NUMBER = "number";
2803 var util = api.util;
2804 var isHostMethod = util.isHostMethod;
2805 var DomRange = api.DomRange;
2806 var WrappedRange = api.WrappedRange;
2807 var DOMException = api.DOMException;
2808 var DomPosition = dom.DomPosition;
2809 var getNativeSelection;
2810 var selectionIsCollapsed;
2811 var features = api.features;
2812 var CONTROL = "Control";
2813 var getDocument = dom.getDocument;
2814 var getBody = dom.getBody;
2815 var rangesEqual = DomRange.rangesEqual;
2818 // Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
2819 // "forward" or "forwards") or a Boolean (true for backwards).
2820 function isDirectionBackward(dir) {
2821 return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
2824 function getWindow(win, methodName) {
2827 } else if (dom.isWindow(win)) {
2829 } else if (win instanceof WrappedSelection) {
2832 var doc = dom.getContentDocument(win, module, methodName);
2833 return dom.getWindow(doc);
2837 function getWinSelection(winParam) {
2838 return getWindow(winParam, "getWinSelection").getSelection();
2841 function getDocSelection(winParam) {
2842 return getWindow(winParam, "getDocSelection").document.selection;
2845 function winSelectionIsBackward(sel) {
2846 var backward = false;
2847 if (sel.anchorNode) {
2848 backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
2853 // Test for the Range/TextRange and Selection features required
2854 // Test for ability to retrieve selection
2855 var implementsWinGetSelection = isHostMethod(window, "getSelection"),
2856 implementsDocSelection = util.isHostObject(document, "selection");
2858 features.implementsWinGetSelection = implementsWinGetSelection;
2859 features.implementsDocSelection = implementsDocSelection;
2861 var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
2863 if (useDocumentSelection) {
2864 getNativeSelection = getDocSelection;
2865 api.isSelectionValid = function(winParam) {
2866 var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
2868 // Check whether the selection TextRange is actually contained within the correct document
2869 return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
2871 } else if (implementsWinGetSelection) {
2872 getNativeSelection = getWinSelection;
2873 api.isSelectionValid = function() {
2877 module.fail("Neither document.selection or window.getSelection() detected.");
2881 api.getNativeSelection = getNativeSelection;
2883 var testSelection = getNativeSelection();
2885 // In Firefox, the selection is null in an iframe with display: none. See issue #138.
2886 if (!testSelection) {
2887 module.fail("Native selection was null (possibly issue 138?)");
2891 var testRange = api.createNativeRange(document);
2892 var body = getBody(document);
2894 // Obtaining a range from a selection
2895 var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
2896 ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
2898 features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
2900 // Test for existence of native selection extend() method
2901 var selectionHasExtend = isHostMethod(testSelection, "extend");
2902 features.selectionHasExtend = selectionHasExtend;
2904 // Test if rangeCount exists
2905 var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
2906 features.selectionHasRangeCount = selectionHasRangeCount;
2908 var selectionSupportsMultipleRanges = false;
2909 var collapsedNonEditableSelectionsSupported = true;
2911 var addRangeBackwardToNative = selectionHasExtend ?
2912 function(nativeSelection, range) {
2913 var doc = DomRange.getRangeDocument(range);
2914 var endRange = api.createRange(doc);
2915 endRange.collapseToPoint(range.endContainer, range.endOffset);
2916 nativeSelection.addRange(getNativeRange(endRange));
2917 nativeSelection.extend(range.startContainer, range.startOffset);
2920 if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
2921 typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
2924 // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
2925 // performed on the current document's selection. See issue 109.
2927 // Note also that if a selection previously existed, it is wiped and later restored by these tests. This
2928 // will result in the selection direction begin reversed if the original selection was backwards and the
2929 // browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
2930 var sel = window.getSelection();
2932 // Store the current selection
2933 var originalSelectionRangeCount = sel.rangeCount;
2934 var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
2935 var originalSelectionRanges = [];
2936 var originalSelectionBackward = winSelectionIsBackward(sel);
2937 for (var i = 0; i < originalSelectionRangeCount; ++i) {
2938 originalSelectionRanges[i] = sel.getRangeAt(i);
2941 // Create some test elements
2942 var testEl = dom.createTestElement(document, "", false);
2943 var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
2945 // Test whether the native selection will allow a collapsed selection within a non-editable element
2946 var r1 = document.createRange();
2948 r1.setStart(textNode, 1);
2950 sel.removeAllRanges();
2952 collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
2953 sel.removeAllRanges();
2955 // Test whether the native selection is capable of supporting multiple ranges.
2956 if (!selectionHasMultipleRanges) {
2957 // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
2958 // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
2959 // nothing we can do about this while retaining the feature test so we have to resort to a browser
2960 // sniff. I'm not happy about it. See
2961 // https://code.google.com/p/chromium/issues/detail?id=399791
2962 var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
2963 if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
2964 selectionSupportsMultipleRanges = false;
2966 var r2 = r1.cloneRange();
2967 r1.setStart(textNode, 0);
2968 r2.setEnd(textNode, 3);
2969 r2.setStart(textNode, 2);
2972 selectionSupportsMultipleRanges = (sel.rangeCount == 2);
2977 dom.removeNode(testEl);
2978 sel.removeAllRanges();
2980 for (i = 0; i < originalSelectionRangeCount; ++i) {
2981 if (i == 0 && originalSelectionBackward) {
2982 if (addRangeBackwardToNative) {
2983 addRangeBackwardToNative(sel, originalSelectionRanges[i]);
2985 api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
2986 sel.addRange(originalSelectionRanges[i]);
2989 sel.addRange(originalSelectionRanges[i]);
2996 features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
2997 features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
3000 var implementsControlRange = false, testControlRange;
3002 if (body && isHostMethod(body, "createControlRange")) {
3003 testControlRange = body.createControlRange();
3004 if (util.areHostProperties(testControlRange, ["item", "add"])) {
3005 implementsControlRange = true;
3008 features.implementsControlRange = implementsControlRange;
3010 // Selection collapsedness
3011 if (selectionHasAnchorAndFocus) {
3012 selectionIsCollapsed = function(sel) {
3013 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
3016 selectionIsCollapsed = function(sel) {
3017 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
3021 function updateAnchorAndFocusFromRange(sel, range, backward) {
3022 var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
3023 sel.anchorNode = range[anchorPrefix + "Container"];
3024 sel.anchorOffset = range[anchorPrefix + "Offset"];
3025 sel.focusNode = range[focusPrefix + "Container"];
3026 sel.focusOffset = range[focusPrefix + "Offset"];
3029 function updateAnchorAndFocusFromNativeSelection(sel) {
3030 var nativeSel = sel.nativeSelection;
3031 sel.anchorNode = nativeSel.anchorNode;
3032 sel.anchorOffset = nativeSel.anchorOffset;
3033 sel.focusNode = nativeSel.focusNode;
3034 sel.focusOffset = nativeSel.focusOffset;
3037 function updateEmptySelection(sel) {
3038 sel.anchorNode = sel.focusNode = null;
3039 sel.anchorOffset = sel.focusOffset = 0;
3041 sel.isCollapsed = true;
3042 sel._ranges.length = 0;
3045 function getNativeRange(range) {
3047 if (range instanceof DomRange) {
3048 nativeRange = api.createNativeRange(range.getDocument());
3049 nativeRange.setEnd(range.endContainer, range.endOffset);
3050 nativeRange.setStart(range.startContainer, range.startOffset);
3051 } else if (range instanceof WrappedRange) {
3052 nativeRange = range.nativeRange;
3053 } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
3054 nativeRange = range;
3059 function rangeContainsSingleElement(rangeNodes) {
3060 if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
3063 for (var i = 1, len = rangeNodes.length; i < len; ++i) {
3064 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
3071 function getSingleElementFromRange(range) {
3072 var nodes = range.getNodes();
3073 if (!rangeContainsSingleElement(nodes)) {
3074 throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
3079 // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
3080 function isTextRange(range) {
3081 return !!range && typeof range.text != "undefined";
3084 function updateFromTextRange(sel, range) {
3085 // Create a Range from the selected TextRange
3086 var wrappedRange = new WrappedRange(range);
3087 sel._ranges = [wrappedRange];
3089 updateAnchorAndFocusFromRange(sel, wrappedRange, false);
3091 sel.isCollapsed = wrappedRange.collapsed;
3094 function updateControlSelection(sel) {
3095 // Update the wrapped selection based on what's now in the native selection
3096 sel._ranges.length = 0;
3097 if (sel.docSelection.type == "None") {
3098 updateEmptySelection(sel);
3100 var controlRange = sel.docSelection.createRange();
3101 if (isTextRange(controlRange)) {
3102 // This case (where the selection type is "Control" and calling createRange() on the selection returns
3103 // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
3104 // ControlRange have been removed from the ControlRange and removed from the document.
3105 updateFromTextRange(sel, controlRange);
3107 sel.rangeCount = controlRange.length;
3108 var range, doc = getDocument(controlRange.item(0));
3109 for (var i = 0; i < sel.rangeCount; ++i) {
3110 range = api.createRange(doc);
3111 range.selectNode(controlRange.item(i));
3112 sel._ranges.push(range);
3114 sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
3115 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
3120 function addRangeToControlSelection(sel, range) {
3121 var controlRange = sel.docSelection.createRange();
3122 var rangeElement = getSingleElementFromRange(range);
3124 // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
3125 // contained by the supplied range
3126 var doc = getDocument(controlRange.item(0));
3127 var newControlRange = getBody(doc).createControlRange();
3128 for (var i = 0, len = controlRange.length; i < len; ++i) {
3129 newControlRange.add(controlRange.item(i));
3132 newControlRange.add(rangeElement);
3134 throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
3136 newControlRange.select();
3138 // Update the wrapped selection based on what's now in the native selection
3139 updateControlSelection(sel);
3142 var getSelectionRangeAt;
3144 if (isHostMethod(testSelection, "getRangeAt")) {
3145 // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
3146 // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
3147 // lesson to us all, especially me.
3148 getSelectionRangeAt = function(sel, index) {
3150 return sel.getRangeAt(index);
3155 } else if (selectionHasAnchorAndFocus) {
3156 getSelectionRangeAt = function(sel) {
3157 var doc = getDocument(sel.anchorNode);
3158 var range = api.createRange(doc);
3159 range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
3161 // Handle the case when the selection was selected backwards (from the end to the start in the
3163 if (range.collapsed !== this.isCollapsed) {
3164 range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
3171 function WrappedSelection(selection, docSelection, win) {
3172 this.nativeSelection = selection;
3173 this.docSelection = docSelection;
3179 WrappedSelection.prototype = api.selectionPrototype;
3181 function deleteProperties(sel) {
3182 sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
3183 sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
3184 sel.detached = true;
3187 var cachedRangySelections = [];
3189 function actOnCachedSelection(win, action) {
3190 var i = cachedRangySelections.length, cached, sel;
3192 cached = cachedRangySelections[i];
3193 sel = cached.selection;
3194 if (action == "deleteAll") {
3195 deleteProperties(sel);
3196 } else if (cached.win == win) {
3197 if (action == "delete") {
3198 cachedRangySelections.splice(i, 1);
3205 if (action == "deleteAll") {
3206 cachedRangySelections.length = 0;
3211 var getSelection = function(win) {
3212 // Check if the parameter is a Rangy Selection object
3213 if (win && win instanceof WrappedSelection) {
3218 win = getWindow(win, "getNativeSelection");
3220 var sel = actOnCachedSelection(win);
3221 var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
3223 sel.nativeSelection = nativeSel;
3224 sel.docSelection = docSel;
3227 sel = new WrappedSelection(nativeSel, docSel, win);
3228 cachedRangySelections.push( { win: win, selection: sel } );
3233 api.getSelection = getSelection;
3235 util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
3237 var selProto = WrappedSelection.prototype;
3239 function createControlSelection(sel, ranges) {
3240 // Ensure that the selection becomes of type "Control"
3241 var doc = getDocument(ranges[0].startContainer);
3242 var controlRange = getBody(doc).createControlRange();
3243 for (var i = 0, el, len = ranges.length; i < len; ++i) {
3244 el = getSingleElementFromRange(ranges[i]);
3246 controlRange.add(el);
3248 throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
3251 controlRange.select();
3253 // Update the wrapped selection based on what's now in the native selection
3254 updateControlSelection(sel);
3257 // Selecting a range
3258 if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
3259 selProto.removeAllRanges = function() {
3260 this.nativeSelection.removeAllRanges();
3261 updateEmptySelection(this);
3264 var addRangeBackward = function(sel, range) {
3265 addRangeBackwardToNative(sel.nativeSelection, range);
3269 if (selectionHasRangeCount) {
3270 selProto.addRange = function(range, direction) {
3271 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3272 addRangeToControlSelection(this, range);
3274 if (isDirectionBackward(direction) && selectionHasExtend) {
3275 addRangeBackward(this, range);
3277 var previousRangeCount;
3278 if (selectionSupportsMultipleRanges) {
3279 previousRangeCount = this.rangeCount;
3281 this.removeAllRanges();
3282 previousRangeCount = 0;
3284 // Clone the native range so that changing the selected range does not affect the selection.
3285 // This is contrary to the spec but is the only way to achieve consistency between browsers. See
3287 var clonedNativeRange = getNativeRange(range).cloneRange();
3289 this.nativeSelection.addRange(clonedNativeRange);
3293 // Check whether adding the range was successful
3294 this.rangeCount = this.nativeSelection.rangeCount;
3296 if (this.rangeCount == previousRangeCount + 1) {
3297 // The range was added successfully
3299 // Check whether the range that we added to the selection is reflected in the last range extracted from
3301 if (api.config.checkSelectionRanges) {
3302 var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
3303 if (nativeRange && !rangesEqual(nativeRange, range)) {
3304 // Happens in WebKit with, for example, a selection placed at the start of a text node
3305 range = new WrappedRange(nativeRange);
3308 this._ranges[this.rangeCount - 1] = range;
3309 updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
3310 this.isCollapsed = selectionIsCollapsed(this);
3312 // The range was not added successfully. The simplest thing is to refresh
3319 selProto.addRange = function(range, direction) {
3320 if (isDirectionBackward(direction) && selectionHasExtend) {
3321 addRangeBackward(this, range);
3323 this.nativeSelection.addRange(getNativeRange(range));
3329 selProto.setRanges = function(ranges) {
3330 if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
3331 createControlSelection(this, ranges);
3333 this.removeAllRanges();
3334 for (var i = 0, len = ranges.length; i < len; ++i) {
3335 this.addRange(ranges[i]);
3339 } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
3340 implementsControlRange && useDocumentSelection) {
3342 selProto.removeAllRanges = function() {
3343 // Added try/catch as fix for issue #21
3345 this.docSelection.empty();
3347 // Check for empty() not working (issue #24)
3348 if (this.docSelection.type != "None") {
3349 // Work around failure to empty a control selection by instead selecting a TextRange and then
3352 if (this.anchorNode) {
3353 doc = getDocument(this.anchorNode);
3354 } else if (this.docSelection.type == CONTROL) {
3355 var controlRange = this.docSelection.createRange();
3356 if (controlRange.length) {
3357 doc = getDocument( controlRange.item(0) );
3361 var textRange = getBody(doc).createTextRange();
3363 this.docSelection.empty();
3367 updateEmptySelection(this);
3370 selProto.addRange = function(range) {
3371 if (this.docSelection.type == CONTROL) {
3372 addRangeToControlSelection(this, range);
3374 api.WrappedTextRange.rangeToTextRange(range).select();
3375 this._ranges[0] = range;
3376 this.rangeCount = 1;
3377 this.isCollapsed = this._ranges[0].collapsed;
3378 updateAnchorAndFocusFromRange(this, range, false);
3382 selProto.setRanges = function(ranges) {
3383 this.removeAllRanges();
3384 var rangeCount = ranges.length;
3385 if (rangeCount > 1) {
3386 createControlSelection(this, ranges);
3387 } else if (rangeCount) {
3388 this.addRange(ranges[0]);
3392 module.fail("No means of selecting a Range or TextRange was found");
3396 selProto.getRangeAt = function(index) {
3397 if (index < 0 || index >= this.rangeCount) {
3398 throw new DOMException("INDEX_SIZE_ERR");
3400 // Clone the range to preserve selection-range independence. See issue 80.
3401 return this._ranges[index].cloneRange();
3405 var refreshSelection;
3407 if (useDocumentSelection) {
3408 refreshSelection = function(sel) {
3410 if (api.isSelectionValid(sel.win)) {
3411 range = sel.docSelection.createRange();
3413 range = getBody(sel.win.document).createTextRange();
3414 range.collapse(true);
3417 if (sel.docSelection.type == CONTROL) {
3418 updateControlSelection(sel);
3419 } else if (isTextRange(range)) {
3420 updateFromTextRange(sel, range);
3422 updateEmptySelection(sel);
3425 } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
3426 refreshSelection = function(sel) {
3427 if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
3428 updateControlSelection(sel);
3430 sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
3431 if (sel.rangeCount) {
3432 for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3433 sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
3435 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
3436 sel.isCollapsed = selectionIsCollapsed(sel);
3438 updateEmptySelection(sel);
3442 } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
3443 refreshSelection = function(sel) {
3444 var range, nativeSel = sel.nativeSelection;
3445 if (nativeSel.anchorNode) {
3446 range = getSelectionRangeAt(nativeSel, 0);
3447 sel._ranges = [range];
3449 updateAnchorAndFocusFromNativeSelection(sel);
3450 sel.isCollapsed = selectionIsCollapsed(sel);
3452 updateEmptySelection(sel);
3456 module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
3460 selProto.refresh = function(checkForChanges) {
3461 var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
3462 var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
3464 refreshSelection(this);
3465 if (checkForChanges) {
3466 // Check the range count first
3467 var i = oldRanges.length;
3468 if (i != this._ranges.length) {
3472 // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
3473 // ranges after this
3474 if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
3478 // Finally, compare each range in turn
3480 if (!rangesEqual(oldRanges[i], this._ranges[i])) {
3488 // Removal of a single range
3489 var removeRangeManually = function(sel, range) {
3490 var ranges = sel.getAllRanges();
3491 sel.removeAllRanges();
3492 for (var i = 0, len = ranges.length; i < len; ++i) {
3493 if (!rangesEqual(range, ranges[i])) {
3494 sel.addRange(ranges[i]);
3497 if (!sel.rangeCount) {
3498 updateEmptySelection(sel);
3502 if (implementsControlRange && implementsDocSelection) {
3503 selProto.removeRange = function(range) {
3504 if (this.docSelection.type == CONTROL) {
3505 var controlRange = this.docSelection.createRange();
3506 var rangeElement = getSingleElementFromRange(range);
3508 // Create a new ControlRange containing all the elements in the selected ControlRange minus the
3509 // element contained by the supplied range
3510 var doc = getDocument(controlRange.item(0));
3511 var newControlRange = getBody(doc).createControlRange();
3512 var el, removed = false;
3513 for (var i = 0, len = controlRange.length; i < len; ++i) {
3514 el = controlRange.item(i);
3515 if (el !== rangeElement || removed) {
3516 newControlRange.add(controlRange.item(i));
3521 newControlRange.select();
3523 // Update the wrapped selection based on what's now in the native selection
3524 updateControlSelection(this);
3526 removeRangeManually(this, range);
3530 selProto.removeRange = function(range) {
3531 removeRangeManually(this, range);
3535 // Detecting if a selection is backward
3536 var selectionIsBackward;
3537 if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
3538 selectionIsBackward = winSelectionIsBackward;
3540 selProto.isBackward = function() {
3541 return selectionIsBackward(this);
3544 selectionIsBackward = selProto.isBackward = function() {
3549 // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
3550 selProto.isBackwards = selProto.isBackward;
3552 // Selection stringifier
3553 // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
3554 // The current spec does not yet define this method.
3555 selProto.toString = function() {
3556 var rangeTexts = [];
3557 for (var i = 0, len = this.rangeCount; i < len; ++i) {
3558 rangeTexts[i] = "" + this._ranges[i];
3560 return rangeTexts.join("");
3563 function assertNodeInSameDocument(sel, node) {
3564 if (sel.win.document != getDocument(node)) {
3565 throw new DOMException("WRONG_DOCUMENT_ERR");
3569 // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
3570 selProto.collapse = function(node, offset) {
3571 assertNodeInSameDocument(this, node);
3572 var range = api.createRange(node);
3573 range.collapseToPoint(node, offset);
3574 this.setSingleRange(range);
3575 this.isCollapsed = true;
3578 selProto.collapseToStart = function() {
3579 if (this.rangeCount) {
3580 var range = this._ranges[0];
3581 this.collapse(range.startContainer, range.startOffset);
3583 throw new DOMException("INVALID_STATE_ERR");
3587 selProto.collapseToEnd = function() {
3588 if (this.rangeCount) {
3589 var range = this._ranges[this.rangeCount - 1];
3590 this.collapse(range.endContainer, range.endOffset);
3592 throw new DOMException("INVALID_STATE_ERR");
3596 // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
3597 // specified so the native implementation is never used by Rangy.
3598 selProto.selectAllChildren = function(node) {
3599 assertNodeInSameDocument(this, node);
3600 var range = api.createRange(node);
3601 range.selectNodeContents(node);
3602 this.setSingleRange(range);
3605 selProto.deleteFromDocument = function() {
3606 // Sepcial behaviour required for IE's control selections
3607 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3608 var controlRange = this.docSelection.createRange();
3610 while (controlRange.length) {
3611 element = controlRange.item(0);
3612 controlRange.remove(element);
3613 dom.removeNode(element);
3616 } else if (this.rangeCount) {
3617 var ranges = this.getAllRanges();
3618 if (ranges.length) {
3619 this.removeAllRanges();
3620 for (var i = 0, len = ranges.length; i < len; ++i) {
3621 ranges[i].deleteContents();
3623 // The spec says nothing about what the selection should contain after calling deleteContents on each
3624 // range. Firefox moves the selection to where the final selected range was, so we emulate that
3625 this.addRange(ranges[len - 1]);
3630 // The following are non-standard extensions
3631 selProto.eachRange = function(func, returnValue) {
3632 for (var i = 0, len = this._ranges.length; i < len; ++i) {
3633 if ( func( this.getRangeAt(i) ) ) {
3639 selProto.getAllRanges = function() {
3641 this.eachRange(function(range) {
3647 selProto.setSingleRange = function(range, direction) {
3648 this.removeAllRanges();
3649 this.addRange(range, direction);
3652 selProto.callMethodOnEachRange = function(methodName, params) {
3654 this.eachRange( function(range) {
3655 results.push( range[methodName].apply(range, params || []) );
3660 function createStartOrEndSetter(isStart) {
3661 return function(node, offset) {
3663 if (this.rangeCount) {
3664 range = this.getRangeAt(0);
3665 range["set" + (isStart ? "Start" : "End")](node, offset);
3667 range = api.createRange(this.win.document);
3668 range.setStartAndEnd(node, offset);
3670 this.setSingleRange(range, this.isBackward());
3674 selProto.setStart = createStartOrEndSetter(true);
3675 selProto.setEnd = createStartOrEndSetter(false);
3677 // Add select() method to Range prototype. Any existing selection will be removed.
3678 api.rangePrototype.select = function(direction) {
3679 getSelection( this.getDocument() ).setSingleRange(this, direction);
3682 selProto.changeEachRange = function(func) {
3684 var backward = this.isBackward();
3686 this.eachRange(function(range) {
3691 this.removeAllRanges();
3692 if (backward && ranges.length == 1) {
3693 this.addRange(ranges[0], "backward");
3695 this.setRanges(ranges);
3699 selProto.containsNode = function(node, allowPartial) {
3700 return this.eachRange( function(range) {
3701 return range.containsNode(node, allowPartial);
3705 selProto.getBookmark = function(containerNode) {
3707 backward: this.isBackward(),
3708 rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
3712 selProto.moveToBookmark = function(bookmark) {
3714 for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
3715 range = api.createRange(this.win);
3716 range.moveToBookmark(rangeBookmark);
3717 selRanges.push(range);
3719 if (bookmark.backward) {
3720 this.setSingleRange(selRanges[0], "backward");
3722 this.setRanges(selRanges);
3726 selProto.saveRanges = function() {
3728 backward: this.isBackward(),
3729 ranges: this.callMethodOnEachRange("cloneRange")
3733 selProto.restoreRanges = function(selRanges) {
3734 this.removeAllRanges();
3735 for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
3736 this.addRange(range, (selRanges.backward && i == 0));
3740 selProto.toHtml = function() {
3741 var rangeHtmls = [];
3742 this.eachRange(function(range) {
3743 rangeHtmls.push( DomRange.toHtml(range) );
3745 return rangeHtmls.join("");
3748 if (features.implementsTextRange) {
3749 selProto.getNativeTextRange = function() {
3751 if ( (sel = this.docSelection) ) {
3752 var range = sel.createRange();
3753 if (isTextRange(range)) {
3756 throw module.createError("getNativeTextRange: selection is a control selection");
3758 } else if (this.rangeCount > 0) {
3759 return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
3761 throw module.createError("getNativeTextRange: selection contains no range");
3766 function inspect(sel) {
3767 var rangeInspects = [];
3768 var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
3769 var focus = new DomPosition(sel.focusNode, sel.focusOffset);
3770 var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
3772 if (typeof sel.rangeCount != "undefined") {
3773 for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3774 rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
3777 return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
3778 ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
3781 selProto.getName = function() {
3782 return "WrappedSelection";
3785 selProto.inspect = function() {
3786 return inspect(this);
3789 selProto.detach = function() {
3790 actOnCachedSelection(this.win, "delete");
3791 deleteProperties(this);
3794 WrappedSelection.detachAll = function() {
3795 actOnCachedSelection(null, "deleteAll");
3798 WrappedSelection.inspect = inspect;
3799 WrappedSelection.isDirectionBackward = isDirectionBackward;
3801 api.Selection = WrappedSelection;
3803 api.selectionPrototype = selProto;
3805 api.addShimListener(function(win) {
3806 if (typeof win.getSelection == "undefined") {
3807 win.getSelection = function() {
3808 return getSelection(win);
3816 /*----------------------------------------------------------------------------------------------------------------*/
3818 // Wait for document to load before initializing
3819 var docReady = false;
3821 var loadHandler = function(e) {
3824 if (!api.initialized && api.config.autoInitialize) {
3831 // Test whether the document has already been loaded and initialize immediately if so
3832 if (document.readyState == "complete") {
3835 if (isHostMethod(document, "addEventListener")) {
3836 document.addEventListener("DOMContentLoaded", loadHandler, false);
3839 // Add a fallback in case the DOMContentLoaded event isn't supported
3840 addListener(window, "load", loadHandler);