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