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.1-dev 8 * Build date: 20 May 2015 9 */ 10 11 (function(factory, root) { 12 if (typeof define == "function" && define.amd) { 13 // AMD. Register as an anonymous module. 14 define(factory); 15 } else if (typeof module != "undefined" && typeof exports == "object") { 16 // Node/CommonJS style 17 module.exports = factory(); 18 } else { 19 // No AMD or CommonJS support so we place Rangy in (probably) the global variable 20 root.rangy = factory(); 21 } 22})(function() { 23 24 var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; 25 26 // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START 27 // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113. 28 var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", 29 "commonAncestorContainer"]; 30 31 // Minimal set of methods required for DOM Level 2 Range compliance 32 var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore", 33 "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
vendor: 22,205 bytes, lines 34-681
34 "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"]; 35 36 var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"]; 37 38 // Subset of TextRange's full set of methods that we're interested in 39 var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select", 40 "setEndPoint", "getBoundingClientRect"]; 41 42 /*----------------------------------------------------------------------------------------------------------------*/ 43 44 // Trio of functions taken from Peter Michaux's article: 45 // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting 46 function isHostMethod(o, p) { 47 var t = typeof o[p]; 48 return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown"; 49 } 50 51 function isHostObject(o, p) { 52 return !!(typeof o[p] == OBJECT && o[p]); 53 } 54 55 function isHostProperty(o, p) { 56 return typeof o[p] != UNDEFINED; 57 } 58 59 // Creates a convenience function to save verbose repeated calls to tests functions 60 function createMultiplePropertyTest(testFunc) { 61 return function(o, props) { 62 var i = props.length; 63 while (i--) { 64 if (!testFunc(o, props[i])) { 65 return false; 66 } 67 } 68 return true; 69 }; 70 } 71 72 // Next trio of functions are a convenience to save verbose repeated calls to previous two functions 73 var areHostMethods = createMultiplePropertyTest(isHostMethod); 74 var areHostObjects = createMultiplePropertyTest(isHostObject); 75 var areHostProperties = createMultiplePropertyTest(isHostProperty); 76 77 function isTextRange(range) { 78 return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); 79 } 80 81 function getBody(doc) { 82 return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; 83 } 84 85 var forEach = [].forEach ? 86 function(arr, func) { 87 arr.forEach(func); 88 } : 89 function(arr, func) { 90 for (var i = 0, len = arr.length; i < len; ++i) { 91 func(arr[i], i); 92 } 93 }; 94 95 var modules = {}; 96 97 var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED); 98 99 var util = { 100 isHostMethod: isHostMethod, 101 isHostObject: isHostObject, 102 isHostProperty: isHostProperty, 103 areHostMethods: areHostMethods, 104 areHostObjects: areHostObjects, 105 areHostProperties: areHostProperties, 106 isTextRange: isTextRange, 107 getBody: getBody, 108 forEach: forEach 109 }; 110 111 var api = { 112 version: "1.3.1-dev", 113 initialized: false, 114 isBrowser: isBrowser, 115 supported: true, 116 util: util, 117 features: {}, 118 modules: modules, 119 config: { 120 alertOnFail: false, 121 alertOnWarn: false, 122 preferTextRange: false, 123 autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize 124 } 125 }; 126 127 function consoleLog(msg) { 128 if (typeof console != UNDEFINED && isHostMethod(console, "log")) { 129 console.log(msg); 130 } 131 } 132 133 function alertOrLog(msg, shouldAlert) { 134 if (isBrowser && shouldAlert) { 135 alert(msg); 136 } else { 137 consoleLog(msg); 138 } 139 } 140 141 function fail(reason) { 142 api.initialized = true; 143 api.supported = false; 144 alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail); 145 } 146 147 api.fail = fail; 148 149 function warn(msg) { 150 alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn); 151 } 152 153 api.warn = warn; 154 155 // Add utility extend() method 156 var extend; 157 if ({}.hasOwnProperty) { 158 util.extend = extend = function(obj, props, deep) { 159 var o, p; 160 for (var i in props) { 161 if (props.hasOwnProperty(i)) { 162 o = obj[i]; 163 p = props[i]; 164 if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") { 165 extend(o, p, true); 166 } 167 obj[i] = p; 168 } 169 } 170 // Special case for toString, which does not show up in for...in loops in IE <= 8 171 if (props.hasOwnProperty("toString")) { 172 obj.toString = props.toString; 173 } 174 return obj; 175 }; 176 177 util.createOptions = function(optionsParam, defaults) { 178 var options = {}; 179 extend(options, defaults); 180 if (optionsParam) { 181 extend(options, optionsParam); 182 } 183 return options; 184 }; 185 } else { 186 fail("hasOwnProperty not supported"); 187 } 188 189 // Test whether we're in a browser and bail out if not 190 if (!isBrowser) { 191 fail("Rangy can only run in a browser"); 192 } 193 194 // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not 195 (function() { 196 var toArray; 197 198 if (isBrowser) { 199 var el = document.createElement("div"); 200 el.appendChild(document.createElement("span")); 201 var slice = [].slice; 202 try { 203 if (slice.call(el.childNodes, 0)[0].nodeType == 1) { 204 toArray = function(arrayLike) { 205 return slice.call(arrayLike, 0); 206 }; 207 } 208 } catch (e) {} 209 } 210 211 if (!toArray) { 212 toArray = function(arrayLike) { 213 var arr = []; 214 for (var i = 0, len = arrayLike.length; i < len; ++i) { 215 arr[i] = arrayLike[i]; 216 } 217 return arr; 218 }; 219 } 220 221 util.toArray = toArray; 222 })(); 223 224 // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or 225 // normalization of event properties 226 var addListener; 227 if (isBrowser) { 228 if (isHostMethod(document, "addEventListener")) { 229 addListener = function(obj, eventType, listener) { 230 obj.addEventListener(eventType, listener, false); 231 }; 232 } else if (isHostMethod(document, "attachEvent")) { 233 addListener = function(obj, eventType, listener) { 234 obj.attachEvent("on" + eventType, listener); 235 }; 236 } else { 237 fail("Document does not have required addEventListener or attachEvent method"); 238 } 239 240 util.addListener = addListener; 241 } 242 243 var initListeners = []; 244 245 function getErrorDesc(ex) { 246 return ex.message || ex.description || String(ex); 247 } 248 249 // Initialization 250 function init() { 251 if (!isBrowser || api.initialized) { 252 return; 253 } 254 var testRange; 255 var implementsDomRange = false, implementsTextRange = false; 256 257 // First, perform basic feature tests 258 259 if (isHostMethod(document, "createRange")) { 260 testRange = document.createRange(); 261 if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { 262 implementsDomRange = true; 263 } 264 } 265 266 var body = getBody(document); 267 if (!body || body.nodeName.toLowerCase() != "body") { 268 fail("No body element found"); 269 return; 270 } 271 272 if (body && isHostMethod(body, "createTextRange")) { 273 testRange = body.createTextRange(); 274 if (isTextRange(testRange)) { 275 implementsTextRange = true; 276 } 277 } 278 279 if (!implementsDomRange && !implementsTextRange) { 280 fail("Neither Range nor TextRange are available"); 281 return; 282 } 283 284 api.initialized = true; 285 api.features = { 286 implementsDomRange: implementsDomRange, 287 implementsTextRange: implementsTextRange 288 }; 289 290 // Initialize modules 291 var module, errorMessage; 292 for (var moduleName in modules) { 293 if ( (module = modules[moduleName]) instanceof Module ) { 294 module.init(module, api); 295 } 296 } 297 298 // Call init listeners 299 for (var i = 0, len = initListeners.length; i < len; ++i) { 300 try { 301 initListeners[i](api); 302 } catch (ex) { 303 errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex); 304 consoleLog(errorMessage); 305 } 306 } 307 } 308 309 function deprecationNotice(deprecated, replacement, module) { 310 if (module) { 311 deprecated += " in module " + module.name; 312 } 313 api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " + 314 replacement + " instead."); 315 } 316 317 function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) { 318 owner[deprecated] = function() { 319 deprecationNotice(deprecated, replacement, module); 320 return owner[replacement].apply(owner, util.toArray(arguments)); 321 }; 322 } 323 324 util.deprecationNotice = deprecationNotice; 325 util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod; 326 327 // Allow external scripts to initialize this library in case it's loaded after the document has loaded 328 api.init = init; 329 330 // Execute listener immediately if already initialized 331 api.addInitListener = function(listener) { 332 if (api.initialized) { 333 listener(api); 334 } else { 335 initListeners.push(listener); 336 } 337 }; 338 339 var shimListeners = []; 340 341 api.addShimListener = function(listener) { 342 shimListeners.push(listener); 343 }; 344 345 function shim(win) { 346 win = win || window; 347 init(); 348 349 // Notify listeners 350 for (var i = 0, len = shimListeners.length; i < len; ++i) { 351 shimListeners[i](win); 352 } 353 } 354 355 if (isBrowser) { 356 api.shim = api.createMissingNativeApi = shim; 357 createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim"); 358 } 359 360 function Module(name, dependencies, initializer) { 361 this.name = name; 362 this.dependencies = dependencies; 363 this.initialized = false; 364 this.supported = false; 365 this.initializer = initializer; 366 } 367 368 Module.prototype = { 369 init: function() { 370 var requiredModuleNames = this.dependencies || []; 371 for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) { 372 moduleName = requiredModuleNames[i]; 373 374 requiredModule = modules[moduleName]; 375 if (!requiredModule || !(requiredModule instanceof Module)) { 376 throw new Error("required module '" + moduleName + "' not found"); 377 } 378 379 requiredModule.init(); 380 381 if (!requiredModule.supported) { 382 throw new Error("required module '" + moduleName + "' not supported"); 383 } 384 } 385 386 // Now run initializer 387 this.initializer(this); 388 }, 389 390 fail: function(reason) { 391 this.initialized = true; 392 this.supported = false; 393 throw new Error(reason); 394 }, 395 396 warn: function(msg) { 397 api.warn("Module " + this.name + ": " + msg); 398 }, 399 400 deprecationNotice: function(deprecated, replacement) { 401 api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " + 402 replacement + " instead"); 403 }, 404 405 createError: function(msg) { 406 return new Error("Error in Rangy " + this.name + " module: " + msg); 407 } 408 }; 409 410 function createModule(name, dependencies, initFunc) { 411 var newModule = new Module(name, dependencies, function(module) { 412 if (!module.initialized) { 413 module.initialized = true; 414 try { 415 initFunc(api, module); 416 module.supported = true; 417 } catch (ex) { 418 var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex); 419 consoleLog(errorMessage); 420 if (ex.stack) { 421 consoleLog(ex.stack); 422 } 423 } 424 } 425 }); 426 modules[name] = newModule; 427 return newModule; 428 } 429 430 api.createModule = function(name) { 431 // Allow 2 or 3 arguments (second argument is an optional array of dependencies) 432 var initFunc, dependencies; 433 if (arguments.length == 2) { 434 initFunc = arguments[1]; 435 dependencies = []; 436 } else { 437 initFunc = arguments[2]; 438 dependencies = arguments[1]; 439 } 440 441 var module = createModule(name, dependencies, initFunc); 442 443 // Initialize the module immediately if the core is already initialized 444 if (api.initialized && api.supported) { 445 module.init(); 446 } 447 }; 448 449 api.createCoreModule = function(name, dependencies, initFunc) { 450 createModule(name, dependencies, initFunc); 451 }; 452 453 /*----------------------------------------------------------------------------------------------------------------*/ 454 455 // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately 456 457 function RangePrototype() {} 458 api.RangePrototype = RangePrototype; 459 api.rangePrototype = new RangePrototype(); 460 461 function SelectionPrototype() {} 462 api.selectionPrototype = new SelectionPrototype(); 463 464 /*----------------------------------------------------------------------------------------------------------------*/ 465 466 // DOM utility methods used by Rangy 467 api.createCoreModule("DomUtil", [], function(api, module) { 468 var UNDEF = "undefined"; 469 var util = api.util; 470 var getBody = util.getBody; 471 472 // Perform feature tests 473 if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) { 474 module.fail("document missing a Node creation method"); 475 } 476 477 if (!util.isHostMethod(document, "getElementsByTagName")) { 478 module.fail("document missing getElementsByTagName method"); 479 } 480 481 var el = document.createElement("div"); 482 if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || 483 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { 484 module.fail("Incomplete Element implementation"); 485 } 486 487 // innerHTML is required for Range's createContextualFragment method 488 if (!util.isHostProperty(el, "innerHTML")) { 489 module.fail("Element is missing innerHTML property"); 490 } 491 492 var textNode = document.createTextNode("test"); 493 if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] || 494 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) || 495 !util.areHostProperties(textNode, ["data"]))) { 496 module.fail("Incomplete Text Node implementation"); 497 } 498 499 /*----------------------------------------------------------------------------------------------------------------*/ 500 501 // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been 502 // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that 503 // contains just the document as a single element and the value searched for is the document. 504 var arrayContains = /*Array.prototype.indexOf ? 505 function(arr, val) { 506 return arr.indexOf(val) > -1; 507 }:*/ 508 509 function(arr, val) { 510 var i = arr.length; 511 while (i--) { 512 if (arr[i] === val) { 513 return true; 514 } 515 } 516 return false; 517 }; 518 519 // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI 520 function isHtmlNamespace(node) { 521 var ns; 522 return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml"); 523 } 524 525 function parentElement(node) { 526 var parent = node.parentNode; 527 return (parent.nodeType == 1) ? parent : null; 528 } 529 530 function getNodeIndex(node) { 531 var i = 0; 532 while( (node = node.previousSibling) ) { 533 ++i; 534 } 535 return i; 536 } 537 538 function getNodeLength(node) { 539 switch (node.nodeType) { 540 case 7: 541 case 10: 542 return 0; 543 case 3: 544 case 8: 545 return node.length; 546 default: 547 return node.childNodes.length; 548 } 549 } 550 551 function getCommonAncestor(node1, node2) { 552 var ancestors = [], n; 553 for (n = node1; n; n = n.parentNode) { 554 ancestors.push(n); 555 } 556 557 for (n = node2; n; n = n.parentNode) { 558 if (arrayContains(ancestors, n)) { 559 return n; 560 } 561 } 562 563 return null; 564 } 565 566 function isAncestorOf(ancestor, descendant, selfIsAncestor) { 567 var n = selfIsAncestor ? descendant : descendant.parentNode; 568 while (n) { 569 if (n === ancestor) { 570 return true; 571 } else { 572 n = n.parentNode; 573 } 574 } 575 return false; 576 } 577 578 function isOrIsAncestorOf(ancestor, descendant) { 579 return isAncestorOf(ancestor, descendant, true); 580 } 581 582 function getClosestAncestorIn(node, ancestor, selfIsAncestor) { 583 var p, n = selfIsAncestor ? node : node.parentNode; 584 while (n) { 585 p = n.parentNode; 586 if (p === ancestor) { 587 return n; 588 } 589 n = p; 590 } 591 return null; 592 } 593 594 function isCharacterDataNode(node) { 595 var t = node.nodeType; 596 return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment 597 } 598 599 function isTextOrCommentNode(node) { 600 if (!node) { 601 return false; 602 } 603 var t = node.nodeType; 604 return t == 3 || t == 8 ; // Text or Comment 605 } 606 607 function insertAfter(node, precedingNode) { 608 var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; 609 if (nextNode) { 610 parent.insertBefore(node, nextNode); 611 } else { 612 parent.appendChild(node); 613 } 614 return node; 615 } 616 617 // Note that we cannot use splitText() because it is bugridden in IE 9. 618 function splitDataNode(node, index, positionsToPreserve) { 619 var newNode = node.cloneNode(false); 620 newNode.deleteData(0, index); 621 node.deleteData(index, node.length - index); 622 insertAfter(newNode, node); 623 624 // Preserve positions 625 if (positionsToPreserve) { 626 for (var i = 0, position; position = positionsToPreserve[i++]; ) { 627 // Handle case where position was inside the portion of node after the split point 628 if (position.node == node && position.offset > index) { 629 position.node = newNode; 630 position.offset -= index; 631 } 632 // Handle the case where the position is a node offset within node's parent 633 else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) { 634 ++position.offset; 635 } 636 } 637 } 638 return newNode; 639 } 640 641 function getDocument(node) { 642 if (node.nodeType == 9) { 643 return node; 644 } else if (typeof node.ownerDocument != UNDEF) { 645 return node.ownerDocument; 646 } else if (typeof node.document != UNDEF) { 647 return node.document; 648 } else if (node.parentNode) { 649 return getDocument(node.parentNode); 650 } else { 651 throw module.createError("getDocument: no document found for node"); 652 } 653 } 654 655 function getWindow(node) { 656 var doc = getDocument(node); 657 if (typeof doc.defaultView != UNDEF) { 658 return doc.defaultView; 659 } else if (typeof doc.parentWindow != UNDEF) { 660 return doc.parentWindow; 661 } else { 662 throw module.createError("Cannot get a window object for node"); 663 } 664 } 665 666 function getIframeDocument(iframeEl) { 667 if (typeof iframeEl.contentDocument != UNDEF) { 668 return iframeEl.contentDocument; 669 } else if (typeof iframeEl.contentWindow != UNDEF) { 670 return iframeEl.contentWindow.document; 671 } else { 672 throw module.createError("getIframeDocument: No Document object found for iframe element"); 673 } 674 } 675 676 function getIframeWindow(iframeEl) { 677 if (typeof iframeEl.contentWindow != UNDEF) { 678 return iframeEl.contentWindow; 679 } else if (typeof iframeEl.contentDocument != UNDEF) { 680 return iframeEl.contentDocument.defaultView; 681 } else {
682 throw module.createError("getIframeWindow: No Window object found for iframe element"); 683 } 684 } 685 686 // This looks bad. Is it worth it? 687 function isWindow(obj) { 688 return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document"); 689 } 690 691 function getContentDocument(obj, module, methodName) { 692 var doc; 693 694 if (!obj) { 695 doc = document; 696 } 697 698 // Test if a DOM node has been passed and obtain a document object for it if so 699 else if (util.isHostProperty(obj, "nodeType")) { 700 doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ? 701 getIframeDocument(obj) : getDocument(obj); 702 } 703 704 // Test if the doc parameter appears to be a Window object 705 else if (isWindow(obj)) { 706 doc = obj.document; 707 } 708 709 if (!doc) { 710 throw module.createError(methodName + "(): Parameter must be a Window object or DOM node"); 711 } 712 713 return doc; 714 } 715 716 function getRootContainer(node) { 717 var parent; 718 while ( (parent = node.parentNode) ) { 719 node = parent; 720 } 721 return node; 722 } 723 724 function comparePoints(nodeA, offsetA, nodeB, offsetB) { 725 // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
vendor: 13,826 bytes, lines 726-1082
726 var nodeC, root, childA, childB, n; 727 if (nodeA == nodeB) { 728 // Case 1: nodes are the same 729 return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1; 730 } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) { 731 // Case 2: node C (container B or an ancestor) is a child node of A 732 return offsetA <= getNodeIndex(nodeC) ? -1 : 1; 733 } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) { 734 // Case 3: node C (container A or an ancestor) is a child node of B 735 return getNodeIndex(nodeC) < offsetB ? -1 : 1; 736 } else { 737 root = getCommonAncestor(nodeA, nodeB); 738 if (!root) { 739 throw new Error("comparePoints error: nodes have no common ancestor"); 740 } 741 742 // Case 4: containers are siblings or descendants of siblings 743 childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true); 744 childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true); 745 746 if (childA === childB) { 747 // This shouldn't be possible 748 throw module.createError("comparePoints got to case 4 and childA and childB are the same!"); 749 } else { 750 n = root.firstChild; 751 while (n) { 752 if (n === childA) { 753 return -1; 754 } else if (n === childB) { 755 return 1; 756 } 757 n = n.nextSibling; 758 } 759 } 760 } 761 } 762 763 /*----------------------------------------------------------------------------------------------------------------*/ 764 765 // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried 766 var crashyTextNodes = false; 767 768 function isBrokenNode(node) { 769 var n; 770 try { 771 n = node.parentNode; 772 return false; 773 } catch (e) { 774 return true; 775 } 776 } 777 778 (function() { 779 var el = document.createElement("b"); 780 el.innerHTML = "1"; 781 var textNode = el.firstChild; 782 el.innerHTML = "<br />"; 783 crashyTextNodes = isBrokenNode(textNode); 784 785 api.features.crashyTextNodes = crashyTextNodes; 786 })(); 787 788 /*----------------------------------------------------------------------------------------------------------------*/ 789 790 function inspectNode(node) { 791 if (!node) { 792 return "[No node]"; 793 } 794 if (crashyTextNodes && isBrokenNode(node)) { 795 return "[Broken node]"; 796 } 797 if (isCharacterDataNode(node)) { 798 return '"' + node.data + '"'; 799 } 800 if (node.nodeType == 1) { 801 var idAttr = node.id ? ' id="' + node.id + '"' : ""; 802 return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]"; 803 } 804 return node.nodeName; 805 } 806 807 function fragmentFromNodeChildren(node) { 808 var fragment = getDocument(node).createDocumentFragment(), child; 809 while ( (child = node.firstChild) ) { 810 fragment.appendChild(child); 811 } 812 return fragment; 813 } 814 815 var getComputedStyleProperty; 816 if (typeof window.getComputedStyle != UNDEF) { 817 getComputedStyleProperty = function(el, propName) { 818 return getWindow(el).getComputedStyle(el, null)[propName]; 819 }; 820 } else if (typeof document.documentElement.currentStyle != UNDEF) { 821 getComputedStyleProperty = function(el, propName) { 822 return el.currentStyle ? el.currentStyle[propName] : ""; 823 }; 824 } else { 825 module.fail("No means of obtaining computed style properties found"); 826 } 827 828 function createTestElement(doc, html, contentEditable) { 829 var body = getBody(doc); 830 var el = doc.createElement("div"); 831 el.contentEditable = "" + !!contentEditable; 832 if (html) { 833 el.innerHTML = html; 834 } 835 836 // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292) 837 var bodyFirstChild = body.firstChild; 838 if (bodyFirstChild) { 839 body.insertBefore(el, bodyFirstChild); 840 } else { 841 body.appendChild(el); 842 } 843 844 return el; 845 } 846 847 function removeNode(node) { 848 return node.parentNode.removeChild(node); 849 } 850 851 function NodeIterator(root) { 852 this.root = root; 853 this._next = root; 854 } 855 856 NodeIterator.prototype = { 857 _current: null, 858 859 hasNext: function() { 860 return !!this._next; 861 }, 862 863 next: function() { 864 var n = this._current = this._next; 865 var child, next; 866 if (this._current) { 867 child = n.firstChild; 868 if (child) { 869 this._next = child; 870 } else { 871 next = null; 872 while ((n !== this.root) && !(next = n.nextSibling)) { 873 n = n.parentNode; 874 } 875 this._next = next; 876 } 877 } 878 return this._current; 879 }, 880 881 detach: function() { 882 this._current = this._next = this.root = null; 883 } 884 }; 885 886 function createIterator(root) { 887 return new NodeIterator(root); 888 } 889 890 function DomPosition(node, offset) { 891 this.node = node; 892 this.offset = offset; 893 } 894 895 DomPosition.prototype = { 896 equals: function(pos) { 897 return !!pos && this.node === pos.node && this.offset == pos.offset; 898 }, 899 900 inspect: function() { 901 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; 902 }, 903 904 toString: function() { 905 return this.inspect(); 906 } 907 }; 908 909 function DOMException(codeName) { 910 this.code = this[codeName]; 911 this.codeName = codeName; 912 this.message = "DOMException: " + this.codeName; 913 } 914 915 DOMException.prototype = { 916 INDEX_SIZE_ERR: 1, 917 HIERARCHY_REQUEST_ERR: 3, 918 WRONG_DOCUMENT_ERR: 4, 919 NO_MODIFICATION_ALLOWED_ERR: 7, 920 NOT_FOUND_ERR: 8, 921 NOT_SUPPORTED_ERR: 9, 922 INVALID_STATE_ERR: 11, 923 INVALID_NODE_TYPE_ERR: 24 924 }; 925 926 DOMException.prototype.toString = function() { 927 return this.message; 928 }; 929 930 api.dom = { 931 arrayContains: arrayContains, 932 isHtmlNamespace: isHtmlNamespace, 933 parentElement: parentElement, 934 getNodeIndex: getNodeIndex, 935 getNodeLength: getNodeLength, 936 getCommonAncestor: getCommonAncestor, 937 isAncestorOf: isAncestorOf, 938 isOrIsAncestorOf: isOrIsAncestorOf, 939 getClosestAncestorIn: getClosestAncestorIn, 940 isCharacterDataNode: isCharacterDataNode, 941 isTextOrCommentNode: isTextOrCommentNode, 942 insertAfter: insertAfter, 943 splitDataNode: splitDataNode, 944 getDocument: getDocument, 945 getWindow: getWindow, 946 getIframeWindow: getIframeWindow, 947 getIframeDocument: getIframeDocument, 948 getBody: getBody, 949 isWindow: isWindow, 950 getContentDocument: getContentDocument, 951 getRootContainer: getRootContainer, 952 comparePoints: comparePoints, 953 isBrokenNode: isBrokenNode, 954 inspectNode: inspectNode, 955 getComputedStyleProperty: getComputedStyleProperty, 956 createTestElement: createTestElement, 957 removeNode: removeNode, 958 fragmentFromNodeChildren: fragmentFromNodeChildren, 959 createIterator: createIterator, 960 DomPosition: DomPosition 961 }; 962 963 api.DOMException = DOMException; 964 }); 965 966 /*----------------------------------------------------------------------------------------------------------------*/ 967 968 // Pure JavaScript implementation of DOM Range 969 api.createCoreModule("DomRange", ["DomUtil"], function(api, module) { 970 var dom = api.dom; 971 var util = api.util; 972 var DomPosition = dom.DomPosition; 973 var DOMException = api.DOMException; 974 975 var isCharacterDataNode = dom.isCharacterDataNode; 976 var getNodeIndex = dom.getNodeIndex; 977 var isOrIsAncestorOf = dom.isOrIsAncestorOf; 978 var getDocument = dom.getDocument; 979 var comparePoints = dom.comparePoints; 980 var splitDataNode = dom.splitDataNode; 981 var getClosestAncestorIn = dom.getClosestAncestorIn; 982 var getNodeLength = dom.getNodeLength; 983 var arrayContains = dom.arrayContains; 984 var getRootContainer = dom.getRootContainer; 985 var crashyTextNodes = api.features.crashyTextNodes; 986 987 var removeNode = dom.removeNode; 988 989 /*----------------------------------------------------------------------------------------------------------------*/ 990 991 // Utility functions 992 993 function isNonTextPartiallySelected(node, range) { 994 return (node.nodeType != 3) && 995 (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer)); 996 } 997 998 function getRangeDocument(range) { 999 return range.document || getDocument(range.startContainer); 1000 } 1001 1002 function getRangeRoot(range) { 1003 return getRootContainer(range.startContainer); 1004 } 1005 1006 function getBoundaryBeforeNode(node) { 1007 return new DomPosition(node.parentNode, getNodeIndex(node)); 1008 } 1009 1010 function getBoundaryAfterNode(node) { 1011 return new DomPosition(node.parentNode, getNodeIndex(node) + 1); 1012 } 1013 1014 function insertNodeAtPosition(node, n, o) { 1015 var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; 1016 if (isCharacterDataNode(n)) { 1017 if (o == n.length) { 1018 dom.insertAfter(node, n); 1019 } else { 1020 n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o)); 1021 } 1022 } else if (o >= n.childNodes.length) { 1023 n.appendChild(node); 1024 } else { 1025 n.insertBefore(node, n.childNodes[o]); 1026 } 1027 return firstNodeInserted; 1028 } 1029 1030 function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) { 1031 assertRangeValid(rangeA); 1032 assertRangeValid(rangeB); 1033 1034 if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) { 1035 throw new DOMException("WRONG_DOCUMENT_ERR"); 1036 } 1037 1038 var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset), 1039 endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset); 1040 1041 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; 1042 } 1043 1044 function cloneSubtree(iterator) { 1045 var partiallySelected; 1046 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { 1047 partiallySelected = iterator.isPartiallySelectedSubtree(); 1048 node = node.cloneNode(!partiallySelected); 1049 if (partiallySelected) { 1050 subIterator = iterator.getSubtreeIterator(); 1051 node.appendChild(cloneSubtree(subIterator)); 1052 subIterator.detach(); 1053 } 1054 1055 if (node.nodeType == 10) { // DocumentType 1056 throw new DOMException("HIERARCHY_REQUEST_ERR"); 1057 } 1058 frag.appendChild(node); 1059 } 1060 return frag; 1061 } 1062 1063 function iterateSubtree(rangeIterator, func, iteratorState) { 1064 var it, n; 1065 iteratorState = iteratorState || { stop: false }; 1066 for (var node, subRangeIterator; node = rangeIterator.next(); ) { 1067 if (rangeIterator.isPartiallySelectedSubtree()) { 1068 if (func(node) === false) { 1069 iteratorState.stop = true; 1070 return; 1071 } else { 1072 // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of 1073 // the node selected by the Range. 1074 subRangeIterator = rangeIterator.getSubtreeIterator(); 1075 iterateSubtree(subRangeIterator, func, iteratorState); 1076 subRangeIterator.detach(); 1077 if (iteratorState.stop) { 1078 return; 1079 } 1080 } 1081 } else { 1082 // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
1083 // descendants 1084 it = dom.createIterator(node); 1085 while ( (n = it.next()) ) { 1086 if (func(n) === false) { 1087 iteratorState.stop = true; 1088 return; 1089 } 1090 } 1091 } 1092 } 1093 } 1094 1095 function deleteSubtree(iterator) { 1096 var subIterator; 1097 while (iterator.next()) { 1098 if (iterator.isPartiallySelectedSubtree()) { 1099 subIterator = iterator.getSubtreeIterator(); 1100 deleteSubtree(subIterator); 1101 subIterator.detach(); 1102 } else { 1103 iterator.remove(); 1104 } 1105 } 1106 } 1107 1108 function extractSubtree(iterator) { 1109 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { 1110 1111 if (iterator.isPartiallySelectedSubtree()) { 1112 node = node.cloneNode(false); 1113 subIterator = iterator.getSubtreeIterator(); 1114 node.appendChild(extractSubtree(subIterator)); 1115 subIterator.detach(); 1116 } else { 1117 iterator.remove(); 1118 } 1119 if (node.nodeType == 10) { // DocumentType 1120 throw new DOMException("HIERARCHY_REQUEST_ERR"); 1121 } 1122 frag.appendChild(node); 1123 } 1124 return frag; 1125 } 1126 1127 function getNodesInRange(range, nodeTypes, filter) { 1128 var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; 1129 var filterExists = !!filter; 1130 if (filterNodeTypes) { 1131 regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); 1132 } 1133 1134 var nodes = []; 1135 iterateSubtree(new RangeIterator(range, false), function(node) { 1136 if (filterNodeTypes && !regex.test(node.nodeType)) { 1137 return; 1138 } 1139 if (filterExists && !filter(node)) { 1140 return; 1141 } 1142 // Don't include a boundary container if it is a character data node and the range does not contain any 1143 // of its character data. See issue 190. 1144 var sc = range.startContainer; 1145 if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) { 1146 return; 1147 } 1148 1149 var ec = range.endContainer; 1150 if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) { 1151 return; 1152 } 1153 1154 nodes.push(node); 1155 }); 1156 return nodes; 1157 } 1158 1159 function inspect(range) { 1160 var name = (typeof range.getName == "undefined") ? "Range" : range.getName(); 1161 return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " + 1162 dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; 1163 } 1164 1165 /*----------------------------------------------------------------------------------------------------------------*/ 1166 1167 // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) 1168 1169 function RangeIterator(range, clonePartiallySelectedTextNodes) { 1170 this.range = range; 1171 this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; 1172 1173 1174 if (!range.collapsed) { 1175 this.sc = range.startContainer; 1176 this.so = range.startOffset; 1177 this.ec = range.endContainer; 1178 this.eo = range.endOffset; 1179 var root = range.commonAncestorContainer; 1180 1181 if (this.sc === this.ec && isCharacterDataNode(this.sc)) { 1182 this.isSingleCharacterDataNode = true; 1183 this._first = this._last = this._next = this.sc; 1184 } else { 1185 this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
1186 this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true); 1187 this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ? 1188 this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true); 1189 } 1190 } 1191 } 1192 1193 RangeIterator.prototype = { 1194 _current: null, 1195 _next: null, 1196 _first: null, 1197 _last: null, 1198 isSingleCharacterDataNode: false, 1199 1200 reset: function() { 1201 this._current = null; 1202 this._next = this._first; 1203 }, 1204 1205 hasNext: function() { 1206 return !!this._next; 1207 }, 1208 1209 next: function() { 1210 // Move to next node 1211 var current = this._current = this._next; 1212 if (current) { 1213 this._next = (current !== this._last) ? current.nextSibling : null; 1214 1215 // Check for partially selected text nodes 1216 if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { 1217 if (current === this.ec) { 1218 (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); 1219 } 1220 if (this._current === this.sc) { 1221 (current = current.cloneNode(true)).deleteData(0, this.so); 1222 } 1223 } 1224 } 1225 1226 return current; 1227 }, 1228 1229 remove: function() { 1230 var current = this._current, start, end; 1231 1232 if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { 1233 start = (current === this.sc) ? this.so : 0; 1234 end = (current === this.ec) ? this.eo : current.length; 1235 if (start != end) { 1236 current.deleteData(start, end - start); 1237 } 1238 } else { 1239 if (current.parentNode) { 1240 removeNode(current); 1241 } else { 1242 } 1243 } 1244 }, 1245 1246 // Checks if the current node is partially selected 1247 isPartiallySelectedSubtree: function() { 1248 var current = this._current; 1249 return isNonTextPartiallySelected(current, this.range); 1250 }, 1251 1252 getSubtreeIterator: function() { 1253 var subRange; 1254 if (this.isSingleCharacterDataNode) { 1255 subRange = this.range.cloneRange(); 1256 subRange.collapse(false); 1257 } else { 1258 subRange = new Range(getRangeDocument(this.range)); 1259 var current = this._current; 1260 var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current); 1261 1262 if (isOrIsAncestorOf(current, this.sc)) { 1263 startContainer = this.sc; 1264 startOffset = this.so; 1265 } 1266 if (isOrIsAncestorOf(current, this.ec)) { 1267 endContainer = this.ec; 1268 endOffset = this.eo; 1269 } 1270 1271 updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); 1272 } 1273 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); 1274 }, 1275 1276 detach: function() { 1277 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; 1278 } 1279 }; 1280 1281 /*----------------------------------------------------------------------------------------------------------------*/ 1282 1283 var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; 1284 var rootContainerNodeTypes = [2, 9, 11]; 1285 var readonlyNodeTypes = [5, 6, 10, 12]; 1286 var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; 1287 var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; 1288 1289 function createAncestorFinder(nodeTypes) { 1290 return function(node, selfIsAncestor) { 1291 var t, n = selfIsAncestor ? node : node.parentNode; 1292 while (n) { 1293 t = n.nodeType; 1294 if (arrayContains(nodeTypes, t)) { 1295 return n; 1296 } 1297 n = n.parentNode; 1298 } 1299 return null; 1300 }; 1301 } 1302 1303 var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); 1304 var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); 1305 var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); 1306 1307 function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { 1308 if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1309 throw new DOMException("INVALID_NODE_TYPE_ERR"); 1310 } 1311 } 1312 1313 function assertValidNodeType(node, invalidTypes) { 1314 if (!arrayContains(invalidTypes, node.nodeType)) { 1315 throw new DOMException("INVALID_NODE_TYPE_ERR"); 1316 } 1317 } 1318 1319 function assertValidOffset(node, offset) { 1320 if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) { 1321 throw new DOMException("INDEX_SIZE_ERR"); 1322 } 1323 } 1324 1325 function assertSameDocumentOrFragment(node1, node2) { 1326 if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { 1327 throw new DOMException("WRONG_DOCUMENT_ERR"); 1328 } 1329 } 1330 1331 function assertNodeNotReadOnly(node) { 1332 if (getReadonlyAncestor(node, true)) { 1333 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); 1334 } 1335 } 1336 1337 function assertNode(node, codeName) { 1338 if (!node) { 1339 throw new DOMException(codeName); 1340 } 1341 } 1342 1343 function isValidOffset(node, offset) { 1344 return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length); 1345 } 1346 1347 function isRangeValid(range) { 1348 return (!!range.startContainer && !!range.endContainer && 1349 !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) && 1350 getRootContainer(range.startContainer) == getRootContainer(range.endContainer) && 1351 isValidOffset(range.startContainer, range.startOffset) && 1352 isValidOffset(range.endContainer, range.endOffset)); 1353 } 1354 1355 function assertRangeValid(range) { 1356 if (!isRangeValid(range)) { 1357 throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")"); 1358 } 1359 } 1360 1361 /*----------------------------------------------------------------------------------------------------------------*/ 1362 1363 // Test the browser's innerHTML support to decide how to implement createContextualFragment 1364 var styleEl = document.createElement("style"); 1365 var htmlParsingConforms = false; 1366 try { 1367 styleEl.innerHTML = "<b>x</b>"; 1368 htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node 1369 } catch (e) { 1370 // IE 6 and 7 throw 1371 } 1372 1373 api.features.htmlParsingConforms = htmlParsingConforms; 1374 1375 var createContextualFragment = htmlParsingConforms ? 1376 1377 // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See 1378 // discussion and base code for this implementation at issue 67. 1379 // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface 1380 // Thanks to Aleks Williams. 1381 function(fragmentStr) { 1382 // "Let node the context object's start's node." 1383 var node = this.startContainer; 1384 var doc = getDocument(node); 1385 1386 // "If the context object's start's node is null, raise an INVALID_STATE_ERR 1387 // exception and abort these steps." 1388 if (!node) { 1389 throw new DOMException("INVALID_STATE_ERR"); 1390 } 1391 1392 // "Let element be as follows, depending on node's interface:" 1393 // Document, Document Fragment: null 1394 var el = null; 1395 1396 // "Element: node" 1397 if (node.nodeType == 1) { 1398 el = node; 1399 1400 // "Text, Comment: node's parentElement" 1401 } else if (isCharacterDataNode(node)) { 1402 el = dom.parentElement(node); 1403 } 1404 1405 // "If either element is null or element's ownerDocument is an HTML document 1406 // and element's local name is "html" and element's namespace is the HTML 1407 // namespace" 1408 if (el === null || ( 1409 el.nodeName == "HTML" && 1410 dom.isHtmlNamespace(getDocument(el).documentElement) && 1411 dom.isHtmlNamespace(el) 1412 )) { 1413 1414 // "let element be a new Element with "body" as its local name and the HTML 1415 // namespace as its namespace."" 1416 el = doc.createElement("body"); 1417 } else { 1418 el = el.cloneNode(false); 1419 } 1420 1421 // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." 1422 // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." 1423 // "In either case, the algorithm must be invoked with fragment as the input 1424 // and element as the context element." 1425 el.innerHTML = fragmentStr; 1426 1427 // "If this raises an exception, then abort these steps. Otherwise, let new 1428 // children be the nodes returned." 1429 1430 // "Let fragment be a new DocumentFragment." 1431 // "Append all new children to fragment." 1432 // "Return fragment." 1433 return dom.fragmentFromNodeChildren(el); 1434 } : 1435 1436 // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that 1437 // previous versions of Rangy used (with the exception of using a body element rather than a div) 1438 function(fragmentStr) { 1439 var doc = getRangeDocument(this); 1440 var el = doc.createElement("body"); 1441 el.innerHTML = fragmentStr; 1442 1443 return dom.fragmentFromNodeChildren(el); 1444 }; 1445 1446 function splitRangeBoundaries(range, positionsToPreserve) { 1447 assertRangeValid(range); 1448 1449 var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset; 1450 var startEndSame = (sc === ec); 1451 1452 if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { 1453 splitDataNode(ec, eo, positionsToPreserve); 1454 } 1455 1456 if (isCharacterDataNode(sc) && so > 0 && so < sc.length) { 1457 sc = splitDataNode(sc, so, positionsToPreserve); 1458 if (startEndSame) { 1459 eo -= so; 1460 ec = sc; 1461 } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) { 1462 eo++; 1463 } 1464 so = 0; 1465 } 1466 range.setStartAndEnd(sc, so, ec, eo); 1467 } 1468 1469 function rangeToHtml(range) { 1470 assertRangeValid(range); 1471 var container = range.commonAncestorContainer.parentNode.cloneNode(false); 1472 container.appendChild( range.cloneContents() ); 1473 return container.innerHTML; 1474 } 1475 1476 /*----------------------------------------------------------------------------------------------------------------*/ 1477 1478 var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", 1479 "commonAncestorContainer"]; 1480 1481 var s2s = 0, s2e = 1, e2e = 2, e2s = 3; 1482 var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; 1483 1484 util.extend(api.rangePrototype, { 1485 compareBoundaryPoints: function(how, range) { 1486 assertRangeValid(this);
1487 assertSameDocumentOrFragment(this.startContainer, range.startContainer); 1488 1489 var nodeA, offsetA, nodeB, offsetB; 1490 var prefixA = (how == e2s || how == s2s) ? "start" : "end"; 1491 var prefixB = (how == s2e || how == s2s) ? "start" : "end"; 1492 nodeA = this[prefixA + "Container"]; 1493 offsetA = this[prefixA + "Offset"]; 1494 nodeB = range[prefixB + "Container"]; 1495 offsetB = range[prefixB + "Offset"]; 1496 return comparePoints(nodeA, offsetA, nodeB, offsetB); 1497 }, 1498 1499 insertNode: function(node) { 1500 assertRangeValid(this); 1501 assertValidNodeType(node, insertableNodeTypes); 1502 assertNodeNotReadOnly(this.startContainer); 1503 1504 if (isOrIsAncestorOf(node, this.startContainer)) { 1505 throw new DOMException("HIERARCHY_REQUEST_ERR"); 1506 } 1507 1508 // No check for whether the container of the start of the Range is of a type that does not allow 1509 // children of the type of node: the browser's DOM implementation should do this for us when we attempt 1510 // to add the node 1511 1512 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); 1513 this.setStartBefore(firstNodeInserted); 1514 }, 1515 1516 cloneContents: function() { 1517 assertRangeValid(this); 1518 1519 var clone, frag; 1520 if (this.collapsed) { 1521 return getRangeDocument(this).createDocumentFragment(); 1522 } else { 1523 if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) { 1524 clone = this.startContainer.cloneNode(true); 1525 clone.data = clone.data.slice(this.startOffset, this.endOffset); 1526 frag = getRangeDocument(this).createDocumentFragment(); 1527 frag.appendChild(clone); 1528 return frag; 1529 } else { 1530 var iterator = new RangeIterator(this, true); 1531 clone = cloneSubtree(iterator); 1532 iterator.detach(); 1533 } 1534 return clone; 1535 } 1536 }, 1537 1538 canSurroundContents: function() { 1539 assertRangeValid(this); 1540 assertNodeNotReadOnly(this.startContainer); 1541 assertNodeNotReadOnly(this.endContainer); 1542 1543 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects 1544 // no non-text nodes. 1545 var iterator = new RangeIterator(this, true); 1546 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || 1547 (iterator._last && isNonTextPartiallySelected(iterator._last, this))); 1548 iterator.detach(); 1549 return !boundariesInvalid; 1550 }, 1551 1552 surroundContents: function(node) { 1553 assertValidNodeType(node, surroundNodeTypes); 1554 1555 if (!this.canSurroundContents()) { 1556 throw new DOMException("INVALID_STATE_ERR"); 1557 } 1558 1559 // Extract the contents 1560 var content = this.extractContents(); 1561 1562 // Clear the children of the node 1563 if (node.hasChildNodes()) { 1564 while (node.lastChild) { 1565 node.removeChild(node.lastChild); 1566 } 1567 } 1568 1569 // Insert the new node and add the extracted contents 1570 insertNodeAtPosition(node, this.startContainer, this.startOffset); 1571 node.appendChild(content); 1572 1573 this.selectNode(node); 1574 }, 1575 1576 cloneRange: function() { 1577 assertRangeValid(this); 1578 var range = new Range(getRangeDocument(this)); 1579 var i = rangeProperties.length, prop; 1580 while (i--) { 1581 prop = rangeProperties[i]; 1582 range[prop] = this[prop]; 1583 } 1584 return range; 1585 }, 1586 1587 toString: function() { 1588 assertRangeValid(this); 1589 var sc = this.startContainer; 1590 if (sc === this.endContainer && isCharacterDataNode(sc)) { 1591 return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset)
vendor: 20,837 bytes, lines 1591-2079
1591: ""; 1592 } else { 1593 var textParts = [], iterator = new RangeIterator(this, true); 1594 iterateSubtree(iterator, function(node) { 1595 // Accept only text or CDATA nodes, not comments 1596 if (node.nodeType == 3 || node.nodeType == 4) { 1597 textParts.push(node.data); 1598 } 1599 }); 1600 iterator.detach(); 1601 return textParts.join(""); 1602 } 1603 }, 1604 1605 // The methods below are all non-standard. The following batch were introduced by Mozilla but have since 1606 // been removed from Mozilla. 1607 1608 compareNode: function(node) { 1609 assertRangeValid(this); 1610 1611 var parent = node.parentNode; 1612 var nodeIndex = getNodeIndex(node); 1613 1614 if (!parent) { 1615 throw new DOMException("NOT_FOUND_ERR"); 1616 } 1617 1618 var startComparison = this.comparePoint(parent, nodeIndex), 1619 endComparison = this.comparePoint(parent, nodeIndex + 1); 1620 1621 if (startComparison < 0) { // Node starts before 1622 return (endComparison > 0) ? n_b_a : n_b; 1623 } else { 1624 return (endComparison > 0) ? n_a : n_i; 1625 } 1626 }, 1627 1628 comparePoint: function(node, offset) { 1629 assertRangeValid(this); 1630 assertNode(node, "HIERARCHY_REQUEST_ERR"); 1631 assertSameDocumentOrFragment(node, this.startContainer); 1632 1633 if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { 1634 return -1; 1635 } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { 1636 return 1; 1637 } 1638 return 0; 1639 }, 1640 1641 createContextualFragment: createContextualFragment, 1642 1643 toHtml: function() { 1644 return rangeToHtml(this); 1645 }, 1646 1647 // touchingIsIntersecting determines whether this method considers a node that borders a range intersects 1648 // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) 1649 intersectsNode: function(node, touchingIsIntersecting) { 1650 assertRangeValid(this); 1651 if (getRootContainer(node) != getRangeRoot(this)) { 1652 return false; 1653 } 1654 1655 var parent = node.parentNode, offset = getNodeIndex(node); 1656 if (!parent) { 1657 return true; 1658 } 1659 1660 var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset), 1661 endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset); 1662 1663 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; 1664 }, 1665 1666 isPointInRange: function(node, offset) { 1667 assertRangeValid(this); 1668 assertNode(node, "HIERARCHY_REQUEST_ERR"); 1669 assertSameDocumentOrFragment(node, this.startContainer); 1670 1671 return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && 1672 (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); 1673 }, 1674 1675 // The methods below are non-standard and invented by me. 1676 1677 // Sharing a boundary start-to-end or end-to-start does not count as intersection. 1678 intersectsRange: function(range) { 1679 return rangesIntersect(this, range, false); 1680 }, 1681 1682 // Sharing a boundary start-to-end or end-to-start does count as intersection. 1683 intersectsOrTouchesRange: function(range) { 1684 return rangesIntersect(this, range, true); 1685 }, 1686 1687 intersection: function(range) { 1688 if (this.intersectsRange(range)) { 1689 var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), 1690 endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); 1691 1692 var intersectionRange = this.cloneRange(); 1693 if (startComparison == -1) { 1694 intersectionRange.setStart(range.startContainer, range.startOffset); 1695 } 1696 if (endComparison == 1) { 1697 intersectionRange.setEnd(range.endContainer, range.endOffset); 1698 } 1699 return intersectionRange; 1700 } 1701 return null; 1702 }, 1703 1704 union: function(range) { 1705 if (this.intersectsOrTouchesRange(range)) { 1706 var unionRange = this.cloneRange(); 1707 if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { 1708 unionRange.setStart(range.startContainer, range.startOffset); 1709 } 1710 if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { 1711 unionRange.setEnd(range.endContainer, range.endOffset); 1712 } 1713 return unionRange; 1714 } else { 1715 throw new DOMException("Ranges do not intersect"); 1716 } 1717 }, 1718 1719 containsNode: function(node, allowPartial) { 1720 if (allowPartial) { 1721 return this.intersectsNode(node, false); 1722 } else { 1723 return this.compareNode(node) == n_i; 1724 } 1725 }, 1726 1727 containsNodeContents: function(node) { 1728 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0; 1729 }, 1730 1731 containsRange: function(range) { 1732 var intersection = this.intersection(range); 1733 return intersection !== null && range.equals(intersection); 1734 }, 1735 1736 containsNodeText: function(node) { 1737 var nodeRange = this.cloneRange(); 1738 nodeRange.selectNode(node); 1739 var textNodes = nodeRange.getNodes([3]); 1740 if (textNodes.length > 0) { 1741 nodeRange.setStart(textNodes[0], 0); 1742 var lastTextNode = textNodes.pop(); 1743 nodeRange.setEnd(lastTextNode, lastTextNode.length); 1744 return this.containsRange(nodeRange); 1745 } else { 1746 return this.containsNodeContents(node); 1747 } 1748 }, 1749 1750 getNodes: function(nodeTypes, filter) { 1751 assertRangeValid(this); 1752 return getNodesInRange(this, nodeTypes, filter); 1753 }, 1754 1755 getDocument: function() { 1756 return getRangeDocument(this); 1757 }, 1758 1759 collapseBefore: function(node) { 1760 this.setEndBefore(node); 1761 this.collapse(false); 1762 }, 1763 1764 collapseAfter: function(node) { 1765 this.setStartAfter(node); 1766 this.collapse(true); 1767 }, 1768 1769 getBookmark: function(containerNode) { 1770 var doc = getRangeDocument(this); 1771 var preSelectionRange = api.createRange(doc); 1772 containerNode = containerNode || dom.getBody(doc); 1773 preSelectionRange.selectNodeContents(containerNode); 1774 var range = this.intersection(preSelectionRange); 1775 var start = 0, end = 0; 1776 if (range) { 1777 preSelectionRange.setEnd(range.startContainer, range.startOffset); 1778 start = preSelectionRange.toString().length; 1779 end = start + range.toString().length; 1780 } 1781 1782 return { 1783 start: start, 1784 end: end, 1785 containerNode: containerNode 1786 }; 1787 }, 1788 1789 moveToBookmark: function(bookmark) { 1790 var containerNode = bookmark.containerNode; 1791 var charIndex = 0; 1792 this.setStart(containerNode, 0); 1793 this.collapse(true); 1794 var nodeStack = [containerNode], node, foundStart = false, stop = false; 1795 var nextCharIndex, i, childNodes; 1796 1797 while (!stop && (node = nodeStack.pop())) { 1798 if (node.nodeType == 3) { 1799 nextCharIndex = charIndex + node.length; 1800 if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) { 1801 this.setStart(node, bookmark.start - charIndex); 1802 foundStart = true; 1803 } 1804 if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) { 1805 this.setEnd(node, bookmark.end - charIndex); 1806 stop = true; 1807 } 1808 charIndex = nextCharIndex; 1809 } else { 1810 childNodes = node.childNodes; 1811 i = childNodes.length; 1812 while (i--) { 1813 nodeStack.push(childNodes[i]); 1814 } 1815 } 1816 } 1817 }, 1818 1819 getName: function() { 1820 return "DomRange"; 1821 }, 1822 1823 equals: function(range) { 1824 return Range.rangesEqual(this, range); 1825 }, 1826 1827 isValid: function() { 1828 return isRangeValid(this); 1829 }, 1830 1831 inspect: function() { 1832 return inspect(this); 1833 }, 1834 1835 detach: function() { 1836 // In DOM4, detach() is now a no-op. 1837 } 1838 }); 1839 1840 function copyComparisonConstantsToObject(obj) { 1841 obj.START_TO_START = s2s; 1842 obj.START_TO_END = s2e; 1843 obj.END_TO_END = e2e; 1844 obj.END_TO_START = e2s; 1845 1846 obj.NODE_BEFORE = n_b; 1847 obj.NODE_AFTER = n_a; 1848 obj.NODE_BEFORE_AND_AFTER = n_b_a; 1849 obj.NODE_INSIDE = n_i; 1850 } 1851 1852 function copyComparisonConstants(constructor) { 1853 copyComparisonConstantsToObject(constructor); 1854 copyComparisonConstantsToObject(constructor.prototype); 1855 } 1856 1857 function createRangeContentRemover(remover, boundaryUpdater) { 1858 return function() { 1859 assertRangeValid(this); 1860 1861 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; 1862 1863 var iterator = new RangeIterator(this, true); 1864 1865 // Work out where to position the range after content removal 1866 var node, boundary; 1867 if (sc !== root) { 1868 node = getClosestAncestorIn(sc, root, true); 1869 boundary = getBoundaryAfterNode(node); 1870 sc = boundary.node; 1871 so = boundary.offset; 1872 } 1873 1874 // Check none of the range is read-only 1875 iterateSubtree(iterator, assertNodeNotReadOnly); 1876 1877 iterator.reset(); 1878 1879 // Remove the content 1880 var returnValue = remover(iterator); 1881 iterator.detach(); 1882 1883 // Move to the new position 1884 boundaryUpdater(this, sc, so, sc, so); 1885 1886 return returnValue; 1887 }; 1888 } 1889 1890 function createPrototypeRange(constructor, boundaryUpdater) { 1891 function createBeforeAfterNodeSetter(isBefore, isStart) { 1892 return function(node) { 1893 assertValidNodeType(node, beforeAfterNodeTypes); 1894 assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); 1895 1896 var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); 1897 (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); 1898 }; 1899 } 1900 1901 function setRangeStart(range, node, offset) { 1902 var ec = range.endContainer, eo = range.endOffset; 1903 if (node !== range.startContainer || offset !== range.startOffset) { 1904 // Check the root containers of the range and the new boundary, and also check whether the new boundary 1905 // is after the current end. In either case, collapse the range to the new position 1906 if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) { 1907 ec = node; 1908 eo = offset; 1909 } 1910 boundaryUpdater(range, node, offset, ec, eo); 1911 } 1912 } 1913 1914 function setRangeEnd(range, node, offset) { 1915 var sc = range.startContainer, so = range.startOffset; 1916 if (node !== range.endContainer || offset !== range.endOffset) { 1917 // Check the root containers of the range and the new boundary, and also check whether the new boundary 1918 // is after the current end. In either case, collapse the range to the new position 1919 if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) { 1920 sc = node; 1921 so = offset; 1922 } 1923 boundaryUpdater(range, sc, so, node, offset); 1924 } 1925 } 1926 1927 // Set up inheritance 1928 var F = function() {}; 1929 F.prototype = api.rangePrototype; 1930 constructor.prototype = new F(); 1931 1932 util.extend(constructor.prototype, { 1933 setStart: function(node, offset) { 1934 assertNoDocTypeNotationEntityAncestor(node, true); 1935 assertValidOffset(node, offset); 1936 1937 setRangeStart(this, node, offset); 1938 }, 1939 1940 setEnd: function(node, offset) { 1941 assertNoDocTypeNotationEntityAncestor(node, true); 1942 assertValidOffset(node, offset); 1943 1944 setRangeEnd(this, node, offset); 1945 }, 1946 1947 /** 1948 * Convenience method to set a range's start and end boundaries. Overloaded as follows: 1949 * - Two parameters (node, offset) creates a collapsed range at that position 1950 * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at 1951 * startOffset and ending at endOffset 1952 * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in 1953 * startNode and ending at endOffset in endNode 1954 */ 1955 setStartAndEnd: function() { 1956 var args = arguments; 1957 var sc = args[0], so = args[1], ec = sc, eo = so; 1958 1959 switch (args.length) { 1960 case 3: 1961 eo = args[2]; 1962 break; 1963 case 4: 1964 ec = args[2]; 1965 eo = args[3]; 1966 break; 1967 } 1968 1969 boundaryUpdater(this, sc, so, ec, eo); 1970 }, 1971 1972 setBoundary: function(node, offset, isStart) { 1973 this["set" + (isStart ? "Start" : "End")](node, offset); 1974 }, 1975 1976 setStartBefore: createBeforeAfterNodeSetter(true, true), 1977 setStartAfter: createBeforeAfterNodeSetter(false, true), 1978 setEndBefore: createBeforeAfterNodeSetter(true, false), 1979 setEndAfter: createBeforeAfterNodeSetter(false, false), 1980 1981 collapse: function(isStart) { 1982 assertRangeValid(this); 1983 if (isStart) { 1984 boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); 1985 } else { 1986 boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); 1987 } 1988 }, 1989 1990 selectNodeContents: function(node) { 1991 assertNoDocTypeNotationEntityAncestor(node, true); 1992 1993 boundaryUpdater(this, node, 0, node, getNodeLength(node)); 1994 }, 1995 1996 selectNode: function(node) { 1997 assertNoDocTypeNotationEntityAncestor(node, false); 1998 assertValidNodeType(node, beforeAfterNodeTypes); 1999 2000 var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); 2001 boundaryUpdater(this, start.node, start.offset, end.node, end.offset); 2002 }, 2003 2004 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), 2005 2006 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), 2007 2008 canSurroundContents: function() { 2009 assertRangeValid(this); 2010 assertNodeNotReadOnly(this.startContainer); 2011 assertNodeNotReadOnly(this.endContainer); 2012 2013 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects 2014 // no non-text nodes. 2015 var iterator = new RangeIterator(this, true); 2016 var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) || 2017 (iterator._last && isNonTextPartiallySelected(iterator._last, this))); 2018 iterator.detach(); 2019 return !boundariesInvalid; 2020 }, 2021 2022 splitBoundaries: function() { 2023 splitRangeBoundaries(this); 2024 }, 2025 2026 splitBoundariesPreservingPositions: function(positionsToPreserve) { 2027 splitRangeBoundaries(this, positionsToPreserve); 2028 }, 2029 2030 normalizeBoundaries: function() { 2031 assertRangeValid(this); 2032 2033 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; 2034 2035 var mergeForward = function(node) { 2036 var sibling = node.nextSibling; 2037 if (sibling && sibling.nodeType == node.nodeType) { 2038 ec = node; 2039 eo = node.length; 2040 node.appendData(sibling.data); 2041 removeNode(sibling); 2042 } 2043 }; 2044 2045 var mergeBackward = function(node) { 2046 var sibling = node.previousSibling; 2047 if (sibling && sibling.nodeType == node.nodeType) { 2048 sc = node; 2049 var nodeLength = node.length; 2050 so = sibling.length; 2051 node.insertData(0, sibling.data); 2052 removeNode(sibling); 2053 if (sc == ec) { 2054 eo += so; 2055 ec = sc; 2056 } else if (ec == node.parentNode) { 2057 var nodeIndex = getNodeIndex(node); 2058 if (eo == nodeIndex) { 2059 ec = node; 2060 eo = nodeLength; 2061 } else if (eo > nodeIndex) { 2062 eo--; 2063 } 2064 } 2065 } 2066 }; 2067 2068 var normalizeStart = true; 2069 var sibling; 2070 2071 if (isCharacterDataNode(ec)) { 2072 if (eo == ec.length) { 2073 mergeForward(ec); 2074 } else if (eo == 0) { 2075 sibling = ec.previousSibling; 2076 if (sibling && sibling.nodeType == ec.nodeType) { 2077 eo = sibling.length; 2078 if (sc == ec) { 2079 normalizeStart = false;
2080 } 2081 sibling.appendData(ec.data); 2082 removeNode(ec); 2083 ec = sibling; 2084 } 2085 } 2086 } else { 2087 if (eo > 0) { 2088 var endNode = ec.childNodes[eo - 1]; 2089 if (endNode && isCharacterDataNode(endNode)) { 2090 mergeForward(endNode); 2091 } 2092 } 2093 normalizeStart = !this.collapsed; 2094 } 2095 2096 if (normalizeStart) { 2097 if (isCharacterDataNode(sc)) { 2098 if (so == 0) { 2099 mergeBackward(sc); 2100 } else if (so == sc.length) { 2101 sibling = sc.nextSibling; 2102 if (sibling && sibling.nodeType == sc.nodeType) { 2103 if (ec == sibling) { 2104 ec = sc; 2105 eo += sc.length; 2106 } 2107 sc.appendData(sibling.data); 2108 removeNode(sibling); 2109 } 2110 } 2111 } else { 2112 if (so < sc.childNodes.length) { 2113 var startNode = sc.childNodes[so]; 2114 if (startNode && isCharacterDataNode(startNode)) { 2115 mergeBackward(startNode); 2116 } 2117 } 2118 } 2119 } else { 2120 sc = ec; 2121 so = eo; 2122 } 2123 2124 boundaryUpdater(this, sc, so, ec, eo); 2125 }, 2126 2127 collapseToPoint: function(node, offset) { 2128 assertNoDocTypeNotationEntityAncestor(node, true); 2129 assertValidOffset(node, offset); 2130 this.setStartAndEnd(node, offset); 2131 } 2132 }); 2133 2134 copyComparisonConstants(constructor); 2135 } 2136 2137 /*----------------------------------------------------------------------------------------------------------------*/ 2138 2139 // Updates commonAncestorContainer and collapsed after boundary change 2140 function updateCollapsedAndCommonAncestor(range) { 2141 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); 2142 range.commonAncestorContainer = range.collapsed ? 2143 range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer); 2144 } 2145 2146 function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { 2147 range.startContainer = startContainer; 2148 range.startOffset = startOffset; 2149 range.endContainer = endContainer; 2150 range.endOffset = endOffset; 2151 range.document = dom.getDocument(startContainer); 2152 2153 updateCollapsedAndCommonAncestor(range); 2154 } 2155 2156 function Range(doc) { 2157 this.startContainer = doc; 2158 this.startOffset = 0; 2159 this.endContainer = doc; 2160 this.endOffset = 0; 2161 this.document = doc; 2162 updateCollapsedAndCommonAncestor(this); 2163 } 2164 2165 createPrototypeRange(Range, updateBoundaries); 2166 2167 util.extend(Range, { 2168 rangeProperties: rangeProperties, 2169 RangeIterator: RangeIterator, 2170 copyComparisonConstants: copyComparisonConstants, 2171 createPrototypeRange: createPrototypeRange, 2172 inspect: inspect, 2173 toHtml: rangeToHtml, 2174 getRangeDocument: getRangeDocument, 2175 rangesEqual: function(r1, r2) { 2176 return r1.startContainer === r2.startContainer && 2177 r1.startOffset === r2.startOffset && 2178 r1.endContainer === r2.endContainer && 2179 r1.endOffset === r2.endOffset; 2180 } 2181 }); 2182 2183 api.DomRange = Range; 2184 }); 2185 2186 /*----------------------------------------------------------------------------------------------------------------*/ 2187 2188 // Wrappers for the browser's native DOM Range and/or TextRange implementation 2189 api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) { 2190 var WrappedRange, WrappedTextRange; 2191 var dom = api.dom; 2192 var util = api.util; 2193 var DomPosition = dom.DomPosition; 2194 var DomRange = api.DomRange; 2195 var getBody = dom.getBody; 2196 var getContentDocument = dom.getContentDocument; 2197 var isCharacterDataNode = dom.isCharacterDataNode; 2198 2199 2200 /*----------------------------------------------------------------------------------------------------------------*/ 2201 2202 if (api.features.implementsDomRange) { 2203 // This is a wrapper around the browser's native DOM Range. It has two aims: 2204 // - Provide workarounds for specific browser bugs 2205 // - provide convenient extensions, which are inherited from Rangy's DomRange 2206 2207 (function() { 2208 var rangeProto; 2209 var rangeProperties = DomRange.rangeProperties; 2210 2211 function updateRangeProperties(range) { 2212 var i = rangeProperties.length, prop; 2213 while (i--) { 2214 prop = rangeProperties[i]; 2215 range[prop] = range.nativeRange[prop]; 2216 } 2217 // Fix for broken collapsed property in IE 9. 2218 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); 2219 } 2220 2221 function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) { 2222 var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset); 2223 var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset); 2224 var nativeRangeDifferent = !range.equals(range.nativeRange); 2225 2226 // Always set both boundaries for the benefit of IE9 (see issue 35) 2227 if (startMoved || endMoved || nativeRangeDifferent) { 2228 range.setEnd(endContainer, endOffset); 2229 range.setStart(startContainer, startOffset); 2230 } 2231 } 2232 2233 var createBeforeAfterNodeSetter; 2234 2235 WrappedRange = function(range) { 2236 if (!range) { 2237 throw module.createError("WrappedRange: Range must be specified"); 2238 } 2239 this.nativeRange = range; 2240 updateRangeProperties(this); 2241 }; 2242 2243 DomRange.createPrototypeRange(WrappedRange, updateNativeRange); 2244 2245 rangeProto = WrappedRange.prototype; 2246 2247 rangeProto.selectNode = function(node) { 2248 this.nativeRange.selectNode(node); 2249 updateRangeProperties(this); 2250 }; 2251 2252 rangeProto.cloneContents = function() { 2253 return this.nativeRange.cloneContents(); 2254 }; 2255 2256 // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
vendor: 11,566 bytes, lines 2257-2503
2257 // insertNode() is never delegated to the native range. 2258 2259 rangeProto.surroundContents = function(node) { 2260 this.nativeRange.surroundContents(node); 2261 updateRangeProperties(this); 2262 }; 2263 2264 rangeProto.collapse = function(isStart) { 2265 this.nativeRange.collapse(isStart); 2266 updateRangeProperties(this); 2267 }; 2268 2269 rangeProto.cloneRange = function() { 2270 return new WrappedRange(this.nativeRange.cloneRange()); 2271 }; 2272 2273 rangeProto.refresh = function() { 2274 updateRangeProperties(this); 2275 }; 2276 2277 rangeProto.toString = function() { 2278 return this.nativeRange.toString(); 2279 }; 2280 2281 // Create test range and node for feature detection 2282 2283 var testTextNode = document.createTextNode("test"); 2284 getBody(document).appendChild(testTextNode); 2285 var range = document.createRange(); 2286 2287 /*--------------------------------------------------------------------------------------------------------*/ 2288 2289 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and 2290 // correct for it 2291 2292 range.setStart(testTextNode, 0); 2293 range.setEnd(testTextNode, 0); 2294 2295 try { 2296 range.setStart(testTextNode, 1); 2297 2298 rangeProto.setStart = function(node, offset) { 2299 this.nativeRange.setStart(node, offset); 2300 updateRangeProperties(this); 2301 }; 2302 2303 rangeProto.setEnd = function(node, offset) { 2304 this.nativeRange.setEnd(node, offset); 2305 updateRangeProperties(this); 2306 }; 2307 2308 createBeforeAfterNodeSetter = function(name) { 2309 return function(node) { 2310 this.nativeRange[name](node); 2311 updateRangeProperties(this); 2312 }; 2313 }; 2314 2315 } catch(ex) { 2316 2317 rangeProto.setStart = function(node, offset) { 2318 try { 2319 this.nativeRange.setStart(node, offset); 2320 } catch (ex) { 2321 this.nativeRange.setEnd(node, offset); 2322 this.nativeRange.setStart(node, offset); 2323 } 2324 updateRangeProperties(this); 2325 }; 2326 2327 rangeProto.setEnd = function(node, offset) { 2328 try { 2329 this.nativeRange.setEnd(node, offset); 2330 } catch (ex) { 2331 this.nativeRange.setStart(node, offset); 2332 this.nativeRange.setEnd(node, offset); 2333 } 2334 updateRangeProperties(this); 2335 }; 2336 2337 createBeforeAfterNodeSetter = function(name, oppositeName) { 2338 return function(node) { 2339 try { 2340 this.nativeRange[name](node); 2341 } catch (ex) { 2342 this.nativeRange[oppositeName](node); 2343 this.nativeRange[name](node); 2344 } 2345 updateRangeProperties(this); 2346 }; 2347 }; 2348 } 2349 2350 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); 2351 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); 2352 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); 2353 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); 2354 2355 /*--------------------------------------------------------------------------------------------------------*/ 2356 2357 // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing 2358 // whether the native implementation can be trusted 2359 rangeProto.selectNodeContents = function(node) { 2360 this.setStartAndEnd(node, 0, dom.getNodeLength(node)); 2361 }; 2362 2363 /*--------------------------------------------------------------------------------------------------------*/ 2364 2365 // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for 2366 // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 2367 2368 range.selectNodeContents(testTextNode); 2369 range.setEnd(testTextNode, 3); 2370 2371 var range2 = document.createRange(); 2372 range2.selectNodeContents(testTextNode); 2373 range2.setEnd(testTextNode, 4); 2374 range2.setStart(testTextNode, 2); 2375 2376 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 && 2377 range.compareBoundaryPoints(range.END_TO_START, range2) == 1) { 2378 // This is the wrong way round, so correct for it 2379 2380 rangeProto.compareBoundaryPoints = function(type, range) { 2381 range = range.nativeRange || range; 2382 if (type == range.START_TO_END) { 2383 type = range.END_TO_START; 2384 } else if (type == range.END_TO_START) { 2385 type = range.START_TO_END; 2386 } 2387 return this.nativeRange.compareBoundaryPoints(type, range); 2388 }; 2389 } else { 2390 rangeProto.compareBoundaryPoints = function(type, range) { 2391 return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range); 2392 }; 2393 } 2394 2395 /*--------------------------------------------------------------------------------------------------------*/ 2396 2397 // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107. 2398 2399 var el = document.createElement("div"); 2400 el.innerHTML = "123"; 2401 var textNode = el.firstChild; 2402 var body = getBody(document); 2403 body.appendChild(el); 2404 2405 range.setStart(textNode, 1); 2406 range.setEnd(textNode, 2); 2407 range.deleteContents(); 2408 2409 if (textNode.data == "13") { 2410 // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and 2411 // extractContents() 2412 rangeProto.deleteContents = function() { 2413 this.nativeRange.deleteContents(); 2414 updateRangeProperties(this); 2415 }; 2416 2417 rangeProto.extractContents = function() { 2418 var frag = this.nativeRange.extractContents(); 2419 updateRangeProperties(this); 2420 return frag; 2421 }; 2422 } else { 2423 } 2424 2425 body.removeChild(el); 2426 body = null; 2427 2428 /*--------------------------------------------------------------------------------------------------------*/ 2429 2430 // Test for existence of createContextualFragment and delegate to it if it exists 2431 if (util.isHostMethod(range, "createContextualFragment")) { 2432 rangeProto.createContextualFragment = function(fragmentStr) { 2433 return this.nativeRange.createContextualFragment(fragmentStr); 2434 }; 2435 } 2436 2437 /*--------------------------------------------------------------------------------------------------------*/ 2438 2439 // Clean up 2440 getBody(document).removeChild(testTextNode); 2441 2442 rangeProto.getName = function() { 2443 return "WrappedRange"; 2444 }; 2445 2446 api.WrappedRange = WrappedRange; 2447 2448 api.createNativeRange = function(doc) { 2449 doc = getContentDocument(doc, module, "createNativeRange"); 2450 return doc.createRange(); 2451 }; 2452 })(); 2453 } 2454 2455 if (api.features.implementsTextRange) { 2456 /* 2457 This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() 2458 method. For example, in the following (where pipes denote the selection boundaries): 2459 2460 <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul> 2461 2462 var range = document.selection.createRange(); 2463 alert(range.parentElement().id); // Should alert "ul" but alerts "b" 2464 2465 This method returns the common ancestor node of the following: 2466 - the parentElement() of the textRange 2467 - the parentElement() of the textRange after calling collapse(true) 2468 - the parentElement() of the textRange after calling collapse(false) 2469 */ 2470 var getTextRangeContainerElement = function(textRange) { 2471 var parentEl = textRange.parentElement(); 2472 var range = textRange.duplicate(); 2473 range.collapse(true); 2474 var startEl = range.parentElement(); 2475 range = textRange.duplicate(); 2476 range.collapse(false); 2477 var endEl = range.parentElement(); 2478 var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl); 2479 2480 return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); 2481 }; 2482 2483 var textRangeIsCollapsed = function(textRange) { 2484 return textRange.compareEndPoints("StartToEnd", textRange) == 0; 2485 }; 2486 2487 // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started 2488 // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) 2489 // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange 2490 // bugs, handling for inputs and images, plus optimizations. 2491 var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) { 2492 var workingRange = textRange.duplicate(); 2493 workingRange.collapse(isStart); 2494 var containerElement = workingRange.parentElement(); 2495 2496 // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so 2497 // check for that 2498 if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) { 2499 containerElement = wholeRangeContainerElement; 2500 } 2501 2502 2503 // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
2504 // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx 2505 if (!containerElement.canHaveHTML) { 2506 var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); 2507 return { 2508 boundaryPosition: pos, 2509 nodeInfo: { 2510 nodeIndex: pos.offset, 2511 containerElement: pos.node 2512 } 2513 }; 2514 } 2515 2516 var workingNode = dom.getDocument(containerElement).createElement("span"); 2517 2518 // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5 2519 // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64 2520 if (workingNode.parentNode) { 2521 dom.removeNode(workingNode); 2522 } 2523 2524 var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; 2525 var previousNode, nextNode, boundaryPosition, boundaryNode; 2526 var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0; 2527 var childNodeCount = containerElement.childNodes.length; 2528 var end = childNodeCount; 2529 2530 // Check end first. Code within the loop assumes that the endth child node of the container is definitely 2531 // after the range boundary. 2532 var nodeIndex = end; 2533 2534 while (true) { 2535 if (nodeIndex == childNodeCount) { 2536 containerElement.appendChild(workingNode); 2537 } else { 2538 containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]); 2539 } 2540 workingRange.moveToElementText(workingNode); 2541 comparison = workingRange.compareEndPoints(workingComparisonType, textRange); 2542 if (comparison == 0 || start == end) { 2543 break; 2544 } else if (comparison == -1) { 2545 if (end == start + 1) { 2546 // We know the endth child node is after the range boundary, so we must be done. 2547 break; 2548 } else { 2549 start = nodeIndex; 2550 } 2551 } else { 2552 end = (end == start + 1) ? start : nodeIndex; 2553 } 2554 nodeIndex = Math.floor((start + end) / 2); 2555 containerElement.removeChild(workingNode); 2556 } 2557 2558 2559 // We've now reached or gone past the boundary of the text range we're interested in 2560 // so have identified the node we want 2561 boundaryNode = workingNode.nextSibling; 2562 2563 if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) { 2564 // This is a character data node (text, comment, cdata). The working range is collapsed at the start of 2565 // the node containing the text range's boundary, so we move the end of the working range to the 2566 // boundary point and measure the length of its text to get the boundary's offset within the node. 2567 workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); 2568 2569 var offset; 2570 2571 if (/[\r\n]/.test(boundaryNode.data)) { 2572 /* 2573 For the particular case of a boundary within a text node containing rendered line breaks (within a 2574 <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in 2575 IE. The facts: 2576 2577 - Each line break is represented as \r in the text node's data/nodeValue properties 2578 - Each line break is represented as \r\n in the TextRange's 'text' property 2579 - The 'text' property of the TextRange does not contain trailing line breaks 2580 2581 To get round the problem presented by the final fact above, we can use the fact that TextRange's 2582 moveStart() and moveEnd() methods return the actual number of characters moved, which is not 2583 necessarily the same as the number of characters it was instructed to move. The simplest approach is 2584 to use this to store the characters moved when moving both the start and end of the range to the 2585 start of the document body and subtracting the start offset from the end offset (the 2586 "move-negative-gazillion" method). However, this is extremely slow when the document is large and 2587 the range is near the end of it. Clearly doing the mirror image (i.e. mov
2587ing the range boundaries to 2588 the end of the document) has the same problem. 2589 2590 Another approach that works is to use moveStart() to move the start boundary of the range up to the 2591 end boundary one character at a time and incrementing a counter with the value returned by the 2592 moveStart() call. However, the check for whether the start boundary has reached the end boundary is 2593 expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected 2594 by the location of the range within the document). 2595 2596 The approach used below is a hybrid of the two methods above. It uses the fact that a string 2597 containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot 2598 be longer than the text of the TextRange, so the start of the range is moved that length initially 2599 and then a character at a time to make up for any trailing line breaks not contained in the 'text' 2600 property. This has good performance in most situations compared to the previous two methods. 2601 */ 2602 var tempRange = workingRange.duplicate(); 2603 var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length; 2604 2605 offset = tempRange.moveStart("character", rangeLength); 2606 while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) { 2607 offset++; 2608 tempRange.moveStart("character", 1); 2609 } 2610 } else { 2611 offset = workingRange.text.length; 2612 } 2613 boundaryPosition = new DomPosition(boundaryNode, offset); 2614 } else { 2615 2616 // If the boundary immediately follows a character data node and this is the end boundary, we should favour 2617 // a position within that, and likewise for a start boundary preceding a character data node 2618 previousNode = (isCollapsed || !isStart) && workingNode.previousSibling; 2619 nextNode = (isCollapsed || isStart) && workingNode.nextSibling; 2620 if (nextNode && isCharacterDataNode(nextNode)) { 2621 boundaryPosition = new DomPosition(nextNode, 0); 2622 } else if (previousNode && isCharacterDataNode(previousNode)) { 2623 boundaryPosition = new DomPosition(previousNode, previousNode.data.length); 2624 } else { 2625 boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode)); 2626 } 2627 } 2628 2629 // Clean up 2630 dom.removeNode(workingNode); 2631 2632 return { 2633 boundaryPosition: boundaryPosition, 2634 nodeInfo: { 2635 nodeIndex: nodeIndex, 2636 containerElement: containerElement 2637 } 2638 }; 2639 }; 2640 2641 // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that 2642 // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange 2643 // (http://code.google.com/p/ierange/) 2644 var createBoundaryTextRange = function(boundaryPosition, isStart) { 2645 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset; 2646 var doc = dom.getDocument(boundaryPosition.node); 2647 var workingNode, childNodes, workingRange = getBody(doc).createTextRange(); 2648 var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node); 2649 2650 if (nodeIsDataNode) { 2651 boundaryNode = boundaryPosition.node; 2652 boundaryParent = boundaryNode.parentNode; 2653 } else { 2654 childNodes = boundaryPosition.node.childNodes; 2655 boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null; 2656 boundaryParent = boundaryPosition.node; 2657 } 2658 2659 // Position the range immediately before the node containing the boundary 2660 workingNode = doc.createElement("span"); 2661 2662 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
vendor: 6,005 bytes, lines 2663-2794
2663 // the element rather than immediately before or after it 2664 workingNode.innerHTML = "&#feff;"; 2665 2666 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report 2667 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12 2668 if (boundaryNode) { 2669 boundaryParent.insertBefore(workingNode, boundaryNode); 2670 } else { 2671 boundaryParent.appendChild(workingNode); 2672 } 2673 2674 workingRange.moveToElementText(workingNode); 2675 workingRange.collapse(!isStart); 2676 2677 // Clean up 2678 boundaryParent.removeChild(workingNode); 2679 2680 // Move the working range to the text offset, if required 2681 if (nodeIsDataNode) { 2682 workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset); 2683 } 2684 2685 return workingRange; 2686 }; 2687 2688 /*------------------------------------------------------------------------------------------------------------*/ 2689 2690 // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a 2691 // prototype 2692 2693 WrappedTextRange = function(textRange) { 2694 this.textRange = textRange; 2695 this.refresh(); 2696 }; 2697 2698 WrappedTextRange.prototype = new DomRange(document); 2699 2700 WrappedTextRange.prototype.refresh = function() { 2701 var start, end, startBoundary; 2702 2703 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that. 2704 var rangeContainerElement = getTextRangeContainerElement(this.textRange); 2705 2706 if (textRangeIsCollapsed(this.textRange)) { 2707 end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, 2708 true).boundaryPosition; 2709 } else { 2710 startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false); 2711 start = startBoundary.boundaryPosition; 2712 2713 // An optimization used here is that if the start and end boundaries have the same parent element, the 2714 // search scope for the end boundary can be limited to exclude the portion of the element that precedes 2715 // the start boundary 2716 end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false, 2717 startBoundary.nodeInfo).boundaryPosition; 2718 } 2719 2720 this.setStart(start.node, start.offset); 2721 this.setEnd(end.node, end.offset); 2722 }; 2723 2724 WrappedTextRange.prototype.getName = function() { 2725 return "WrappedTextRange"; 2726 }; 2727 2728 DomRange.copyComparisonConstants(WrappedTextRange); 2729 2730 var rangeToTextRange = function(range) { 2731 if (range.collapsed) { 2732 return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); 2733 } else { 2734 var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); 2735 var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false); 2736 var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange(); 2737 textRange.setEndPoint("StartToStart", startRange); 2738 textRange.setEndPoint("EndToEnd", endRange); 2739 return textRange; 2740 } 2741 }; 2742 2743 WrappedTextRange.rangeToTextRange = rangeToTextRange; 2744 2745 WrappedTextRange.prototype.toTextRange = function() { 2746 return rangeToTextRange(this); 2747 }; 2748 2749 api.WrappedTextRange = WrappedTextRange; 2750 2751 // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which 2752 // implementation to use by default. 2753 if (!api.features.implementsDomRange || api.config.preferTextRange) { 2754 // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work 2755 var globalObj = (function(f) { return f("return this;")(); })(Function); 2756 if (typeof globalObj.Range == "undefined") { 2757 globalObj.Range = WrappedTextRange; 2758 } 2759 2760 api.createNativeRange = function(doc) { 2761 doc = getContentDocument(doc, module, "createNativeRange"); 2762 return getBody(doc).createTextRange(); 2763 }; 2764 2765 api.WrappedRange = WrappedTextRange; 2766 } 2767 } 2768 2769 api.createRange = function(doc) { 2770 doc = getContentDocument(doc, module, "createRange"); 2771 return new api.WrappedRange(api.createNativeRange(doc)); 2772 }; 2773 2774 api.createRangyRange = function(doc) { 2775 doc = getContentDocument(doc, module, "createRangyRange"); 2776 return new DomRange(doc); 2777 }; 2778 2779 util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange"); 2780 util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange"); 2781 2782 api.addShimListener(function(win) { 2783 var doc = win.document; 2784 if (typeof doc.createRange == "undefined") { 2785 doc.createRange = function() { 2786 return api.createRange(doc); 2787 }; 2788 } 2789 doc = win = null; 2790 }); 2791 }); 2792 2793 /*----------------------------------------------------------------------------------------------------------------*/ 2794
2795 // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification 2796 // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections) 2797 api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) { 2798 api.config.checkSelectionRanges = true; 2799 2800 var BOOLEAN = "boolean"; 2801 var NUMBER = "number"; 2802 var dom = api.dom; 2803 var util = api.util; 2804 var isHostMethod = util.isHostMethod; 2805 var DomRange = api.DomRange; 2806 var WrappedRange = api.WrappedRange; 2807 var DOMException = api.DOMException; 2808 var DomPosition = dom.DomPosition; 2809 var getNativeSelection; 2810 var selectionIsCollapsed; 2811 var features = api.features; 2812 var CONTROL = "Control"; 2813 var getDocument = dom.getDocument; 2814 var getBody = dom.getBody; 2815 var rangesEqual = DomRange.rangesEqual; 2816 2817 2818 // Utility function to support direction parameters in the API that may be a string ("backward", "backwards", 2819 // "forward" or "forwards") or a Boolean (true for backwards). 2820 function isDirectionBackward(dir) { 2821 return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir; 2822 } 2823 2824 function getWindow(win, methodName) { 2825 if (!win) { 2826 return window; 2827 } else if (dom.isWindow(win)) { 2828 return win; 2829 } else if (win instanceof WrappedSelection) { 2830 return win.win; 2831 } else { 2832 var doc = dom.getContentDocument(win, module, methodName); 2833 return dom.getWindow(doc); 2834 } 2835 } 2836 2837 function getWinSelection(winParam) { 2838 return getWindow(winParam, "getWinSelection").getSelection(); 2839 } 2840 2841 function getDocSelection(winParam) { 2842 return getWindow(winParam, "getDocSelection").document.selection; 2843 } 2844 2845 function winSelectionIsBackward(sel) { 2846 var backward = false; 2847 if (sel.anchorNode) { 2848 backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1); 2849 } 2850 return backward; 2851 } 2852 2853 // Test for the Range/TextRange and Selection features required 2854 // Test for ability to retrieve selection 2855 var implementsWinGetSelection = isHostMethod(window, "getSelection"), 2856 implementsDocSelection = util.isHostObject(document, "selection"); 2857 2858 features.implementsWinGetSelection = implementsWinGetSelection; 2859 features.implementsDocSelection = implementsDocSelection; 2860 2861 var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange); 2862 2863 if (useDocumentSelection) { 2864 getNativeSelection = getDocSelection; 2865 api.isSelectionValid = function(winParam) { 2866 var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection; 2867 2868 // Check whether the selection TextRange is actually contained within the correct document 2869 return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc); 2870 }; 2871 } else if (implementsWinGetSelection) { 2872 getNativeSelection = getWinSelection; 2873 api.isSelectionValid = function() { 2874 return true; 2875 }; 2876 } else { 2877 module.fail("Neither document.selection or window.getSelection() detected."); 2878 return false; 2879 } 2880 2881 api.getNativeSelection = getNativeSelection; 2882 2883 var testSelection = getNativeSelection(); 2884 2885 // In Firefox, the selection is null in an iframe with display: none. See issue #138. 2886 if (!testSelection) { 2887 module.fail("Native selection was null (possibly issue 138?)"); 2888 return false; 2889 } 2890 2891 var testRange = api.createNativeRange(document); 2892 var body = getBody(document); 2893 2894 // Obtaining a range from a selection 2895 var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
2896 ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]); 2897 2898 features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus; 2899 2900 // Test for existence of native selection extend() method 2901 var selectionHasExtend = isHostMethod(testSelection, "extend"); 2902 features.selectionHasExtend = selectionHasExtend; 2903 2904 // Test if rangeCount exists 2905 var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER); 2906 features.selectionHasRangeCount = selectionHasRangeCount; 2907 2908 var selectionSupportsMultipleRanges = false; 2909 var collapsedNonEditableSelectionsSupported = true; 2910 2911 var addRangeBackwardToNative = selectionHasExtend ? 2912 function(nativeSelection, range) { 2913 var doc = DomRange.getRangeDocument(range); 2914 var endRange = api.createRange(doc); 2915 endRange.collapseToPoint(range.endContainer, range.endOffset); 2916 nativeSelection.addRange(getNativeRange(endRange)); 2917 nativeSelection.extend(range.startContainer, range.startOffset); 2918 } : null; 2919 2920 if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) && 2921 typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) { 2922 2923 (function() { 2924 // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are 2925 // performed on the current document's selection. See issue 109. 2926 2927 // Note also that if a selection previously existed, it is wiped and later restored by these tests. This 2928 // will result in the selection direction begin reversed if the original selection was backwards and the 2929 // browser does not support setting backwards selections (Internet Explorer, I'm looking at you). 2930 var sel = window.getSelection(); 2931 if (sel) { 2932 // Store the current selection 2933 var originalSelectionRangeCount = sel.rangeCount; 2934 var selectionHasMultipleRanges = (originalSelectionRangeCount > 1); 2935 var originalSelectionRanges = []; 2936 var originalSelectionBackward = winSelectionIsBackward(sel); 2937 for (var i = 0; i < originalSelectionRangeCount; ++i) { 2938 originalSelectionRanges[i] = sel.getRangeAt(i); 2939 } 2940 2941 // Create some test elements 2942 var testEl = dom.createTestElement(document, "", false); 2943 var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") ); 2944 2945 // Test whether the native selection will allow a collapsed selection within a non-editable element 2946 var r1 = document.createRange(); 2947 2948 r1.setStart(textNode, 1); 2949 r1.collapse(true); 2950 sel.removeAllRanges(); 2951 sel.addRange(r1); 2952 collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1); 2953 sel.removeAllRanges(); 2954 2955 // Test whether the native selection is capable of supporting multiple ranges. 2956 if (!selectionHasMultipleRanges) { 2957 // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a 2958 // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's 2959 // nothing we can do about this while retaining the feature test so we have to resort to a browser 2960 // sniff. I'm not happy about it. See 2961 // https://code.google.com/p/chromium/issues/detail?id=399791 2962 var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /); 2963 if (chromeMatch && parseInt(chromeMatch[1]) >= 36) { 2964 selectionSupportsMultipleRanges = false; 2965 } else { 2966 var r2 = r1.cloneRange(); 2967 r1.setStart(textNode, 0); 2968 r2.setEnd(textNode, 3); 2969 r2.setStart(textNode, 2); 2970 sel.addRange(r1); 2971 sel.addRange(r2); 2972 selectionSupportsMultipleRanges = (sel.rangeCount == 2); 2973 } 2974 } 2975 2976 // Clean up 2977 dom.removeNode(testEl); 2978 sel.removeAllRanges(); 2979 2980 for (i = 0; i < originalSelectionRangeCount; ++i) { 2981 if (i == 0 && originalSelectionBackward) { 2982 if (addRangeBackwardToNative) { 2983 addRangeBackwardToNative(sel, originalSelectionRanges[i]); 2984 } else { 2985 api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend"); 2986 sel.addRange(originalSelectionRanges[i]); 2987 } 2988 } else { 2989 sel.addRange(originalSelectionRanges[i]); 2990 } 2991 } 2992 } 2993 })(); 2994 } 2995 2996 features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges; 2997 features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported; 2998 2999 // ControlRanges 3000 var implementsControlRange = false, testControlRange; 3001 3002 if (body && isHostMethod(body, "createControlRange")) {
vendor: 32,474 bytes, lines 3003-3761
3003 testControlRange = body.createControlRange(); 3004 if (util.areHostProperties(testControlRange, ["item", "add"])) { 3005 implementsControlRange = true; 3006 } 3007 } 3008 features.implementsControlRange = implementsControlRange; 3009 3010 // Selection collapsedness 3011 if (selectionHasAnchorAndFocus) { 3012 selectionIsCollapsed = function(sel) { 3013 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset; 3014 }; 3015 } else { 3016 selectionIsCollapsed = function(sel) { 3017 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false; 3018 }; 3019 } 3020 3021 function updateAnchorAndFocusFromRange(sel, range, backward) { 3022 var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end"; 3023 sel.anchorNode = range[anchorPrefix + "Container"]; 3024 sel.anchorOffset = range[anchorPrefix + "Offset"]; 3025 sel.focusNode = range[focusPrefix + "Container"]; 3026 sel.focusOffset = range[focusPrefix + "Offset"]; 3027 } 3028 3029 function updateAnchorAndFocusFromNativeSelection(sel) { 3030 var nativeSel = sel.nativeSelection; 3031 sel.anchorNode = nativeSel.anchorNode; 3032 sel.anchorOffset = nativeSel.anchorOffset; 3033 sel.focusNode = nativeSel.focusNode; 3034 sel.focusOffset = nativeSel.focusOffset; 3035 } 3036 3037 function updateEmptySelection(sel) { 3038 sel.anchorNode = sel.focusNode = null; 3039 sel.anchorOffset = sel.focusOffset = 0; 3040 sel.rangeCount = 0; 3041 sel.isCollapsed = true; 3042 sel._ranges.length = 0; 3043 } 3044 3045 function getNativeRange(range) { 3046 var nativeRange; 3047 if (range instanceof DomRange) { 3048 nativeRange = api.createNativeRange(range.getDocument()); 3049 nativeRange.setEnd(range.endContainer, range.endOffset); 3050 nativeRange.setStart(range.startContainer, range.startOffset); 3051 } else if (range instanceof WrappedRange) { 3052 nativeRange = range.nativeRange; 3053 } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) { 3054 nativeRange = range; 3055 } 3056 return nativeRange; 3057 } 3058 3059 function rangeContainsSingleElement(rangeNodes) { 3060 if (!rangeNodes.length || rangeNodes[0].nodeType != 1) { 3061 return false; 3062 } 3063 for (var i = 1, len = rangeNodes.length; i < len; ++i) { 3064 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) { 3065 return false; 3066 } 3067 } 3068 return true; 3069 } 3070 3071 function getSingleElementFromRange(range) { 3072 var nodes = range.getNodes(); 3073 if (!rangeContainsSingleElement(nodes)) { 3074 throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element"); 3075 } 3076 return nodes[0]; 3077 } 3078 3079 // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange 3080 function isTextRange(range) { 3081 return !!range && typeof range.text != "undefined"; 3082 } 3083 3084 function updateFromTextRange(sel, range) { 3085 // Create a Range from the selected TextRange 3086 var wrappedRange = new WrappedRange(range); 3087 sel._ranges = [wrappedRange]; 3088 3089 updateAnchorAndFocusFromRange(sel, wrappedRange, false); 3090 sel.rangeCount = 1; 3091 sel.isCollapsed = wrappedRange.collapsed; 3092 } 3093 3094 function updateControlSelection(sel) { 3095 // Update the wrapped selection based on what's now in the native selection 3096 sel._ranges.length = 0; 3097 if (sel.docSelection.type == "None") { 3098 updateEmptySelection(sel); 3099 } else { 3100 var controlRange = sel.docSelection.createRange(); 3101 if (isTextRange(controlRange)) { 3102 // This case (where the selection type is "Control" and calling createRange() on the selection returns 3103 // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected 3104 // ControlRange have been removed from the ControlRange and removed from the document. 3105 updateFromTextRange(sel, controlRange); 3106 } else { 3107 sel.rangeCount = controlRange.length; 3108 var range, doc = getDocument(controlRange.item(0)); 3109 for (var i = 0; i < sel.rangeCount; ++i) { 3110 range = api.createRange(doc); 3111 range.selectNode(controlRange.item(i)); 3112 sel._ranges.push(range); 3113 } 3114 sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed; 3115 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false); 3116 } 3117 } 3118 } 3119 3120 function addRangeToControlSelection(sel, range) { 3121 var controlRange = sel.docSelection.createRange(); 3122 var rangeElement = getSingleElementFromRange(range); 3123 3124 // Create a new ControlRange containing all the elements in the selected ControlRange plus the element 3125 // contained by the supplied range 3126 var doc = getDocument(controlRange.item(0)); 3127 var newControlRange = getBody(doc).createControlRange(); 3128 for (var i = 0, len = controlRange.length; i < len; ++i) { 3129 newControlRange.add(controlRange.item(i)); 3130 } 3131 try { 3132 newControlRange.add(rangeElement); 3133 } catch (ex) { 3134 throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)"); 3135 } 3136 newControlRange.select(); 3137 3138 // Update the wrapped selection based on what's now in the native selection 3139 updateControlSelection(sel); 3140 } 3141 3142 var getSelectionRangeAt; 3143 3144 if (isHostMethod(testSelection, "getRangeAt")) { 3145 // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation. 3146 // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a 3147 // lesson to us all, especially me. 3148 getSelectionRangeAt = function(sel, index) { 3149 try { 3150 return sel.getRangeAt(index); 3151 } catch (ex) { 3152 return null; 3153 } 3154 }; 3155 } else if (selectionHasAnchorAndFocus) { 3156 getSelectionRangeAt = function(sel) { 3157 var doc = getDocument(sel.anchorNode); 3158 var range = api.createRange(doc); 3159 range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset); 3160 3161 // Handle the case when the selection was selected backwards (from the end to the start in the 3162 // document) 3163 if (range.collapsed !== this.isCollapsed) { 3164 range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset); 3165 } 3166 3167 return range; 3168 }; 3169 } 3170 3171 function WrappedSelection(selection, docSelection, win) { 3172 this.nativeSelection = selection; 3173 this.docSelection = docSelection; 3174 this._ranges = []; 3175 this.win = win; 3176 this.refresh(); 3177 } 3178 3179 WrappedSelection.prototype = api.selectionPrototype; 3180 3181 function deleteProperties(sel) { 3182 sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null; 3183 sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0; 3184 sel.detached = true; 3185 } 3186 3187 var cachedRangySelections = []; 3188 3189 function actOnCachedSelection(win, action) { 3190 var i = cachedRangySelections.length, cached, sel; 3191 while (i--) { 3192 cached = cachedRangySelections[i]; 3193 sel = cached.selection; 3194 if (action == "deleteAll") { 3195 deleteProperties(sel); 3196 } else if (cached.win == win) { 3197 if (action == "delete") { 3198 cachedRangySelections.splice(i, 1); 3199 return true; 3200 } else { 3201 return sel; 3202 } 3203 } 3204 } 3205 if (action == "deleteAll") { 3206 cachedRangySelections.length = 0; 3207 } 3208 return null; 3209 } 3210 3211 var getSelection = function(win) { 3212 // Check if the parameter is a Rangy Selection object 3213 if (win && win instanceof WrappedSelection) { 3214 win.refresh(); 3215 return win; 3216 } 3217 3218 win = getWindow(win, "getNativeSelection"); 3219 3220 var sel = actOnCachedSelection(win); 3221 var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null; 3222 if (sel) { 3223 sel.nativeSelection = nativeSel; 3224 sel.docSelection = docSel; 3225 sel.refresh(); 3226 } else { 3227 sel = new WrappedSelection(nativeSel, docSel, win); 3228 cachedRangySelections.push( { win: win, selection: sel } ); 3229 } 3230 return sel; 3231 }; 3232 3233 api.getSelection = getSelection; 3234 3235 util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection"); 3236 3237 var selProto = WrappedSelection.prototype; 3238 3239 function createControlSelection(sel, ranges) { 3240 // Ensure that the selection becomes of type "Control" 3241 var doc = getDocument(ranges[0].startContainer); 3242 var controlRange = getBody(doc).createControlRange(); 3243 for (var i = 0, el, len = ranges.length; i < len; ++i) { 3244 el = getSingleElementFromRange(ranges[i]); 3245 try { 3246 controlRange.add(el); 3247 } catch (ex) { 3248 throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)"); 3249 } 3250 } 3251 controlRange.select(); 3252 3253 // Update the wrapped selection based on what's now in the native selection 3254 updateControlSelection(sel); 3255 } 3256 3257 // Selecting a range 3258 if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) { 3259 selProto.removeAllRanges = function() { 3260 this.nativeSelection.removeAllRanges(); 3261 updateEmptySelection(this); 3262 }; 3263 3264 var addRangeBackward = function(sel, range) { 3265 addRangeBackwardToNative(sel.nativeSelection, range); 3266 sel.refresh(); 3267 }; 3268 3269 if (selectionHasRangeCount) { 3270 selProto.addRange = function(range, direction) { 3271 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { 3272 addRangeToControlSelection(this, range); 3273 } else { 3274 if (isDirectionBackward(direction) && selectionHasExtend) { 3275 addRangeBackward(this, range); 3276 } else { 3277 var previousRangeCount; 3278 if (selectionSupportsMultipleRanges) { 3279 previousRangeCount = this.rangeCount; 3280 } else { 3281 this.removeAllRanges(); 3282 previousRangeCount = 0; 3283 } 3284 // Clone the native range so that changing the selected range does not affect the selection. 3285 // This is contrary to the spec but is the only way to achieve consistency between browsers. See 3286 // issue 80. 3287 var clonedNativeRange = getNativeRange(range).cloneRange(); 3288 try { 3289 this.nativeSelection.addRange(clonedNativeRange); 3290 } catch (ex) { 3291 } 3292 3293 // Check whether adding the range was successful 3294 this.rangeCount = this.nativeSelection.rangeCount; 3295 3296 if (this.rangeCount == previousRangeCount + 1) { 3297 // The range was added successfully 3298 3299 // Check whether the range that we added to the selection is reflected in the last range extracted from 3300 // the selection 3301 if (api.config.checkSelectionRanges) { 3302 var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1); 3303 if (nativeRange && !rangesEqual(nativeRange, range)) { 3304 // Happens in WebKit with, for example, a selection placed at the start of a text node 3305 range = new WrappedRange(nativeRange); 3306 } 3307 } 3308 this._ranges[this.rangeCount - 1] = range; 3309 updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection)); 3310 this.isCollapsed = selectionIsCollapsed(this); 3311 } else { 3312 // The range was not added successfully. The simplest thing is to refresh 3313 this.refresh(); 3314 } 3315 } 3316 } 3317 }; 3318 } else { 3319 selProto.addRange = function(range, direction) { 3320 if (isDirectionBackward(direction) && selectionHasExtend) { 3321 addRangeBackward(this, range); 3322 } else { 3323 this.nativeSelection.addRange(getNativeRange(range)); 3324 this.refresh(); 3325 } 3326 }; 3327 } 3328 3329 selProto.setRanges = function(ranges) { 3330 if (implementsControlRange && implementsDocSelection && ranges.length > 1) { 3331 createControlSelection(this, ranges); 3332 } else { 3333 this.removeAllRanges(); 3334 for (var i = 0, len = ranges.length; i < len; ++i) { 3335 this.addRange(ranges[i]); 3336 } 3337 } 3338 }; 3339 } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") && 3340 implementsControlRange && useDocumentSelection) { 3341 3342 selProto.removeAllRanges = function() { 3343 // Added try/catch as fix for issue #21 3344 try { 3345 this.docSelection.empty(); 3346 3347 // Check for empty() not working (issue #24) 3348 if (this.docSelection.type != "None") { 3349 // Work around failure to empty a control selection by instead selecting a TextRange and then 3350 // calling empty() 3351 var doc; 3352 if (this.anchorNode) { 3353 doc = getDocument(this.anchorNode); 3354 } else if (this.docSelection.type == CONTROL) { 3355 var controlRange = this.docSelection.createRange(); 3356 if (controlRange.length) { 3357 doc = getDocument( controlRange.item(0) ); 3358 } 3359 } 3360 if (doc) { 3361 var textRange = getBody(doc).createTextRange(); 3362 textRange.select(); 3363 this.docSelection.empty(); 3364 } 3365 } 3366 } catch(ex) {} 3367 updateEmptySelection(this); 3368 }; 3369 3370 selProto.addRange = function(range) { 3371 if (this.docSelection.type == CONTROL) { 3372 addRangeToControlSelection(this, range); 3373 } else { 3374 api.WrappedTextRange.rangeToTextRange(range).select(); 3375 this._ranges[0] = range; 3376 this.rangeCount = 1; 3377 this.isCollapsed = this._ranges[0].collapsed; 3378 updateAnchorAndFocusFromRange(this, range, false); 3379 } 3380 }; 3381 3382 selProto.setRanges = function(ranges) { 3383 this.removeAllRanges(); 3384 var rangeCount = ranges.length; 3385 if (rangeCount > 1) { 3386 createControlSelection(this, ranges); 3387 } else if (rangeCount) { 3388 this.addRange(ranges[0]); 3389 } 3390 }; 3391 } else { 3392 module.fail("No means of selecting a Range or TextRange was found"); 3393 return false; 3394 } 3395 3396 selProto.getRangeAt = function(index) { 3397 if (index < 0 || index >= this.rangeCount) { 3398 throw new DOMException("INDEX_SIZE_ERR"); 3399 } else { 3400 // Clone the range to preserve selection-range independence. See issue 80. 3401 return this._ranges[index].cloneRange(); 3402 } 3403 }; 3404 3405 var refreshSelection; 3406 3407 if (useDocumentSelection) { 3408 refreshSelection = function(sel) { 3409 var range; 3410 if (api.isSelectionValid(sel.win)) { 3411 range = sel.docSelection.createRange(); 3412 } else { 3413 range = getBody(sel.win.document).createTextRange(); 3414 range.collapse(true); 3415 } 3416 3417 if (sel.docSelection.type == CONTROL) { 3418 updateControlSelection(sel); 3419 } else if (isTextRange(range)) { 3420 updateFromTextRange(sel, range); 3421 } else { 3422 updateEmptySelection(sel); 3423 } 3424 }; 3425 } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) { 3426 refreshSelection = function(sel) { 3427 if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) { 3428 updateControlSelection(sel); 3429 } else { 3430 sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount; 3431 if (sel.rangeCount) { 3432 for (var i = 0, len = sel.rangeCount; i < len; ++i) { 3433 sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i)); 3434 } 3435 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection)); 3436 sel.isCollapsed = selectionIsCollapsed(sel); 3437 } else { 3438 updateEmptySelection(sel); 3439 } 3440 } 3441 }; 3442 } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) { 3443 refreshSelection = function(sel) { 3444 var range, nativeSel = sel.nativeSelection; 3445 if (nativeSel.anchorNode) { 3446 range = getSelectionRangeAt(nativeSel, 0); 3447 sel._ranges = [range]; 3448 sel.rangeCount = 1; 3449 updateAnchorAndFocusFromNativeSelection(sel); 3450 sel.isCollapsed = selectionIsCollapsed(sel); 3451 } else { 3452 updateEmptySelection(sel); 3453 } 3454 }; 3455 } else { 3456 module.fail("No means of obtaining a Range or TextRange from the user's selection was found"); 3457 return false; 3458 } 3459 3460 selProto.refresh = function(checkForChanges) { 3461 var oldRanges = checkForChanges ? this._ranges.slice(0) : null; 3462 var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset; 3463 3464 refreshSelection(this); 3465 if (checkForChanges) { 3466 // Check the range count first 3467 var i = oldRanges.length; 3468 if (i != this._ranges.length) { 3469 return true; 3470 } 3471 3472 // Now check the direction. Checking the anchor position is the same is enough since we're checking all the 3473 // ranges after this 3474 if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) { 3475 return true; 3476 } 3477 3478 // Finally, compare each range in turn 3479 while (i--) { 3480 if (!rangesEqual(oldRanges[i], this._ranges[i])) { 3481 return true; 3482 } 3483 } 3484 return false; 3485 } 3486 }; 3487 3488 // Removal of a single range 3489 var removeRangeManually = function(sel, range) { 3490 var ranges = sel.getAllRanges(); 3491 sel.removeAllRanges(); 3492 for (var i = 0, len = ranges.length; i < len; ++i) { 3493 if (!rangesEqual(range, ranges[i])) { 3494 sel.addRange(ranges[i]); 3495 } 3496 } 3497 if (!sel.rangeCount) { 3498 updateEmptySelection(sel); 3499 } 3500 }; 3501 3502 if (implementsControlRange && implementsDocSelection) { 3503 selProto.removeRange = function(range) { 3504 if (this.docSelection.type == CONTROL) { 3505 var controlRange = this.docSelection.createRange(); 3506 var rangeElement = getSingleElementFromRange(range); 3507 3508 // Create a new ControlRange containing all the elements in the selected ControlRange minus the 3509 // element contained by the supplied range 3510 var doc = getDocument(controlRange.item(0)); 3511 var newControlRange = getBody(doc).createControlRange(); 3512 var el, removed = false; 3513 for (var i = 0, len = controlRange.length; i < len; ++i) { 3514 el = controlRange.item(i); 3515 if (el !== rangeElement || removed) { 3516 newControlRange.add(controlRange.item(i)); 3517 } else { 3518 removed = true; 3519 } 3520 } 3521 newControlRange.select(); 3522 3523 // Update the wrapped selection based on what's now in the native selection 3524 updateControlSelection(this); 3525 } else { 3526 removeRangeManually(this, range); 3527 } 3528 }; 3529 } else { 3530 selProto.removeRange = function(range) { 3531 removeRangeManually(this, range); 3532 }; 3533 } 3534 3535 // Detecting if a selection is backward 3536 var selectionIsBackward; 3537 if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) { 3538 selectionIsBackward = winSelectionIsBackward; 3539 3540 selProto.isBackward = function() { 3541 return selectionIsBackward(this); 3542 }; 3543 } else { 3544 selectionIsBackward = selProto.isBackward = function() { 3545 return false; 3546 }; 3547 } 3548 3549 // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards" 3550 selProto.isBackwards = selProto.isBackward; 3551 3552 // Selection stringifier 3553 // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation. 3554 // The current spec does not yet define this method. 3555 selProto.toString = function() { 3556 var rangeTexts = []; 3557 for (var i = 0, len = this.rangeCount; i < len; ++i) { 3558 rangeTexts[i] = "" + this._ranges[i]; 3559 } 3560 return rangeTexts.join(""); 3561 }; 3562 3563 function assertNodeInSameDocument(sel, node) { 3564 if (sel.win.document != getDocument(node)) { 3565 throw new DOMException("WRONG_DOCUMENT_ERR"); 3566 } 3567 } 3568 3569 // No current browser conforms fully to the spec for this method, so Rangy's own method is always used 3570 selProto.collapse = function(node, offset) { 3571 assertNodeInSameDocument(this, node); 3572 var range = api.createRange(node); 3573 range.collapseToPoint(node, offset); 3574 this.setSingleRange(range); 3575 this.isCollapsed = true; 3576 }; 3577 3578 selProto.collapseToStart = function() { 3579 if (this.rangeCount) { 3580 var range = this._ranges[0]; 3581 this.collapse(range.startContainer, range.startOffset); 3582 } else { 3583 throw new DOMException("INVALID_STATE_ERR"); 3584 } 3585 }; 3586 3587 selProto.collapseToEnd = function() { 3588 if (this.rangeCount) { 3589 var range = this._ranges[this.rangeCount - 1]; 3590 this.collapse(range.endContainer, range.endOffset); 3591 } else { 3592 throw new DOMException("INVALID_STATE_ERR"); 3593 } 3594 }; 3595 3596 // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as 3597 // specified so the native implementation is never used by Rangy. 3598 selProto.selectAllChildren = function(node) { 3599 assertNodeInSameDocument(this, node); 3600 var range = api.createRange(node); 3601 range.selectNodeContents(node); 3602 this.setSingleRange(range); 3603 }; 3604 3605 selProto.deleteFromDocument = function() { 3606 // Sepcial behaviour required for IE's control selections 3607 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { 3608 var controlRange = this.docSelection.createRange(); 3609 var element; 3610 while (controlRange.length) { 3611 element = controlRange.item(0); 3612 controlRange.remove(element); 3613 dom.removeNode(element); 3614 } 3615 this.refresh(); 3616 } else if (this.rangeCount) { 3617 var ranges = this.getAllRanges(); 3618 if (ranges.length) { 3619 this.removeAllRanges(); 3620 for (var i = 0, len = ranges.length; i < len; ++i) { 3621 ranges[i].deleteContents(); 3622 } 3623 // The spec says nothing about what the selection should contain after calling deleteContents on each 3624 // range. Firefox moves the selection to where the final selected range was, so we emulate that 3625 this.addRange(ranges[len - 1]); 3626 } 3627 } 3628 }; 3629 3630 // The following are non-standard extensions 3631 selProto.eachRange = function(func, returnValue) { 3632 for (var i = 0, len = this._ranges.length; i < len; ++i) { 3633 if ( func( this.getRangeAt(i) ) ) { 3634 return returnValue; 3635 } 3636 } 3637 }; 3638 3639 selProto.getAllRanges = function() { 3640 var ranges = []; 3641 this.eachRange(function(range) { 3642 ranges.push(range); 3643 }); 3644 return ranges; 3645 }; 3646 3647 selProto.setSingleRange = function(range, direction) { 3648 this.removeAllRanges(); 3649 this.addRange(range, direction); 3650 }; 3651 3652 selProto.callMethodOnEachRange = function(methodName, params) { 3653 var results = []; 3654 this.eachRange( function(range) { 3655 results.push( range[methodName].apply(range, params || []) ); 3656 } ); 3657 return results; 3658 }; 3659 3660 function createStartOrEndSetter(isStart) { 3661 return function(node, offset) { 3662 var range; 3663 if (this.rangeCount) { 3664 range = this.getRangeAt(0); 3665 range["set" + (isStart ? "Start" : "End")](node, offset); 3666 } else { 3667 range = api.createRange(this.win.document); 3668 range.setStartAndEnd(node, offset); 3669 } 3670 this.setSingleRange(range, this.isBackward()); 3671 }; 3672 } 3673 3674 selProto.setStart = createStartOrEndSetter(true); 3675 selProto.setEnd = createStartOrEndSetter(false); 3676 3677 // Add select() method to Range prototype. Any existing selection will be removed. 3678 api.rangePrototype.select = function(direction) { 3679 getSelection( this.getDocument() ).setSingleRange(this, direction); 3680 }; 3681 3682 selProto.changeEachRange = function(func) { 3683 var ranges = []; 3684 var backward = this.isBackward(); 3685 3686 this.eachRange(function(range) { 3687 func(range); 3688 ranges.push(range); 3689 }); 3690 3691 this.removeAllRanges(); 3692 if (backward && ranges.length == 1) { 3693 this.addRange(ranges[0], "backward"); 3694 } else { 3695 this.setRanges(ranges); 3696 } 3697 }; 3698 3699 selProto.containsNode = function(node, allowPartial) { 3700 return this.eachRange( function(range) { 3701 return range.containsNode(node, allowPartial); 3702 }, true ) || false; 3703 }; 3704 3705 selProto.getBookmark = function(containerNode) { 3706 return { 3707 backward: this.isBackward(), 3708 rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode]) 3709 }; 3710 }; 3711 3712 selProto.moveToBookmark = function(bookmark) { 3713 var selRanges = []; 3714 for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) { 3715 range = api.createRange(this.win); 3716 range.moveToBookmark(rangeBookmark); 3717 selRanges.push(range); 3718 } 3719 if (bookmark.backward) { 3720 this.setSingleRange(selRanges[0], "backward"); 3721 } else { 3722 this.setRanges(selRanges); 3723 } 3724 }; 3725 3726 selProto.saveRanges = function() { 3727 return { 3728 backward: this.isBackward(), 3729 ranges: this.callMethodOnEachRange("cloneRange") 3730 }; 3731 }; 3732 3733 selProto.restoreRanges = function(selRanges) { 3734 this.removeAllRanges(); 3735 for (var i = 0, range; range = selRanges.ranges[i]; ++i) { 3736 this.addRange(range, (selRanges.backward && i == 0)); 3737 } 3738 }; 3739 3740 selProto.toHtml = function() { 3741 var rangeHtmls = []; 3742 this.eachRange(function(range) { 3743 rangeHtmls.push( DomRange.toHtml(range) ); 3744 }); 3745 return rangeHtmls.join(""); 3746 }; 3747 3748 if (features.implementsTextRange) { 3749 selProto.getNativeTextRange = function() { 3750 var sel, textRange; 3751 if ( (sel = this.docSelection) ) { 3752 var range = sel.createRange(); 3753 if (isTextRange(range)) { 3754 return range; 3755 } else { 3756 throw module.createError("getNativeTextRange: selection is a control selection"); 3757 } 3758 } else if (this.rangeCount > 0) { 3759 return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) ); 3760 } else { 3761 throw module.createError("getNativeTextRange: selection contains
3761no range"); 3762 } 3763 }; 3764 } 3765 3766 function inspect(sel) { 3767 var rangeInspects = []; 3768 var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset); 3769 var focus = new DomPosition(sel.focusNode, sel.focusOffset); 3770 var name = (typeof sel.getName == "function") ? sel.getName() : "Selection"; 3771 3772 if (typeof sel.rangeCount != "undefined") { 3773 for (var i = 0, len = sel.rangeCount; i < len; ++i) { 3774 rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i)); 3775 } 3776 } 3777 return "[" + name + "(Ranges: " + rangeInspects.join(", ") + 3778 ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]"; 3779 } 3780 3781 selProto.getName = function() { 3782 return "WrappedSelection"; 3783 }; 3784 3785 selProto.inspect = function() { 3786 return inspect(this); 3787 }; 3788 3789 selProto.detach = function() { 3790 actOnCachedSelection(this.win, "delete"); 3791 deleteProperties(this); 3792 }; 3793 3794 WrappedSelection.detachAll = function() { 3795 actOnCachedSelection(null, "deleteAll"); 3796 }; 3797 3798 WrappedSelection.inspect = inspect; 3799 WrappedSelection.isDirectionBackward = isDirectionBackward; 3800 3801 api.Selection = WrappedSelection; 3802 3803 api.selectionPrototype = selProto; 3804 3805 api.addShimListener(function(win) { 3806 if (typeof win.getSelection == "undefined") { 3807 win.getSelection = function() { 3808 return getSelection(win); 3809 }; 3810 } 3811 win = null; 3812 }); 3813 }); 3814 3815 3816 /*----------------------------------------------------------------------------------------------------------------*/ 3817 3818 // Wait for document to load before initializing 3819 var docReady = false; 3820 3821 var loadHandler = function(e) { 3822 if (!docReady) { 3823 docReady = true; 3824 if (!api.initialized && api.config.autoInitialize) { 3825 init(); 3826 } 3827 } 3828 }; 3829 3830 if (isBrowser) { 3831 // Test whether the document has already been loaded and initialize immediately if so 3832 if (document.readyState == "complete") { 3833 loadHandler(); 3834 } else { 3835 if (isHostMethod(document, "addEventListener")) { 3836 document.addEventListener("DOMContentLoaded", loadHandler, false); 3837 } 3838 3839 // Add a fallback in case the DOMContentLoaded event isn't supported 3840 addListener(window, "load", loadHandler); 3841 } 3842 } 3843 3844 return api; 3845}, this); 3846 3847 3848// 3849 3850/** 3851 * Selection save and restore module for Rangy. 3852 * Saves and restores user selections using marker invisible elements in the DOM. 3853 * 3854 * Part of Rangy, a cross-browser JavaScript range and selection library 3855 * https://github.com/timdown/rangy 3856 * 3857 * Depends on Rangy core. 3858 * 3859 * Copyright 2015, Tim Down 3860 * Licensed under the MIT license. 3861 * Version: 1.3.1-dev 3862 * Build date: 20 May 2015 3863 */ 3864 (function(factory, root) { 3865 if (typeof define == "function" && define.amd) { 3866 // AMD. Register as an anonymous module with a dependency on Rangy. 3867 define(["./rangy-core"], factory); 3868 } else if (typeof module != "undefined" && typeof exports == "object") { 3869 // Node/CommonJS style 3870 module.exports = factory( require("rangy") ); 3871 } else { 3872 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 3873 factory(root.rangy); 3874 } 3875})(function(rangy) { 3876 rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) { 3877 var dom = api.dom; 3878 var removeNode = dom.removeNode; 3879 var isDirectionBackward = api.Selection.isDirectionBackward; 3880 var markerTextChar = "\ufeff"; 3881 3882 function gEBI(id, doc) { 3883 return (doc || document).getElementById(id); 3884 } 3885 3886 function insertRangeBoundaryMarker(range, atStart) { 3887 var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2); 3888 var markerEl; 3889 var doc = dom.getDocument(range.startContainer); 3890 3891 // Clone the Range and collapse to the appropriate boundary point 3892 var boundaryRange = range.cloneRange(); 3893 boundaryRange.collapse(atStart); 3894 3895 // Create the marker element containing a single invisible character using DOM methods and insert it 3896 markerEl = doc.createElement("span"); 3897 markerEl.id = markerId; 3898 markerEl.style.lineHeight = "0"; 3899 markerEl.style.display = "none"; 3900 markerEl.className = "rangySelectionBoundary"; 3901 markerEl.appendChild(doc.createTextNode(markerTextChar)); 3902
3903 boundaryRange.insertNode(markerEl); 3904 return markerEl; 3905 } 3906 3907 function setRangeBoundary(doc, range, markerId, atStart) { 3908 var markerEl = gEBI(markerId, doc); 3909 if (markerEl) { 3910 range[atStart ? "setStartBefore" : "setEndBefore"](markerEl); 3911 removeNode(markerEl); 3912 } else { 3913 module.warn("Marker element has been removed. Cannot restore selection."); 3914 } 3915 } 3916 3917 function compareRanges(r1, r2) { 3918 return r2.compareBoundaryPoints(r1.START_TO_START, r1); 3919 } 3920 3921 function saveRange(range, direction) { 3922 var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString(); 3923 var backward = isDirectionBackward(direction); 3924 3925 if (range.collapsed) { 3926 endEl = insertRangeBoundaryMarker(range, false); 3927 return { 3928 document: doc, 3929 markerId: endEl.id, 3930 collapsed: true 3931 }; 3932 } else { 3933 endEl = insertRangeBoundaryMarker(range, false); 3934 startEl = insertRangeBoundaryMarker(range, true); 3935 3936 return { 3937 document: doc, 3938 startMarkerId: startEl.id, 3939 endMarkerId: endEl.id, 3940 collapsed: false, 3941 backward: backward, 3942 toString: function() { 3943 return "original text: '" + text + "', new text: '" + range.toString() + "'"; 3944 } 3945 }; 3946 } 3947 } 3948 3949 function restoreRange(rangeInfo, normalize) { 3950 var doc = rangeInfo.document; 3951 if (typeof normalize == "undefined") { 3952 normalize = true; 3953 } 3954 var range = api.createRange(doc); 3955 if (rangeInfo.collapsed) { 3956 var markerEl = gEBI(rangeInfo.markerId, doc); 3957 if (markerEl) { 3958 markerEl.style.display = "inline"; 3959 var previousNode = markerEl.previousSibling; 3960 3961 // Workaround for issue 17 3962 if (previousNode && previousNode.nodeType == 3) { 3963 removeNode(markerEl); 3964 range.collapseToPoint(previousNode, previousNode.length); 3965 } else { 3966 range.collapseBefore(markerEl); 3967 removeNode(markerEl); 3968 } 3969 } else { 3970 module.warn("Marker element has been removed. Cannot restore selection."); 3971 } 3972 } else { 3973 setRangeBoundary(doc, range, rangeInfo.startMarkerId, true); 3974 setRangeBoundary(doc, range, rangeInfo.endMarkerId, false); 3975 } 3976 3977 if (normalize) { 3978 range.normalizeBoundaries(); 3979 } 3980 3981 return range; 3982 } 3983 3984 function saveRanges(ranges, direction) { 3985 var rangeInfos = [], range, doc; 3986 var backward = isDirectionBackward(direction); 3987 3988 // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched 3989 ranges = ranges.slice(0); 3990 ranges.sort(compareRanges); 3991 3992 for (var i = 0, len = ranges.length; i < len; ++i) { 3993 rangeInfos[i] = saveRange(ranges[i], backward); 3994 } 3995 3996 // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie 3997 // between its markers 3998 for (i = len - 1; i >= 0; --i) { 3999 range = ranges[i]; 4000 doc = api.DomRange.getRangeDocument(range); 4001 if (range.collapsed) { 4002 range.collapseAfter(gEBI(rangeInfos[i].markerId, doc)); 4003 } else { 4004 range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc)); 4005 range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc)); 4006 } 4007 } 4008 4009 return rangeInfos; 4010 } 4011 4012 function saveSelection(win) { 4013 if (!api.isSelectionValid(win)) { 4014 module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus."); 4015 return null; 4016 } 4017 var sel = api.getSelection(win); 4018 var ranges = sel.getAllRanges(); 4019 var backward = (ranges.length == 1 && sel.isBackward()); 4020 4021 var rangeInfos = saveRanges(ranges, backward); 4022 4023 // Ensure current selection is unaffected 4024 if (backward) { 4025 sel.setSingleRange(ranges[0], backward); 4026 } else { 4027 sel.setRanges(ranges); 4028 } 4029 4030 return { 4031 win: win, 4032 rangeInfos: rangeInfos, 4033 restored: false 4034 }; 4035 } 4036 4037 function restoreRanges(rangeInfos) { 4038 var ranges = []; 4039 4040 // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid 4041 // normalization affecting previously restored ranges. 4042 var rangeCount = rangeInfos.length; 4043 4044 for (var i = rangeCount - 1; i >= 0; i--) { 4045 ranges[i] = restoreRange(rangeInfos[i], true); 4046 } 4047 4048 return ranges; 4049 } 4050 4051 function restoreSelection(savedSelection, preserveDirection) { 4052 if (!savedSelection.restored) { 4053 var rangeInfos = savedSelection.rangeInfos; 4054 var sel = api.getSelection(savedSelection.win); 4055 var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length; 4056 4057 if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) { 4058 sel.removeAllRanges(); 4059 sel.addRange(ranges[0], true); 4060 } else { 4061 sel.setRanges(ranges); 4062 } 4063 4064 savedSelection.restored = true; 4065 } 4066 } 4067 4068 function removeMarkerElement(doc, markerId) { 4069 var markerEl = gEBI(markerId, doc); 4070 if (markerEl) { 4071 removeNode(markerEl); 4072 } 4073 } 4074 4075 function removeMarkers(savedSelection) { 4076 var rangeInfos = savedSelection.rangeInfos; 4077 for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) { 4078 rangeInfo = rangeInfos[i]; 4079 if (rangeInfo.collapsed) { 4080 removeMarkerElement(savedSelection.doc, rangeInfo.markerId); 4081 } else { 4082 removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId); 4083 removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId); 4084 } 4085 } 4086 } 4087 4088 api.util.extend(api, { 4089 saveRange: saveRange, 4090 restoreRange: restoreRange, 4091 saveRanges: saveRanges, 4092 restoreRanges: restoreRanges, 4093 saveSelection: saveSelection, 4094 restoreSelection: restoreSelection, 4095 removeMarkerElement: removeMarkerElement, 4096 removeMarkers: removeMarkers 4097 }); 4098 }); 4099 4100 return rangy; 4101}, this); 4102 4103 4104 4105/** 4106 * Serializer module for Rangy. 4107 * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a 4108 * cookie or local storage and restore it on the user's next visit to the same page. 4109 * 4110 * Part of Rangy, a cross-browser JavaScript range and selection library 4111 * https://github.com/timdown/rangy 4112 * 4113 * Depends on Rangy core. 4114 * 4115 * Copyright 2015, Tim Down 4116 * Licensed under the MIT license. 4117 * Version: 1.3.1-dev 4118 * Build date: 20 May 2015 4119 */ 4120 (function(factory, root) { 4121 if (typeof define == "function" && define.amd) { 4122 // AMD. Register as an anonymous module with a dependency on Rangy. 4123 define(["./rangy-core"], factory); 4124 } else if (typeof module != "undefined" && typeof exports == "object") { 4125 // Node/CommonJS style 4126 module.exports = factory( require("rangy") ); 4127 } else { 4128 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 4129 factory(root.rangy); 4130 } 4131})(function(rangy) { 4132 rangy.createModule("Serializer", ["WrappedSelection"], function(api, module) { 4133 var UNDEF = "undefined"; 4134 var util = api.util; 4135 4136 // encodeURIComponent and decodeURIComponent are required for cookie handling 4137 if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) { 4138 module.fail("encodeURIComponent and/or decodeURIComponent method is missing"); 4139 } 4140 4141 // Checksum for checking whether range can be serialized 4142 var crc32 = (function() { 4143 function utf8encode(str) {
4144 var utf8CharCodes = []; 4145 4146 for (var i = 0, len = str.length, c; i < len; ++i) { 4147 c = str.charCodeAt(i); 4148 if (c < 128) { 4149 utf8CharCodes.push(c); 4150 } else if (c < 2048) { 4151 utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128); 4152 } else { 4153 utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128); 4154 } 4155 } 4156 return utf8CharCodes; 4157 } 4158 4159 var cachedCrcTable = null; 4160 4161 function buildCRCTable() { 4162 var table = []; 4163 for (var i = 0, j, crc; i < 256; ++i) { 4164 crc = i; 4165 j = 8; 4166 while (j--) { 4167 if ((crc & 1) == 1) { 4168 crc = (crc >>> 1) ^ 0xEDB88320; 4169 } else { 4170 crc >>>= 1; 4171 } 4172 } 4173 table[i] = crc >>> 0; 4174 } 4175 return table; 4176 } 4177 4178 function getCrcTable() { 4179 if (!cachedCrcTable) { 4180 cachedCrcTable = buildCRCTable(); 4181 } 4182 return cachedCrcTable; 4183 } 4184 4185 return function(str) { 4186 var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable(); 4187 for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) { 4188 y = (crc ^ utf8CharCodes[i]) & 0xFF; 4189 crc = (crc >>> 8) ^ crcTable[y]; 4190 } 4191 return (crc ^ -1) >>> 0; 4192 }; 4193 })(); 4194 4195 var dom = api.dom; 4196 4197 function escapeTextForHtml(str) { 4198 return str.replace(/</g, "<").replace(/>/g, ">"); 4199 } 4200 4201 function nodeToInfoString(node, infoParts) { 4202 infoParts = infoParts || []; 4203 var nodeType = node.nodeType, children = node.childNodes, childCount = children.length; 4204 var nodeInfo = [nodeType, node.nodeName, childCount].join(":"); 4205 var start = "", end = ""; 4206 switch (nodeType) { 4207 case 3: // Text node 4208 start = escapeTextForHtml(node.nodeValue); 4209 break; 4210 case 8: // Comment 4211 start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->"; 4212 break; 4213 default: 4214 start = "<" + nodeInfo + ">"; 4215 end = "</>"; 4216 break; 4217 } 4218 if (start) { 4219 infoParts.push(start); 4220 } 4221 for (var i = 0; i < childCount; ++i) { 4222 nodeToInfoString(children[i], infoParts); 4223 } 4224 if (end) { 4225 infoParts.push(end); 4226 } 4227 return infoParts; 4228 } 4229 4230 // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all 4231 // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around 4232 // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's 4233 // innerHTML whenever the user changes an input within the element. 4234 function getElementChecksum(el) { 4235 var info = nodeToInfoString(el).join(""); 4236 return crc32(info).toString(16); 4237 } 4238 4239 function serializePosition(node, offset, rootNode) { 4240 var pathParts = [], n = node; 4241 rootNode = rootNode || dom.getDocument(node).documentElement; 4242 while (n && n != rootNode) { 4243 pathParts.push(dom.getNodeIndex(n, true)); 4244 n = n.parentNode; 4245 } 4246 return pathParts.join("/") + ":" + offset; 4247 } 4248 4249 function deserializePosition(serialized, rootNode, doc) { 4250 if (!rootNode) { 4251 rootNode = (doc || document).documentElement; 4252 } 4253 var parts = serialized.split(":"); 4254 var node = rootNode; 4255 var nodeIndices = parts[0] ? parts[0].split("/") : [], i = nodeIndices.length, nodeIndex; 4256 4257 while (i--) { 4258 nodeIndex = parseInt(nodeIndices[i], 10);
4259 if (nodeIndex < node.childNodes.length) { 4260 node = node.childNodes[nodeIndex]; 4261 } else { 4262 throw module.createError("deserializePosition() failed: node " + dom.inspectNode(node) + 4263 " has no child with index " + nodeIndex + ", " + i); 4264 } 4265 } 4266 4267 return new dom.DomPosition(node, parseInt(parts[1], 10)); 4268 } 4269 4270 function serializeRange(range, omitChecksum, rootNode) { 4271 rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement; 4272 if (!dom.isOrIsAncestorOf(rootNode, range.commonAncestorContainer)) { 4273 throw module.createError("serializeRange(): range " + range.inspect() + 4274 " is not wholly contained within specified root node " + dom.inspectNode(rootNode)); 4275 } 4276 var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," + 4277 serializePosition(range.endContainer, range.endOffset, rootNode); 4278 if (!omitChecksum) { 4279 serialized += "{" + getElementChecksum(rootNode) + "}"; 4280 } 4281 return serialized; 4282 } 4283 4284 var deserializeRegex = /^([^,]+),([^,\{]+)(\{([^}]+)\})?$/; 4285 4286 function deserializeRange(serialized, rootNode, doc) { 4287 if (rootNode) { 4288 doc = doc || dom.getDocument(rootNode); 4289 } else { 4290 doc = doc || document; 4291 rootNode = doc.documentElement; 4292 } 4293 var result = deserializeRegex.exec(serialized); 4294 var checksum = result[4]; 4295 if (checksum) { 4296 var rootNodeChecksum = getElementChecksum(rootNode); 4297 if (checksum !== rootNodeChecksum) { 4298 throw module.createError("deserializeRange(): checksums of serialized range root node (" + checksum + 4299 ") and target root node (" + rootNodeChecksum + ") do not match"); 4300 } 4301 } 4302 var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc); 4303 var range = api.createRange(doc); 4304 range.setStartAndEnd(start.node, start.offset, end.node, end.offset); 4305 return range; 4306 } 4307 4308 function canDeserializeRange(serialized, rootNode, doc) { 4309 if (!rootNode) { 4310 rootNode = (doc || document).documentElement; 4311 } 4312 var result = deserializeRegex.exec(serialized); 4313 var checksum = result[3]; 4314 return !checksum || checksum === getElementChecksum(rootNode); 4315 } 4316 4317 function serializeSelection(selection, omitChecksum, rootNode) { 4318 selection = api.getSelection(selection); 4319 var ranges = selection.getAllRanges(), serializedRanges = []; 4320 for (var i = 0, len = ranges.length; i < len; ++i) { 4321 serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode); 4322 } 4323 return serializedRanges.join("|"); 4324 } 4325 4326 function deserializeSelection(serialized, rootNode, win) { 4327 if (rootNode) { 4328 win = win || dom.getWindow(rootNode); 4329 } else { 4330 win = win || window; 4331 rootNode = win.document.documentElement; 4332 } 4333 var serializedRanges = serialized.split("|"); 4334 var sel = api.getSelection(win); 4335 var ranges = []; 4336 4337 for (var i = 0, len = serializedRanges.length; i < len; ++i) { 4338 ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document); 4339 } 4340 sel.setRanges(ranges); 4341 4342 return sel; 4343 } 4344 4345 function canDeserializeSelection(serialized, rootNode, win) { 4346 var doc; 4347 if (rootNode) { 4348 doc = win ? win.document : dom.getDocument(rootNode); 4349 } else { 4350 win = win || window; 4351 rootNode = win.document.documentElement; 4352 } 4353 var serializedRanges = serialized.split("|"); 4354 4355 for (var i = 0, len = serializedRanges.length; i < len; ++i) { 4356 if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) { 4357 return false;
4358 } 4359 } 4360 4361 return true; 4362 } 4363 4364 var cookieName = "rangySerializedSelection"; 4365 4366 function getSerializedSelectionFromCookie(cookie) { 4367 var parts = cookie.split(/[;,]/); 4368 for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) { 4369 nameVal = parts[i].split("="); 4370 if (nameVal[0].replace(/^\s+/, "") == cookieName) { 4371 val = nameVal[1]; 4372 if (val) { 4373 return decodeURIComponent(val.replace(/\s+$/, "")); 4374 } 4375 } 4376 } 4377 return null; 4378 } 4379 4380 function restoreSelectionFromCookie(win) { 4381 win = win || window; 4382 var serialized = getSerializedSelectionFromCookie(win.document.cookie); 4383 if (serialized) { 4384 deserializeSelection(serialized, win.doc); 4385 } 4386 } 4387 4388 function saveSelectionCookie(win, props) { 4389 win = win || window; 4390 props = (typeof props == "object") ? props : {}; 4391 var expires = props.expires ? ";expires=" + props.expires.toUTCString() : ""; 4392 var path = props.path ? ";path=" + props.path : ""; 4393 var domain = props.domain ? ";domain=" + props.domain : ""; 4394 var secure = props.secure ? ";secure" : ""; 4395 var serialized = serializeSelection(api.getSelection(win)); 4396 win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure; 4397 } 4398 4399 util.extend(api, { 4400 serializePosition: serializePosition, 4401 deserializePosition: deserializePosition, 4402 serializeRange: serializeRange, 4403 deserializeRange: deserializeRange, 4404 canDeserializeRange: canDeserializeRange, 4405 serializeSelection: serializeSelection, 4406 deserializeSelection: deserializeSelection, 4407 canDeserializeSelection: canDeserializeSelection, 4408 restoreSelectionFromCookie: restoreSelectionFromCookie, 4409 saveSelectionCookie: saveSelectionCookie, 4410 getElementChecksum: getElementChecksum, 4411 nodeToInfoString: nodeToInfoString 4412 }); 4413 4414 util.crc32 = crc32; 4415 }); 4416 4417 return rangy; 4418}, this); 4419 4420 4421/** 4422 * Class Applier module for Rangy. 4423 * Adds, removes and toggles classes on Ranges and Selections 4424 * 4425 * Part of Rangy, a cross-browser JavaScript range and selection library 4426 * https://github.com/timdown/rangy 4427 * 4428 * Depends on Rangy core. 4429 * 4430 * Copyright 2015, Tim Down 4431 * Licensed under the MIT license. 4432 * Version: 1.3.1-dev 4433 * Build date: 20 May 2015 4434 */ 4435 (function(factory, root) { 4436 if (typeof define == "function" && define.amd) { 4437 // AMD. Register as an anonymous module with a dependency on Rangy. 4438 define(["./rangy-core"], factory); 4439 } else if (typeof module != "undefined" && typeof exports == "object") { 4440 // Node/CommonJS style 4441 module.exports = factory( require("rangy") ); 4442 } else { 4443 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 4444 factory(root.rangy); 4445 } 4446})(function(rangy) { 4447 rangy.createModule("ClassApplier", ["WrappedSelection"], function(api, module) { 4448 var dom = api.dom; 4449 var DomPosition = dom.DomPosition; 4450 var contains = dom.arrayContains; 4451 var util = api.util; 4452 var forEach = util.forEach; 4453 4454 4455 var defaultTagName = "span"; 4456 var createElementNSSupported = util.isHostMethod(document, "createElementNS"); 4457 4458 function each(obj, func) { 4459 for (var i in obj) { 4460 if (obj.hasOwnProperty(i)) { 4461 if (func(i, obj[i]) === false) { 4462 return false; 4463 } 4464 } 4465 } 4466 return true; 4467 } 4468 4469 function trim(str) { 4470 return str.replace(/^\s\s*/, "").replace(/\s\s*$/, ""); 4471 } 4472 4473 function classNameContainsClass(fullClassName, className) { 4474 return !!fullClassName && new RegExp("(?:^|\\s)" + className + "(?:\\s|$)").test(fullClassName); 4475 } 4476 4477 // Inefficient, inelegant nonsense for IE's svg element, which has no classList and non-HTML className implementation 4478 function hasClass(el, className) { 4479 if (typeof el.classList == "object") { 4480 return el.classList.contains(className); 4481 } else { 4482 var classNameSupported = (typeof el.className == "string"); 4483 var elClass = classNameSupported ? el.className : el.getAttribute("class"); 4484 return classNameContainsClass(elClass, className); 4485 } 4486 } 4487 4488 function addClass(el, className) { 4489 if (typeof el.classList == "object") { 4490 el.classList.add(className); 4491 } else { 4492 var classNameSupported = (typeof el.className == "string"); 4493 var elClass = classNameSupported ? el.className : el.getAttribute("class"); 4494 if (elClass) { 4495 if (!classNameContainsClass(elClass, className)) { 4496 elClass += " " + className; 4497 } 4498 } else { 4499 elClass = className; 4500 } 4501 if (classNameSupported) { 4502 el.className = elClass; 4503 } else { 4504 el.setAttribute("class", elClass); 4505 } 4506 } 4507 } 4508 4509 var removeClass = (function() { 4510 function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) { 4511 return (whiteSpaceBefore && whiteSpaceAfter) ? " " : ""; 4512 } 4513 4514 return function(el, className) { 4515 if (typeof el.classList == "object") { 4516 el.classList.remove(className); 4517 } else { 4518 var classNameSupported = (typeof el.className == "string"); 4519 var elClass = classNameSupported ? el.className : el.getAttribute("class"); 4520 elClass = elClass.replace(new RegExp("(^|\\s)" + className + "(\\s|$)"), replacer); 4521 if (classNameSupported) { 4522 el.className = elClass; 4523 } else { 4524 el.setAttribute("class", elClass); 4525 } 4526 } 4527 }; 4528 })(); 4529 4530 function getClass(el) { 4531 var classNameSupported = (typeof el.className == "string"); 4532 return classNameSupported ? el.className : el.getAttribute("class"); 4533 } 4534 4535 function sortClassName(className) { 4536 return className && className.split(/\s+/).sort().join(" "); 4537 } 4538 4539 function getSortedClassName(el) { 4540 return sortClassName( getClass(el) ); 4541 } 4542 4543 function haveSameClasses(el1, el2) { 4544 return getSortedClassName(el1) == getSortedClassName(el2); 4545 } 4546 4547 function hasAllClasses(el, className) { 4548 var classes = className.split(/\s+/); 4549 for (var i = 0, len = classes.length; i < len; ++i) { 4550 if (!hasClass(el, trim(classes[i]))) { 4551 return false;
4552 } 4553 } 4554 return true; 4555 } 4556 4557 function canTextBeStyled(textNode) { 4558 var parent = textNode.parentNode; 4559 return (parent && parent.nodeType == 1 && !/^(textarea|style|script|select|iframe)$/i.test(parent.nodeName)); 4560 } 4561 4562 function movePosition(position, oldParent, oldIndex, newParent, newIndex) { 4563 var posNode = position.node, posOffset = position.offset; 4564 var newNode = posNode, newOffset = posOffset; 4565 4566 if (posNode == newParent && posOffset > newIndex) { 4567 ++newOffset; 4568 } 4569 4570 if (posNode == oldParent && (posOffset == oldIndex || posOffset == oldIndex + 1)) { 4571 newNode = newParent; 4572 newOffset += newIndex - oldIndex; 4573 } 4574 4575 if (posNode == oldParent && posOffset > oldIndex + 1) { 4576 --newOffset; 4577 } 4578 4579 position.node = newNode; 4580 position.offset = newOffset; 4581 } 4582 4583 function movePositionWhenRemovingNode(position, parentNode, index) { 4584 if (position.node == parentNode && position.offset > index) { 4585 --position.offset; 4586 } 4587 } 4588 4589 function movePreservingPositions(node, newParent, newIndex, positionsToPreserve) { 4590 // For convenience, allow newIndex to be -1 to mean "insert at the end". 4591 if (newIndex == -1) { 4592 newIndex = newParent.childNodes.length; 4593 } 4594 4595 var oldParent = node.parentNode; 4596 var oldIndex = dom.getNodeIndex(node); 4597 4598 forEach(positionsToPreserve, function(position) { 4599 movePosition(position, oldParent, oldIndex, newParent, newIndex); 4600 }); 4601 4602 // Now actually move the node. 4603 if (newParent.childNodes.length == newIndex) { 4604 newParent.appendChild(node); 4605 } else { 4606 newParent.insertBefore(node, newParent.childNodes[newIndex]); 4607 } 4608 } 4609 4610 function removePreservingPositions(node, positionsToPreserve) { 4611 4612 var oldParent = node.parentNode; 4613 var oldIndex = dom.getNodeIndex(node); 4614 4615 forEach(positionsToPreserve, function(position) { 4616 movePositionWhenRemovingNode(position, oldParent, oldIndex); 4617 }); 4618 4619 dom.removeNode(node); 4620 } 4621 4622 function moveChildrenPreservingPositions(node, newParent, newIndex, removeNode, positionsToPreserve) { 4623 var child, children = []; 4624 while ( (child = node.firstChild) ) { 4625 movePreservingPositions(child, newParent, newIndex++, positionsToPreserve); 4626 children.push(child); 4627 } 4628 if (removeNode) { 4629 removePreservingPositions(node, positionsToPreserve); 4630 } 4631 return children; 4632 } 4633 4634 function replaceWithOwnChildrenPreservingPositions(element, positionsToPreserve) { 4635 return moveChildrenPreservingPositions(element, element.parentNode, dom.getNodeIndex(element), true, positionsToPreserve); 4636 } 4637 4638 function rangeSelectsAnyText(range, textNode) { 4639 var textNodeRange = range.cloneRange(); 4640 textNodeRange.selectNodeContents(textNode); 4641 4642 var intersectionRange = textNodeRange.intersection(range); 4643 var text = intersectionRange ? intersectionRange.toString() : ""; 4644 4645 return text != ""; 4646 } 4647 4648 function getEffectiveTextNodes(range) { 4649 var nodes = range.getNodes([3]); 4650 4651 // Optimization as per issue 145 4652 4653 // Remove non-intersecting text nodes from the start of the range 4654 var start = 0, node; 4655 while ( (node = nodes[start]) && !rangeSelectsAnyText(range, node) ) { 4656 ++start; 4657 } 4658 4659 // Remove non-intersecting text nodes from the start of the range 4660 var end = nodes.length - 1; 4661 while ( (node = nodes[end]) && !rangeSelectsAnyText(range, node) ) { 4662 --end; 4663 } 4664 4665 return nodes.slice(start, end + 1); 4666 } 4667 4668 function elementsHaveSameNonClassAttributes(el1, el2) { 4669 if (el1.attributes.length != el2.attributes.length) return false;
4670 for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) { 4671 attr1 = el1.attributes[i]; 4672 name = attr1.name; 4673 if (name != "class") { 4674 attr2 = el2.attributes.getNamedItem(name); 4675 if ( (attr1 === null) != (attr2 === null) ) return false; 4676 if (attr1.specified != attr2.specified) return false; 4677 if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false; 4678 } 4679 } 4680 return true; 4681 } 4682 4683 function elementHasNonClassAttributes(el, exceptions) { 4684 for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) { 4685 attrName = el.attributes[i].name; 4686 if ( !(exceptions && contains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") { 4687 return true; 4688 } 4689 } 4690 return false; 4691 } 4692 4693 var getComputedStyleProperty = dom.getComputedStyleProperty; 4694 var isEditableElement = (function() { 4695 var testEl = document.createElement("div"); 4696 return typeof testEl.isContentEditable == "boolean" ? 4697 function (node) { 4698 return node && node.nodeType == 1 && node.isContentEditable; 4699 } : 4700 function (node) { 4701 if (!node || node.nodeType != 1 || node.contentEditable == "false") { 4702 return false; 4703 } 4704 return node.contentEditable == "true" || isEditableElement(node.parentNode); 4705 }; 4706 })(); 4707 4708 function isEditingHost(node) { 4709 var parent; 4710 return node && node.nodeType == 1 && 4711 (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on") || 4712 (isEditableElement(node) && !isEditableElement(node.parentNode))); 4713 } 4714 4715 function isEditable(node) { 4716 return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node); 4717 } 4718 4719 var inlineDisplayRegex = /^inline(-block|-table)?$/i; 4720 4721 function isNonInlineElement(node) { 4722 return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display")); 4723 } 4724 4725 // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html) 4726 var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/; 4727 4728 function isUnrenderedWhiteSpaceNode(node) { 4729 if (node.data.length == 0) { 4730 return true; 4731 } 4732 if (htmlNonWhiteSpaceRegex.test(node.data)) { 4733 return false; 4734 } 4735 var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace"); 4736 switch (cssWhiteSpace) { 4737 case "pre": 4738 case "pre-wrap": 4739 case "-moz-pre-wrap": 4740 return false; 4741 case "pre-line": 4742 if (/[\r\n]/.test(node.data)) { 4743 return false; 4744 } 4745 } 4746 4747 // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a 4748 // non-inline element, it will not be rendered. This seems to be a good enough definition. 4749 return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling); 4750 } 4751 4752 function getRangeBoundaries(ranges) { 4753 var positions = [], i, range; 4754 for (i = 0; range = ranges[i++]; ) { 4755 positions.push( 4756 new DomPosition(range.startContainer, range.startOffset), 4757 new DomPosition(range.endContainer, range.endOffset) 4758 ); 4759 } 4760 return positions; 4761 } 4762 4763 function updateRangesFromBoundaries(ranges, positions) { 4764 for (var i = 0, range, start, end, len = ranges.length; i < len; ++i) { 4765 range = ranges[i]; 4766 start = positions[i * 2]; 4767 end = positions[i * 2 + 1]; 4768 range.setStartAndEnd(start.node, start.offset, end.node, end.offset); 4769 } 4770 } 4771 4772 function isSplitPoint(node, offset) { 4773 if (dom.isCharacterDataNode(node)) { 4774 if (offset == 0) { 4775 return !!node.previousSibling; 4776 } else if (offset == node.length) { 4777 return !!node.nextSibling; 4778 } else { 4779 return true; 4780 } 4781 } 4782 4783 return offset > 0 && offset < node.childNodes.length; 4784 } 4785 4786 function splitNodeAt(node, descendantNode, descendantOffset, positionsToPreserve) { 4787 var newNode, parentNode; 4788 var splitAtStart = (descendantOffset == 0); 4789 4790 if (dom.isAncestorOf(descendantNode, node)) { 4791 return node; 4792 } 4793 4794 if (dom.isCharacterDataNode(descendantNode)) { 4795 var descendantIndex = dom.getNodeIndex(descendantNode); 4796 if (descendantOffset == 0) {
4797 descendantOffset = descendantIndex; 4798 } else if (descendantOffset == descendantNode.length) { 4799 descendantOffset = descendantIndex + 1; 4800 } else { 4801 throw module.createError("splitNodeAt() should not be called with offset in the middle of a data node (" + 4802 descendantOffset + " in " + descendantNode.data); 4803 } 4804 descendantNode = descendantNode.parentNode; 4805 } 4806 4807 if (isSplitPoint(descendantNode, descendantOffset)) { 4808 // descendantNode is now guaranteed not to be a text or other character node 4809 newNode = descendantNode.cloneNode(false); 4810 parentNode = descendantNode.parentNode; 4811 if (newNode.id) { 4812 newNode.removeAttribute("id"); 4813 } 4814 var child, newChildIndex = 0; 4815 4816 while ( (child = descendantNode.childNodes[descendantOffset]) ) { 4817 movePreservingPositions(child, newNode, newChildIndex++, positionsToPreserve); 4818 } 4819 movePreservingPositions(newNode, parentNode, dom.getNodeIndex(descendantNode) + 1, positionsToPreserve); 4820 return (descendantNode == node) ? newNode : splitNodeAt(node, parentNode, dom.getNodeIndex(newNode), positionsToPreserve); 4821 } else if (node != descendantNode) { 4822 newNode = descendantNode.parentNode; 4823 4824 // Work out a new split point in the parent node 4825 var newNodeIndex = dom.getNodeIndex(descendantNode); 4826 4827 if (!splitAtStart) { 4828 newNodeIndex++; 4829 } 4830 return splitNodeAt(node, newNode, newNodeIndex, positionsToPreserve); 4831 } 4832 return node; 4833 } 4834 4835 function areElementsMergeable(el1, el2) { 4836 return el1.namespaceURI == el2.namespaceURI && 4837 el1.tagName.toLowerCase() == el2.tagName.toLowerCase() && 4838 haveSameClasses(el1, el2) && 4839 elementsHaveSameNonClassAttributes(el1, el2) && 4840 getComputedStyleProperty(el1, "display") == "inline" && 4841 getComputedStyleProperty(el2, "display") == "inline"; 4842 } 4843 4844 function createAdjacentMergeableTextNodeGetter(forward) { 4845 var siblingPropName = forward ? "nextSibling" : "previousSibling"; 4846 4847 return function(textNode, checkParentElement) { 4848 var el = textNode.parentNode; 4849 var adjacentNode = textNode[siblingPropName]; 4850 if (adjacentNode) { 4851 // Can merge if the node's previous/next sibling is a text node 4852 if (adjacentNode && adjacentNode.nodeType == 3) { 4853 return adjacentNode; 4854 } 4855 } else if (checkParentElement) { 4856 // Compare text node parent element with its sibling 4857 adjacentNode = el[siblingPropName]; 4858 if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) { 4859 var adjacentNodeChild = adjacentNode[forward ? "firstChild" : "lastChild"]; 4860 if (adjacentNodeChild && adjacentNodeChild.nodeType == 3) { 4861 return adjacentNodeChild; 4862 } 4863 } 4864 } 4865 return null; 4866 }; 4867 } 4868 4869 var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false), 4870 getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true); 4871 4872 4873 function Merge(firstNode) { 4874 this.isElementMerge = (firstNode.nodeType == 1); 4875 this.textNodes = []; 4876 var firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; 4877 if (firstTextNode) { 4878 this.textNodes[0] = firstTextNode; 4879 } 4880 } 4881 4882 Merge.prototype = { 4883 doMerge: function(positionsToPreserve) { 4884 var textNodes = this.textNodes; 4885 var firstTextNode = textNodes[0]; 4886 if (textNodes.length > 1) { 4887 var firstTextNodeIndex = dom.getNodeIndex(firstTextNode); 4888 var textParts = [], combinedTextLength = 0, textNode, parent;
4889 forEach(textNodes, function(textNode, i) { 4890 parent = textNode.parentNode; 4891 if (i > 0) { 4892 parent.removeChild(textNode); 4893 if (!parent.hasChildNodes()) { 4894 dom.removeNode(parent); 4895 } 4896 if (positionsToPreserve) { 4897 forEach(positionsToPreserve, function(position) { 4898 // Handle case where position is inside the text node being merged into a preceding node 4899 if (position.node == textNode) { 4900 position.node = firstTextNode; 4901 position.offset += combinedTextLength; 4902 } 4903 // Handle case where both text nodes precede the position within the same parent node 4904 if (position.node == parent && position.offset > firstTextNodeIndex) { 4905 --position.offset; 4906 if (position.offset == firstTextNodeIndex + 1 && i < len - 1) { 4907 position.node = firstTextNode; 4908 position.offset = combinedTextLength; 4909 } 4910 } 4911 }); 4912 } 4913 } 4914 textParts[i] = textNode.data; 4915 combinedTextLength += textNode.data.length; 4916 }); 4917 firstTextNode.data = textParts.join(""); 4918 } 4919 return firstTextNode.data; 4920 }, 4921 4922 getLength: function() { 4923 var i = this.textNodes.length, len = 0; 4924 while (i--) { 4925 len += this.textNodes[i].length; 4926 } 4927 return len; 4928 }, 4929 4930 toString: function() { 4931 var textParts = []; 4932 forEach(this.textNodes, function(textNode, i) { 4933 textParts[i] = "'" + textNode.data + "'"; 4934 }); 4935 return "[Merge(" + textParts.join(",") + ")]"; 4936 } 4937 }; 4938 4939 var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly", "useExistingElements", 4940 "removeEmptyElements", "onElementCreate"]; 4941 4942 // TODO: Populate this with every attribute name that corresponds to a property with a different name. Really?? 4943 var attrNamesForProperties = {}; 4944 4945 function ClassApplier(className, options, tagNames) { 4946 var normalize, i, len, propName, applier = this; 4947 applier.cssClass = applier.className = className; // cssClass property is for backward compatibility 4948 4949 var elementPropertiesFromOptions = null, elementAttributes = {}; 4950 4951 // Initialize from options object 4952 if (typeof options == "object" && options !== null) { 4953 if (typeof options.elementTagName !== "undefined") { 4954 options.elementTagName = options.elementTagName.toLowerCase(); 4955 } 4956 tagNames = options.tagNames; 4957 elementPropertiesFromOptions = options.elementProperties; 4958 elementAttributes = options.elementAttributes; 4959 4960 for (i = 0; propName = optionProperties[i++]; ) { 4961 if (options.hasOwnProperty(propName)) { 4962 applier[propName] = options[propName]; 4963 } 4964 } 4965 normalize = options.normalize; 4966 } else { 4967 normalize = options; 4968 } 4969 4970 // Backward compatibility: the second parameter can also be a Boolean indicating to normalize after unapplying 4971 applier.normalize = (typeof normalize == "undefined") ? true : normalize; 4972 4973 // Initialize element properties and attribute exceptions 4974 applier.attrExceptions = []; 4975 var el = document.createElement(applier.elementTagName); 4976 applier.elementProperties = applier.copyPropertiesToElement(elementPropertiesFromOptions, el, true); 4977 each(elementAttributes, function(attrName, attrValue) { 4978 applier.attrExceptions.push(attrName); 4979 // Ensure each attribute value is a string 4980 elementAttributes[attrName] = "" + attrValue; 4981 }); 4982 applier.elementAttributes = elementAttributes; 4983 4984 applier.elementSortedClassName = applier.elementProperties.hasOwnProperty("className") ? 4985 sortClassName(applier.elementProperties.className + " " + className) : className; 4986 4987 // Initialize tag names 4988 applier.applyToAnyTagName = false;
4989 var type = typeof tagNames; 4990 if (type == "string") { 4991 if (tagNames == "*") { 4992 applier.applyToAnyTagName = true; 4993 } else { 4994 applier.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/); 4995 } 4996 } else if (type == "object" && typeof tagNames.length == "number") { 4997 applier.tagNames = []; 4998 for (i = 0, len = tagNames.length; i < len; ++i) { 4999 if (tagNames[i] == "*") { 5000 applier.applyToAnyTagName = true; 5001 } else { 5002 applier.tagNames.push(tagNames[i].toLowerCase()); 5003 } 5004 } 5005 } else { 5006 applier.tagNames = [applier.elementTagName]; 5007 } 5008 } 5009 5010 ClassApplier.prototype = { 5011 elementTagName: defaultTagName, 5012 elementProperties: {}, 5013 elementAttributes: {}, 5014 ignoreWhiteSpace: true, 5015 applyToEditableOnly: false, 5016 useExistingElements: true, 5017 removeEmptyElements: true, 5018 onElementCreate: null, 5019 5020 copyPropertiesToElement: function(props, el, createCopy) { 5021 var s, elStyle, elProps = {}, elPropsStyle, propValue, elPropValue, attrName; 5022 5023 for (var p in props) { 5024 if (props.hasOwnProperty(p)) { 5025 propValue = props[p]; 5026 elPropValue = el[p]; 5027 5028 // Special case for class. The copied properties object has the applier's class as well as its own 5029 // to simplify checks when removing styling elements 5030 if (p == "className") { 5031 addClass(el, propValue); 5032 addClass(el, this.className); 5033 el[p] = sortClassName(el[p]); 5034 if (createCopy) { 5035 elProps[p] = propValue; 5036 } 5037 } 5038 5039 // Special case for style 5040 else if (p == "style") { 5041 elStyle = elPropValue; 5042 if (createCopy) { 5043 elProps[p] = elPropsStyle = {}; 5044 } 5045 for (s in props[p]) { 5046 if (props[p].hasOwnProperty(s)) { 5047 elStyle[s] = propValue[s]; 5048 if (createCopy) { 5049 elPropsStyle[s] = elStyle[s]; 5050 } 5051 } 5052 } 5053 this.attrExceptions.push(p); 5054 } else { 5055 el[p] = propValue; 5056 // Copy the property back from the dummy element so that later comparisons to check whether 5057 // elements may be removed are checking against the right value. For example, the href property 5058 // of an element returns a fully qualified URL even if it was previously assigned a relative 5059 // URL. 5060 if (createCopy) { 5061 elProps[p] = el[p]; 5062 5063 // Not all properties map to identically-named attributes 5064 attrName = attrNamesForProperties.hasOwnProperty(p) ? attrNamesForProperties[p] : p; 5065 this.attrExceptions.push(attrName); 5066 } 5067 } 5068 } 5069 } 5070 5071 return createCopy ? elProps : ""; 5072 }, 5073 5074 copyAttributesToElement: function(attrs, el) { 5075 for (var attrName in attrs) { 5076 if (attrs.hasOwnProperty(attrName) && !/^class(?:Name)?$/i.test(attrName)) { 5077 el.setAttribute(attrName, attrs[attrName]); 5078 } 5079 } 5080 }, 5081 5082 appliesToElement: function(el) { 5083 return contains(this.tagNames, el.tagName.toLowerCase()); 5084 }, 5085 5086 getEmptyElements: function(range) { 5087 var applier = this; 5088 return range.getNodes([1], function(el) { 5089 return applier.appliesToElement(el) && !el.hasChildNodes(); 5090 }); 5091 }, 5092 5093 hasClass: function(node) { 5094 return node.nodeType == 1 && 5095 (this.applyToAnyTagName || this.appliesToElement(node)) && 5096 hasClass(node, this.className); 5097 }, 5098 5099 getSelfOrAncestorWithClass: function(node) { 5100 while (node) { 5101 if (this.hasClass(node)) { 5102 return node; 5103 } 5104 node = node.parentNode; 5105 } 5106 return null; 5107 }, 5108 5109 isModifiable: function(node) { 5110 return !this.applyToEditableOnly || isEditable(node); 5111 }, 5112 5113 // White space adjacent to an unwrappable node can be ignored for wrapping 5114 isIgnorableWhiteSpaceNode: function(node) { 5115 return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node); 5116 }, 5117 5118 // Normalizes nodes after applying a class to a Range. 5119 postApply: function(textNodes, range, positionsToPreserve, isUndo) { 5120 var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1]; 5121 5122 var merges = [], currentMerge; 5123 5124 var rangeStartNode = firstNode, rangeEndNode = lastNode; 5125 var rangeStartOffset = 0, rangeEndOffset = lastNode.length; 5126 5127 var textNode, precedingTextNode; 5128 5129 // Check for every required merge and create a Merge object for each
5130 forEach(textNodes, function(textNode) { 5131 precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo); 5132 if (precedingTextNode) { 5133 if (!currentMerge) { 5134 currentMerge = new Merge(precedingTextNode); 5135 merges.push(currentMerge); 5136 } 5137 currentMerge.textNodes.push(textNode); 5138 if (textNode === firstNode) { 5139 rangeStartNode = currentMerge.textNodes[0]; 5140 rangeStartOffset = rangeStartNode.length; 5141 } 5142 if (textNode === lastNode) { 5143 rangeEndNode = currentMerge.textNodes[0]; 5144 rangeEndOffset = currentMerge.getLength(); 5145 } 5146 } else { 5147 currentMerge = null; 5148 } 5149 }); 5150 5151 // Test whether the first node after the range needs merging 5152 var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo); 5153 5154 if (nextTextNode) { 5155 if (!currentMerge) { 5156 currentMerge = new Merge(lastNode); 5157 merges.push(currentMerge); 5158 } 5159 currentMerge.textNodes.push(nextTextNode); 5160 } 5161 5162 // Apply the merges 5163 if (merges.length) { 5164 for (i = 0, len = merges.length; i < len; ++i) { 5165 merges[i].doMerge(positionsToPreserve); 5166 } 5167 5168 // Set the range boundaries 5169 range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset); 5170 } 5171 }, 5172 5173 createContainer: function(parentNode) { 5174 var doc = dom.getDocument(parentNode); 5175 var namespace; 5176 var el = createElementNSSupported && !dom.isHtmlNamespace(parentNode) && (namespace = parentNode.namespaceURI) ? 5177 doc.createElementNS(parentNode.namespaceURI, this.elementTagName) : 5178 doc.createElement(this.elementTagName); 5179 5180 this.copyPropertiesToElement(this.elementProperties, el, false); 5181 this.copyAttributesToElement(this.elementAttributes, el); 5182 addClass(el, this.className); 5183 if (this.onElementCreate) { 5184 this.onElementCreate(el, this); 5185 } 5186 return el; 5187 }, 5188 5189 elementHasProperties: function(el, props) { 5190 var applier = this; 5191 return each(props, function(p, propValue) { 5192 if (p == "className") { 5193 // For checking whether we should reuse an existing element, we just want to check that the element 5194 // has all the classes specified in the className property. When deciding whether the element is 5195 // removable when unapplying a class, there is separate special handling to check whether the 5196 // element has extra classes so the same simple check will do. 5197 return hasAllClasses(el, propValue); 5198 } else if (typeof propValue == "object") { 5199 if (!applier.elementHasProperties(el[p], propValue)) { 5200 return false; 5201 } 5202 } else if (el[p] !== propValue) { 5203 return false; 5204 } 5205 }); 5206 }, 5207 5208 elementHasAttributes: function(el, attrs) { 5209 return each(attrs, function(name, value) { 5210 if (el.getAttribute(name) !== value) { 5211 return false; 5212 } 5213 }); 5214 }, 5215 5216 applyToTextNode: function(textNode, positionsToPreserve) { 5217 5218 // Check whether the text node can be styled. Text within a <style> or <script> element, for example, 5219 // should not be styled. See issue 283. 5220 if (canTextBeStyled(textNode)) { 5221 var parent = textNode.parentNode; 5222 if (parent.childNodes.length == 1 && 5223 this.useExistingElements && 5224 this.appliesToElement(parent) && 5225 this.elementHasProperties(parent, this.elementProperties) && 5226 this.elementHasAttributes(parent, this.elementAttributes)) { 5227 5228 addClass(parent, this.className); 5229 } else { 5230 var textNodeParent = textNode.parentNode; 5231 var el = this.createContainer(textNodeParent); 5232 textNodeParent.insertBefore(el, textNode); 5233 el.appendChild(textNode); 5234 } 5235 } 5236 5237 }, 5238 5239 isRemovable: function(el) { 5240 return el.tagName.toLowerCase() == this.elementTagName && 5241 getSortedClassName(el) == this.elementSortedClassName && 5242 this.elementHasProperties(el, this.elementProperties) && 5243 !elementHasNonClassAttributes(el, this.attrExceptions) && 5244 this.elementHasAttributes(el, this.elementAttributes) && 5245 this.isModifiable(el); 5246 }, 5247 5248 isEmptyContainer: function(el) { 5249 var childNodeCount = el.childNodes.length; 5250 return el.nodeType == 1 && 5251 this.isRemovable(el) && 5252 (childNodeCount == 0 || (childNodeCount == 1 && this.isEmptyContainer(el.firstChild))); 5253 }, 5254 5255 removeEmptyContainers: function(range) { 5256 var applier = this; 5257 var nodesToRemove = range.getNodes([1], function(el) { 5258 return applier.isEmptyContainer(el); 5259 }); 5260 5261 var rangesToPreserve = [range]; 5262 var positionsToPreserve = getRangeBoundaries(rangesToPreserve); 5263
5264 forEach(nodesToRemove, function(node) { 5265 removePreservingPositions(node, positionsToPreserve); 5266 }); 5267 5268 // Update the range from the preserved boundary positions 5269 updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve); 5270 }, 5271 5272 undoToTextNode: function(textNode, range, ancestorWithClass, positionsToPreserve) { 5273 if (!range.containsNode(ancestorWithClass)) { 5274 // Split out the portion of the ancestor from which we can remove the class 5275 //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass); 5276 var ancestorRange = range.cloneRange(); 5277 ancestorRange.selectNode(ancestorWithClass); 5278 if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)) { 5279 splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, positionsToPreserve); 5280 range.setEndAfter(ancestorWithClass); 5281 } 5282 if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)) { 5283 ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, positionsToPreserve); 5284 } 5285 } 5286 5287 if (this.isRemovable(ancestorWithClass)) { 5288 replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve); 5289 } else { 5290 removeClass(ancestorWithClass, this.className); 5291 } 5292 }, 5293 5294 splitAncestorWithClass: function(container, offset, positionsToPreserve) { 5295 var ancestorWithClass = this.getSelfOrAncestorWithClass(container); 5296 if (ancestorWithClass) { 5297 splitNodeAt(ancestorWithClass, container, offset, positionsToPreserve); 5298 } 5299 }, 5300 5301 undoToAncestor: function(ancestorWithClass, positionsToPreserve) { 5302 if (this.isRemovable(ancestorWithClass)) { 5303 replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve); 5304 } else { 5305 removeClass(ancestorWithClass, this.className); 5306 } 5307 }, 5308 5309 applyToRange: function(range, rangesToPreserve) { 5310 var applier = this; 5311 rangesToPreserve = rangesToPreserve || []; 5312 5313 // Create an array of range boundaries to preserve 5314 var positionsToPreserve = getRangeBoundaries(rangesToPreserve || []); 5315 5316 range.splitBoundariesPreservingPositions(positionsToPreserve); 5317 5318 // Tidy up the DOM by removing empty containers 5319 if (applier.removeEmptyElements) { 5320 applier.removeEmptyContainers(range); 5321 } 5322 5323 var textNodes = getEffectiveTextNodes(range); 5324 5325 if (textNodes.length) { 5326 forEach(textNodes, function(textNode) { 5327 if (!applier.isIgnorableWhiteSpaceNode(textNode) && !applier.getSelfOrAncestorWithClass(textNode) && 5328 applier.isModifiable(textNode)) { 5329 applier.applyToTextNode(textNode, positionsToPreserve); 5330 } 5331 }); 5332 var lastTextNode = textNodes[textNodes.length - 1]; 5333 range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length); 5334 if (applier.normalize) { 5335 applier.postApply(textNodes, range, positionsToPreserve, false); 5336 } 5337 5338 // Update the ranges from the preserved boundary positions 5339 updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve); 5340 } 5341 5342 // Apply classes to any appropriate empty elements 5343 var emptyElements = applier.getEmptyElements(range); 5344
5345 forEach(emptyElements, function(el) { 5346 addClass(el, applier.className); 5347 }); 5348 }, 5349 5350 applyToRanges: function(ranges) { 5351 5352 var i = ranges.length; 5353 while (i--) { 5354 this.applyToRange(ranges[i], ranges); 5355 } 5356 5357 5358 return ranges; 5359 }, 5360 5361 applyToSelection: function(win) { 5362 var sel = api.getSelection(win); 5363 sel.setRanges( this.applyToRanges(sel.getAllRanges()) ); 5364 }, 5365 5366 undoToRange: function(range, rangesToPreserve) { 5367 var applier = this; 5368 // Create an array of range boundaries to preserve 5369 rangesToPreserve = rangesToPreserve || []; 5370 var positionsToPreserve = getRangeBoundaries(rangesToPreserve); 5371 5372 5373 range.splitBoundariesPreservingPositions(positionsToPreserve); 5374 5375 // Tidy up the DOM by removing empty containers 5376 if (applier.removeEmptyElements) { 5377 applier.removeEmptyContainers(range, positionsToPreserve); 5378 } 5379 5380 var textNodes = getEffectiveTextNodes(range); 5381 var textNode, ancestorWithClass; 5382 var lastTextNode = textNodes[textNodes.length - 1]; 5383 5384 if (textNodes.length) { 5385 applier.splitAncestorWithClass(range.endContainer, range.endOffset, positionsToPreserve); 5386 applier.splitAncestorWithClass(range.startContainer, range.startOffset, positionsToPreserve); 5387 for (var i = 0, len = textNodes.length; i < len; ++i) { 5388 textNode = textNodes[i]; 5389 ancestorWithClass = applier.getSelfOrAncestorWithClass(textNode); 5390 if (ancestorWithClass && applier.isModifiable(textNode)) { 5391 applier.undoToAncestor(ancestorWithClass, positionsToPreserve); 5392 } 5393 } 5394 // Ensure the range is still valid 5395 range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length); 5396 5397 5398 if (applier.normalize) { 5399 applier.postApply(textNodes, range, positionsToPreserve, true); 5400 } 5401 5402 // Update the ranges from the preserved boundary positions 5403 updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve); 5404 } 5405 5406 // Remove class from any appropriate empty elements 5407 var emptyElements = applier.getEmptyElements(range); 5408 5409 forEach(emptyElements, function(el) { 5410 removeClass(el, applier.className); 5411 }); 5412 }, 5413 5414 undoToRanges: function(ranges) { 5415 // Get ranges returned in document order 5416 var i = ranges.length; 5417 5418 while (i--) { 5419 this.undoToRange(ranges[i], ranges); 5420 } 5421 5422 return ranges; 5423 }, 5424 5425 undoToSelection: function(win) { 5426 var sel = api.getSelection(win); 5427 var ranges = api.getSelection(win).getAllRanges(); 5428 this.undoToRanges(ranges); 5429 sel.setRanges(ranges); 5430 }, 5431 5432 isAppliedToRange: function(range) { 5433 if (range.collapsed || range.toString() == "") { 5434 return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer); 5435 } else { 5436 var textNodes = range.getNodes( [3] ); 5437 if (textNodes.length) 5438 for (var i = 0, textNode; textNode = textNodes[i++]; ) { 5439 if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode) && 5440 this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) { 5441 return false; 5442 } 5443 } 5444 return true; 5445 } 5446 }, 5447 5448 isAppliedToRanges: function(ranges) { 5449 var i = ranges.length; 5450 if (i == 0) { 5451 return false;
5452 } 5453 while (i--) { 5454 if (!this.isAppliedToRange(ranges[i])) { 5455 return false; 5456 } 5457 } 5458 return true; 5459 }, 5460 5461 isAppliedToSelection: function(win) { 5462 var sel = api.getSelection(win); 5463 return this.isAppliedToRanges(sel.getAllRanges()); 5464 }, 5465 5466 toggleRange: function(range) { 5467 if (this.isAppliedToRange(range)) { 5468 this.undoToRange(range); 5469 } else { 5470 this.applyToRange(range); 5471 } 5472 }, 5473 5474 toggleSelection: function(win) { 5475 if (this.isAppliedToSelection(win)) { 5476 this.undoToSelection(win); 5477 } else { 5478 this.applyToSelection(win); 5479 } 5480 }, 5481 5482 getElementsWithClassIntersectingRange: function(range) { 5483 var elements = []; 5484 var applier = this; 5485 range.getNodes([3], function(textNode) { 5486 var el = applier.getSelfOrAncestorWithClass(textNode); 5487 if (el && !contains(elements, el)) { 5488 elements.push(el); 5489 } 5490 }); 5491 return elements; 5492 }, 5493 5494 detach: function() {} 5495 }; 5496 5497 function createClassApplier(className, options, tagNames) { 5498 return new ClassApplier(className, options, tagNames); 5499 } 5500 5501 ClassApplier.util = { 5502 hasClass: hasClass, 5503 addClass: addClass, 5504 removeClass: removeClass, 5505 getClass: getClass, 5506 hasSameClasses: haveSameClasses, 5507 hasAllClasses: hasAllClasses, 5508 replaceWithOwnChildren: replaceWithOwnChildrenPreservingPositions, 5509 elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes, 5510 elementHasNonClassAttributes: elementHasNonClassAttributes, 5511 splitNodeAt: splitNodeAt, 5512 isEditableElement: isEditableElement, 5513 isEditingHost: isEditingHost, 5514 isEditable: isEditable 5515 }; 5516 5517 api.CssClassApplier = api.ClassApplier = ClassApplier; 5518 api.createClassApplier = createClassApplier; 5519 util.createAliasForDeprecatedMethod(api, "createCssClassApplier", "createClassApplier", module); 5520 }); 5521 5522 return rangy; 5523}, this); 5524 5525/** 5526 * Class Applier module for Rangy. 5527 * Adds, removes and toggles classes on Ranges and Selections 5528 * 5529 * Part of Rangy, a cross-browser JavaScript range and selection library 5530 * https://github.com/timdown/rangy 5531 * 5532 * Depends on Rangy core. 5533 * 5534 * Copyright 2015, Tim Down 5535 * Licensed under the MIT license. 5536 * Version: 1.3.1-dev 5537 * Build date: 20 May 2015 5538 */ 5539 (function(factory, root) { 5540 if (typeof define == "function" && define.amd) { 5541 // AMD. Register as an anonymous module with a dependency on Rangy. 5542 define(["./rangy-core"], factory); 5543 } else if (typeof module != "undefined" && typeof exports == "object") { 5544 // Node/CommonJS style 5545 module.exports = factory( require("rangy") ); 5546 } else { 5547 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 5548 factory(root.rangy); 5549 } 5550})(function(rangy) { 5551 rangy.createModule("ClassApplier", ["WrappedSelection"], function(api, module) { 5552 var dom = api.dom; 5553 var DomPosition = dom.DomPosition; 5554 var contains = dom.arrayContains; 5555 var util = api.util; 5556 var forEach = util.forEach; 5557 5558 5559 var defaultTagName = "span"; 5560 var createElementNSSupported = util.isHostMethod(document, "createElementNS"); 5561 5562 function each(obj, func) { 5563 for (var i in obj) { 5564 if (obj.hasOwnProperty(i)) { 5565 if (func(i, obj[i]) === false) { 5566 return false; 5567 } 5568 } 5569 } 5570 return true; 5571 } 5572 5573 function trim(str) { 5574 return str.replace(/^\s\s*/, "").replace(/\s\s*$/, ""); 5575 } 5576 5577 function classNameContainsClass(fullClassName, className) { 5578 return !!fullClassName && new RegExp("(?:^|\\s)" + className + "(?:\\s|$)").test(fullClassName); 5579 } 5580 5581 // Inefficient, inelegant nonsense for IE's svg element, which has no classList and non-HTML className implementation 5582 function hasClass(el, className) { 5583 if (typeof el.classList == "object") { 5584 return el.classList.contains(className); 5585 } else { 5586 var classNameSupported = (typeof el.className == "string"); 5587 var elClass = classNameSupported ? el.className : el.getAttribute("class"); 5588 return classNameContainsClass(elClass, className); 5589 } 5590 } 5591 5592 function addClass(el, className) { 5593 if (typeof el.classList == "object") { 5594 el.classList.add(className); 5595 } else { 5596 var classNameSupported = (typeof el.className == "string"); 5597 var elClass = classNameSupported ? el.className : el.getAttribute("class"); 5598 if (elClass) { 5599 if (!classNameContainsClass(elClass, className)) { 5600 elClass += " " + className; 5601 } 5602 } else { 5603 elClass = className; 5604 } 5605 if (classNameSupported) { 5606 el.className = elClass; 5607 } else { 5608 el.setAttribute("class", elClass); 5609 } 5610 } 5611 } 5612 5613 var removeClass = (function() { 5614 function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) { 5615 return (whiteSpaceBefore && whiteSpaceAfter) ? " " : ""; 5616 } 5617 5618 return function(el, className) { 5619 if (typeof el.classList == "object") { 5620 el.classList.remove(className); 5621 } else { 5622 var classNameSupported = (typeof el.className == "string"); 5623 var elClass = classNameSupported ? el.className : el.getAttribute("class"); 5624 elClass = elClass.replace(new RegExp("(^|\\s)" + className + "(\\s|$)"), replacer); 5625 if (classNameSupported) { 5626 el.className = elClass; 5627 } else { 5628 el.setAttribute("class", elClass); 5629 } 5630 } 5631 }; 5632 })(); 5633 5634 function getClass(el) { 5635 var classNameSupported = (typeof el.className == "string"); 5636 return classNameSupported ? el.className : el.getAttribute("class"); 5637 } 5638 5639 function sortClassName(className) { 5640 return className && className.split(/\s+/).sort().join(" "); 5641 } 5642 5643 function getSortedClassName(el) { 5644 return sortClassName( getClass(el) ); 5645 } 5646 5647 function haveSameClasses(el1, el2) { 5648 return getSortedClassName(el1) == getSortedClassName(el2); 5649 } 5650 5651 function hasAllClasses(el, className) { 5652 var classes = className.split(/\s+/); 5653 for (var i = 0, len = classes.length; i < len; ++i) { 5654 if (!hasClass(el, trim(classes[i]))) { 5655 return false;
5656 } 5657 } 5658 return true; 5659 } 5660 5661 function canTextBeStyled(textNode) { 5662 var parent = textNode.parentNode; 5663 return (parent && parent.nodeType == 1 && !/^(textarea|style|script|select|iframe)$/i.test(parent.nodeName)); 5664 } 5665 5666 function movePosition(position, oldParent, oldIndex, newParent, newIndex) { 5667 var posNode = position.node, posOffset = position.offset; 5668 var newNode = posNode, newOffset = posOffset; 5669 5670 if (posNode == newParent && posOffset > newIndex) { 5671 ++newOffset; 5672 } 5673 5674 if (posNode == oldParent && (posOffset == oldIndex || posOffset == oldIndex + 1)) { 5675 newNode = newParent; 5676 newOffset += newIndex - oldIndex; 5677 } 5678 5679 if (posNode == oldParent && posOffset > oldIndex + 1) { 5680 --newOffset; 5681 } 5682 5683 position.node = newNode; 5684 position.offset = newOffset; 5685 } 5686 5687 function movePositionWhenRemovingNode(position, parentNode, index) { 5688 if (position.node == parentNode && position.offset > index) { 5689 --position.offset; 5690 } 5691 } 5692 5693 function movePreservingPositions(node, newParent, newIndex, positionsToPreserve) { 5694 // For convenience, allow newIndex to be -1 to mean "insert at the end". 5695 if (newIndex == -1) { 5696 newIndex = newParent.childNodes.length; 5697 } 5698 5699 var oldParent = node.parentNode; 5700 var oldIndex = dom.getNodeIndex(node); 5701 5702 forEach(positionsToPreserve, function(position) { 5703 movePosition(position, oldParent, oldIndex, newParent, newIndex); 5704 }); 5705 5706 // Now actually move the node. 5707 if (newParent.childNodes.length == newIndex) { 5708 newParent.appendChild(node); 5709 } else { 5710 newParent.insertBefore(node, newParent.childNodes[newIndex]); 5711 } 5712 } 5713 5714 function removePreservingPositions(node, positionsToPreserve) { 5715 5716 var oldParent = node.parentNode; 5717 var oldIndex = dom.getNodeIndex(node); 5718 5719 forEach(positionsToPreserve, function(position) { 5720 movePositionWhenRemovingNode(position, oldParent, oldIndex); 5721 }); 5722 5723 dom.removeNode(node); 5724 } 5725 5726 function moveChildrenPreservingPositions(node, newParent, newIndex, removeNode, positionsToPreserve) { 5727 var child, children = []; 5728 while ( (child = node.firstChild) ) { 5729 movePreservingPositions(child, newParent, newIndex++, positionsToPreserve); 5730 children.push(child); 5731 } 5732 if (removeNode) { 5733 removePreservingPositions(node, positionsToPreserve); 5734 } 5735 return children; 5736 } 5737 5738 function replaceWithOwnChildrenPreservingPositions(element, positionsToPreserve) { 5739 return moveChildrenPreservingPositions(element, element.parentNode, dom.getNodeIndex(element), true, positionsToPreserve); 5740 } 5741 5742 function rangeSelectsAnyText(range, textNode) { 5743 var textNodeRange = range.cloneRange(); 5744 textNodeRange.selectNodeContents(textNode); 5745 5746 var intersectionRange = textNodeRange.intersection(range); 5747 var text = intersectionRange ? intersectionRange.toString() : ""; 5748 5749 return text != ""; 5750 } 5751 5752 function getEffectiveTextNodes(range) { 5753 var nodes = range.getNodes([3]); 5754 5755 // Optimization as per issue 145 5756 5757 // Remove non-intersecting text nodes from the start of the range 5758 var start = 0, node; 5759 while ( (node = nodes[start]) && !rangeSelectsAnyText(range, node) ) { 5760 ++start; 5761 } 5762 5763 // Remove non-intersecting text nodes from the start of the range 5764 var end = nodes.length - 1; 5765 while ( (node = nodes[end]) && !rangeSelectsAnyText(range, node) ) { 5766 --end; 5767 } 5768 5769 return nodes.slice(start, end + 1); 5770 } 5771 5772 function elementsHaveSameNonClassAttributes(el1, el2) { 5773 if (el1.attributes.length != el2.attributes.length) return false;
5774 for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) { 5775 attr1 = el1.attributes[i]; 5776 name = attr1.name; 5777 if (name != "class") { 5778 attr2 = el2.attributes.getNamedItem(name); 5779 if ( (attr1 === null) != (attr2 === null) ) return false; 5780 if (attr1.specified != attr2.specified) return false; 5781 if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false; 5782 } 5783 } 5784 return true; 5785 } 5786 5787 function elementHasNonClassAttributes(el, exceptions) { 5788 for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) { 5789 attrName = el.attributes[i].name; 5790 if ( !(exceptions && contains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") { 5791 return true; 5792 } 5793 } 5794 return false; 5795 } 5796 5797 var getComputedStyleProperty = dom.getComputedStyleProperty; 5798 var isEditableElement = (function() { 5799 var testEl = document.createElement("div"); 5800 return typeof testEl.isContentEditable == "boolean" ? 5801 function (node) { 5802 return node && node.nodeType == 1 && node.isContentEditable; 5803 } : 5804 function (node) { 5805 if (!node || node.nodeType != 1 || node.contentEditable == "false") { 5806 return false; 5807 } 5808 return node.contentEditable == "true" || isEditableElement(node.parentNode); 5809 }; 5810 })(); 5811 5812 function isEditingHost(node) { 5813 var parent; 5814 return node && node.nodeType == 1 && 5815 (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on") || 5816 (isEditableElement(node) && !isEditableElement(node.parentNode))); 5817 } 5818 5819 function isEditable(node) { 5820 return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node); 5821 } 5822 5823 var inlineDisplayRegex = /^inline(-block|-table)?$/i; 5824 5825 function isNonInlineElement(node) { 5826 return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display")); 5827 } 5828 5829 // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html) 5830 var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/; 5831 5832 function isUnrenderedWhiteSpaceNode(node) { 5833 if (node.data.length == 0) { 5834 return true; 5835 } 5836 if (htmlNonWhiteSpaceRegex.test(node.data)) { 5837 return false; 5838 } 5839 var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace"); 5840 switch (cssWhiteSpace) { 5841 case "pre": 5842 case "pre-wrap": 5843 case "-moz-pre-wrap": 5844 return false; 5845 case "pre-line": 5846 if (/[\r\n]/.test(node.data)) { 5847 return false; 5848 } 5849 } 5850 5851 // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a 5852 // non-inline element, it will not be rendered. This seems to be a good enough definition. 5853 return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling); 5854 } 5855 5856 function getRangeBoundaries(ranges) { 5857 var positions = [], i, range; 5858 for (i = 0; range = ranges[i++]; ) { 5859 positions.push( 5860 new DomPosition(range.startContainer, range.startOffset), 5861 new DomPosition(range.endContainer, range.endOffset) 5862 ); 5863 } 5864 return positions; 5865 } 5866 5867 function updateRangesFromBoundaries(ranges, positions) { 5868 for (var i = 0, range, start, end, len = ranges.length; i < len; ++i) { 5869 range = ranges[i]; 5870 start = positions[i * 2]; 5871 end = positions[i * 2 + 1]; 5872 range.setStartAndEnd(start.node, start.offset, end.node, end.offset); 5873 } 5874 } 5875 5876 function isSplitPoint(node, offset) { 5877 if (dom.isCharacterDataNode(node)) { 5878 if (offset == 0) { 5879 return !!node.previousSibling; 5880 } else if (offset == node.length) { 5881 return !!node.nextSibling; 5882 } else { 5883 return true; 5884 } 5885 } 5886 5887 return offset > 0 && offset < node.childNodes.length; 5888 } 5889 5890 function splitNodeAt(node, descendantNode, descendantOffset, positionsToPreserve) { 5891 var newNode, parentNode; 5892 var splitAtStart = (descendantOffset == 0); 5893 5894 if (dom.isAncestorOf(descendantNode, node)) { 5895 return node; 5896 } 5897 5898 if (dom.isCharacterDataNode(descendantNode)) { 5899 var descendantIndex = dom.getNodeIndex(descendantNode); 5900 if (descendantOffset == 0) {
5901 descendantOffset = descendantIndex; 5902 } else if (descendantOffset == descendantNode.length) { 5903 descendantOffset = descendantIndex + 1; 5904 } else { 5905 throw module.createError("splitNodeAt() should not be called with offset in the middle of a data node (" + 5906 descendantOffset + " in " + descendantNode.data); 5907 } 5908 descendantNode = descendantNode.parentNode; 5909 } 5910 5911 if (isSplitPoint(descendantNode, descendantOffset)) { 5912 // descendantNode is now guaranteed not to be a text or other character node 5913 newNode = descendantNode.cloneNode(false); 5914 parentNode = descendantNode.parentNode; 5915 if (newNode.id) { 5916 newNode.removeAttribute("id"); 5917 } 5918 var child, newChildIndex = 0; 5919 5920 while ( (child = descendantNode.childNodes[descendantOffset]) ) { 5921 movePreservingPositions(child, newNode, newChildIndex++, positionsToPreserve); 5922 } 5923 movePreservingPositions(newNode, parentNode, dom.getNodeIndex(descendantNode) + 1, positionsToPreserve); 5924 return (descendantNode == node) ? newNode : splitNodeAt(node, parentNode, dom.getNodeIndex(newNode), positionsToPreserve); 5925 } else if (node != descendantNode) { 5926 newNode = descendantNode.parentNode; 5927 5928 // Work out a new split point in the parent node 5929 var newNodeIndex = dom.getNodeIndex(descendantNode); 5930 5931 if (!splitAtStart) { 5932 newNodeIndex++; 5933 } 5934 return splitNodeAt(node, newNode, newNodeIndex, positionsToPreserve); 5935 } 5936 return node; 5937 } 5938 5939 function areElementsMergeable(el1, el2) { 5940 return el1.namespaceURI == el2.namespaceURI && 5941 el1.tagName.toLowerCase() == el2.tagName.toLowerCase() && 5942 haveSameClasses(el1, el2) && 5943 elementsHaveSameNonClassAttributes(el1, el2) && 5944 getComputedStyleProperty(el1, "display") == "inline" && 5945 getComputedStyleProperty(el2, "display") == "inline"; 5946 } 5947 5948 function createAdjacentMergeableTextNodeGetter(forward) { 5949 var siblingPropName = forward ? "nextSibling" : "previousSibling"; 5950 5951 return function(textNode, checkParentElement) { 5952 var el = textNode.parentNode; 5953 var adjacentNode = textNode[siblingPropName]; 5954 if (adjacentNode) { 5955 // Can merge if the node's previous/next sibling is a text node 5956 if (adjacentNode && adjacentNode.nodeType == 3) { 5957 return adjacentNode; 5958 } 5959 } else if (checkParentElement) { 5960 // Compare text node parent element with its sibling 5961 adjacentNode = el[siblingPropName]; 5962 if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) { 5963 var adjacentNodeChild = adjacentNode[forward ? "firstChild" : "lastChild"]; 5964 if (adjacentNodeChild && adjacentNodeChild.nodeType == 3) { 5965 return adjacentNodeChild; 5966 } 5967 } 5968 } 5969 return null; 5970 }; 5971 } 5972 5973 var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false), 5974 getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true); 5975 5976 5977 function Merge(firstNode) { 5978 this.isElementMerge = (firstNode.nodeType == 1); 5979 this.textNodes = []; 5980 var firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; 5981 if (firstTextNode) { 5982 this.textNodes[0] = firstTextNode; 5983 } 5984 } 5985 5986 Merge.prototype = { 5987 doMerge: function(positionsToPreserve) { 5988 var textNodes = this.textNodes; 5989 var firstTextNode = textNodes[0]; 5990 if (textNodes.length > 1) { 5991 var firstTextNodeIndex = dom.getNodeIndex(firstTextNode); 5992 var textParts = [], combinedTextLength = 0, textNode, parent;
5993 forEach(textNodes, function(textNode, i) { 5994 parent = textNode.parentNode; 5995 if (i > 0) { 5996 parent.removeChild(textNode); 5997 if (!parent.hasChildNodes()) { 5998 dom.removeNode(parent); 5999 } 6000 if (positionsToPreserve) { 6001 forEach(positionsToPreserve, function(position) { 6002 // Handle case where position is inside the text node being merged into a preceding node 6003 if (position.node == textNode) { 6004 position.node = firstTextNode; 6005 position.offset += combinedTextLength; 6006 } 6007 // Handle case where both text nodes precede the position within the same parent node 6008 if (position.node == parent && position.offset > firstTextNodeIndex) { 6009 --position.offset; 6010 if (position.offset == firstTextNodeIndex + 1 && i < len - 1) { 6011 position.node = firstTextNode; 6012 position.offset = combinedTextLength; 6013 } 6014 } 6015 }); 6016 } 6017 } 6018 textParts[i] = textNode.data; 6019 combinedTextLength += textNode.data.length; 6020 }); 6021 firstTextNode.data = textParts.join(""); 6022 } 6023 return firstTextNode.data; 6024 }, 6025 6026 getLength: function() { 6027 var i = this.textNodes.length, len = 0; 6028 while (i--) { 6029 len += this.textNodes[i].length; 6030 } 6031 return len; 6032 }, 6033 6034 toString: function() { 6035 var textParts = []; 6036 forEach(this.textNodes, function(textNode, i) { 6037 textParts[i] = "'" + textNode.data + "'"; 6038 }); 6039 return "[Merge(" + textParts.join(",") + ")]"; 6040 } 6041 }; 6042 6043 var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly", "useExistingElements", 6044 "removeEmptyElements", "onElementCreate"]; 6045 6046 // TODO: Populate this with every attribute name that corresponds to a property with a different name. Really?? 6047 var attrNamesForProperties = {}; 6048 6049 function ClassApplier(className, options, tagNames) { 6050 var normalize, i, len, propName, applier = this; 6051 applier.cssClass = applier.className = className; // cssClass property is for backward compatibility 6052 6053 var elementPropertiesFromOptions = null, elementAttributes = {}; 6054 6055 // Initialize from options object 6056 if (typeof options == "object" && options !== null) { 6057 if (typeof options.elementTagName !== "undefined") { 6058 options.elementTagName = options.elementTagName.toLowerCase(); 6059 } 6060 tagNames = options.tagNames; 6061 elementPropertiesFromOptions = options.elementProperties; 6062 elementAttributes = options.elementAttributes; 6063 6064 for (i = 0; propName = optionProperties[i++]; ) { 6065 if (options.hasOwnProperty(propName)) { 6066 applier[propName] = options[propName]; 6067 } 6068 } 6069 normalize = options.normalize; 6070 } else { 6071 normalize = options; 6072 } 6073 6074 // Backward compatibility: the second parameter can also be a Boolean indicating to normalize after unapplying 6075 applier.normalize = (typeof normalize == "undefined") ? true : normalize; 6076 6077 // Initialize element properties and attribute exceptions 6078 applier.attrExceptions = []; 6079 var el = document.createElement(applier.elementTagName); 6080 applier.elementProperties = applier.copyPropertiesToElement(elementPropertiesFromOptions, el, true); 6081 each(elementAttributes, function(attrName, attrValue) { 6082 applier.attrExceptions.push(attrName); 6083 // Ensure each attribute value is a string 6084 elementAttributes[attrName] = "" + attrValue; 6085 }); 6086 applier.elementAttributes = elementAttributes; 6087 6088 applier.elementSortedClassName = applier.elementProperties.hasOwnProperty("className") ? 6089 sortClassName(applier.elementProperties.className + " " + className) : className; 6090 6091 // Initialize tag names 6092 applier.applyToAnyTagName = false;
6093 var type = typeof tagNames; 6094 if (type == "string") { 6095 if (tagNames == "*") { 6096 applier.applyToAnyTagName = true; 6097 } else { 6098 applier.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/); 6099 } 6100 } else if (type == "object" && typeof tagNames.length == "number") { 6101 applier.tagNames = []; 6102 for (i = 0, len = tagNames.length; i < len; ++i) { 6103 if (tagNames[i] == "*") { 6104 applier.applyToAnyTagName = true; 6105 } else { 6106 applier.tagNames.push(tagNames[i].toLowerCase()); 6107 } 6108 } 6109 } else { 6110 applier.tagNames = [applier.elementTagName]; 6111 } 6112 } 6113 6114 ClassApplier.prototype = { 6115 elementTagName: defaultTagName, 6116 elementProperties: {}, 6117 elementAttributes: {}, 6118 ignoreWhiteSpace: true, 6119 applyToEditableOnly: false, 6120 useExistingElements: true, 6121 removeEmptyElements: true, 6122 onElementCreate: null, 6123 6124 copyPropertiesToElement: function(props, el, createCopy) { 6125 var s, elStyle, elProps = {}, elPropsStyle, propValue, elPropValue, attrName; 6126 6127 for (var p in props) { 6128 if (props.hasOwnProperty(p)) { 6129 propValue = props[p]; 6130 elPropValue = el[p]; 6131 6132 // Special case for class. The copied properties object has the applier's class as well as its own 6133 // to simplify checks when removing styling elements 6134 if (p == "className") { 6135 addClass(el, propValue); 6136 addClass(el, this.className); 6137 el[p] = sortClassName(el[p]); 6138 if (createCopy) { 6139 elProps[p] = propValue; 6140 } 6141 } 6142 6143 // Special case for style 6144 else if (p == "style") { 6145 elStyle = elPropValue; 6146 if (createCopy) { 6147 elProps[p] = elPropsStyle = {}; 6148 } 6149 for (s in props[p]) { 6150 if (props[p].hasOwnProperty(s)) { 6151 elStyle[s] = propValue[s]; 6152 if (createCopy) { 6153 elPropsStyle[s] = elStyle[s]; 6154 } 6155 } 6156 } 6157 this.attrExceptions.push(p); 6158 } else { 6159 el[p] = propValue; 6160 // Copy the property back from the dummy element so that later comparisons to check whether 6161 // elements may be removed are checking against the right value. For example, the href property 6162 // of an element returns a fully qualified URL even if it was previously assigned a relative 6163 // URL. 6164 if (createCopy) { 6165 elProps[p] = el[p]; 6166 6167 // Not all properties map to identically-named attributes 6168 attrName = attrNamesForProperties.hasOwnProperty(p) ? attrNamesForProperties[p] : p; 6169 this.attrExceptions.push(attrName); 6170 } 6171 } 6172 } 6173 } 6174 6175 return createCopy ? elProps : ""; 6176 }, 6177 6178 copyAttributesToElement: function(attrs, el) { 6179 for (var attrName in attrs) { 6180 if (attrs.hasOwnProperty(attrName) && !/^class(?:Name)?$/i.test(attrName)) { 6181 el.setAttribute(attrName, attrs[attrName]); 6182 } 6183 } 6184 }, 6185 6186 appliesToElement: function(el) { 6187 return contains(this.tagNames, el.tagName.toLowerCase()); 6188 }, 6189 6190 getEmptyElements: function(range) { 6191 var applier = this; 6192 return range.getNodes([1], function(el) { 6193 return applier.appliesToElement(el) && !el.hasChildNodes(); 6194 }); 6195 }, 6196 6197 hasClass: function(node) { 6198 return node.nodeType == 1 && 6199 (this.applyToAnyTagName || this.appliesToElement(node)) && 6200 hasClass(node, this.className); 6201 }, 6202 6203 getSelfOrAncestorWithClass: function(node) { 6204 while (node) { 6205 if (this.hasClass(node)) { 6206 return node; 6207 } 6208 node = node.parentNode; 6209 } 6210 return null; 6211 }, 6212 6213 isModifiable: function(node) { 6214 return !this.applyToEditableOnly || isEditable(node); 6215 }, 6216 6217 // White space adjacent to an unwrappable node can be ignored for wrapping 6218 isIgnorableWhiteSpaceNode: function(node) { 6219 return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node); 6220 }, 6221 6222 // Normalizes nodes after applying a class to a Range. 6223 postApply: function(textNodes, range, positionsToPreserve, isUndo) { 6224 var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1]; 6225 6226 var merges = [], currentMerge; 6227 6228 var rangeStartNode = firstNode, rangeEndNode = lastNode; 6229 var rangeStartOffset = 0, rangeEndOffset = lastNode.length; 6230 6231 var textNode, precedingTextNode; 6232 6233 // Check for every required merge and create a Merge object for each
6234 forEach(textNodes, function(textNode) { 6235 precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo); 6236 if (precedingTextNode) { 6237 if (!currentMerge) { 6238 currentMerge = new Merge(precedingTextNode); 6239 merges.push(currentMerge); 6240 } 6241 currentMerge.textNodes.push(textNode); 6242 if (textNode === firstNode) { 6243 rangeStartNode = currentMerge.textNodes[0]; 6244 rangeStartOffset = rangeStartNode.length; 6245 } 6246 if (textNode === lastNode) { 6247 rangeEndNode = currentMerge.textNodes[0]; 6248 rangeEndOffset = currentMerge.getLength(); 6249 } 6250 } else { 6251 currentMerge = null; 6252 } 6253 }); 6254 6255 // Test whether the first node after the range needs merging 6256 var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo); 6257 6258 if (nextTextNode) { 6259 if (!currentMerge) { 6260 currentMerge = new Merge(lastNode); 6261 merges.push(currentMerge); 6262 } 6263 currentMerge.textNodes.push(nextTextNode); 6264 } 6265 6266 // Apply the merges 6267 if (merges.length) { 6268 for (i = 0, len = merges.length; i < len; ++i) { 6269 merges[i].doMerge(positionsToPreserve); 6270 } 6271 6272 // Set the range boundaries 6273 range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset); 6274 } 6275 }, 6276 6277 createContainer: function(parentNode) { 6278 var doc = dom.getDocument(parentNode); 6279 var namespace; 6280 var el = createElementNSSupported && !dom.isHtmlNamespace(parentNode) && (namespace = parentNode.namespaceURI) ? 6281 doc.createElementNS(parentNode.namespaceURI, this.elementTagName) : 6282 doc.createElement(this.elementTagName); 6283 6284 this.copyPropertiesToElement(this.elementProperties, el, false); 6285 this.copyAttributesToElement(this.elementAttributes, el); 6286 addClass(el, this.className); 6287 if (this.onElementCreate) { 6288 this.onElementCreate(el, this); 6289 } 6290 return el; 6291 }, 6292 6293 elementHasProperties: function(el, props) { 6294 var applier = this; 6295 return each(props, function(p, propValue) { 6296 if (p == "className") { 6297 // For checking whether we should reuse an existing element, we just want to check that the element 6298 // has all the classes specified in the className property. When deciding whether the element is 6299 // removable when unapplying a class, there is separate special handling to check whether the 6300 // element has extra classes so the same simple check will do. 6301 return hasAllClasses(el, propValue); 6302 } else if (typeof propValue == "object") { 6303 if (!applier.elementHasProperties(el[p], propValue)) { 6304 return false; 6305 } 6306 } else if (el[p] !== propValue) { 6307 return false; 6308 } 6309 }); 6310 }, 6311 6312 elementHasAttributes: function(el, attrs) { 6313 return each(attrs, function(name, value) { 6314 if (el.getAttribute(name) !== value) { 6315 return false; 6316 } 6317 }); 6318 }, 6319 6320 applyToTextNode: function(textNode, positionsToPreserve) { 6321 6322 // Check whether the text node can be styled. Text within a <style> or <script> element, for example, 6323 // should not be styled. See issue 283. 6324 if (canTextBeStyled(textNode)) { 6325 var parent = textNode.parentNode; 6326 if (parent.childNodes.length == 1 && 6327 this.useExistingElements && 6328 this.appliesToElement(parent) && 6329 this.elementHasProperties(parent, this.elementProperties) && 6330 this.elementHasAttributes(parent, this.elementAttributes)) { 6331 6332 addClass(parent, this.className); 6333 } else { 6334 var textNodeParent = textNode.parentNode; 6335 var el = this.createContainer(textNodeParent); 6336 textNodeParent.insertBefore(el, textNode); 6337 el.appendChild(textNode); 6338 } 6339 } 6340 6341 }, 6342 6343 isRemovable: function(el) { 6344 return el.tagName.toLowerCase() == this.elementTagName && 6345 getSortedClassName(el) == this.elementSortedClassName && 6346 this.elementHasProperties(el, this.elementProperties) && 6347 !elementHasNonClassAttributes(el, this.attrExceptions) && 6348 this.elementHasAttributes(el, this.elementAttributes) && 6349 this.isModifiable(el); 6350 }, 6351 6352 isEmptyContainer: function(el) { 6353 var childNodeCount = el.childNodes.length; 6354 return el.nodeType == 1 && 6355 this.isRemovable(el) && 6356 (childNodeCount == 0 || (childNodeCount == 1 && this.isEmptyContainer(el.firstChild))); 6357 }, 6358 6359 removeEmptyContainers: function(range) { 6360 var applier = this; 6361 var nodesToRemove = range.getNodes([1], function(el) { 6362 return applier.isEmptyContainer(el); 6363 }); 6364 6365 var rangesToPreserve = [range]; 6366 var positionsToPreserve = getRangeBoundaries(rangesToPreserve); 6367
6368 forEach(nodesToRemove, function(node) { 6369 removePreservingPositions(node, positionsToPreserve); 6370 }); 6371 6372 // Update the range from the preserved boundary positions 6373 updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve); 6374 }, 6375 6376 undoToTextNode: function(textNode, range, ancestorWithClass, positionsToPreserve) { 6377 if (!range.containsNode(ancestorWithClass)) { 6378 // Split out the portion of the ancestor from which we can remove the class 6379 //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass); 6380 var ancestorRange = range.cloneRange(); 6381 ancestorRange.selectNode(ancestorWithClass); 6382 if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)) { 6383 splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, positionsToPreserve); 6384 range.setEndAfter(ancestorWithClass); 6385 } 6386 if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)) { 6387 ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, positionsToPreserve); 6388 } 6389 } 6390 6391 if (this.isRemovable(ancestorWithClass)) { 6392 replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve); 6393 } else { 6394 removeClass(ancestorWithClass, this.className); 6395 } 6396 }, 6397 6398 splitAncestorWithClass: function(container, offset, positionsToPreserve) { 6399 var ancestorWithClass = this.getSelfOrAncestorWithClass(container); 6400 if (ancestorWithClass) { 6401 splitNodeAt(ancestorWithClass, container, offset, positionsToPreserve); 6402 } 6403 }, 6404 6405 undoToAncestor: function(ancestorWithClass, positionsToPreserve) { 6406 if (this.isRemovable(ancestorWithClass)) { 6407 replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve); 6408 } else { 6409 removeClass(ancestorWithClass, this.className); 6410 } 6411 }, 6412 6413 applyToRange: function(range, rangesToPreserve) { 6414 var applier = this; 6415 rangesToPreserve = rangesToPreserve || []; 6416 6417 // Create an array of range boundaries to preserve 6418 var positionsToPreserve = getRangeBoundaries(rangesToPreserve || []); 6419 6420 range.splitBoundariesPreservingPositions(positionsToPreserve); 6421 6422 // Tidy up the DOM by removing empty containers 6423 if (applier.removeEmptyElements) { 6424 applier.removeEmptyContainers(range); 6425 } 6426 6427 var textNodes = getEffectiveTextNodes(range); 6428 6429 if (textNodes.length) { 6430 forEach(textNodes, function(textNode) { 6431 if (!applier.isIgnorableWhiteSpaceNode(textNode) && !applier.getSelfOrAncestorWithClass(textNode) && 6432 applier.isModifiable(textNode)) { 6433 applier.applyToTextNode(textNode, positionsToPreserve); 6434 } 6435 }); 6436 var lastTextNode = textNodes[textNodes.length - 1]; 6437 range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length); 6438 if (applier.normalize) { 6439 applier.postApply(textNodes, range, positionsToPreserve, false); 6440 } 6441 6442 // Update the ranges from the preserved boundary positions 6443 updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve); 6444 } 6445 6446 // Apply classes to any appropriate empty elements 6447 var emptyElements = applier.getEmptyElements(range); 6448
6449 forEach(emptyElements, function(el) { 6450 addClass(el, applier.className); 6451 }); 6452 }, 6453 6454 applyToRanges: function(ranges) { 6455 6456 var i = ranges.length; 6457 while (i--) { 6458 this.applyToRange(ranges[i], ranges); 6459 } 6460 6461 6462 return ranges; 6463 }, 6464 6465 applyToSelection: function(win) { 6466 var sel = api.getSelection(win); 6467 sel.setRanges( this.applyToRanges(sel.getAllRanges()) ); 6468 }, 6469 6470 undoToRange: function(range, rangesToPreserve) { 6471 var applier = this; 6472 // Create an array of range boundaries to preserve 6473 rangesToPreserve = rangesToPreserve || []; 6474 var positionsToPreserve = getRangeBoundaries(rangesToPreserve); 6475 6476 6477 range.splitBoundariesPreservingPositions(positionsToPreserve); 6478 6479 // Tidy up the DOM by removing empty containers 6480 if (applier.removeEmptyElements) { 6481 applier.removeEmptyContainers(range, positionsToPreserve); 6482 } 6483 6484 var textNodes = getEffectiveTextNodes(range); 6485 var textNode, ancestorWithClass; 6486 var lastTextNode = textNodes[textNodes.length - 1]; 6487 6488 if (textNodes.length) { 6489 applier.splitAncestorWithClass(range.endContainer, range.endOffset, positionsToPreserve); 6490 applier.splitAncestorWithClass(range.startContainer, range.startOffset, positionsToPreserve); 6491 for (var i = 0, len = textNodes.length; i < len; ++i) { 6492 textNode = textNodes[i]; 6493 ancestorWithClass = applier.getSelfOrAncestorWithClass(textNode); 6494 if (ancestorWithClass && applier.isModifiable(textNode)) { 6495 applier.undoToAncestor(ancestorWithClass, positionsToPreserve); 6496 } 6497 } 6498 // Ensure the range is still valid 6499 range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length); 6500 6501 6502 if (applier.normalize) { 6503 applier.postApply(textNodes, range, positionsToPreserve, true); 6504 } 6505 6506 // Update the ranges from the preserved boundary positions 6507 updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve); 6508 } 6509 6510 // Remove class from any appropriate empty elements 6511 var emptyElements = applier.getEmptyElements(range); 6512 6513 forEach(emptyElements, function(el) { 6514 removeClass(el, applier.className); 6515 }); 6516 }, 6517 6518 undoToRanges: function(ranges) { 6519 // Get ranges returned in document order 6520 var i = ranges.length; 6521 6522 while (i--) { 6523 this.undoToRange(ranges[i], ranges); 6524 } 6525 6526 return ranges; 6527 }, 6528 6529 undoToSelection: function(win) { 6530 var sel = api.getSelection(win); 6531 var ranges = api.getSelection(win).getAllRanges(); 6532 this.undoToRanges(ranges); 6533 sel.setRanges(ranges); 6534 }, 6535 6536 isAppliedToRange: function(range) { 6537 if (range.collapsed || range.toString() == "") { 6538 return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer); 6539 } else { 6540 var textNodes = range.getNodes( [3] ); 6541 if (textNodes.length) 6542 for (var i = 0, textNode; textNode = textNodes[i++]; ) { 6543 if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode) && 6544 this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) { 6545 return false; 6546 } 6547 } 6548 return true; 6549 } 6550 }, 6551 6552 isAppliedToRanges: function(ranges) { 6553 var i = ranges.length; 6554 if (i == 0) { 6555 return false;
6556 } 6557 while (i--) { 6558 if (!this.isAppliedToRange(ranges[i])) { 6559 return false; 6560 } 6561 } 6562 return true; 6563 }, 6564 6565 isAppliedToSelection: function(win) { 6566 var sel = api.getSelection(win); 6567 return this.isAppliedToRanges(sel.getAllRanges()); 6568 }, 6569 6570 toggleRange: function(range) { 6571 if (this.isAppliedToRange(range)) { 6572 this.undoToRange(range); 6573 } else { 6574 this.applyToRange(range); 6575 } 6576 }, 6577 6578 toggleSelection: function(win) { 6579 if (this.isAppliedToSelection(win)) { 6580 this.undoToSelection(win); 6581 } else { 6582 this.applyToSelection(win); 6583 } 6584 }, 6585 6586 getElementsWithClassIntersectingRange: function(range) { 6587 var elements = []; 6588 var applier = this; 6589 range.getNodes([3], function(textNode) { 6590 var el = applier.getSelfOrAncestorWithClass(textNode); 6591 if (el && !contains(elements, el)) { 6592 elements.push(el); 6593 } 6594 }); 6595 return elements; 6596 }, 6597 6598 detach: function() {} 6599 }; 6600 6601 function createClassApplier(className, options, tagNames) { 6602 return new ClassApplier(className, options, tagNames); 6603 } 6604 6605 ClassApplier.util = { 6606 hasClass: hasClass, 6607 addClass: addClass, 6608 removeClass: removeClass, 6609 getClass: getClass, 6610 hasSameClasses: haveSameClasses, 6611 hasAllClasses: hasAllClasses, 6612 replaceWithOwnChildren: replaceWithOwnChildrenPreservingPositions, 6613 elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes, 6614 elementHasNonClassAttributes: elementHasNonClassAttributes, 6615 splitNodeAt: splitNodeAt, 6616 isEditableElement: isEditableElement, 6617 isEditingHost: isEditingHost, 6618 isEditable: isEditable 6619 }; 6620 6621 api.CssClassApplier = api.ClassApplier = ClassApplier; 6622 api.createClassApplier = createClassApplier; 6623 util.createAliasForDeprecatedMethod(api, "createCssClassApplier", "createClassApplier", module); 6624 }); 6625 6626 return rangy; 6627}, this); 6628 6629 6630/** 6631 * Text range module for Rangy. 6632 * Text-based manipulation and searching of ranges and selections. 6633 * 6634 * Features 6635 * 6636 * - Ability to move range boundaries by character or word offsets 6637 * - Customizable word tokenizer 6638 * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties 6639 * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case 6640 * sensitivity 6641 * - Selection and range save/restore as text offsets within a node 6642 * - Methods to return visible text within a range or selection 6643 * - innerText method for elements 6644 * 6645 * References 6646 * 6647 * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145 6648 * http://aryeh.name/spec/innertext/innertext.html 6649 * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html 6650 * 6651 * Part of Rangy, a cross-browser JavaScript range and selection library 6652 * https://github.com/timdown/rangy 6653 * 6654 * Depends on Rangy core. 6655 * 6656 * Copyright 2015, Tim Down 6657 * Licensed under the MIT license. 6658 * Version: 1.3.1-dev 6659 * Build date: 20 May 2015 6660 */ 6661 6662/** 6663 * Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers. 6664 * 6665 * First, a <br>: this is relatively simple. For the following HTML: 6666 * 6667 * 1 <br>2 6668 * 6669 * - IE and WebKit render the space, include it in the selection (i.e. when the content is selected and pasted into a 6670 * textarea, the space is present) and allow the caret to be placed after it. 6671 * - Firefox does not acknowledge the space in the selection but it is possible to place the caret after it. 6672 * - Opera does not render the space but has two separate caret positions on either side of the space (left and right 6673 * arrow keys show this) and includes the space in the selection. 6674 * 6675 * The other case is the line break or breaks implied by block elements. For the following HTML: 6676 * 6677 * <p>1 </p><p>2<p> 6678 * 6679 * - WebKit does not acknowledge the space in any way 6680 * - Firefox, IE and Opera as per <br> 6681 *
6682 * One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML: 6683 * 6684 * <p style="white-space: pre-line">1 6685 * 2</p> 6686 * 6687 * - Firefox and WebKit include the space in caret positions 6688 * - IE does not support pre-line up to and including version 9 6689 * - Opera ignores the space 6690 * - Trailing space only renders if there is a non-collapsed character in the line 6691 * 6692 * Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be 6693 * feature-tested 6694 */ 6695 (function(factory, root) { 6696 if (typeof define == "function" && define.amd) { 6697 // AMD. Register as an anonymous module with a dependency on Rangy. 6698 define(["./rangy-core"], factory); 6699 } else if (typeof module != "undefined" && typeof exports == "object") { 6700 // Node/CommonJS style 6701 module.exports = factory( require("rangy") ); 6702 } else { 6703 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 6704 factory(root.rangy); 6705 } 6706})(function(rangy) { 6707 rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) { 6708 var UNDEF = "undefined"; 6709 var CHARACTER = "character", WORD = "word"; 6710 var dom = api.dom, util = api.util; 6711 var extend = util.extend; 6712 var createOptions = util.createOptions; 6713 var getBody = dom.getBody; 6714 6715 6716 var spacesRegex = /^[ \t\f\r\n]+$/; 6717 var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/; 6718 var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/; 6719 var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/; 6720 var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/; 6721 6722 var defaultLanguage = "en"; 6723 6724 var isDirectionBackward = api.Selection.isDirectionBackward; 6725 6726 // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit, 6727 // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed. 6728 var trailingSpaceInBlockCollapses = false; 6729 var trailingSpaceBeforeBrCollapses = false; 6730 var trailingSpaceBeforeBlockCollapses = false; 6731 var trailingSpaceBeforeLineBreakInPreLineCollapses = true; 6732 6733 (function() { 6734 var el = dom.createTestElement(document, "<p>1 </p><p></p>", true); 6735 var p = el.firstChild; 6736 var sel = api.getSelection(); 6737 sel.collapse(p.lastChild, 2); 6738 sel.setStart(p.firstChild, 0); 6739 trailingSpaceInBlockCollapses = ("" + sel).length == 1; 6740 6741 el.innerHTML = "1 <br />"; 6742 sel.collapse(el, 2); 6743 sel.setStart(el.firstChild, 0); 6744 trailingSpaceBeforeBrCollapses = ("" + sel).length == 1; 6745 6746 el.innerHTML = "1 <p>1</p>"; 6747 sel.collapse(el, 2); 6748 sel.setStart(el.firstChild, 0); 6749 trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1; 6750 6751 dom.removeNode(el); 6752 sel.removeAllRanges(); 6753 })(); 6754 6755 /*----------------------------------------------------------------------------------------------------------------*/ 6756 6757 // This function must create word and non-word tokens for the whole of the text supplied to it 6758 function defaultTokenizer(chars, wordOptions) { 6759 var word = chars.join(""), result, tokenRanges = []; 6760 6761 function createTokenRange(start, end, isWord) { 6762 tokenRanges.push( { start: start, end: end, isWord: isWord } ); 6763 } 6764 6765 // Match words and mark characters 6766 var lastWordEnd = 0, wordStart, wordEnd; 6767 while ( (result = wordOptions.wordRegex.exec(word)) ) { 6768 wordStart = result.index; 6769 wordEnd = wordStart + result[0].length; 6770 6771 // Create token for non-word characters preceding this word 6772 if (wordStart > lastWordEnd) { 6773 createTokenRange(lastWordEnd, wordStart, false); 6774 } 6775 6776 // Get trailing space characters for word 6777 if (wordOptions.includeTrailingSpace) { 6778 while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) { 6779 ++wordEnd; 6780 } 6781 } 6782 createTokenRange(wordStart, wordEnd, true); 6783 lastWordEnd = wordEnd; 6784 } 6785 6786 // Create token for trailing non-word characters, if any exist 6787 if (lastWordEnd < chars.length) { 6788 createTokenRange(lastWordEnd, chars.length, false); 6789 } 6790 6791 return tokenRanges; 6792 } 6793 6794 function convertCharRangeToToken(chars, tokenRange) { 6795 var tokenChars = chars.slice(tokenRange.start, tokenRange.end); 6796 var token = { 6797 isWord: tokenRange.isWord, 6798 chars: tokenChars, 6799 toString: function() { 6800 return tokenChars.join(""); 6801 } 6802 }; 6803 for (var i = 0, len = tokenChars.length; i < len; ++i) { 6804 tokenChars[i].token = token; 6805 } 6806 return token; 6807 } 6808
vendor: 4,193 bytes, lines 6809-6921
6809 function tokenize(chars, wordOptions, tokenizer) { 6810 var tokenRanges = tokenizer(chars, wordOptions); 6811 var tokens = []; 6812 for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) { 6813 tokens.push( convertCharRangeToToken(chars, tokenRange) ); 6814 } 6815 return tokens; 6816 } 6817 6818 var defaultCharacterOptions = { 6819 includeBlockContentTrailingSpace: true, 6820 includeSpaceBeforeBr: true, 6821 includeSpaceBeforeBlock: true, 6822 includePreLineTrailingSpace: true, 6823 ignoreCharacters: "" 6824 }; 6825 6826 function normalizeIgnoredCharacters(ignoredCharacters) { 6827 // Check if character is ignored 6828 var ignoredChars = ignoredCharacters || ""; 6829 6830 // Normalize ignored characters into a string consisting of characters in ascending order of character code 6831 var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars; 6832 ignoredCharsArray.sort(function(char1, char2) { 6833 return char1.charCodeAt(0) - char2.charCodeAt(0); 6834 }); 6835 6836 /// Convert back to a string and remove duplicates 6837 return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1"); 6838 } 6839 6840 var defaultCaretCharacterOptions = { 6841 includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses, 6842 includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses, 6843 includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses, 6844 includePreLineTrailingSpace: true 6845 }; 6846 6847 var defaultWordOptions = { 6848 "en": { 6849 wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi, 6850 includeTrailingSpace: false, 6851 tokenizer: defaultTokenizer 6852 } 6853 }; 6854 6855 var defaultFindOptions = { 6856 caseSensitive: false, 6857 withinRange: null, 6858 wholeWordsOnly: false, 6859 wrap: false, 6860 direction: "forward", 6861 wordOptions: null, 6862 characterOptions: null 6863 }; 6864 6865 var defaultMoveOptions = { 6866 wordOptions: null, 6867 characterOptions: null 6868 }; 6869 6870 var defaultExpandOptions = { 6871 wordOptions: null, 6872 characterOptions: null, 6873 trim: false, 6874 trimStart: true, 6875 trimEnd: true 6876 }; 6877 6878 var defaultWordIteratorOptions = { 6879 wordOptions: null, 6880 characterOptions: null, 6881 direction: "forward" 6882 }; 6883 6884 function createWordOptions(options) { 6885 var lang, defaults; 6886 if (!options) { 6887 return defaultWordOptions[defaultLanguage]; 6888 } else { 6889 lang = options.language || defaultLanguage; 6890 defaults = {}; 6891 extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]); 6892 extend(defaults, options); 6893 return defaults; 6894 } 6895 } 6896 6897 function createNestedOptions(optionsParam, defaults) { 6898 var options = createOptions(optionsParam, defaults); 6899 if (defaults.hasOwnProperty("wordOptions")) { 6900 options.wordOptions = createWordOptions(options.wordOptions); 6901 } 6902 if (defaults.hasOwnProperty("characterOptions")) { 6903 options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions); 6904 } 6905 return options; 6906 } 6907 6908 /*----------------------------------------------------------------------------------------------------------------*/ 6909 6910 /* DOM utility functions */ 6911 var getComputedStyleProperty = dom.getComputedStyleProperty; 6912 6913 // Create cachable versions of DOM functions 6914 6915 // Test for old IE's incorrect display properties 6916 var tableCssDisplayBlock; 6917 (function() { 6918 var table = document.createElement("table"); 6919 var body = getBody(document); 6920 body.appendChild(table); 6921 tableCssDisplayBlock = (getComputedStyleProperty(table, "dis
6921play") == "block"); 6922 body.removeChild(table); 6923 })(); 6924 6925 var defaultDisplayValueForTag = { 6926 table: "table", 6927 caption: "table-caption", 6928 colgroup: "table-column-group", 6929 col: "table-column", 6930 thead: "table-header-group", 6931 tbody: "table-row-group", 6932 tfoot: "table-footer-group", 6933 tr: "table-row", 6934 td: "table-cell", 6935 th: "table-cell" 6936 }; 6937 6938 // Corrects IE's "block" value for table-related elements 6939 function getComputedDisplay(el, win) { 6940 var display = getComputedStyleProperty(el, "display", win); 6941 var tagName = el.tagName.toLowerCase(); 6942 return (display == "block" && 6943 tableCssDisplayBlock && 6944 defaultDisplayValueForTag.hasOwnProperty(tagName)) ? 6945 defaultDisplayValueForTag[tagName] : display; 6946 } 6947 6948 function isHidden(node) { 6949 var ancestors = getAncestorsAndSelf(node); 6950 for (var i = 0, len = ancestors.length; i < len; ++i) { 6951 if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") { 6952 return true; 6953 } 6954 } 6955 6956 return false; 6957 } 6958 6959 function isVisibilityHiddenTextNode(textNode) { 6960 var el; 6961 return textNode.nodeType == 3 && 6962 (el = textNode.parentNode) && 6963 getComputedStyleProperty(el, "visibility") == "hidden"; 6964 } 6965 6966 /*----------------------------------------------------------------------------------------------------------------*/ 6967 6968 6969 // "A block node is either an Element whose "display" property does not have 6970 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a 6971 // Document, or a DocumentFragment." 6972 function isBlockNode(node) { 6973 return node && 6974 ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) || 6975 node.nodeType == 9 || node.nodeType == 11); 6976 } 6977 6978 function getLastDescendantOrSelf(node) { 6979 var lastChild = node.lastChild; 6980 return lastChild ? getLastDescendantOrSelf(lastChild) : node; 6981 } 6982 6983 function containsPositions(node) { 6984 return dom.isCharacterDataNode(node) || 6985 !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName); 6986 } 6987 6988 function getAncestors(node) { 6989 var ancestors = []; 6990 while (node.parentNode) { 6991 ancestors.unshift(node.parentNode); 6992 node = node.parentNode; 6993 } 6994 return ancestors; 6995 } 6996 6997 function getAncestorsAndSelf(node) { 6998 return getAncestors(node).concat([node]); 6999 } 7000 7001 function nextNodeDescendants(node) { 7002 while (node && !node.nextSibling) { 7003 node = node.parentNode; 7004 } 7005 if (!node) { 7006 return null; 7007 } 7008 return node.nextSibling; 7009 } 7010 7011 function nextNode(node, excludeChildren) { 7012 if (!excludeChildren && node.hasChildNodes()) { 7013 return node.firstChild; 7014 } 7015 return nextNodeDescendants(node); 7016 } 7017 7018 function previousNode(node) { 7019 var previous = node.previousSibling; 7020 if (previous) { 7021 node = previous; 7022 while (node.hasChildNodes()) { 7023 node = node.lastChild; 7024 } 7025 return node; 7026 } 7027 var parent = node.parentNode; 7028 if (parent && parent.nodeType == 1) { 7029 return parent; 7030 } 7031 return null; 7032 } 7033 7034 // Adpated from Aryeh's code. 7035 // "A whitespace node is either a Text node whose data is the empty string; or 7036 // a Text node whose data consists only of one or more tabs (0x0009), line 7037 // feeds (0x000A), carriage returns (0x000D), and/or space
7037s (0x0020), and whose 7038 // parent is an Element whose resolved value for "white-space" is "normal" or 7039 // "nowrap"; or a Text node whose data consists only of one or more tabs 7040 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose 7041 // parent is an Element whose resolved value for "white-space" is "pre-line"." 7042 function isWhitespaceNode(node) { 7043 if (!node || node.nodeType != 3) { 7044 return false; 7045 } 7046 var text = node.data; 7047 if (text === "") { 7048 return true; 7049 } 7050 var parent = node.parentNode; 7051 if (!parent || parent.nodeType != 1) { 7052 return false; 7053 } 7054 var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace"); 7055 7056 return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) || 7057 (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line"); 7058 } 7059 7060 // Adpated from Aryeh's code. 7061 // "node is a collapsed whitespace node if the following algorithm returns 7062 // true:" 7063 function isCollapsedWhitespaceNode(node) { 7064 // "If node's data is the empty string, return true." 7065 if (node.data === "") { 7066 return true; 7067 } 7068 7069 // "If node is not a whitespace node, return false." 7070 if (!isWhitespaceNode(node)) { 7071 return false; 7072 } 7073 7074 // "Let ancestor be node's parent." 7075 var ancestor = node.parentNode; 7076 7077 // "If ancestor is null, return true." 7078 if (!ancestor) { 7079 return true; 7080 } 7081 7082 // "If the "display" property of some ancestor of node has resolved value "none", return true." 7083 if (isHidden(node)) { 7084 return true; 7085 } 7086 7087 return false; 7088 } 7089 7090 function isCollapsedNode(node) { 7091 var type = node.nodeType; 7092 return type == 7 /* PROCESSING_INSTRUCTION */ || 7093 type == 8 /* COMMENT */ || 7094 isHidden(node) || 7095 /^(script|style)$/i.test(node.nodeName) || 7096 isVisibilityHiddenTextNode(node) || 7097 isCollapsedWhitespaceNode(node); 7098 } 7099 7100 function isIgnoredNode(node, win) { 7101 var type = node.nodeType; 7102 return type == 7 /* PROCESSING_INSTRUCTION */ || 7103 type == 8 /* COMMENT */ || 7104 (type == 1 && getComputedDisplay(node, win) == "none"); 7105 } 7106 7107 /*----------------------------------------------------------------------------------------------------------------*/ 7108 7109 // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down 7110 7111 function Cache() { 7112 this.store = {}; 7113 } 7114 7115 Cache.prototype = { 7116 get: function(key) { 7117 return this.store.hasOwnProperty(key) ? this.store[key] : null; 7118 }, 7119 7120 set: function(key, value) { 7121 return this.store[key] = value; 7122 } 7123 }; 7124 7125 var cachedCount = 0, uncachedCount = 0; 7126 7127 function createCachingGetter(methodName, func, objProperty) { 7128 return function(args) { 7129 var cache = this.cache; 7130 if (cache.hasOwnProperty(methodName)) { 7131 cachedCount++; 7132 return cache[methodName]; 7133 } else { 7134 uncachedCount++; 7135 var value = func.call(this, objProperty ? this[objProperty] : this, args); 7136 cache[methodName] = value; 7137 return value; 7138 } 7139 }; 7140 } 7141 7142 /*----------------------------------------------------------------------------------------------------------------*/ 7143 7144 function NodeWrapper(node, session) { 7145 this.node = node; 7146 this.session = session; 7147 this.cache = new Cache(); 7148 this.positions = new Cache(); 7149 } 7150 7151 var nodeProto = { 7152 getPosition: function(offset) { 7153 var positions = this.positions; 7154 return positions.get(offset) || positions.set(offset, new Position(this, offset)); 7155 }, 7156 7157 toString: function() { 7158 return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]"; 7159 } 7160 }; 7161 7162 NodeWrapper.prototype = nodeProto; 7163 7164 var EMPTY = "EMPTY", 7165 NON_SPACE = "NON_SPACE", 7166 UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE", 7167 COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE", 7168 TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK", 7169 TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK", 7170 TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR", 7171 PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK", 7172 TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR", 7173 INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR"; 7174 7175 extend(nodeProto, { 7176 isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"), 7177 getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"), 7178 getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"), 7179 containsPositions: createCachingGetter("containsPositions", containsPositions, "node"), 7180 isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"), 7181 isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"), 7182 getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"), 7183 isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"), 7184 isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"), 7185 next: createCachingGetter("nextPos", nextNode, "node"), 7186 previous: createCachingGetter("previous", previousNode, "node"), 7187 7188 getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) { 7189 var spaceRegex = null, collapseSpaces = false;
7190 var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace"); 7191 var preLine = (cssWhitespace == "pre-line"); 7192 if (preLine) { 7193 spaceRegex = spacesMinusLineBreaksRegex; 7194 collapseSpaces = true; 7195 } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") { 7196 spaceRegex = spacesRegex; 7197 collapseSpaces = true; 7198 } 7199 7200 return { 7201 node: textNode, 7202 text: textNode.data, 7203 spaceRegex: spaceRegex, 7204 collapseSpaces: collapseSpaces, 7205 preLine: preLine 7206 }; 7207 }, "node"), 7208 7209 hasInnerText: createCachingGetter("hasInnerText", function(el, backward) { 7210 var session = this.session; 7211 var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1); 7212 var firstPosInEl = session.getPosition(el, 0); 7213 7214 var pos = backward ? posAfterEl : firstPosInEl; 7215 var endPos = backward ? firstPosInEl : posAfterEl; 7216 7217 /* 7218 <body><p>X </p><p>Y</p></body> 7219 7220 Positions: 7221 7222 body:0:"" 7223 p:0:"" 7224 text:0:"" 7225 text:1:"X" 7226 text:2:TRAILING_SPACE_IN_BLOCK 7227 text:3:COLLAPSED_SPACE 7228 p:1:"" 7229 body:1:"\n" 7230 p:0:"" 7231 text:0:"" 7232 text:1:"Y" 7233 7234 A character is a TRAILING_SPACE_IN_BLOCK iff: 7235 7236 - There is no uncollapsed character after it within the visible containing block element 7237 7238 A character is a TRAILING_SPACE_BEFORE_BR iff: 7239 7240 - There is no uncollapsed character after it preceding a <br> element 7241 7242 An element has inner text iff 7243 7244 - It is not hidden 7245 - It contains an uncollapsed character 7246 7247 All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render. 7248 */ 7249 7250 while (pos !== endPos) { 7251 pos.prepopulateChar(); 7252 if (pos.isDefinitelyNonEmpty()) { 7253 return true; 7254 } 7255 pos = backward ? pos.previousVisible() : pos.nextVisible(); 7256 } 7257 7258 return false; 7259 }, "node"), 7260 7261 isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) { 7262 // Ensure that a block element containing a <br> is considered to have inner text 7263 var brs = el.getElementsByTagName("br"); 7264 for (var i = 0, len = brs.length; i < len; ++i) { 7265 if (!isCollapsedNode(brs[i])) { 7266 return true; 7267 } 7268 } 7269 // Ensure that a block element containing a <svg> is considered to have inner text 7270 // as otherwise it will crash IE11 7271 var svgs = el.getElementsByTagName("svg"); 7272 if (svgs.length) { 7273 return true; 7274 } 7275 return this.hasInnerText(); 7276 }, "node"), 7277 7278 getTrailingSpace: createCachingGetter("trailingSpace", function(el) { 7279 if (el.tagName.toLowerCase() == "br") { 7280 return ""; 7281 } else { 7282 switch (this.getComputedDisplay()) { 7283 case "inline": 7284 var child = el.lastChild; 7285 while (child) { 7286 if (!isIgnoredNode(child)) { 7287 return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : ""; 7288 } 7289 child = child.previousSibling; 7290 } 7291 break; 7292 case "inline-block": 7293 case "inline-table": 7294 case "none": 7295 case "table-column": 7296 case "table-column-group": 7297 break; 7298 case "table-cell": 7299 return "\t"; 7300 default: 7301 return this.isRenderedBlock(true) ? "\n" : ""; 7302 } 7303 } 7304 return ""; 7305 }, "node"), 7306 7307 getLeadingSpace: createCachingGetter("leadingSpace", function(el) { 7308 switch (this.getComputedDisplay()) { 7309 case "inline": 7310 case "inline-block": 7311 case "inline-table": 7312 case "none": 7313 case "table-column": 7314 case "table-column-group": 7315 case "table-cell": 7316 break; 7317 default: 7318 return this.isRenderedBlock(false) ? "\n" : ""; 7319 } 7320 return ""; 7321 }, "node") 7322 }); 7323 7324 /*----------------------------------------------------------------------------------------------------------------*/ 7325 7326 function Position(nodeWrapper, offset) { 7327 this.offset = offset; 7328 this.nodeWrapper = nodeWrapper; 7329 this.node = nodeWrapper.node; 7330 this.session = nodeWrapper.session; 7331 this.cache = new Cache(); 7332 } 7333 7334 function inspectPosition() { 7335 return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]"; 7336 } 7337 7338 var positionProto = { 7339 character: "", 7340 characterType: EMPTY, 7341 isBr: false, 7342 7343 /* 7344 This method: 7345 - Fully populates positions that have characters that can be determined independently of any other characters. 7346 - Populates most types of space positions with a provisional character. The character is finalized later. 7347 */ 7348 prepopulateChar: function() { 7349 var pos = this; 7350 if (!pos.prepopulatedChar) { 7351 var node = pos.node, offset = pos.offset; 7352 var visibleChar = "", charType = EMPTY; 7353 var finalizedChar = false;
vendor: 4,602 bytes, lines 7354-7435
7354 if (offset > 0) { 7355 if (node.nodeType == 3) { 7356 var text = node.data; 7357 var textChar = text.charAt(offset - 1); 7358 7359 var nodeInfo = pos.nodeWrapper.getTextNodeInfo(); 7360 var spaceRegex = nodeInfo.spaceRegex; 7361 if (nodeInfo.collapseSpaces) { 7362 if (spaceRegex.test(textChar)) { 7363 // "If the character at position is from set, append a single space (U+0020) to newdata and advance 7364 // position until the character at position is not from set." 7365 7366 // We also need to check for the case where we're in a pre-line and we have a space preceding a 7367 // line break, because such spaces are collapsed in some browsers 7368 if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) { 7369 } else if (nodeInfo.preLine && text.charAt(offset) === "\n") { 7370 visibleChar = " "; 7371 charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK; 7372 } else { 7373 visibleChar = " "; 7374 //pos.checkForFollowingLineBreak = true; 7375 charType = COLLAPSIBLE_SPACE; 7376 } 7377 } else { 7378 visibleChar = textChar; 7379 charType = NON_SPACE; 7380 finalizedChar = true; 7381 } 7382 } else { 7383 visibleChar = textChar; 7384 charType = UNCOLLAPSIBLE_SPACE; 7385 finalizedChar = true; 7386 } 7387 } else { 7388 var nodePassed = node.childNodes[offset - 1]; 7389 if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) { 7390 if (nodePassed.tagName.toLowerCase() == "br") { 7391 visibleChar = "\n"; 7392 pos.isBr = true; 7393 charType = COLLAPSIBLE_SPACE; 7394 finalizedChar = false; 7395 } else { 7396 pos.checkForTrailingSpace = true; 7397 } 7398 } 7399 7400 // Check the leading space of the next node for the case when a block element follows an inline 7401 // element or text node. In that case, there is an implied line break between the two nodes. 7402 if (!visibleChar) { 7403 var nextNode = node.childNodes[offset]; 7404 if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) { 7405 pos.checkForLeadingSpace = true; 7406 } 7407 } 7408 } 7409 } 7410 7411 pos.prepopulatedChar = true; 7412 pos.character = visibleChar; 7413 pos.characterType = charType; 7414 pos.isCharInvariant = finalizedChar; 7415 } 7416 }, 7417 7418 isDefinitelyNonEmpty: function() { 7419 var charType = this.characterType; 7420 return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE; 7421 }, 7422 7423 // Resolve leading and trailing spaces, which may involve prepopulating other positions 7424 resolveLeadingAndTrailingSpaces: function() { 7425 if (!this.prepopulatedChar) { 7426 this.prepopulateChar(); 7427 } 7428 if (this.checkForTrailingSpace) { 7429 var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace(); 7430 if (trailingSpace) { 7431 this.isTrailingSpace = true; 7432 this.character = trailingSpace; 7433 this.characterType = COLLAPSIBLE_SPACE; 7434 } 7435 this.checkForTrailingSpace = false;
7436 } 7437 if (this.checkForLeadingSpace) { 7438 var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace(); 7439 if (leadingSpace) { 7440 this.isLeadingSpace = true; 7441 this.character = leadingSpace; 7442 this.characterType = COLLAPSIBLE_SPACE; 7443 } 7444 this.checkForLeadingSpace = false; 7445 } 7446 }, 7447 7448 getPrecedingUncollapsedPosition: function(characterOptions) { 7449 var pos = this, character; 7450 while ( (pos = pos.previousVisible()) ) { 7451 character = pos.getCharacter(characterOptions); 7452 if (character !== "") { 7453 return pos; 7454 } 7455 } 7456 7457 return null; 7458 }, 7459 7460 getCharacter: function(characterOptions) { 7461 this.resolveLeadingAndTrailingSpaces(); 7462 7463 var thisChar = this.character, returnChar; 7464 7465 // Check if character is ignored 7466 var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters); 7467 var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1); 7468 7469 // Check if this position's character is invariant (i.e. not dependent on character options) and return it 7470 // if so 7471 if (this.isCharInvariant) { 7472 returnChar = isIgnoredCharacter ? "" : thisChar; 7473 return returnChar; 7474 } 7475 7476 var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_"); 7477 var cachedChar = this.cache.get(cacheKey); 7478 if (cachedChar !== null) { 7479 return cachedChar; 7480 } 7481 7482 // We need to actually get the character now 7483 var character = ""; 7484 var collapsible = (this.characterType == COLLAPSIBLE_SPACE); 7485 7486 var nextPos, previousPos; 7487 var gotPreviousPos = false; 7488 var pos = this; 7489 7490 function getPreviousPos() { 7491 if (!gotPreviousPos) { 7492 previousPos = pos.getPrecedingUncollapsedPosition(characterOptions); 7493 gotPreviousPos = true; 7494 } 7495 return previousPos; 7496 } 7497 7498 // Disallow a collapsible space that is followed by a line break or is the last character 7499 if (collapsible) { 7500 // Allow a trailing space that we've previously determined should be included 7501 if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) { 7502 character = "\n"; 7503 } 7504 // Disallow a collapsible space that follows a trailing space or line break, or is the first character, 7505 // or follows a collapsible included space 7506 else if (thisChar == " " && 7507 (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) { 7508 } 7509 // Allow a leading line break unless it follows a line break 7510 else if (thisChar == "\n" && this.isLeadingSpace) { 7511 if (getPreviousPos() && previousPos.character != "\n") { 7512 character = "\n"; 7513 } else { 7514 } 7515 } else { 7516 nextPos = this.nextUncollapsed(); 7517 if (nextPos) { 7518 if (nextPos.isBr) { 7519 this.type = TRAILING_SPACE_BEFORE_BR; 7520 } else if (nextPos.isTrailingSpace && nextPos.character == "\n") { 7521 this.type = TRAILING_SPACE_IN_BLOCK; 7522 } else if (nextPos.isLeadingSpace && nextPos.character == "\n") { 7523 this.type = TRAILING_SPACE_BEFORE_BLOCK; 7524 } 7525 7526 if (nextPos.character == "\n") {
vendor: 5,167 bytes, lines 7527-7634
7527 if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) { 7528 } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) { 7529 } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) { 7530 } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) { 7531 } else if (thisChar == "\n") { 7532 if (nextPos.isTrailingSpace) { 7533 if (this.isTrailingSpace) { 7534 } else if (this.isBr) { 7535 nextPos.type = TRAILING_LINE_BREAK_AFTER_BR; 7536 7537 if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") { 7538 nextPos.character = ""; 7539 } else { 7540 nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR; 7541 } 7542 } 7543 } else { 7544 character = "\n"; 7545 } 7546 } else if (thisChar == " ") { 7547 character = " "; 7548 } else { 7549 } 7550 } else { 7551 character = thisChar; 7552 } 7553 } else { 7554 } 7555 } 7556 } 7557 7558 if (ignoredChars.indexOf(character) > -1) { 7559 character = ""; 7560 } 7561 7562 7563 this.cache.set(cacheKey, character); 7564 7565 return character; 7566 }, 7567 7568 equals: function(pos) { 7569 return !!pos && this.node === pos.node && this.offset === pos.offset; 7570 }, 7571 7572 inspect: inspectPosition, 7573 7574 toString: function() { 7575 return this.character; 7576 } 7577 }; 7578 7579 Position.prototype = positionProto; 7580 7581 extend(positionProto, { 7582 next: createCachingGetter("nextPos", function(pos) { 7583 var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session; 7584 if (!node) { 7585 return null; 7586 } 7587 var nextNode, nextOffset, child; 7588 if (offset == nodeWrapper.getLength()) { 7589 // Move onto the next node 7590 nextNode = node.parentNode; 7591 nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0; 7592 } else { 7593 if (nodeWrapper.isCharacterDataNode()) { 7594 nextNode = node; 7595 nextOffset = offset + 1; 7596 } else { 7597 child = node.childNodes[offset]; 7598 // Go into the children next, if children there are 7599 if (session.getNodeWrapper(child).containsPositions()) { 7600 nextNode = child; 7601 nextOffset = 0; 7602 } else { 7603 nextNode = node; 7604 nextOffset = offset + 1; 7605 } 7606 } 7607 } 7608 7609 return nextNode ? session.getPosition(nextNode, nextOffset) : null; 7610 }), 7611 7612 previous: createCachingGetter("previous", function(pos) { 7613 var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session; 7614 var previousNode, previousOffset, child; 7615 if (offset == 0) { 7616 previousNode = node.parentNode; 7617 previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0; 7618 } else { 7619 if (nodeWrapper.isCharacterDataNode()) { 7620 previousNode = node; 7621 previousOffset = offset - 1; 7622 } else { 7623 child = node.childNodes[offset - 1]; 7624 // Go into the children next, if children there are 7625 if (session.getNodeWrapper(child).containsPositions()) { 7626 previousNode = child; 7627 previousOffset = dom.getNodeLength(child); 7628 } else { 7629 previousNode = node; 7630 previousOffset = offset - 1; 7631 } 7632 } 7633 } 7634 return previousNode ? session.getPosition(previousNode, previousOffset)
7634: null; 7635 }), 7636 7637 /* 7638 Next and previous position moving functions that filter out 7639 7640 - Hidden (CSS visibility/display) elements 7641 - Script and style elements 7642 */ 7643 nextVisible: createCachingGetter("nextVisible", function(pos) { 7644 var next = pos.next(); 7645 if (!next) { 7646 return null; 7647 } 7648 var nodeWrapper = next.nodeWrapper, node = next.node; 7649 var newPos = next; 7650 if (nodeWrapper.isCollapsed()) { 7651 // We're skipping this node and all its descendants 7652 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1); 7653 } 7654 return newPos; 7655 }), 7656 7657 nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) { 7658 var nextPos = pos; 7659 while ( (nextPos = nextPos.nextVisible()) ) { 7660 nextPos.resolveLeadingAndTrailingSpaces(); 7661 if (nextPos.character !== "") { 7662 return nextPos; 7663 } 7664 } 7665 return null; 7666 }), 7667 7668 previousVisible: createCachingGetter("previousVisible", function(pos) { 7669 var previous = pos.previous(); 7670 if (!previous) { 7671 return null; 7672 } 7673 var nodeWrapper = previous.nodeWrapper, node = previous.node; 7674 var newPos = previous; 7675 if (nodeWrapper.isCollapsed()) { 7676 // We're skipping this node and all its desce
7676ndants 7677 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex()); 7678 } 7679 return newPos; 7680 }) 7681 }); 7682 7683 /*----------------------------------------------------------------------------------------------------------------*/ 7684 7685 var currentSession = null; 7686 7687 var Session = (function() { 7688 function createWrapperCache(nodeProperty) { 7689 var cache = new Cache(); 7690 7691 return { 7692 get: function(node) { 7693 var wrappersByProperty = cache.get(node[nodeProperty]); 7694 if (wrappersByProperty) { 7695 for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) { 7696 if (wrapper.node === node) { 7697 return wrapper; 7698 } 7699 } 7700 } 7701 return null; 7702 }, 7703 7704 set: function(nodeWrapper) { 7705 var property = nodeWrapper.node[nodeProperty]; 7706 var wrappersByProperty = cache.get(property) || cache.set(property, []); 7707 wrappersByProperty.push(nodeWrapper); 7708 } 7709 }; 7710 } 7711 7712 var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID"); 7713 7714 function Session() { 7715 this.initCaches(); 7716 } 7717 7718 Session.prototype = { 7719 initCaches: function() { 7720 this.elementCache = uniqueIDSupported ? (function() { 7721 var elementsCache = new Cache(); 7722 7723 return { 7724 get: function(el) { 7725 return elementsCache.get(el.uniqueID); 7726 }, 7727 7728 set: function(elWrapper) { 7729 elementsCache.set(elWrapper.node.uniqueID, elWrapper); 7730 } 7731 }; 7732 })() : createWrapperCache("tagName"); 7733 7734 // Store text nodes keyed by data, although we may need to truncate this 7735 this.textNodeCache = createWrapperCache("data"); 7736 this.otherNodeCache = createWrapperCache("nodeName"); 7737 }, 7738 7739 getNodeWrapper: function(node) { 7740 var wrapperCache; 7741 switch (node.nodeType) { 7742 case 1: 7743 wrapperCache = this.elementCache; 7744 break; 7745 case 3: 7746 wrapperCache = this.textNodeCache; 7747 break; 7748 default: 7749 wrapperCache = this.otherNodeCache; 7750 break; 7751 } 7752 7753 var wrapper = wrapperCache.get(node); 7754 if (!wrapper) { 7755 wrapper = new NodeWrapper(node, this); 7756 wrapperCache.set(wrapper); 7757 } 7758 return wrapper; 7759 }, 7760 7761 getPosition: function(node, offset) { 7762 return this.getNodeWrapper(node).getPosition(offset); 7763 }, 7764 7765 getRangeBoundaryPosition: function(range, isStart) { 7766 var prefix = isStart ? "start" : "end"; 7767 return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]); 7768 }, 7769 7770 detach: function() { 7771 this.elementCache = this.textNodeCache = this.otherNodeCache = null; 7772 } 7773 }; 7774 7775 return Session; 7776 })(); 7777 7778 /*----------------------------------------------------------------------------------------------------------------*/ 7779 7780 function startSession() { 7781 endSession(); 7782 return (currentSession = new Session()); 7783 } 7784 7785 function getSession() { 7786 return currentSession || startSession(); 7787 } 7788 7789 function endSession() { 7790 if (currentSession) { 7791 currentSession.detach(); 7792 } 7793 currentSession = null; 7794 } 7795 7796 /*----------------------------------------------------------------------------------------------------------------*/ 7797 7798 // Extensions to the rangy.dom utility object 7799 7800 extend(dom, { 7801 nextNode: nextNode, 7802 previousNode: previousNode 7803 }); 7804 7805 /*----------------------------------------------------------------------------------------------------------------*/ 7806 7807 function createCharacterIterator(startPos, backward, endPos, characterOptions) { 7808 7809 // Adjust the end position to ensure that it is actually reached 7810 if (endPos) { 7811 if (backward) { 7812 if (isCollapsedNode(endPos.node)) { 7813 endPos = startPos.previousVisible(); 7814 } 7815 } else { 7816 if (isCollapsedNode(endPos.node)) { 7817 endPos = endPos.nextVisible(); 7818 } 7819 } 7820 } 7821 7822 var pos = startPos, finished = false;
vendor: 11,504 bytes, lines 7823-8090
7823 7824 function next() { 7825 var charPos = null; 7826 if (backward) { 7827 charPos = pos; 7828 if (!finished) { 7829 pos = pos.previousVisible(); 7830 finished = !pos || (endPos && pos.equals(endPos)); 7831 } 7832 } else { 7833 if (!finished) { 7834 charPos = pos = pos.nextVisible(); 7835 finished = !pos || (endPos && pos.equals(endPos)); 7836 } 7837 } 7838 if (finished) { 7839 pos = null; 7840 } 7841 return charPos; 7842 } 7843 7844 var previousTextPos, returnPreviousTextPos = false; 7845 7846 return { 7847 next: function() { 7848 if (returnPreviousTextPos) { 7849 returnPreviousTextPos = false; 7850 return previousTextPos; 7851 } else { 7852 var pos, character; 7853 while ( (pos = next()) ) { 7854 character = pos.getCharacter(characterOptions); 7855 if (character) { 7856 previousTextPos = pos; 7857 return pos; 7858 } 7859 } 7860 return null; 7861 } 7862 }, 7863 7864 rewind: function() { 7865 if (previousTextPos) { 7866 returnPreviousTextPos = true; 7867 } else { 7868 throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound."); 7869 } 7870 }, 7871 7872 dispose: function() { 7873 startPos = endPos = null; 7874 } 7875 }; 7876 } 7877 7878 var arrayIndexOf = Array.prototype.indexOf ? 7879 function(arr, val) { 7880 return arr.indexOf(val); 7881 } : 7882 function(arr, val) { 7883 for (var i = 0, len = arr.length; i < len; ++i) { 7884 if (arr[i] === val) { 7885 return i; 7886 } 7887 } 7888 return -1; 7889 }; 7890 7891 // Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next() 7892 // is called and there is no more tokenized text 7893 function createTokenizedTextProvider(pos, characterOptions, wordOptions) { 7894 var forwardIterator = createCharacterIterator(pos, false, null, characterOptions); 7895 var backwardIterator = createCharacterIterator(pos, true, null, characterOptions); 7896 var tokenizer = wordOptions.tokenizer; 7897 7898 // Consumes a word and the whitespace beyond it 7899 function consumeWord(forward) { 7900 var pos, textChar; 7901 var newChars = [], it = forward ? forwardIterator : backwardIterator; 7902 7903 var passedWordBoundary = false, insideWord = false; 7904 7905 while ( (pos = it.next()) ) { 7906 textChar = pos.character; 7907 7908 7909 if (allWhiteSpaceRegex.test(textChar)) { 7910 if (insideWord) { 7911 insideWord = false; 7912 passedWordBoundary = true; 7913 } 7914 } else { 7915 if (passedWordBoundary) { 7916 it.rewind(); 7917 break; 7918 } else { 7919 insideWord = true; 7920 } 7921 } 7922 newChars.push(pos); 7923 } 7924 7925 7926 return newChars; 7927 } 7928 7929 // Get initial word surrounding initial position and tokenize it 7930 var forwardChars = consumeWord(true); 7931 var backwardChars = consumeWord(false).reverse(); 7932 var tokens = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer); 7933 7934 // Create initial token buffers 7935 var forwardTokensBuffer = forwardChars.length ? 7936 tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : []; 7937 7938 var backwardTokensBuffer = backwardChars.length ? 7939 tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : []; 7940 7941 function inspectBuffer(buffer) { 7942 var textPositions = ["[" + buffer.length + "]"]; 7943 for (var i = 0; i < buffer.length; ++i) { 7944 textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")"); 7945 } 7946 return textPositions; 7947 } 7948 7949 7950 return { 7951 nextEndToken: function() { 7952 var lastToken, forwardChars; 7953 7954 // If we're down to the last token, consume character chunks until we have a word or run out of 7955 // characters to consume 7956 while ( forwardTokensBuffer.length == 1 && 7957 !(lastToken = forwardTokensBuffer[0]).isWord && 7958 (forwardChars = consumeWord(true)).length > 0) { 7959 7960 // Merge trailing non-word into next word and tokenize 7961 forwardTokensBuffer = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer); 7962 } 7963 7964 return forwardTokensBuffer.shift(); 7965 }, 7966 7967 previousStartToken: function() { 7968 var lastToken, backwardChars; 7969 7970 // If we're down to the last token, consume character chunks until we have a word or run out of 7971 // characters to consume 7972 while ( backwardTokensBuffer.length == 1 && 7973 !(lastToken = backwardTokensBuffer[0]).isWord && 7974 (backwardChars = consumeWord(false)).length > 0) { 7975 7976 // Merge leading non-word into next word and tokenize 7977 backwardTokensBuffer = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer); 7978 } 7979 7980 return backwardTokensBuffer.pop(); 7981 }, 7982 7983 dispose: function() { 7984 forwardIterator.dispose(); 7985 backwardIterator.dispose(); 7986 forwardTokensBuffer = backwardTokensBuffer = null; 7987 } 7988 }; 7989 } 7990 7991 function movePositionBy(pos, unit, count, characterOptions, wordOptions) { 7992 var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token; 7993 if (count !== 0) { 7994 var backward = (count < 0); 7995 7996 switch (unit) { 7997 case CHARACTER: 7998 charIterator = createCharacterIterator(pos, backward, null, characterOptions); 7999 while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) { 8000 ++unitsMoved; 8001 newPos = currentPos; 8002 } 8003 nextPos = currentPos; 8004 charIterator.dispose(); 8005 break; 8006 case WORD: 8007 var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions); 8008 var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken; 8009 8010 while ( (token = next()) && unitsMoved < absCount ) { 8011 if (token.isWord) { 8012 ++unitsMoved; 8013 newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1]; 8014 } 8015 } 8016 break; 8017 default: 8018 throw new Error("movePositionBy: unit '" + unit + "' not implemented"); 8019 } 8020 8021 // Perform any necessary position tweaks 8022 if (backward) { 8023 newPos = newPos.previousVisible(); 8024 unitsMoved = -unitsMoved; 8025 } else if (newPos && newPos.isLeadingSpace && !newPos.isTrailingSpace) { 8026 // Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space 8027 // before a block element (for example, the line break between "1" and "2" in the following HTML: 8028 // "1<p>2</p>") is considered to be attached to the position immediately before the block element, which 8029 // corresponds with a different selection position in most browsers from the one we want (i.e. at the 8030 // start of the contents of the block element). We get round this by advancing the position returned to 8031 // the last possible equivalent visible position. 8032 if (unit == WORD) { 8033 charIterator = createCharacterIterator(pos, false, null, characterOptions); 8034 nextPos = charIterator.next(); 8035 charIterator.dispose(); 8036 } 8037 if (nextPos) { 8038 newPos = nextPos.previousVisible(); 8039 } 8040 } 8041 } 8042 8043 8044 return { 8045 position: newPos, 8046 unitsMoved: unitsMoved 8047 }; 8048 } 8049 8050 function createRangeCharacterIterator(session, range, characterOptions, backward) { 8051 var rangeStart = session.getRangeBoundaryPosition(range, true); 8052 var rangeEnd = session.getRangeBoundaryPosition(range, false); 8053 var itStart = backward ? rangeEnd : rangeStart; 8054 var itEnd = backward ? rangeStart : rangeEnd; 8055 8056 return createCharacterIterator(itStart, !!backward, itEnd, characterOptions); 8057 } 8058 8059 function getRangeCharacters(session, range, characterOptions) { 8060 8061 var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos; 8062 while ( (pos = it.next()) ) { 8063 chars.push(pos); 8064 } 8065 8066 it.dispose(); 8067 return chars; 8068 } 8069 8070 function isWholeWord(startPos, endPos, wordOptions) { 8071 var range = api.createRange(startPos.node); 8072 range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset); 8073 return !range.expand("word", { wordOptions: wordOptions }); 8074 } 8075 8076 function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) { 8077 var backward = isDirectionBackward(findOptions.direction); 8078 var it = createCharacterIterator( 8079 initialPos, 8080 backward, 8081 initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward), 8082 findOptions.characterOptions 8083 ); 8084 var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex; 8085 var result, insideRegexMatch; 8086 var returnValue = null; 8087 8088 function handleMatch(startIndex, endIndex) { 8089 var startPos = chars[startIndex].previousVisible(); 8090 var endPos = chars[endIndex - 1];
8091 var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions)); 8092 8093 return { 8094 startPos: startPos, 8095 endPos: endPos, 8096 valid: valid 8097 }; 8098 } 8099 8100 while ( (pos = it.next()) ) { 8101 currentChar = pos.character; 8102 if (!isRegex && !findOptions.caseSensitive) { 8103 currentChar = currentChar.toLowerCase(); 8104 } 8105 8106 if (backward) { 8107 chars.unshift(pos); 8108 text = currentChar + text; 8109 } else { 8110 chars.push(pos); 8111 text += currentChar; 8112 } 8113 8114 if (isRegex) { 8115 result = searchTerm.exec(text); 8116 if (result) { 8117 matchStartIndex = result.index; 8118 matchEndIndex = matchStartIndex + result[0].length; 8119 if (insideRegexMatch) { 8120 // Check whether the match is now over 8121 if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) { 8122 returnValue = handleMatch(matchStartIndex, matchEndIndex); 8123 break; 8124 } 8125 } else { 8126 insideRegexMatch = true; 8127 } 8128 } 8129 } else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) { 8130 returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length); 8131 break; 8132 } 8133 } 8134 8135 // Check whether regex match extends to the end of the range 8136 if (insideRegexMatch) { 8137 returnValue = handleMatch(matchStartIndex, matchEndIndex); 8138 } 8139 it.dispose(); 8140 8141 return returnValue; 8142 } 8143 8144 function createEntryPointFunction(func) { 8145 return function() { 8146 var sessionRunning = !!currentSession; 8147 var session = getSession(); 8148 var args = [session].concat( util.toArray(arguments) ); 8149 var returnValue = func.apply(this, args); 8150 if (!sessionRunning) { 8151 endSession(); 8152 } 8153 return returnValue; 8154 }; 8155 } 8156 8157 /*----------------------------------------------------------------------------------------------------------------*/ 8158 8159 // Extensions to the Rangy Range object 8160 8161 function createRangeBoundaryMover(isStart, collapse) { 8162 /* 8163 Unit can be "character" or "word" 8164 Options: 8165 8166 - includeTrailingSpace 8167 - wordRegex 8168 - tokenizer 8169 - collapseSpaceBeforeLineBreak 8170 */ 8171 return createEntryPointFunction( 8172 function(session, unit, count, moveOptions) { 8173 if (typeof count == UNDEF) { 8174 count = unit; 8175 unit = CHARACTER; 8176 } 8177 moveOptions = createNestedOptions(moveOptions, defaultMoveOptions); 8178 8179 var boundaryIsStart = isStart; 8180 if (collapse) { 8181 boundaryIsStart = (count >= 0); 8182 this.collapse(!boundaryIsStart); 8183 } 8184 var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.wordOptions); 8185 var newPos = moveResult.position; 8186 this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset); 8187 return moveResult.unitsMoved; 8188 } 8189 ); 8190 } 8191 8192 function createRangeTrimmer(isStart) { 8193 return createEntryPointFunction( 8194 function(session, characterOptions) { 8195 characterOptions = createOptions(characterOptions, defaultCharacterOptions); 8196 var pos; 8197 var it = createRangeCharacterIterator(session, this, characterOptions, !isStart); 8198 var trimCharCount = 0; 8199 while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) { 8200 ++trimCharCount; 8201 } 8202 it.dispose(); 8203 var trimmed = (trimCharCount > 0); 8204 if (trimmed) { 8205 this[isStart ? "moveStart" : "moveEnd"]( 8206 "character", 8207 isStart ? trimCharCount : -trimCharCount, 8208 { characterOptions: characterOptions } 8209 ); 8210 } 8211 return trimmed; 8212 } 8213 ); 8214 } 8215 8216 extend(api.rangePrototype, { 8217 moveStart: createRangeBoundaryMover(true, false), 8218 8219 moveEnd: createRangeBoundaryMover(false, false), 8220 8221 move: createRangeBoundaryMover(true, true), 8222 8223 trimStart: createRangeTrimmer(true), 8224 8225 trimEnd: createRangeTrimmer(false), 8226 8227 trim: createEntryPointFunction( 8228 function(session, characterOptions) { 8229 var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions); 8230 return startTrimmed || endTrimmed; 8231 } 8232 ), 8233 8234 expand: createEntryPointFunction( 8235 function(session, unit, expandOptions) { 8236 var moved = false;
vendor: 16,222 bytes, lines 8237-8595
8237 expandOptions = createNestedOptions(expandOptions, defaultExpandOptions); 8238 var characterOptions = expandOptions.characterOptions; 8239 if (!unit) { 8240 unit = CHARACTER; 8241 } 8242 if (unit == WORD) { 8243 var wordOptions = expandOptions.wordOptions; 8244 var startPos = session.getRangeBoundaryPosition(this, true); 8245 var endPos = session.getRangeBoundaryPosition(this, false); 8246 8247 var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions); 8248 var startToken = startTokenizedTextProvider.nextEndToken(); 8249 var newStartPos = startToken.chars[0].previousVisible(); 8250 var endToken, newEndPos; 8251 8252 if (this.collapsed) { 8253 endToken = startToken; 8254 } else { 8255 var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions); 8256 endToken = endTokenizedTextProvider.previousStartToken(); 8257 } 8258 newEndPos = endToken.chars[endToken.chars.length - 1]; 8259 8260 if (!newStartPos.equals(startPos)) { 8261 this.setStart(newStartPos.node, newStartPos.offset); 8262 moved = true; 8263 } 8264 if (newEndPos && !newEndPos.equals(endPos)) { 8265 this.setEnd(newEndPos.node, newEndPos.offset); 8266 moved = true; 8267 } 8268 8269 if (expandOptions.trim) { 8270 if (expandOptions.trimStart) { 8271 moved = this.trimStart(characterOptions) || moved; 8272 } 8273 if (expandOptions.trimEnd) { 8274 moved = this.trimEnd(characterOptions) || moved; 8275 } 8276 } 8277 8278 return moved; 8279 } else { 8280 return this.moveEnd(CHARACTER, 1, expandOptions); 8281 } 8282 } 8283 ), 8284 8285 text: createEntryPointFunction( 8286 function(session, characterOptions) { 8287 return this.collapsed ? 8288 "" : getRangeCharacters(session, this, createOptions(characterOptions, defaultCharacterOptions)).join(""); 8289 } 8290 ), 8291 8292 selectCharacters: createEntryPointFunction( 8293 function(session, containerNode, startIndex, endIndex, characterOptions) { 8294 var moveOptions = { characterOptions: characterOptions }; 8295 if (!containerNode) { 8296 containerNode = getBody( this.getDocument() ); 8297 } 8298 this.selectNodeContents(containerNode); 8299 this.collapse(true); 8300 this.moveStart("character", startIndex, moveOptions); 8301 this.collapse(true); 8302 this.moveEnd("character", endIndex - startIndex, moveOptions); 8303 } 8304 ), 8305 8306 // Character indexes are relative to the start of node 8307 toCharacterRange: createEntryPointFunction( 8308 function(session, containerNode, characterOptions) { 8309 if (!containerNode) { 8310 containerNode = getBody( this.getDocument() ); 8311 } 8312 var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode); 8313 var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1); 8314 var rangeBetween = this.cloneRange(); 8315 var startIndex, endIndex; 8316 if (rangeStartsBeforeNode) { 8317 rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex); 8318 startIndex = -rangeBetween.text(characterOptions).length; 8319 } else { 8320 rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset); 8321 startIndex = rangeBetween.text(characterOptions).length; 8322 } 8323 endIndex = startIndex + this.text(characterOptions).length; 8324 8325 return { 8326 start: startIndex, 8327 end: endIndex 8328 }; 8329 } 8330 ), 8331 8332 findText: createEntryPointFunction( 8333 function(session, searchTermParam, findOptions) { 8334 // Set up options 8335 findOptions = createNestedOptions(findOptions, defaultFindOptions); 8336 8337 // Create word options if we're matching whole words only 8338 if (findOptions.wholeWordsOnly) { 8339 // We don't ever want trailing spaces for search results 8340 findOptions.wordOptions.includeTrailingSpace = false; 8341 } 8342 8343 var backward = isDirectionBackward(findOptions.direction); 8344 8345 // Create a range representing the search scope if none was provided 8346 var searchScopeRange = findOptions.withinRange; 8347 if (!searchScopeRange) { 8348 searchScopeRange = api.createRange(); 8349 searchScopeRange.selectNodeContents(this.getDocument()); 8350 } 8351 8352 // Examine and prepare the search term 8353 var searchTerm = searchTermParam, isRegex = false; 8354 if (typeof searchTerm == "string") { 8355 if (!findOptions.caseSensitive) { 8356 searchTerm = searchTerm.toLowerCase(); 8357 } 8358 } else { 8359 isRegex = true; 8360 } 8361 8362 var initialPos = session.getRangeBoundaryPosition(this, !backward); 8363 8364 // Adjust initial position if it lies outside the search scope 8365 var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset); 8366 8367 if (comparison === -1) { 8368 initialPos = session.getRangeBoundaryPosition(searchScopeRange, true); 8369 } else if (comparison === 1) { 8370 initialPos = session.getRangeBoundaryPosition(searchScopeRange, false); 8371 } 8372 8373 var pos = initialPos; 8374 var wrappedAround = false; 8375 8376 // Try to find a match and ignore invalid ones 8377 var findResult; 8378 while (true) { 8379 findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions); 8380 8381 if (findResult) { 8382 if (findResult.valid) { 8383 this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset); 8384 return true; 8385 } else { 8386 // We've found a match that is not a whole word, so we carry on searching from the point immediately 8387 // after the match 8388 pos = backward ? findResult.startPos : findResult.endPos; 8389 } 8390 } else if (findOptions.wrap && !wrappedAround) { 8391 // No result found but we're wrapping around and limiting the scope to the unsearched part of the range 8392 searchScopeRange = searchScopeRange.cloneRange(); 8393 pos = session.getRangeBoundaryPosition(searchScopeRange, !backward); 8394 searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward); 8395 wrappedAround = true; 8396 } else { 8397 // Nothing found and we can't wrap around, so we're done 8398 return false; 8399 } 8400 } 8401 } 8402 ), 8403 8404 pasteHtml: function(html) { 8405 this.deleteContents(); 8406 if (html) { 8407 var frag = this.createContextualFragment(html); 8408 var lastChild = frag.lastChild; 8409 this.insertNode(frag); 8410 this.collapseAfter(lastChild); 8411 } 8412 } 8413 }); 8414 8415 /*----------------------------------------------------------------------------------------------------------------*/ 8416 8417 // Extensions to the Rangy Selection object 8418 8419 function createSelectionTrimmer(methodName) { 8420 return createEntryPointFunction( 8421 function(session, characterOptions) { 8422 var trimmed = false; 8423 this.changeEachRange(function(range) { 8424 trimmed = range[methodName](characterOptions) || trimmed; 8425 }); 8426 return trimmed; 8427 } 8428 ); 8429 } 8430 8431 extend(api.selectionPrototype, { 8432 expand: createEntryPointFunction( 8433 function(session, unit, expandOptions) { 8434 this.changeEachRange(function(range) { 8435 range.expand(unit, expandOptions); 8436 }); 8437 } 8438 ), 8439 8440 move: createEntryPointFunction( 8441 function(session, unit, count, options) { 8442 var unitsMoved = 0; 8443 if (this.focusNode) { 8444 this.collapse(this.focusNode, this.focusOffset); 8445 var range = this.getRangeAt(0); 8446 if (!options) { 8447 options = {}; 8448 } 8449 options.characterOptions = createOptions(options.characterOptions, defaultCaretCharacterOptions); 8450 unitsMoved = range.move(unit, count, options); 8451 this.setSingleRange(range); 8452 } 8453 return unitsMoved; 8454 } 8455 ), 8456 8457 trimStart: createSelectionTrimmer("trimStart"), 8458 trimEnd: createSelectionTrimmer("trimEnd"), 8459 trim: createSelectionTrimmer("trim"), 8460 8461 selectCharacters: createEntryPointFunction( 8462 function(session, containerNode, startIndex, endIndex, direction, characterOptions) { 8463 var range = api.createRange(containerNode); 8464 range.selectCharacters(containerNode, startIndex, endIndex, characterOptions); 8465 this.setSingleRange(range, direction); 8466 } 8467 ), 8468 8469 saveCharacterRanges: createEntryPointFunction( 8470 function(session, containerNode, characterOptions) { 8471 var ranges = this.getAllRanges(), rangeCount = ranges.length; 8472 var rangeInfos = []; 8473 8474 var backward = rangeCount == 1 && this.isBackward(); 8475 8476 for (var i = 0, len = ranges.length; i < len; ++i) { 8477 rangeInfos[i] = { 8478 characterRange: ranges[i].toCharacterRange(containerNode, characterOptions), 8479 backward: backward, 8480 characterOptions: characterOptions 8481 }; 8482 } 8483 8484 return rangeInfos; 8485 } 8486 ), 8487 8488 restoreCharacterRanges: createEntryPointFunction( 8489 function(session, containerNode, saved) { 8490 this.removeAllRanges(); 8491 for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) { 8492 rangeInfo = saved[i]; 8493 characterRange = rangeInfo.characterRange; 8494 range = api.createRange(containerNode); 8495 range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions); 8496 this.addRange(range, rangeInfo.backward); 8497 } 8498 } 8499 ), 8500 8501 text: createEntryPointFunction( 8502 function(session, characterOptions) { 8503 var rangeTexts = []; 8504 for (var i = 0, len = this.rangeCount; i < len; ++i) { 8505 rangeTexts[i] = this.getRangeAt(i).text(characterOptions); 8506 } 8507 return rangeTexts.join(""); 8508 } 8509 ) 8510 }); 8511 8512 /*----------------------------------------------------------------------------------------------------------------*/ 8513 8514 // Extensions to the core rangy object 8515 8516 api.innerText = function(el, characterOptions) { 8517 var range = api.createRange(el); 8518 range.selectNodeContents(el); 8519 var text = range.text(characterOptions); 8520 return text; 8521 }; 8522 8523 api.createWordIterator = function(startNode, startOffset, iteratorOptions) { 8524 var session = getSession(); 8525 iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions); 8526 var startPos = session.getPosition(startNode, startOffset); 8527 var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.wordOptions); 8528 var backward = isDirectionBackward(iteratorOptions.direction); 8529 8530 return { 8531 next: function() { 8532 return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken(); 8533 }, 8534 8535 dispose: function() { 8536 tokenizedTextProvider.dispose(); 8537 this.next = function() {}; 8538 } 8539 }; 8540 }; 8541 8542 /*----------------------------------------------------------------------------------------------------------------*/ 8543 8544 api.noMutation = function(func) { 8545 var session = getSession(); 8546 func(session); 8547 endSession(); 8548 }; 8549 8550 api.noMutation.createEntryPointFunction = createEntryPointFunction; 8551 8552 api.textRange = { 8553 isBlockNode: isBlockNode, 8554 isCollapsedWhitespaceNode: isCollapsedWhitespaceNode, 8555 8556 createPosition: createEntryPointFunction( 8557 function(session, node, offset) { 8558 return session.getPosition(node, offset); 8559 } 8560 ) 8561 }; 8562 }); 8563 8564 return rangy; 8565}, this); 8566 8567 8568/** 8569 * Highlighter module for Rangy, a cross-browser JavaScript range and selection library 8570 * https://github.com/timdown/rangy 8571 * 8572 * Depends on Rangy core, ClassApplier and optionally TextRange modules. 8573 * 8574 * Copyright 2015, Tim Down 8575 * Licensed under the MIT license. 8576 * Version: 1.3.1-dev 8577 * Build date: 20 May 2015 8578 */ 8579 (function(factory, root) { 8580 if (typeof define == "function" && define.amd) { 8581 // AMD. Register as an anonymous module with a dependency on Rangy. 8582 define(["./rangy-core"], factory); 8583 } else if (typeof module != "undefined" && typeof exports == "object") { 8584 // Node/CommonJS style 8585 module.exports = factory( require("rangy") ); 8586 } else { 8587 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 8588 factory(root.rangy); 8589 } 8590})(function(rangy) { 8591 rangy.createModule("Highlighter", ["ClassApplier"], function(api, module) { 8592 var dom = api.dom; 8593 var contains = dom.arrayContains; 8594 var getBody = dom.getBody; 8595 var createOptions = api.util.createOptions;
8596 var forEach = api.util.forEach; 8597 var nextHighlightId = 1; 8598 8599 // Puts highlights in order, last in document first. 8600 function compareHighlights(h1, h2) { 8601 return h1.characterRange.start - h2.characterRange.start; 8602 } 8603 8604 function getContainerElement(doc, id) { 8605 return id ? doc.getElementById(id) : getBody(doc); 8606 } 8607 8608 /*----------------------------------------------------------------------------------------------------------------*/ 8609 8610 var highlighterTypes = {}; 8611 8612 function HighlighterType(type, converterCreator) { 8613 this.type = type; 8614 this.converterCreator = converterCreator; 8615 } 8616 8617 HighlighterType.prototype.create = function() { 8618 var converter = this.converterCreator(); 8619 converter.type = this.type; 8620 return converter; 8621 }; 8622 8623 function registerHighlighterType(type, converterCreator) { 8624 highlighterTypes[type] = new HighlighterType(type, converterCreator); 8625 } 8626 8627 function getConverter(type) { 8628 var highlighterType = highlighterTypes[type]; 8629 if (highlighterType instanceof HighlighterType) { 8630 return highlighterType.create(); 8631 } else { 8632 throw new Error("Highlighter type '" + type + "' is not valid"); 8633 } 8634 } 8635 8636 api.registerHighlighterType = registerHighlighterType; 8637 8638 /*----------------------------------------------------------------------------------------------------------------*/ 8639 8640 function CharacterRange(start, end) { 8641 this.start = start; 8642 this.end = end; 8643 } 8644 8645 CharacterRange.prototype = { 8646 intersects: function(charRange) { 8647 return this.start < charRange.end && this.end > charRange.start; 8648 }, 8649 8650 isContiguousWith: function(charRange) { 8651 return this.start == charRange.end || this.end == charRange.start; 8652 }, 8653 8654 union: function(charRange) { 8655 return new CharacterRange(Math.min(this.start, charRange.start), Math.max(this.end, charRange.end)); 8656 }, 8657 8658 intersection: function(charRange) { 8659 return new CharacterRange(Math.max(this.start, charRange.start), Math.min(this.end, charRange.end)); 8660 }, 8661 8662 getComplements: function(charRange) { 8663 var ranges = []; 8664 if (this.start >= charRange.start) { 8665 if (this.end <= charRange.end) { 8666 return []; 8667 } 8668 ranges.push(new CharacterRange(charRange.end, this.end)); 8669 } else { 8670 ranges.push(new CharacterRange(this.start, Math.min(this.end, charRange.start))); 8671 if (this.end > charRange.end) { 8672 ranges.push(new CharacterRange(charRange.end, this.end)); 8673 } 8674 } 8675 return ranges; 8676 }, 8677 8678 toString: function() { 8679 return "[CharacterRange(" + this.start + ", " + this.end + ")]"; 8680 } 8681 }; 8682 8683 CharacterRange.fromCharacterRange = function(charRange) { 8684 return new CharacterRange(charRange.start, charRange.end); 8685 }; 8686 8687 /*----------------------------------------------------------------------------------------------------------------*/ 8688 8689 var textContentConverter = { 8690 rangeToCharacterRange: function(range, containerNode) { 8691 var bookmark = range.getBookmark(containerNode); 8692 return new CharacterRange(bookmark.start, bookmark.end); 8693 }, 8694 8695 characterRangeToRange: function(doc, characterRange, containerNode) { 8696 var range = api.createRange(doc); 8697 range.moveToBookmark({ 8698 start: characterRange.start, 8699 end: characterRange.end, 8700 containerNode: containerNode 8701 }); 8702 8703 return range; 8704 }, 8705 8706 serializeSelection: function(selection, containerNode) { 8707 var ranges = selection.getAllRanges(), rangeCount = ranges.length; 8708 var rangeInfos = []; 8709 8710 var backward = rangeCount == 1 && selection.isBackward(); 8711 8712 for (var i = 0, len = ranges.length; i < len; ++i) { 8713 rangeInfos[i] = { 8714 characterRange: this.rangeToCharacterRange(ranges[i], containerNode), 8715 backward: backward 8716 }; 8717 } 8718 8719 return rangeInfos; 8720 }, 8721 8722 restoreSelection: function(selection, savedSelection, containerNode) { 8723 selection.removeAllRanges(); 8724 var doc = selection.win.document;
8725 for (var i = 0, len = savedSelection.length, range, rangeInfo, characterRange; i < len; ++i) { 8726 rangeInfo = savedSelection[i]; 8727 characterRange = rangeInfo.characterRange; 8728 range = this.characterRangeToRange(doc, rangeInfo.characterRange, containerNode); 8729 selection.addRange(range, rangeInfo.backward); 8730 } 8731 } 8732 }; 8733 8734 registerHighlighterType("textContent", function() { 8735 return textContentConverter; 8736 }); 8737 8738 /*----------------------------------------------------------------------------------------------------------------*/ 8739 8740 // Lazily load the TextRange-based converter so that the dependency is only checked when required. 8741 registerHighlighterType("TextRange", (function() { 8742 var converter; 8743 8744 return function() { 8745 if (!converter) { 8746 // Test that textRangeModule exists and is supported 8747 var textRangeModule = api.modules.TextRange; 8748 if (!textRangeModule) { 8749 throw new Error("TextRange module is missing."); 8750 } else if (!textRangeModule.supported) { 8751 throw new Error("TextRange module is present but not supported."); 8752 } 8753 8754 converter = { 8755 rangeToCharacterRange: function(range, containerNode) { 8756 return CharacterRange.fromCharacterRange( range.toCharacterRange(containerNode) ); 8757 }, 8758 8759 characterRangeToRange: function(doc, characterRange, containerNode) { 8760 var range = api.createRange(doc); 8761 range.selectCharacters(containerNode, characterRange.start, characterRange.end); 8762 return range; 8763 }, 8764 8765 serializeSelection: function(selection, containerNode) { 8766 return selection.saveCharacterRanges(containerNode); 8767 }, 8768 8769 restoreSelection: function(selection, savedSelection, containerNode) { 8770 selection.restoreCharacterRanges(containerNode, savedSelection); 8771 } 8772 }; 8773 } 8774 8775 return converter; 8776 }; 8777 })()); 8778 8779 /*----------------------------------------------------------------------------------------------------------------*/ 8780 8781 function Highlight(doc, characterRange, classApplier, converter, id, containerElementId) { 8782 if (id) { 8783 this.id = id; 8784 nextHighlightId = Math.max(nextHighlightId, id + 1); 8785 } else { 8786 this.id = nextHighlightId++; 8787 } 8788 this.characterRange = characterRange; 8789 this.doc = doc; 8790 this.classApplier = classApplier; 8791 this.converter = converter; 8792 this.containerElementId = containerElementId || null; 8793 this.applied = false; 8794 } 8795 8796 Highlight.prototype = { 8797 getContainerElement: function() { 8798 return getContainerElement(this.doc, this.containerElementId); 8799 }, 8800 8801 getRange: function() { 8802 return this.converter.characterRangeToRange(this.doc, this.characterRange, this.getContainerElement()); 8803 }, 8804 8805 fromRange: function(range) { 8806 this.characterRange = this.converter.rangeToCharacterRange(range, this.getContainerElement()); 8807 }, 8808 8809 getText: function() { 8810 return this.getRange().toString(); 8811 }, 8812 8813 containsElement: function(el) { 8814 return this.getRange().containsNodeContents(el.firstChild); 8815 }, 8816 8817 unapply: function() { 8818 this.classApplier.undoToRange(this.getRange()); 8819 this.applied = false; 8820 }, 8821 8822 apply: function() { 8823 this.classApplier.applyToRange(this.getRange()); 8824 this.applied = true; 8825 }, 8826 8827 getHighlightElements: function() { 8828 return this.classApplier.getElementsWithClassIntersectingRange(this.getRange()); 8829 }, 8830 8831 toString: function() { 8832 return "[Highlight(ID: " + this.id + ", class: " + this.classApplier.className + ", character range: " + 8833 this.characterRange.start + " - " + this.characterRange.end + ")]"; 8834 } 8835 }; 8836 8837 /*----------------------------------------------------------------------------------------------------------------*/ 8838 8839 function Highlighter(doc, type) { 8840 type = type || "textContent"; 8841 this.doc = doc || document; 8842 this.classAppliers = {}; 8843 this.highlights = []; 8844 this.converter = getConverter(type); 8845 } 8846 8847 Highlighter.prototype = { 8848 addClassApplier: function(classApplier) { 8849 this.classAppliers[classApplier.className] = classApplier; 8850 }, 8851 8852 getHighlightForElement: function(el) { 8853 var highlights = this.highlights; 8854 for (var i = 0, len = highlights.length; i < len; ++i) { 8855 if (highlights[i].containsElement(el)) { 8856 return highlights[i]; 8857 } 8858 } 8859 return null; 8860 }, 8861 8862 removeHighlights: function(highlights) { 8863 for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) { 8864 highlight = this.highlights[i]; 8865 if (contains(highlights, highlight)) { 8866 highlight.unapply(); 8867 this.highlights.splice(i--, 1); 8868 } 8869 } 8870 }, 8871 8872 removeAllHighlights: function() { 8873 this.removeHighlights(this.highlights); 8874 }, 8875 8876 getIntersectingHighlights: function(ranges) { 8877 // Test each range against each of the highlighted ranges to see whether they overlap 8878 var intersectingHighlights = [], highlights = this.highlights;
8879 forEach(ranges, function(range) { 8880 //var selCharRange = converter.rangeToCharacterRange(range); 8881 forEach(highlights, function(highlight) { 8882 if (range.intersectsRange( highlight.getRange() ) && !contains(intersectingHighlights, highlight)) { 8883 intersectingHighlights.push(highlight); 8884 } 8885 }); 8886 }); 8887 8888 return intersectingHighlights; 8889 }, 8890 8891 highlightCharacterRanges: function(className, charRanges, options) { 8892 var i, len, j; 8893 var highlights = this.highlights; 8894 var converter = this.converter; 8895 var doc = this.doc; 8896 var highlightsToRemove = []; 8897 var classApplier = className ? this.classAppliers[className] : null; 8898 8899 options = createOptions(options, { 8900 containerElementId: null, 8901 exclusive: true 8902 }); 8903 8904 var containerElementId = options.containerElementId; 8905 var exclusive = options.exclusive; 8906 8907 var containerElement, containerElementRange, containerElementCharRange; 8908 if (containerElementId) { 8909 containerElement = this.doc.getElementById(containerElementId); 8910 if (containerElement) { 8911 containerElementRange = api.createRange(this.doc); 8912 containerElementRange.selectNodeContents(containerElement); 8913 containerElementCharRange = new CharacterRange(0, containerElementRange.toString().length); 8914 } 8915 } 8916 8917 var charRange, highlightCharRange, removeHighlight, isSameClassApplier, highlightsToKeep, splitHighlight; 8918 8919 for (i = 0, len = charRanges.length; i < len; ++i) { 8920 charRange = charRanges[i]; 8921 highlightsToKeep = []; 8922 8923 // Restrict character range to container element, if it exists 8924 if (containerElementCharRange) { 8925 charRange = charRange.intersection(containerElementCharRange); 8926 } 8927 8928 // Ignore empty ranges 8929 if (charRange.start == charRange.end) { 8930 continue; 8931 } 8932 8933 // Check for intersection with existing highlights. For each intersection, create a new highlight 8934 // which is the union of the highlight range and the selected range 8935 for (j = 0; j < highlights.length; ++j) { 8936 removeHighlight = false; 8937 8938 if (containerElementId == highlights[j].containerElementId) { 8939 highlightCharRange = highlights[j].characterRange; 8940 isSameClassApplier = (classApplier == highlights[j].classApplier); 8941 splitHighlight = !isSameClassApplier && exclusive; 8942 8943 // Replace the existing highlight if it needs to be: 8944 // 1. merged (isSameClassApplier) 8945 // 2. partially or entirely erased (className === null) 8946 // 3. partially or entirely replaced (isSameClassApplier == false && exclusive == true) 8947 if ( (highlightCharRange.intersects(charRange) || highlightCharRange.isContiguousWith(charRange)) && 8948 (isSameClassApplier || splitHighlight) ) { 8949 8950 // Remove existing highlights, keeping the unselected parts 8951 if (splitHighlight) { 8952 forEach(highlightCharRange.getComplements(charRange), function(rangeToAdd) { 8953 highlightsToKeep.push( new Highlight(doc, rangeToAdd, highlights[j].classApplier, converter, null, containerElementId) ); 8954 }); 8955 } 8956 8957 removeHighlight = true; 8958 if (isSameClassApplier) { 8959 charRange = highlightCharRange.union(charRange); 8960 } 8961 } 8962 } 8963 8964 if (removeHighlight) { 8965 highlightsToRemove.push(highlights[j]); 8966 highlights[j] = new Highlight(doc, highlightCharRange.union(charRange), classApplier, converter, null, containerElementId); 8967 } else { 8968 highlightsToKeep.push(highlights[j]); 8969 } 8970 } 8971 8972 // Add new range 8973 if (classApplier) { 8974 highlightsToKeep.push(new Highlight(doc, charRange, classApplier, converter, null, containerElementId)); 8975 } 8976 this.highlights = highlights = highlightsToKeep; 8977 } 8978 8979 // Remove the old highlights
8980 forEach(highlightsToRemove, function(highlightToRemove) { 8981 highlightToRemove.unapply(); 8982 }); 8983 8984 // Apply new highlights 8985 var newHighlights = []; 8986 forEach(highlights, function(highlight) { 8987 if (!highlight.applied) { 8988 highlight.apply(); 8989 newHighlights.push(highlight); 8990 } 8991 }); 8992 8993 return newHighlights; 8994 }, 8995 8996 highlightRanges: function(className, ranges, options) { 8997 var selCharRanges = []; 8998 var converter = this.converter; 8999 9000 options = createOptions(options, { 9001 containerElement: null, 9002 exclusive: true 9003 }); 9004 9005 var containerElement = options.containerElement; 9006 var containerElementId = containerElement ? containerElement.id : null; 9007 var containerElementRange; 9008 if (containerElement) { 9009 containerElementRange = api.createRange(containerElement); 9010 containerElementRange.selectNodeContents(containerElement); 9011 } 9012 9013 forEach(ranges, function(range) { 9014 var scopedRange = containerElement ? containerElementRange.intersection(range) : range; 9015 selCharRanges.push( converter.rangeToCharacterRange(scopedRange, containerElement || getBody(range.getDocument())) ); 9016 }); 9017 9018 return this.highlightCharacterRanges(className, selCharRanges, { 9019 containerElementId: containerElementId, 9020 exclusive: options.exclusive 9021 }); 9022 }, 9023 9024 highlightSelection: function(className, options) { 9025 var converter = this.converter; 9026 var classApplier = className ? this.classAppliers[className] : false; 9027 9028 options = createOptions(options, { 9029 containerElementId: null, 9030 exclusive: true 9031 }); 9032 9033 var containerElementId = options.containerElementId; 9034 var exclusive = options.exclusive; 9035 var selection = options.selection || api.getSelection(this.doc); 9036 var doc = selection.win.document; 9037 var containerElement = getContainerElement(doc, containerElementId); 9038 9039 if (!classApplier && className !== false) { 9040 throw new Error("No class applier found for class '" + className + "'"); 9041 } 9042 9043 // Store the existing selection as character ranges 9044 var serializedSelection = converter.serializeSelection(selection, containerElement); 9045 9046 // Create an array of selected character ranges 9047 var selCharRanges = []; 9048 forEach(serializedSelection, function(rangeInfo) { 9049 selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) ); 9050 }); 9051 9052 var newHighlights = this.highlightCharacterRanges(className, selCharRanges, { 9053 containerElementId: containerElementId, 9054 exclusive: exclusive 9055 }); 9056 9057 // Restore selection 9058 converter.restoreSelection(selection, serializedSelection, containerElement); 9059 9060 return newHighlights; 9061 }, 9062 9063 unhighlightSelection: function(selection) { 9064 selection = selection || api.getSelection(this.doc); 9065 var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() ); 9066 this.removeHighlights(intersectingHighlights); 9067 selection.removeAllRanges(); 9068 return intersectingHighlights; 9069 }, 9070 9071 getHighlightsInSelection: function(selection) { 9072 selection = selection || api.getSelection(this.doc); 9073 return this.getIntersectingHighlights(selection.getAllRanges()); 9074 }, 9075 9076 selectionOverlapsHighlight: function(selection) { 9077 return this.getHighlightsInSelection(selection).length > 0; 9078 }, 9079 9080 serialize: function(options) { 9081 var highlighter = this; 9082 var highlights = highlighter.highlights; 9083 var serializedType, serializedHighlights, convertType, serializationConverter; 9084 9085 highlights.sort(compareHighlights); 9086 options = createOptions(options, { 9087 serializeHighlightText: false, 9088 type: highlighter.converter.type 9089 }); 9090 9091 serializedType = options.type; 9092 convertType = (serializedType != highlighter.converter.type); 9093 9094 if (convertType) { 9095 serializationConverter = getConverter(serializedType); 9096 } 9097 9098 serializedHighlights = ["type:" + serializedType]; 9099
9100 forEach(highlights, function(highlight) { 9101 var characterRange = highlight.characterRange; 9102 var containerElement; 9103 9104 // Convert to the current Highlighter's type, if different from the serialization type 9105 if (convertType) { 9106 containerElement = highlight.getContainerElement(); 9107 characterRange = serializationConverter.rangeToCharacterRange( 9108 highlighter.converter.characterRangeToRange(highlighter.doc, characterRange, containerElement), 9109 containerElement 9110 ); 9111 } 9112 9113 var parts = [ 9114 characterRange.start, 9115 characterRange.end, 9116 highlight.id, 9117 highlight.classApplier.className, 9118 highlight.containerElementId 9119 ]; 9120 9121 if (options.serializeHighlightText) { 9122 parts.push(highlight.getText()); 9123 } 9124 serializedHighlights.push( parts.join("$") ); 9125 }); 9126 9127 return serializedHighlights.join("|"); 9128 }, 9129 9130 deserialize: function(serialized) { 9131 var serializedHighlights = serialized.split("|"); 9132 var highlights = []; 9133 9134 var firstHighlight = serializedHighlights[0]; 9135 var regexResult; 9136 var serializationType, serializationConverter, convertType = false;
9137 if ( firstHighlight && (regexResult = /^type:(\w+)$/.exec(firstHighlight)) ) { 9138 serializationType = regexResult[1]; 9139 if (serializationType != this.converter.type) { 9140 serializationConverter = getConverter(serializationType); 9141 convertType = true; 9142 } 9143 serializedHighlights.shift(); 9144 } else { 9145 throw new Error("Serialized highlights are invalid."); 9146 } 9147 9148 var classApplier, highlight, characterRange, containerElementId, containerElement; 9149 9150 for (var i = serializedHighlights.length, parts; i-- > 0; ) { 9151 parts = serializedHighlights[i].split("$"); 9152 characterRange = new CharacterRange(+parts[0], +parts[1]); 9153 containerElementId = parts[4] || null; 9154 9155 // Convert to the current Highlighter's type, if different from the serialization type 9156 if (convertType) { 9157 containerElement = getContainerElement(this.doc, containerElementId); 9158 characterRange = this.converter.rangeToCharacterRange( 9159 serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement), 9160 containerElement 9161 ); 9162 } 9163 9164 classApplier = this.classAppliers[ parts[3] ]; 9165 9166 if (!classApplier) { 9167 throw new Error("No class applier found for class '" + parts[3] + "'"); 9168 } 9169 9170 highlight = new Highlight(this.doc, characterRange, classApplier, this.converter, parseInt(parts[2]), containerElementId); 9171 highlight.apply(); 9172 highlights.push(highlight); 9173 } 9174 this.highlights = highlights; 9175 } 9176 }; 9177 9178 api.Highlighter = Highlighter; 9179 9180 api.createHighlighter = function(doc, rangeCharacterOffsetConverterType) { 9181 return new Highlighter(doc, rangeCharacterOffsetConverterType); 9182 }; 9183 }); 9184 9185 return rangy; 9186}, this); 9187 9188 9189/** 9190 * Position module for Rangy. 9191 * Extensions to Range and Selection objects to provide access to pixel positions relative to the viewport or document. 9192 * 9193 * Part of Rangy, a cross-browser JavaScript range and selection library 9194 * https://github.com/timdown/rangy 9195 * 9196 * Depends on Rangy core. 9197 * 9198 * Copyright %%build:year%%, Tim Down 9199 * Licensed under the MIT license. 9200 * Version: %%build:version%% 9201 * Build date: %%build:date%% 9202 */ 9203 rangy.createModule("Position", ["WrappedSelection"], function(api, module) { 9204 //var log = log4javascript.getLogger("rangy.position"); 9205 9206 var NUMBER = "number", UNDEF = "undefined"; 9207 var WrappedRange = api.WrappedRange; 9208 var WrappedTextRange = api.WrappedTextRange; 9209 var dom = api.dom, util = api.util, DomPosition = dom.DomPosition; 9210 9211 // Feature detection 9212 9213 //var caretPositionFromPointSupported = (typeof document.caretPositionFromPoint != UNDEF); 9214 9215 // Since Rangy can deal with multiple documents which could be in different modes, we have to do the checks every 9216 // time, unless we cache a getScrollPosition function in each document. This would necessarily pollute the 9217 // document's global namespace, which I'm choosing to view as a greater evil than a slight performance hit. 9218 function getScrollPosition(win) { 9219 var x = 0, y = 0; 9220 if (typeof win.pageXOffset == NUMBER && typeof win.pageYOffset == NUMBER) { 9221 x = win.pageXOffset; 9222 y = win.pageYOffset; 9223 } else { 9224 var doc = win.document; 9225 var docEl = doc.documentElement; 9226 var compatMode = doc.compatMode; 9227 var scrollEl = (typeof compatMode == "string" && compatMode.indexOf("CSS") >= 0 && docEl) 9228 ? docEl : dom.getBody(doc); 9229 9230 if (scrollEl && typeof scrollEl.scrollLeft == NUMBER && typeof scrollEl.scrollTop == NUMBER) { 9231 try { 9232 x = scrollEl.scrollLeft; 9233 y = scrollEl.scrollTop; 9234 } catch (ex) {} 9235 } 9236 } 9237 return { x: x, y: y }; 9238 } 9239 9240 function getAncestorElement(node, tagName) { 9241 tagName = tagName.toLowerCase(); 9242 while (node) { 9243 if (node.nodeType == 1 && node.tagName.toLowerCase() == tagName) { 9244 return node; 9245 } 9246 node = node.parentNode; 9247 } 9248 return null; 9249 } 9250 9251 function Rect(top, right, bottom, left) { 9252 this.top = top; 9253 this.right = right; 9254 this.bottom = bottom; 9255 this.left = left; 9256 this.width = right - left; 9257 this.height = bottom - top; 9258 } 9259 9260 function createRelativeRect(rect, dx, dy) { 9261 return new Rect(rect.top + dy, rect.right + dx, rect.bottom + dy, rect.left + dx); 9262 } 9263 9264 function adjustClientRect(rect, doc) { 9265 // Older IEs have an issue with a two pixel margin on the body element 9266 var dx = 0, dy = 0; 9267 var docEl = doc.documentElement, body = dom.getBody(doc); 9268 var container = (docEl.clientWidth === 0 && typeof body.clientTop == NUMBER) ? body : docEl; 9269 var clientLeft = container.clientLeft, clientTop = container.clientTop; 9270 if (clientLeft) { 9271 dx = -clientLeft; 9272 } 9273 if (clientTop) { 9274 dy = -clientTop; 9275 } 9276 return createRelativeRect(rect, dx, dy); 9277 } 9278 9279 function mergeRects(rects) { 9280 var tops = [], bottoms = [], lefts = [], rights = []; 9281 for (var i = 0, len = rects.length, rect; i < len; ++i) { 9282 rect = rects[i]; 9283 if (rect) { 9284 tops.push(rect.top); 9285 bottoms.push(rect.bottom); 9286 lefts.push(rect.left); 9287 rights.push(rect.right); 9288 } 9289 } 9290 return new Rect( 9291 Math.min.apply(Math, tops), 9292 Math.max.apply(Math, rights), 9293 Math.max.apply(Math, bottoms), 9294 Math.min.apply(Math, lefts) 9295 ); 9296 } 9297 9298 function getTextRangePosition(doc, x, y) { 9299 var textRange = dom.getBody(doc).createTextRange(); 9300 textRange.moveToPoint(x, y); 9301 var range = new api.WrappedTextRange(textRange); 9302 return new DomPosition(range.startContainer, range.startOffset); 9303 } 9304 9305 function caretPositionFromPoint(doc, x, y) { 9306 var pos = doc.caretPositionFromPoint(x, y); 9307 return new DomPosition(pos.offsetNode, pos.offset); 9308 } 9309 9310 function caretRangeFromPoint(doc, x, y) { 9311 var range = doc.caretRangeFromPoint(x, y); 9312 return new DomPosition(range.startContainer, range.startOffset); 9313 } 9314
9315 function getLastRangeRect(range) { 9316 var rects = (range.nativeRange || range).getClientRects(); 9317 return (rects.length > 0) ? rects[rects.length - 1] : null; 9318 } 9319 9320 function pointIsInOrAboveRect(x, y, rect) { 9321 console.log("pointIsInOrAboveRect", x, y, Math.floor(rect.top), Math.floor(rect.right), Math.floor(rect.bottom), Math.floor(rect.left)) 9322 return y < rect.bottom && x >= rect.left && x <= rect.right; 9323 } 9324 9325 function positionFromPoint(doc, x, y, favourPrecedingPosition) { 9326 var el = doc.elementFromPoint(x, y); 9327 9328 console.log("elementFromPoint is ", el); 9329 9330 var range = api.createRange(doc); 9331 range.selectNodeContents(el); 9332 range.collapse(true); 9333 9334 var node = el.firstChild, offset, rect, textLen; 9335 9336 if (!node) { 9337 node = el.parentNode; 9338 offset = dom.getNodeIndex(el); 9339 if (!favourPrecedingPosition) { 9340 ++offset; 9341 } 9342 } else { 9343 // Search through the text node children of el 9344 main: while (node) { 9345 console.log(node); 9346 if (node.nodeType == 3) { 9347 // Go through the text node character by character 9348 for (offset = 0, textLen = node.length; offset <= textLen; ++offset) { 9349 range.setEnd(node, offset); 9350 rect = getLastRangeRect(range); 9351 if (rect && pointIsInOrAboveRect(x, y, rect)) { 9352 // We've gone past the point. Now we check which side (left or right) of the character the point is nearer to 9353 if (rect.right - x > x - rect.left) { 9354 --offset; 9355 } 9356 break main; 9357 } 9358 } 9359 } else { 9360 // Handle elements 9361 range.setEndAfter(node); 9362 rect = getLastRangeRect(range); 9363 if (rect && pointIsInOrAboveRect(x, y, rect)) { 9364 offset = dom.getNodeIndex(node); 9365 node = el.parentNode; 9366 if (!favourPrecedingPosition) { 9367 ++offset; 9368 } 9369 break; 9370 } 9371 } 9372 9373 node = node.nextSibling; 9374 } 9375 if (!node) { 9376 node = el; 9377 offset = el.childNodes.length; 9378 } 9379 } 9380 9381 return new DomPosition(node, offset); 9382 } 9383 9384 function createCaretPositionFromPointGetter(doc) { 9385 if (api.features.implementsTextRange) { 9386 return getTextRangePosition; 9387 } else if (typeof doc.caretPositionFromPoint != UNDEF) { 9388 return caretPositionFromPoint; 9389 } else if (typeof doc.caretRangeFromPoint != UNDEF) { 9390 return caretRangeFromPoint; 9391 } else if (typeof doc.elementFromPoint != UNDEF && rangeSupportsGetClientRects) { 9392 return positionFromPoint; 9393 } else { 9394 throw module.createError("createCaretPositionFromPointGetter(): Browser does not provide a recognised method to create a selection from pixel coordinates"); 9395 } 9396 } 9397 9398 function createRangeFromPoints(startX, startY, endX, endY, doc) { 9399 doc = dom.getContentDocument(doc, module, "createRangeFromPoints"); 9400 var positionFinder = createCaretPositionFromPointGetter(doc); 9401 var startPos = positionFinder(doc, startX, startY, false); 9402 var endPos = positionFinder(doc, endX, endY, true); 9403 console.log(startPos.node, startPos.offset, endPos.node, endPos.offset); 9404 var range = api.createRange(doc); 9405 range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset); 9406 return range; 9407 } 9408 9409 function moveSelectionToPoints(anchorX, anchorY, focusX, focusY, doc) { 9410 var startX, startY, endX, endY; 9411 9412 // Detect backward selection for coordinates and flip start and end coordinates if necessary 9413 var backward = focusY < anchorY || (anchorY == focusY && focusX < anchorX); 9414 9415 if (backward) {
9416 startX = focusX; 9417 startY = focusY; 9418 endX = anchorX; 9419 endY = anchorY; 9420 } else { 9421 startX = anchorX; 9422 startY = anchorY; 9423 endX = focusX; 9424 endY = focusY; 9425 } 9426 9427 var sel = rangy.getSelection(doc); 9428 var range = createRangeFromPoints(startX, startY, endX, endY, doc); 9429 sel.setSingleRange(range); 9430 return sel; 9431 } 9432 9433 // Test that <span> elements support getBoundingClientRect 9434 var span = document.createElement("span"); 9435 var elementSupportsGetBoundingClientRect = util.isHostMethod(span, "getBoundingClientRect"); 9436 span = null; 9437 9438 // Test for getBoundingClientRect support in Range 9439 var rangeSupportsGetClientRects = false, rangeSupportsGetBoundingClientRect = false; 9440 if (api.features.implementsDomRange) { 9441 var testRange = api.createNativeRange(); 9442 rangeSupportsGetClientRects = util.isHostMethod(testRange, "getClientRects"); 9443 rangeSupportsGetBoundingClientRect = util.isHostMethod(testRange, "getBoundingClientRect"); 9444 } 9445 9446 util.extend(api.features, { 9447 rangeSupportsGetBoundingClientRect: rangeSupportsGetBoundingClientRect, 9448 rangeSupportsGetClientRects: rangeSupportsGetClientRects, 9449 elementSupportsGetBoundingClientRect: elementSupportsGetBoundingClientRect 9450 }); 9451 9452 var createClientBoundaryPosGetter = function(isStart) { 9453 return function() { 9454 var boundaryRange = this.cloneRange(); 9455 boundaryRange.collapse(isStart); 9456 var rect = boundaryRange.getBoundingClientRect(); 9457 return { 9458 x: rect[isStart ? "left" : "right"], 9459 y: rect[isStart ? "top" : "bottom"] 9460 }; 9461 }; 9462 }; 9463 9464 var rangeProto = api.rangePrototype; 9465 9466 if (api.features.implementsTextRange && elementSupportsGetBoundingClientRect) { 9467 rangeProto.getBoundingClientRect = function() { 9468 // We need a TextRange 9469 var textRange = WrappedTextRange.rangeToTextRange(this); 9470 9471 // Work around table problems (table cell bounding rects seem not to count if TextRange spans cells) 9472 var cells = this.getNodes([1], function(el) { 9473 return /^t[dh]$/i.test(el.tagName); 9474 }); 9475 9476 // Merge rects for each cell selected by the range into overall rect 9477 var rect, rects = []; 9478 if (cells.length > 0) { 9479 var lastTable = getAncestorElement(this.startContainer, "table"); 9480 9481 for (var i = 0, cell, tempTextRange, table, subRange, subRect; cell = cells[i]; ++i) { 9482 // Handle non-table sections of the range 9483 table = getAncestorElement(cell, "table"); 9484 if (!lastTable || table != lastTable) { 9485 // There is a section of the range prior to the current table, or lying between tables. 9486 // Merge in its rect 9487 subRange = this.cloneRange(); 9488 if (lastTable) { 9489 subRange.setStartAfter(lastTable); 9490 } 9491 subRange.setEndBefore(table); 9492 rects.push(WrappedTextRange.rangeToTextRange(subRange).getBoundingClientRect()); 9493 } 9494 9495 if (this.containsNode(cell)) { 9496 rects.push(cell.getBoundingClientRect()); 9497 } else { 9498 tempTextRange = textRange.duplicate(); 9499 tempTextRange.moveToElementText(cell); 9500 if (tempTextRange.compareEndPoints("StartToStart", textRange) == -1) { 9501 tempTextRange.setEndPoint("StartToStart", textRange); 9502 } else if (tempTextRange.compareEndPoints("EndToEnd", textRange) == 1) { 9503 tempTextRange.setEndPoint("EndToEnd", textRange); 9504 } 9505 rects.push(tempTextRange.getBoundingClientRect()); 9506 } 9507 lastTable = table; 9508 } 9509 9510 // Merge in the rect for any content lying after the final table 9511 var endTable = getAncestorElement(this.endContainer, "table"); 9512 if (!endTable && lastTable) { 9513 subRange = this.cloneRange(); 9514 subRange.setStartAfter(lastTable); 9515 rects.push(WrappedTextRange.rangeToTextRange(subRange).getBoundingClientRect()); 9516 } 9517 rect = mergeRects(rects); 9518 } else { 9519 rect = textRange.getBoundingClientRect(); 9520 } 9521 9522 return adjustClientRect(rect, dom.getDocument(this.startContainer)); 9523 }; 9524 } else if (api.features.implementsDomRange) { 9525 var createWrappedRange = function(range) { 9526 return (range instanceof WrappedRange) ? range : new WrappedRange(range); 9527 }; 9528 9529 if (rangeSupportsGetBoundingClientRect) { 9530 rangeProto.getBoundingClientRect = function() { 9531 var nativeRange = createWrappedRange(this).nativeRange; 9532 // Test for WebKit getBoundingClientRect bug (https://bugs.webkit.org/show_bug.cgi?id=65324) 9533 var rect = nativeRange.getBoundingClientRect() || nativeRange.getClientRects()[0]; 9534 return adjustClientRect(rect, dom.getDocument(this.startContainer)); 9535 }; 9536 9537 if (rangeSupportsGetClientRects) { 9538 var getElementRectsForPosition = function(node, offset) { 9539 var children = node.childNodes; 9540 //if (offset < children.length) 9541 }; 9542 9543 createClientBoundaryPosGetter = function(isStart) { 9544 return function() { 9545 var rect, nativeRange = createWrappedRange(this).nativeRange; 9546 var rects = nativeRange.getClientRects(); 9547 9548 if (rects.length == 0 && elementSupportsGetBoundingClientRect) { 9549 if (isStart) { 9550 9551 9552 } 9553 9554 console.log(nativeRange, nativeRange.getClientRects(), nativeRange.getBoundingClientRect()); 9555 if (this.collapsed 9556 && this.startContainer.nodeType == 1 9557 && this.startOffset < this.startContainer.childNodes.length) { 9558 var n = this.startContainer.childNodes[this.startOffset]; 9559 if (n.getClientRects) { 9560 console.log(n, n.getClientRects(), this.startContainer.getClientRects()) 9561 } 9562 9563 } 9564 } 9565 9566 if (rects.length > 0) { 9567 if (isStart) { 9568 rect = rects[0]; 9569 return { x: rect.left, y: rect.top }; 9570 } else { 9571 rect = rects[rects.length - 1]; 9572 return { x: rect.right, y: rect.bottom }; 9573 } 9574 } else { 9575 throw module.createError("Cannot get position for range " + this.inspect()); 9576 } 9577 }; 9578 } 9579 } 9580 } else { 9581 var getElementBoundingClientRect = elementSupportsGetBoundingClientRect ? 9582 function(el) { 9583 return adjustClientRect(el.getBoundingClientRect(), dom.getDocument(el)); 9584 } : 9585 9586 // This implementation is very naive. There are many browser quirks that make it extremely 9587 // difficult to get accurate element coordinates in all situations 9588 function(el) { 9589 var x = 0, y = 0, offsetEl = el, width = el.offsetWidth, height = el.offsetHeight; 9590 while (offsetEl) { 9591 x += offsetEl.offsetLeft; 9592 y += offsetEl.offsetTop; 9593 offsetEl = offsetEl.offsetParent; 9594 } 9595 9596 return adjustClientRect(new Rect(y, x + width, y + height, x), dom.getDocument(el)); 9597 }; 9598 9599 var getRectFromBoundaries = function(range) { 9600 var rect; 9601 range.splitBoundaries(); 9602 var span = document.createElement("span"); 9603 9604 if (range.collapsed) {
9605 range.insertNode(span); 9606 rect = getElementBoundingClientRect(span); 9607 span.parentNode.removeChild(span); 9608 } else { 9609 // TODO: This isn't right. I'm not sure it can be made right sensibly. Consider what to do. 9610 // This doesn't consider all the line boxes it needs to consider. 9611 var workingRange = range.cloneRange(); 9612 9613 // Get the start rectangle 9614 workingRange.collapse(true); 9615 workingRange.insertNode(span); 9616 var startRect = getElementBoundingClientRect(span); 9617 span.parentNode.removeChild(span); 9618 9619 // Get the end rectangle 9620 workingRange.collapseToPoint(range.endContainer, range.endOffset); 9621 workingRange.insertNode(span); 9622 var endRect = getElementBoundingClientRect(span); 9623 span.parentNode.removeChild(span); 9624 9625 // Merge the start and end rects 9626 var rects = [startRect, endRect]; 9627 9628 // Merge in rectangles for all elements in the range 9629 var elements = range.getNodes([1], function(el) { 9630 return range.containsNode(el); 9631 }); 9632 9633 for (var i = 0, len = elements.length; i < len; ++i) { 9634 rects.push(getElementBoundingClientRect(elements[i])); 9635 } 9636 rect = mergeRects(rects) 9637 } 9638 9639 // Clean up 9640 range.normalizeBoundaries(); 9641 return rect; 9642 }; 9643 9644 rangeProto.getBoundingClientRect = function(range) { 9645 return getRectFromBoundaries(createWrappedRange(range)); 9646 }; 9647 } 9648 9649 function createDocumentBoundaryPosGetter(isStart) { 9650 return function() { 9651 var pos = this["get" + (isStart ? "Start" : "End") + "ClientPos"](); 9652 var scrollPos = getScrollPosition( dom.getWindow(this.startContainer) ); 9653 return { x: pos.x + scrollPos.x, y: pos.y + scrollPos.y }; 9654 }; 9655 } 9656 } 9657 9658 util.extend(rangeProto, { 9659 getBoundingDocumentRect: function() { 9660 var scrollPos = getScrollPosition( dom.getWindow(this.startContainer) ); 9661 return createRelativeRect(this.getBoundingClientRect(), scrollPos.x, scrollPos.y); 9662 }, 9663 9664 getStartClientPos: createClientBoundaryPosGetter(true), 9665 getEndClientPos: createClientBoundaryPosGetter(false), 9666 9667 getStartDocumentPos: createDocumentBoundaryPosGetter(true), 9668 getEndDocumentPos: createDocumentBoundaryPosGetter(false) 9669 }); 9670 9671 // Add Selection methods 9672 function compareRanges(r1, r2) { 9673 return r1.compareBoundaryPoints(r2.START_TO_START, r2); 9674 } 9675 9676 function createSelectionRectGetter(isDocument) { 9677 return function() { 9678 var rangeMethodName = "getBounding" + (isDocument ? "Document" : "Client") + "Rect"; 9679 var rects = []; 9680 for (var i = 0, rect = null, rangeRect; i < this.rangeCount; ++i) { 9681 rects.push(this.getRangeAt(i)[rangeMethodName]()); 9682 } 9683 return mergeRects(rects); 9684 }; 9685 } 9686 9687 function createSelectionBoundaryPosGetter(isStart, isDocument) { 9688 return function() { 9689 if (this.rangeCount == 0) { 9690 return null; 9691 } 9692 9693 var posType = isDocument ? "Document" : "Client"; 9694 9695 var ranges = this.getAllRanges(); 9696 if (ranges.length > 1) { 9697 // Order the ranges by position within the DOM 9698 ranges.sort(compareRanges); 9699 } 9700 9701 return isStart ? 9702 ranges[0]["getStart" + posType + "Pos"]() : 9703 ranges[ranges.length - 1]["getEnd" + posType + "Pos"](); 9704 }; 9705 } 9706 9707 util.extend(api.selectionPrototype, { 9708 getBoundingClientRect: createSelectionRectGetter(false), 9709 getBoundingDocumentRect: createSelectionRectGetter(true), 9710 9711 getStartClientPos: createSelectionBoundaryPosGetter(true, false), 9712 getEndClientPos: createSelectionBoundaryPosGetter(false, false), 9713 9714 getStartDocumentPos: createSelectionBoundaryPosGetter(true, true), 9715 getEndDocumentPos: createSelectionBoundaryPosGetter(false, true) 9716 }); 9717 9718 api.positionFromPoint = function(x, y, doc) { 9719 doc = dom.getContentDocument(doc, module, "positionFromPoint"); 9720 return createCaretPositionFromPointGetter(doc)(doc, x, y); 9721 }; 9722 9723 api.createRangeFromPoints = createRangeFromPoints; 9724 api.moveSelectionToPoints = moveSelectionToPoints; 9725});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.