1 /** 2 * @license Rangy, a cross-browser JavaScript range and selection library 3 * http://code.google.com/p/rangy/ 4 * 5 * Copyright 2011, Tim Down 6 * Licensed under the MIT license. 7 * Version: 1.2.1 8 * Build date: 8 October 2011 9 */ 10 define( 'aloha/rangy-core', [], function(){} ); 11 var rangy = (function() { 12 13 14 var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; 15 16 var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", 17 "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"]; 18 19 var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore", 20 "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents", 21 "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"]; 22 23 var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"]; 24 25 // Subset of TextRange's full set of methods that we're interested in 26 var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark", 27 "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"]; 28 29 /*----------------------------------------------------------------------------------------------------------------*/ 30 31 // Trio of functions taken from Peter Michaux's article: 32 // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting 33 function isHostMethod(o, p) { 34 var t = typeof o[p]; 35 return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown"; 36 } 37 38 function isHostObject(o, p) { 39 return !!(typeof o[p] == OBJECT && o[p]); 40 } 41 42 function isHostProperty(o, p) { 43 return typeof o[p] != UNDEFINED; 44 } 45 46 // Creates a convenience function to save verbose repeated calls to tests functions 47 function createMultiplePropertyTest(testFunc) { 48 return function(o, props) { 49 var i = props.length; 50 while (i--) { 51 if (!testFunc(o, props[i])) { 52 return false; 53 } 54 } 55 return true; 56 }; 57 } 58 59 // Next trio of functions are a convenience to save verbose repeated calls to previous two functions 60 var areHostMethods = createMultiplePropertyTest(isHostMethod); 61 var areHostObjects = createMultiplePropertyTest(isHostObject); 62 var areHostProperties = createMultiplePropertyTest(isHostProperty); 63 64 function isTextRange(range) { 65 return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); 66 } 67 68 var api = { 69 version: "1.2.1", 70 initialized: false, 71 supported: true, 72 73 util: { 74 isHostMethod: isHostMethod, 75 isHostObject: isHostObject, 76 isHostProperty: isHostProperty, 77 areHostMethods: areHostMethods, 78 areHostObjects: areHostObjects, 79 areHostProperties: areHostProperties, 80 isTextRange: isTextRange 81 }, 82 83 features: {}, 84 85 modules: {}, 86 config: { 87 alertOnWarn: false, 88 preferTextRange: false 89 } 90 }; 91 92 function fail(reason) { 93 window.alert("Rangy not supported in your browser. Reason: " + reason); 94 api.initialized = true; 95 api.supported = false; 96 } 97 98 api.fail = fail; 99 100 function warn(msg) { 101 var warningMessage = "Rangy warning: " + msg; 102 if (api.config.alertOnWarn) { 103 window.alert(warningMessage); 104 } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) { 105 window.console.log(warningMessage); 106 } 107 } 108 109 api.warn = warn; 110 111 if ({}.hasOwnProperty) { 112 api.util.extend = function(o, props) { 113 for (var i in props) { 114 if (props.hasOwnProperty(i)) { 115 o[i] = props[i]; 116 } 117 } 118 }; 119 } else { 120 fail("hasOwnProperty not supported"); 121 } 122 123 var initListeners = []; 124 var moduleInitializers = []; 125 126 127 // Initialization 128 function init() { 129 if (api.initialized) { 130 return; 131 } 132 var testRange; 133 var implementsDomRange = false, implementsTextRange = false; 134 135 // First, perform basic feature tests 136 137 if (isHostMethod(document, "createRange")) { 138 testRange = document.createRange(); 139 if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { 140 implementsDomRange = true; 141 } 142 testRange.detach(); 143 } 144 145 var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0]; 146 147 if (body && isHostMethod(body, "createTextRange")) { 148 testRange = body.createTextRange(); 149 if (isTextRange(testRange)) { 150 implementsTextRange = true; 151 } 152 } 153 154 if (!implementsDomRange && !implementsTextRange) { 155 fail("Neither Range nor TextRange are implemented"); 156 } 157 158 api.initialized = true; 159 api.features = { 160 implementsDomRange: implementsDomRange, 161 implementsTextRange: implementsTextRange 162 }; 163 164 // Initialize modules and call init listeners 165 var allListeners = moduleInitializers.concat(initListeners); 166 for (var i = 0, len = allListeners.length; i < len; ++i) { 167 try { 168 allListeners[i](api); 169 } catch (ex) { 170 if (isHostObject(window, "console") && isHostMethod(window.console, "log")) { 171 window.console.log("Init listener threw an exception. Continuing.", ex); 172 } 173 174 } 175 } 176 } 177 178 // Allow external scripts to initialize this library in case it's loaded after the document has loaded 179 api.init = init; 180 181 // Execute listener immediately if already initialized 182 api.addInitListener = function(listener) { 183 if (api.initialized) { 184 listener(api); 185 } else { 186 initListeners.push(listener); 187 } 188 }; 189 190 var createMissingNativeApiListeners = []; 191 192 api.addCreateMissingNativeApiListener = function(listener) { 193 createMissingNativeApiListeners.push(listener); 194 }; 195 196 function createMissingNativeApi(win) { 197 win = win || window; 198 init(); 199 200 // Notify listeners 201 for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) { 202 createMissingNativeApiListeners[i](win); 203 } 204 } 205 206 api.createMissingNativeApi = createMissingNativeApi; 207 208 /** 209 * @constructor 210 */ 211 function Module(name) { 212 this.name = name; 213 this.initialized = false; 214 this.supported = false; 215 } 216 217 Module.prototype.fail = function(reason) { 218 this.initialized = true; 219 this.supported = false; 220 221 throw new Error("Module '" + this.name + "' failed to load: " + reason); 222 }; 223 224 Module.prototype.warn = function(msg) { 225 api.warn("Module " + this.name + ": " + msg); 226 }; 227 228 Module.prototype.createError = function(msg) { 229 return new Error("Error in Rangy " + this.name + " module: " + msg); 230 }; 231 232 api.createModule = function(name, initFunc) { 233 var module = new Module(name); 234 api.modules[name] = module; 235 236 moduleInitializers.push(function(api) { 237 initFunc(api, module); 238 module.initialized = true; 239 module.supported = true; 240 }); 241 }; 242 243 api.requireModules = function(modules) { 244 for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) { 245 moduleName = modules[i]; 246 module = api.modules[moduleName]; 247 if (!module || !(module instanceof Module)) { 248 throw new Error("Module '" + moduleName + "' not found"); 249 } 250 if (!module.supported) { 251 throw new Error("Module '" + moduleName + "' not supported"); 252 } 253 } 254 }; 255 256 257 /*----------------------------------------------------------------------------------------------------------------*/ 258 259 // Wait for document to load before running tests 260 261 var docReady = false; 262 263 var loadHandler = function(e) { 264 265 if (!docReady) { 266 docReady = true; 267 if (!api.initialized) { 268 init(); 269 } 270 } 271 }; 272 273 // Test whether we have window and document objects that we will need 274 if (typeof window == UNDEFINED) { 275 fail("No window found"); 276 return; 277 } 278 if (typeof document == UNDEFINED) { 279 fail("No document found"); 280 return; 281 } 282 283 if (isHostMethod(document, "addEventListener")) { 284 document.addEventListener("DOMContentLoaded", loadHandler, false); 285 } 286 287 // Add a fallback in case the DOMContentLoaded event isn't supported 288 if (isHostMethod(window, "addEventListener")) { 289 window.addEventListener("load", loadHandler, false); 290 } else if (isHostMethod(window, "attachEvent")) { 291 window.attachEvent("onload", loadHandler); 292 } else { 293 fail("Window does not have required addEventListener or attachEvent method"); 294 } 295 296 return api; 297 })(); 298 rangy.createModule("DomUtil", function(api, module) { 299 300 var UNDEF = "undefined"; 301 var util = api.util; 302 303 // Perform feature tests 304 if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) { 305 module.fail("document missing a Node creation method"); 306 } 307 308 if (!util.isHostMethod(document, "getElementsByTagName")) { 309 module.fail("document missing getElementsByTagName method"); 310 } 311 312 var el = document.createElement("div"); 313 if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || 314 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { 315 module.fail("Incomplete Element implementation"); 316 } 317 318 // innerHTML is required for Range's createContextualFragment method 319 if (!util.isHostProperty(el, "innerHTML")) { 320 module.fail("Element is missing innerHTML property"); 321 } 322 323 var textNode = document.createTextNode("test"); 324 if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] || 325 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) || 326 !util.areHostProperties(textNode, ["data"]))) { 327 module.fail("Incomplete Text Node implementation"); 328 } 329 330 /*----------------------------------------------------------------------------------------------------------------*/ 331 332 // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been 333 // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that 334 // contains just the document as a single element and the value searched for is the document. 335 var arrayContains = /*Array.prototype.indexOf ? 336 function(arr, val) { 337 return arr.indexOf(val) > -1; 338 }:*/ 339 340 function(arr, val) { 341 var i = arr.length; 342 while (i--) { 343 if (arr[i] === val) { 344 return true; 345 } 346 } 347 return false; 348 }; 349 350 // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI 351 function isHtmlNamespace(node) { 352 var ns; 353 return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml"); 354 } 355 356 function parentElement(node) { 357 var parent = node.parentNode; 358 return (parent.nodeType == 1) ? parent : null; 359 } 360 361 function getNodeIndex(node) { 362 var i = 0; 363 while( (node = node.previousSibling) ) { 364 i++; 365 } 366 return i; 367 } 368 369 function getNodeLength(node) { 370 var childNodes; 371 return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0); 372 } 373 374 function getCommonAncestor(node1, node2) { 375 var ancestors = [], n; 376 for (n = node1; n; n = n.parentNode) { 377 ancestors.push(n); 378 } 379 380 for (n = node2; n; n = n.parentNode) { 381 if (arrayContains(ancestors, n)) { 382 return n; 383 } 384 } 385 386 return null; 387 } 388 389 function isAncestorOf(ancestor, descendant, selfIsAncestor) { 390 var n = selfIsAncestor ? descendant : descendant.parentNode; 391 while (n) { 392 if (n === ancestor) { 393 return true; 394 } else { 395 n = n.parentNode; 396 } 397 } 398 return false; 399 } 400 401 function getClosestAncestorIn(node, ancestor, selfIsAncestor) { 402 var p, n = selfIsAncestor ? node : node.parentNode; 403 while (n) { 404 p = n.parentNode; 405 if (p === ancestor) { 406 return n; 407 } 408 n = p; 409 } 410 return null; 411 } 412 413 function isCharacterDataNode(node) { 414 var t = node.nodeType; 415 return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment 416 } 417 418 function insertAfter(node, precedingNode) { 419 var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; 420 if (nextNode) { 421 parent.insertBefore(node, nextNode); 422 } else { 423 parent.appendChild(node); 424 } 425 return node; 426 } 427 428 // Note that we cannot use splitText() because it is bugridden in IE 9. 429 function splitDataNode(node, index) { 430 var newNode = node.cloneNode(false); 431 newNode.deleteData(0, index); 432 node.deleteData(index, node.length - index); 433 insertAfter(newNode, node); 434 return newNode; 435 } 436 437 function getDocument(node) { 438 if (node.nodeType == 9) { 439 return node; 440 } else if (typeof node.ownerDocument != UNDEF) { 441 return node.ownerDocument; 442 } else if (typeof node.document != UNDEF) { 443 return node.document; 444 } else if (node.parentNode) { 445 return getDocument(node.parentNode); 446 } else { 447 throw new Error("getDocument: no document found for node"); 448 } 449 } 450 451 function getWindow(node) { 452 var doc = getDocument(node); 453 if (typeof doc.defaultView != UNDEF) { 454 return doc.defaultView; 455 } else if (typeof doc.parentWindow != UNDEF) { 456 return doc.parentWindow; 457 } else { 458 throw new Error("Cannot get a window object for node"); 459 } 460 } 461 462 function getIframeDocument(iframeEl) { 463 if (typeof iframeEl.contentDocument != UNDEF) { 464 return iframeEl.contentDocument; 465 } else if (typeof iframeEl.contentWindow != UNDEF) { 466 return iframeEl.contentWindow.document; 467 } else { 468 throw new Error("getIframeWindow: No Document object found for iframe element"); 469 } 470 } 471 472 function getIframeWindow(iframeEl) { 473 if (typeof iframeEl.contentWindow != UNDEF) { 474 return iframeEl.contentWindow; 475 } else if (typeof iframeEl.contentDocument != UNDEF) { 476 return iframeEl.contentDocument.defaultView; 477 } else { 478 throw new Error("getIframeWindow: No Window object found for iframe element"); 479 } 480 } 481 482 function getBody(doc) { 483 return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; 484 } 485 486 function getRootContainer(node) { 487 var parent; 488 while ( (parent = node.parentNode) ) { 489 node = parent; 490 } 491 return node; 492 } 493 494 /** 495 * This is a very ugly workaround for an IE9 issue Before comparing DOM 496 * elements "normalize" them. There are cases, where anchorNode and 497 * focusNode in a nativeselection point to DOM elements with same 498 * parentNode, same previousSibling and same nextSibling, but the nodes 499 * themselves are not the same 500 * If such nodes are compared in the comparePoints method, an error occurs. 501 * To fix this, we move to the previousSibling/nextSibling/parentNode and back, to hopefully get 502 * the "correct" node in the DOM 503 * @param node node to fix 504 * @return normalized node 505 506 */ 507 function fixNode(node) { 508 if (!node) { 509 return; 510 } 511 if (node.previousSibling) { 512 return node.previousSibling.nextSibling; 513 } else if (node.nextSibling) { 514 return node.nextSibling.previousSibling; 515 } else if (node.parentNode) { 516 return node.parentNode.firstChild; 517 } else { 518 return node; 519 } 520 } 521 522 function comparePoints(nodeA, offsetA, nodeB, offsetB) { 523 // fix the nodes before comparing them 524 nodeA = fixNode(nodeA); 525 nodeB = fixNode(nodeB); 526 // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing 527 var nodeC, root, childA, childB, n; 528 if (nodeA == nodeB) { 529 530 // Case 1: nodes are the same 531 return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1; 532 } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) { 533 534 // Case 2: node C (container B or an ancestor) is a child node of A 535 536 return offsetA <= getNodeIndex(nodeC) ? -1 : 1; 537 } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) { 538 539 // Case 3: node C (container A or an ancestor) is a child node of B 540 return getNodeIndex(nodeC) < offsetB ? -1 : 1; 541 } else { 542 543 // Case 4: containers are siblings or descendants of siblings 544 root = getCommonAncestor(nodeA, nodeB); 545 childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true); 546 childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true); 547 548 if (childA === childB) { 549 // This shouldn't be possible 550 551 throw new Error("comparePoints got to case 4 and childA and childB are the same!"); 552 } else { 553 n = root.firstChild; 554 while (n) { 555 if (n === childA) { 556 return -1; 557 } else if (n === childB) { 558 return 1; 559 } 560 n = n.nextSibling; 561 } 562 throw new Error("Should not be here!"); 563 } 564 } 565 } 566 567 function fragmentFromNodeChildren(node) { 568 var fragment = getDocument(node).createDocumentFragment(), child; 569 while ( (child = node.firstChild) ) { 570 fragment.appendChild(child); 571 } 572 573 return fragment; 574 } 575 576 function inspectNode(node) { 577 if (!node) { 578 return "[No node]"; 579 } 580 if (isCharacterDataNode(node)) { 581 return '"' + node.data + '"'; 582 } else if (node.nodeType == 1) { 583 var idAttr = node.id ? ' id="' + node.id + '"' : ""; 584 return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]"; 585 } else { 586 return node.nodeName; 587 } 588 } 589 590 /** 591 * @constructor 592 */ 593 function NodeIterator(root) { 594 this.root = root; 595 this._next = root; 596 } 597 598 NodeIterator.prototype = { 599 _current: null, 600 601 hasNext: function() { 602 return !!this._next; 603 }, 604 605 next: function() { 606 var n = this._current = this._next; 607 var child, next; 608 if (this._current) { 609 child = n.firstChild; 610 if (child) { 611 this._next = child; 612 } else { 613 next = null; 614 while ((n !== this.root) && !(next = n.nextSibling)) { 615 n = n.parentNode; 616 } 617 this._next = next; 618 } 619 } 620 return this._current; 621 }, 622 623 detach: function() { 624 this._current = this._next = this.root = null; 625 } 626 }; 627 628 function createIterator(root) { 629 return new NodeIterator(root); 630 } 631 632 /** 633 * @constructor 634 */ 635 function DomPosition(node, offset) { 636 this.node = node; 637 this.offset = offset; 638 } 639 640 DomPosition.prototype = { 641 equals: function(pos) { 642 return this.node === pos.node & this.offset == pos.offset; 643 }, 644 645 inspect: function() { 646 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; 647 } 648 }; 649 650 /** 651 * @constructor 652 */ 653 function DOMException(codeName) { 654 this.code = this[codeName]; 655 this.codeName = codeName; 656 this.message = "DOMException: " + this.codeName; 657 } 658 659 DOMException.prototype = { 660 INDEX_SIZE_ERR: 1, 661 HIERARCHY_REQUEST_ERR: 3, 662 WRONG_DOCUMENT_ERR: 4, 663 NO_MODIFICATION_ALLOWED_ERR: 7, 664 NOT_FOUND_ERR: 8, 665 NOT_SUPPORTED_ERR: 9, 666 INVALID_STATE_ERR: 11 667 }; 668 669 DOMException.prototype.toString = function() { 670 return this.message; 671 }; 672 673 api.dom = { 674 arrayContains: arrayContains, 675 isHtmlNamespace: isHtmlNamespace, 676 parentElement: parentElement, 677 getNodeIndex: getNodeIndex, 678 getNodeLength: getNodeLength, 679 getCommonAncestor: getCommonAncestor, 680 isAncestorOf: isAncestorOf, 681 getClosestAncestorIn: getClosestAncestorIn, 682 isCharacterDataNode: isCharacterDataNode, 683 insertAfter: insertAfter, 684 splitDataNode: splitDataNode, 685 getDocument: getDocument, 686 getWindow: getWindow, 687 getIframeWindow: getIframeWindow, 688 getIframeDocument: getIframeDocument, 689 getBody: getBody, 690 getRootContainer: getRootContainer, 691 comparePoints: comparePoints, 692 inspectNode: inspectNode, 693 fragmentFromNodeChildren: fragmentFromNodeChildren, 694 createIterator: createIterator, 695 DomPosition: DomPosition 696 }; 697 698 api.DOMException = DOMException; 699 });rangy.createModule("DomRange", function(api, module) { 700 api.requireModules( ["DomUtil"] ); 701 702 703 var dom = api.dom; 704 var DomPosition = dom.DomPosition; 705 var DOMException = api.DOMException; 706 707 /*----------------------------------------------------------------------------------------------------------------*/ 708 709 // Utility functions 710 711 function isNonTextPartiallySelected(node, range) { 712 return (node.nodeType != 3) && 713 (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true)); 714 } 715 716 function getRangeDocument(range) { 717 return dom.getDocument(range.startContainer); 718 } 719 720 function dispatchEvent(range, type, args) { 721 var listeners = range._listeners[type]; 722 if (listeners) { 723 for (var i = 0, len = listeners.length; i < len; ++i) { 724 listeners[i].call(range, {target: range, args: args}); 725 } 726 } 727 } 728 729 function getBoundaryBeforeNode(node) { 730 return new DomPosition(node.parentNode, dom.getNodeIndex(node)); 731 } 732 733 function getBoundaryAfterNode(node) { 734 return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1); 735 } 736 737 function insertNodeAtPosition(node, n, o) { 738 var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; 739 if (dom.isCharacterDataNode(n)) { 740 if (o == n.length) { 741 dom.insertAfter(node, n); 742 } else { 743 n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o)); 744 } 745 } else if (o >= n.childNodes.length) { 746 n.appendChild(node); 747 } else { 748 n.insertBefore(node, n.childNodes[o]); 749 } 750 return firstNodeInserted; 751 } 752 753 function cloneSubtree(iterator) { 754 var partiallySelected; 755 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { 756 partiallySelected = iterator.isPartiallySelectedSubtree(); 757 758 node = node.cloneNode(!partiallySelected); 759 760 if (partiallySelected) { 761 subIterator = iterator.getSubtreeIterator(); 762 node.appendChild(cloneSubtree(subIterator)); 763 subIterator.detach(true); 764 } 765 766 if (node.nodeType == 10) { // DocumentType 767 throw new DOMException("HIERARCHY_REQUEST_ERR"); 768 } 769 frag.appendChild(node); 770 } 771 return frag; 772 } 773 774 function iterateSubtree(rangeIterator, func, iteratorState) { 775 var it, n; 776 iteratorState = iteratorState || { stop: false }; 777 for (var node, subRangeIterator; node = rangeIterator.next(); ) { 778 //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node)); 779 if (rangeIterator.isPartiallySelectedSubtree()) { 780 // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the 781 // node selected by the Range. 782 if (func(node) === false) { 783 iteratorState.stop = true; 784 return; 785 } else { 786 787 subRangeIterator = rangeIterator.getSubtreeIterator(); 788 iterateSubtree(subRangeIterator, func, iteratorState); 789 subRangeIterator.detach(true); 790 if (iteratorState.stop) { 791 return; 792 } 793 } 794 } else { 795 // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its 796 // descendant 797 it = dom.createIterator(node); 798 while ( (n = it.next()) ) { 799 if (func(n) === false) { 800 iteratorState.stop = true; 801 return; 802 } 803 } 804 } 805 } 806 } 807 808 function deleteSubtree(iterator) { 809 var subIterator; 810 while (iterator.next()) { 811 if (iterator.isPartiallySelectedSubtree()) { 812 subIterator = iterator.getSubtreeIterator(); 813 deleteSubtree(subIterator); 814 subIterator.detach(true); 815 } else { 816 iterator.remove(); 817 } 818 } 819 } 820 821 function extractSubtree(iterator) { 822 823 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { 824 825 826 if (iterator.isPartiallySelectedSubtree()) { 827 node = node.cloneNode(false); 828 subIterator = iterator.getSubtreeIterator(); 829 node.appendChild(extractSubtree(subIterator)); 830 subIterator.detach(true); 831 } else { 832 iterator.remove(); 833 } 834 if (node.nodeType == 10) { // DocumentType 835 throw new DOMException("HIERARCHY_REQUEST_ERR"); 836 } 837 frag.appendChild(node); 838 } 839 return frag; 840 } 841 842 function getNodesInRange(range, nodeTypes, filter) { 843 //log.info("getNodesInRange, " + nodeTypes.join(",")); 844 var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; 845 var filterExists = !!filter; 846 if (filterNodeTypes) { 847 regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); 848 } 849 850 var nodes = []; 851 iterateSubtree(new RangeIterator(range, false), function(node) { 852 if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) { 853 nodes.push(node); 854 } 855 }); 856 return nodes; 857 } 858 859 function inspect(range) { 860 var name = (typeof range.getName == "undefined") ? "Range" : range.getName(); 861 return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " + 862 dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; 863 } 864 865 /*----------------------------------------------------------------------------------------------------------------*/ 866 867 // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) 868 869 /** 870 * @constructor 871 */ 872 function RangeIterator(range, clonePartiallySelectedTextNodes) { 873 this.range = range; 874 this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; 875 876 877 878 if (!range.collapsed) { 879 this.sc = range.startContainer; 880 this.so = range.startOffset; 881 this.ec = range.endContainer; 882 this.eo = range.endOffset; 883 var root = range.commonAncestorContainer; 884 885 if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) { 886 this.isSingleCharacterDataNode = true; 887 this._first = this._last = this._next = this.sc; 888 } else { 889 this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ? 890 this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true); 891 this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ? 892 this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true); 893 } 894 895 } 896 } 897 898 RangeIterator.prototype = { 899 _current: null, 900 _next: null, 901 _first: null, 902 _last: null, 903 isSingleCharacterDataNode: false, 904 905 reset: function() { 906 this._current = null; 907 this._next = this._first; 908 }, 909 910 hasNext: function() { 911 return !!this._next; 912 }, 913 914 next: function() { 915 // Move to next node 916 var current = this._current = this._next; 917 if (current) { 918 this._next = (current !== this._last) ? current.nextSibling : null; 919 920 // Check for partially selected text nodes 921 if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { 922 if (current === this.ec) { 923 924 (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); 925 } 926 if (this._current === this.sc) { 927 928 (current = current.cloneNode(true)).deleteData(0, this.so); 929 } 930 } 931 } 932 933 return current; 934 }, 935 936 remove: function() { 937 var current = this._current, start, end; 938 939 if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { 940 start = (current === this.sc) ? this.so : 0; 941 end = (current === this.ec) ? this.eo : current.length; 942 if (start != end) { 943 current.deleteData(start, end - start); 944 } 945 } else { 946 if (current.parentNode) { 947 current.parentNode.removeChild(current); 948 } else { 949 950 } 951 } 952 }, 953 954 // Checks if the current node is partially selected 955 isPartiallySelectedSubtree: function() { 956 var current = this._current; 957 return isNonTextPartiallySelected(current, this.range); 958 }, 959 960 getSubtreeIterator: function() { 961 var subRange; 962 if (this.isSingleCharacterDataNode) { 963 subRange = this.range.cloneRange(); 964 subRange.collapse(); 965 } else { 966 subRange = new Range(getRangeDocument(this.range)); 967 var current = this._current; 968 var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current); 969 970 if (dom.isAncestorOf(current, this.sc, true)) { 971 startContainer = this.sc; 972 startOffset = this.so; 973 } 974 if (dom.isAncestorOf(current, this.ec, true)) { 975 endContainer = this.ec; 976 endOffset = this.eo; 977 } 978 979 updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); 980 } 981 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); 982 }, 983 984 detach: function(detachRange) { 985 if (detachRange) { 986 this.range.detach(); 987 } 988 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; 989 } 990 }; 991 992 /*----------------------------------------------------------------------------------------------------------------*/ 993 994 // Exceptions 995 996 /** 997 * @constructor 998 */ 999 function RangeException(codeName) { 1000 this.code = this[codeName]; 1001 this.codeName = codeName; 1002 this.message = "RangeException: " + this.codeName; 1003 } 1004 1005 RangeException.prototype = { 1006 BAD_BOUNDARYPOINTS_ERR: 1, 1007 INVALID_NODE_TYPE_ERR: 2 1008 }; 1009 1010 RangeException.prototype.toString = function() { 1011 return this.message; 1012 }; 1013 1014 /*----------------------------------------------------------------------------------------------------------------*/ 1015 1016 /** 1017 * Currently iterates through all nodes in the range on creation until I think of a decent way to do it 1018 * TODO: Look into making this a proper iterator, not requiring preloading everything first 1019 * @constructor 1020 */ 1021 function RangeNodeIterator(range, nodeTypes, filter) { 1022 this.nodes = getNodesInRange(range, nodeTypes, filter); 1023 this._next = this.nodes[0]; 1024 this._position = 0; 1025 } 1026 1027 RangeNodeIterator.prototype = { 1028 _current: null, 1029 1030 hasNext: function() { 1031 return !!this._next; 1032 }, 1033 1034 next: function() { 1035 this._current = this._next; 1036 this._next = this.nodes[ ++this._position ]; 1037 return this._current; 1038 }, 1039 1040 detach: function() { 1041 this._current = this._next = this.nodes = null; 1042 } 1043 }; 1044 1045 var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; 1046 var rootContainerNodeTypes = [2, 9, 11]; 1047 var readonlyNodeTypes = [5, 6, 10, 12]; 1048 var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; 1049 var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; 1050 1051 function createAncestorFinder(nodeTypes) { 1052 return function(node, selfIsAncestor) { 1053 var t, n = selfIsAncestor ? node : node.parentNode; 1054 while (n) { 1055 t = n.nodeType; 1056 if (dom.arrayContains(nodeTypes, t)) { 1057 return n; 1058 } 1059 n = n.parentNode; 1060 } 1061 return null; 1062 }; 1063 } 1064 1065 var getRootContainer = dom.getRootContainer; 1066 var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); 1067 var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); 1068 var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); 1069 1070 function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { 1071 if (getDocTypeNotationEntityAncestor(node, allowSelf)) { 1072 throw new RangeException("INVALID_NODE_TYPE_ERR"); 1073 } 1074 } 1075 1076 function assertNotDetached(range) { 1077 if (!range.startContainer) { 1078 throw new DOMException("INVALID_STATE_ERR"); 1079 } 1080 } 1081 1082 function assertValidNodeType(node, invalidTypes) { 1083 if (!dom.arrayContains(invalidTypes, node.nodeType)) { 1084 throw new RangeException("INVALID_NODE_TYPE_ERR"); 1085 } 1086 } 1087 1088 function assertValidOffset(node, offset) { 1089 if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) { 1090 throw new DOMException("INDEX_SIZE_ERR"); 1091 } 1092 } 1093 1094 function assertSameDocumentOrFragment(node1, node2) { 1095 if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { 1096 throw new DOMException("WRONG_DOCUMENT_ERR"); 1097 } 1098 } 1099 1100 function assertNodeNotReadOnly(node) { 1101 if (getReadonlyAncestor(node, true)) { 1102 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); 1103 } 1104 } 1105 1106 function assertNode(node, codeName) { 1107 if (!node) { 1108 throw new DOMException(codeName); 1109 } 1110 } 1111 1112 function isOrphan(node) { 1113 return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true); 1114 } 1115 1116 function isValidOffset(node, offset) { 1117 return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length); 1118 } 1119 1120 function assertRangeValid(range) { 1121 assertNotDetached(range); 1122 if (isOrphan(range.startContainer) || isOrphan(range.endContainer) || 1123 !isValidOffset(range.startContainer, range.startOffset) || 1124 !isValidOffset(range.endContainer, range.endOffset)) { 1125 throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")"); 1126 } 1127 } 1128 1129 /*----------------------------------------------------------------------------------------------------------------*/ 1130 1131 // Test the browser's innerHTML support to decide how to implement createContextualFragment 1132 var styleEl = document.createElement("style"); 1133 var htmlParsingConforms = false; 1134 try { 1135 styleEl.innerHTML = "<b>x</b>"; 1136 htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node 1137 } catch (e) { 1138 // IE 6 and 7 throw 1139 } 1140 1141 api.features.htmlParsingConforms = htmlParsingConforms; 1142 1143 var createContextualFragment = htmlParsingConforms ? 1144 1145 // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See 1146 // discussion and base code for this implementation at issue 67. 1147 // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface 1148 // Thanks to Aleks Williams. 1149 function(fragmentStr) { 1150 // "Let node the context object's start's node." 1151 var node = this.startContainer; 1152 var doc = dom.getDocument(node); 1153 1154 // "If the context object's start's node is null, raise an INVALID_STATE_ERR 1155 // exception and abort these steps." 1156 if (!node) { 1157 throw new DOMException("INVALID_STATE_ERR"); 1158 } 1159 1160 // "Let element be as follows, depending on node's interface:" 1161 // Document, Document Fragment: null 1162 var el = null; 1163 1164 // "Element: node" 1165 if (node.nodeType == 1) { 1166 el = node; 1167 1168 // "Text, Comment: node's parentElement" 1169 } else if (dom.isCharacterDataNode(node)) { 1170 el = dom.parentElement(node); 1171 } 1172 1173 // "If either element is null or element's ownerDocument is an HTML document 1174 // and element's local name is "html" and element's namespace is the HTML 1175 // namespace" 1176 if (el === null || ( 1177 el.nodeName == "HTML" 1178 && dom.isHtmlNamespace(dom.getDocument(el).documentElement) 1179 && dom.isHtmlNamespace(el) 1180 )) { 1181 1182 // "let element be a new Element with "body" as its local name and the HTML 1183 // namespace as its namespace."" 1184 el = doc.createElement("body"); 1185 } else { 1186 el = el.cloneNode(false); 1187 } 1188 1189 // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." 1190 // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." 1191 // "In either case, the algorithm must be invoked with fragment as the input 1192 // and element as the context element." 1193 el.innerHTML = fragmentStr; 1194 1195 // "If this raises an exception, then abort these steps. Otherwise, let new 1196 // children be the nodes returned." 1197 1198 // "Let fragment be a new DocumentFragment." 1199 // "Append all new children to fragment." 1200 // "Return fragment." 1201 return dom.fragmentFromNodeChildren(el); 1202 } : 1203 1204 // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that 1205 // previous versions of Rangy used (with the exception of using a body element rather than a div) 1206 function(fragmentStr) { 1207 assertNotDetached(this); 1208 var doc = getRangeDocument(this); 1209 var el = doc.createElement("body"); 1210 el.innerHTML = fragmentStr; 1211 1212 return dom.fragmentFromNodeChildren(el); 1213 }; 1214 1215 /*----------------------------------------------------------------------------------------------------------------*/ 1216 1217 var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", 1218 "commonAncestorContainer"]; 1219 1220 var s2s = 0, s2e = 1, e2e = 2, e2s = 3; 1221 var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; 1222 1223 function RangePrototype() {} 1224 1225 RangePrototype.prototype = { 1226 attachListener: function(type, listener) { 1227 this._listeners[type].push(listener); 1228 }, 1229 1230 compareBoundaryPoints: function(how, range) { 1231 assertRangeValid(this); 1232 assertSameDocumentOrFragment(this.startContainer, range.startContainer); 1233 1234 var nodeA, offsetA, nodeB, offsetB; 1235 var prefixA = (how == e2s || how == s2s) ? "start" : "end"; 1236 var prefixB = (how == s2e || how == s2s) ? "start" : "end"; 1237 nodeA = this[prefixA + "Container"]; 1238 offsetA = this[prefixA + "Offset"]; 1239 nodeB = range[prefixB + "Container"]; 1240 offsetB = range[prefixB + "Offset"]; 1241 return dom.comparePoints(nodeA, offsetA, nodeB, offsetB); 1242 }, 1243 1244 insertNode: function(node) { 1245 assertRangeValid(this); 1246 assertValidNodeType(node, insertableNodeTypes); 1247 assertNodeNotReadOnly(this.startContainer); 1248 1249 if (dom.isAncestorOf(node, this.startContainer, true)) { 1250 throw new DOMException("HIERARCHY_REQUEST_ERR"); 1251 } 1252 1253 // No check for whether the container of the start of the Range is of a type that does not allow 1254 // children of the type of node: the browser's DOM implementation should do this for us when we attempt 1255 // to add the node 1256 1257 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); 1258 this.setStartBefore(firstNodeInserted); 1259 }, 1260 1261 cloneContents: function() { 1262 assertRangeValid(this); 1263 1264 var clone, frag; 1265 if (this.collapsed) { 1266 return getRangeDocument(this).createDocumentFragment(); 1267 } else { 1268 if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) { 1269 clone = this.startContainer.cloneNode(true); 1270 clone.data = clone.data.slice(this.startOffset, this.endOffset); 1271 frag = getRangeDocument(this).createDocumentFragment(); 1272 frag.appendChild(clone); 1273 return frag; 1274 } else { 1275 var iterator = new RangeIterator(this, true); 1276 clone = cloneSubtree(iterator); 1277 iterator.detach(); 1278 } 1279 return clone; 1280 } 1281 }, 1282 1283 canSurroundContents: function() { 1284 assertRangeValid(this); 1285 assertNodeNotReadOnly(this.startContainer); 1286 assertNodeNotReadOnly(this.endContainer); 1287 1288 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects 1289 // no non-text nodes. 1290 var iterator = new RangeIterator(this, true); 1291 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || 1292 (iterator._last && isNonTextPartiallySelected(iterator._last, this))); 1293 iterator.detach(); 1294 return !boundariesInvalid; 1295 }, 1296 1297 surroundContents: function(node) { 1298 assertValidNodeType(node, surroundNodeTypes); 1299 1300 if (!this.canSurroundContents()) { 1301 throw new RangeException("BAD_BOUNDARYPOINTS_ERR"); 1302 } 1303 1304 // Extract the contents 1305 var content = this.extractContents(); 1306 1307 // Clear the children of the node 1308 if (node.hasChildNodes()) { 1309 while (node.lastChild) { 1310 node.removeChild(node.lastChild); 1311 } 1312 } 1313 1314 // Insert the new node and add the extracted contents 1315 insertNodeAtPosition(node, this.startContainer, this.startOffset); 1316 node.appendChild(content); 1317 1318 this.selectNode(node); 1319 }, 1320 1321 cloneRange: function() { 1322 assertRangeValid(this); 1323 var range = new Range(getRangeDocument(this)); 1324 var i = rangeProperties.length, prop; 1325 while (i--) { 1326 prop = rangeProperties[i]; 1327 range[prop] = this[prop]; 1328 } 1329 return range; 1330 }, 1331 1332 toString: function() { 1333 assertRangeValid(this); 1334 var sc = this.startContainer; 1335 if (sc === this.endContainer && dom.isCharacterDataNode(sc)) { 1336 return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; 1337 } else { 1338 var textBits = [], iterator = new RangeIterator(this, true); 1339 1340 iterateSubtree(iterator, function(node) { 1341 // Accept only text or CDATA nodes, not comments 1342 1343 if (node.nodeType == 3 || node.nodeType == 4) { 1344 textBits.push(node.data); 1345 } 1346 }); 1347 iterator.detach(); 1348 return textBits.join(""); 1349 } 1350 }, 1351 1352 // The methods below are all non-standard. The following batch were introduced by Mozilla but have since 1353 // been removed from Mozilla. 1354 1355 compareNode: function(node) { 1356 assertRangeValid(this); 1357 1358 var parent = node.parentNode; 1359 var nodeIndex = dom.getNodeIndex(node); 1360 1361 if (!parent) { 1362 throw new DOMException("NOT_FOUND_ERR"); 1363 } 1364 1365 var startComparison = this.comparePoint(parent, nodeIndex), 1366 endComparison = this.comparePoint(parent, nodeIndex + 1); 1367 1368 if (startComparison < 0) { // Node starts before 1369 return (endComparison > 0) ? n_b_a : n_b; 1370 } else { 1371 return (endComparison > 0) ? n_a : n_i; 1372 } 1373 }, 1374 1375 comparePoint: function(node, offset) { 1376 assertRangeValid(this); 1377 assertNode(node, "HIERARCHY_REQUEST_ERR"); 1378 assertSameDocumentOrFragment(node, this.startContainer); 1379 1380 if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { 1381 return -1; 1382 } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { 1383 return 1; 1384 } 1385 return 0; 1386 }, 1387 1388 createContextualFragment: createContextualFragment, 1389 1390 toHtml: function() { 1391 assertRangeValid(this); 1392 var container = getRangeDocument(this).createElement("div"); 1393 container.appendChild(this.cloneContents()); 1394 return container.innerHTML; 1395 }, 1396 1397 // touchingIsIntersecting determines whether this method considers a node that borders a range intersects 1398 // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) 1399 intersectsNode: function(node, touchingIsIntersecting) { 1400 assertRangeValid(this); 1401 assertNode(node, "NOT_FOUND_ERR"); 1402 if (dom.getDocument(node) !== getRangeDocument(this)) { 1403 return false; 1404 } 1405 1406 var parent = node.parentNode, offset = dom.getNodeIndex(node); 1407 assertNode(parent, "NOT_FOUND_ERR"); 1408 1409 var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset), 1410 endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset); 1411 1412 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; 1413 }, 1414 1415 1416 isPointInRange: function(node, offset) { 1417 assertRangeValid(this); 1418 assertNode(node, "HIERARCHY_REQUEST_ERR"); 1419 assertSameDocumentOrFragment(node, this.startContainer); 1420 1421 return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && 1422 (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); 1423 }, 1424 1425 // The methods below are non-standard and invented by me. 1426 1427 // Sharing a boundary start-to-end or end-to-start does not count as intersection. 1428 intersectsRange: function(range, touchingIsIntersecting) { 1429 assertRangeValid(this); 1430 1431 if (getRangeDocument(range) != getRangeDocument(this)) { 1432 throw new DOMException("WRONG_DOCUMENT_ERR"); 1433 } 1434 1435 var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset), 1436 endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset); 1437 1438 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; 1439 }, 1440 1441 intersection: function(range) { 1442 if (this.intersectsRange(range)) { 1443 var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), 1444 endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); 1445 1446 var intersectionRange = this.cloneRange(); 1447 1448 if (startComparison == -1) { 1449 intersectionRange.setStart(range.startContainer, range.startOffset); 1450 } 1451 if (endComparison == 1) { 1452 intersectionRange.setEnd(range.endContainer, range.endOffset); 1453 } 1454 return intersectionRange; 1455 } 1456 return null; 1457 }, 1458 1459 union: function(range) { 1460 if (this.intersectsRange(range, true)) { 1461 var unionRange = this.cloneRange(); 1462 if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { 1463 unionRange.setStart(range.startContainer, range.startOffset); 1464 } 1465 1466 if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { 1467 unionRange.setEnd(range.endContainer, range.endOffset); 1468 } 1469 return unionRange; 1470 } else { 1471 throw new RangeException("Ranges do not intersect"); 1472 } 1473 1474 }, 1475 1476 containsNode: function(node, allowPartial) { 1477 if (allowPartial) { 1478 return this.intersectsNode(node, false); 1479 } else { 1480 return this.compareNode(node) == n_i; 1481 } 1482 }, 1483 1484 containsNodeContents: function(node) { 1485 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0; 1486 }, 1487 1488 containsRange: function(range) { 1489 return this.intersection(range).equals(range); 1490 }, 1491 1492 containsNodeText: function(node) { 1493 var nodeRange = this.cloneRange(); 1494 nodeRange.selectNode(node); 1495 var textNodes = nodeRange.getNodes([3]); 1496 if (textNodes.length > 0) { 1497 nodeRange.setStart(textNodes[0], 0); 1498 var lastTextNode = textNodes.pop(); 1499 nodeRange.setEnd(lastTextNode, lastTextNode.length); 1500 var contains = this.containsRange(nodeRange); 1501 nodeRange.detach(); 1502 return contains; 1503 } else { 1504 return this.containsNodeContents(node); 1505 } 1506 }, 1507 1508 createNodeIterator: function(nodeTypes, filter) { 1509 assertRangeValid(this); 1510 return new RangeNodeIterator(this, nodeTypes, filter); 1511 }, 1512 1513 getNodes: function(nodeTypes, filter) { 1514 assertRangeValid(this); 1515 return getNodesInRange(this, nodeTypes, filter); 1516 }, 1517 1518 getDocument: function() { 1519 return getRangeDocument(this); 1520 }, 1521 1522 collapseBefore: function(node) { 1523 assertNotDetached(this); 1524 1525 this.setEndBefore(node); 1526 this.collapse(false); 1527 }, 1528 1529 collapseAfter: function(node) { 1530 assertNotDetached(this); 1531 1532 this.setStartAfter(node); 1533 this.collapse(true); 1534 }, 1535 1536 getName: function() { 1537 return "DomRange"; 1538 }, 1539 1540 equals: function(range) { 1541 return Range.rangesEqual(this, range); 1542 }, 1543 1544 inspect: function() { 1545 return inspect(this); 1546 } 1547 }; 1548 1549 function copyComparisonConstantsToObject(obj) { 1550 obj.START_TO_START = s2s; 1551 obj.START_TO_END = s2e; 1552 obj.END_TO_END = e2e; 1553 obj.END_TO_START = e2s; 1554 1555 obj.NODE_BEFORE = n_b; 1556 obj.NODE_AFTER = n_a; 1557 obj.NODE_BEFORE_AND_AFTER = n_b_a; 1558 obj.NODE_INSIDE = n_i; 1559 } 1560 1561 function copyComparisonConstants(constructor) { 1562 copyComparisonConstantsToObject(constructor); 1563 copyComparisonConstantsToObject(constructor.prototype); 1564 } 1565 1566 function createRangeContentRemover(remover, boundaryUpdater) { 1567 return function() { 1568 assertRangeValid(this); 1569 1570 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; 1571 1572 var iterator = new RangeIterator(this, true); 1573 1574 // Work out where to position the range after content removal 1575 var node, boundary; 1576 if (sc !== root) { 1577 node = dom.getClosestAncestorIn(sc, root, true); 1578 boundary = getBoundaryAfterNode(node); 1579 sc = boundary.node; 1580 so = boundary.offset; 1581 } 1582 1583 // Check none of the range is read-only 1584 iterateSubtree(iterator, assertNodeNotReadOnly); 1585 1586 iterator.reset(); 1587 1588 // Remove the content 1589 var returnValue = remover(iterator); 1590 iterator.detach(); 1591 1592 // Move to the new position 1593 boundaryUpdater(this, sc, so, sc, so); 1594 1595 return returnValue; 1596 }; 1597 } 1598 1599 1600 function createPrototypeRange(constructor, boundaryUpdater, detacher) { 1601 function createBeforeAfterNodeSetter(isBefore, isStart) { 1602 return function(node) { 1603 assertNotDetached(this); 1604 assertValidNodeType(node, beforeAfterNodeTypes); 1605 assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); 1606 1607 var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); 1608 (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); 1609 }; 1610 } 1611 1612 function setRangeStart(range, node, offset) { 1613 var ec = range.endContainer, eo = range.endOffset; 1614 if (node !== range.startContainer || offset !== this.startOffset) { 1615 // Check the root containers of the range and the new boundary, and also check whether the new boundary 1616 // is after the current end. In either case, collapse the range to the new position 1617 if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) { 1618 ec = node; 1619 eo = offset; 1620 } 1621 boundaryUpdater(range, node, offset, ec, eo); 1622 } 1623 } 1624 1625 function setRangeEnd(range, node, offset) { 1626 var sc = range.startContainer, so = range.startOffset; 1627 if (node !== range.endContainer || offset !== this.endOffset) { 1628 // Check the root containers of the range and the new boundary, and also check whether the new boundary 1629 // is after the current end. In either case, collapse the range to the new position 1630 if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) { 1631 sc = node; 1632 so = offset; 1633 } 1634 boundaryUpdater(range, sc, so, node, offset); 1635 } 1636 } 1637 1638 function setRangeStartAndEnd(range, node, offset) { 1639 if (node !== range.startContainer || offset !== this.startOffset || node !== range.endContainer || offset !== this.endOffset) { 1640 boundaryUpdater(range, node, offset, node, offset); 1641 } 1642 } 1643 1644 constructor.prototype = new RangePrototype(); 1645 1646 1647 api.util.extend(constructor.prototype, { 1648 setStart: function(node, offset) { 1649 assertNotDetached(this); 1650 assertNoDocTypeNotationEntityAncestor(node, true); 1651 assertValidOffset(node, offset); 1652 1653 1654 setRangeStart(this, node, offset); 1655 }, 1656 1657 1658 setEnd: function(node, offset) { 1659 assertNotDetached(this); 1660 assertNoDocTypeNotationEntityAncestor(node, true); 1661 assertValidOffset(node, offset); 1662 1663 setRangeEnd(this, node, offset); 1664 }, 1665 1666 setStartBefore: createBeforeAfterNodeSetter(true, true), 1667 setStartAfter: createBeforeAfterNodeSetter(false, true), 1668 setEndBefore: createBeforeAfterNodeSetter(true, false), 1669 setEndAfter: createBeforeAfterNodeSetter(false, false), 1670 1671 collapse: function(isStart) { 1672 assertRangeValid(this); 1673 if (isStart) { 1674 boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); 1675 } else { 1676 boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); 1677 } 1678 }, 1679 1680 selectNodeContents: function(node) { 1681 // This doesn't seem well specified: the spec talks only about selecting the node's contents, which 1682 // could be taken to mean only its children. However, browsers implement this the same as selectNode for 1683 // text nodes, so I shall do likewise 1684 assertNotDetached(this); 1685 assertNoDocTypeNotationEntityAncestor(node, true); 1686 1687 boundaryUpdater(this, node, 0, node, dom.getNodeLength(node)); 1688 }, 1689 1690 selectNode: function(node) { 1691 assertNotDetached(this); 1692 assertNoDocTypeNotationEntityAncestor(node, false); 1693 assertValidNodeType(node, beforeAfterNodeTypes); 1694 1695 var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); 1696 boundaryUpdater(this, start.node, start.offset, end.node, end.offset); 1697 }, 1698 1699 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), 1700 1701 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), 1702 1703 canSurroundContents: function() { 1704 assertRangeValid(this); 1705 assertNodeNotReadOnly(this.startContainer); 1706 assertNodeNotReadOnly(this.endContainer); 1707 1708 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects 1709 // no non-text nodes. 1710 var iterator = new RangeIterator(this, true); 1711 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || 1712 (iterator._last && isNonTextPartiallySelected(iterator._last, this))); 1713 iterator.detach(); 1714 return !boundariesInvalid; 1715 }, 1716 1717 detach: function() { 1718 detacher(this); 1719 }, 1720 1721 splitBoundaries: function() { 1722 assertRangeValid(this); 1723 1724 1725 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; 1726 var startEndSame = (sc === ec); 1727 1728 if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { 1729 dom.splitDataNode(ec, eo); 1730 1731 } 1732 1733 if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) { 1734 1735 sc = dom.splitDataNode(sc, so); 1736 if (startEndSame) { 1737 eo -= so; 1738 ec = sc; 1739 } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) { 1740 eo++; 1741 } 1742 so = 0; 1743 1744 } 1745 boundaryUpdater(this, sc, so, ec, eo); 1746 }, 1747 1748 normalizeBoundaries: function() { 1749 assertRangeValid(this); 1750 1751 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; 1752 1753 var mergeForward = function(node) { 1754 var sibling = node.nextSibling; 1755 if (sibling && sibling.nodeType == node.nodeType) { 1756 ec = node; 1757 eo = node.length; 1758 node.appendData(sibling.data); 1759 sibling.parentNode.removeChild(sibling); 1760 } 1761 }; 1762 1763 var mergeBackward = function(node) { 1764 var sibling = node.previousSibling; 1765 if (sibling && sibling.nodeType == node.nodeType) { 1766 sc = node; 1767 var nodeLength = node.length; 1768 so = sibling.length; 1769 node.insertData(0, sibling.data); 1770 sibling.parentNode.removeChild(sibling); 1771 if (sc == ec) { 1772 eo += so; 1773 ec = sc; 1774 } else if (ec == node.parentNode) { 1775 var nodeIndex = dom.getNodeIndex(node); 1776 if (eo == nodeIndex) { 1777 ec = node; 1778 eo = nodeLength; 1779 } else if (eo > nodeIndex) { 1780 eo--; 1781 } 1782 } 1783 } 1784 }; 1785 1786 var normalizeStart = true; 1787 1788 if (dom.isCharacterDataNode(ec)) { 1789 if (ec.length == eo) { 1790 mergeForward(ec); 1791 } 1792 } else { 1793 if (eo > 0) { 1794 var endNode = ec.childNodes[eo - 1]; 1795 if (endNode && dom.isCharacterDataNode(endNode)) { 1796 mergeForward(endNode); 1797 } 1798 } 1799 normalizeStart = !this.collapsed; 1800 } 1801 1802 if (normalizeStart) { 1803 if (dom.isCharacterDataNode(sc)) { 1804 if (so == 0) { 1805 mergeBackward(sc); 1806 } 1807 } else { 1808 if (so < sc.childNodes.length) { 1809 var startNode = sc.childNodes[so]; 1810 if (startNode && dom.isCharacterDataNode(startNode)) { 1811 mergeBackward(startNode); 1812 } 1813 } 1814 } 1815 } else { 1816 sc = ec; 1817 so = eo; 1818 } 1819 1820 boundaryUpdater(this, sc, so, ec, eo); 1821 }, 1822 1823 collapseToPoint: function(node, offset) { 1824 assertNotDetached(this); 1825 1826 assertNoDocTypeNotationEntityAncestor(node, true); 1827 assertValidOffset(node, offset); 1828 1829 setRangeStartAndEnd(this, node, offset); 1830 } 1831 }); 1832 1833 copyComparisonConstants(constructor); 1834 } 1835 1836 /*----------------------------------------------------------------------------------------------------------------*/ 1837 1838 // Updates commonAncestorContainer and collapsed after boundary change 1839 function updateCollapsedAndCommonAncestor(range) { 1840 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); 1841 range.commonAncestorContainer = range.collapsed ? 1842 range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer); 1843 } 1844 1845 function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { 1846 var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset); 1847 var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset); 1848 1849 range.startContainer = startContainer; 1850 range.startOffset = startOffset; 1851 range.endContainer = endContainer; 1852 range.endOffset = endOffset; 1853 1854 updateCollapsedAndCommonAncestor(range); 1855 dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved}); 1856 } 1857 1858 function detach(range) { 1859 assertNotDetached(range); 1860 range.startContainer = range.startOffset = range.endContainer = range.endOffset = null; 1861 range.collapsed = range.commonAncestorContainer = null; 1862 dispatchEvent(range, "detach", null); 1863 range._listeners = null; 1864 } 1865 1866 /** 1867 * @constructor 1868 */ 1869 function Range(doc) { 1870 this.startContainer = doc; 1871 this.startOffset = 0; 1872 this.endContainer = doc; 1873 this.endOffset = 0; 1874 this._listeners = { 1875 boundarychange: [], 1876 detach: [] 1877 }; 1878 updateCollapsedAndCommonAncestor(this); 1879 } 1880 1881 createPrototypeRange(Range, updateBoundaries, detach); 1882 1883 api.rangePrototype = RangePrototype.prototype; 1884 1885 Range.rangeProperties = rangeProperties; 1886 Range.RangeIterator = RangeIterator; 1887 Range.copyComparisonConstants = copyComparisonConstants; 1888 Range.createPrototypeRange = createPrototypeRange; 1889 Range.inspect = inspect; 1890 Range.getRangeDocument = getRangeDocument; 1891 Range.rangesEqual = function(r1, r2) { 1892 return r1.startContainer === r2.startContainer && 1893 r1.startOffset === r2.startOffset && 1894 r1.endContainer === r2.endContainer && 1895 r1.endOffset === r2.endOffset; 1896 }; 1897 1898 api.DomRange = Range; 1899 api.RangeException = RangeException; 1900 });rangy.createModule("WrappedRange", function(api, module) { 1901 api.requireModules( ["DomUtil", "DomRange"] ); 1902 1903 /** 1904 * @constructor 1905 */ 1906 var WrappedRange; 1907 var dom = api.dom; 1908 var DomPosition = dom.DomPosition; 1909 var DomRange = api.DomRange; 1910 1911 1912 1913 /*----------------------------------------------------------------------------------------------------------------*/ 1914 1915 /* 1916 This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() 1917 method. For example, in the following (where pipes denote the selection boundaries): 1918 1919 <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul> 1920 1921 1922 var range = document.selection.createRange(); 1923 alert(range.parentElement().id); // Should alert "ul" but alerts "b" 1924 1925 This method returns the common ancestor node of the following: 1926 - the parentElement() of the textRange 1927 - the parentElement() of the textRange after calling collapse(true) 1928 - the parentElement() of the textRange after calling collapse(false) 1929 */ 1930 function getTextRangeContainerElement(textRange) { 1931 var parentEl = textRange.parentElement(); 1932 1933 var range = textRange.duplicate(); 1934 range.collapse(true); 1935 var startEl = range.parentElement(); 1936 range = textRange.duplicate(); 1937 range.collapse(false); 1938 var endEl = range.parentElement(); 1939 var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl); 1940 1941 return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); 1942 } 1943 1944 function textRangeIsCollapsed(textRange) { 1945 return textRange.compareEndPoints("StartToEnd", textRange) == 0; 1946 } 1947 1948 // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as 1949 // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has 1950 // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling 1951 // for inputs and images, plus optimizations. 1952 function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) { 1953 var workingRange = textRange.duplicate(); 1954 1955 workingRange.collapse(isStart); 1956 var containerElement = workingRange.parentElement(); 1957 1958 // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so 1959 // check for that 1960 // TODO: Find out when. Workaround for wholeRangeContainerElement may break this 1961 if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) { 1962 containerElement = wholeRangeContainerElement; 1963 1964 } 1965 1966 1967 1968 // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and 1969 // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx 1970 if (!containerElement.canHaveHTML) { 1971 return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); 1972 } 1973 1974 var workingNode = dom.getDocument(containerElement).createElement("span"); 1975 var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; 1976 var previousNode, nextNode, boundaryPosition, boundaryNode; 1977 1978 // Move the working range through the container's children, starting at the end and working backwards, until the 1979 // working range reaches or goes past the boundary we're interested in 1980 do { 1981 containerElement.insertBefore(workingNode, workingNode.previousSibling); 1982 workingRange.moveToElementText(workingNode); 1983 } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 && 1984 workingNode.previousSibling); 1985 1986 // We've now reached or gone past the boundary of the text range we're interested in 1987 // so have identified the node we want 1988 boundaryNode = workingNode.nextSibling; 1989 1990 if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) { 1991 // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the 1992 // node containing the text range's boundary, so we move the end of the working range to the boundary point 1993 // and measure the length of its text to get the boundary's offset within the node. 1994 workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); 1995 1996 1997 var offset; 1998 1999 if (/[\r\n]/.test(boundaryNode.data)) { 2000 /* 2001 For the particular case of a boundary within a text node containing line breaks (within a <pre> element, 2002 for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts: 2003 2004 - Each line break is represented as \r in the text node's data/nodeValue properties 2005 - Each line break is represented as \r\n in the TextRange's 'text' property 2006 - The 'text' property of the TextRange does not contain trailing line breaks 2007 2008 To get round the problem presented by the final fact above, we can use the fact that TextRange's 2009 moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily 2010 the same as the number of characters it was instructed to move. The simplest approach is to use this to 2011 store the characters moved when moving both the start and end of the range to the start of the document 2012 body and subtracting the start offset from the end offset (the "move-negative-gazillion" method). 2013 However, this is extremely slow when the document is large and the range is near the end of it. Clearly 2014 doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same 2015 problem. 2016 2017 Another approach that works is to use moveStart() to move the start boundary of the range up to the end 2018 boundary one character at a time and incrementing a counter with the value returned by the moveStart() 2019 call. However, the check for whether the start boundary has reached the end boundary is expensive, so 2020 this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of 2021 the range within the document). 2022 2023 The method below is a hybrid of the two methods above. It uses the fact that a string containing the 2024 TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the 2025 text of the TextRange, so the start of the range is moved that length initially and then a character at 2026 a time to make up for any trailing line breaks not contained in the 'text' property. This has good 2027 performance in most situations compared to the previous two methods. 2028 */ 2029 var tempRange = workingRange.duplicate(); 2030 var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length; 2031 2032 offset = tempRange.moveStart("character", rangeLength); 2033 while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) { 2034 offset++; 2035 tempRange.moveStart("character", 1); 2036 } 2037 } else { 2038 offset = workingRange.text.length; 2039 } 2040 boundaryPosition = new DomPosition(boundaryNode, offset); 2041 } else { 2042 2043 2044 // If the boundary immediately follows a character data node and this is the end boundary, we should favour 2045 // a position within that, and likewise for a start boundary preceding a character data node 2046 previousNode = (isCollapsed || !isStart) && workingNode.previousSibling; 2047 nextNode = (isCollapsed || isStart) && workingNode.nextSibling; 2048 2049 2050 2051 if (nextNode && dom.isCharacterDataNode(nextNode)) { 2052 boundaryPosition = new DomPosition(nextNode, 0); 2053 } else if (previousNode && dom.isCharacterDataNode(previousNode)) { 2054 boundaryPosition = new DomPosition(previousNode, previousNode.length); 2055 } else { 2056 boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode)); 2057 } 2058 } 2059 2060 // Clean up 2061 workingNode.parentNode.removeChild(workingNode); 2062 2063 return boundaryPosition; 2064 } 2065 2066 // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node. 2067 // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange 2068 // (http://code.google.com/p/ierange/) 2069 function createBoundaryTextRange(boundaryPosition, isStart) { 2070 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset; 2071 var doc = dom.getDocument(boundaryPosition.node); 2072 var workingNode, childNodes, workingRange = doc.body.createTextRange(); 2073 var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node); 2074 2075 if (nodeIsDataNode) { 2076 boundaryNode = boundaryPosition.node; 2077 boundaryParent = boundaryNode.parentNode; 2078 } else { 2079 childNodes = boundaryPosition.node.childNodes; 2080 boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null; 2081 boundaryParent = boundaryPosition.node; 2082 } 2083 2084 // Position the range immediately before the node containing the boundary 2085 workingNode = doc.createElement("span"); 2086 2087 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the 2088 // element rather than immediately before or after it, which is what we want 2089 workingNode.innerHTML = "feff;"; 2090 2091 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report 2092 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12 2093 if (boundaryNode) { 2094 boundaryParent.insertBefore(workingNode, boundaryNode); 2095 } else { 2096 boundaryParent.appendChild(workingNode); 2097 } 2098 2099 workingRange.moveToElementText(workingNode); 2100 workingRange.collapse(!isStart); 2101 2102 // Clean up 2103 boundaryParent.removeChild(workingNode); 2104 2105 // Move the working range to the text offset, if required 2106 if (nodeIsDataNode) { 2107 workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset); 2108 } 2109 2110 return workingRange; 2111 } 2112 2113 /*----------------------------------------------------------------------------------------------------------------*/ 2114 2115 if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) { 2116 // This is a wrapper around the browser's native DOM Range. It has two aims: 2117 // - Provide workarounds for specific browser bugs 2118 // - provide convenient extensions, which are inherited from Rangy's DomRange 2119 2120 (function() { 2121 var rangeProto; 2122 var rangeProperties = DomRange.rangeProperties; 2123 var canSetRangeStartAfterEnd; 2124 2125 function updateRangeProperties(range) { 2126 var i = rangeProperties.length, prop; 2127 while (i--) { 2128 prop = rangeProperties[i]; 2129 range[prop] = range.nativeRange[prop]; 2130 } 2131 } 2132 2133 function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) { 2134 var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset); 2135 var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset); 2136 2137 // Always set both boundaries for the benefit of IE9 (see issue 35) 2138 if (startMoved || endMoved) { 2139 range.setEnd(endContainer, endOffset); 2140 range.setStart(startContainer, startOffset); 2141 } 2142 } 2143 2144 function detach(range) { 2145 range.nativeRange.detach(); 2146 range.detached = true; 2147 var i = rangeProperties.length, prop; 2148 while (i--) { 2149 prop = rangeProperties[i]; 2150 range[prop] = null; 2151 } 2152 } 2153 2154 var createBeforeAfterNodeSetter; 2155 2156 WrappedRange = function(range) { 2157 if (!range) { 2158 throw new Error("Range must be specified"); 2159 } 2160 this.nativeRange = range; 2161 updateRangeProperties(this); 2162 }; 2163 2164 DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach); 2165 2166 rangeProto = WrappedRange.prototype; 2167 2168 rangeProto.selectNode = function(node) { 2169 this.nativeRange.selectNode(node); 2170 updateRangeProperties(this); 2171 }; 2172 2173 rangeProto.deleteContents = function() { 2174 this.nativeRange.deleteContents(); 2175 updateRangeProperties(this); 2176 }; 2177 2178 rangeProto.extractContents = function() { 2179 var frag = this.nativeRange.extractContents(); 2180 updateRangeProperties(this); 2181 return frag; 2182 }; 2183 2184 rangeProto.cloneContents = function() { 2185 return this.nativeRange.cloneContents(); 2186 }; 2187 2188 // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still 2189 // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for 2190 // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of 2191 // insertNode, which works but is almost certainly slower than the native implementation. 2192 /* 2193 rangeProto.insertNode = function(node) { 2194 this.nativeRange.insertNode(node); 2195 updateRangeProperties(this); 2196 }; 2197 */ 2198 2199 rangeProto.surroundContents = function(node) { 2200 this.nativeRange.surroundContents(node); 2201 updateRangeProperties(this); 2202 }; 2203 2204 rangeProto.collapse = function(isStart) { 2205 this.nativeRange.collapse(isStart); 2206 updateRangeProperties(this); 2207 }; 2208 2209 rangeProto.cloneRange = function() { 2210 return new WrappedRange(this.nativeRange.cloneRange()); 2211 }; 2212 2213 rangeProto.refresh = function() { 2214 updateRangeProperties(this); 2215 }; 2216 2217 rangeProto.toString = function() { 2218 return this.nativeRange.toString(); 2219 }; 2220 2221 // Create test range and node for feature detection 2222 2223 var testTextNode = document.createTextNode("test"); 2224 dom.getBody(document).appendChild(testTextNode); 2225 var range = document.createRange(); 2226 2227 /*--------------------------------------------------------------------------------------------------------*/ 2228 2229 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and 2230 // correct for it 2231 2232 range.setStart(testTextNode, 0); 2233 range.setEnd(testTextNode, 0); 2234 2235 try { 2236 range.setStart(testTextNode, 1); 2237 canSetRangeStartAfterEnd = true; 2238 2239 rangeProto.setStart = function(node, offset) { 2240 this.nativeRange.setStart(node, offset); 2241 updateRangeProperties(this); 2242 }; 2243 2244 rangeProto.setEnd = function(node, offset) { 2245 this.nativeRange.setEnd(node, offset); 2246 updateRangeProperties(this); 2247 }; 2248 2249 createBeforeAfterNodeSetter = function(name) { 2250 return function(node) { 2251 this.nativeRange[name](node); 2252 updateRangeProperties(this); 2253 }; 2254 }; 2255 2256 } catch(ex) { 2257 2258 2259 canSetRangeStartAfterEnd = false; 2260 2261 rangeProto.setStart = function(node, offset) { 2262 try { 2263 this.nativeRange.setStart(node, offset); 2264 } catch (ex) { 2265 this.nativeRange.setEnd(node, offset); 2266 this.nativeRange.setStart(node, offset); 2267 } 2268 updateRangeProperties(this); 2269 }; 2270 2271 rangeProto.setEnd = function(node, offset) { 2272 try { 2273 this.nativeRange.setEnd(node, offset); 2274 } catch (ex) { 2275 this.nativeRange.setStart(node, offset); 2276 this.nativeRange.setEnd(node, offset); 2277 } 2278 updateRangeProperties(this); 2279 }; 2280 2281 createBeforeAfterNodeSetter = function(name, oppositeName) { 2282 return function(node) { 2283 try { 2284 this.nativeRange[name](node); 2285 } catch (ex) { 2286 this.nativeRange[oppositeName](node); 2287 this.nativeRange[name](node); 2288 } 2289 updateRangeProperties(this); 2290 }; 2291 }; 2292 } 2293 2294 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); 2295 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); 2296 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); 2297 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); 2298 2299 /*--------------------------------------------------------------------------------------------------------*/ 2300 2301 // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to 2302 // the 0th character of the text node 2303 range.selectNodeContents(testTextNode); 2304 if (range.startContainer == testTextNode && range.endContainer == testTextNode && 2305 range.startOffset == 0 && range.endOffset == testTextNode.length) { 2306 rangeProto.selectNodeContents = function(node) { 2307 this.nativeRange.selectNodeContents(node); 2308 updateRangeProperties(this); 2309 }; 2310 } else { 2311 rangeProto.selectNodeContents = function(node) { 2312 this.setStart(node, 0); 2313 this.setEnd(node, DomRange.getEndOffset(node)); 2314 }; 2315 } 2316 2317 /*--------------------------------------------------------------------------------------------------------*/ 2318 2319 // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants 2320 // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 2321 2322 range.selectNodeContents(testTextNode); 2323 range.setEnd(testTextNode, 3); 2324 2325 var range2 = document.createRange(); 2326 2327 range2.selectNodeContents(testTextNode); 2328 range2.setEnd(testTextNode, 4); 2329 range2.setStart(testTextNode, 2); 2330 2331 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 & 2332 range.compareBoundaryPoints(range.END_TO_START, range2) == 1) { 2333 // This is the wrong way round, so correct for it 2334 2335 2336 rangeProto.compareBoundaryPoints = function(type, range) { 2337 range = range.nativeRange || range; 2338 if (type == range.START_TO_END) { 2339 type = range.END_TO_START; 2340 } else if (type == range.END_TO_START) { 2341 type = range.START_TO_END; 2342 } 2343 return this.nativeRange.compareBoundaryPoints(type, range); 2344 }; 2345 } else { 2346 rangeProto.compareBoundaryPoints = function(type, range) { 2347 return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range); 2348 }; 2349 } 2350 2351 /*--------------------------------------------------------------------------------------------------------*/ 2352 2353 // Test for existence of createContextualFragment and delegate to it if it exists 2354 if (api.util.isHostMethod(range, "createContextualFragment")) { 2355 rangeProto.createContextualFragment = function(fragmentStr) { 2356 return this.nativeRange.createContextualFragment(fragmentStr); 2357 }; 2358 } 2359 2360 /*--------------------------------------------------------------------------------------------------------*/ 2361 2362 // Clean up 2363 dom.getBody(document).removeChild(testTextNode); 2364 range.detach(); 2365 range2.detach(); 2366 })(); 2367 2368 api.createNativeRange = function(doc) { 2369 doc = doc || document; 2370 return doc.createRange(); 2371 }; 2372 } else if (api.features.implementsTextRange) { 2373 // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a 2374 // prototype 2375 2376 WrappedRange = function(textRange) { 2377 this.textRange = textRange; 2378 this.refresh(); 2379 }; 2380 2381 WrappedRange.prototype = new DomRange(document); 2382 2383 WrappedRange.prototype.refresh = function() { 2384 var start, end; 2385 2386 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that. 2387 var rangeContainerElement = getTextRangeContainerElement(this.textRange); 2388 2389 if (textRangeIsCollapsed(this.textRange)) { 2390 end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true); 2391 } else { 2392 2393 start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false); 2394 end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false); 2395 } 2396 2397 this.setStart(start.node, start.offset); 2398 this.setEnd(end.node, end.offset); 2399 }; 2400 2401 DomRange.copyComparisonConstants(WrappedRange); 2402 2403 // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work 2404 var globalObj = (function() { return this; })(); 2405 if (typeof globalObj.Range == "undefined") { 2406 globalObj.Range = WrappedRange; 2407 } 2408 2409 api.createNativeRange = function(doc) { 2410 doc = doc || document; 2411 return doc.body.createTextRange(); 2412 }; 2413 } 2414 2415 if (api.features.implementsTextRange) { 2416 WrappedRange.rangeToTextRange = function(range) { 2417 if (range.collapsed) { 2418 var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); 2419 2420 2421 2422 return tr; 2423 2424 //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); 2425 } else { 2426 var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); 2427 var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false); 2428 var textRange = dom.getDocument(range.startContainer).body.createTextRange(); 2429 textRange.setEndPoint("StartToStart", startRange); 2430 textRange.setEndPoint("EndToEnd", endRange); 2431 return textRange; 2432 } 2433 }; 2434 } 2435 2436 WrappedRange.prototype.getName = function() { 2437 return "WrappedRange"; 2438 }; 2439 2440 api.WrappedRange = WrappedRange; 2441 2442 api.createRange = function(doc) { 2443 doc = doc || document; 2444 return new WrappedRange(api.createNativeRange(doc)); 2445 }; 2446 2447 api.createRangyRange = function(doc) { 2448 doc = doc || document; 2449 return new DomRange(doc); 2450 }; 2451 2452 api.createIframeRange = function(iframeEl) { 2453 return api.createRange(dom.getIframeDocument(iframeEl)); 2454 }; 2455 2456 api.createIframeRangyRange = function(iframeEl) { 2457 return api.createRangyRange(dom.getIframeDocument(iframeEl)); 2458 }; 2459 2460 api.addCreateMissingNativeApiListener(function(win) { 2461 var doc = win.document; 2462 if (typeof doc.createRange == "undefined") { 2463 doc.createRange = function() { 2464 return api.createRange(this); 2465 }; 2466 } 2467 doc = win = null; 2468 }); 2469 });rangy.createModule("WrappedSelection", function(api, module) { 2470 // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range 2471 // spec (http://html5.org/specs/dom-range.html) 2472 2473 api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] ); 2474 2475 api.config.checkSelectionRanges = true; 2476 2477 var BOOLEAN = "boolean", 2478 windowPropertyName = "_rangySelection", 2479 dom = api.dom, 2480 util = api.util, 2481 DomRange = api.DomRange, 2482 WrappedRange = api.WrappedRange, 2483 DOMException = api.DOMException, 2484 DomPosition = dom.DomPosition, 2485 getSelection, 2486 selectionIsCollapsed, 2487 CONTROL = "Control"; 2488 2489 2490 2491 function getWinSelection(winParam) { 2492 return (winParam || window).getSelection(); 2493 } 2494 2495 function getDocSelection(winParam) { 2496 return (winParam || window).document.selection; 2497 } 2498 2499 // Test for the Range/TextRange and Selection features required 2500 // Test for ability to retrieve selection 2501 var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"), 2502 implementsDocSelection = api.util.isHostObject(document, "selection"); 2503 2504 var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange); 2505 2506 if (useDocumentSelection) { 2507 getSelection = getDocSelection; 2508 api.isSelectionValid = function(winParam) { 2509 var doc = (winParam || window).document, nativeSel = doc.selection; 2510 2511 // Check whether the selection TextRange is actually contained within the correct document 2512 return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc); 2513 }; 2514 } else if (implementsWinGetSelection) { 2515 getSelection = getWinSelection; 2516 api.isSelectionValid = function() { 2517 return true; 2518 }; 2519 } else { 2520 module.fail("Neither document.selection or window.getSelection() detected."); 2521 } 2522 2523 api.getNativeSelection = getSelection; 2524 2525 var testSelection = getSelection(); 2526 var testRange = api.createNativeRange(document); 2527 var body = dom.getBody(document); 2528 2529 // Obtaining a range from a selection 2530 var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] && 2531 util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"])); 2532 api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus; 2533 2534 // Test for existence of native selection extend() method 2535 var selectionHasExtend = util.isHostMethod(testSelection, "extend"); 2536 api.features.selectionHasExtend = selectionHasExtend; 2537 2538 // Test if rangeCount exists 2539 var selectionHasRangeCount = (typeof testSelection.rangeCount == "number"); 2540 api.features.selectionHasRangeCount = selectionHasRangeCount; 2541 2542 var selectionSupportsMultipleRanges = false; 2543 var collapsedNonEditableSelectionsSupported = true; 2544 2545 if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) && 2546 typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) { 2547 2548 (function() { 2549 var iframe = document.createElement("iframe"); 2550 body.appendChild(iframe); 2551 2552 var iframeDoc = dom.getIframeDocument(iframe); 2553 iframeDoc.open(); 2554 iframeDoc.write("<html><head></head><body>12</body></html>"); 2555 iframeDoc.close(); 2556 2557 var sel = dom.getIframeWindow(iframe).getSelection(); 2558 var docEl = iframeDoc.documentElement; 2559 var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild; 2560 2561 // Test whether the native selection will allow a collapsed selection within a non-editable element 2562 var r1 = iframeDoc.createRange(); 2563 r1.setStart(textNode, 1); 2564 r1.collapse(true); 2565 sel.addRange(r1); 2566 collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1); 2567 sel.removeAllRanges(); 2568 2569 // Test whether the native selection is capable of supporting multiple ranges 2570 var r2 = r1.cloneRange(); 2571 r1.setStart(textNode, 0); 2572 r2.setEnd(textNode, 2); 2573 sel.addRange(r1); 2574 sel.addRange(r2); 2575 2576 selectionSupportsMultipleRanges = (sel.rangeCount == 2); 2577 2578 // Clean up 2579 r1.detach(); 2580 r2.detach(); 2581 2582 body.removeChild(iframe); 2583 })(); 2584 } 2585 2586 api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges; 2587 api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported; 2588 2589 // ControlRanges 2590 var implementsControlRange = false, testControlRange; 2591 2592 if (body && util.isHostMethod(body, "createControlRange")) { 2593 testControlRange = body.createControlRange(); 2594 if (util.areHostProperties(testControlRange, ["item", "add"])) { 2595 implementsControlRange = true; 2596 } 2597 } 2598 api.features.implementsControlRange = implementsControlRange; 2599 2600 // Selection collapsedness 2601 if (selectionHasAnchorAndFocus) { 2602 selectionIsCollapsed = function(sel) { 2603 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset; 2604 }; 2605 } else { 2606 selectionIsCollapsed = function(sel) { 2607 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false; 2608 }; 2609 } 2610 2611 function updateAnchorAndFocusFromRange(sel, range, backwards) { 2612 var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end"; 2613 sel.anchorNode = range[anchorPrefix + "Container"]; 2614 sel.anchorOffset = range[anchorPrefix + "Offset"]; 2615 sel.focusNode = range[focusPrefix + "Container"]; 2616 sel.focusOffset = range[focusPrefix + "Offset"]; 2617 } 2618 2619 function updateAnchorAndFocusFromNativeSelection(sel) { 2620 var nativeSel = sel.nativeSelection; 2621 sel.anchorNode = nativeSel.anchorNode; 2622 sel.anchorOffset = nativeSel.anchorOffset; 2623 sel.focusNode = nativeSel.focusNode; 2624 sel.focusOffset = nativeSel.focusOffset; 2625 } 2626 2627 function updateEmptySelection(sel) { 2628 sel.anchorNode = sel.focusNode = null; 2629 sel.anchorOffset = sel.focusOffset = 0; 2630 sel.rangeCount = 0; 2631 sel.isCollapsed = true; 2632 sel._ranges.length = 0; 2633 } 2634 2635 function getNativeRange(range) { 2636 var nativeRange; 2637 if (range instanceof DomRange) { 2638 nativeRange = range._selectionNativeRange; 2639 if (!nativeRange) { 2640 nativeRange = api.createNativeRange(dom.getDocument(range.startContainer)); 2641 nativeRange.setEnd(range.endContainer, range.endOffset); 2642 nativeRange.setStart(range.startContainer, range.startOffset); 2643 range._selectionNativeRange = nativeRange; 2644 range.attachListener("detach", function() { 2645 2646 this._selectionNativeRange = null; 2647 }); 2648 } 2649 } else if (range instanceof WrappedRange) { 2650 nativeRange = range.nativeRange; 2651 } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) { 2652 nativeRange = range; 2653 } 2654 return nativeRange; 2655 } 2656 2657 function rangeContainsSingleElement(rangeNodes) { 2658 if (!rangeNodes.length || rangeNodes[0].nodeType != 1) { 2659 return false; 2660 } 2661 for (var i = 1, len = rangeNodes.length; i < len; ++i) { 2662 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) { 2663 return false; 2664 } 2665 } 2666 return true; 2667 } 2668 2669 function getSingleElementFromRange(range) { 2670 var nodes = range.getNodes(); 2671 if (!rangeContainsSingleElement(nodes)) { 2672 throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element"); 2673 } 2674 return nodes[0]; 2675 } 2676 2677 function isTextRange(range) { 2678 return !!range && typeof range.text != "undefined"; 2679 } 2680 2681 function updateFromTextRange(sel, range) { 2682 // Create a Range from the selected TextRange 2683 var wrappedRange = new WrappedRange(range); 2684 sel._ranges = [wrappedRange]; 2685 2686 updateAnchorAndFocusFromRange(sel, wrappedRange, false); 2687 sel.rangeCount = 1; 2688 sel.isCollapsed = wrappedRange.collapsed; 2689 } 2690 2691 function updateControlSelection(sel) { 2692 // Update the wrapped selection based on what's now in the native selection 2693 sel._ranges.length = 0; 2694 if (sel.docSelection.type == "None") { 2695 updateEmptySelection(sel); 2696 } else { 2697 var controlRange = sel.docSelection.createRange(); 2698 if (isTextRange(controlRange)) { 2699 // This case (where the selection type is "Control" and calling createRange() on the selection returns 2700 // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected 2701 // ControlRange have been removed from the ControlRange and removed from the document. 2702 updateFromTextRange(sel, controlRange); 2703 } else { 2704 sel.rangeCount = controlRange.length; 2705 var range, doc = dom.getDocument(controlRange.item(0)); 2706 for (var i = 0; i < sel.rangeCount; ++i) { 2707 range = api.createRange(doc); 2708 range.selectNode(controlRange.item(i)); 2709 sel._ranges.push(range); 2710 } 2711 sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed; 2712 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false); 2713 } 2714 } 2715 } 2716 2717 function addRangeToControlSelection(sel, range) { 2718 var controlRange = sel.docSelection.createRange(); 2719 var rangeElement = getSingleElementFromRange(range); 2720 2721 // Create a new ControlRange containing all the elements in the selected ControlRange plus the element 2722 // contained by the supplied range 2723 var doc = dom.getDocument(controlRange.item(0)); 2724 var newControlRange = dom.getBody(doc).createControlRange(); 2725 for (var i = 0, len = controlRange.length; i < len; ++i) { 2726 newControlRange.add(controlRange.item(i)); 2727 } 2728 try { 2729 newControlRange.add(rangeElement); 2730 } catch (ex) { 2731 throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)"); 2732 } 2733 newControlRange.select(); 2734 2735 // Update the wrapped selection based on what's now in the native selection 2736 updateControlSelection(sel); 2737 } 2738 2739 var getSelectionRangeAt; 2740 2741 if (util.isHostMethod(testSelection, "getRangeAt")) { 2742 getSelectionRangeAt = function(sel, index) { 2743 try { 2744 return sel.getRangeAt(index); 2745 } catch(ex) { 2746 return null; 2747 } 2748 }; 2749 } else if (selectionHasAnchorAndFocus) { 2750 getSelectionRangeAt = function(sel) { 2751 var doc = dom.getDocument(sel.anchorNode); 2752 var range = api.createRange(doc); 2753 range.setStart(sel.anchorNode, sel.anchorOffset); 2754 range.setEnd(sel.focusNode, sel.focusOffset); 2755 2756 // Handle the case when the selection was selected backwards (from the end to the start in the 2757 // document) 2758 if (range.collapsed !== this.isCollapsed) { 2759 range.setStart(sel.focusNode, sel.focusOffset); 2760 range.setEnd(sel.anchorNode, sel.anchorOffset); 2761 } 2762 2763 return range; 2764 }; 2765 } 2766 2767 /** 2768 * @constructor 2769 */ 2770 function WrappedSelection(selection, docSelection, win) { 2771 this.nativeSelection = selection; 2772 this.docSelection = docSelection; 2773 this._ranges = []; 2774 this.win = win; 2775 this.refresh(); 2776 } 2777 2778 api.getSelection = function(win) { 2779 win = win || window; 2780 var sel = win[windowPropertyName]; 2781 var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null; 2782 if (sel) { 2783 sel.nativeSelection = nativeSel; 2784 sel.docSelection = docSel; 2785 sel.refresh(win); 2786 } else { 2787 sel = new WrappedSelection(nativeSel, docSel, win); 2788 win[windowPropertyName] = sel; 2789 } 2790 return sel; 2791 }; 2792 2793 api.getIframeSelection = function(iframeEl) { 2794 return api.getSelection(dom.getIframeWindow(iframeEl)); 2795 }; 2796 2797 var selProto = WrappedSelection.prototype; 2798 2799 function createControlSelection(sel, ranges) { 2800 // Ensure that the selection becomes of type "Control" 2801 var doc = dom.getDocument(ranges[0].startContainer); 2802 var controlRange = dom.getBody(doc).createControlRange(); 2803 for (var i = 0, el; i < rangeCount; ++i) { 2804 el = getSingleElementFromRange(ranges[i]); 2805 try { 2806 controlRange.add(el); 2807 } catch (ex) { 2808 throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)"); 2809 } 2810 } 2811 controlRange.select(); 2812 2813 // Update the wrapped selection based on what's now in the native selection 2814 updateControlSelection(sel); 2815 } 2816 2817 // Selecting a range 2818 if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) { 2819 selProto.removeAllRanges = function() { 2820 this.nativeSelection.removeAllRanges(); 2821 updateEmptySelection(this); 2822 }; 2823 2824 var addRangeBackwards = function(sel, range) { 2825 var doc = DomRange.getRangeDocument(range); 2826 var endRange = api.createRange(doc); 2827 endRange.collapseToPoint(range.endContainer, range.endOffset); 2828 sel.nativeSelection.addRange(getNativeRange(endRange)); 2829 sel.nativeSelection.extend(range.startContainer, range.startOffset); 2830 sel.refresh(); 2831 }; 2832 2833 if (selectionHasRangeCount) { 2834 selProto.addRange = function(range, backwards) { 2835 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { 2836 addRangeToControlSelection(this, range); 2837 } else { 2838 if (backwards && selectionHasExtend) { 2839 addRangeBackwards(this, range); 2840 } else { 2841 var previousRangeCount; 2842 if (selectionSupportsMultipleRanges) { 2843 previousRangeCount = this.rangeCount; 2844 } else { 2845 this.removeAllRanges(); 2846 previousRangeCount = 0; 2847 } 2848 this.nativeSelection.addRange(getNativeRange(range)); 2849 2850 // Check whether adding the range was successful 2851 this.rangeCount = this.nativeSelection.rangeCount; 2852 2853 if (this.rangeCount == previousRangeCount + 1) { 2854 // The range was added successfully 2855 2856 // Check whether the range that we added to the selection is reflected in the last range extracted from 2857 // the selection 2858 if (api.config.checkSelectionRanges) { 2859 var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1); 2860 if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) { 2861 // Happens in WebKit with, for example, a selection placed at the start of a text node 2862 range = new WrappedRange(nativeRange); 2863 } 2864 } 2865 this._ranges[this.rangeCount - 1] = range; 2866 updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection)); 2867 this.isCollapsed = selectionIsCollapsed(this); 2868 } else { 2869 // The range was not added successfully. The simplest thing is to refresh 2870 this.refresh(); 2871 } 2872 } 2873 } 2874 }; 2875 } else { 2876 selProto.addRange = function(range, backwards) { 2877 if (backwards && selectionHasExtend) { 2878 addRangeBackwards(this, range); 2879 } else { 2880 this.nativeSelection.addRange(getNativeRange(range)); 2881 this.refresh(); 2882 } 2883 }; 2884 } 2885 2886 selProto.setRanges = function(ranges) { 2887 if (implementsControlRange && ranges.length > 1) { 2888 createControlSelection(this, ranges); 2889 } else { 2890 this.removeAllRanges(); 2891 for (var i = 0, len = ranges.length; i < len; ++i) { 2892 this.addRange(ranges[i]); 2893 } 2894 } 2895 }; 2896 } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") && 2897 implementsControlRange && useDocumentSelection) { 2898 2899 selProto.removeAllRanges = function() { 2900 // Added try/catch as fix for issue #21 2901 try { 2902 this.docSelection.empty(); 2903 2904 // Check for empty() not working (issue #24) 2905 if (this.docSelection.type != "None") { 2906 // Work around failure to empty a control selection by instead selecting a TextRange and then 2907 // calling empty() 2908 var doc; 2909 if (this.anchorNode) { 2910 doc = dom.getDocument(this.anchorNode); 2911 } else if (this.docSelection.type == CONTROL) { 2912 var controlRange = this.docSelection.createRange(); 2913 if (controlRange.length) { 2914 doc = dom.getDocument(controlRange.item(0)).body.createTextRange(); 2915 } 2916 } 2917 if (doc) { 2918 var textRange = doc.body.createTextRange(); 2919 textRange.select(); 2920 this.docSelection.empty(); 2921 } 2922 } 2923 } catch(ex) {} 2924 updateEmptySelection(this); 2925 }; 2926 2927 selProto.addRange = function(range) { 2928 if (this.docSelection.type == CONTROL) { 2929 addRangeToControlSelection(this, range); 2930 } else { 2931 WrappedRange.rangeToTextRange(range).select(); 2932 this._ranges[0] = range; 2933 this.rangeCount = 1; 2934 this.isCollapsed = this._ranges[0].collapsed; 2935 updateAnchorAndFocusFromRange(this, range, false); 2936 } 2937 }; 2938 2939 selProto.setRanges = function(ranges) { 2940 this.removeAllRanges(); 2941 var rangeCount = ranges.length; 2942 if (rangeCount > 1) { 2943 createControlSelection(this, ranges); 2944 2945 } else if (rangeCount) { 2946 this.addRange(ranges[0]); 2947 } 2948 }; 2949 } else { 2950 module.fail("No means of selecting a Range or TextRange was found"); 2951 return false; 2952 } 2953 2954 selProto.getRangeAt = function(index) { 2955 if (index < 0 || index >= this.rangeCount) { 2956 throw new DOMException("INDEX_SIZE_ERR"); 2957 } else { 2958 return this._ranges[index]; 2959 } 2960 }; 2961 2962 var refreshSelection; 2963 2964 if (useDocumentSelection) { 2965 refreshSelection = function(sel) { 2966 var range; 2967 if (api.isSelectionValid(sel.win)) { 2968 range = sel.docSelection.createRange(); 2969 } else { 2970 range = dom.getBody(sel.win.document).createTextRange(); 2971 range.collapse(true); 2972 } 2973 2974 2975 if (sel.docSelection.type == CONTROL) { 2976 updateControlSelection(sel); 2977 } else if (isTextRange(range)) { 2978 updateFromTextRange(sel, range); 2979 } else { 2980 updateEmptySelection(sel); 2981 } 2982 }; 2983 } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") { 2984 refreshSelection = function(sel) { 2985 if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) { 2986 updateControlSelection(sel); 2987 } else { 2988 sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount; 2989 if (sel.rangeCount) { 2990 for (var i = 0, len = sel.rangeCount; i < len; ++i) { 2991 sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i)); 2992 } 2993 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection)); 2994 sel.isCollapsed = selectionIsCollapsed(sel); 2995 } else { 2996 updateEmptySelection(sel); 2997 } 2998 } 2999 }; 3000 } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) { 3001 refreshSelection = function(sel) { 3002 var range, nativeSel = sel.nativeSelection; 3003 if (nativeSel.anchorNode) { 3004 range = getSelectionRangeAt(nativeSel, 0); 3005 sel._ranges = [range]; 3006 sel.rangeCount = 1; 3007 updateAnchorAndFocusFromNativeSelection(sel); 3008 sel.isCollapsed = selectionIsCollapsed(sel); 3009 } else { 3010 updateEmptySelection(sel); 3011 } 3012 }; 3013 } else { 3014 module.fail("No means of obtaining a Range or TextRange from the user's selection was found"); 3015 return false; 3016 } 3017 3018 selProto.refresh = function(checkForChanges) { 3019 var oldRanges = checkForChanges ? this._ranges.slice(0) : null; 3020 refreshSelection(this); 3021 if (checkForChanges) { 3022 var i = oldRanges.length; 3023 if (i != this._ranges.length) { 3024 return false; 3025 } 3026 while (i--) { 3027 if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) { 3028 return false; 3029 } 3030 } 3031 return true; 3032 } 3033 }; 3034 3035 // Removal of a single range 3036 var removeRangeManually = function(sel, range) { 3037 var ranges = sel.getAllRanges(), removed = false; 3038 sel.removeAllRanges(); 3039 for (var i = 0, len = ranges.length; i < len; ++i) { 3040 if (removed || range !== ranges[i]) { 3041 sel.addRange(ranges[i]); 3042 } else { 3043 // According to the draft WHATWG Range spec, the same range may be added to the selection multiple 3044 // times. removeRange should only remove the first instance, so the following ensures only the first 3045 // instance is removed 3046 removed = true; 3047 } 3048 } 3049 if (!sel.rangeCount) { 3050 updateEmptySelection(sel); 3051 } 3052 }; 3053 3054 if (implementsControlRange) { 3055 selProto.removeRange = function(range) { 3056 if (this.docSelection.type == CONTROL) { 3057 var controlRange = this.docSelection.createRange(); 3058 var rangeElement = getSingleElementFromRange(range); 3059 3060 // Create a new ControlRange containing all the elements in the selected ControlRange minus the 3061 // element contained by the supplied range 3062 var doc = dom.getDocument(controlRange.item(0)); 3063 var newControlRange = dom.getBody(doc).createControlRange(); 3064 var el, removed = false; 3065 for (var i = 0, len = controlRange.length; i < len; ++i) { 3066 el = controlRange.item(i); 3067 if (el !== rangeElement || removed) { 3068 newControlRange.add(controlRange.item(i)); 3069 } else { 3070 removed = true; 3071 } 3072 } 3073 newControlRange.select(); 3074 3075 // Update the wrapped selection based on what's now in the native selection 3076 updateControlSelection(this); 3077 } else { 3078 removeRangeManually(this, range); 3079 } 3080 }; 3081 } else { 3082 selProto.removeRange = function(range) { 3083 removeRangeManually(this, range); 3084 }; 3085 } 3086 3087 // Detecting if a selection is backwards 3088 var selectionIsBackwards; 3089 if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) { 3090 selectionIsBackwards = function(sel) { 3091 var backwards = false; 3092 if (sel.anchorNode) { 3093 backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1); 3094 } 3095 return backwards; 3096 }; 3097 3098 selProto.isBackwards = function() { 3099 return selectionIsBackwards(this); 3100 }; 3101 } else { 3102 selectionIsBackwards = selProto.isBackwards = function() { 3103 return false; 3104 }; 3105 } 3106 3107 // Selection text 3108 // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation 3109 selProto.toString = function() { 3110 3111 var rangeTexts = []; 3112 for (var i = 0, len = this.rangeCount; i < len; ++i) { 3113 rangeTexts[i] = "" + this._ranges[i]; 3114 } 3115 return rangeTexts.join(""); 3116 }; 3117 3118 function assertNodeInSameDocument(sel, node) { 3119 if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) { 3120 throw new DOMException("WRONG_DOCUMENT_ERR"); 3121 } 3122 } 3123 3124 // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used 3125 selProto.collapse = function(node, offset) { 3126 assertNodeInSameDocument(this, node); 3127 var range = api.createRange(dom.getDocument(node)); 3128 range.collapseToPoint(node, offset); 3129 this.removeAllRanges(); 3130 this.addRange(range); 3131 this.isCollapsed = true; 3132 }; 3133 3134 selProto.collapseToStart = function() { 3135 if (this.rangeCount) { 3136 var range = this._ranges[0]; 3137 this.collapse(range.startContainer, range.startOffset); 3138 3139 } else { 3140 throw new DOMException("INVALID_STATE_ERR"); 3141 } 3142 }; 3143 3144 selProto.collapseToEnd = function() { 3145 if (this.rangeCount) { 3146 var range = this._ranges[this.rangeCount - 1]; 3147 this.collapse(range.endContainer, range.endOffset); 3148 } else { 3149 throw new DOMException("INVALID_STATE_ERR"); 3150 } 3151 }; 3152 3153 // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is 3154 // never used by Rangy. 3155 selProto.selectAllChildren = function(node) { 3156 3157 assertNodeInSameDocument(this, node); 3158 var range = api.createRange(dom.getDocument(node)); 3159 range.selectNodeContents(node); 3160 this.removeAllRanges(); 3161 this.addRange(range); 3162 }; 3163 3164 selProto.deleteFromDocument = function() { 3165 // Sepcial behaviour required for Control selections 3166 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { 3167 var controlRange = this.docSelection.createRange(); 3168 var element; 3169 while (controlRange.length) { 3170 element = controlRange.item(0); 3171 controlRange.remove(element); 3172 element.parentNode.removeChild(element); 3173 } 3174 this.refresh(); 3175 } else if (this.rangeCount) { 3176 var ranges = this.getAllRanges(); 3177 this.removeAllRanges(); 3178 for (var i = 0, len = ranges.length; i < len; ++i) { 3179 ranges[i].deleteContents(); 3180 } 3181 // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each 3182 // range. Firefox moves the selection to where the final selected range was, so we emulate that 3183 this.addRange(ranges[len - 1]); 3184 } 3185 }; 3186 3187 // The following are non-standard extensions 3188 selProto.getAllRanges = function() { 3189 return this._ranges.slice(0); 3190 }; 3191 3192 selProto.setSingleRange = function(range) { 3193 this.setRanges( [range] ); 3194 }; 3195 3196 selProto.containsNode = function(node, allowPartial) { 3197 for (var i = 0, len = this._ranges.length; i < len; ++i) { 3198 if (this._ranges[i].containsNode(node, allowPartial)) { 3199 return true; 3200 } 3201 } 3202 return false; 3203 }; 3204 3205 selProto.toHtml = function() { 3206 var html = ""; 3207 if (this.rangeCount) { 3208 var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div"); 3209 for (var i = 0, len = this._ranges.length; i < len; ++i) { 3210 container.appendChild(this._ranges[i].cloneContents()); 3211 } 3212 html = container.innerHTML; 3213 } 3214 return html; 3215 }; 3216 3217 function inspect(sel) { 3218 var rangeInspects = []; 3219 var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset); 3220 var focus = new DomPosition(sel.focusNode, sel.focusOffset); 3221 var name = (typeof sel.getName == "function") ? sel.getName() : "Selection"; 3222 3223 if (typeof sel.rangeCount != "undefined") { 3224 for (var i = 0, len = sel.rangeCount; i < len; ++i) { 3225 rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i)); 3226 } 3227 } 3228 return "[" + name + "(Ranges: " + rangeInspects.join(", ") + 3229 ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]"; 3230 3231 } 3232 3233 selProto.getName = function() { 3234 return "WrappedSelection"; 3235 }; 3236 3237 selProto.inspect = function() { 3238 return inspect(this); 3239 }; 3240 3241 selProto.detach = function() { 3242 this.win[windowPropertyName] = null; 3243 this.win = this.anchorNode = this.focusNode = null; 3244 }; 3245 3246 WrappedSelection.inspect = inspect; 3247 3248 api.Selection = WrappedSelection; 3249 3250 api.selectionPrototype = selProto; 3251 3252 api.addCreateMissingNativeApiListener(function(win) { 3253 if (typeof win.getSelection == "undefined") { 3254 win.getSelection = function() { 3255 return api.getSelection(this); 3256 }; 3257 } 3258 win = null; 3259 }); 3260 }); 3261