2 * @license Rangy, a cross-browser JavaScript range and selection library
\r
3 * http://code.google.com/p/rangy/
\r
5 * Copyright 2012, Tim Down
\r
6 * Licensed under the MIT license.
\r
8 * Build date: 26 February 2012
\r
10 window['rangy'] = (function() {
\r
13 var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
\r
15 var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
\r
16 "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"];
\r
18 var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
\r
19 "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
\r
20 "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
\r
22 var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
\r
24 // Subset of TextRange's full set of methods that we're interested in
\r
25 var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark",
\r
26 "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"];
\r
28 /*----------------------------------------------------------------------------------------------------------------*/
\r
30 // Trio of functions taken from Peter Michaux's article:
\r
31 // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
\r
32 function isHostMethod(o, p) {
\r
33 var t = typeof o[p];
\r
34 return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
\r
37 function isHostObject(o, p) {
\r
38 return !!(typeof o[p] == OBJECT && o[p]);
\r
41 function isHostProperty(o, p) {
\r
42 return typeof o[p] != UNDEFINED;
\r
45 // Creates a convenience function to save verbose repeated calls to tests functions
\r
46 function createMultiplePropertyTest(testFunc) {
\r
47 return function(o, props) {
\r
48 var i = props.length;
\r
50 if (!testFunc(o, props[i])) {
\r
58 // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
\r
59 var areHostMethods = createMultiplePropertyTest(isHostMethod);
\r
60 var areHostObjects = createMultiplePropertyTest(isHostObject);
\r
61 var areHostProperties = createMultiplePropertyTest(isHostProperty);
\r
63 function isTextRange(range) {
\r
64 return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
\r
73 isHostMethod: isHostMethod,
\r
74 isHostObject: isHostObject,
\r
75 isHostProperty: isHostProperty,
\r
76 areHostMethods: areHostMethods,
\r
77 areHostObjects: areHostObjects,
\r
78 areHostProperties: areHostProperties,
\r
79 isTextRange: isTextRange
\r
87 preferTextRange: false
\r
91 function fail(reason) {
\r
92 window.alert("Rangy not supported in your browser. Reason: " + reason);
\r
93 api.initialized = true;
\r
94 api.supported = false;
\r
99 function warn(msg) {
\r
100 var warningMessage = "Rangy warning: " + msg;
\r
101 if (api.config.alertOnWarn) {
\r
102 window.alert(warningMessage);
\r
103 } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) {
\r
104 window.console.log(warningMessage);
\r
110 if ({}.hasOwnProperty) {
\r
111 api.util.extend = function(o, props) {
\r
112 for (var i in props) {
\r
113 if (props.hasOwnProperty(i)) {
\r
119 fail("hasOwnProperty not supported");
\r
122 var initListeners = [];
\r
123 var moduleInitializers = [];
\r
127 if (api.initialized) {
\r
131 var implementsDomRange = false, implementsTextRange = false;
\r
133 // First, perform basic feature tests
\r
135 if (isHostMethod(document, "createRange")) {
\r
136 testRange = document.createRange();
\r
137 if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
\r
138 implementsDomRange = true;
\r
140 testRange.detach();
\r
143 var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];
\r
145 if (body && isHostMethod(body, "createTextRange")) {
\r
146 testRange = body.createTextRange();
\r
147 if (isTextRange(testRange)) {
\r
148 implementsTextRange = true;
\r
152 if (!implementsDomRange && !implementsTextRange) {
\r
153 fail("Neither Range nor TextRange are implemented");
\r
156 api.initialized = true;
\r
158 implementsDomRange: implementsDomRange,
\r
159 implementsTextRange: implementsTextRange
\r
162 // Initialize modules and call init listeners
\r
163 var allListeners = moduleInitializers.concat(initListeners);
\r
164 for (var i = 0, len = allListeners.length; i < len; ++i) {
\r
166 allListeners[i](api);
\r
168 if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {
\r
169 window.console.log("Init listener threw an exception. Continuing.", ex);
\r
176 // Allow external scripts to initialize this library in case it's loaded after the document has loaded
\r
179 // Execute listener immediately if already initialized
\r
180 api.addInitListener = function(listener) {
\r
181 if (api.initialized) {
\r
184 initListeners.push(listener);
\r
188 var createMissingNativeApiListeners = [];
\r
190 api.addCreateMissingNativeApiListener = function(listener) {
\r
191 createMissingNativeApiListeners.push(listener);
\r
194 function createMissingNativeApi(win) {
\r
195 win = win || window;
\r
198 // Notify listeners
\r
199 for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) {
\r
200 createMissingNativeApiListeners[i](win);
\r
204 api.createMissingNativeApi = createMissingNativeApi;
\r
209 function Module(name) {
\r
211 this.initialized = false;
\r
212 this.supported = false;
\r
215 Module.prototype.fail = function(reason) {
\r
216 this.initialized = true;
\r
217 this.supported = false;
\r
219 throw new Error("Module '" + this.name + "' failed to load: " + reason);
\r
222 Module.prototype.warn = function(msg) {
\r
223 api.warn("Module " + this.name + ": " + msg);
\r
226 Module.prototype.createError = function(msg) {
\r
227 return new Error("Error in Rangy " + this.name + " module: " + msg);
\r
230 api.createModule = function(name, initFunc) {
\r
231 var module = new Module(name);
\r
232 api.modules[name] = module;
\r
234 moduleInitializers.push(function(api) {
\r
235 initFunc(api, module);
\r
236 module.initialized = true;
\r
237 module.supported = true;
\r
241 api.requireModules = function(modules) {
\r
242 for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) {
\r
243 moduleName = modules[i];
\r
244 module = api.modules[moduleName];
\r
245 if (!module || !(module instanceof Module)) {
\r
246 throw new Error("Module '" + moduleName + "' not found");
\r
248 if (!module.supported) {
\r
249 throw new Error("Module '" + moduleName + "' not supported");
\r
254 /*----------------------------------------------------------------------------------------------------------------*/
\r
256 // Wait for document to load before running tests
\r
258 var docReady = false;
\r
260 var loadHandler = function(e) {
\r
264 if (!api.initialized) {
\r
270 // Test whether we have window and document objects that we will need
\r
271 if (typeof window == UNDEFINED) {
\r
272 fail("No window found");
\r
275 if (typeof document == UNDEFINED) {
\r
276 fail("No document found");
\r
280 if (isHostMethod(document, "addEventListener")) {
\r
281 document.addEventListener("DOMContentLoaded", loadHandler, false);
\r
284 // Add a fallback in case the DOMContentLoaded event isn't supported
\r
285 if (isHostMethod(window, "addEventListener")) {
\r
286 window.addEventListener("load", loadHandler, false);
\r
287 } else if (isHostMethod(window, "attachEvent")) {
\r
288 window.attachEvent("onload", loadHandler);
\r
290 fail("Window does not have required addEventListener or attachEvent method");
\r
295 rangy.createModule("DomUtil", function(api, module) {
\r
297 var UNDEF = "undefined";
\r
298 var util = api.util;
\r
300 // Perform feature tests
\r
301 if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
\r
302 module.fail("document missing a Node creation method");
\r
305 if (!util.isHostMethod(document, "getElementsByTagName")) {
\r
306 module.fail("document missing getElementsByTagName method");
\r
309 var el = document.createElement("div");
\r
310 if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
\r
311 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
\r
312 module.fail("Incomplete Element implementation");
\r
315 // innerHTML is required for Range's createContextualFragment method
\r
316 if (!util.isHostProperty(el, "innerHTML")) {
\r
317 module.fail("Element is missing innerHTML property");
\r
320 var textNode = document.createTextNode("test");
\r
321 if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
\r
322 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
\r
323 !util.areHostProperties(textNode, ["data"]))) {
\r
324 module.fail("Incomplete Text Node implementation");
\r
327 /*----------------------------------------------------------------------------------------------------------------*/
\r
329 // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
\r
330 // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
\r
331 // contains just the document as a single element and the value searched for is the document.
\r
332 var arrayContains = /*Array.prototype.indexOf ?
\r
333 function(arr, val) {
\r
334 return arr.indexOf(val) > -1;
\r
337 function(arr, val) {
\r
338 var i = arr.length;
\r
340 if (arr[i] === val) {
\r
347 // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
\r
348 function isHtmlNamespace(node) {
\r
350 return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
\r
353 function parentElement(node) {
\r
354 var parent = node.parentNode;
\r
355 return (parent.nodeType == 1) ? parent : null;
\r
358 function getNodeIndex(node) {
\r
360 while( (node = node.previousSibling) ) {
\r
366 function getNodeLength(node) {
\r
368 return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0);
\r
371 function getCommonAncestor(node1, node2) {
\r
372 var ancestors = [], n;
\r
373 for (n = node1; n; n = n.parentNode) {
\r
377 for (n = node2; n; n = n.parentNode) {
\r
378 if (arrayContains(ancestors, n)) {
\r
386 function isAncestorOf(ancestor, descendant, selfIsAncestor) {
\r
387 var n = selfIsAncestor ? descendant : descendant.parentNode;
\r
389 if (n === ancestor) {
\r
398 function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
\r
399 var p, n = selfIsAncestor ? node : node.parentNode;
\r
402 if (p === ancestor) {
\r
410 function isCharacterDataNode(node) {
\r
411 var t = node.nodeType;
\r
412 return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
\r
415 function insertAfter(node, precedingNode) {
\r
416 var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
\r
418 parent.insertBefore(node, nextNode);
\r
420 parent.appendChild(node);
\r
425 // Note that we cannot use splitText() because it is bugridden in IE 9.
\r
426 function splitDataNode(node, index) {
\r
427 var newNode = node.cloneNode(false);
\r
428 newNode.deleteData(0, index);
\r
429 node.deleteData(index, node.length - index);
\r
430 insertAfter(newNode, node);
\r
434 function getDocument(node) {
\r
435 if (node.nodeType == 9) {
\r
437 } else if (typeof node.ownerDocument != UNDEF) {
\r
438 return node.ownerDocument;
\r
439 } else if (typeof node.document != UNDEF) {
\r
440 return node.document;
\r
441 } else if (node.parentNode) {
\r
442 return getDocument(node.parentNode);
\r
444 throw new Error("getDocument: no document found for node");
\r
448 function getWindow(node) {
\r
449 var doc = getDocument(node);
\r
450 if (typeof doc.defaultView != UNDEF) {
\r
451 return doc.defaultView;
\r
452 } else if (typeof doc.parentWindow != UNDEF) {
\r
453 return doc.parentWindow;
\r
455 throw new Error("Cannot get a window object for node");
\r
459 function getIframeDocument(iframeEl) {
\r
460 if (typeof iframeEl.contentDocument != UNDEF) {
\r
461 return iframeEl.contentDocument;
\r
462 } else if (typeof iframeEl.contentWindow != UNDEF) {
\r
463 return iframeEl.contentWindow.document;
\r
465 throw new Error("getIframeWindow: No Document object found for iframe element");
\r
469 function getIframeWindow(iframeEl) {
\r
470 if (typeof iframeEl.contentWindow != UNDEF) {
\r
471 return iframeEl.contentWindow;
\r
472 } else if (typeof iframeEl.contentDocument != UNDEF) {
\r
473 return iframeEl.contentDocument.defaultView;
\r
475 throw new Error("getIframeWindow: No Window object found for iframe element");
\r
479 function getBody(doc) {
\r
480 return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
\r
483 function getRootContainer(node) {
\r
485 while ( (parent = node.parentNode) ) {
\r
491 function comparePoints(nodeA, offsetA, nodeB, offsetB) {
\r
492 // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
\r
493 var nodeC, root, childA, childB, n;
\r
494 if (nodeA == nodeB) {
\r
496 // Case 1: nodes are the same
\r
497 return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
\r
498 } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
\r
500 // Case 2: node C (container B or an ancestor) is a child node of A
\r
501 return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
\r
502 } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
\r
504 // Case 3: node C (container A or an ancestor) is a child node of B
\r
505 return getNodeIndex(nodeC) < offsetB ? -1 : 1;
\r
508 // Case 4: containers are siblings or descendants of siblings
\r
509 root = getCommonAncestor(nodeA, nodeB);
\r
510 childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
\r
511 childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
\r
513 if (childA === childB) {
\r
514 // This shouldn't be possible
\r
516 throw new Error("comparePoints got to case 4 and childA and childB are the same!");
\r
518 n = root.firstChild;
\r
520 if (n === childA) {
\r
522 } else if (n === childB) {
\r
527 throw new Error("Should not be here!");
\r
532 function fragmentFromNodeChildren(node) {
\r
533 var fragment = getDocument(node).createDocumentFragment(), child;
\r
534 while ( (child = node.firstChild) ) {
\r
535 fragment.appendChild(child);
\r
540 function inspectNode(node) {
\r
542 return "[No node]";
\r
544 if (isCharacterDataNode(node)) {
\r
545 return '"' + node.data + '"';
\r
546 } else if (node.nodeType == 1) {
\r
547 var idAttr = node.id ? ' id="' + node.id + '"' : "";
\r
548 return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]";
\r
550 return node.nodeName;
\r
557 function NodeIterator(root) {
\r
562 NodeIterator.prototype = {
\r
565 hasNext: function() {
\r
566 return !!this._next;
\r
570 var n = this._current = this._next;
\r
572 if (this._current) {
\r
573 child = n.firstChild;
\r
575 this._next = child;
\r
578 while ((n !== this.root) && !(next = n.nextSibling)) {
\r
584 return this._current;
\r
587 detach: function() {
\r
588 this._current = this._next = this.root = null;
\r
592 function createIterator(root) {
\r
593 return new NodeIterator(root);
\r
599 function DomPosition(node, offset) {
\r
601 this.offset = offset;
\r
604 DomPosition.prototype = {
\r
605 equals: function(pos) {
\r
606 return this.node === pos.node & this.offset == pos.offset;
\r
609 inspect: function() {
\r
610 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
\r
617 function DOMException(codeName) {
\r
618 this.code = this[codeName];
\r
619 this.codeName = codeName;
\r
620 this.message = "DOMException: " + this.codeName;
\r
623 DOMException.prototype = {
\r
625 HIERARCHY_REQUEST_ERR: 3,
\r
626 WRONG_DOCUMENT_ERR: 4,
\r
627 NO_MODIFICATION_ALLOWED_ERR: 7,
\r
629 NOT_SUPPORTED_ERR: 9,
\r
630 INVALID_STATE_ERR: 11
\r
633 DOMException.prototype.toString = function() {
\r
634 return this.message;
\r
638 arrayContains: arrayContains,
\r
639 isHtmlNamespace: isHtmlNamespace,
\r
640 parentElement: parentElement,
\r
641 getNodeIndex: getNodeIndex,
\r
642 getNodeLength: getNodeLength,
\r
643 getCommonAncestor: getCommonAncestor,
\r
644 isAncestorOf: isAncestorOf,
\r
645 getClosestAncestorIn: getClosestAncestorIn,
\r
646 isCharacterDataNode: isCharacterDataNode,
\r
647 insertAfter: insertAfter,
\r
648 splitDataNode: splitDataNode,
\r
649 getDocument: getDocument,
\r
650 getWindow: getWindow,
\r
651 getIframeWindow: getIframeWindow,
\r
652 getIframeDocument: getIframeDocument,
\r
654 getRootContainer: getRootContainer,
\r
655 comparePoints: comparePoints,
\r
656 inspectNode: inspectNode,
\r
657 fragmentFromNodeChildren: fragmentFromNodeChildren,
\r
658 createIterator: createIterator,
\r
659 DomPosition: DomPosition
\r
662 api.DOMException = DOMException;
\r
663 });rangy.createModule("DomRange", function(api, module) {
664 api.requireModules( ["DomUtil"] );
668 var DomPosition = dom.DomPosition;
669 var DOMException = api.DOMException;
671 /*----------------------------------------------------------------------------------------------------------------*/
675 function isNonTextPartiallySelected(node, range) {
676 return (node.nodeType != 3) &&
677 (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true));
680 function getRangeDocument(range) {
681 return dom.getDocument(range.startContainer);
684 function dispatchEvent(range, type, args) {
685 var listeners = range._listeners[type];
687 for (var i = 0, len = listeners.length; i < len; ++i) {
688 listeners[i].call(range, {target: range, args: args});
693 function getBoundaryBeforeNode(node) {
694 return new DomPosition(node.parentNode, dom.getNodeIndex(node));
697 function getBoundaryAfterNode(node) {
698 return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1);
701 function insertNodeAtPosition(node, n, o) {
702 var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
703 if (dom.isCharacterDataNode(n)) {
705 dom.insertAfter(node, n);
707 n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o));
709 } else if (o >= n.childNodes.length) {
712 n.insertBefore(node, n.childNodes[o]);
714 return firstNodeInserted;
717 function cloneSubtree(iterator) {
718 var partiallySelected;
719 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
720 partiallySelected = iterator.isPartiallySelectedSubtree();
722 node = node.cloneNode(!partiallySelected);
723 if (partiallySelected) {
724 subIterator = iterator.getSubtreeIterator();
725 node.appendChild(cloneSubtree(subIterator));
726 subIterator.detach(true);
729 if (node.nodeType == 10) { // DocumentType
730 throw new DOMException("HIERARCHY_REQUEST_ERR");
732 frag.appendChild(node);
737 function iterateSubtree(rangeIterator, func, iteratorState) {
739 iteratorState = iteratorState || { stop: false };
740 for (var node, subRangeIterator; node = rangeIterator.next(); ) {
741 //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));
742 if (rangeIterator.isPartiallySelectedSubtree()) {
743 // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the
744 // node selected by the Range.
745 if (func(node) === false) {
746 iteratorState.stop = true;
749 subRangeIterator = rangeIterator.getSubtreeIterator();
750 iterateSubtree(subRangeIterator, func, iteratorState);
751 subRangeIterator.detach(true);
752 if (iteratorState.stop) {
757 // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
759 it = dom.createIterator(node);
760 while ( (n = it.next()) ) {
761 if (func(n) === false) {
762 iteratorState.stop = true;
770 function deleteSubtree(iterator) {
772 while (iterator.next()) {
773 if (iterator.isPartiallySelectedSubtree()) {
774 subIterator = iterator.getSubtreeIterator();
775 deleteSubtree(subIterator);
776 subIterator.detach(true);
783 function extractSubtree(iterator) {
785 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
788 if (iterator.isPartiallySelectedSubtree()) {
789 node = node.cloneNode(false);
790 subIterator = iterator.getSubtreeIterator();
791 node.appendChild(extractSubtree(subIterator));
792 subIterator.detach(true);
796 if (node.nodeType == 10) { // DocumentType
797 throw new DOMException("HIERARCHY_REQUEST_ERR");
799 frag.appendChild(node);
804 function getNodesInRange(range, nodeTypes, filter) {
805 //log.info("getNodesInRange, " + nodeTypes.join(","));
806 var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
807 var filterExists = !!filter;
808 if (filterNodeTypes) {
809 regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
813 iterateSubtree(new RangeIterator(range, false), function(node) {
814 if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) {
821 function inspect(range) {
822 var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
823 return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
824 dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
827 /*----------------------------------------------------------------------------------------------------------------*/
829 // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
834 function RangeIterator(range, clonePartiallySelectedTextNodes) {
836 this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
840 if (!range.collapsed) {
841 this.sc = range.startContainer;
842 this.so = range.startOffset;
843 this.ec = range.endContainer;
844 this.eo = range.endOffset;
845 var root = range.commonAncestorContainer;
847 if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) {
848 this.isSingleCharacterDataNode = true;
849 this._first = this._last = this._next = this.sc;
851 this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ?
852 this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true);
853 this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ?
854 this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true);
860 RangeIterator.prototype = {
865 isSingleCharacterDataNode: false,
868 this._current = null;
869 this._next = this._first;
872 hasNext: function() {
878 var current = this._current = this._next;
880 this._next = (current !== this._last) ? current.nextSibling : null;
882 // Check for partially selected text nodes
883 if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
884 if (current === this.ec) {
886 (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
888 if (this._current === this.sc) {
890 (current = current.cloneNode(true)).deleteData(0, this.so);
899 var current = this._current, start, end;
901 if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
902 start = (current === this.sc) ? this.so : 0;
903 end = (current === this.ec) ? this.eo : current.length;
905 current.deleteData(start, end - start);
908 if (current.parentNode) {
909 current.parentNode.removeChild(current);
916 // Checks if the current node is partially selected
917 isPartiallySelectedSubtree: function() {
918 var current = this._current;
919 return isNonTextPartiallySelected(current, this.range);
922 getSubtreeIterator: function() {
924 if (this.isSingleCharacterDataNode) {
925 subRange = this.range.cloneRange();
928 subRange = new Range(getRangeDocument(this.range));
929 var current = this._current;
930 var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current);
932 if (dom.isAncestorOf(current, this.sc, true)) {
933 startContainer = this.sc;
934 startOffset = this.so;
936 if (dom.isAncestorOf(current, this.ec, true)) {
937 endContainer = this.ec;
941 updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
943 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
946 detach: function(detachRange) {
950 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
954 /*----------------------------------------------------------------------------------------------------------------*/
961 function RangeException(codeName) {
962 this.code = this[codeName];
963 this.codeName = codeName;
964 this.message = "RangeException: " + this.codeName;
967 RangeException.prototype = {
968 BAD_BOUNDARYPOINTS_ERR: 1,
969 INVALID_NODE_TYPE_ERR: 2
972 RangeException.prototype.toString = function() {
976 /*----------------------------------------------------------------------------------------------------------------*/
979 * Currently iterates through all nodes in the range on creation until I think of a decent way to do it
980 * TODO: Look into making this a proper iterator, not requiring preloading everything first
983 function RangeNodeIterator(range, nodeTypes, filter) {
984 this.nodes = getNodesInRange(range, nodeTypes, filter);
985 this._next = this.nodes[0];
989 RangeNodeIterator.prototype = {
992 hasNext: function() {
997 this._current = this._next;
998 this._next = this.nodes[ ++this._position ];
999 return this._current;
1002 detach: function() {
1003 this._current = this._next = this.nodes = null;
1007 var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
1008 var rootContainerNodeTypes = [2, 9, 11];
1009 var readonlyNodeTypes = [5, 6, 10, 12];
1010 var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
1011 var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
1013 function createAncestorFinder(nodeTypes) {
1014 return function(node, selfIsAncestor) {
1015 var t, n = selfIsAncestor ? node : node.parentNode;
1018 if (dom.arrayContains(nodeTypes, t)) {
1027 var getRootContainer = dom.getRootContainer;
1028 var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1029 var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1030 var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
1032 function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1033 if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1034 throw new RangeException("INVALID_NODE_TYPE_ERR");
1038 function assertNotDetached(range) {
1039 if (!range.startContainer) {
1040 throw new DOMException("INVALID_STATE_ERR");
1044 function assertValidNodeType(node, invalidTypes) {
1045 if (!dom.arrayContains(invalidTypes, node.nodeType)) {
1046 throw new RangeException("INVALID_NODE_TYPE_ERR");
1050 function assertValidOffset(node, offset) {
1051 if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1052 throw new DOMException("INDEX_SIZE_ERR");
1056 function assertSameDocumentOrFragment(node1, node2) {
1057 if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1058 throw new DOMException("WRONG_DOCUMENT_ERR");
1062 function assertNodeNotReadOnly(node) {
1063 if (getReadonlyAncestor(node, true)) {
1064 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1068 function assertNode(node, codeName) {
1070 throw new DOMException(codeName);
1074 function isOrphan(node) {
1075 return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
1078 function isValidOffset(node, offset) {
1079 return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length);
1082 function isRangeValid(range) {
1083 return (!!range.startContainer && !!range.endContainer
1084 && !isOrphan(range.startContainer)
1085 && !isOrphan(range.endContainer)
1086 && isValidOffset(range.startContainer, range.startOffset)
1087 && isValidOffset(range.endContainer, range.endOffset));
1090 function assertRangeValid(range) {
1091 assertNotDetached(range);
1092 if (!isRangeValid(range)) {
1093 throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
1097 /*----------------------------------------------------------------------------------------------------------------*/
1099 // Test the browser's innerHTML support to decide how to implement createContextualFragment
1100 var styleEl = document.createElement("style");
1101 var htmlParsingConforms = false;
1103 styleEl.innerHTML = "<b>x</b>";
1104 htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
1109 api.features.htmlParsingConforms = htmlParsingConforms;
1111 var createContextualFragment = htmlParsingConforms ?
1113 // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
1114 // discussion and base code for this implementation at issue 67.
1115 // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
1116 // Thanks to Aleks Williams.
1117 function(fragmentStr) {
1118 // "Let node the context object's start's node."
1119 var node = this.startContainer;
1120 var doc = dom.getDocument(node);
1122 // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1123 // exception and abort these steps."
1125 throw new DOMException("INVALID_STATE_ERR");
1128 // "Let element be as follows, depending on node's interface:"
1129 // Document, Document Fragment: null
1133 if (node.nodeType == 1) {
1136 // "Text, Comment: node's parentElement"
1137 } else if (dom.isCharacterDataNode(node)) {
1138 el = dom.parentElement(node);
1141 // "If either element is null or element's ownerDocument is an HTML document
1142 // and element's local name is "html" and element's namespace is the HTML
1144 if (el === null || (
1145 el.nodeName == "HTML"
1146 && dom.isHtmlNamespace(dom.getDocument(el).documentElement)
1147 && dom.isHtmlNamespace(el)
1150 // "let element be a new Element with "body" as its local name and the HTML
1151 // namespace as its namespace.""
1152 el = doc.createElement("body");
1154 el = el.cloneNode(false);
1157 // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
1158 // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
1159 // "In either case, the algorithm must be invoked with fragment as the input
1160 // and element as the context element."
1161 el.innerHTML = fragmentStr;
1163 // "If this raises an exception, then abort these steps. Otherwise, let new
1164 // children be the nodes returned."
1166 // "Let fragment be a new DocumentFragment."
1167 // "Append all new children to fragment."
1168 // "Return fragment."
1169 return dom.fragmentFromNodeChildren(el);
1172 // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
1173 // previous versions of Rangy used (with the exception of using a body element rather than a div)
1174 function(fragmentStr) {
1175 assertNotDetached(this);
1176 var doc = getRangeDocument(this);
1177 var el = doc.createElement("body");
1178 el.innerHTML = fragmentStr;
1180 return dom.fragmentFromNodeChildren(el);
1183 /*----------------------------------------------------------------------------------------------------------------*/
1185 var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1186 "commonAncestorContainer"];
1188 var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
1189 var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
1191 function RangePrototype() {}
1193 RangePrototype.prototype = {
1194 attachListener: function(type, listener) {
1195 this._listeners[type].push(listener);
1198 compareBoundaryPoints: function(how, range) {
1199 assertRangeValid(this);
1200 assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1202 var nodeA, offsetA, nodeB, offsetB;
1203 var prefixA = (how == e2s || how == s2s) ? "start" : "end";
1204 var prefixB = (how == s2e || how == s2s) ? "start" : "end";
1205 nodeA = this[prefixA + "Container"];
1206 offsetA = this[prefixA + "Offset"];
1207 nodeB = range[prefixB + "Container"];
1208 offsetB = range[prefixB + "Offset"];
1209 return dom.comparePoints(nodeA, offsetA, nodeB, offsetB);
1212 insertNode: function(node) {
1213 assertRangeValid(this);
1214 assertValidNodeType(node, insertableNodeTypes);
1215 assertNodeNotReadOnly(this.startContainer);
1217 if (dom.isAncestorOf(node, this.startContainer, true)) {
1218 throw new DOMException("HIERARCHY_REQUEST_ERR");
1221 // No check for whether the container of the start of the Range is of a type that does not allow
1222 // children of the type of node: the browser's DOM implementation should do this for us when we attempt
1225 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1226 this.setStartBefore(firstNodeInserted);
1229 cloneContents: function() {
1230 assertRangeValid(this);
1233 if (this.collapsed) {
1234 return getRangeDocument(this).createDocumentFragment();
1236 if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) {
1237 clone = this.startContainer.cloneNode(true);
1238 clone.data = clone.data.slice(this.startOffset, this.endOffset);
1239 frag = getRangeDocument(this).createDocumentFragment();
1240 frag.appendChild(clone);
1243 var iterator = new RangeIterator(this, true);
1244 clone = cloneSubtree(iterator);
1251 canSurroundContents: function() {
1252 assertRangeValid(this);
1253 assertNodeNotReadOnly(this.startContainer);
1254 assertNodeNotReadOnly(this.endContainer);
1256 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1257 // no non-text nodes.
1258 var iterator = new RangeIterator(this, true);
1259 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1260 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1262 return !boundariesInvalid;
1265 surroundContents: function(node) {
1266 assertValidNodeType(node, surroundNodeTypes);
1268 if (!this.canSurroundContents()) {
1269 throw new RangeException("BAD_BOUNDARYPOINTS_ERR");
1272 // Extract the contents
1273 var content = this.extractContents();
1275 // Clear the children of the node
1276 if (node.hasChildNodes()) {
1277 while (node.lastChild) {
1278 node.removeChild(node.lastChild);
1282 // Insert the new node and add the extracted contents
1283 insertNodeAtPosition(node, this.startContainer, this.startOffset);
1284 node.appendChild(content);
1286 this.selectNode(node);
1289 cloneRange: function() {
1290 assertRangeValid(this);
1291 var range = new Range(getRangeDocument(this));
1292 var i = rangeProperties.length, prop;
1294 prop = rangeProperties[i];
1295 range[prop] = this[prop];
1300 toString: function() {
1301 assertRangeValid(this);
1302 var sc = this.startContainer;
1303 if (sc === this.endContainer && dom.isCharacterDataNode(sc)) {
1304 return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
1306 var textBits = [], iterator = new RangeIterator(this, true);
1308 iterateSubtree(iterator, function(node) {
1309 // Accept only text or CDATA nodes, not comments
1311 if (node.nodeType == 3 || node.nodeType == 4) {
1312 textBits.push(node.data);
1316 return textBits.join("");
1320 // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
1321 // been removed from Mozilla.
1323 compareNode: function(node) {
1324 assertRangeValid(this);
1326 var parent = node.parentNode;
1327 var nodeIndex = dom.getNodeIndex(node);
1330 throw new DOMException("NOT_FOUND_ERR");
1333 var startComparison = this.comparePoint(parent, nodeIndex),
1334 endComparison = this.comparePoint(parent, nodeIndex + 1);
1336 if (startComparison < 0) { // Node starts before
1337 return (endComparison > 0) ? n_b_a : n_b;
1339 return (endComparison > 0) ? n_a : n_i;
1343 comparePoint: function(node, offset) {
1344 assertRangeValid(this);
1345 assertNode(node, "HIERARCHY_REQUEST_ERR");
1346 assertSameDocumentOrFragment(node, this.startContainer);
1348 if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
1350 } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
1356 createContextualFragment: createContextualFragment,
1358 toHtml: function() {
1359 assertRangeValid(this);
1360 var container = getRangeDocument(this).createElement("div");
1361 container.appendChild(this.cloneContents());
1362 return container.innerHTML;
1365 // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
1366 // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
1367 intersectsNode: function(node, touchingIsIntersecting) {
1368 assertRangeValid(this);
1369 assertNode(node, "NOT_FOUND_ERR");
1370 if (dom.getDocument(node) !== getRangeDocument(this)) {
1374 var parent = node.parentNode, offset = dom.getNodeIndex(node);
1375 assertNode(parent, "NOT_FOUND_ERR");
1377 var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset),
1378 endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
1380 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1384 isPointInRange: function(node, offset) {
1385 assertRangeValid(this);
1386 assertNode(node, "HIERARCHY_REQUEST_ERR");
1387 assertSameDocumentOrFragment(node, this.startContainer);
1389 return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
1390 (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
1393 // The methods below are non-standard and invented by me.
1395 // Sharing a boundary start-to-end or end-to-start does not count as intersection.
1396 intersectsRange: function(range, touchingIsIntersecting) {
1397 assertRangeValid(this);
1399 if (getRangeDocument(range) != getRangeDocument(this)) {
1400 throw new DOMException("WRONG_DOCUMENT_ERR");
1403 var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset),
1404 endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset);
1406 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1409 intersection: function(range) {
1410 if (this.intersectsRange(range)) {
1411 var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
1412 endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
1414 var intersectionRange = this.cloneRange();
1416 if (startComparison == -1) {
1417 intersectionRange.setStart(range.startContainer, range.startOffset);
1419 if (endComparison == 1) {
1420 intersectionRange.setEnd(range.endContainer, range.endOffset);
1422 return intersectionRange;
1427 union: function(range) {
1428 if (this.intersectsRange(range, true)) {
1429 var unionRange = this.cloneRange();
1430 if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
1431 unionRange.setStart(range.startContainer, range.startOffset);
1433 if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
1434 unionRange.setEnd(range.endContainer, range.endOffset);
1438 throw new RangeException("Ranges do not intersect");
1442 containsNode: function(node, allowPartial) {
1444 return this.intersectsNode(node, false);
1446 return this.compareNode(node) == n_i;
1450 containsNodeContents: function(node) {
1451 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0;
1454 containsRange: function(range) {
1455 return this.intersection(range).equals(range);
1458 containsNodeText: function(node) {
1459 var nodeRange = this.cloneRange();
1460 nodeRange.selectNode(node);
1461 var textNodes = nodeRange.getNodes([3]);
1462 if (textNodes.length > 0) {
1463 nodeRange.setStart(textNodes[0], 0);
1464 var lastTextNode = textNodes.pop();
1465 nodeRange.setEnd(lastTextNode, lastTextNode.length);
1466 var contains = this.containsRange(nodeRange);
1470 return this.containsNodeContents(node);
1474 createNodeIterator: function(nodeTypes, filter) {
1475 assertRangeValid(this);
1476 return new RangeNodeIterator(this, nodeTypes, filter);
1479 getNodes: function(nodeTypes, filter) {
1480 assertRangeValid(this);
1481 return getNodesInRange(this, nodeTypes, filter);
1484 getDocument: function() {
1485 return getRangeDocument(this);
1488 collapseBefore: function(node) {
1489 assertNotDetached(this);
1491 this.setEndBefore(node);
1492 this.collapse(false);
1495 collapseAfter: function(node) {
1496 assertNotDetached(this);
1498 this.setStartAfter(node);
1499 this.collapse(true);
1502 getName: function() {
1506 equals: function(range) {
1507 return Range.rangesEqual(this, range);
1510 isValid: function() {
1511 return isRangeValid(this);
1514 inspect: function() {
1515 return inspect(this);
1519 function copyComparisonConstantsToObject(obj) {
1520 obj.START_TO_START = s2s;
1521 obj.START_TO_END = s2e;
1522 obj.END_TO_END = e2e;
1523 obj.END_TO_START = e2s;
1525 obj.NODE_BEFORE = n_b;
1526 obj.NODE_AFTER = n_a;
1527 obj.NODE_BEFORE_AND_AFTER = n_b_a;
1528 obj.NODE_INSIDE = n_i;
1531 function copyComparisonConstants(constructor) {
1532 copyComparisonConstantsToObject(constructor);
1533 copyComparisonConstantsToObject(constructor.prototype);
1536 function createRangeContentRemover(remover, boundaryUpdater) {
1538 assertRangeValid(this);
1540 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
1542 var iterator = new RangeIterator(this, true);
1544 // Work out where to position the range after content removal
1547 node = dom.getClosestAncestorIn(sc, root, true);
1548 boundary = getBoundaryAfterNode(node);
1550 so = boundary.offset;
1553 // Check none of the range is read-only
1554 iterateSubtree(iterator, assertNodeNotReadOnly);
1558 // Remove the content
1559 var returnValue = remover(iterator);
1562 // Move to the new position
1563 boundaryUpdater(this, sc, so, sc, so);
1569 function createPrototypeRange(constructor, boundaryUpdater, detacher) {
1570 function createBeforeAfterNodeSetter(isBefore, isStart) {
1571 return function(node) {
1572 assertNotDetached(this);
1573 assertValidNodeType(node, beforeAfterNodeTypes);
1574 assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
1576 var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
1577 (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
1581 function setRangeStart(range, node, offset) {
1582 var ec = range.endContainer, eo = range.endOffset;
1583 if (node !== range.startContainer || offset !== range.startOffset) {
1584 // Check the root containers of the range and the new boundary, and also check whether the new boundary
1585 // is after the current end. In either case, collapse the range to the new position
1586 if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) {
1590 boundaryUpdater(range, node, offset, ec, eo);
1594 function setRangeEnd(range, node, offset) {
1595 var sc = range.startContainer, so = range.startOffset;
1596 if (node !== range.endContainer || offset !== range.endOffset) {
1597 // Check the root containers of the range and the new boundary, and also check whether the new boundary
1598 // is after the current end. In either case, collapse the range to the new position
1599 if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) {
1603 boundaryUpdater(range, sc, so, node, offset);
1607 function setRangeStartAndEnd(range, node, offset) {
1608 if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) {
1609 boundaryUpdater(range, node, offset, node, offset);
1613 constructor.prototype = new RangePrototype();
1615 api.util.extend(constructor.prototype, {
1616 setStart: function(node, offset) {
1617 assertNotDetached(this);
1618 assertNoDocTypeNotationEntityAncestor(node, true);
1619 assertValidOffset(node, offset);
1621 setRangeStart(this, node, offset);
1624 setEnd: function(node, offset) {
1625 assertNotDetached(this);
1626 assertNoDocTypeNotationEntityAncestor(node, true);
1627 assertValidOffset(node, offset);
1629 setRangeEnd(this, node, offset);
1632 setStartBefore: createBeforeAfterNodeSetter(true, true),
1633 setStartAfter: createBeforeAfterNodeSetter(false, true),
1634 setEndBefore: createBeforeAfterNodeSetter(true, false),
1635 setEndAfter: createBeforeAfterNodeSetter(false, false),
1637 collapse: function(isStart) {
1638 assertRangeValid(this);
1640 boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
1642 boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
1646 selectNodeContents: function(node) {
1647 // This doesn't seem well specified: the spec talks only about selecting the node's contents, which
1648 // could be taken to mean only its children. However, browsers implement this the same as selectNode for
1649 // text nodes, so I shall do likewise
1650 assertNotDetached(this);
1651 assertNoDocTypeNotationEntityAncestor(node, true);
1653 boundaryUpdater(this, node, 0, node, dom.getNodeLength(node));
1656 selectNode: function(node) {
1657 assertNotDetached(this);
1658 assertNoDocTypeNotationEntityAncestor(node, false);
1659 assertValidNodeType(node, beforeAfterNodeTypes);
1661 var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
1662 boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
1665 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
1667 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
1669 canSurroundContents: function() {
1670 assertRangeValid(this);
1671 assertNodeNotReadOnly(this.startContainer);
1672 assertNodeNotReadOnly(this.endContainer);
1674 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1675 // no non-text nodes.
1676 var iterator = new RangeIterator(this, true);
1677 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1678 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1680 return !boundariesInvalid;
1683 detach: function() {
1687 splitBoundaries: function() {
1688 assertRangeValid(this);
1691 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
1692 var startEndSame = (sc === ec);
1694 if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1695 dom.splitDataNode(ec, eo);
1699 if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1701 sc = dom.splitDataNode(sc, so);
1705 } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) {
1711 boundaryUpdater(this, sc, so, ec, eo);
1714 normalizeBoundaries: function() {
1715 assertRangeValid(this);
1717 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
1719 var mergeForward = function(node) {
1720 var sibling = node.nextSibling;
1721 if (sibling && sibling.nodeType == node.nodeType) {
1724 node.appendData(sibling.data);
1725 sibling.parentNode.removeChild(sibling);
1729 var mergeBackward = function(node) {
1730 var sibling = node.previousSibling;
1731 if (sibling && sibling.nodeType == node.nodeType) {
1733 var nodeLength = node.length;
1734 so = sibling.length;
1735 node.insertData(0, sibling.data);
1736 sibling.parentNode.removeChild(sibling);
1740 } else if (ec == node.parentNode) {
1741 var nodeIndex = dom.getNodeIndex(node);
1742 if (eo == nodeIndex) {
1745 } else if (eo > nodeIndex) {
1752 var normalizeStart = true;
1754 if (dom.isCharacterDataNode(ec)) {
1755 if (ec.length == eo) {
1760 var endNode = ec.childNodes[eo - 1];
1761 if (endNode && dom.isCharacterDataNode(endNode)) {
1762 mergeForward(endNode);
1765 normalizeStart = !this.collapsed;
1768 if (normalizeStart) {
1769 if (dom.isCharacterDataNode(sc)) {
1774 if (so < sc.childNodes.length) {
1775 var startNode = sc.childNodes[so];
1776 if (startNode && dom.isCharacterDataNode(startNode)) {
1777 mergeBackward(startNode);
1786 boundaryUpdater(this, sc, so, ec, eo);
1789 collapseToPoint: function(node, offset) {
1790 assertNotDetached(this);
1792 assertNoDocTypeNotationEntityAncestor(node, true);
1793 assertValidOffset(node, offset);
1795 setRangeStartAndEnd(this, node, offset);
1799 copyComparisonConstants(constructor);
1802 /*----------------------------------------------------------------------------------------------------------------*/
1804 // Updates commonAncestorContainer and collapsed after boundary change
1805 function updateCollapsedAndCommonAncestor(range) {
1806 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
1807 range.commonAncestorContainer = range.collapsed ?
1808 range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
1811 function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
1812 var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset);
1813 var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset);
1815 range.startContainer = startContainer;
1816 range.startOffset = startOffset;
1817 range.endContainer = endContainer;
1818 range.endOffset = endOffset;
1820 updateCollapsedAndCommonAncestor(range);
1821 dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved});
1824 function detach(range) {
1825 assertNotDetached(range);
1826 range.startContainer = range.startOffset = range.endContainer = range.endOffset = null;
1827 range.collapsed = range.commonAncestorContainer = null;
1828 dispatchEvent(range, "detach", null);
1829 range._listeners = null;
1835 function Range(doc) {
1836 this.startContainer = doc;
1837 this.startOffset = 0;
1838 this.endContainer = doc;
1844 updateCollapsedAndCommonAncestor(this);
1847 createPrototypeRange(Range, updateBoundaries, detach);
1849 api.rangePrototype = RangePrototype.prototype;
1851 Range.rangeProperties = rangeProperties;
1852 Range.RangeIterator = RangeIterator;
1853 Range.copyComparisonConstants = copyComparisonConstants;
1854 Range.createPrototypeRange = createPrototypeRange;
1855 Range.inspect = inspect;
1856 Range.getRangeDocument = getRangeDocument;
1857 Range.rangesEqual = function(r1, r2) {
1858 return r1.startContainer === r2.startContainer &&
1859 r1.startOffset === r2.startOffset &&
1860 r1.endContainer === r2.endContainer &&
1861 r1.endOffset === r2.endOffset;
1864 api.DomRange = Range;
1865 api.RangeException = RangeException;
1866 });rangy.createModule("WrappedRange", function(api, module) {
\r
1867 api.requireModules( ["DomUtil", "DomRange"] );
\r
1873 var dom = api.dom;
\r
1874 var DomPosition = dom.DomPosition;
\r
1875 var DomRange = api.DomRange;
\r
1879 /*----------------------------------------------------------------------------------------------------------------*/
\r
1882 This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
\r
1883 method. For example, in the following (where pipes denote the selection boundaries):
\r
1885 <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
\r
1887 var range = document.selection.createRange();
\r
1888 alert(range.parentElement().id); // Should alert "ul" but alerts "b"
\r
1890 This method returns the common ancestor node of the following:
\r
1891 - the parentElement() of the textRange
\r
1892 - the parentElement() of the textRange after calling collapse(true)
\r
1893 - the parentElement() of the textRange after calling collapse(false)
\r
1895 function getTextRangeContainerElement(textRange) {
\r
1896 var parentEl = textRange.parentElement();
\r
1898 var range = textRange.duplicate();
\r
1899 range.collapse(true);
\r
1900 var startEl = range.parentElement();
\r
1901 range = textRange.duplicate();
\r
1902 range.collapse(false);
\r
1903 var endEl = range.parentElement();
\r
1904 var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
\r
1906 return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
\r
1909 function textRangeIsCollapsed(textRange) {
\r
1910 return textRange.compareEndPoints("StartToEnd", textRange) == 0;
\r
1913 // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as
\r
1914 // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has
\r
1915 // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling
\r
1916 // for inputs and images, plus optimizations.
\r
1917 function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) {
\r
1918 var workingRange = textRange.duplicate();
\r
1920 workingRange.collapse(isStart);
\r
1921 var containerElement = workingRange.parentElement();
\r
1923 // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
\r
1925 // TODO: Find out when. Workaround for wholeRangeContainerElement may break this
\r
1926 if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) {
\r
1927 containerElement = wholeRangeContainerElement;
\r
1933 // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
\r
1934 // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
\r
1935 if (!containerElement.canHaveHTML) {
\r
1936 return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
\r
1939 var workingNode = dom.getDocument(containerElement).createElement("span");
\r
1940 var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
\r
1941 var previousNode, nextNode, boundaryPosition, boundaryNode;
\r
1943 // Move the working range through the container's children, starting at the end and working backwards, until the
\r
1944 // working range reaches or goes past the boundary we're interested in
\r
1946 containerElement.insertBefore(workingNode, workingNode.previousSibling);
\r
1947 workingRange.moveToElementText(workingNode);
\r
1948 } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&
\r
1949 workingNode.previousSibling);
\r
1951 // We've now reached or gone past the boundary of the text range we're interested in
\r
1952 // so have identified the node we want
\r
1953 boundaryNode = workingNode.nextSibling;
\r
1955 if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) {
\r
1956 // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the
\r
1957 // node containing the text range's boundary, so we move the end of the working range to the boundary point
\r
1958 // and measure the length of its text to get the boundary's offset within the node.
\r
1959 workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
\r
1964 if (/[\r\n]/.test(boundaryNode.data)) {
\r
1966 For the particular case of a boundary within a text node containing line breaks (within a <pre> element,
\r
1967 for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:
\r
1969 - Each line break is represented as \r in the text node's data/nodeValue properties
\r
1970 - Each line break is represented as \r\n in the TextRange's 'text' property
\r
1971 - The 'text' property of the TextRange does not contain trailing line breaks
\r
1973 To get round the problem presented by the final fact above, we can use the fact that TextRange's
\r
1974 moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily
\r
1975 the same as the number of characters it was instructed to move. The simplest approach is to use this to
\r
1976 store the characters moved when moving both the start and end of the range to the start of the document
\r
1977 body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).
\r
1978 However, this is extremely slow when the document is large and the range is near the end of it. Clearly
\r
1979 doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same
\r
1982 Another approach that works is to use moveStart() to move the start boundary of the range up to the end
\r
1983 boundary one character at a time and incrementing a counter with the value returned by the moveStart()
\r
1984 call. However, the check for whether the start boundary has reached the end boundary is expensive, so
\r
1985 this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of
\r
1986 the range within the document).
\r
1988 The method below is a hybrid of the two methods above. It uses the fact that a string containing the
\r
1989 TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the
\r
1990 text of the TextRange, so the start of the range is moved that length initially and then a character at
\r
1991 a time to make up for any trailing line breaks not contained in the 'text' property. This has good
\r
1992 performance in most situations compared to the previous two methods.
\r
1994 var tempRange = workingRange.duplicate();
\r
1995 var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
\r
1997 offset = tempRange.moveStart("character", rangeLength);
\r
1998 while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
\r
2000 tempRange.moveStart("character", 1);
\r
2003 offset = workingRange.text.length;
\r
2005 boundaryPosition = new DomPosition(boundaryNode, offset);
\r
2009 // If the boundary immediately follows a character data node and this is the end boundary, we should favour
\r
2010 // a position within that, and likewise for a start boundary preceding a character data node
\r
2011 previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
\r
2012 nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
\r
2016 if (nextNode && dom.isCharacterDataNode(nextNode)) {
\r
2017 boundaryPosition = new DomPosition(nextNode, 0);
\r
2018 } else if (previousNode && dom.isCharacterDataNode(previousNode)) {
\r
2019 boundaryPosition = new DomPosition(previousNode, previousNode.length);
\r
2021 boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
\r
2026 workingNode.parentNode.removeChild(workingNode);
\r
2028 return boundaryPosition;
\r
2031 // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.
\r
2032 // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
\r
2033 // (http://code.google.com/p/ierange/)
\r
2034 function createBoundaryTextRange(boundaryPosition, isStart) {
\r
2035 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
\r
2036 var doc = dom.getDocument(boundaryPosition.node);
\r
2037 var workingNode, childNodes, workingRange = doc.body.createTextRange();
\r
2038 var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node);
\r
2040 if (nodeIsDataNode) {
\r
2041 boundaryNode = boundaryPosition.node;
\r
2042 boundaryParent = boundaryNode.parentNode;
\r
2044 childNodes = boundaryPosition.node.childNodes;
\r
2045 boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
\r
2046 boundaryParent = boundaryPosition.node;
\r
2049 // Position the range immediately before the node containing the boundary
\r
2050 workingNode = doc.createElement("span");
\r
2052 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the
\r
2053 // element rather than immediately before or after it, which is what we want
\r
2054 workingNode.innerHTML = "&#feff;";
\r
2056 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
\r
2057 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
\r
2058 if (boundaryNode) {
\r
2059 boundaryParent.insertBefore(workingNode, boundaryNode);
\r
2061 boundaryParent.appendChild(workingNode);
\r
2064 workingRange.moveToElementText(workingNode);
\r
2065 workingRange.collapse(!isStart);
\r
2068 boundaryParent.removeChild(workingNode);
\r
2070 // Move the working range to the text offset, if required
\r
2071 if (nodeIsDataNode) {
\r
2072 workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
\r
2075 return workingRange;
\r
2078 /*----------------------------------------------------------------------------------------------------------------*/
\r
2080 if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {
\r
2081 // This is a wrapper around the browser's native DOM Range. It has two aims:
\r
2082 // - Provide workarounds for specific browser bugs
\r
2083 // - provide convenient extensions, which are inherited from Rangy's DomRange
\r
2087 var rangeProperties = DomRange.rangeProperties;
\r
2088 var canSetRangeStartAfterEnd;
\r
2090 function updateRangeProperties(range) {
\r
2091 var i = rangeProperties.length, prop;
\r
2093 prop = rangeProperties[i];
\r
2094 range[prop] = range.nativeRange[prop];
\r
2098 function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {
\r
2099 var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
\r
2100 var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
\r
2102 // Always set both boundaries for the benefit of IE9 (see issue 35)
\r
2103 if (startMoved || endMoved) {
\r
2104 range.setEnd(endContainer, endOffset);
\r
2105 range.setStart(startContainer, startOffset);
\r
2109 function detach(range) {
\r
2110 range.nativeRange.detach();
\r
2111 range.detached = true;
\r
2112 var i = rangeProperties.length, prop;
\r
2114 prop = rangeProperties[i];
\r
2115 range[prop] = null;
\r
2119 var createBeforeAfterNodeSetter;
\r
2121 WrappedRange = function(range) {
\r
2123 throw new Error("Range must be specified");
\r
2125 this.nativeRange = range;
\r
2126 updateRangeProperties(this);
\r
2129 DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);
\r
2131 rangeProto = WrappedRange.prototype;
\r
2133 rangeProto.selectNode = function(node) {
\r
2134 this.nativeRange.selectNode(node);
\r
2135 updateRangeProperties(this);
\r
2138 rangeProto.deleteContents = function() {
\r
2139 this.nativeRange.deleteContents();
\r
2140 updateRangeProperties(this);
\r
2143 rangeProto.extractContents = function() {
\r
2144 var frag = this.nativeRange.extractContents();
\r
2145 updateRangeProperties(this);
\r
2149 rangeProto.cloneContents = function() {
\r
2150 return this.nativeRange.cloneContents();
\r
2153 // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still
\r
2154 // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for
\r
2155 // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of
\r
2156 // insertNode, which works but is almost certainly slower than the native implementation.
\r
2158 rangeProto.insertNode = function(node) {
\r
2159 this.nativeRange.insertNode(node);
\r
2160 updateRangeProperties(this);
\r
2164 rangeProto.surroundContents = function(node) {
\r
2165 this.nativeRange.surroundContents(node);
\r
2166 updateRangeProperties(this);
\r
2169 rangeProto.collapse = function(isStart) {
\r
2170 this.nativeRange.collapse(isStart);
\r
2171 updateRangeProperties(this);
\r
2174 rangeProto.cloneRange = function() {
\r
2175 return new WrappedRange(this.nativeRange.cloneRange());
\r
2178 rangeProto.refresh = function() {
\r
2179 updateRangeProperties(this);
\r
2182 rangeProto.toString = function() {
\r
2183 return this.nativeRange.toString();
\r
2186 // Create test range and node for feature detection
\r
2188 var testTextNode = document.createTextNode("test");
\r
2189 dom.getBody(document).appendChild(testTextNode);
\r
2190 var range = document.createRange();
\r
2192 /*--------------------------------------------------------------------------------------------------------*/
\r
2194 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
\r
2197 range.setStart(testTextNode, 0);
\r
2198 range.setEnd(testTextNode, 0);
\r
2201 range.setStart(testTextNode, 1);
\r
2202 canSetRangeStartAfterEnd = true;
\r
2204 rangeProto.setStart = function(node, offset) {
\r
2205 this.nativeRange.setStart(node, offset);
\r
2206 updateRangeProperties(this);
\r
2209 rangeProto.setEnd = function(node, offset) {
\r
2210 this.nativeRange.setEnd(node, offset);
\r
2211 updateRangeProperties(this);
\r
2214 createBeforeAfterNodeSetter = function(name) {
\r
2215 return function(node) {
\r
2216 this.nativeRange[name](node);
\r
2217 updateRangeProperties(this);
\r
2224 canSetRangeStartAfterEnd = false;
\r
2226 rangeProto.setStart = function(node, offset) {
\r
2228 this.nativeRange.setStart(node, offset);
\r
2230 this.nativeRange.setEnd(node, offset);
\r
2231 this.nativeRange.setStart(node, offset);
\r
2233 updateRangeProperties(this);
\r
2236 rangeProto.setEnd = function(node, offset) {
\r
2238 this.nativeRange.setEnd(node, offset);
\r
2240 this.nativeRange.setStart(node, offset);
\r
2241 this.nativeRange.setEnd(node, offset);
\r
2243 updateRangeProperties(this);
\r
2246 createBeforeAfterNodeSetter = function(name, oppositeName) {
\r
2247 return function(node) {
\r
2249 this.nativeRange[name](node);
\r
2251 this.nativeRange[oppositeName](node);
\r
2252 this.nativeRange[name](node);
\r
2254 updateRangeProperties(this);
\r
2259 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
\r
2260 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
\r
2261 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
\r
2262 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
\r
2264 /*--------------------------------------------------------------------------------------------------------*/
\r
2266 // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to
\r
2267 // the 0th character of the text node
\r
2268 range.selectNodeContents(testTextNode);
\r
2269 if (range.startContainer == testTextNode && range.endContainer == testTextNode &&
\r
2270 range.startOffset == 0 && range.endOffset == testTextNode.length) {
\r
2271 rangeProto.selectNodeContents = function(node) {
\r
2272 this.nativeRange.selectNodeContents(node);
\r
2273 updateRangeProperties(this);
\r
2276 rangeProto.selectNodeContents = function(node) {
\r
2277 this.setStart(node, 0);
\r
2278 this.setEnd(node, DomRange.getEndOffset(node));
\r
2282 /*--------------------------------------------------------------------------------------------------------*/
\r
2284 // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants
\r
2285 // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
\r
2287 range.selectNodeContents(testTextNode);
\r
2288 range.setEnd(testTextNode, 3);
\r
2290 var range2 = document.createRange();
\r
2291 range2.selectNodeContents(testTextNode);
\r
2292 range2.setEnd(testTextNode, 4);
\r
2293 range2.setStart(testTextNode, 2);
\r
2295 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &
\r
2296 range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
\r
2297 // This is the wrong way round, so correct for it
\r
2300 rangeProto.compareBoundaryPoints = function(type, range) {
\r
2301 range = range.nativeRange || range;
\r
2302 if (type == range.START_TO_END) {
\r
2303 type = range.END_TO_START;
\r
2304 } else if (type == range.END_TO_START) {
\r
2305 type = range.START_TO_END;
\r
2307 return this.nativeRange.compareBoundaryPoints(type, range);
\r
2310 rangeProto.compareBoundaryPoints = function(type, range) {
\r
2311 return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
\r
2315 /*--------------------------------------------------------------------------------------------------------*/
\r
2317 // Test for existence of createContextualFragment and delegate to it if it exists
\r
2318 if (api.util.isHostMethod(range, "createContextualFragment")) {
\r
2319 rangeProto.createContextualFragment = function(fragmentStr) {
\r
2320 return this.nativeRange.createContextualFragment(fragmentStr);
\r
2324 /*--------------------------------------------------------------------------------------------------------*/
\r
2327 dom.getBody(document).removeChild(testTextNode);
\r
2332 api.createNativeRange = function(doc) {
\r
2333 doc = doc || document;
\r
2334 return doc.createRange();
\r
2336 } else if (api.features.implementsTextRange) {
\r
2337 // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
\r
2340 WrappedRange = function(textRange) {
\r
2341 this.textRange = textRange;
\r
2345 WrappedRange.prototype = new DomRange(document);
\r
2347 WrappedRange.prototype.refresh = function() {
\r
2350 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
\r
2351 var rangeContainerElement = getTextRangeContainerElement(this.textRange);
\r
2353 if (textRangeIsCollapsed(this.textRange)) {
\r
2354 end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);
\r
2357 start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
\r
2358 end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);
\r
2361 this.setStart(start.node, start.offset);
\r
2362 this.setEnd(end.node, end.offset);
\r
2365 DomRange.copyComparisonConstants(WrappedRange);
\r
2367 // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work
\r
2368 var globalObj = (function() { return this; })();
\r
2369 if (typeof globalObj.Range == "undefined") {
\r
2370 globalObj.Range = WrappedRange;
\r
2373 api.createNativeRange = function(doc) {
\r
2374 doc = doc || document;
\r
2375 return doc.body.createTextRange();
\r
2379 if (api.features.implementsTextRange) {
\r
2380 WrappedRange.rangeToTextRange = function(range) {
\r
2381 if (range.collapsed) {
\r
2382 var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
\r
2388 //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
\r
2390 var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
\r
2391 var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
\r
2392 var textRange = dom.getDocument(range.startContainer).body.createTextRange();
\r
2393 textRange.setEndPoint("StartToStart", startRange);
\r
2394 textRange.setEndPoint("EndToEnd", endRange);
\r
2400 WrappedRange.prototype.getName = function() {
\r
2401 return "WrappedRange";
\r
2404 api.WrappedRange = WrappedRange;
\r
2406 api.createRange = function(doc) {
\r
2407 doc = doc || document;
\r
2408 return new WrappedRange(api.createNativeRange(doc));
\r
2411 api.createRangyRange = function(doc) {
\r
2412 doc = doc || document;
\r
2413 return new DomRange(doc);
\r
2416 api.createIframeRange = function(iframeEl) {
\r
2417 return api.createRange(dom.getIframeDocument(iframeEl));
\r
2420 api.createIframeRangyRange = function(iframeEl) {
\r
2421 return api.createRangyRange(dom.getIframeDocument(iframeEl));
\r
2424 api.addCreateMissingNativeApiListener(function(win) {
\r
2425 var doc = win.document;
\r
2426 if (typeof doc.createRange == "undefined") {
\r
2427 doc.createRange = function() {
\r
2428 return api.createRange(this);
\r
2433 });rangy.createModule("WrappedSelection", function(api, module) {
\r
2434 // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range
\r
2435 // spec (http://html5.org/specs/dom-range.html)
\r
2437 api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
\r
2439 api.config.checkSelectionRanges = true;
\r
2441 var BOOLEAN = "boolean",
\r
2442 windowPropertyName = "_rangySelection",
\r
2445 DomRange = api.DomRange,
\r
2446 WrappedRange = api.WrappedRange,
\r
2447 DOMException = api.DOMException,
\r
2448 DomPosition = dom.DomPosition,
\r
2450 selectionIsCollapsed,
\r
2451 CONTROL = "Control";
\r
2455 function getWinSelection(winParam) {
\r
2456 return (winParam || window).getSelection();
\r
2459 function getDocSelection(winParam) {
\r
2460 return (winParam || window).document.selection;
\r
2463 // Test for the Range/TextRange and Selection features required
\r
2464 // Test for ability to retrieve selection
\r
2465 var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),
\r
2466 implementsDocSelection = api.util.isHostObject(document, "selection");
\r
2468 var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
\r
2470 if (useDocumentSelection) {
\r
2471 getSelection = getDocSelection;
\r
2472 api.isSelectionValid = function(winParam) {
\r
2473 var doc = (winParam || window).document, nativeSel = doc.selection;
\r
2475 // Check whether the selection TextRange is actually contained within the correct document
\r
2476 return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);
\r
2478 } else if (implementsWinGetSelection) {
\r
2479 getSelection = getWinSelection;
\r
2480 api.isSelectionValid = function() {
\r
2484 module.fail("Neither document.selection or window.getSelection() detected.");
\r
2487 api.getNativeSelection = getSelection;
\r
2489 var testSelection = getSelection();
\r
2490 var testRange = api.createNativeRange(document);
\r
2491 var body = dom.getBody(document);
\r
2493 // Obtaining a range from a selection
\r
2494 var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&
\r
2495 util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));
\r
2496 api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
\r
2498 // Test for existence of native selection extend() method
\r
2499 var selectionHasExtend = util.isHostMethod(testSelection, "extend");
\r
2500 api.features.selectionHasExtend = selectionHasExtend;
\r
2502 // Test if rangeCount exists
\r
2503 var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
\r
2504 api.features.selectionHasRangeCount = selectionHasRangeCount;
\r
2506 var selectionSupportsMultipleRanges = false;
\r
2507 var collapsedNonEditableSelectionsSupported = true;
\r
2509 if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
\r
2510 typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {
\r
2513 var iframe = document.createElement("iframe");
\r
2514 iframe.frameBorder = 0;
\r
2515 iframe.style.position = "absolute";
\r
2516 iframe.style.left = "-10000px";
\r
2517 body.appendChild(iframe);
\r
2519 var iframeDoc = dom.getIframeDocument(iframe);
\r
2521 iframeDoc.write("<html><head></head><body>12</body></html>");
\r
2522 iframeDoc.close();
\r
2524 var sel = dom.getIframeWindow(iframe).getSelection();
\r
2525 var docEl = iframeDoc.documentElement;
\r
2526 var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;
\r
2528 // Test whether the native selection will allow a collapsed selection within a non-editable element
\r
2529 var r1 = iframeDoc.createRange();
\r
2530 r1.setStart(textNode, 1);
\r
2531 r1.collapse(true);
\r
2533 collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
\r
2534 sel.removeAllRanges();
\r
2536 // Test whether the native selection is capable of supporting multiple ranges
\r
2537 var r2 = r1.cloneRange();
\r
2538 r1.setStart(textNode, 0);
\r
2539 r2.setEnd(textNode, 2);
\r
2543 selectionSupportsMultipleRanges = (sel.rangeCount == 2);
\r
2549 body.removeChild(iframe);
\r
2553 api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
\r
2554 api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
\r
2557 var implementsControlRange = false, testControlRange;
\r
2559 if (body && util.isHostMethod(body, "createControlRange")) {
\r
2560 testControlRange = body.createControlRange();
\r
2561 if (util.areHostProperties(testControlRange, ["item", "add"])) {
\r
2562 implementsControlRange = true;
\r
2565 api.features.implementsControlRange = implementsControlRange;
\r
2567 // Selection collapsedness
\r
2568 if (selectionHasAnchorAndFocus) {
\r
2569 selectionIsCollapsed = function(sel) {
\r
2570 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
\r
2573 selectionIsCollapsed = function(sel) {
\r
2574 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
\r
2578 function updateAnchorAndFocusFromRange(sel, range, backwards) {
\r
2579 var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end";
\r
2580 sel.anchorNode = range[anchorPrefix + "Container"];
\r
2581 sel.anchorOffset = range[anchorPrefix + "Offset"];
\r
2582 sel.focusNode = range[focusPrefix + "Container"];
\r
2583 sel.focusOffset = range[focusPrefix + "Offset"];
\r
2586 function updateAnchorAndFocusFromNativeSelection(sel) {
\r
2587 var nativeSel = sel.nativeSelection;
\r
2588 sel.anchorNode = nativeSel.anchorNode;
\r
2589 sel.anchorOffset = nativeSel.anchorOffset;
\r
2590 sel.focusNode = nativeSel.focusNode;
\r
2591 sel.focusOffset = nativeSel.focusOffset;
\r
2594 function updateEmptySelection(sel) {
\r
2595 sel.anchorNode = sel.focusNode = null;
\r
2596 sel.anchorOffset = sel.focusOffset = 0;
\r
2597 sel.rangeCount = 0;
\r
2598 sel.isCollapsed = true;
\r
2599 sel._ranges.length = 0;
\r
2602 function getNativeRange(range) {
\r
2604 if (range instanceof DomRange) {
\r
2605 nativeRange = range._selectionNativeRange;
\r
2606 if (!nativeRange) {
\r
2607 nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));
\r
2608 nativeRange.setEnd(range.endContainer, range.endOffset);
\r
2609 nativeRange.setStart(range.startContainer, range.startOffset);
\r
2610 range._selectionNativeRange = nativeRange;
\r
2611 range.attachListener("detach", function() {
\r
2613 this._selectionNativeRange = null;
\r
2616 } else if (range instanceof WrappedRange) {
\r
2617 nativeRange = range.nativeRange;
\r
2618 } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
\r
2619 nativeRange = range;
\r
2621 return nativeRange;
\r
2624 function rangeContainsSingleElement(rangeNodes) {
\r
2625 if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
\r
2628 for (var i = 1, len = rangeNodes.length; i < len; ++i) {
\r
2629 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
\r
2636 function getSingleElementFromRange(range) {
\r
2637 var nodes = range.getNodes();
\r
2638 if (!rangeContainsSingleElement(nodes)) {
\r
2639 throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
\r
2644 function isTextRange(range) {
\r
2645 return !!range && typeof range.text != "undefined";
\r
2648 function updateFromTextRange(sel, range) {
\r
2649 // Create a Range from the selected TextRange
\r
2650 var wrappedRange = new WrappedRange(range);
\r
2651 sel._ranges = [wrappedRange];
\r
2653 updateAnchorAndFocusFromRange(sel, wrappedRange, false);
\r
2654 sel.rangeCount = 1;
\r
2655 sel.isCollapsed = wrappedRange.collapsed;
\r
2658 function updateControlSelection(sel) {
\r
2659 // Update the wrapped selection based on what's now in the native selection
\r
2660 sel._ranges.length = 0;
\r
2661 if (sel.docSelection.type == "None") {
\r
2662 updateEmptySelection(sel);
\r
2664 var controlRange = sel.docSelection.createRange();
\r
2665 if (isTextRange(controlRange)) {
\r
2666 // This case (where the selection type is "Control" and calling createRange() on the selection returns
\r
2667 // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
\r
2668 // ControlRange have been removed from the ControlRange and removed from the document.
\r
2669 updateFromTextRange(sel, controlRange);
\r
2671 sel.rangeCount = controlRange.length;
\r
2672 var range, doc = dom.getDocument(controlRange.item(0));
\r
2673 for (var i = 0; i < sel.rangeCount; ++i) {
\r
2674 range = api.createRange(doc);
\r
2675 range.selectNode(controlRange.item(i));
\r
2676 sel._ranges.push(range);
\r
2678 sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
\r
2679 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
\r
2684 function addRangeToControlSelection(sel, range) {
\r
2685 var controlRange = sel.docSelection.createRange();
\r
2686 var rangeElement = getSingleElementFromRange(range);
\r
2688 // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
\r
2689 // contained by the supplied range
\r
2690 var doc = dom.getDocument(controlRange.item(0));
\r
2691 var newControlRange = dom.getBody(doc).createControlRange();
\r
2692 for (var i = 0, len = controlRange.length; i < len; ++i) {
\r
2693 newControlRange.add(controlRange.item(i));
\r
2696 newControlRange.add(rangeElement);
\r
2698 throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
\r
2700 newControlRange.select();
\r
2702 // Update the wrapped selection based on what's now in the native selection
\r
2703 updateControlSelection(sel);
\r
2706 var getSelectionRangeAt;
\r
2708 if (util.isHostMethod(testSelection, "getRangeAt")) {
\r
2709 getSelectionRangeAt = function(sel, index) {
\r
2711 return sel.getRangeAt(index);
\r
2716 } else if (selectionHasAnchorAndFocus) {
\r
2717 getSelectionRangeAt = function(sel) {
\r
2718 var doc = dom.getDocument(sel.anchorNode);
\r
2719 var range = api.createRange(doc);
\r
2720 range.setStart(sel.anchorNode, sel.anchorOffset);
\r
2721 range.setEnd(sel.focusNode, sel.focusOffset);
\r
2723 // Handle the case when the selection was selected backwards (from the end to the start in the
\r
2725 if (range.collapsed !== this.isCollapsed) {
\r
2726 range.setStart(sel.focusNode, sel.focusOffset);
\r
2727 range.setEnd(sel.anchorNode, sel.anchorOffset);
\r
2737 function WrappedSelection(selection, docSelection, win) {
\r
2738 this.nativeSelection = selection;
\r
2739 this.docSelection = docSelection;
\r
2740 this._ranges = [];
\r
2745 api.getSelection = function(win) {
\r
2746 win = win || window;
\r
2747 var sel = win[windowPropertyName];
\r
2748 var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
\r
2750 sel.nativeSelection = nativeSel;
\r
2751 sel.docSelection = docSel;
\r
2754 sel = new WrappedSelection(nativeSel, docSel, win);
\r
2755 win[windowPropertyName] = sel;
\r
2760 api.getIframeSelection = function(iframeEl) {
\r
2761 return api.getSelection(dom.getIframeWindow(iframeEl));
\r
2764 var selProto = WrappedSelection.prototype;
\r
2766 function createControlSelection(sel, ranges) {
\r
2767 // Ensure that the selection becomes of type "Control"
\r
2768 var doc = dom.getDocument(ranges[0].startContainer);
\r
2769 var controlRange = dom.getBody(doc).createControlRange();
\r
2770 for (var i = 0, el; i < rangeCount; ++i) {
\r
2771 el = getSingleElementFromRange(ranges[i]);
\r
2773 controlRange.add(el);
\r
2775 throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");
\r
2778 controlRange.select();
\r
2780 // Update the wrapped selection based on what's now in the native selection
\r
2781 updateControlSelection(sel);
\r
2784 // Selecting a range
\r
2785 if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
\r
2786 selProto.removeAllRanges = function() {
\r
2787 this.nativeSelection.removeAllRanges();
\r
2788 updateEmptySelection(this);
\r
2791 var addRangeBackwards = function(sel, range) {
\r
2792 var doc = DomRange.getRangeDocument(range);
\r
2793 var endRange = api.createRange(doc);
\r
2794 endRange.collapseToPoint(range.endContainer, range.endOffset);
\r
2795 sel.nativeSelection.addRange(getNativeRange(endRange));
\r
2796 sel.nativeSelection.extend(range.startContainer, range.startOffset);
\r
2800 if (selectionHasRangeCount) {
\r
2801 selProto.addRange = function(range, backwards) {
\r
2802 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
\r
2803 addRangeToControlSelection(this, range);
\r
2805 if (backwards && selectionHasExtend) {
\r
2806 addRangeBackwards(this, range);
\r
2808 var previousRangeCount;
\r
2809 if (selectionSupportsMultipleRanges) {
\r
2810 previousRangeCount = this.rangeCount;
\r
2812 this.removeAllRanges();
\r
2813 previousRangeCount = 0;
\r
2815 this.nativeSelection.addRange(getNativeRange(range));
\r
2817 // Check whether adding the range was successful
\r
2818 this.rangeCount = this.nativeSelection.rangeCount;
\r
2820 if (this.rangeCount == previousRangeCount + 1) {
\r
2821 // The range was added successfully
\r
2823 // Check whether the range that we added to the selection is reflected in the last range extracted from
\r
2825 if (api.config.checkSelectionRanges) {
\r
2826 var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
\r
2827 if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) {
\r
2828 // Happens in WebKit with, for example, a selection placed at the start of a text node
\r
2829 range = new WrappedRange(nativeRange);
\r
2832 this._ranges[this.rangeCount - 1] = range;
\r
2833 updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection));
\r
2834 this.isCollapsed = selectionIsCollapsed(this);
\r
2836 // The range was not added successfully. The simplest thing is to refresh
\r
2843 selProto.addRange = function(range, backwards) {
\r
2844 if (backwards && selectionHasExtend) {
\r
2845 addRangeBackwards(this, range);
\r
2847 this.nativeSelection.addRange(getNativeRange(range));
\r
2853 selProto.setRanges = function(ranges) {
\r
2854 if (implementsControlRange && ranges.length > 1) {
\r
2855 createControlSelection(this, ranges);
\r
2857 this.removeAllRanges();
\r
2858 for (var i = 0, len = ranges.length; i < len; ++i) {
\r
2859 this.addRange(ranges[i]);
\r
2863 } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&
\r
2864 implementsControlRange && useDocumentSelection) {
\r
2866 selProto.removeAllRanges = function() {
\r
2867 // Added try/catch as fix for issue #21
\r
2869 this.docSelection.empty();
\r
2871 // Check for empty() not working (issue #24)
\r
2872 if (this.docSelection.type != "None") {
\r
2873 // Work around failure to empty a control selection by instead selecting a TextRange and then
\r
2874 // calling empty()
\r
2876 if (this.anchorNode) {
\r
2877 doc = dom.getDocument(this.anchorNode);
\r
2878 } else if (this.docSelection.type == CONTROL) {
\r
2879 var controlRange = this.docSelection.createRange();
\r
2880 if (controlRange.length) {
\r
2881 doc = dom.getDocument(controlRange.item(0)).body.createTextRange();
\r
2885 var textRange = doc.body.createTextRange();
\r
2886 textRange.select();
\r
2887 this.docSelection.empty();
\r
2891 updateEmptySelection(this);
\r
2894 selProto.addRange = function(range) {
\r
2895 if (this.docSelection.type == CONTROL) {
\r
2896 addRangeToControlSelection(this, range);
\r
2898 WrappedRange.rangeToTextRange(range).select();
\r
2899 this._ranges[0] = range;
\r
2900 this.rangeCount = 1;
\r
2901 this.isCollapsed = this._ranges[0].collapsed;
\r
2902 updateAnchorAndFocusFromRange(this, range, false);
\r
2906 selProto.setRanges = function(ranges) {
\r
2907 this.removeAllRanges();
\r
2908 var rangeCount = ranges.length;
\r
2909 if (rangeCount > 1) {
\r
2910 createControlSelection(this, ranges);
\r
2911 } else if (rangeCount) {
\r
2912 this.addRange(ranges[0]);
\r
2916 module.fail("No means of selecting a Range or TextRange was found");
\r
2920 selProto.getRangeAt = function(index) {
\r
2921 if (index < 0 || index >= this.rangeCount) {
\r
2922 throw new DOMException("INDEX_SIZE_ERR");
\r
2924 return this._ranges[index];
\r
2928 var refreshSelection;
\r
2930 if (useDocumentSelection) {
\r
2931 refreshSelection = function(sel) {
\r
2933 if (api.isSelectionValid(sel.win)) {
\r
2934 range = sel.docSelection.createRange();
\r
2936 range = dom.getBody(sel.win.document).createTextRange();
\r
2937 range.collapse(true);
\r
2941 if (sel.docSelection.type == CONTROL) {
\r
2942 updateControlSelection(sel);
\r
2943 } else if (isTextRange(range)) {
\r
2944 updateFromTextRange(sel, range);
\r
2946 updateEmptySelection(sel);
\r
2949 } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") {
\r
2950 refreshSelection = function(sel) {
\r
2951 if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
\r
2952 updateControlSelection(sel);
\r
2954 sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
\r
2955 if (sel.rangeCount) {
\r
2956 for (var i = 0, len = sel.rangeCount; i < len; ++i) {
\r
2957 sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
\r
2959 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection));
\r
2960 sel.isCollapsed = selectionIsCollapsed(sel);
\r
2962 updateEmptySelection(sel);
\r
2966 } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) {
\r
2967 refreshSelection = function(sel) {
\r
2968 var range, nativeSel = sel.nativeSelection;
\r
2969 if (nativeSel.anchorNode) {
\r
2970 range = getSelectionRangeAt(nativeSel, 0);
\r
2971 sel._ranges = [range];
\r
2972 sel.rangeCount = 1;
\r
2973 updateAnchorAndFocusFromNativeSelection(sel);
\r
2974 sel.isCollapsed = selectionIsCollapsed(sel);
\r
2976 updateEmptySelection(sel);
\r
2980 module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
\r
2984 selProto.refresh = function(checkForChanges) {
\r
2985 var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
\r
2986 refreshSelection(this);
\r
2987 if (checkForChanges) {
\r
2988 var i = oldRanges.length;
\r
2989 if (i != this._ranges.length) {
\r
2993 if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {
\r
3001 // Removal of a single range
\r
3002 var removeRangeManually = function(sel, range) {
\r
3003 var ranges = sel.getAllRanges(), removed = false;
\r
3004 sel.removeAllRanges();
\r
3005 for (var i = 0, len = ranges.length; i < len; ++i) {
\r
3006 if (removed || range !== ranges[i]) {
\r
3007 sel.addRange(ranges[i]);
\r
3009 // According to the draft WHATWG Range spec, the same range may be added to the selection multiple
\r
3010 // times. removeRange should only remove the first instance, so the following ensures only the first
\r
3011 // instance is removed
\r
3015 if (!sel.rangeCount) {
\r
3016 updateEmptySelection(sel);
\r
3020 if (implementsControlRange) {
\r
3021 selProto.removeRange = function(range) {
\r
3022 if (this.docSelection.type == CONTROL) {
\r
3023 var controlRange = this.docSelection.createRange();
\r
3024 var rangeElement = getSingleElementFromRange(range);
\r
3026 // Create a new ControlRange containing all the elements in the selected ControlRange minus the
\r
3027 // element contained by the supplied range
\r
3028 var doc = dom.getDocument(controlRange.item(0));
\r
3029 var newControlRange = dom.getBody(doc).createControlRange();
\r
3030 var el, removed = false;
\r
3031 for (var i = 0, len = controlRange.length; i < len; ++i) {
\r
3032 el = controlRange.item(i);
\r
3033 if (el !== rangeElement || removed) {
\r
3034 newControlRange.add(controlRange.item(i));
\r
3039 newControlRange.select();
\r
3041 // Update the wrapped selection based on what's now in the native selection
\r
3042 updateControlSelection(this);
\r
3044 removeRangeManually(this, range);
\r
3048 selProto.removeRange = function(range) {
\r
3049 removeRangeManually(this, range);
\r
3053 // Detecting if a selection is backwards
\r
3054 var selectionIsBackwards;
\r
3055 if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {
\r
3056 selectionIsBackwards = function(sel) {
\r
3057 var backwards = false;
\r
3058 if (sel.anchorNode) {
\r
3059 backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
\r
3064 selProto.isBackwards = function() {
\r
3065 return selectionIsBackwards(this);
\r
3068 selectionIsBackwards = selProto.isBackwards = function() {
\r
3074 // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation
\r
3075 selProto.toString = function() {
\r
3077 var rangeTexts = [];
\r
3078 for (var i = 0, len = this.rangeCount; i < len; ++i) {
\r
3079 rangeTexts[i] = "" + this._ranges[i];
\r
3081 return rangeTexts.join("");
\r
3084 function assertNodeInSameDocument(sel, node) {
\r
3085 if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {
\r
3086 throw new DOMException("WRONG_DOCUMENT_ERR");
\r
3090 // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used
\r
3091 selProto.collapse = function(node, offset) {
\r
3092 assertNodeInSameDocument(this, node);
\r
3093 var range = api.createRange(dom.getDocument(node));
\r
3094 range.collapseToPoint(node, offset);
\r
3095 this.removeAllRanges();
\r
3096 this.addRange(range);
\r
3097 this.isCollapsed = true;
\r
3100 selProto.collapseToStart = function() {
\r
3101 if (this.rangeCount) {
\r
3102 var range = this._ranges[0];
\r
3103 this.collapse(range.startContainer, range.startOffset);
\r
3105 throw new DOMException("INVALID_STATE_ERR");
\r
3109 selProto.collapseToEnd = function() {
\r
3110 if (this.rangeCount) {
\r
3111 var range = this._ranges[this.rangeCount - 1];
\r
3112 this.collapse(range.endContainer, range.endOffset);
\r
3114 throw new DOMException("INVALID_STATE_ERR");
\r
3118 // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is
\r
3119 // never used by Rangy.
\r
3120 selProto.selectAllChildren = function(node) {
\r
3121 assertNodeInSameDocument(this, node);
\r
3122 var range = api.createRange(dom.getDocument(node));
\r
3123 range.selectNodeContents(node);
\r
3124 this.removeAllRanges();
\r
3125 this.addRange(range);
\r
3128 selProto.deleteFromDocument = function() {
\r
3129 // Sepcial behaviour required for Control selections
\r
3130 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
\r
3131 var controlRange = this.docSelection.createRange();
\r
3133 while (controlRange.length) {
\r
3134 element = controlRange.item(0);
\r
3135 controlRange.remove(element);
\r
3136 element.parentNode.removeChild(element);
\r
3139 } else if (this.rangeCount) {
\r
3140 var ranges = this.getAllRanges();
\r
3141 this.removeAllRanges();
\r
3142 for (var i = 0, len = ranges.length; i < len; ++i) {
\r
3143 ranges[i].deleteContents();
\r
3145 // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each
\r
3146 // range. Firefox moves the selection to where the final selected range was, so we emulate that
\r
3147 this.addRange(ranges[len - 1]);
\r
3151 // The following are non-standard extensions
\r
3152 selProto.getAllRanges = function() {
\r
3153 return this._ranges.slice(0);
\r
3156 selProto.setSingleRange = function(range) {
\r
3157 this.setRanges( [range] );
\r
3160 selProto.containsNode = function(node, allowPartial) {
\r
3161 for (var i = 0, len = this._ranges.length; i < len; ++i) {
\r
3162 if (this._ranges[i].containsNode(node, allowPartial)) {
\r
3169 selProto.toHtml = function() {
\r
3171 if (this.rangeCount) {
\r
3172 var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div");
\r
3173 for (var i = 0, len = this._ranges.length; i < len; ++i) {
\r
3174 container.appendChild(this._ranges[i].cloneContents());
\r
3176 html = container.innerHTML;
\r
3181 function inspect(sel) {
\r
3182 var rangeInspects = [];
\r
3183 var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
\r
3184 var focus = new DomPosition(sel.focusNode, sel.focusOffset);
\r
3185 var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
\r
3187 if (typeof sel.rangeCount != "undefined") {
\r
3188 for (var i = 0, len = sel.rangeCount; i < len; ++i) {
\r
3189 rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
\r
3192 return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
\r
3193 ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
\r
3197 selProto.getName = function() {
\r
3198 return "WrappedSelection";
\r
3201 selProto.inspect = function() {
\r
3202 return inspect(this);
\r
3205 selProto.detach = function() {
\r
3206 this.win[windowPropertyName] = null;
\r
3207 this.win = this.anchorNode = this.focusNode = null;
\r
3210 WrappedSelection.inspect = inspect;
\r
3212 api.Selection = WrappedSelection;
\r
3214 api.selectionPrototype = selProto;
\r
3216 api.addCreateMissingNativeApiListener(function(win) {
\r
3217 if (typeof win.getSelection == "undefined") {
\r
3218 win.getSelection = function() {
\r
3219 return api.getSelection(this);
\r