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