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