[ 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); 3838 3839 /** 3840 * Selection save and restore module for Rangy. 3841 * Saves and restores user selections using marker invisible elements in the DOM. 3842 * 3843 * Part of Rangy, a cross-browser JavaScript range and selection library 3844 * https://github.com/timdown/rangy 3845 * 3846 * Depends on Rangy core. 3847 * 3848 * Copyright 2015, Tim Down 3849 * Licensed under the MIT license. 3850 * Version: 1.3.0 3851 * Build date: 10 May 2015 3852 */ 3853 (function(factory, root) { 3854 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 3855 factory(root.rangy); 3856 })(function(rangy) { 3857 rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) { 3858 var dom = api.dom; 3859 var removeNode = dom.removeNode; 3860 var isDirectionBackward = api.Selection.isDirectionBackward; 3861 var markerTextChar = "\ufeff"; 3862 3863 function gEBI(id, doc) { 3864 return (doc || document).getElementById(id); 3865 } 3866 3867 function insertRangeBoundaryMarker(range, atStart) { 3868 var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2); 3869 var markerEl; 3870 var doc = dom.getDocument(range.startContainer); 3871 3872 // Clone the Range and collapse to the appropriate boundary point 3873 var boundaryRange = range.cloneRange(); 3874 boundaryRange.collapse(atStart); 3875 3876 // Create the marker element containing a single invisible character using DOM methods and insert it 3877 markerEl = doc.createElement("span"); 3878 markerEl.id = markerId; 3879 markerEl.style.lineHeight = "0"; 3880 markerEl.style.display = "none"; 3881 markerEl.className = "rangySelectionBoundary"; 3882 markerEl.appendChild(doc.createTextNode(markerTextChar)); 3883 3884 boundaryRange.insertNode(markerEl); 3885 return markerEl; 3886 } 3887 3888 function setRangeBoundary(doc, range, markerId, atStart) { 3889 var markerEl = gEBI(markerId, doc); 3890 if (markerEl) { 3891 range[atStart ? "setStartBefore" : "setEndBefore"](markerEl); 3892 removeNode(markerEl); 3893 } else { 3894 module.warn("Marker element has been removed. Cannot restore selection."); 3895 } 3896 } 3897 3898 function compareRanges(r1, r2) { 3899 return r2.compareBoundaryPoints(r1.START_TO_START, r1); 3900 } 3901 3902 function saveRange(range, direction) { 3903 var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString(); 3904 var backward = isDirectionBackward(direction); 3905 3906 if (range.collapsed) { 3907 endEl = insertRangeBoundaryMarker(range, false); 3908 return { 3909 document: doc, 3910 markerId: endEl.id, 3911 collapsed: true 3912 }; 3913 } else { 3914 endEl = insertRangeBoundaryMarker(range, false); 3915 startEl = insertRangeBoundaryMarker(range, true); 3916 3917 return { 3918 document: doc, 3919 startMarkerId: startEl.id, 3920 endMarkerId: endEl.id, 3921 collapsed: false, 3922 backward: backward, 3923 toString: function() { 3924 return "original text: '" + text + "', new text: '" + range.toString() + "'"; 3925 } 3926 }; 3927 } 3928 } 3929 3930 function restoreRange(rangeInfo, normalize) { 3931 var doc = rangeInfo.document; 3932 if (typeof normalize == "undefined") { 3933 normalize = true; 3934 } 3935 var range = api.createRange(doc); 3936 if (rangeInfo.collapsed) { 3937 var markerEl = gEBI(rangeInfo.markerId, doc); 3938 if (markerEl) { 3939 markerEl.style.display = "inline"; 3940 var previousNode = markerEl.previousSibling; 3941 3942 // Workaround for issue 17 3943 if (previousNode && previousNode.nodeType == 3) { 3944 removeNode(markerEl); 3945 range.collapseToPoint(previousNode, previousNode.length); 3946 } else { 3947 range.collapseBefore(markerEl); 3948 removeNode(markerEl); 3949 } 3950 } else { 3951 module.warn("Marker element has been removed. Cannot restore selection."); 3952 } 3953 } else { 3954 setRangeBoundary(doc, range, rangeInfo.startMarkerId, true); 3955 setRangeBoundary(doc, range, rangeInfo.endMarkerId, false); 3956 } 3957 3958 if (normalize) { 3959 range.normalizeBoundaries(); 3960 } 3961 3962 return range; 3963 } 3964 3965 function saveRanges(ranges, direction) { 3966 var rangeInfos = [], range, doc; 3967 var backward = isDirectionBackward(direction); 3968 3969 // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched 3970 ranges = ranges.slice(0); 3971 ranges.sort(compareRanges); 3972 3973 for (var i = 0, len = ranges.length; i < len; ++i) { 3974 rangeInfos[i] = saveRange(ranges[i], backward); 3975 } 3976 3977 // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie 3978 // between its markers 3979 for (i = len - 1; i >= 0; --i) { 3980 range = ranges[i]; 3981 doc = api.DomRange.getRangeDocument(range); 3982 if (range.collapsed) { 3983 range.collapseAfter(gEBI(rangeInfos[i].markerId, doc)); 3984 } else { 3985 range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc)); 3986 range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc)); 3987 } 3988 } 3989 3990 return rangeInfos; 3991 } 3992 3993 function saveSelection(win) { 3994 if (!api.isSelectionValid(win)) { 3995 module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus."); 3996 return null; 3997 } 3998 var sel = api.getSelection(win); 3999 var ranges = sel.getAllRanges(); 4000 var backward = (ranges.length == 1 && sel.isBackward()); 4001 4002 var rangeInfos = saveRanges(ranges, backward); 4003 4004 // Ensure current selection is unaffected 4005 if (backward) { 4006 sel.setSingleRange(ranges[0], backward); 4007 } else { 4008 sel.setRanges(ranges); 4009 } 4010 4011 return { 4012 win: win, 4013 rangeInfos: rangeInfos, 4014 restored: false 4015 }; 4016 } 4017 4018 function restoreRanges(rangeInfos) { 4019 var ranges = []; 4020 4021 // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid 4022 // normalization affecting previously restored ranges. 4023 var rangeCount = rangeInfos.length; 4024 4025 for (var i = rangeCount - 1; i >= 0; i--) { 4026 ranges[i] = restoreRange(rangeInfos[i], true); 4027 } 4028 4029 return ranges; 4030 } 4031 4032 function restoreSelection(savedSelection, preserveDirection) { 4033 if (!savedSelection.restored) { 4034 var rangeInfos = savedSelection.rangeInfos; 4035 var sel = api.getSelection(savedSelection.win); 4036 var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length; 4037 4038 if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) { 4039 sel.removeAllRanges(); 4040 sel.addRange(ranges[0], true); 4041 } else { 4042 sel.setRanges(ranges); 4043 } 4044 4045 savedSelection.restored = true; 4046 } 4047 } 4048 4049 function removeMarkerElement(doc, markerId) { 4050 var markerEl = gEBI(markerId, doc); 4051 if (markerEl) { 4052 removeNode(markerEl); 4053 } 4054 } 4055 4056 function removeMarkers(savedSelection) { 4057 var rangeInfos = savedSelection.rangeInfos; 4058 for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) { 4059 rangeInfo = rangeInfos[i]; 4060 if (rangeInfo.collapsed) { 4061 removeMarkerElement(savedSelection.doc, rangeInfo.markerId); 4062 } else { 4063 removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId); 4064 removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId); 4065 } 4066 } 4067 } 4068 4069 api.util.extend(api, { 4070 saveRange: saveRange, 4071 restoreRange: restoreRange, 4072 saveRanges: saveRanges, 4073 restoreRanges: restoreRanges, 4074 saveSelection: saveSelection, 4075 restoreSelection: restoreSelection, 4076 removeMarkerElement: removeMarkerElement, 4077 removeMarkers: removeMarkers 4078 }); 4079 }); 4080 4081 return rangy; 4082 }, this); 4083 4084 /** 4085 * Serializer module for Rangy. 4086 * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a 4087 * cookie or local storage and restore it on the user's next visit to the same page. 4088 * 4089 * Part of Rangy, a cross-browser JavaScript range and selection library 4090 * https://github.com/timdown/rangy 4091 * 4092 * Depends on Rangy core. 4093 * 4094 * Copyright 2015, Tim Down 4095 * Licensed under the MIT license. 4096 * Version: 1.3.0 4097 * Build date: 10 May 2015 4098 */ 4099 (function(factory, root) { 4100 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 4101 factory(root.rangy); 4102 })(function(rangy) { 4103 rangy.createModule("Serializer", ["WrappedSelection"], function(api, module) { 4104 var UNDEF = "undefined"; 4105 var util = api.util; 4106 4107 // encodeURIComponent and decodeURIComponent are required for cookie handling 4108 if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) { 4109 module.fail("encodeURIComponent and/or decodeURIComponent method is missing"); 4110 } 4111 4112 // Checksum for checking whether range can be serialized 4113 var crc32 = (function() { 4114 function utf8encode(str) { 4115 var utf8CharCodes = []; 4116 4117 for (var i = 0, len = str.length, c; i < len; ++i) { 4118 c = str.charCodeAt(i); 4119 if (c < 128) { 4120 utf8CharCodes.push(c); 4121 } else if (c < 2048) { 4122 utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128); 4123 } else { 4124 utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128); 4125 } 4126 } 4127 return utf8CharCodes; 4128 } 4129 4130 var cachedCrcTable = null; 4131 4132 function buildCRCTable() { 4133 var table = []; 4134 for (var i = 0, j, crc; i < 256; ++i) { 4135 crc = i; 4136 j = 8; 4137 while (j--) { 4138 if ((crc & 1) == 1) { 4139 crc = (crc >>> 1) ^ 0xEDB88320; 4140 } else { 4141 crc >>>= 1; 4142 } 4143 } 4144 table[i] = crc >>> 0; 4145 } 4146 return table; 4147 } 4148 4149 function getCrcTable() { 4150 if (!cachedCrcTable) { 4151 cachedCrcTable = buildCRCTable(); 4152 } 4153 return cachedCrcTable; 4154 } 4155 4156 return function(str) { 4157 var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable(); 4158 for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) { 4159 y = (crc ^ utf8CharCodes[i]) & 0xFF; 4160 crc = (crc >>> 8) ^ crcTable[y]; 4161 } 4162 return (crc ^ -1) >>> 0; 4163 }; 4164 })(); 4165 4166 var dom = api.dom; 4167 4168 function escapeTextForHtml(str) { 4169 return str.replace(/</g, "<").replace(/>/g, ">"); 4170 } 4171 4172 function nodeToInfoString(node, infoParts) { 4173 infoParts = infoParts || []; 4174 var nodeType = node.nodeType, children = node.childNodes, childCount = children.length; 4175 var nodeInfo = [nodeType, node.nodeName, childCount].join(":"); 4176 var start = "", end = ""; 4177 switch (nodeType) { 4178 case 3: // Text node 4179 start = escapeTextForHtml(node.nodeValue); 4180 break; 4181 case 8: // Comment 4182 start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->"; 4183 break; 4184 default: 4185 start = "<" + nodeInfo + ">"; 4186 end = "</>"; 4187 break; 4188 } 4189 if (start) { 4190 infoParts.push(start); 4191 } 4192 for (var i = 0; i < childCount; ++i) { 4193 nodeToInfoString(children[i], infoParts); 4194 } 4195 if (end) { 4196 infoParts.push(end); 4197 } 4198 return infoParts; 4199 } 4200 4201 // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all 4202 // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around 4203 // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's 4204 // innerHTML whenever the user changes an input within the element. 4205 function getElementChecksum(el) { 4206 var info = nodeToInfoString(el).join(""); 4207 return crc32(info).toString(16); 4208 } 4209 4210 function serializePosition(node, offset, rootNode) { 4211 var pathParts = [], n = node; 4212 rootNode = rootNode || dom.getDocument(node).documentElement; 4213 while (n && n != rootNode) { 4214 pathParts.push(dom.getNodeIndex(n, true)); 4215 n = n.parentNode; 4216 } 4217 return pathParts.join("/") + ":" + offset; 4218 } 4219 4220 function deserializePosition(serialized, rootNode, doc) { 4221 if (!rootNode) { 4222 rootNode = (doc || document).documentElement; 4223 } 4224 var parts = serialized.split(":"); 4225 var node = rootNode; 4226 var nodeIndices = parts[0] ? parts[0].split("/") : [], i = nodeIndices.length, nodeIndex; 4227 4228 while (i--) { 4229 nodeIndex = parseInt(nodeIndices[i], 10); 4230 if (nodeIndex < node.childNodes.length) { 4231 node = node.childNodes[nodeIndex]; 4232 } else { 4233 throw module.createError("deserializePosition() failed: node " + dom.inspectNode(node) + 4234 " has no child with index " + nodeIndex + ", " + i); 4235 } 4236 } 4237 4238 return new dom.DomPosition(node, parseInt(parts[1], 10)); 4239 } 4240 4241 function serializeRange(range, omitChecksum, rootNode) { 4242 rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement; 4243 if (!dom.isOrIsAncestorOf(rootNode, range.commonAncestorContainer)) { 4244 throw module.createError("serializeRange(): range " + range.inspect() + 4245 " is not wholly contained within specified root node " + dom.inspectNode(rootNode)); 4246 } 4247 var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," + 4248 serializePosition(range.endContainer, range.endOffset, rootNode); 4249 if (!omitChecksum) { 4250 serialized += "{" + getElementChecksum(rootNode) + "}"; 4251 } 4252 return serialized; 4253 } 4254 4255 var deserializeRegex = /^([^,]+),([^,\{]+)(\{([^}]+)\})?$/; 4256 4257 function deserializeRange(serialized, rootNode, doc) { 4258 if (rootNode) { 4259 doc = doc || dom.getDocument(rootNode); 4260 } else { 4261 doc = doc || document; 4262 rootNode = doc.documentElement; 4263 } 4264 var result = deserializeRegex.exec(serialized); 4265 var checksum = result[4]; 4266 if (checksum) { 4267 var rootNodeChecksum = getElementChecksum(rootNode); 4268 if (checksum !== rootNodeChecksum) { 4269 throw module.createError("deserializeRange(): checksums of serialized range root node (" + checksum + 4270 ") and target root node (" + rootNodeChecksum + ") do not match"); 4271 } 4272 } 4273 var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc); 4274 var range = api.createRange(doc); 4275 range.setStartAndEnd(start.node, start.offset, end.node, end.offset); 4276 return range; 4277 } 4278 4279 function canDeserializeRange(serialized, rootNode, doc) { 4280 if (!rootNode) { 4281 rootNode = (doc || document).documentElement; 4282 } 4283 var result = deserializeRegex.exec(serialized); 4284 var checksum = result[3]; 4285 return !checksum || checksum === getElementChecksum(rootNode); 4286 } 4287 4288 function serializeSelection(selection, omitChecksum, rootNode) { 4289 selection = api.getSelection(selection); 4290 var ranges = selection.getAllRanges(), serializedRanges = []; 4291 for (var i = 0, len = ranges.length; i < len; ++i) { 4292 serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode); 4293 } 4294 return serializedRanges.join("|"); 4295 } 4296 4297 function deserializeSelection(serialized, rootNode, win) { 4298 if (rootNode) { 4299 win = win || dom.getWindow(rootNode); 4300 } else { 4301 win = win || window; 4302 rootNode = win.document.documentElement; 4303 } 4304 var serializedRanges = serialized.split("|"); 4305 var sel = api.getSelection(win); 4306 var ranges = []; 4307 4308 for (var i = 0, len = serializedRanges.length; i < len; ++i) { 4309 ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document); 4310 } 4311 sel.setRanges(ranges); 4312 4313 return sel; 4314 } 4315 4316 function canDeserializeSelection(serialized, rootNode, win) { 4317 var doc; 4318 if (rootNode) { 4319 doc = win ? win.document : dom.getDocument(rootNode); 4320 } else { 4321 win = win || window; 4322 rootNode = win.document.documentElement; 4323 } 4324 var serializedRanges = serialized.split("|"); 4325 4326 for (var i = 0, len = serializedRanges.length; i < len; ++i) { 4327 if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) { 4328 return false; 4329 } 4330 } 4331 4332 return true; 4333 } 4334 4335 var cookieName = "rangySerializedSelection"; 4336 4337 function getSerializedSelectionFromCookie(cookie) { 4338 var parts = cookie.split(/[;,]/); 4339 for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) { 4340 nameVal = parts[i].split("="); 4341 if (nameVal[0].replace(/^\s+/, "") == cookieName) { 4342 val = nameVal[1]; 4343 if (val) { 4344 return decodeURIComponent(val.replace(/\s+$/, "")); 4345 } 4346 } 4347 } 4348 return null; 4349 } 4350 4351 function restoreSelectionFromCookie(win) { 4352 win = win || window; 4353 var serialized = getSerializedSelectionFromCookie(win.document.cookie); 4354 if (serialized) { 4355 deserializeSelection(serialized, win.doc); 4356 } 4357 } 4358 4359 function saveSelectionCookie(win, props) { 4360 win = win || window; 4361 props = (typeof props == "object") ? props : {}; 4362 var expires = props.expires ? ";expires=" + props.expires.toUTCString() : ""; 4363 var path = props.path ? ";path=" + props.path : ""; 4364 var domain = props.domain ? ";domain=" + props.domain : ""; 4365 var secure = props.secure ? ";secure" : ""; 4366 var serialized = serializeSelection(api.getSelection(win)); 4367 win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure; 4368 } 4369 4370 util.extend(api, { 4371 serializePosition: serializePosition, 4372 deserializePosition: deserializePosition, 4373 serializeRange: serializeRange, 4374 deserializeRange: deserializeRange, 4375 canDeserializeRange: canDeserializeRange, 4376 serializeSelection: serializeSelection, 4377 deserializeSelection: deserializeSelection, 4378 canDeserializeSelection: canDeserializeSelection, 4379 restoreSelectionFromCookie: restoreSelectionFromCookie, 4380 saveSelectionCookie: saveSelectionCookie, 4381 getElementChecksum: getElementChecksum, 4382 nodeToInfoString: nodeToInfoString 4383 }); 4384 4385 util.crc32 = crc32; 4386 }); 4387 4388 return rangy; 4389 }, this); 4390 4391 /** 4392 * Class Applier module for Rangy. 4393 * Adds, removes and toggles classes on Ranges and Selections 4394 * 4395 * Part of Rangy, a cross-browser JavaScript range and selection library 4396 * https://github.com/timdown/rangy 4397 * 4398 * Depends on Rangy core. 4399 * 4400 * Copyright 2015, Tim Down 4401 * Licensed under the MIT license. 4402 * Version: 1.3.0 4403 * Build date: 10 May 2015 4404 */ 4405 (function(factory, root) { 4406 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 4407 factory(root.rangy); 4408 })(function(rangy) { 4409 rangy.createModule("ClassApplier", ["WrappedSelection"], function(api, module) { 4410 var dom = api.dom; 4411 var DomPosition = dom.DomPosition; 4412 var contains = dom.arrayContains; 4413 var util = api.util; 4414 var forEach = util.forEach; 4415 4416 4417 var defaultTagName = "span"; 4418 var createElementNSSupported = util.isHostMethod(document, "createElementNS"); 4419 4420 function each(obj, func) { 4421 for (var i in obj) { 4422 if (obj.hasOwnProperty(i)) { 4423 if (func(i, obj[i]) === false) { 4424 return false; 4425 } 4426 } 4427 } 4428 return true; 4429 } 4430 4431 function trim(str) { 4432 return str.replace(/^\s\s*/, "").replace(/\s\s*$/, ""); 4433 } 4434 4435 function classNameContainsClass(fullClassName, className) { 4436 return !!fullClassName && new RegExp("(?:^|\\s)" + className + "(?:\\s|$)").test(fullClassName); 4437 } 4438 4439 // Inefficient, inelegant nonsense for IE's svg element, which has no classList and non-HTML className implementation 4440 function hasClass(el, className) { 4441 if (typeof el.classList == "object") { 4442 return el.classList.contains(className); 4443 } else { 4444 var classNameSupported = (typeof el.className == "string"); 4445 var elClass = classNameSupported ? el.className : el.getAttribute("class"); 4446 return classNameContainsClass(elClass, className); 4447 } 4448 } 4449 4450 function addClass(el, className) { 4451 if (typeof el.classList == "object") { 4452 el.classList.add(className); 4453 } else { 4454 var classNameSupported = (typeof el.className == "string"); 4455 var elClass = classNameSupported ? el.className : el.getAttribute("class"); 4456 if (elClass) { 4457 if (!classNameContainsClass(elClass, className)) { 4458 elClass += " " + className; 4459 } 4460 } else { 4461 elClass = className; 4462 } 4463 if (classNameSupported) { 4464 el.className = elClass; 4465 } else { 4466 el.setAttribute("class", elClass); 4467 } 4468 } 4469 } 4470 4471 var removeClass = (function() { 4472 function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) { 4473 return (whiteSpaceBefore && whiteSpaceAfter) ? " " : ""; 4474 } 4475 4476 return function(el, className) { 4477 if (typeof el.classList == "object") { 4478 el.classList.remove(className); 4479 } else { 4480 var classNameSupported = (typeof el.className == "string"); 4481 var elClass = classNameSupported ? el.className : el.getAttribute("class"); 4482 elClass = elClass.replace(new RegExp("(^|\\s)" + className + "(\\s|$)"), replacer); 4483 if (classNameSupported) { 4484 el.className = elClass; 4485 } else { 4486 el.setAttribute("class", elClass); 4487 } 4488 } 4489 }; 4490 })(); 4491 4492 function getClass(el) { 4493 var classNameSupported = (typeof el.className == "string"); 4494 return classNameSupported ? el.className : el.getAttribute("class"); 4495 } 4496 4497 function sortClassName(className) { 4498 return className && className.split(/\s+/).sort().join(" "); 4499 } 4500 4501 function getSortedClassName(el) { 4502 return sortClassName( getClass(el) ); 4503 } 4504 4505 function haveSameClasses(el1, el2) { 4506 return getSortedClassName(el1) == getSortedClassName(el2); 4507 } 4508 4509 function hasAllClasses(el, className) { 4510 var classes = className.split(/\s+/); 4511 for (var i = 0, len = classes.length; i < len; ++i) { 4512 if (!hasClass(el, trim(classes[i]))) { 4513 return false; 4514 } 4515 } 4516 return true; 4517 } 4518 4519 function canTextBeStyled(textNode) { 4520 var parent = textNode.parentNode; 4521 return (parent && parent.nodeType == 1 && !/^(textarea|style|script|select|iframe)$/i.test(parent.nodeName)); 4522 } 4523 4524 function movePosition(position, oldParent, oldIndex, newParent, newIndex) { 4525 var posNode = position.node, posOffset = position.offset; 4526 var newNode = posNode, newOffset = posOffset; 4527 4528 if (posNode == newParent && posOffset > newIndex) { 4529 ++newOffset; 4530 } 4531 4532 if (posNode == oldParent && (posOffset == oldIndex || posOffset == oldIndex + 1)) { 4533 newNode = newParent; 4534 newOffset += newIndex - oldIndex; 4535 } 4536 4537 if (posNode == oldParent && posOffset > oldIndex + 1) { 4538 --newOffset; 4539 } 4540 4541 position.node = newNode; 4542 position.offset = newOffset; 4543 } 4544 4545 function movePositionWhenRemovingNode(position, parentNode, index) { 4546 if (position.node == parentNode && position.offset > index) { 4547 --position.offset; 4548 } 4549 } 4550 4551 function movePreservingPositions(node, newParent, newIndex, positionsToPreserve) { 4552 // For convenience, allow newIndex to be -1 to mean "insert at the end". 4553 if (newIndex == -1) { 4554 newIndex = newParent.childNodes.length; 4555 } 4556 4557 var oldParent = node.parentNode; 4558 var oldIndex = dom.getNodeIndex(node); 4559 4560 forEach(positionsToPreserve, function(position) { 4561 movePosition(position, oldParent, oldIndex, newParent, newIndex); 4562 }); 4563 4564 // Now actually move the node. 4565 if (newParent.childNodes.length == newIndex) { 4566 newParent.appendChild(node); 4567 } else { 4568 newParent.insertBefore(node, newParent.childNodes[newIndex]); 4569 } 4570 } 4571 4572 function removePreservingPositions(node, positionsToPreserve) { 4573 4574 var oldParent = node.parentNode; 4575 var oldIndex = dom.getNodeIndex(node); 4576 4577 forEach(positionsToPreserve, function(position) { 4578 movePositionWhenRemovingNode(position, oldParent, oldIndex); 4579 }); 4580 4581 dom.removeNode(node); 4582 } 4583 4584 function moveChildrenPreservingPositions(node, newParent, newIndex, removeNode, positionsToPreserve) { 4585 var child, children = []; 4586 while ( (child = node.firstChild) ) { 4587 movePreservingPositions(child, newParent, newIndex++, positionsToPreserve); 4588 children.push(child); 4589 } 4590 if (removeNode) { 4591 removePreservingPositions(node, positionsToPreserve); 4592 } 4593 return children; 4594 } 4595 4596 function replaceWithOwnChildrenPreservingPositions(element, positionsToPreserve) { 4597 return moveChildrenPreservingPositions(element, element.parentNode, dom.getNodeIndex(element), true, positionsToPreserve); 4598 } 4599 4600 function rangeSelectsAnyText(range, textNode) { 4601 var textNodeRange = range.cloneRange(); 4602 textNodeRange.selectNodeContents(textNode); 4603 4604 var intersectionRange = textNodeRange.intersection(range); 4605 var text = intersectionRange ? intersectionRange.toString() : ""; 4606 4607 return text != ""; 4608 } 4609 4610 function getEffectiveTextNodes(range) { 4611 var nodes = range.getNodes([3]); 4612 4613 // Optimization as per issue 145 4614 4615 // Remove non-intersecting text nodes from the start of the range 4616 var start = 0, node; 4617 while ( (node = nodes[start]) && !rangeSelectsAnyText(range, node) ) { 4618 ++start; 4619 } 4620 4621 // Remove non-intersecting text nodes from the start of the range 4622 var end = nodes.length - 1; 4623 while ( (node = nodes[end]) && !rangeSelectsAnyText(range, node) ) { 4624 --end; 4625 } 4626 4627 return nodes.slice(start, end + 1); 4628 } 4629 4630 function elementsHaveSameNonClassAttributes(el1, el2) { 4631 if (el1.attributes.length != el2.attributes.length) return false; 4632 for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) { 4633 attr1 = el1.attributes[i]; 4634 name = attr1.name; 4635 if (name != "class") { 4636 attr2 = el2.attributes.getNamedItem(name); 4637 if ( (attr1 === null) != (attr2 === null) ) return false; 4638 if (attr1.specified != attr2.specified) return false; 4639 if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false; 4640 } 4641 } 4642 return true; 4643 } 4644 4645 function elementHasNonClassAttributes(el, exceptions) { 4646 for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) { 4647 attrName = el.attributes[i].name; 4648 if ( !(exceptions && contains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") { 4649 return true; 4650 } 4651 } 4652 return false; 4653 } 4654 4655 var getComputedStyleProperty = dom.getComputedStyleProperty; 4656 var isEditableElement = (function() { 4657 var testEl = document.createElement("div"); 4658 return typeof testEl.isContentEditable == "boolean" ? 4659 function (node) { 4660 return node && node.nodeType == 1 && node.isContentEditable; 4661 } : 4662 function (node) { 4663 if (!node || node.nodeType != 1 || node.contentEditable == "false") { 4664 return false; 4665 } 4666 return node.contentEditable == "true" || isEditableElement(node.parentNode); 4667 }; 4668 })(); 4669 4670 function isEditingHost(node) { 4671 var parent; 4672 return node && node.nodeType == 1 && 4673 (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on") || 4674 (isEditableElement(node) && !isEditableElement(node.parentNode))); 4675 } 4676 4677 function isEditable(node) { 4678 return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node); 4679 } 4680 4681 var inlineDisplayRegex = /^inline(-block|-table)?$/i; 4682 4683 function isNonInlineElement(node) { 4684 return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display")); 4685 } 4686 4687 // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html) 4688 var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/; 4689 4690 function isUnrenderedWhiteSpaceNode(node) { 4691 if (node.data.length == 0) { 4692 return true; 4693 } 4694 if (htmlNonWhiteSpaceRegex.test(node.data)) { 4695 return false; 4696 } 4697 var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace"); 4698 switch (cssWhiteSpace) { 4699 case "pre": 4700 case "pre-wrap": 4701 case "-moz-pre-wrap": 4702 return false; 4703 case "pre-line": 4704 if (/[\r\n]/.test(node.data)) { 4705 return false; 4706 } 4707 } 4708 4709 // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a 4710 // non-inline element, it will not be rendered. This seems to be a good enough definition. 4711 return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling); 4712 } 4713 4714 function getRangeBoundaries(ranges) { 4715 var positions = [], i, range; 4716 for (i = 0; range = ranges[i++]; ) { 4717 positions.push( 4718 new DomPosition(range.startContainer, range.startOffset), 4719 new DomPosition(range.endContainer, range.endOffset) 4720 ); 4721 } 4722 return positions; 4723 } 4724 4725 function updateRangesFromBoundaries(ranges, positions) { 4726 for (var i = 0, range, start, end, len = ranges.length; i < len; ++i) { 4727 range = ranges[i]; 4728 start = positions[i * 2]; 4729 end = positions[i * 2 + 1]; 4730 range.setStartAndEnd(start.node, start.offset, end.node, end.offset); 4731 } 4732 } 4733 4734 function isSplitPoint(node, offset) { 4735 if (dom.isCharacterDataNode(node)) { 4736 if (offset == 0) { 4737 return !!node.previousSibling; 4738 } else if (offset == node.length) { 4739 return !!node.nextSibling; 4740 } else { 4741 return true; 4742 } 4743 } 4744 4745 return offset > 0 && offset < node.childNodes.length; 4746 } 4747 4748 function splitNodeAt(node, descendantNode, descendantOffset, positionsToPreserve) { 4749 var newNode, parentNode; 4750 var splitAtStart = (descendantOffset == 0); 4751 4752 if (dom.isAncestorOf(descendantNode, node)) { 4753 return node; 4754 } 4755 4756 if (dom.isCharacterDataNode(descendantNode)) { 4757 var descendantIndex = dom.getNodeIndex(descendantNode); 4758 if (descendantOffset == 0) { 4759 descendantOffset = descendantIndex; 4760 } else if (descendantOffset == descendantNode.length) { 4761 descendantOffset = descendantIndex + 1; 4762 } else { 4763 throw module.createError("splitNodeAt() should not be called with offset in the middle of a data node (" + 4764 descendantOffset + " in " + descendantNode.data); 4765 } 4766 descendantNode = descendantNode.parentNode; 4767 } 4768 4769 if (isSplitPoint(descendantNode, descendantOffset)) { 4770 // descendantNode is now guaranteed not to be a text or other character node 4771 newNode = descendantNode.cloneNode(false); 4772 parentNode = descendantNode.parentNode; 4773 if (newNode.id) { 4774 newNode.removeAttribute("id"); 4775 } 4776 var child, newChildIndex = 0; 4777 4778 while ( (child = descendantNode.childNodes[descendantOffset]) ) { 4779 movePreservingPositions(child, newNode, newChildIndex++, positionsToPreserve); 4780 } 4781 movePreservingPositions(newNode, parentNode, dom.getNodeIndex(descendantNode) + 1, positionsToPreserve); 4782 return (descendantNode == node) ? newNode : splitNodeAt(node, parentNode, dom.getNodeIndex(newNode), positionsToPreserve); 4783 } else if (node != descendantNode) { 4784 newNode = descendantNode.parentNode; 4785 4786 // Work out a new split point in the parent node 4787 var newNodeIndex = dom.getNodeIndex(descendantNode); 4788 4789 if (!splitAtStart) { 4790 newNodeIndex++; 4791 } 4792 return splitNodeAt(node, newNode, newNodeIndex, positionsToPreserve); 4793 } 4794 return node; 4795 } 4796 4797 function areElementsMergeable(el1, el2) { 4798 return el1.namespaceURI == el2.namespaceURI && 4799 el1.tagName.toLowerCase() == el2.tagName.toLowerCase() && 4800 haveSameClasses(el1, el2) && 4801 elementsHaveSameNonClassAttributes(el1, el2) && 4802 getComputedStyleProperty(el1, "display") == "inline" && 4803 getComputedStyleProperty(el2, "display") == "inline"; 4804 } 4805 4806 function createAdjacentMergeableTextNodeGetter(forward) { 4807 var siblingPropName = forward ? "nextSibling" : "previousSibling"; 4808 4809 return function(textNode, checkParentElement) { 4810 var el = textNode.parentNode; 4811 var adjacentNode = textNode[siblingPropName]; 4812 if (adjacentNode) { 4813 // Can merge if the node's previous/next sibling is a text node 4814 if (adjacentNode && adjacentNode.nodeType == 3) { 4815 return adjacentNode; 4816 } 4817 } else if (checkParentElement) { 4818 // Compare text node parent element with its sibling 4819 adjacentNode = el[siblingPropName]; 4820 if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) { 4821 var adjacentNodeChild = adjacentNode[forward ? "firstChild" : "lastChild"]; 4822 if (adjacentNodeChild && adjacentNodeChild.nodeType == 3) { 4823 return adjacentNodeChild; 4824 } 4825 } 4826 } 4827 return null; 4828 }; 4829 } 4830 4831 var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false), 4832 getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true); 4833 4834 4835 function Merge(firstNode) { 4836 this.isElementMerge = (firstNode.nodeType == 1); 4837 this.textNodes = []; 4838 var firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; 4839 if (firstTextNode) { 4840 this.textNodes[0] = firstTextNode; 4841 } 4842 } 4843 4844 Merge.prototype = { 4845 doMerge: function(positionsToPreserve) { 4846 var textNodes = this.textNodes; 4847 var firstTextNode = textNodes[0]; 4848 if (textNodes.length > 1) { 4849 var firstTextNodeIndex = dom.getNodeIndex(firstTextNode); 4850 var textParts = [], combinedTextLength = 0, textNode, parent; 4851 forEach(textNodes, function(textNode, i) { 4852 parent = textNode.parentNode; 4853 if (i > 0) { 4854 parent.removeChild(textNode); 4855 if (!parent.hasChildNodes()) { 4856 dom.removeNode(parent); 4857 } 4858 if (positionsToPreserve) { 4859 forEach(positionsToPreserve, function(position) { 4860 // Handle case where position is inside the text node being merged into a preceding node 4861 if (position.node == textNode) { 4862 position.node = firstTextNode; 4863 position.offset += combinedTextLength; 4864 } 4865 // Handle case where both text nodes precede the position within the same parent node 4866 if (position.node == parent && position.offset > firstTextNodeIndex) { 4867 --position.offset; 4868 if (position.offset == firstTextNodeIndex + 1 && i < len - 1) { 4869 position.node = firstTextNode; 4870 position.offset = combinedTextLength; 4871 } 4872 } 4873 }); 4874 } 4875 } 4876 textParts[i] = textNode.data; 4877 combinedTextLength += textNode.data.length; 4878 }); 4879 firstTextNode.data = textParts.join(""); 4880 } 4881 return firstTextNode.data; 4882 }, 4883 4884 getLength: function() { 4885 var i = this.textNodes.length, len = 0; 4886 while (i--) { 4887 len += this.textNodes[i].length; 4888 } 4889 return len; 4890 }, 4891 4892 toString: function() { 4893 var textParts = []; 4894 forEach(this.textNodes, function(textNode, i) { 4895 textParts[i] = "'" + textNode.data + "'"; 4896 }); 4897 return "[Merge(" + textParts.join(",") + ")]"; 4898 } 4899 }; 4900 4901 var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly", "useExistingElements", 4902 "removeEmptyElements", "onElementCreate"]; 4903 4904 // TODO: Populate this with every attribute name that corresponds to a property with a different name. Really?? 4905 var attrNamesForProperties = {}; 4906 4907 function ClassApplier(className, options, tagNames) { 4908 var normalize, i, len, propName, applier = this; 4909 applier.cssClass = applier.className = className; // cssClass property is for backward compatibility 4910 4911 var elementPropertiesFromOptions = null, elementAttributes = {}; 4912 4913 // Initialize from options object 4914 if (typeof options == "object" && options !== null) { 4915 if (typeof options.elementTagName !== "undefined") { 4916 options.elementTagName = options.elementTagName.toLowerCase(); 4917 } 4918 tagNames = options.tagNames; 4919 elementPropertiesFromOptions = options.elementProperties; 4920 elementAttributes = options.elementAttributes; 4921 4922 for (i = 0; propName = optionProperties[i++]; ) { 4923 if (options.hasOwnProperty(propName)) { 4924 applier[propName] = options[propName]; 4925 } 4926 } 4927 normalize = options.normalize; 4928 } else { 4929 normalize = options; 4930 } 4931 4932 // Backward compatibility: the second parameter can also be a Boolean indicating to normalize after unapplying 4933 applier.normalize = (typeof normalize == "undefined") ? true : normalize; 4934 4935 // Initialize element properties and attribute exceptions 4936 applier.attrExceptions = []; 4937 var el = document.createElement(applier.elementTagName); 4938 applier.elementProperties = applier.copyPropertiesToElement(elementPropertiesFromOptions, el, true); 4939 each(elementAttributes, function(attrName, attrValue) { 4940 applier.attrExceptions.push(attrName); 4941 // Ensure each attribute value is a string 4942 elementAttributes[attrName] = "" + attrValue; 4943 }); 4944 applier.elementAttributes = elementAttributes; 4945 4946 applier.elementSortedClassName = applier.elementProperties.hasOwnProperty("className") ? 4947 sortClassName(applier.elementProperties.className + " " + className) : className; 4948 4949 // Initialize tag names 4950 applier.applyToAnyTagName = false; 4951 var type = typeof tagNames; 4952 if (type == "string") { 4953 if (tagNames == "*") { 4954 applier.applyToAnyTagName = true; 4955 } else { 4956 applier.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/); 4957 } 4958 } else if (type == "object" && typeof tagNames.length == "number") { 4959 applier.tagNames = []; 4960 for (i = 0, len = tagNames.length; i < len; ++i) { 4961 if (tagNames[i] == "*") { 4962 applier.applyToAnyTagName = true; 4963 } else { 4964 applier.tagNames.push(tagNames[i].toLowerCase()); 4965 } 4966 } 4967 } else { 4968 applier.tagNames = [applier.elementTagName]; 4969 } 4970 } 4971 4972 ClassApplier.prototype = { 4973 elementTagName: defaultTagName, 4974 elementProperties: {}, 4975 elementAttributes: {}, 4976 ignoreWhiteSpace: true, 4977 applyToEditableOnly: false, 4978 useExistingElements: true, 4979 removeEmptyElements: true, 4980 onElementCreate: null, 4981 4982 copyPropertiesToElement: function(props, el, createCopy) { 4983 var s, elStyle, elProps = {}, elPropsStyle, propValue, elPropValue, attrName; 4984 4985 for (var p in props) { 4986 if (props.hasOwnProperty(p)) { 4987 propValue = props[p]; 4988 elPropValue = el[p]; 4989 4990 // Special case for class. The copied properties object has the applier's class as well as its own 4991 // to simplify checks when removing styling elements 4992 if (p == "className") { 4993 addClass(el, propValue); 4994 addClass(el, this.className); 4995 el[p] = sortClassName(el[p]); 4996 if (createCopy) { 4997 elProps[p] = propValue; 4998 } 4999 } 5000 5001 // Special case for style 5002 else if (p == "style") { 5003 elStyle = elPropValue; 5004 if (createCopy) { 5005 elProps[p] = elPropsStyle = {}; 5006 } 5007 for (s in props[p]) { 5008 if (props[p].hasOwnProperty(s)) { 5009 elStyle[s] = propValue[s]; 5010 if (createCopy) { 5011 elPropsStyle[s] = elStyle[s]; 5012 } 5013 } 5014 } 5015 this.attrExceptions.push(p); 5016 } else { 5017 el[p] = propValue; 5018 // Copy the property back from the dummy element so that later comparisons to check whether 5019 // elements may be removed are checking against the right value. For example, the href property 5020 // of an element returns a fully qualified URL even if it was previously assigned a relative 5021 // URL. 5022 if (createCopy) { 5023 elProps[p] = el[p]; 5024 5025 // Not all properties map to identically-named attributes 5026 attrName = attrNamesForProperties.hasOwnProperty(p) ? attrNamesForProperties[p] : p; 5027 this.attrExceptions.push(attrName); 5028 } 5029 } 5030 } 5031 } 5032 5033 return createCopy ? elProps : ""; 5034 }, 5035 5036 copyAttributesToElement: function(attrs, el) { 5037 for (var attrName in attrs) { 5038 if (attrs.hasOwnProperty(attrName) && !/^class(?:Name)?$/i.test(attrName)) { 5039 el.setAttribute(attrName, attrs[attrName]); 5040 } 5041 } 5042 }, 5043 5044 appliesToElement: function(el) { 5045 return contains(this.tagNames, el.tagName.toLowerCase()); 5046 }, 5047 5048 getEmptyElements: function(range) { 5049 var applier = this; 5050 return range.getNodes([1], function(el) { 5051 return applier.appliesToElement(el) && !el.hasChildNodes(); 5052 }); 5053 }, 5054 5055 hasClass: function(node) { 5056 return node.nodeType == 1 && 5057 (this.applyToAnyTagName || this.appliesToElement(node)) && 5058 hasClass(node, this.className); 5059 }, 5060 5061 getSelfOrAncestorWithClass: function(node) { 5062 while (node) { 5063 if (this.hasClass(node)) { 5064 return node; 5065 } 5066 node = node.parentNode; 5067 } 5068 return null; 5069 }, 5070 5071 isModifiable: function(node) { 5072 return !this.applyToEditableOnly || isEditable(node); 5073 }, 5074 5075 // White space adjacent to an unwrappable node can be ignored for wrapping 5076 isIgnorableWhiteSpaceNode: function(node) { 5077 return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node); 5078 }, 5079 5080 // Normalizes nodes after applying a class to a Range. 5081 postApply: function(textNodes, range, positionsToPreserve, isUndo) { 5082 var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1]; 5083 5084 var merges = [], currentMerge; 5085 5086 var rangeStartNode = firstNode, rangeEndNode = lastNode; 5087 var rangeStartOffset = 0, rangeEndOffset = lastNode.length; 5088 5089 var textNode, precedingTextNode; 5090 5091 // Check for every required merge and create a Merge object for each 5092 forEach(textNodes, function(textNode) { 5093 precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo); 5094 if (precedingTextNode) { 5095 if (!currentMerge) { 5096 currentMerge = new Merge(precedingTextNode); 5097 merges.push(currentMerge); 5098 } 5099 currentMerge.textNodes.push(textNode); 5100 if (textNode === firstNode) { 5101 rangeStartNode = currentMerge.textNodes[0]; 5102 rangeStartOffset = rangeStartNode.length; 5103 } 5104 if (textNode === lastNode) { 5105 rangeEndNode = currentMerge.textNodes[0]; 5106 rangeEndOffset = currentMerge.getLength(); 5107 } 5108 } else { 5109 currentMerge = null; 5110 } 5111 }); 5112 5113 // Test whether the first node after the range needs merging 5114 var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo); 5115 5116 if (nextTextNode) { 5117 if (!currentMerge) { 5118 currentMerge = new Merge(lastNode); 5119 merges.push(currentMerge); 5120 } 5121 currentMerge.textNodes.push(nextTextNode); 5122 } 5123 5124 // Apply the merges 5125 if (merges.length) { 5126 for (i = 0, len = merges.length; i < len; ++i) { 5127 merges[i].doMerge(positionsToPreserve); 5128 } 5129 5130 // Set the range boundaries 5131 range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset); 5132 } 5133 }, 5134 5135 createContainer: function(parentNode) { 5136 var doc = dom.getDocument(parentNode); 5137 var namespace; 5138 var el = createElementNSSupported && !dom.isHtmlNamespace(parentNode) && (namespace = parentNode.namespaceURI) ? 5139 doc.createElementNS(parentNode.namespaceURI, this.elementTagName) : 5140 doc.createElement(this.elementTagName); 5141 5142 this.copyPropertiesToElement(this.elementProperties, el, false); 5143 this.copyAttributesToElement(this.elementAttributes, el); 5144 addClass(el, this.className); 5145 if (this.onElementCreate) { 5146 this.onElementCreate(el, this); 5147 } 5148 return el; 5149 }, 5150 5151 elementHasProperties: function(el, props) { 5152 var applier = this; 5153 return each(props, function(p, propValue) { 5154 if (p == "className") { 5155 // For checking whether we should reuse an existing element, we just want to check that the element 5156 // has all the classes specified in the className property. When deciding whether the element is 5157 // removable when unapplying a class, there is separate special handling to check whether the 5158 // element has extra classes so the same simple check will do. 5159 return hasAllClasses(el, propValue); 5160 } else if (typeof propValue == "object") { 5161 if (!applier.elementHasProperties(el[p], propValue)) { 5162 return false; 5163 } 5164 } else if (el[p] !== propValue) { 5165 return false; 5166 } 5167 }); 5168 }, 5169 5170 elementHasAttributes: function(el, attrs) { 5171 return each(attrs, function(name, value) { 5172 if (el.getAttribute(name) !== value) { 5173 return false; 5174 } 5175 }); 5176 }, 5177 5178 applyToTextNode: function(textNode, positionsToPreserve) { 5179 5180 // Check whether the text node can be styled. Text within a <style> or <script> element, for example, 5181 // should not be styled. See issue 283. 5182 if (canTextBeStyled(textNode)) { 5183 var parent = textNode.parentNode; 5184 if (parent.childNodes.length == 1 && 5185 this.useExistingElements && 5186 this.appliesToElement(parent) && 5187 this.elementHasProperties(parent, this.elementProperties) && 5188 this.elementHasAttributes(parent, this.elementAttributes)) { 5189 5190 addClass(parent, this.className); 5191 } else { 5192 var textNodeParent = textNode.parentNode; 5193 var el = this.createContainer(textNodeParent); 5194 textNodeParent.insertBefore(el, textNode); 5195 el.appendChild(textNode); 5196 } 5197 } 5198 5199 }, 5200 5201 isRemovable: function(el) { 5202 return el.tagName.toLowerCase() == this.elementTagName && 5203 getSortedClassName(el) == this.elementSortedClassName && 5204 this.elementHasProperties(el, this.elementProperties) && 5205 !elementHasNonClassAttributes(el, this.attrExceptions) && 5206 this.elementHasAttributes(el, this.elementAttributes) && 5207 this.isModifiable(el); 5208 }, 5209 5210 isEmptyContainer: function(el) { 5211 var childNodeCount = el.childNodes.length; 5212 return el.nodeType == 1 && 5213 this.isRemovable(el) && 5214 (childNodeCount == 0 || (childNodeCount == 1 && this.isEmptyContainer(el.firstChild))); 5215 }, 5216 5217 removeEmptyContainers: function(range) { 5218 var applier = this; 5219 var nodesToRemove = range.getNodes([1], function(el) { 5220 return applier.isEmptyContainer(el); 5221 }); 5222 5223 var rangesToPreserve = [range]; 5224 var positionsToPreserve = getRangeBoundaries(rangesToPreserve); 5225 5226 forEach(nodesToRemove, function(node) { 5227 removePreservingPositions(node, positionsToPreserve); 5228 }); 5229 5230 // Update the range from the preserved boundary positions 5231 updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve); 5232 }, 5233 5234 undoToTextNode: function(textNode, range, ancestorWithClass, positionsToPreserve) { 5235 if (!range.containsNode(ancestorWithClass)) { 5236 // Split out the portion of the ancestor from which we can remove the class 5237 //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass); 5238 var ancestorRange = range.cloneRange(); 5239 ancestorRange.selectNode(ancestorWithClass); 5240 if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)) { 5241 splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, positionsToPreserve); 5242 range.setEndAfter(ancestorWithClass); 5243 } 5244 if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)) { 5245 ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, positionsToPreserve); 5246 } 5247 } 5248 5249 if (this.isRemovable(ancestorWithClass)) { 5250 replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve); 5251 } else { 5252 removeClass(ancestorWithClass, this.className); 5253 } 5254 }, 5255 5256 splitAncestorWithClass: function(container, offset, positionsToPreserve) { 5257 var ancestorWithClass = this.getSelfOrAncestorWithClass(container); 5258 if (ancestorWithClass) { 5259 splitNodeAt(ancestorWithClass, container, offset, positionsToPreserve); 5260 } 5261 }, 5262 5263 undoToAncestor: function(ancestorWithClass, positionsToPreserve) { 5264 if (this.isRemovable(ancestorWithClass)) { 5265 replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve); 5266 } else { 5267 removeClass(ancestorWithClass, this.className); 5268 } 5269 }, 5270 5271 applyToRange: function(range, rangesToPreserve) { 5272 var applier = this; 5273 rangesToPreserve = rangesToPreserve || []; 5274 5275 // Create an array of range boundaries to preserve 5276 var positionsToPreserve = getRangeBoundaries(rangesToPreserve || []); 5277 5278 range.splitBoundariesPreservingPositions(positionsToPreserve); 5279 5280 // Tidy up the DOM by removing empty containers 5281 if (applier.removeEmptyElements) { 5282 applier.removeEmptyContainers(range); 5283 } 5284 5285 var textNodes = getEffectiveTextNodes(range); 5286 5287 if (textNodes.length) { 5288 forEach(textNodes, function(textNode) { 5289 if (!applier.isIgnorableWhiteSpaceNode(textNode) && !applier.getSelfOrAncestorWithClass(textNode) && 5290 applier.isModifiable(textNode)) { 5291 applier.applyToTextNode(textNode, positionsToPreserve); 5292 } 5293 }); 5294 var lastTextNode = textNodes[textNodes.length - 1]; 5295 range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length); 5296 if (applier.normalize) { 5297 applier.postApply(textNodes, range, positionsToPreserve, false); 5298 } 5299 5300 // Update the ranges from the preserved boundary positions 5301 updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve); 5302 } 5303 5304 // Apply classes to any appropriate empty elements 5305 var emptyElements = applier.getEmptyElements(range); 5306 5307 forEach(emptyElements, function(el) { 5308 addClass(el, applier.className); 5309 }); 5310 }, 5311 5312 applyToRanges: function(ranges) { 5313 5314 var i = ranges.length; 5315 while (i--) { 5316 this.applyToRange(ranges[i], ranges); 5317 } 5318 5319 5320 return ranges; 5321 }, 5322 5323 applyToSelection: function(win) { 5324 var sel = api.getSelection(win); 5325 sel.setRanges( this.applyToRanges(sel.getAllRanges()) ); 5326 }, 5327 5328 undoToRange: function(range, rangesToPreserve) { 5329 var applier = this; 5330 // Create an array of range boundaries to preserve 5331 rangesToPreserve = rangesToPreserve || []; 5332 var positionsToPreserve = getRangeBoundaries(rangesToPreserve); 5333 5334 5335 range.splitBoundariesPreservingPositions(positionsToPreserve); 5336 5337 // Tidy up the DOM by removing empty containers 5338 if (applier.removeEmptyElements) { 5339 applier.removeEmptyContainers(range, positionsToPreserve); 5340 } 5341 5342 var textNodes = getEffectiveTextNodes(range); 5343 var textNode, ancestorWithClass; 5344 var lastTextNode = textNodes[textNodes.length - 1]; 5345 5346 if (textNodes.length) { 5347 applier.splitAncestorWithClass(range.endContainer, range.endOffset, positionsToPreserve); 5348 applier.splitAncestorWithClass(range.startContainer, range.startOffset, positionsToPreserve); 5349 for (var i = 0, len = textNodes.length; i < len; ++i) { 5350 textNode = textNodes[i]; 5351 ancestorWithClass = applier.getSelfOrAncestorWithClass(textNode); 5352 if (ancestorWithClass && applier.isModifiable(textNode)) { 5353 applier.undoToAncestor(ancestorWithClass, positionsToPreserve); 5354 } 5355 } 5356 // Ensure the range is still valid 5357 range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length); 5358 5359 5360 if (applier.normalize) { 5361 applier.postApply(textNodes, range, positionsToPreserve, true); 5362 } 5363 5364 // Update the ranges from the preserved boundary positions 5365 updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve); 5366 } 5367 5368 // Remove class from any appropriate empty elements 5369 var emptyElements = applier.getEmptyElements(range); 5370 5371 forEach(emptyElements, function(el) { 5372 removeClass(el, applier.className); 5373 }); 5374 }, 5375 5376 undoToRanges: function(ranges) { 5377 // Get ranges returned in document order 5378 var i = ranges.length; 5379 5380 while (i--) { 5381 this.undoToRange(ranges[i], ranges); 5382 } 5383 5384 return ranges; 5385 }, 5386 5387 undoToSelection: function(win) { 5388 var sel = api.getSelection(win); 5389 var ranges = api.getSelection(win).getAllRanges(); 5390 this.undoToRanges(ranges); 5391 sel.setRanges(ranges); 5392 }, 5393 5394 isAppliedToRange: function(range) { 5395 if (range.collapsed || range.toString() == "") { 5396 return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer); 5397 } else { 5398 var textNodes = range.getNodes( [3] ); 5399 if (textNodes.length) 5400 for (var i = 0, textNode; textNode = textNodes[i++]; ) { 5401 if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode) && 5402 this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) { 5403 return false; 5404 } 5405 } 5406 return true; 5407 } 5408 }, 5409 5410 isAppliedToRanges: function(ranges) { 5411 var i = ranges.length; 5412 if (i == 0) { 5413 return false; 5414 } 5415 while (i--) { 5416 if (!this.isAppliedToRange(ranges[i])) { 5417 return false; 5418 } 5419 } 5420 return true; 5421 }, 5422 5423 isAppliedToSelection: function(win) { 5424 var sel = api.getSelection(win); 5425 return this.isAppliedToRanges(sel.getAllRanges()); 5426 }, 5427 5428 toggleRange: function(range) { 5429 if (this.isAppliedToRange(range)) { 5430 this.undoToRange(range); 5431 } else { 5432 this.applyToRange(range); 5433 } 5434 }, 5435 5436 toggleSelection: function(win) { 5437 if (this.isAppliedToSelection(win)) { 5438 this.undoToSelection(win); 5439 } else { 5440 this.applyToSelection(win); 5441 } 5442 }, 5443 5444 getElementsWithClassIntersectingRange: function(range) { 5445 var elements = []; 5446 var applier = this; 5447 range.getNodes([3], function(textNode) { 5448 var el = applier.getSelfOrAncestorWithClass(textNode); 5449 if (el && !contains(elements, el)) { 5450 elements.push(el); 5451 } 5452 }); 5453 return elements; 5454 }, 5455 5456 detach: function() {} 5457 }; 5458 5459 function createClassApplier(className, options, tagNames) { 5460 return new ClassApplier(className, options, tagNames); 5461 } 5462 5463 ClassApplier.util = { 5464 hasClass: hasClass, 5465 addClass: addClass, 5466 removeClass: removeClass, 5467 getClass: getClass, 5468 hasSameClasses: haveSameClasses, 5469 hasAllClasses: hasAllClasses, 5470 replaceWithOwnChildren: replaceWithOwnChildrenPreservingPositions, 5471 elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes, 5472 elementHasNonClassAttributes: elementHasNonClassAttributes, 5473 splitNodeAt: splitNodeAt, 5474 isEditableElement: isEditableElement, 5475 isEditingHost: isEditingHost, 5476 isEditable: isEditable 5477 }; 5478 5479 api.CssClassApplier = api.ClassApplier = ClassApplier; 5480 api.createClassApplier = createClassApplier; 5481 util.createAliasForDeprecatedMethod(api, "createCssClassApplier", "createClassApplier", module); 5482 }); 5483 5484 return rangy; 5485 }, this); 5486 5487 /** 5488 * Highlighter module for Rangy, a cross-browser JavaScript range and selection library 5489 * https://github.com/timdown/rangy 5490 * 5491 * Depends on Rangy core, ClassApplier and optionally TextRange modules. 5492 * 5493 * Copyright 2015, Tim Down 5494 * Licensed under the MIT license. 5495 * Version: 1.3.0 5496 * Build date: 10 May 2015 5497 */ 5498 (function(factory, root) { 5499 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 5500 factory(root.rangy); 5501 })(function(rangy) { 5502 rangy.createModule("Highlighter", ["ClassApplier"], function(api, module) { 5503 var dom = api.dom; 5504 var contains = dom.arrayContains; 5505 var getBody = dom.getBody; 5506 var createOptions = api.util.createOptions; 5507 var forEach = api.util.forEach; 5508 var nextHighlightId = 1; 5509 5510 // Puts highlights in order, last in document first. 5511 function compareHighlights(h1, h2) { 5512 return h1.characterRange.start - h2.characterRange.start; 5513 } 5514 5515 function getContainerElement(doc, id) { 5516 return id ? doc.getElementById(id) : getBody(doc); 5517 } 5518 5519 /*----------------------------------------------------------------------------------------------------------------*/ 5520 5521 var highlighterTypes = {}; 5522 5523 function HighlighterType(type, converterCreator) { 5524 this.type = type; 5525 this.converterCreator = converterCreator; 5526 } 5527 5528 HighlighterType.prototype.create = function() { 5529 var converter = this.converterCreator(); 5530 converter.type = this.type; 5531 return converter; 5532 }; 5533 5534 function registerHighlighterType(type, converterCreator) { 5535 highlighterTypes[type] = new HighlighterType(type, converterCreator); 5536 } 5537 5538 function getConverter(type) { 5539 var highlighterType = highlighterTypes[type]; 5540 if (highlighterType instanceof HighlighterType) { 5541 return highlighterType.create(); 5542 } else { 5543 throw new Error("Highlighter type '" + type + "' is not valid"); 5544 } 5545 } 5546 5547 api.registerHighlighterType = registerHighlighterType; 5548 5549 /*----------------------------------------------------------------------------------------------------------------*/ 5550 5551 function CharacterRange(start, end) { 5552 this.start = start; 5553 this.end = end; 5554 } 5555 5556 CharacterRange.prototype = { 5557 intersects: function(charRange) { 5558 return this.start < charRange.end && this.end > charRange.start; 5559 }, 5560 5561 isContiguousWith: function(charRange) { 5562 return this.start == charRange.end || this.end == charRange.start; 5563 }, 5564 5565 union: function(charRange) { 5566 return new CharacterRange(Math.min(this.start, charRange.start), Math.max(this.end, charRange.end)); 5567 }, 5568 5569 intersection: function(charRange) { 5570 return new CharacterRange(Math.max(this.start, charRange.start), Math.min(this.end, charRange.end)); 5571 }, 5572 5573 getComplements: function(charRange) { 5574 var ranges = []; 5575 if (this.start >= charRange.start) { 5576 if (this.end <= charRange.end) { 5577 return []; 5578 } 5579 ranges.push(new CharacterRange(charRange.end, this.end)); 5580 } else { 5581 ranges.push(new CharacterRange(this.start, Math.min(this.end, charRange.start))); 5582 if (this.end > charRange.end) { 5583 ranges.push(new CharacterRange(charRange.end, this.end)); 5584 } 5585 } 5586 return ranges; 5587 }, 5588 5589 toString: function() { 5590 return "[CharacterRange(" + this.start + ", " + this.end + ")]"; 5591 } 5592 }; 5593 5594 CharacterRange.fromCharacterRange = function(charRange) { 5595 return new CharacterRange(charRange.start, charRange.end); 5596 }; 5597 5598 /*----------------------------------------------------------------------------------------------------------------*/ 5599 5600 var textContentConverter = { 5601 rangeToCharacterRange: function(range, containerNode) { 5602 var bookmark = range.getBookmark(containerNode); 5603 return new CharacterRange(bookmark.start, bookmark.end); 5604 }, 5605 5606 characterRangeToRange: function(doc, characterRange, containerNode) { 5607 var range = api.createRange(doc); 5608 range.moveToBookmark({ 5609 start: characterRange.start, 5610 end: characterRange.end, 5611 containerNode: containerNode 5612 }); 5613 5614 return range; 5615 }, 5616 5617 serializeSelection: function(selection, containerNode) { 5618 var ranges = selection.getAllRanges(), rangeCount = ranges.length; 5619 var rangeInfos = []; 5620 5621 var backward = rangeCount == 1 && selection.isBackward(); 5622 5623 for (var i = 0, len = ranges.length; i < len; ++i) { 5624 rangeInfos[i] = { 5625 characterRange: this.rangeToCharacterRange(ranges[i], containerNode), 5626 backward: backward 5627 }; 5628 } 5629 5630 return rangeInfos; 5631 }, 5632 5633 restoreSelection: function(selection, savedSelection, containerNode) { 5634 selection.removeAllRanges(); 5635 var doc = selection.win.document; 5636 for (var i = 0, len = savedSelection.length, range, rangeInfo, characterRange; i < len; ++i) { 5637 rangeInfo = savedSelection[i]; 5638 characterRange = rangeInfo.characterRange; 5639 range = this.characterRangeToRange(doc, rangeInfo.characterRange, containerNode); 5640 selection.addRange(range, rangeInfo.backward); 5641 } 5642 } 5643 }; 5644 5645 registerHighlighterType("textContent", function() { 5646 return textContentConverter; 5647 }); 5648 5649 /*----------------------------------------------------------------------------------------------------------------*/ 5650 5651 // Lazily load the TextRange-based converter so that the dependency is only checked when required. 5652 registerHighlighterType("TextRange", (function() { 5653 var converter; 5654 5655 return function() { 5656 if (!converter) { 5657 // Test that textRangeModule exists and is supported 5658 var textRangeModule = api.modules.TextRange; 5659 if (!textRangeModule) { 5660 throw new Error("TextRange module is missing."); 5661 } else if (!textRangeModule.supported) { 5662 throw new Error("TextRange module is present but not supported."); 5663 } 5664 5665 converter = { 5666 rangeToCharacterRange: function(range, containerNode) { 5667 return CharacterRange.fromCharacterRange( range.toCharacterRange(containerNode) ); 5668 }, 5669 5670 characterRangeToRange: function(doc, characterRange, containerNode) { 5671 var range = api.createRange(doc); 5672 range.selectCharacters(containerNode, characterRange.start, characterRange.end); 5673 return range; 5674 }, 5675 5676 serializeSelection: function(selection, containerNode) { 5677 return selection.saveCharacterRanges(containerNode); 5678 }, 5679 5680 restoreSelection: function(selection, savedSelection, containerNode) { 5681 selection.restoreCharacterRanges(containerNode, savedSelection); 5682 } 5683 }; 5684 } 5685 5686 return converter; 5687 }; 5688 })()); 5689 5690 /*----------------------------------------------------------------------------------------------------------------*/ 5691 5692 function Highlight(doc, characterRange, classApplier, converter, id, containerElementId) { 5693 if (id) { 5694 this.id = id; 5695 nextHighlightId = Math.max(nextHighlightId, id + 1); 5696 } else { 5697 this.id = nextHighlightId++; 5698 } 5699 this.characterRange = characterRange; 5700 this.doc = doc; 5701 this.classApplier = classApplier; 5702 this.converter = converter; 5703 this.containerElementId = containerElementId || null; 5704 this.applied = false; 5705 } 5706 5707 Highlight.prototype = { 5708 getContainerElement: function() { 5709 return getContainerElement(this.doc, this.containerElementId); 5710 }, 5711 5712 getRange: function() { 5713 return this.converter.characterRangeToRange(this.doc, this.characterRange, this.getContainerElement()); 5714 }, 5715 5716 fromRange: function(range) { 5717 this.characterRange = this.converter.rangeToCharacterRange(range, this.getContainerElement()); 5718 }, 5719 5720 getText: function() { 5721 return this.getRange().toString(); 5722 }, 5723 5724 containsElement: function(el) { 5725 return this.getRange().containsNodeContents(el.firstChild); 5726 }, 5727 5728 unapply: function() { 5729 this.classApplier.undoToRange(this.getRange()); 5730 this.applied = false; 5731 }, 5732 5733 apply: function() { 5734 this.classApplier.applyToRange(this.getRange()); 5735 this.applied = true; 5736 }, 5737 5738 getHighlightElements: function() { 5739 return this.classApplier.getElementsWithClassIntersectingRange(this.getRange()); 5740 }, 5741 5742 toString: function() { 5743 return "[Highlight(ID: " + this.id + ", class: " + this.classApplier.className + ", character range: " + 5744 this.characterRange.start + " - " + this.characterRange.end + ")]"; 5745 } 5746 }; 5747 5748 /*----------------------------------------------------------------------------------------------------------------*/ 5749 5750 function Highlighter(doc, type) { 5751 type = type || "textContent"; 5752 this.doc = doc || document; 5753 this.classAppliers = {}; 5754 this.highlights = []; 5755 this.converter = getConverter(type); 5756 } 5757 5758 Highlighter.prototype = { 5759 addClassApplier: function(classApplier) { 5760 this.classAppliers[classApplier.className] = classApplier; 5761 }, 5762 5763 getHighlightForElement: function(el) { 5764 var highlights = this.highlights; 5765 for (var i = 0, len = highlights.length; i < len; ++i) { 5766 if (highlights[i].containsElement(el)) { 5767 return highlights[i]; 5768 } 5769 } 5770 return null; 5771 }, 5772 5773 removeHighlights: function(highlights) { 5774 for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) { 5775 highlight = this.highlights[i]; 5776 if (contains(highlights, highlight)) { 5777 highlight.unapply(); 5778 this.highlights.splice(i--, 1); 5779 } 5780 } 5781 }, 5782 5783 removeAllHighlights: function() { 5784 this.removeHighlights(this.highlights); 5785 }, 5786 5787 getIntersectingHighlights: function(ranges) { 5788 // Test each range against each of the highlighted ranges to see whether they overlap 5789 var intersectingHighlights = [], highlights = this.highlights; 5790 forEach(ranges, function(range) { 5791 //var selCharRange = converter.rangeToCharacterRange(range); 5792 forEach(highlights, function(highlight) { 5793 if (range.intersectsRange( highlight.getRange() ) && !contains(intersectingHighlights, highlight)) { 5794 intersectingHighlights.push(highlight); 5795 } 5796 }); 5797 }); 5798 5799 return intersectingHighlights; 5800 }, 5801 5802 highlightCharacterRanges: function(className, charRanges, options) { 5803 var i, len, j; 5804 var highlights = this.highlights; 5805 var converter = this.converter; 5806 var doc = this.doc; 5807 var highlightsToRemove = []; 5808 var classApplier = className ? this.classAppliers[className] : null; 5809 5810 options = createOptions(options, { 5811 containerElementId: null, 5812 exclusive: true 5813 }); 5814 5815 var containerElementId = options.containerElementId; 5816 var exclusive = options.exclusive; 5817 5818 var containerElement, containerElementRange, containerElementCharRange; 5819 if (containerElementId) { 5820 containerElement = this.doc.getElementById(containerElementId); 5821 if (containerElement) { 5822 containerElementRange = api.createRange(this.doc); 5823 containerElementRange.selectNodeContents(containerElement); 5824 containerElementCharRange = new CharacterRange(0, containerElementRange.toString().length); 5825 } 5826 } 5827 5828 var charRange, highlightCharRange, removeHighlight, isSameClassApplier, highlightsToKeep, splitHighlight; 5829 5830 for (i = 0, len = charRanges.length; i < len; ++i) { 5831 charRange = charRanges[i]; 5832 highlightsToKeep = []; 5833 5834 // Restrict character range to container element, if it exists 5835 if (containerElementCharRange) { 5836 charRange = charRange.intersection(containerElementCharRange); 5837 } 5838 5839 // Ignore empty ranges 5840 if (charRange.start == charRange.end) { 5841 continue; 5842 } 5843 5844 // Check for intersection with existing highlights. For each intersection, create a new highlight 5845 // which is the union of the highlight range and the selected range 5846 for (j = 0; j < highlights.length; ++j) { 5847 removeHighlight = false; 5848 5849 if (containerElementId == highlights[j].containerElementId) { 5850 highlightCharRange = highlights[j].characterRange; 5851 isSameClassApplier = (classApplier == highlights[j].classApplier); 5852 splitHighlight = !isSameClassApplier && exclusive; 5853 5854 // Replace the existing highlight if it needs to be: 5855 // 1. merged (isSameClassApplier) 5856 // 2. partially or entirely erased (className === null) 5857 // 3. partially or entirely replaced (isSameClassApplier == false && exclusive == true) 5858 if ( (highlightCharRange.intersects(charRange) || highlightCharRange.isContiguousWith(charRange)) && 5859 (isSameClassApplier || splitHighlight) ) { 5860 5861 // Remove existing highlights, keeping the unselected parts 5862 if (splitHighlight) { 5863 forEach(highlightCharRange.getComplements(charRange), function(rangeToAdd) { 5864 highlightsToKeep.push( new Highlight(doc, rangeToAdd, highlights[j].classApplier, converter, null, containerElementId) ); 5865 }); 5866 } 5867 5868 removeHighlight = true; 5869 if (isSameClassApplier) { 5870 charRange = highlightCharRange.union(charRange); 5871 } 5872 } 5873 } 5874 5875 if (removeHighlight) { 5876 highlightsToRemove.push(highlights[j]); 5877 highlights[j] = new Highlight(doc, highlightCharRange.union(charRange), classApplier, converter, null, containerElementId); 5878 } else { 5879 highlightsToKeep.push(highlights[j]); 5880 } 5881 } 5882 5883 // Add new range 5884 if (classApplier) { 5885 highlightsToKeep.push(new Highlight(doc, charRange, classApplier, converter, null, containerElementId)); 5886 } 5887 this.highlights = highlights = highlightsToKeep; 5888 } 5889 5890 // Remove the old highlights 5891 forEach(highlightsToRemove, function(highlightToRemove) { 5892 highlightToRemove.unapply(); 5893 }); 5894 5895 // Apply new highlights 5896 var newHighlights = []; 5897 forEach(highlights, function(highlight) { 5898 if (!highlight.applied) { 5899 highlight.apply(); 5900 newHighlights.push(highlight); 5901 } 5902 }); 5903 5904 return newHighlights; 5905 }, 5906 5907 highlightRanges: function(className, ranges, options) { 5908 var selCharRanges = []; 5909 var converter = this.converter; 5910 5911 options = createOptions(options, { 5912 containerElement: null, 5913 exclusive: true 5914 }); 5915 5916 var containerElement = options.containerElement; 5917 var containerElementId = containerElement ? containerElement.id : null; 5918 var containerElementRange; 5919 if (containerElement) { 5920 containerElementRange = api.createRange(containerElement); 5921 containerElementRange.selectNodeContents(containerElement); 5922 } 5923 5924 forEach(ranges, function(range) { 5925 var scopedRange = containerElement ? containerElementRange.intersection(range) : range; 5926 selCharRanges.push( converter.rangeToCharacterRange(scopedRange, containerElement || getBody(range.getDocument())) ); 5927 }); 5928 5929 return this.highlightCharacterRanges(className, selCharRanges, { 5930 containerElementId: containerElementId, 5931 exclusive: options.exclusive 5932 }); 5933 }, 5934 5935 highlightSelection: function(className, options) { 5936 var converter = this.converter; 5937 var classApplier = className ? this.classAppliers[className] : false; 5938 5939 options = createOptions(options, { 5940 containerElementId: null, 5941 selection: api.getSelection(this.doc), 5942 exclusive: true 5943 }); 5944 5945 var containerElementId = options.containerElementId; 5946 var exclusive = options.exclusive; 5947 var selection = options.selection; 5948 var doc = selection.win.document; 5949 var containerElement = getContainerElement(doc, containerElementId); 5950 5951 if (!classApplier && className !== false) { 5952 throw new Error("No class applier found for class '" + className + "'"); 5953 } 5954 5955 // Store the existing selection as character ranges 5956 var serializedSelection = converter.serializeSelection(selection, containerElement); 5957 5958 // Create an array of selected character ranges 5959 var selCharRanges = []; 5960 forEach(serializedSelection, function(rangeInfo) { 5961 selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) ); 5962 }); 5963 5964 var newHighlights = this.highlightCharacterRanges(className, selCharRanges, { 5965 containerElementId: containerElementId, 5966 exclusive: exclusive 5967 }); 5968 5969 // Restore selection 5970 converter.restoreSelection(selection, serializedSelection, containerElement); 5971 5972 return newHighlights; 5973 }, 5974 5975 unhighlightSelection: function(selection) { 5976 selection = selection || api.getSelection(this.doc); 5977 var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() ); 5978 this.removeHighlights(intersectingHighlights); 5979 selection.removeAllRanges(); 5980 return intersectingHighlights; 5981 }, 5982 5983 getHighlightsInSelection: function(selection) { 5984 selection = selection || api.getSelection(this.doc); 5985 return this.getIntersectingHighlights(selection.getAllRanges()); 5986 }, 5987 5988 selectionOverlapsHighlight: function(selection) { 5989 return this.getHighlightsInSelection(selection).length > 0; 5990 }, 5991 5992 serialize: function(options) { 5993 var highlighter = this; 5994 var highlights = highlighter.highlights; 5995 var serializedType, serializedHighlights, convertType, serializationConverter; 5996 5997 highlights.sort(compareHighlights); 5998 options = createOptions(options, { 5999 serializeHighlightText: false, 6000 type: highlighter.converter.type 6001 }); 6002 6003 serializedType = options.type; 6004 convertType = (serializedType != highlighter.converter.type); 6005 6006 if (convertType) { 6007 serializationConverter = getConverter(serializedType); 6008 } 6009 6010 serializedHighlights = ["type:" + serializedType]; 6011 6012 forEach(highlights, function(highlight) { 6013 var characterRange = highlight.characterRange; 6014 var containerElement; 6015 6016 // Convert to the current Highlighter's type, if different from the serialization type 6017 if (convertType) { 6018 containerElement = highlight.getContainerElement(); 6019 characterRange = serializationConverter.rangeToCharacterRange( 6020 highlighter.converter.characterRangeToRange(highlighter.doc, characterRange, containerElement), 6021 containerElement 6022 ); 6023 } 6024 6025 var parts = [ 6026 characterRange.start, 6027 characterRange.end, 6028 highlight.id, 6029 highlight.classApplier.className, 6030 highlight.containerElementId 6031 ]; 6032 6033 if (options.serializeHighlightText) { 6034 parts.push(highlight.getText()); 6035 } 6036 serializedHighlights.push( parts.join("$") ); 6037 }); 6038 6039 return serializedHighlights.join("|"); 6040 }, 6041 6042 deserialize: function(serialized) { 6043 var serializedHighlights = serialized.split("|"); 6044 var highlights = []; 6045 6046 var firstHighlight = serializedHighlights[0]; 6047 var regexResult; 6048 var serializationType, serializationConverter, convertType = false; 6049 if ( firstHighlight && (regexResult = /^type:(\w+)$/.exec(firstHighlight)) ) { 6050 serializationType = regexResult[1]; 6051 if (serializationType != this.converter.type) { 6052 serializationConverter = getConverter(serializationType); 6053 convertType = true; 6054 } 6055 serializedHighlights.shift(); 6056 } else { 6057 throw new Error("Serialized highlights are invalid."); 6058 } 6059 6060 var classApplier, highlight, characterRange, containerElementId, containerElement; 6061 6062 for (var i = serializedHighlights.length, parts; i-- > 0; ) { 6063 parts = serializedHighlights[i].split("$"); 6064 characterRange = new CharacterRange(+parts[0], +parts[1]); 6065 containerElementId = parts[4] || null; 6066 6067 // Convert to the current Highlighter's type, if different from the serialization type 6068 if (convertType) { 6069 containerElement = getContainerElement(this.doc, containerElementId); 6070 characterRange = this.converter.rangeToCharacterRange( 6071 serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement), 6072 containerElement 6073 ); 6074 } 6075 6076 classApplier = this.classAppliers[ parts[3] ]; 6077 6078 if (!classApplier) { 6079 throw new Error("No class applier found for class '" + parts[3] + "'"); 6080 } 6081 6082 highlight = new Highlight(this.doc, characterRange, classApplier, this.converter, parseInt(parts[2]), containerElementId); 6083 highlight.apply(); 6084 highlights.push(highlight); 6085 } 6086 this.highlights = highlights; 6087 } 6088 }; 6089 6090 api.Highlighter = Highlighter; 6091 6092 api.createHighlighter = function(doc, rangeCharacterOffsetConverterType) { 6093 return new Highlighter(doc, rangeCharacterOffsetConverterType); 6094 }; 6095 }); 6096 6097 return rangy; 6098 }, this); 6099 6100 /** 6101 * Text range module for Rangy. 6102 * Text-based manipulation and searching of ranges and selections. 6103 * 6104 * Features 6105 * 6106 * - Ability to move range boundaries by character or word offsets 6107 * - Customizable word tokenizer 6108 * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties 6109 * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case 6110 * sensitivity 6111 * - Selection and range save/restore as text offsets within a node 6112 * - Methods to return visible text within a range or selection 6113 * - innerText method for elements 6114 * 6115 * References 6116 * 6117 * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145 6118 * http://aryeh.name/spec/innertext/innertext.html 6119 * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html 6120 * 6121 * Part of Rangy, a cross-browser JavaScript range and selection library 6122 * https://github.com/timdown/rangy 6123 * 6124 * Depends on Rangy core. 6125 * 6126 * Copyright 2015, Tim Down 6127 * Licensed under the MIT license. 6128 * Version: 1.3.0 6129 * Build date: 10 May 2015 6130 */ 6131 6132 /** 6133 * Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers. 6134 * 6135 * First, a <br>: this is relatively simple. For the following HTML: 6136 * 6137 * 1 <br>2 6138 * 6139 * - IE and WebKit render the space, include it in the selection (i.e. when the content is selected and pasted into a 6140 * textarea, the space is present) and allow the caret to be placed after it. 6141 * - Firefox does not acknowledge the space in the selection but it is possible to place the caret after it. 6142 * - Opera does not render the space but has two separate caret positions on either side of the space (left and right 6143 * arrow keys show this) and includes the space in the selection. 6144 * 6145 * The other case is the line break or breaks implied by block elements. For the following HTML: 6146 * 6147 * <p>1 </p><p>2<p> 6148 * 6149 * - WebKit does not acknowledge the space in any way 6150 * - Firefox, IE and Opera as per <br> 6151 * 6152 * One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML: 6153 * 6154 * <p style="white-space: pre-line">1 6155 * 2</p> 6156 * 6157 * - Firefox and WebKit include the space in caret positions 6158 * - IE does not support pre-line up to and including version 9 6159 * - Opera ignores the space 6160 * - Trailing space only renders if there is a non-collapsed character in the line 6161 * 6162 * Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be 6163 * feature-tested 6164 */ 6165 (function(factory, root) { 6166 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 6167 factory(root.rangy); 6168 })(function(rangy) { 6169 rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) { 6170 var UNDEF = "undefined"; 6171 var CHARACTER = "character", WORD = "word"; 6172 var dom = api.dom, util = api.util; 6173 var extend = util.extend; 6174 var createOptions = util.createOptions; 6175 var getBody = dom.getBody; 6176 6177 6178 var spacesRegex = /^[ \t\f\r\n]+$/; 6179 var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/; 6180 var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/; 6181 var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/; 6182 var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/; 6183 6184 var defaultLanguage = "en"; 6185 6186 var isDirectionBackward = api.Selection.isDirectionBackward; 6187 6188 // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit, 6189 // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed. 6190 var trailingSpaceInBlockCollapses = false; 6191 var trailingSpaceBeforeBrCollapses = false; 6192 var trailingSpaceBeforeBlockCollapses = false; 6193 var trailingSpaceBeforeLineBreakInPreLineCollapses = true; 6194 6195 (function() { 6196 var el = dom.createTestElement(document, "<p>1 </p><p></p>", true); 6197 var p = el.firstChild; 6198 var sel = api.getSelection(); 6199 sel.collapse(p.lastChild, 2); 6200 sel.setStart(p.firstChild, 0); 6201 trailingSpaceInBlockCollapses = ("" + sel).length == 1; 6202 6203 el.innerHTML = "1 <br />"; 6204 sel.collapse(el, 2); 6205 sel.setStart(el.firstChild, 0); 6206 trailingSpaceBeforeBrCollapses = ("" + sel).length == 1; 6207 6208 el.innerHTML = "1 <p>1</p>"; 6209 sel.collapse(el, 2); 6210 sel.setStart(el.firstChild, 0); 6211 trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1; 6212 6213 dom.removeNode(el); 6214 sel.removeAllRanges(); 6215 })(); 6216 6217 /*----------------------------------------------------------------------------------------------------------------*/ 6218 6219 // This function must create word and non-word tokens for the whole of the text supplied to it 6220 function defaultTokenizer(chars, wordOptions) { 6221 var word = chars.join(""), result, tokenRanges = []; 6222 6223 function createTokenRange(start, end, isWord) { 6224 tokenRanges.push( { start: start, end: end, isWord: isWord } ); 6225 } 6226 6227 // Match words and mark characters 6228 var lastWordEnd = 0, wordStart, wordEnd; 6229 while ( (result = wordOptions.wordRegex.exec(word)) ) { 6230 wordStart = result.index; 6231 wordEnd = wordStart + result[0].length; 6232 6233 // Create token for non-word characters preceding this word 6234 if (wordStart > lastWordEnd) { 6235 createTokenRange(lastWordEnd, wordStart, false); 6236 } 6237 6238 // Get trailing space characters for word 6239 if (wordOptions.includeTrailingSpace) { 6240 while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) { 6241 ++wordEnd; 6242 } 6243 } 6244 createTokenRange(wordStart, wordEnd, true); 6245 lastWordEnd = wordEnd; 6246 } 6247 6248 // Create token for trailing non-word characters, if any exist 6249 if (lastWordEnd < chars.length) { 6250 createTokenRange(lastWordEnd, chars.length, false); 6251 } 6252 6253 return tokenRanges; 6254 } 6255 6256 function convertCharRangeToToken(chars, tokenRange) { 6257 var tokenChars = chars.slice(tokenRange.start, tokenRange.end); 6258 var token = { 6259 isWord: tokenRange.isWord, 6260 chars: tokenChars, 6261 toString: function() { 6262 return tokenChars.join(""); 6263 } 6264 }; 6265 for (var i = 0, len = tokenChars.length; i < len; ++i) { 6266 tokenChars[i].token = token; 6267 } 6268 return token; 6269 } 6270 6271 function tokenize(chars, wordOptions, tokenizer) { 6272 var tokenRanges = tokenizer(chars, wordOptions); 6273 var tokens = []; 6274 for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) { 6275 tokens.push( convertCharRangeToToken(chars, tokenRange) ); 6276 } 6277 return tokens; 6278 } 6279 6280 var defaultCharacterOptions = { 6281 includeBlockContentTrailingSpace: true, 6282 includeSpaceBeforeBr: true, 6283 includeSpaceBeforeBlock: true, 6284 includePreLineTrailingSpace: true, 6285 ignoreCharacters: "" 6286 }; 6287 6288 function normalizeIgnoredCharacters(ignoredCharacters) { 6289 // Check if character is ignored 6290 var ignoredChars = ignoredCharacters || ""; 6291 6292 // Normalize ignored characters into a string consisting of characters in ascending order of character code 6293 var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars; 6294 ignoredCharsArray.sort(function(char1, char2) { 6295 return char1.charCodeAt(0) - char2.charCodeAt(0); 6296 }); 6297 6298 /// Convert back to a string and remove duplicates 6299 return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1"); 6300 } 6301 6302 var defaultCaretCharacterOptions = { 6303 includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses, 6304 includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses, 6305 includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses, 6306 includePreLineTrailingSpace: true 6307 }; 6308 6309 var defaultWordOptions = { 6310 "en": { 6311 wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi, 6312 includeTrailingSpace: false, 6313 tokenizer: defaultTokenizer 6314 } 6315 }; 6316 6317 var defaultFindOptions = { 6318 caseSensitive: false, 6319 withinRange: null, 6320 wholeWordsOnly: false, 6321 wrap: false, 6322 direction: "forward", 6323 wordOptions: null, 6324 characterOptions: null 6325 }; 6326 6327 var defaultMoveOptions = { 6328 wordOptions: null, 6329 characterOptions: null 6330 }; 6331 6332 var defaultExpandOptions = { 6333 wordOptions: null, 6334 characterOptions: null, 6335 trim: false, 6336 trimStart: true, 6337 trimEnd: true 6338 }; 6339 6340 var defaultWordIteratorOptions = { 6341 wordOptions: null, 6342 characterOptions: null, 6343 direction: "forward" 6344 }; 6345 6346 function createWordOptions(options) { 6347 var lang, defaults; 6348 if (!options) { 6349 return defaultWordOptions[defaultLanguage]; 6350 } else { 6351 lang = options.language || defaultLanguage; 6352 defaults = {}; 6353 extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]); 6354 extend(defaults, options); 6355 return defaults; 6356 } 6357 } 6358 6359 function createNestedOptions(optionsParam, defaults) { 6360 var options = createOptions(optionsParam, defaults); 6361 if (defaults.hasOwnProperty("wordOptions")) { 6362 options.wordOptions = createWordOptions(options.wordOptions); 6363 } 6364 if (defaults.hasOwnProperty("characterOptions")) { 6365 options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions); 6366 } 6367 return options; 6368 } 6369 6370 /*----------------------------------------------------------------------------------------------------------------*/ 6371 6372 /* DOM utility functions */ 6373 var getComputedStyleProperty = dom.getComputedStyleProperty; 6374 6375 // Create cachable versions of DOM functions 6376 6377 // Test for old IE's incorrect display properties 6378 var tableCssDisplayBlock; 6379 (function() { 6380 var table = document.createElement("table"); 6381 var body = getBody(document); 6382 body.appendChild(table); 6383 tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block"); 6384 body.removeChild(table); 6385 })(); 6386 6387 var defaultDisplayValueForTag = { 6388 table: "table", 6389 caption: "table-caption", 6390 colgroup: "table-column-group", 6391 col: "table-column", 6392 thead: "table-header-group", 6393 tbody: "table-row-group", 6394 tfoot: "table-footer-group", 6395 tr: "table-row", 6396 td: "table-cell", 6397 th: "table-cell" 6398 }; 6399 6400 // Corrects IE's "block" value for table-related elements 6401 function getComputedDisplay(el, win) { 6402 var display = getComputedStyleProperty(el, "display", win); 6403 var tagName = el.tagName.toLowerCase(); 6404 return (display == "block" && 6405 tableCssDisplayBlock && 6406 defaultDisplayValueForTag.hasOwnProperty(tagName)) ? 6407 defaultDisplayValueForTag[tagName] : display; 6408 } 6409 6410 function isHidden(node) { 6411 var ancestors = getAncestorsAndSelf(node); 6412 for (var i = 0, len = ancestors.length; i < len; ++i) { 6413 if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") { 6414 return true; 6415 } 6416 } 6417 6418 return false; 6419 } 6420 6421 function isVisibilityHiddenTextNode(textNode) { 6422 var el; 6423 return textNode.nodeType == 3 && 6424 (el = textNode.parentNode) && 6425 getComputedStyleProperty(el, "visibility") == "hidden"; 6426 } 6427 6428 /*----------------------------------------------------------------------------------------------------------------*/ 6429 6430 6431 // "A block node is either an Element whose "display" property does not have 6432 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a 6433 // Document, or a DocumentFragment." 6434 function isBlockNode(node) { 6435 return node && 6436 ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) || 6437 node.nodeType == 9 || node.nodeType == 11); 6438 } 6439 6440 function getLastDescendantOrSelf(node) { 6441 var lastChild = node.lastChild; 6442 return lastChild ? getLastDescendantOrSelf(lastChild) : node; 6443 } 6444 6445 function containsPositions(node) { 6446 return dom.isCharacterDataNode(node) || 6447 !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName); 6448 } 6449 6450 function getAncestors(node) { 6451 var ancestors = []; 6452 while (node.parentNode) { 6453 ancestors.unshift(node.parentNode); 6454 node = node.parentNode; 6455 } 6456 return ancestors; 6457 } 6458 6459 function getAncestorsAndSelf(node) { 6460 return getAncestors(node).concat([node]); 6461 } 6462 6463 function nextNodeDescendants(node) { 6464 while (node && !node.nextSibling) { 6465 node = node.parentNode; 6466 } 6467 if (!node) { 6468 return null; 6469 } 6470 return node.nextSibling; 6471 } 6472 6473 function nextNode(node, excludeChildren) { 6474 if (!excludeChildren && node.hasChildNodes()) { 6475 return node.firstChild; 6476 } 6477 return nextNodeDescendants(node); 6478 } 6479 6480 function previousNode(node) { 6481 var previous = node.previousSibling; 6482 if (previous) { 6483 node = previous; 6484 while (node.hasChildNodes()) { 6485 node = node.lastChild; 6486 } 6487 return node; 6488 } 6489 var parent = node.parentNode; 6490 if (parent && parent.nodeType == 1) { 6491 return parent; 6492 } 6493 return null; 6494 } 6495 6496 // Adpated from Aryeh's code. 6497 // "A whitespace node is either a Text node whose data is the empty string; or 6498 // a Text node whose data consists only of one or more tabs (0x0009), line 6499 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose 6500 // parent is an Element whose resolved value for "white-space" is "normal" or 6501 // "nowrap"; or a Text node whose data consists only of one or more tabs 6502 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose 6503 // parent is an Element whose resolved value for "white-space" is "pre-line"." 6504 function isWhitespaceNode(node) { 6505 if (!node || node.nodeType != 3) { 6506 return false; 6507 } 6508 var text = node.data; 6509 if (text === "") { 6510 return true; 6511 } 6512 var parent = node.parentNode; 6513 if (!parent || parent.nodeType != 1) { 6514 return false; 6515 } 6516 var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace"); 6517 6518 return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) || 6519 (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line"); 6520 } 6521 6522 // Adpated from Aryeh's code. 6523 // "node is a collapsed whitespace node if the following algorithm returns 6524 // true:" 6525 function isCollapsedWhitespaceNode(node) { 6526 // "If node's data is the empty string, return true." 6527 if (node.data === "") { 6528 return true; 6529 } 6530 6531 // "If node is not a whitespace node, return false." 6532 if (!isWhitespaceNode(node)) { 6533 return false; 6534 } 6535 6536 // "Let ancestor be node's parent." 6537 var ancestor = node.parentNode; 6538 6539 // "If ancestor is null, return true." 6540 if (!ancestor) { 6541 return true; 6542 } 6543 6544 // "If the "display" property of some ancestor of node has resolved value "none", return true." 6545 if (isHidden(node)) { 6546 return true; 6547 } 6548 6549 return false; 6550 } 6551 6552 function isCollapsedNode(node) { 6553 var type = node.nodeType; 6554 return type == 7 /* PROCESSING_INSTRUCTION */ || 6555 type == 8 /* COMMENT */ || 6556 isHidden(node) || 6557 /^(script|style)$/i.test(node.nodeName) || 6558 isVisibilityHiddenTextNode(node) || 6559 isCollapsedWhitespaceNode(node); 6560 } 6561 6562 function isIgnoredNode(node, win) { 6563 var type = node.nodeType; 6564 return type == 7 /* PROCESSING_INSTRUCTION */ || 6565 type == 8 /* COMMENT */ || 6566 (type == 1 && getComputedDisplay(node, win) == "none"); 6567 } 6568 6569 /*----------------------------------------------------------------------------------------------------------------*/ 6570 6571 // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down 6572 6573 function Cache() { 6574 this.store = {}; 6575 } 6576 6577 Cache.prototype = { 6578 get: function(key) { 6579 return this.store.hasOwnProperty(key) ? this.store[key] : null; 6580 }, 6581 6582 set: function(key, value) { 6583 return this.store[key] = value; 6584 } 6585 }; 6586 6587 var cachedCount = 0, uncachedCount = 0; 6588 6589 function createCachingGetter(methodName, func, objProperty) { 6590 return function(args) { 6591 var cache = this.cache; 6592 if (cache.hasOwnProperty(methodName)) { 6593 cachedCount++; 6594 return cache[methodName]; 6595 } else { 6596 uncachedCount++; 6597 var value = func.call(this, objProperty ? this[objProperty] : this, args); 6598 cache[methodName] = value; 6599 return value; 6600 } 6601 }; 6602 } 6603 6604 /*----------------------------------------------------------------------------------------------------------------*/ 6605 6606 function NodeWrapper(node, session) { 6607 this.node = node; 6608 this.session = session; 6609 this.cache = new Cache(); 6610 this.positions = new Cache(); 6611 } 6612 6613 var nodeProto = { 6614 getPosition: function(offset) { 6615 var positions = this.positions; 6616 return positions.get(offset) || positions.set(offset, new Position(this, offset)); 6617 }, 6618 6619 toString: function() { 6620 return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]"; 6621 } 6622 }; 6623 6624 NodeWrapper.prototype = nodeProto; 6625 6626 var EMPTY = "EMPTY", 6627 NON_SPACE = "NON_SPACE", 6628 UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE", 6629 COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE", 6630 TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK", 6631 TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK", 6632 TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR", 6633 PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK", 6634 TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR", 6635 INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR"; 6636 6637 extend(nodeProto, { 6638 isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"), 6639 getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"), 6640 getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"), 6641 containsPositions: createCachingGetter("containsPositions", containsPositions, "node"), 6642 isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"), 6643 isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"), 6644 getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"), 6645 isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"), 6646 isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"), 6647 next: createCachingGetter("nextPos", nextNode, "node"), 6648 previous: createCachingGetter("previous", previousNode, "node"), 6649 6650 getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) { 6651 var spaceRegex = null, collapseSpaces = false; 6652 var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace"); 6653 var preLine = (cssWhitespace == "pre-line"); 6654 if (preLine) { 6655 spaceRegex = spacesMinusLineBreaksRegex; 6656 collapseSpaces = true; 6657 } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") { 6658 spaceRegex = spacesRegex; 6659 collapseSpaces = true; 6660 } 6661 6662 return { 6663 node: textNode, 6664 text: textNode.data, 6665 spaceRegex: spaceRegex, 6666 collapseSpaces: collapseSpaces, 6667 preLine: preLine 6668 }; 6669 }, "node"), 6670 6671 hasInnerText: createCachingGetter("hasInnerText", function(el, backward) { 6672 var session = this.session; 6673 var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1); 6674 var firstPosInEl = session.getPosition(el, 0); 6675 6676 var pos = backward ? posAfterEl : firstPosInEl; 6677 var endPos = backward ? firstPosInEl : posAfterEl; 6678 6679 /* 6680 <body><p>X </p><p>Y</p></body> 6681 6682 Positions: 6683 6684 body:0:"" 6685 p:0:"" 6686 text:0:"" 6687 text:1:"X" 6688 text:2:TRAILING_SPACE_IN_BLOCK 6689 text:3:COLLAPSED_SPACE 6690 p:1:"" 6691 body:1:"\n" 6692 p:0:"" 6693 text:0:"" 6694 text:1:"Y" 6695 6696 A character is a TRAILING_SPACE_IN_BLOCK iff: 6697 6698 - There is no uncollapsed character after it within the visible containing block element 6699 6700 A character is a TRAILING_SPACE_BEFORE_BR iff: 6701 6702 - There is no uncollapsed character after it preceding a <br> element 6703 6704 An element has inner text iff 6705 6706 - It is not hidden 6707 - It contains an uncollapsed character 6708 6709 All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render. 6710 */ 6711 6712 while (pos !== endPos) { 6713 pos.prepopulateChar(); 6714 if (pos.isDefinitelyNonEmpty()) { 6715 return true; 6716 } 6717 pos = backward ? pos.previousVisible() : pos.nextVisible(); 6718 } 6719 6720 return false; 6721 }, "node"), 6722 6723 isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) { 6724 // Ensure that a block element containing a <br> is considered to have inner text 6725 var brs = el.getElementsByTagName("br"); 6726 for (var i = 0, len = brs.length; i < len; ++i) { 6727 if (!isCollapsedNode(brs[i])) { 6728 return true; 6729 } 6730 } 6731 return this.hasInnerText(); 6732 }, "node"), 6733 6734 getTrailingSpace: createCachingGetter("trailingSpace", function(el) { 6735 if (el.tagName.toLowerCase() == "br") { 6736 return ""; 6737 } else { 6738 switch (this.getComputedDisplay()) { 6739 case "inline": 6740 var child = el.lastChild; 6741 while (child) { 6742 if (!isIgnoredNode(child)) { 6743 return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : ""; 6744 } 6745 child = child.previousSibling; 6746 } 6747 break; 6748 case "inline-block": 6749 case "inline-table": 6750 case "none": 6751 case "table-column": 6752 case "table-column-group": 6753 break; 6754 case "table-cell": 6755 return "\t"; 6756 default: 6757 return this.isRenderedBlock(true) ? "\n" : ""; 6758 } 6759 } 6760 return ""; 6761 }, "node"), 6762 6763 getLeadingSpace: createCachingGetter("leadingSpace", function(el) { 6764 switch (this.getComputedDisplay()) { 6765 case "inline": 6766 case "inline-block": 6767 case "inline-table": 6768 case "none": 6769 case "table-column": 6770 case "table-column-group": 6771 case "table-cell": 6772 break; 6773 default: 6774 return this.isRenderedBlock(false) ? "\n" : ""; 6775 } 6776 return ""; 6777 }, "node") 6778 }); 6779 6780 /*----------------------------------------------------------------------------------------------------------------*/ 6781 6782 function Position(nodeWrapper, offset) { 6783 this.offset = offset; 6784 this.nodeWrapper = nodeWrapper; 6785 this.node = nodeWrapper.node; 6786 this.session = nodeWrapper.session; 6787 this.cache = new Cache(); 6788 } 6789 6790 function inspectPosition() { 6791 return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]"; 6792 } 6793 6794 var positionProto = { 6795 character: "", 6796 characterType: EMPTY, 6797 isBr: false, 6798 6799 /* 6800 This method: 6801 - Fully populates positions that have characters that can be determined independently of any other characters. 6802 - Populates most types of space positions with a provisional character. The character is finalized later. 6803 */ 6804 prepopulateChar: function() { 6805 var pos = this; 6806 if (!pos.prepopulatedChar) { 6807 var node = pos.node, offset = pos.offset; 6808 var visibleChar = "", charType = EMPTY; 6809 var finalizedChar = false; 6810 if (offset > 0) { 6811 if (node.nodeType == 3) { 6812 var text = node.data; 6813 var textChar = text.charAt(offset - 1); 6814 6815 var nodeInfo = pos.nodeWrapper.getTextNodeInfo(); 6816 var spaceRegex = nodeInfo.spaceRegex; 6817 if (nodeInfo.collapseSpaces) { 6818 if (spaceRegex.test(textChar)) { 6819 // "If the character at position is from set, append a single space (U+0020) to newdata and advance 6820 // position until the character at position is not from set." 6821 6822 // We also need to check for the case where we're in a pre-line and we have a space preceding a 6823 // line break, because such spaces are collapsed in some browsers 6824 if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) { 6825 } else if (nodeInfo.preLine && text.charAt(offset) === "\n") { 6826 visibleChar = " "; 6827 charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK; 6828 } else { 6829 visibleChar = " "; 6830 //pos.checkForFollowingLineBreak = true; 6831 charType = COLLAPSIBLE_SPACE; 6832 } 6833 } else { 6834 visibleChar = textChar; 6835 charType = NON_SPACE; 6836 finalizedChar = true; 6837 } 6838 } else { 6839 visibleChar = textChar; 6840 charType = UNCOLLAPSIBLE_SPACE; 6841 finalizedChar = true; 6842 } 6843 } else { 6844 var nodePassed = node.childNodes[offset - 1]; 6845 if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) { 6846 if (nodePassed.tagName.toLowerCase() == "br") { 6847 visibleChar = "\n"; 6848 pos.isBr = true; 6849 charType = COLLAPSIBLE_SPACE; 6850 finalizedChar = false; 6851 } else { 6852 pos.checkForTrailingSpace = true; 6853 } 6854 } 6855 6856 // Check the leading space of the next node for the case when a block element follows an inline 6857 // element or text node. In that case, there is an implied line break between the two nodes. 6858 if (!visibleChar) { 6859 var nextNode = node.childNodes[offset]; 6860 if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) { 6861 pos.checkForLeadingSpace = true; 6862 } 6863 } 6864 } 6865 } 6866 6867 pos.prepopulatedChar = true; 6868 pos.character = visibleChar; 6869 pos.characterType = charType; 6870 pos.isCharInvariant = finalizedChar; 6871 } 6872 }, 6873 6874 isDefinitelyNonEmpty: function() { 6875 var charType = this.characterType; 6876 return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE; 6877 }, 6878 6879 // Resolve leading and trailing spaces, which may involve prepopulating other positions 6880 resolveLeadingAndTrailingSpaces: function() { 6881 if (!this.prepopulatedChar) { 6882 this.prepopulateChar(); 6883 } 6884 if (this.checkForTrailingSpace) { 6885 var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace(); 6886 if (trailingSpace) { 6887 this.isTrailingSpace = true; 6888 this.character = trailingSpace; 6889 this.characterType = COLLAPSIBLE_SPACE; 6890 } 6891 this.checkForTrailingSpace = false; 6892 } 6893 if (this.checkForLeadingSpace) { 6894 var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace(); 6895 if (leadingSpace) { 6896 this.isLeadingSpace = true; 6897 this.character = leadingSpace; 6898 this.characterType = COLLAPSIBLE_SPACE; 6899 } 6900 this.checkForLeadingSpace = false; 6901 } 6902 }, 6903 6904 getPrecedingUncollapsedPosition: function(characterOptions) { 6905 var pos = this, character; 6906 while ( (pos = pos.previousVisible()) ) { 6907 character = pos.getCharacter(characterOptions); 6908 if (character !== "") { 6909 return pos; 6910 } 6911 } 6912 6913 return null; 6914 }, 6915 6916 getCharacter: function(characterOptions) { 6917 this.resolveLeadingAndTrailingSpaces(); 6918 6919 var thisChar = this.character, returnChar; 6920 6921 // Check if character is ignored 6922 var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters); 6923 var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1); 6924 6925 // Check if this position's character is invariant (i.e. not dependent on character options) and return it 6926 // if so 6927 if (this.isCharInvariant) { 6928 returnChar = isIgnoredCharacter ? "" : thisChar; 6929 return returnChar; 6930 } 6931 6932 var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_"); 6933 var cachedChar = this.cache.get(cacheKey); 6934 if (cachedChar !== null) { 6935 return cachedChar; 6936 } 6937 6938 // We need to actually get the character now 6939 var character = ""; 6940 var collapsible = (this.characterType == COLLAPSIBLE_SPACE); 6941 6942 var nextPos, previousPos; 6943 var gotPreviousPos = false; 6944 var pos = this; 6945 6946 function getPreviousPos() { 6947 if (!gotPreviousPos) { 6948 previousPos = pos.getPrecedingUncollapsedPosition(characterOptions); 6949 gotPreviousPos = true; 6950 } 6951 return previousPos; 6952 } 6953 6954 // Disallow a collapsible space that is followed by a line break or is the last character 6955 if (collapsible) { 6956 // Allow a trailing space that we've previously determined should be included 6957 if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) { 6958 character = "\n"; 6959 } 6960 // Disallow a collapsible space that follows a trailing space or line break, or is the first character, 6961 // or follows a collapsible included space 6962 else if (thisChar == " " && 6963 (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) { 6964 } 6965 // Allow a leading line break unless it follows a line break 6966 else if (thisChar == "\n" && this.isLeadingSpace) { 6967 if (getPreviousPos() && previousPos.character != "\n") { 6968 character = "\n"; 6969 } else { 6970 } 6971 } else { 6972 nextPos = this.nextUncollapsed(); 6973 if (nextPos) { 6974 if (nextPos.isBr) { 6975 this.type = TRAILING_SPACE_BEFORE_BR; 6976 } else if (nextPos.isTrailingSpace && nextPos.character == "\n") { 6977 this.type = TRAILING_SPACE_IN_BLOCK; 6978 } else if (nextPos.isLeadingSpace && nextPos.character == "\n") { 6979 this.type = TRAILING_SPACE_BEFORE_BLOCK; 6980 } 6981 6982 if (nextPos.character == "\n") { 6983 if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) { 6984 } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) { 6985 } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) { 6986 } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) { 6987 } else if (thisChar == "\n") { 6988 if (nextPos.isTrailingSpace) { 6989 if (this.isTrailingSpace) { 6990 } else if (this.isBr) { 6991 nextPos.type = TRAILING_LINE_BREAK_AFTER_BR; 6992 6993 if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") { 6994 nextPos.character = ""; 6995 } else { 6996 nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR; 6997 } 6998 } 6999 } else { 7000 character = "\n"; 7001 } 7002 } else if (thisChar == " ") { 7003 character = " "; 7004 } else { 7005 } 7006 } else { 7007 character = thisChar; 7008 } 7009 } else { 7010 } 7011 } 7012 } 7013 7014 if (ignoredChars.indexOf(character) > -1) { 7015 character = ""; 7016 } 7017 7018 7019 this.cache.set(cacheKey, character); 7020 7021 return character; 7022 }, 7023 7024 equals: function(pos) { 7025 return !!pos && this.node === pos.node && this.offset === pos.offset; 7026 }, 7027 7028 inspect: inspectPosition, 7029 7030 toString: function() { 7031 return this.character; 7032 } 7033 }; 7034 7035 Position.prototype = positionProto; 7036 7037 extend(positionProto, { 7038 next: createCachingGetter("nextPos", function(pos) { 7039 var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session; 7040 if (!node) { 7041 return null; 7042 } 7043 var nextNode, nextOffset, child; 7044 if (offset == nodeWrapper.getLength()) { 7045 // Move onto the next node 7046 nextNode = node.parentNode; 7047 nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0; 7048 } else { 7049 if (nodeWrapper.isCharacterDataNode()) { 7050 nextNode = node; 7051 nextOffset = offset + 1; 7052 } else { 7053 child = node.childNodes[offset]; 7054 // Go into the children next, if children there are 7055 if (session.getNodeWrapper(child).containsPositions()) { 7056 nextNode = child; 7057 nextOffset = 0; 7058 } else { 7059 nextNode = node; 7060 nextOffset = offset + 1; 7061 } 7062 } 7063 } 7064 7065 return nextNode ? session.getPosition(nextNode, nextOffset) : null; 7066 }), 7067 7068 previous: createCachingGetter("previous", function(pos) { 7069 var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session; 7070 var previousNode, previousOffset, child; 7071 if (offset == 0) { 7072 previousNode = node.parentNode; 7073 previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0; 7074 } else { 7075 if (nodeWrapper.isCharacterDataNode()) { 7076 previousNode = node; 7077 previousOffset = offset - 1; 7078 } else { 7079 child = node.childNodes[offset - 1]; 7080 // Go into the children next, if children there are 7081 if (session.getNodeWrapper(child).containsPositions()) { 7082 previousNode = child; 7083 previousOffset = dom.getNodeLength(child); 7084 } else { 7085 previousNode = node; 7086 previousOffset = offset - 1; 7087 } 7088 } 7089 } 7090 return previousNode ? session.getPosition(previousNode, previousOffset) : null; 7091 }), 7092 7093 /* 7094 Next and previous position moving functions that filter out 7095 7096 - Hidden (CSS visibility/display) elements 7097 - Script and style elements 7098 */ 7099 nextVisible: createCachingGetter("nextVisible", function(pos) { 7100 var next = pos.next(); 7101 if (!next) { 7102 return null; 7103 } 7104 var nodeWrapper = next.nodeWrapper, node = next.node; 7105 var newPos = next; 7106 if (nodeWrapper.isCollapsed()) { 7107 // We're skipping this node and all its descendants 7108 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1); 7109 } 7110 return newPos; 7111 }), 7112 7113 nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) { 7114 var nextPos = pos; 7115 while ( (nextPos = nextPos.nextVisible()) ) { 7116 nextPos.resolveLeadingAndTrailingSpaces(); 7117 if (nextPos.character !== "") { 7118 return nextPos; 7119 } 7120 } 7121 return null; 7122 }), 7123 7124 previousVisible: createCachingGetter("previousVisible", function(pos) { 7125 var previous = pos.previous(); 7126 if (!previous) { 7127 return null; 7128 } 7129 var nodeWrapper = previous.nodeWrapper, node = previous.node; 7130 var newPos = previous; 7131 if (nodeWrapper.isCollapsed()) { 7132 // We're skipping this node and all its descendants 7133 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex()); 7134 } 7135 return newPos; 7136 }) 7137 }); 7138 7139 /*----------------------------------------------------------------------------------------------------------------*/ 7140 7141 var currentSession = null; 7142 7143 var Session = (function() { 7144 function createWrapperCache(nodeProperty) { 7145 var cache = new Cache(); 7146 7147 return { 7148 get: function(node) { 7149 var wrappersByProperty = cache.get(node[nodeProperty]); 7150 if (wrappersByProperty) { 7151 for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) { 7152 if (wrapper.node === node) { 7153 return wrapper; 7154 } 7155 } 7156 } 7157 return null; 7158 }, 7159 7160 set: function(nodeWrapper) { 7161 var property = nodeWrapper.node[nodeProperty]; 7162 var wrappersByProperty = cache.get(property) || cache.set(property, []); 7163 wrappersByProperty.push(nodeWrapper); 7164 } 7165 }; 7166 } 7167 7168 var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID"); 7169 7170 function Session() { 7171 this.initCaches(); 7172 } 7173 7174 Session.prototype = { 7175 initCaches: function() { 7176 this.elementCache = uniqueIDSupported ? (function() { 7177 var elementsCache = new Cache(); 7178 7179 return { 7180 get: function(el) { 7181 return elementsCache.get(el.uniqueID); 7182 }, 7183 7184 set: function(elWrapper) { 7185 elementsCache.set(elWrapper.node.uniqueID, elWrapper); 7186 } 7187 }; 7188 })() : createWrapperCache("tagName"); 7189 7190 // Store text nodes keyed by data, although we may need to truncate this 7191 this.textNodeCache = createWrapperCache("data"); 7192 this.otherNodeCache = createWrapperCache("nodeName"); 7193 }, 7194 7195 getNodeWrapper: function(node) { 7196 var wrapperCache; 7197 switch (node.nodeType) { 7198 case 1: 7199 wrapperCache = this.elementCache; 7200 break; 7201 case 3: 7202 wrapperCache = this.textNodeCache; 7203 break; 7204 default: 7205 wrapperCache = this.otherNodeCache; 7206 break; 7207 } 7208 7209 var wrapper = wrapperCache.get(node); 7210 if (!wrapper) { 7211 wrapper = new NodeWrapper(node, this); 7212 wrapperCache.set(wrapper); 7213 } 7214 return wrapper; 7215 }, 7216 7217 getPosition: function(node, offset) { 7218 return this.getNodeWrapper(node).getPosition(offset); 7219 }, 7220 7221 getRangeBoundaryPosition: function(range, isStart) { 7222 var prefix = isStart ? "start" : "end"; 7223 return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]); 7224 }, 7225 7226 detach: function() { 7227 this.elementCache = this.textNodeCache = this.otherNodeCache = null; 7228 } 7229 }; 7230 7231 return Session; 7232 })(); 7233 7234 /*----------------------------------------------------------------------------------------------------------------*/ 7235 7236 function startSession() { 7237 endSession(); 7238 return (currentSession = new Session()); 7239 } 7240 7241 function getSession() { 7242 return currentSession || startSession(); 7243 } 7244 7245 function endSession() { 7246 if (currentSession) { 7247 currentSession.detach(); 7248 } 7249 currentSession = null; 7250 } 7251 7252 /*----------------------------------------------------------------------------------------------------------------*/ 7253 7254 // Extensions to the rangy.dom utility object 7255 7256 extend(dom, { 7257 nextNode: nextNode, 7258 previousNode: previousNode 7259 }); 7260 7261 /*----------------------------------------------------------------------------------------------------------------*/ 7262 7263 function createCharacterIterator(startPos, backward, endPos, characterOptions) { 7264 7265 // Adjust the end position to ensure that it is actually reached 7266 if (endPos) { 7267 if (backward) { 7268 if (isCollapsedNode(endPos.node)) { 7269 endPos = startPos.previousVisible(); 7270 } 7271 } else { 7272 if (isCollapsedNode(endPos.node)) { 7273 endPos = endPos.nextVisible(); 7274 } 7275 } 7276 } 7277 7278 var pos = startPos, finished = false; 7279 7280 function next() { 7281 var charPos = null; 7282 if (backward) { 7283 charPos = pos; 7284 if (!finished) { 7285 pos = pos.previousVisible(); 7286 finished = !pos || (endPos && pos.equals(endPos)); 7287 } 7288 } else { 7289 if (!finished) { 7290 charPos = pos = pos.nextVisible(); 7291 finished = !pos || (endPos && pos.equals(endPos)); 7292 } 7293 } 7294 if (finished) { 7295 pos = null; 7296 } 7297 return charPos; 7298 } 7299 7300 var previousTextPos, returnPreviousTextPos = false; 7301 7302 return { 7303 next: function() { 7304 if (returnPreviousTextPos) { 7305 returnPreviousTextPos = false; 7306 return previousTextPos; 7307 } else { 7308 var pos, character; 7309 while ( (pos = next()) ) { 7310 character = pos.getCharacter(characterOptions); 7311 if (character) { 7312 previousTextPos = pos; 7313 return pos; 7314 } 7315 } 7316 return null; 7317 } 7318 }, 7319 7320 rewind: function() { 7321 if (previousTextPos) { 7322 returnPreviousTextPos = true; 7323 } else { 7324 throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound."); 7325 } 7326 }, 7327 7328 dispose: function() { 7329 startPos = endPos = null; 7330 } 7331 }; 7332 } 7333 7334 var arrayIndexOf = Array.prototype.indexOf ? 7335 function(arr, val) { 7336 return arr.indexOf(val); 7337 } : 7338 function(arr, val) { 7339 for (var i = 0, len = arr.length; i < len; ++i) { 7340 if (arr[i] === val) { 7341 return i; 7342 } 7343 } 7344 return -1; 7345 }; 7346 7347 // Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next() 7348 // is called and there is no more tokenized text 7349 function createTokenizedTextProvider(pos, characterOptions, wordOptions) { 7350 var forwardIterator = createCharacterIterator(pos, false, null, characterOptions); 7351 var backwardIterator = createCharacterIterator(pos, true, null, characterOptions); 7352 var tokenizer = wordOptions.tokenizer; 7353 7354 // Consumes a word and the whitespace beyond it 7355 function consumeWord(forward) { 7356 var pos, textChar; 7357 var newChars = [], it = forward ? forwardIterator : backwardIterator; 7358 7359 var passedWordBoundary = false, insideWord = false; 7360 7361 while ( (pos = it.next()) ) { 7362 textChar = pos.character; 7363 7364 7365 if (allWhiteSpaceRegex.test(textChar)) { 7366 if (insideWord) { 7367 insideWord = false; 7368 passedWordBoundary = true; 7369 } 7370 } else { 7371 if (passedWordBoundary) { 7372 it.rewind(); 7373 break; 7374 } else { 7375 insideWord = true; 7376 } 7377 } 7378 newChars.push(pos); 7379 } 7380 7381 7382 return newChars; 7383 } 7384 7385 // Get initial word surrounding initial position and tokenize it 7386 var forwardChars = consumeWord(true); 7387 var backwardChars = consumeWord(false).reverse(); 7388 var tokens = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer); 7389 7390 // Create initial token buffers 7391 var forwardTokensBuffer = forwardChars.length ? 7392 tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : []; 7393 7394 var backwardTokensBuffer = backwardChars.length ? 7395 tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : []; 7396 7397 function inspectBuffer(buffer) { 7398 var textPositions = ["[" + buffer.length + "]"]; 7399 for (var i = 0; i < buffer.length; ++i) { 7400 textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")"); 7401 } 7402 return textPositions; 7403 } 7404 7405 7406 return { 7407 nextEndToken: function() { 7408 var lastToken, forwardChars; 7409 7410 // If we're down to the last token, consume character chunks until we have a word or run out of 7411 // characters to consume 7412 while ( forwardTokensBuffer.length == 1 && 7413 !(lastToken = forwardTokensBuffer[0]).isWord && 7414 (forwardChars = consumeWord(true)).length > 0) { 7415 7416 // Merge trailing non-word into next word and tokenize 7417 forwardTokensBuffer = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer); 7418 } 7419 7420 return forwardTokensBuffer.shift(); 7421 }, 7422 7423 previousStartToken: function() { 7424 var lastToken, backwardChars; 7425 7426 // If we're down to the last token, consume character chunks until we have a word or run out of 7427 // characters to consume 7428 while ( backwardTokensBuffer.length == 1 && 7429 !(lastToken = backwardTokensBuffer[0]).isWord && 7430 (backwardChars = consumeWord(false)).length > 0) { 7431 7432 // Merge leading non-word into next word and tokenize 7433 backwardTokensBuffer = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer); 7434 } 7435 7436 return backwardTokensBuffer.pop(); 7437 }, 7438 7439 dispose: function() { 7440 forwardIterator.dispose(); 7441 backwardIterator.dispose(); 7442 forwardTokensBuffer = backwardTokensBuffer = null; 7443 } 7444 }; 7445 } 7446 7447 function movePositionBy(pos, unit, count, characterOptions, wordOptions) { 7448 var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token; 7449 if (count !== 0) { 7450 var backward = (count < 0); 7451 7452 switch (unit) { 7453 case CHARACTER: 7454 charIterator = createCharacterIterator(pos, backward, null, characterOptions); 7455 while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) { 7456 ++unitsMoved; 7457 newPos = currentPos; 7458 } 7459 nextPos = currentPos; 7460 charIterator.dispose(); 7461 break; 7462 case WORD: 7463 var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions); 7464 var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken; 7465 7466 while ( (token = next()) && unitsMoved < absCount ) { 7467 if (token.isWord) { 7468 ++unitsMoved; 7469 newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1]; 7470 } 7471 } 7472 break; 7473 default: 7474 throw new Error("movePositionBy: unit '" + unit + "' not implemented"); 7475 } 7476 7477 // Perform any necessary position tweaks 7478 if (backward) { 7479 newPos = newPos.previousVisible(); 7480 unitsMoved = -unitsMoved; 7481 } else if (newPos && newPos.isLeadingSpace && !newPos.isTrailingSpace) { 7482 // Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space 7483 // before a block element (for example, the line break between "1" and "2" in the following HTML: 7484 // "1<p>2</p>") is considered to be attached to the position immediately before the block element, which 7485 // corresponds with a different selection position in most browsers from the one we want (i.e. at the 7486 // start of the contents of the block element). We get round this by advancing the position returned to 7487 // the last possible equivalent visible position. 7488 if (unit == WORD) { 7489 charIterator = createCharacterIterator(pos, false, null, characterOptions); 7490 nextPos = charIterator.next(); 7491 charIterator.dispose(); 7492 } 7493 if (nextPos) { 7494 newPos = nextPos.previousVisible(); 7495 } 7496 } 7497 } 7498 7499 7500 return { 7501 position: newPos, 7502 unitsMoved: unitsMoved 7503 }; 7504 } 7505 7506 function createRangeCharacterIterator(session, range, characterOptions, backward) { 7507 var rangeStart = session.getRangeBoundaryPosition(range, true); 7508 var rangeEnd = session.getRangeBoundaryPosition(range, false); 7509 var itStart = backward ? rangeEnd : rangeStart; 7510 var itEnd = backward ? rangeStart : rangeEnd; 7511 7512 return createCharacterIterator(itStart, !!backward, itEnd, characterOptions); 7513 } 7514 7515 function getRangeCharacters(session, range, characterOptions) { 7516 7517 var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos; 7518 while ( (pos = it.next()) ) { 7519 chars.push(pos); 7520 } 7521 7522 it.dispose(); 7523 return chars; 7524 } 7525 7526 function isWholeWord(startPos, endPos, wordOptions) { 7527 var range = api.createRange(startPos.node); 7528 range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset); 7529 return !range.expand("word", { wordOptions: wordOptions }); 7530 } 7531 7532 function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) { 7533 var backward = isDirectionBackward(findOptions.direction); 7534 var it = createCharacterIterator( 7535 initialPos, 7536 backward, 7537 initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward), 7538 findOptions.characterOptions 7539 ); 7540 var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex; 7541 var result, insideRegexMatch; 7542 var returnValue = null; 7543 7544 function handleMatch(startIndex, endIndex) { 7545 var startPos = chars[startIndex].previousVisible(); 7546 var endPos = chars[endIndex - 1]; 7547 var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions)); 7548 7549 return { 7550 startPos: startPos, 7551 endPos: endPos, 7552 valid: valid 7553 }; 7554 } 7555 7556 while ( (pos = it.next()) ) { 7557 currentChar = pos.character; 7558 if (!isRegex && !findOptions.caseSensitive) { 7559 currentChar = currentChar.toLowerCase(); 7560 } 7561 7562 if (backward) { 7563 chars.unshift(pos); 7564 text = currentChar + text; 7565 } else { 7566 chars.push(pos); 7567 text += currentChar; 7568 } 7569 7570 if (isRegex) { 7571 result = searchTerm.exec(text); 7572 if (result) { 7573 matchStartIndex = result.index; 7574 matchEndIndex = matchStartIndex + result[0].length; 7575 if (insideRegexMatch) { 7576 // Check whether the match is now over 7577 if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) { 7578 returnValue = handleMatch(matchStartIndex, matchEndIndex); 7579 break; 7580 } 7581 } else { 7582 insideRegexMatch = true; 7583 } 7584 } 7585 } else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) { 7586 returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length); 7587 break; 7588 } 7589 } 7590 7591 // Check whether regex match extends to the end of the range 7592 if (insideRegexMatch) { 7593 returnValue = handleMatch(matchStartIndex, matchEndIndex); 7594 } 7595 it.dispose(); 7596 7597 return returnValue; 7598 } 7599 7600 function createEntryPointFunction(func) { 7601 return function() { 7602 var sessionRunning = !!currentSession; 7603 var session = getSession(); 7604 var args = [session].concat( util.toArray(arguments) ); 7605 var returnValue = func.apply(this, args); 7606 if (!sessionRunning) { 7607 endSession(); 7608 } 7609 return returnValue; 7610 }; 7611 } 7612 7613 /*----------------------------------------------------------------------------------------------------------------*/ 7614 7615 // Extensions to the Rangy Range object 7616 7617 function createRangeBoundaryMover(isStart, collapse) { 7618 /* 7619 Unit can be "character" or "word" 7620 Options: 7621 7622 - includeTrailingSpace 7623 - wordRegex 7624 - tokenizer 7625 - collapseSpaceBeforeLineBreak 7626 */ 7627 return createEntryPointFunction( 7628 function(session, unit, count, moveOptions) { 7629 if (typeof count == UNDEF) { 7630 count = unit; 7631 unit = CHARACTER; 7632 } 7633 moveOptions = createNestedOptions(moveOptions, defaultMoveOptions); 7634 7635 var boundaryIsStart = isStart; 7636 if (collapse) { 7637 boundaryIsStart = (count >= 0); 7638 this.collapse(!boundaryIsStart); 7639 } 7640 var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.wordOptions); 7641 var newPos = moveResult.position; 7642 this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset); 7643 return moveResult.unitsMoved; 7644 } 7645 ); 7646 } 7647 7648 function createRangeTrimmer(isStart) { 7649 return createEntryPointFunction( 7650 function(session, characterOptions) { 7651 characterOptions = createOptions(characterOptions, defaultCharacterOptions); 7652 var pos; 7653 var it = createRangeCharacterIterator(session, this, characterOptions, !isStart); 7654 var trimCharCount = 0; 7655 while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) { 7656 ++trimCharCount; 7657 } 7658 it.dispose(); 7659 var trimmed = (trimCharCount > 0); 7660 if (trimmed) { 7661 this[isStart ? "moveStart" : "moveEnd"]( 7662 "character", 7663 isStart ? trimCharCount : -trimCharCount, 7664 { characterOptions: characterOptions } 7665 ); 7666 } 7667 return trimmed; 7668 } 7669 ); 7670 } 7671 7672 extend(api.rangePrototype, { 7673 moveStart: createRangeBoundaryMover(true, false), 7674 7675 moveEnd: createRangeBoundaryMover(false, false), 7676 7677 move: createRangeBoundaryMover(true, true), 7678 7679 trimStart: createRangeTrimmer(true), 7680 7681 trimEnd: createRangeTrimmer(false), 7682 7683 trim: createEntryPointFunction( 7684 function(session, characterOptions) { 7685 var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions); 7686 return startTrimmed || endTrimmed; 7687 } 7688 ), 7689 7690 expand: createEntryPointFunction( 7691 function(session, unit, expandOptions) { 7692 var moved = false; 7693 expandOptions = createNestedOptions(expandOptions, defaultExpandOptions); 7694 var characterOptions = expandOptions.characterOptions; 7695 if (!unit) { 7696 unit = CHARACTER; 7697 } 7698 if (unit == WORD) { 7699 var wordOptions = expandOptions.wordOptions; 7700 var startPos = session.getRangeBoundaryPosition(this, true); 7701 var endPos = session.getRangeBoundaryPosition(this, false); 7702 7703 var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions); 7704 var startToken = startTokenizedTextProvider.nextEndToken(); 7705 var newStartPos = startToken.chars[0].previousVisible(); 7706 var endToken, newEndPos; 7707 7708 if (this.collapsed) { 7709 endToken = startToken; 7710 } else { 7711 var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions); 7712 endToken = endTokenizedTextProvider.previousStartToken(); 7713 } 7714 newEndPos = endToken.chars[endToken.chars.length - 1]; 7715 7716 if (!newStartPos.equals(startPos)) { 7717 this.setStart(newStartPos.node, newStartPos.offset); 7718 moved = true; 7719 } 7720 if (newEndPos && !newEndPos.equals(endPos)) { 7721 this.setEnd(newEndPos.node, newEndPos.offset); 7722 moved = true; 7723 } 7724 7725 if (expandOptions.trim) { 7726 if (expandOptions.trimStart) { 7727 moved = this.trimStart(characterOptions) || moved; 7728 } 7729 if (expandOptions.trimEnd) { 7730 moved = this.trimEnd(characterOptions) || moved; 7731 } 7732 } 7733 7734 return moved; 7735 } else { 7736 return this.moveEnd(CHARACTER, 1, expandOptions); 7737 } 7738 } 7739 ), 7740 7741 text: createEntryPointFunction( 7742 function(session, characterOptions) { 7743 return this.collapsed ? 7744 "" : getRangeCharacters(session, this, createOptions(characterOptions, defaultCharacterOptions)).join(""); 7745 } 7746 ), 7747 7748 selectCharacters: createEntryPointFunction( 7749 function(session, containerNode, startIndex, endIndex, characterOptions) { 7750 var moveOptions = { characterOptions: characterOptions }; 7751 if (!containerNode) { 7752 containerNode = getBody( this.getDocument() ); 7753 } 7754 this.selectNodeContents(containerNode); 7755 this.collapse(true); 7756 this.moveStart("character", startIndex, moveOptions); 7757 this.collapse(true); 7758 this.moveEnd("character", endIndex - startIndex, moveOptions); 7759 } 7760 ), 7761 7762 // Character indexes are relative to the start of node 7763 toCharacterRange: createEntryPointFunction( 7764 function(session, containerNode, characterOptions) { 7765 if (!containerNode) { 7766 containerNode = getBody( this.getDocument() ); 7767 } 7768 var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode); 7769 var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1); 7770 var rangeBetween = this.cloneRange(); 7771 var startIndex, endIndex; 7772 if (rangeStartsBeforeNode) { 7773 rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex); 7774 startIndex = -rangeBetween.text(characterOptions).length; 7775 } else { 7776 rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset); 7777 startIndex = rangeBetween.text(characterOptions).length; 7778 } 7779 endIndex = startIndex + this.text(characterOptions).length; 7780 7781 return { 7782 start: startIndex, 7783 end: endIndex 7784 }; 7785 } 7786 ), 7787 7788 findText: createEntryPointFunction( 7789 function(session, searchTermParam, findOptions) { 7790 // Set up options 7791 findOptions = createNestedOptions(findOptions, defaultFindOptions); 7792 7793 // Create word options if we're matching whole words only 7794 if (findOptions.wholeWordsOnly) { 7795 // We don't ever want trailing spaces for search results 7796 findOptions.wordOptions.includeTrailingSpace = false; 7797 } 7798 7799 var backward = isDirectionBackward(findOptions.direction); 7800 7801 // Create a range representing the search scope if none was provided 7802 var searchScopeRange = findOptions.withinRange; 7803 if (!searchScopeRange) { 7804 searchScopeRange = api.createRange(); 7805 searchScopeRange.selectNodeContents(this.getDocument()); 7806 } 7807 7808 // Examine and prepare the search term 7809 var searchTerm = searchTermParam, isRegex = false; 7810 if (typeof searchTerm == "string") { 7811 if (!findOptions.caseSensitive) { 7812 searchTerm = searchTerm.toLowerCase(); 7813 } 7814 } else { 7815 isRegex = true; 7816 } 7817 7818 var initialPos = session.getRangeBoundaryPosition(this, !backward); 7819 7820 // Adjust initial position if it lies outside the search scope 7821 var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset); 7822 7823 if (comparison === -1) { 7824 initialPos = session.getRangeBoundaryPosition(searchScopeRange, true); 7825 } else if (comparison === 1) { 7826 initialPos = session.getRangeBoundaryPosition(searchScopeRange, false); 7827 } 7828 7829 var pos = initialPos; 7830 var wrappedAround = false; 7831 7832 // Try to find a match and ignore invalid ones 7833 var findResult; 7834 while (true) { 7835 findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions); 7836 7837 if (findResult) { 7838 if (findResult.valid) { 7839 this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset); 7840 return true; 7841 } else { 7842 // We've found a match that is not a whole word, so we carry on searching from the point immediately 7843 // after the match 7844 pos = backward ? findResult.startPos : findResult.endPos; 7845 } 7846 } else if (findOptions.wrap && !wrappedAround) { 7847 // No result found but we're wrapping around and limiting the scope to the unsearched part of the range 7848 searchScopeRange = searchScopeRange.cloneRange(); 7849 pos = session.getRangeBoundaryPosition(searchScopeRange, !backward); 7850 searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward); 7851 wrappedAround = true; 7852 } else { 7853 // Nothing found and we can't wrap around, so we're done 7854 return false; 7855 } 7856 } 7857 } 7858 ), 7859 7860 pasteHtml: function(html) { 7861 this.deleteContents(); 7862 if (html) { 7863 var frag = this.createContextualFragment(html); 7864 var lastChild = frag.lastChild; 7865 this.insertNode(frag); 7866 this.collapseAfter(lastChild); 7867 } 7868 } 7869 }); 7870 7871 /*----------------------------------------------------------------------------------------------------------------*/ 7872 7873 // Extensions to the Rangy Selection object 7874 7875 function createSelectionTrimmer(methodName) { 7876 return createEntryPointFunction( 7877 function(session, characterOptions) { 7878 var trimmed = false; 7879 this.changeEachRange(function(range) { 7880 trimmed = range[methodName](characterOptions) || trimmed; 7881 }); 7882 return trimmed; 7883 } 7884 ); 7885 } 7886 7887 extend(api.selectionPrototype, { 7888 expand: createEntryPointFunction( 7889 function(session, unit, expandOptions) { 7890 this.changeEachRange(function(range) { 7891 range.expand(unit, expandOptions); 7892 }); 7893 } 7894 ), 7895 7896 move: createEntryPointFunction( 7897 function(session, unit, count, options) { 7898 var unitsMoved = 0; 7899 if (this.focusNode) { 7900 this.collapse(this.focusNode, this.focusOffset); 7901 var range = this.getRangeAt(0); 7902 if (!options) { 7903 options = {}; 7904 } 7905 options.characterOptions = createOptions(options.characterOptions, defaultCaretCharacterOptions); 7906 unitsMoved = range.move(unit, count, options); 7907 this.setSingleRange(range); 7908 } 7909 return unitsMoved; 7910 } 7911 ), 7912 7913 trimStart: createSelectionTrimmer("trimStart"), 7914 trimEnd: createSelectionTrimmer("trimEnd"), 7915 trim: createSelectionTrimmer("trim"), 7916 7917 selectCharacters: createEntryPointFunction( 7918 function(session, containerNode, startIndex, endIndex, direction, characterOptions) { 7919 var range = api.createRange(containerNode); 7920 range.selectCharacters(containerNode, startIndex, endIndex, characterOptions); 7921 this.setSingleRange(range, direction); 7922 } 7923 ), 7924 7925 saveCharacterRanges: createEntryPointFunction( 7926 function(session, containerNode, characterOptions) { 7927 var ranges = this.getAllRanges(), rangeCount = ranges.length; 7928 var rangeInfos = []; 7929 7930 var backward = rangeCount == 1 && this.isBackward(); 7931 7932 for (var i = 0, len = ranges.length; i < len; ++i) { 7933 rangeInfos[i] = { 7934 characterRange: ranges[i].toCharacterRange(containerNode, characterOptions), 7935 backward: backward, 7936 characterOptions: characterOptions 7937 }; 7938 } 7939 7940 return rangeInfos; 7941 } 7942 ), 7943 7944 restoreCharacterRanges: createEntryPointFunction( 7945 function(session, containerNode, saved) { 7946 this.removeAllRanges(); 7947 for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) { 7948 rangeInfo = saved[i]; 7949 characterRange = rangeInfo.characterRange; 7950 range = api.createRange(containerNode); 7951 range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions); 7952 this.addRange(range, rangeInfo.backward); 7953 } 7954 } 7955 ), 7956 7957 text: createEntryPointFunction( 7958 function(session, characterOptions) { 7959 var rangeTexts = []; 7960 for (var i = 0, len = this.rangeCount; i < len; ++i) { 7961 rangeTexts[i] = this.getRangeAt(i).text(characterOptions); 7962 } 7963 return rangeTexts.join(""); 7964 } 7965 ) 7966 }); 7967 7968 /*----------------------------------------------------------------------------------------------------------------*/ 7969 7970 // Extensions to the core rangy object 7971 7972 api.innerText = function(el, characterOptions) { 7973 var range = api.createRange(el); 7974 range.selectNodeContents(el); 7975 var text = range.text(characterOptions); 7976 return text; 7977 }; 7978 7979 api.createWordIterator = function(startNode, startOffset, iteratorOptions) { 7980 var session = getSession(); 7981 iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions); 7982 var startPos = session.getPosition(startNode, startOffset); 7983 var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.wordOptions); 7984 var backward = isDirectionBackward(iteratorOptions.direction); 7985 7986 return { 7987 next: function() { 7988 return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken(); 7989 }, 7990 7991 dispose: function() { 7992 tokenizedTextProvider.dispose(); 7993 this.next = function() {}; 7994 } 7995 }; 7996 }; 7997 7998 /*----------------------------------------------------------------------------------------------------------------*/ 7999 8000 api.noMutation = function(func) { 8001 var session = getSession(); 8002 func(session); 8003 endSession(); 8004 }; 8005 8006 api.noMutation.createEntryPointFunction = createEntryPointFunction; 8007 8008 api.textRange = { 8009 isBlockNode: isBlockNode, 8010 isCollapsedWhitespaceNode: isCollapsedWhitespaceNode, 8011 8012 createPosition: createEntryPointFunction( 8013 function(session, node, offset) { 8014 return session.getPosition(node, offset); 8015 } 8016 ) 8017 }; 8018 }); 8019 8020 return rangy; 8021 }, this); 8022 YUI.add('moodle-editor_atto-rangy', function (Y, NAME) { 8023 8024 // This file is part of Moodle - http://moodle.org/ 8025 // 8026 // Moodle is free software: you can redistribute it and/or modify 8027 // it under the terms of the GNU General Public License as published by 8028 // the Free Software Foundation, either version 3 of the License, or 8029 // (at your option) any later version. 8030 // 8031 // Moodle is distributed in the hope that it will be useful, 8032 // but WITHOUT ANY WARRANTY; without even the implied warranty of 8033 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 8034 // GNU General Public License for more details. 8035 // 8036 // You should have received a copy of the GNU General Public License 8037 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 8038 8039 if (!rangy.initialized) { 8040 rangy.init(); 8041 } 8042 8043 8044 }, '@VERSION@', {"requires": []});
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 |