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