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 offset = workingRange.text.length; 2033 } 2034 boundaryPosition = new DomPosition(boundaryNode, offset); 2035 } else { 2036 2037 2038 // If the boundary immediately follows a character data node and this is the end boundary, we should favour 2039 // a position within that, and likewise for a start boundary preceding a character data node 2040 previousNode = (isCollapsed || !isStart) && workingNode.previousSibling; 2041 nextNode = (isCollapsed || isStart) && workingNode.nextSibling; 2042 2043 2044 2045 if (nextNode && dom.isCharacterDataNode(nextNode)) { 2046 boundaryPosition = new DomPosition(nextNode, 0); 2047 } else if (previousNode && dom.isCharacterDataNode(previousNode)) { 2048 boundaryPosition = new DomPosition(previousNode, previousNode.length); 2049 } else { 2050 boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode)); 2051 } 2052 } 2053 2054 // Clean up 2055 workingNode.parentNode.removeChild(workingNode); 2056 2057 return boundaryPosition; 2058 } 2059 2060 // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node. 2061 // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange 2062 // (http://code.google.com/p/ierange/) 2063 function createBoundaryTextRange(boundaryPosition, isStart) { 2064 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset; 2065 var doc = dom.getDocument(boundaryPosition.node); 2066 var workingNode, childNodes, workingRange = doc.body.createTextRange(); 2067 var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node); 2068 2069 if (nodeIsDataNode) { 2070 boundaryNode = boundaryPosition.node; 2071 boundaryParent = boundaryNode.parentNode; 2072 } else { 2073 childNodes = boundaryPosition.node.childNodes; 2074 boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null; 2075 boundaryParent = boundaryPosition.node; 2076 } 2077 2078 // Position the range immediately before the node containing the boundary 2079 workingNode = doc.createElement("span"); 2080 2081 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the 2082 // element rather than immediately before or after it, which is what we want 2083 workingNode.innerHTML = "feff;"; 2084 2085 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report 2086 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12 2087 if (boundaryNode) { 2088 boundaryParent.insertBefore(workingNode, boundaryNode); 2089 } else { 2090 boundaryParent.appendChild(workingNode); 2091 } 2092 2093 try { 2094 workingRange.moveToElementText(workingNode); 2095 workingRange.collapse(!isStart); 2096 } catch ( err ) { 2097 // @todo window.console.log('problem with moveToElementText'); 2098 //return false; 2099 } 2100 2101 // Clean up 2102 boundaryParent.removeChild(workingNode); 2103 2104 // Move the working range to the text offset, if required 2105 if (nodeIsDataNode) { 2106 workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset); 2107 } 2108 2109 return workingRange; 2110 } 2111 2112 /*----------------------------------------------------------------------------------------------------------------*/ 2113 2114 if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) { 2115 // This is a wrapper around the browser's native DOM Range. It has two aims: 2116 // - Provide workarounds for specific browser bugs 2117 // - provide convenient extensions, which are inherited from Rangy's DomRange 2118 2119 (function() { 2120 var rangeProto; 2121 var rangeProperties = DomRange.rangeProperties; 2122 var canSetRangeStartAfterEnd; 2123 2124 function updateRangeProperties(range) { 2125 var i = rangeProperties.length, prop; 2126 while (i--) { 2127 prop = rangeProperties[i]; 2128 range[prop] = range.nativeRange[prop]; 2129 } 2130 } 2131 2132 function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) { 2133 var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset); 2134 var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset); 2135 2136 // Always set both boundaries for the benefit of IE9 (see issue 35) 2137 if (startMoved || endMoved) { 2138 range.setEnd(endContainer, endOffset); 2139 range.setStart(startContainer, startOffset); 2140 } 2141 } 2142 2143 function detach(range) { 2144 range.nativeRange.detach(); 2145 range.detached = true; 2146 var i = rangeProperties.length, prop; 2147 while (i--) { 2148 prop = rangeProperties[i]; 2149 range[prop] = null; 2150 } 2151 } 2152 2153 var createBeforeAfterNodeSetter; 2154 2155 WrappedRange = function(range) { 2156 if (!range) { 2157 throw new Error("Range must be specified"); 2158 } 2159 this.nativeRange = range; 2160 updateRangeProperties(this); 2161 }; 2162 2163 DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach); 2164 2165 rangeProto = WrappedRange.prototype; 2166 2167 rangeProto.selectNode = function(node) { 2168 this.nativeRange.selectNode(node); 2169 updateRangeProperties(this); 2170 }; 2171 2172 rangeProto.deleteContents = function() { 2173 this.nativeRange.deleteContents(); 2174 updateRangeProperties(this); 2175 }; 2176 2177 rangeProto.extractContents = function() { 2178 var frag = this.nativeRange.extractContents(); 2179 updateRangeProperties(this); 2180 return frag; 2181 }; 2182 2183 rangeProto.cloneContents = function() { 2184 return this.nativeRange.cloneContents(); 2185 }; 2186 2187 // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still 2188 // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for 2189 // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of 2190 // insertNode, which works but is almost certainly slower than the native implementation. 2191 /* 2192 rangeProto.insertNode = function(node) { 2193 this.nativeRange.insertNode(node); 2194 updateRangeProperties(this); 2195 }; 2196 */ 2197 2198 rangeProto.surroundContents = function(node) { 2199 this.nativeRange.surroundContents(node); 2200 updateRangeProperties(this); 2201 }; 2202 2203 rangeProto.collapse = function(isStart) { 2204 this.nativeRange.collapse(isStart); 2205 updateRangeProperties(this); 2206 }; 2207 2208 rangeProto.cloneRange = function() { 2209 return new WrappedRange(this.nativeRange.cloneRange()); 2210 }; 2211 2212 rangeProto.refresh = function() { 2213 updateRangeProperties(this); 2214 }; 2215 2216 rangeProto.toString = function() { 2217 return this.nativeRange.toString(); 2218 }; 2219 2220 // Create test range and node for feature detection 2221 2222 var testTextNode = document.createTextNode("test"); 2223 dom.getBody(document).appendChild(testTextNode); 2224 var range = document.createRange(); 2225 2226 /*--------------------------------------------------------------------------------------------------------*/ 2227 2228 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and 2229 // correct for it 2230 2231 range.setStart(testTextNode, 0); 2232 range.setEnd(testTextNode, 0); 2233 2234 try { 2235 range.setStart(testTextNode, 1); 2236 canSetRangeStartAfterEnd = true; 2237 2238 rangeProto.setStart = function(node, offset) { 2239 this.nativeRange.setStart(node, offset); 2240 updateRangeProperties(this); 2241 }; 2242 2243 rangeProto.setEnd = function(node, offset) { 2244 this.nativeRange.setEnd(node, offset); 2245 updateRangeProperties(this); 2246 }; 2247 2248 createBeforeAfterNodeSetter = function(name) { 2249 return function(node) { 2250 this.nativeRange[name](node); 2251 updateRangeProperties(this); 2252 }; 2253 }; 2254 2255 } catch(ex) { 2256 2257 2258 canSetRangeStartAfterEnd = false; 2259 2260 rangeProto.setStart = function(node, offset) { 2261 try { 2262 this.nativeRange.setStart(node, offset); 2263 } catch (ex) { 2264 this.nativeRange.setEnd(node, offset); 2265 this.nativeRange.setStart(node, offset); 2266 } 2267 updateRangeProperties(this); 2268 }; 2269 2270 rangeProto.setEnd = function(node, offset) { 2271 try { 2272 this.nativeRange.setEnd(node, offset); 2273 } catch (ex) { 2274 this.nativeRange.setStart(node, offset); 2275 this.nativeRange.setEnd(node, offset); 2276 } 2277 updateRangeProperties(this); 2278 }; 2279 2280 createBeforeAfterNodeSetter = function(name, oppositeName) { 2281 return function(node) { 2282 try { 2283 this.nativeRange[name](node); 2284 } catch (ex) { 2285 this.nativeRange[oppositeName](node); 2286 this.nativeRange[name](node); 2287 } 2288 updateRangeProperties(this); 2289 }; 2290 }; 2291 } 2292 2293 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); 2294 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); 2295 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); 2296 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); 2297 2298 /*--------------------------------------------------------------------------------------------------------*/ 2299 2300 // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to 2301 // the 0th character of the text node 2302 range.selectNodeContents(testTextNode); 2303 if (range.startContainer == testTextNode && range.endContainer == testTextNode && 2304 range.startOffset == 0 && range.endOffset == testTextNode.length) { 2305 rangeProto.selectNodeContents = function(node) { 2306 this.nativeRange.selectNodeContents(node); 2307 updateRangeProperties(this); 2308 }; 2309 } else { 2310 rangeProto.selectNodeContents = function(node) { 2311 this.setStart(node, 0); 2312 this.setEnd(node, DomRange.getEndOffset(node)); 2313 }; 2314 } 2315 2316 /*--------------------------------------------------------------------------------------------------------*/ 2317 2318 // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants 2319 // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 2320 2321 range.selectNodeContents(testTextNode); 2322 range.setEnd(testTextNode, 3); 2323 2324 var range2 = document.createRange(); 2325 range2.selectNodeContents(testTextNode); 2326 range2.setEnd(testTextNode, 4); 2327 range2.setStart(testTextNode, 2); 2328 2329 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 & 2330 range.compareBoundaryPoints(range.END_TO_START, range2) == 1) { 2331 // This is the wrong way round, so correct for it 2332 2333 2334 rangeProto.compareBoundaryPoints = function(type, range) { 2335 range = range.nativeRange || range; 2336 if (type == range.START_TO_END) { 2337 type = range.END_TO_START; 2338 } else if (type == range.END_TO_START) { 2339 type = range.START_TO_END; 2340 } 2341 return this.nativeRange.compareBoundaryPoints(type, range); 2342 }; 2343 } else { 2344 rangeProto.compareBoundaryPoints = function(type, range) { 2345 return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range); 2346 }; 2347 } 2348 2349 /*--------------------------------------------------------------------------------------------------------*/ 2350 2351 // Test for existence of createContextualFragment and delegate to it if it exists 2352 if (api.util.isHostMethod(range, "createContextualFragment")) { 2353 rangeProto.createContextualFragment = function(fragmentStr) { 2354 return this.nativeRange.createContextualFragment(fragmentStr); 2355 }; 2356 } 2357 2358 /*--------------------------------------------------------------------------------------------------------*/ 2359 2360 // Clean up 2361 dom.getBody(document).removeChild(testTextNode); 2362 range.detach(); 2363 range2.detach(); 2364 })(); 2365 2366 api.createNativeRange = function(doc) { 2367 doc = doc || document; 2368 return doc.createRange(); 2369 }; 2370 } else if (api.features.implementsTextRange) { 2371 // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a 2372 // prototype 2373 2374 WrappedRange = function(textRange) { 2375 this.textRange = textRange; 2376 this.refresh(); 2377 }; 2378 2379 WrappedRange.prototype = new DomRange(document); 2380 2381 WrappedRange.prototype.refresh = function() { 2382 var start, end; 2383 2384 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that. 2385 var rangeContainerElement = getTextRangeContainerElement(this.textRange); 2386 2387 if (textRangeIsCollapsed(this.textRange)) { 2388 end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true); 2389 } else { 2390 2391 start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false); 2392 end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false); 2393 } 2394 2395 this.setStart(start.node, start.offset); 2396 this.setEnd(end.node, end.offset); 2397 }; 2398 2399 2400 DomRange.copyComparisonConstants(WrappedRange); 2401 2402 // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work 2403 var globalObj = (function() { return this; })(); 2404 if (typeof globalObj.Range == "undefined") { 2405 globalObj.Range = WrappedRange; 2406 } 2407 2408 api.createNativeRange = function(doc) { 2409 doc = doc || document; 2410 return doc.body.createTextRange(); 2411 }; 2412 } 2413 2414 if (api.features.implementsTextRange) { 2415 WrappedRange.rangeToTextRange = function(range) { 2416 if (range.collapsed) { 2417 var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); 2418 return tr; 2419 //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); 2420 } else { 2421 var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); 2422 var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false); 2423 var textRange = dom.getDocument(range.startContainer).body.createTextRange(); 2424 textRange.setEndPoint("StartToStart", startRange); 2425 textRange.setEndPoint("EndToEnd", endRange); 2426 return textRange; 2427 } 2428 }; 2429 } 2430 2431 WrappedRange.prototype.getName = function() { 2432 return "WrappedRange"; 2433 }; 2434 2435 api.WrappedRange = WrappedRange; 2436 2437 api.createRange = function(doc) { 2438 doc = doc || document; 2439 return new WrappedRange(api.createNativeRange(doc)); 2440 }; 2441 2442 api.createRangyRange = function(doc) { 2443 doc = doc || document; 2444 return new DomRange(doc); 2445 }; 2446 2447 api.createIframeRange = function(iframeEl) { 2448 return api.createRange(dom.getIframeDocument(iframeEl)); 2449 }; 2450 2451 api.createIframeRangyRange = function(iframeEl) { 2452 return api.createRangyRange(dom.getIframeDocument(iframeEl)); 2453 }; 2454 2455 api.addCreateMissingNativeApiListener(function(win) { 2456 var doc = win.document; 2457 if (typeof doc.createRange == "undefined") { 2458 doc.createRange = function() { 2459 return api.createRange(this); 2460 }; 2461 } 2462 doc = win = null; 2463 }); 2464 });rangy.createModule("WrappedSelection", function(api, module) { 2465 // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range 2466 // spec (http://html5.org/specs/dom-range.html) 2467 2468 api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] ); 2469 2470 api.config.checkSelectionRanges = true; 2471 2472 var BOOLEAN = "boolean", 2473 windowPropertyName = "_rangySelection", 2474 dom = api.dom, 2475 util = api.util, 2476 DomRange = api.DomRange, 2477 WrappedRange = api.WrappedRange, 2478 DOMException = api.DOMException, 2479 DomPosition = dom.DomPosition, 2480 getSelection, 2481 selectionIsCollapsed, 2482 CONTROL = "Control"; 2483 2484 2485 2486 function getWinSelection(winParam) { 2487 return (winParam || window).getSelection(); 2488 } 2489 2490 function getDocSelection(winParam) { 2491 return (winParam || window).document.selection; 2492 } 2493 2494 // Test for the Range/TextRange and Selection features required 2495 // Test for ability to retrieve selection 2496 var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"), 2497 implementsDocSelection = api.util.isHostObject(document, "selection"); 2498 2499 var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange); 2500 2501 if (useDocumentSelection) { 2502 getSelection = getDocSelection; 2503 api.isSelectionValid = function(winParam) { 2504 var doc = (winParam || window).document, nativeSel = doc.selection; 2505 2506 // Check whether the selection TextRange is actually contained within the correct document 2507 return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc); 2508 }; 2509 } else if (implementsWinGetSelection) { 2510 getSelection = getWinSelection; 2511 api.isSelectionValid = function() { 2512 return true; 2513 }; 2514 } else { 2515 module.fail("Neither document.selection or window.getSelection() detected."); 2516 } 2517 2518 api.getNativeSelection = getSelection; 2519 2520 var testSelection = getSelection(); 2521 var testRange = api.createNativeRange(document); 2522 var body = dom.getBody(document); 2523 2524 // Obtaining a range from a selection 2525 var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] && 2526 util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"])); 2527 api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus; 2528 2529 // Test for existence of native selection extend() method 2530 var selectionHasExtend = util.isHostMethod(testSelection, "extend"); 2531 api.features.selectionHasExtend = selectionHasExtend; 2532 2533 // Test if rangeCount exists 2534 var selectionHasRangeCount = (typeof testSelection.rangeCount == "number"); 2535 api.features.selectionHasRangeCount = selectionHasRangeCount; 2536 2537 var selectionSupportsMultipleRanges = false; 2538 var collapsedNonEditableSelectionsSupported = true; 2539 2540 if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) && 2541 typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) { 2542 2543 (function() { 2544 var iframe = document.createElement("iframe"); 2545 body.appendChild(iframe); 2546 2547 2548 var iframeDoc = dom.getIframeDocument(iframe); 2549 iframeDoc.open(); 2550 iframeDoc.write("<html><head></head><body>12</body></html>"); 2551 iframeDoc.close(); 2552 2553 var sel = dom.getIframeWindow(iframe).getSelection(); 2554 var docEl = iframeDoc.documentElement; 2555 var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild; 2556 2557 // Test whether the native selection will allow a collapsed selection within a non-editable element 2558 var r1 = iframeDoc.createRange(); 2559 r1.setStart(textNode, 1); 2560 r1.collapse(true); 2561 sel.addRange(r1); 2562 collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1); 2563 sel.removeAllRanges(); 2564 2565 // Test whether the native selection is capable of supporting multiple ranges 2566 var r2 = r1.cloneRange(); 2567 r1.setStart(textNode, 0); 2568 r2.setEnd(textNode, 2); 2569 sel.addRange(r1); 2570 sel.addRange(r2); 2571 2572 selectionSupportsMultipleRanges = (sel.rangeCount == 2); 2573 2574 // Clean up 2575 r1.detach(); 2576 r2.detach(); 2577 2578 body.removeChild(iframe); 2579 })(); 2580 } 2581 2582 api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges; 2583 api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported; 2584 2585 // ControlRanges 2586 var implementsControlRange = false, testControlRange; 2587 2588 if (body && util.isHostMethod(body, "createControlRange")) { 2589 testControlRange = body.createControlRange(); 2590 if (util.areHostProperties(testControlRange, ["item", "add"])) { 2591 implementsControlRange = true; 2592 } 2593 } 2594 api.features.implementsControlRange = implementsControlRange; 2595 2596 // Selection collapsedness 2597 if (selectionHasAnchorAndFocus) { 2598 selectionIsCollapsed = function(sel) { 2599 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset; 2600 }; 2601 } else { 2602 selectionIsCollapsed = function(sel) { 2603 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false; 2604 }; 2605 } 2606 2607 function updateAnchorAndFocusFromRange(sel, range, backwards) { 2608 var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end"; 2609 sel.anchorNode = range[anchorPrefix + "Container"]; 2610 sel.anchorOffset = range[anchorPrefix + "Offset"]; 2611 sel.focusNode = range[focusPrefix + "Container"]; 2612 sel.focusOffset = range[focusPrefix + "Offset"]; 2613 } 2614 2615 function updateAnchorAndFocusFromNativeSelection(sel) { 2616 var nativeSel = sel.nativeSelection; 2617 sel.anchorNode = nativeSel.anchorNode; 2618 sel.anchorOffset = nativeSel.anchorOffset; 2619 sel.focusNode = nativeSel.focusNode; 2620 sel.focusOffset = nativeSel.focusOffset; 2621 } 2622 2623 function updateEmptySelection(sel) { 2624 sel.anchorNode = sel.focusNode = null; 2625 sel.anchorOffset = sel.focusOffset = 0; 2626 sel.rangeCount = 0; 2627 sel.isCollapsed = true; 2628 sel._ranges.length = 0; 2629 } 2630 2631 function getNativeRange(range) { 2632 var nativeRange; 2633 if (range instanceof DomRange) { 2634 nativeRange = range._selectionNativeRange; 2635 if (!nativeRange) { 2636 nativeRange = api.createNativeRange(dom.getDocument(range.startContainer)); 2637 nativeRange.setEnd(range.endContainer, range.endOffset); 2638 nativeRange.setStart(range.startContainer, range.startOffset); 2639 range._selectionNativeRange = nativeRange; 2640 range.attachListener("detach", function() { 2641 2642 this._selectionNativeRange = null; 2643 }); 2644 } 2645 } else if (range instanceof WrappedRange) { 2646 nativeRange = range.nativeRange; 2647 } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) { 2648 nativeRange = range; 2649 } 2650 return nativeRange; 2651 } 2652 2653 function rangeContainsSingleElement(rangeNodes) { 2654 if (!rangeNodes.length || rangeNodes[0].nodeType != 1) { 2655 return false; 2656 } 2657 for (var i = 1, len = rangeNodes.length; i < len; ++i) { 2658 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) { 2659 return false; 2660 } 2661 } 2662 return true; 2663 } 2664 2665 function getSingleElementFromRange(range) { 2666 var nodes = range.getNodes(); 2667 if (!rangeContainsSingleElement(nodes)) { 2668 throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element"); 2669 } 2670 return nodes[0]; 2671 } 2672 2673 function isTextRange(range) { 2674 return !!range && typeof range.text != "undefined"; 2675 } 2676 2677 function updateFromTextRange(sel, range) { 2678 // Create a Range from the selected TextRange 2679 var wrappedRange = new WrappedRange(range); 2680 sel._ranges = [wrappedRange]; 2681 2682 updateAnchorAndFocusFromRange(sel, wrappedRange, false); 2683 sel.rangeCount = 1; 2684 sel.isCollapsed = wrappedRange.collapsed; 2685 } 2686 2687 function updateControlSelection(sel) { 2688 // Update the wrapped selection based on what's now in the native selection 2689 sel._ranges.length = 0; 2690 if (sel.docSelection.type == "None") { 2691 updateEmptySelection(sel); 2692 } else { 2693 var controlRange = sel.docSelection.createRange(); 2694 if (isTextRange(controlRange)) { 2695 // This case (where the selection type is "Control" and calling createRange() on the selection returns 2696 // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected 2697 // ControlRange have been removed from the ControlRange and removed from the document. 2698 updateFromTextRange(sel, controlRange); 2699 } else { 2700 sel.rangeCount = controlRange.length; 2701 var range, doc = dom.getDocument(controlRange.item(0)); 2702 for (var i = 0; i < sel.rangeCount; ++i) { 2703 range = api.createRange(doc); 2704 range.selectNode(controlRange.item(i)); 2705 sel._ranges.push(range); 2706 } 2707 sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed; 2708 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false); 2709 } 2710 } 2711 } 2712 2713 function addRangeToControlSelection(sel, range) { 2714 var controlRange = sel.docSelection.createRange(); 2715 var rangeElement = getSingleElementFromRange(range); 2716 2717 // Create a new ControlRange containing all the elements in the selected ControlRange plus the element 2718 // contained by the supplied range 2719 var doc = dom.getDocument(controlRange.item(0)); 2720 var newControlRange = dom.getBody(doc).createControlRange(); 2721 for (var i = 0, len = controlRange.length; i < len; ++i) { 2722 newControlRange.add(controlRange.item(i)); 2723 } 2724 try { 2725 newControlRange.add(rangeElement); 2726 } catch (ex) { 2727 throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)"); 2728 } 2729 newControlRange.select(); 2730 2731 // Update the wrapped selection based on what's now in the native selection 2732 updateControlSelection(sel); 2733 } 2734 2735 var getSelectionRangeAt; 2736 2737 if (util.isHostMethod(testSelection, "getRangeAt")) { 2738 getSelectionRangeAt = function(sel, index) { 2739 try { 2740 return sel.getRangeAt(index); 2741 } catch(ex) { 2742 return null; 2743 } 2744 }; 2745 } else if (selectionHasAnchorAndFocus) { 2746 getSelectionRangeAt = function(sel) { 2747 var doc = dom.getDocument(sel.anchorNode); 2748 var range = api.createRange(doc); 2749 range.setStart(sel.anchorNode, sel.anchorOffset); 2750 range.setEnd(sel.focusNode, sel.focusOffset); 2751 2752 // Handle the case when the selection was selected backwards (from the end to the start in the 2753 // document) 2754 if (range.collapsed !== this.isCollapsed) { 2755 range.setStart(sel.focusNode, sel.focusOffset); 2756 range.setEnd(sel.anchorNode, sel.anchorOffset); 2757 } 2758 2759 return range; 2760 }; 2761 } 2762 2763 /** 2764 * @constructor 2765 */ 2766 function WrappedSelection(selection, docSelection, win) { 2767 this.nativeSelection = selection; 2768 this.docSelection = docSelection; 2769 this._ranges = []; 2770 this.win = win; 2771 this.refresh(); 2772 } 2773 2774 api.getSelection = function(win) { 2775 win = win || window; 2776 var sel = win[windowPropertyName]; 2777 var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null; 2778 if (sel) { 2779 sel.nativeSelection = nativeSel; 2780 sel.docSelection = docSel; 2781 sel.refresh(win); 2782 } else { 2783 sel = new WrappedSelection(nativeSel, docSel, win); 2784 win[windowPropertyName] = sel; 2785 } 2786 return sel; 2787 }; 2788 2789 api.getIframeSelection = function(iframeEl) { 2790 return api.getSelection(dom.getIframeWindow(iframeEl)); 2791 }; 2792 2793 var selProto = WrappedSelection.prototype; 2794 2795 function createControlSelection(sel, ranges) { 2796 // Ensure that the selection becomes of type "Control" 2797 var doc = dom.getDocument(ranges[0].startContainer); 2798 var controlRange = dom.getBody(doc).createControlRange(); 2799 for (var i = 0, el; i < rangeCount; ++i) { 2800 el = getSingleElementFromRange(ranges[i]); 2801 try { 2802 controlRange.add(el); 2803 } catch (ex) { 2804 throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)"); 2805 } 2806 } 2807 controlRange.select(); 2808 2809 // Update the wrapped selection based on what's now in the native selection 2810 updateControlSelection(sel); 2811 } 2812 2813 // Selecting a range 2814 if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) { 2815 selProto.removeAllRanges = function() { 2816 this.nativeSelection.removeAllRanges(); 2817 updateEmptySelection(this); 2818 }; 2819 2820 var addRangeBackwards = function(sel, range) { 2821 var doc = DomRange.getRangeDocument(range); 2822 var endRange = api.createRange(doc); 2823 endRange.collapseToPoint(range.endContainer, range.endOffset); 2824 sel.nativeSelection.addRange(getNativeRange(endRange)); 2825 sel.nativeSelection.extend(range.startContainer, range.startOffset); 2826 sel.refresh(); 2827 }; 2828 2829 if (selectionHasRangeCount) { 2830 selProto.addRange = function(range, backwards) { 2831 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { 2832 addRangeToControlSelection(this, range); 2833 } else { 2834 if (backwards && selectionHasExtend) { 2835 addRangeBackwards(this, range); 2836 } else { 2837 var previousRangeCount; 2838 if (selectionSupportsMultipleRanges) { 2839 previousRangeCount = this.rangeCount; 2840 } else { 2841 this.removeAllRanges(); 2842 previousRangeCount = 0; 2843 } 2844 this.nativeSelection.addRange(getNativeRange(range)); 2845 2846 // Check whether adding the range was successful 2847 this.rangeCount = this.nativeSelection.rangeCount; 2848 2849 if (this.rangeCount == previousRangeCount + 1) { 2850 // The range was added successfully 2851 2852 // Check whether the range that we added to the selection is reflected in the last range extracted from 2853 // the selection 2854 if (api.config.checkSelectionRanges) { 2855 var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1); 2856 if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) { 2857 // Happens in WebKit with, for example, a selection placed at the start of a text node 2858 range = new WrappedRange(nativeRange); 2859 } 2860 } 2861 this._ranges[this.rangeCount - 1] = range; 2862 updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection)); 2863 this.isCollapsed = selectionIsCollapsed(this); 2864 } else { 2865 // The range was not added successfully. The simplest thing is to refresh 2866 this.refresh(); 2867 } 2868 } 2869 } 2870 }; 2871 } else { 2872 selProto.addRange = function(range, backwards) { 2873 if (backwards && selectionHasExtend) { 2874 addRangeBackwards(this, range); 2875 } else { 2876 this.nativeSelection.addRange(getNativeRange(range)); 2877 this.refresh(); 2878 } 2879 }; 2880 } 2881 2882 selProto.setRanges = function(ranges) { 2883 if (implementsControlRange && ranges.length > 1) { 2884 createControlSelection(this, ranges); 2885 } else { 2886 this.removeAllRanges(); 2887 for (var i = 0, len = ranges.length; i < len; ++i) { 2888 this.addRange(ranges[i]); 2889 } 2890 } 2891 }; 2892 } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") && 2893 implementsControlRange && useDocumentSelection) { 2894 2895 selProto.removeAllRanges = function() { 2896 // Added try/catch as fix for issue #21 2897 try { 2898 2899 var isNativeIE7 = (jQuery.browser.msie && jQuery.version < 8 && (typeof document.documentMode === 'undefined')); 2900 if (!isNativeIE7) { 2901 this.docSelection.empty(); 2902 } 2903 2904 // Check for empty() not working (issue #24) 2905 if (this.docSelection.type != "None") { 2906 2907 if (isNativeIE7) { 2908 this.docSelection.empty(); 2909 } 2910 2911 // Work around failure to empty a control selection by instead selecting a TextRange and then 2912 // calling empty() 2913 var doc; 2914 if (this.anchorNode) { 2915 doc = dom.getDocument(this.anchorNode); 2916 } else if (this.docSelection.type == CONTROL) { 2917 var controlRange = this.docSelection.createRange(); 2918 if (controlRange.length) { 2919 doc = dom.getDocument(controlRange.item(0)).body.createTextRange(); 2920 } 2921 } 2922 if (doc) { 2923 var textRange = doc.body.createTextRange(); 2924 textRange.select(); 2925 this.docSelection.empty(); 2926 } 2927 } 2928 } catch(ex) {} 2929 updateEmptySelection(this); 2930 }; 2931 2932 selProto.addRange = function(range) { 2933 if (this.docSelection.type == CONTROL) { 2934 addRangeToControlSelection(this, range); 2935 } else { 2936 try { 2937 WrappedRange.rangeToTextRange(range).select(); 2938 this._ranges[0] = range; 2939 this.rangeCount = 1; 2940 this.isCollapsed = this._ranges[0].collapsed; 2941 updateAnchorAndFocusFromRange(this, range, false); 2942 } catch (e) { 2943 // @todo 2944 // window.console.log('problem at addRange'); 2945 } 2946 } 2947 }; 2948 2949 selProto.setRanges = function(ranges) { 2950 this.removeAllRanges(); 2951 var rangeCount = ranges.length; 2952 if (rangeCount > 1) { 2953 createControlSelection(this, ranges); 2954 } else if (rangeCount) { 2955 this.addRange(ranges[0]); 2956 } 2957 }; 2958 } else { 2959 module.fail("No means of selecting a Range or TextRange was found"); 2960 return false; 2961 } 2962 2963 selProto.getRangeAt = function(index) { 2964 if (index < 0 || index >= this.rangeCount) { 2965 throw new DOMException("INDEX_SIZE_ERR"); 2966 } else { 2967 return this._ranges[index]; 2968 } 2969 }; 2970 2971 var refreshSelection; 2972 2973 if (useDocumentSelection) { 2974 refreshSelection = function(sel) { 2975 var range; 2976 if (api.isSelectionValid(sel.win)) { 2977 range = sel.docSelection.createRange(); 2978 } else { 2979 range = dom.getBody(sel.win.document).createTextRange(); 2980 range.collapse(true); 2981 } 2982 2983 2984 if (sel.docSelection.type == CONTROL) { 2985 updateControlSelection(sel); 2986 } else if (isTextRange(range)) { 2987 updateFromTextRange(sel, range); 2988 } else { 2989 updateEmptySelection(sel); 2990 } 2991 }; 2992 } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") { 2993 refreshSelection = function(sel) { 2994 if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) { 2995 updateControlSelection(sel); 2996 } else { 2997 sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount; 2998 if (sel.rangeCount) { 2999 for (var i = 0, len = sel.rangeCount; i < len; ++i) { 3000 sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i)); 3001 } 3002 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection)); 3003 sel.isCollapsed = selectionIsCollapsed(sel); 3004 } else { 3005 updateEmptySelection(sel); 3006 } 3007 } 3008 }; 3009 } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) { 3010 refreshSelection = function(sel) { 3011 var range, nativeSel = sel.nativeSelection; 3012 if (nativeSel.anchorNode) { 3013 range = getSelectionRangeAt(nativeSel, 0); 3014 sel._ranges = [range]; 3015 sel.rangeCount = 1; 3016 updateAnchorAndFocusFromNativeSelection(sel); 3017 sel.isCollapsed = selectionIsCollapsed(sel); 3018 } else { 3019 updateEmptySelection(sel); 3020 } 3021 }; 3022 } else { 3023 module.fail("No means of obtaining a Range or TextRange from the user's selection was found"); 3024 return false; 3025 } 3026 3027 selProto.refresh = function(checkForChanges) { 3028 var oldRanges = checkForChanges ? this._ranges.slice(0) : null; 3029 refreshSelection(this); 3030 if (checkForChanges) { 3031 var i = oldRanges.length; 3032 if (i != this._ranges.length) { 3033 return false; 3034 } 3035 while (i--) { 3036 if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) { 3037 return false; 3038 } 3039 } 3040 return true; 3041 } 3042 }; 3043 3044 // Removal of a single range 3045 var removeRangeManually = function(sel, range) { 3046 var ranges = sel.getAllRanges(), removed = false; 3047 sel.removeAllRanges(); 3048 for (var i = 0, len = ranges.length; i < len; ++i) { 3049 if (removed || range !== ranges[i]) { 3050 sel.addRange(ranges[i]); 3051 } else { 3052 // According to the draft WHATWG Range spec, the same range may be added to the selection multiple 3053 // times. removeRange should only remove the first instance, so the following ensures only the first 3054 // instance is removed 3055 removed = true; 3056 } 3057 } 3058 if (!sel.rangeCount) { 3059 updateEmptySelection(sel); 3060 } 3061 }; 3062 3063 if (implementsControlRange) { 3064 selProto.removeRange = function(range) { 3065 if (this.docSelection.type == CONTROL) { 3066 var controlRange = this.docSelection.createRange(); 3067 var rangeElement = getSingleElementFromRange(range); 3068 3069 // Create a new ControlRange containing all the elements in the selected ControlRange minus the 3070 // element contained by the supplied range 3071 var doc = dom.getDocument(controlRange.item(0)); 3072 var newControlRange = dom.getBody(doc).createControlRange(); 3073 var el, removed = false; 3074 for (var i = 0, len = controlRange.length; i < len; ++i) { 3075 el = controlRange.item(i); 3076 if (el !== rangeElement || removed) { 3077 newControlRange.add(controlRange.item(i)); 3078 } else { 3079 removed = true; 3080 } 3081 } 3082 newControlRange.select(); 3083 3084 // Update the wrapped selection based on what's now in the native selection 3085 updateControlSelection(this); 3086 } else { 3087 removeRangeManually(this, range); 3088 } 3089 }; 3090 } else { 3091 selProto.removeRange = function(range) { 3092 removeRangeManually(this, range); 3093 }; 3094 } 3095 3096 // Detecting if a selection is backwards 3097 var selectionIsBackwards; 3098 if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) { 3099 selectionIsBackwards = function(sel) { 3100 var backwards = false; 3101 if (sel.anchorNode) { 3102 backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1); 3103 } 3104 return backwards; 3105 }; 3106 3107 selProto.isBackwards = function() { 3108 return selectionIsBackwards(this); 3109 }; 3110 } else { 3111 selectionIsBackwards = selProto.isBackwards = function() { 3112 return false; 3113 }; 3114 } 3115 3116 // Selection text 3117 // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation 3118 selProto.toString = function() { 3119 3120 var rangeTexts = []; 3121 for (var i = 0, len = this.rangeCount; i < len; ++i) { 3122 rangeTexts[i] = "" + this._ranges[i]; 3123 } 3124 return rangeTexts.join(""); 3125 }; 3126 3127 function assertNodeInSameDocument(sel, node) { 3128 if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) { 3129 throw new DOMException("WRONG_DOCUMENT_ERR"); 3130 } 3131 } 3132 3133 // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used 3134 selProto.collapse = function(node, offset) { 3135 assertNodeInSameDocument(this, node); 3136 var range = api.createRange(dom.getDocument(node)); 3137 range.collapseToPoint(node, offset); 3138 this.removeAllRanges(); 3139 this.addRange(range); 3140 this.isCollapsed = true; 3141 }; 3142 3143 selProto.collapseToStart = function() { 3144 if (this.rangeCount) { 3145 var range = this._ranges[0]; 3146 this.collapse(range.startContainer, range.startOffset); 3147 } else { 3148 throw new DOMException("INVALID_STATE_ERR"); 3149 } 3150 }; 3151 3152 selProto.collapseToEnd = function() { 3153 if (this.rangeCount) { 3154 var range = this._ranges[this.rangeCount - 1]; 3155 this.collapse(range.endContainer, range.endOffset); 3156 } else { 3157 throw new DOMException("INVALID_STATE_ERR"); 3158 } 3159 }; 3160 3161 // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is 3162 // never used by Rangy. 3163 selProto.selectAllChildren = function(node) { 3164 assertNodeInSameDocument(this, node); 3165 var range = api.createRange(dom.getDocument(node)); 3166 range.selectNodeContents(node); 3167 this.removeAllRanges(); 3168 this.addRange(range); 3169 }; 3170 3171 selProto.deleteFromDocument = function() { 3172 // Sepcial behaviour required for Control selections 3173 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { 3174 var controlRange = this.docSelection.createRange(); 3175 var element; 3176 while (controlRange.length) { 3177 element = controlRange.item(0); 3178 controlRange.remove(element); 3179 element.parentNode.removeChild(element); 3180 } 3181 this.refresh(); 3182 } else if (this.rangeCount) { 3183 var ranges = this.getAllRanges(); 3184 this.removeAllRanges(); 3185 for (var i = 0, len = ranges.length; i < len; ++i) { 3186 ranges[i].deleteContents(); 3187 } 3188 // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each 3189 // range. Firefox moves the selection to where the final selected range was, so we emulate that 3190 this.addRange(ranges[len - 1]); 3191 } 3192 }; 3193 3194 // The following are non-standard extensions 3195 selProto.getAllRanges = function() { 3196 return this._ranges.slice(0); 3197 }; 3198 3199 selProto.setSingleRange = function(range) { 3200 this.setRanges( [range] ); 3201 }; 3202 3203 selProto.containsNode = function(node, allowPartial) { 3204 for (var i = 0, len = this._ranges.length; i < len; ++i) { 3205 if (this._ranges[i].containsNode(node, allowPartial)) { 3206 return true; 3207 } 3208 } 3209 return false; 3210 }; 3211 3212 selProto.toHtml = function() { 3213 var html = ""; 3214 if (this.rangeCount) { 3215 var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div"); 3216 for (var i = 0, len = this._ranges.length; i < len; ++i) { 3217 container.appendChild(this._ranges[i].cloneContents()); 3218 } 3219 html = container.innerHTML; 3220 } 3221 return html; 3222 }; 3223 3224 function inspect(sel) { 3225 var rangeInspects = []; 3226 var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset); 3227 var focus = new DomPosition(sel.focusNode, sel.focusOffset); 3228 var name = (typeof sel.getName == "function") ? sel.getName() : "Selection"; 3229 3230 if (typeof sel.rangeCount != "undefined") { 3231 for (var i = 0, len = sel.rangeCount; i < len; ++i) { 3232 rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i)); 3233 } 3234 } 3235 return "[" + name + "(Ranges: " + rangeInspects.join(", ") + 3236 ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]"; 3237 3238 } 3239 3240 selProto.getName = function() { 3241 return "WrappedSelection"; 3242 }; 3243 3244 selProto.inspect = function() { 3245 return inspect(this); 3246 }; 3247 3248 selProto.detach = function() { 3249 this.win[windowPropertyName] = null; 3250 this.win = this.anchorNode = this.focusNode = null; 3251 }; 3252 3253 WrappedSelection.inspect = inspect; 3254 3255 api.Selection = WrappedSelection; 3256 3257 api.selectionPrototype = selProto; 3258 3259 api.addCreateMissingNativeApiListener(function(win) { 3260 if (typeof win.getSelection == "undefined") { 3261 win.getSelection = function() { 3262 return api.getSelection(this); 3263 }; 3264 } 3265 win = null; 3266 }); 3267 }); 3268 3269 // TODO we should avoid populating the global namespace 3270 window.rangy = rangy; 3271 return rangy; 3272 }); 3273