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