[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/editor/atto/yui/src/rangy/js/ -> rangy-core.js (source)

   1  /**

   2   * Rangy, a cross-browser JavaScript range and selection library

   3   * https://github.com/timdown/rangy

   4   *

   5   * Copyright 2015, Tim Down

   6   * Licensed under the MIT license.

   7   * Version: 1.3.0

   8   * Build date: 10 May 2015

   9   */
  10  
  11  (function(factory, root) {
  12      // No AMD or CommonJS support so we place Rangy in (probably) the global variable

  13      root.rangy = factory();
  14  })(function() {
  15  
  16      var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
  17  
  18      // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START

  19      // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.

  20      var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
  21          "commonAncestorContainer"];
  22  
  23      // Minimal set of methods required for DOM Level 2 Range compliance

  24      var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
  25          "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
  26          "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
  27  
  28      var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
  29  
  30      // Subset of TextRange's full set of methods that we're interested in

  31      var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
  32          "setEndPoint", "getBoundingClientRect"];
  33  
  34      /*----------------------------------------------------------------------------------------------------------------*/

  35  
  36      // Trio of functions taken from Peter Michaux's article:

  37      // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting

  38      function isHostMethod(o, p) {
  39          var t = typeof o[p];
  40          return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
  41      }
  42  
  43      function isHostObject(o, p) {
  44          return !!(typeof o[p] == OBJECT && o[p]);
  45      }
  46  
  47      function isHostProperty(o, p) {
  48          return typeof o[p] != UNDEFINED;
  49      }
  50  
  51      // Creates a convenience function to save verbose repeated calls to tests functions

  52      function createMultiplePropertyTest(testFunc) {
  53          return function(o, props) {
  54              var i = props.length;
  55              while (i--) {
  56                  if (!testFunc(o, props[i])) {
  57                      return false;
  58                  }
  59              }
  60              return true;
  61          };
  62      }
  63  
  64      // Next trio of functions are a convenience to save verbose repeated calls to previous two functions

  65      var areHostMethods = createMultiplePropertyTest(isHostMethod);
  66      var areHostObjects = createMultiplePropertyTest(isHostObject);
  67      var areHostProperties = createMultiplePropertyTest(isHostProperty);
  68  
  69      function isTextRange(range) {
  70          return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
  71      }
  72  
  73      function getBody(doc) {
  74          return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
  75      }
  76  
  77      var forEach = [].forEach ?
  78          function(arr, func) {
  79              arr.forEach(func);
  80          } :
  81          function(arr, func) {
  82              for (var i = 0, len = arr.length; i < len; ++i) {
  83                  func(arr[i], i);
  84              }
  85          };
  86  
  87      var modules = {};
  88  
  89      var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);
  90  
  91      var util = {
  92          isHostMethod: isHostMethod,
  93          isHostObject: isHostObject,
  94          isHostProperty: isHostProperty,
  95          areHostMethods: areHostMethods,
  96          areHostObjects: areHostObjects,
  97          areHostProperties: areHostProperties,
  98          isTextRange: isTextRange,
  99          getBody: getBody,
 100          forEach: forEach
 101      };
 102  
 103      var api = {
 104          version: "1.3.0",
 105          initialized: false,
 106          isBrowser: isBrowser,
 107          supported: true,
 108          util: util,
 109          features: {},
 110          modules: modules,
 111          config: {
 112              alertOnFail: false,
 113              alertOnWarn: false,
 114              preferTextRange: false,
 115              autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
 116          }
 117      };
 118  
 119      function consoleLog(msg) {
 120          if (typeof console != UNDEFINED && isHostMethod(console, "log")) {
 121              console.log(msg);
 122          }
 123      }
 124  
 125      function alertOrLog(msg, shouldAlert) {
 126          if (isBrowser && shouldAlert) {
 127              alert(msg);
 128          } else  {
 129              consoleLog(msg);
 130          }
 131      }
 132  
 133      function fail(reason) {
 134          api.initialized = true;
 135          api.supported = false;
 136          alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);
 137      }
 138  
 139      api.fail = fail;
 140  
 141      function warn(msg) {
 142          alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
 143      }
 144  
 145      api.warn = warn;
 146  
 147      // Add utility extend() method

 148      var extend;
 149      if ({}.hasOwnProperty) {
 150          util.extend = extend = function(obj, props, deep) {
 151              var o, p;
 152              for (var i in props) {
 153                  if (props.hasOwnProperty(i)) {
 154                      o = obj[i];
 155                      p = props[i];
 156                      if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
 157                          extend(o, p, true);
 158                      }
 159                      obj[i] = p;
 160                  }
 161              }
 162              // Special case for toString, which does not show up in for...in loops in IE <= 8

 163              if (props.hasOwnProperty("toString")) {
 164                  obj.toString = props.toString;
 165              }
 166              return obj;
 167          };
 168  
 169          util.createOptions = function(optionsParam, defaults) {
 170              var options = {};
 171              extend(options, defaults);
 172              if (optionsParam) {
 173                  extend(options, optionsParam);
 174              }
 175              return options;
 176          };
 177      } else {
 178          fail("hasOwnProperty not supported");
 179      }
 180  
 181      // Test whether we're in a browser and bail out if not

 182      if (!isBrowser) {
 183          fail("Rangy can only run in a browser");
 184      }
 185  
 186      // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not

 187      (function() {
 188          var toArray;
 189  
 190          if (isBrowser) {
 191              var el = document.createElement("div");
 192              el.appendChild(document.createElement("span"));
 193              var slice = [].slice;
 194              try {
 195                  if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
 196                      toArray = function(arrayLike) {
 197                          return slice.call(arrayLike, 0);
 198                      };
 199                  }
 200              } catch (e) {}
 201          }
 202  
 203          if (!toArray) {
 204              toArray = function(arrayLike) {
 205                  var arr = [];
 206                  for (var i = 0, len = arrayLike.length; i < len; ++i) {
 207                      arr[i] = arrayLike[i];
 208                  }
 209                  return arr;
 210              };
 211          }
 212  
 213          util.toArray = toArray;
 214      })();
 215  
 216      // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or

 217      // normalization of event properties

 218      var addListener;
 219      if (isBrowser) {
 220          if (isHostMethod(document, "addEventListener")) {
 221              addListener = function(obj, eventType, listener) {
 222                  obj.addEventListener(eventType, listener, false);
 223              };
 224          } else if (isHostMethod(document, "attachEvent")) {
 225              addListener = function(obj, eventType, listener) {
 226                  obj.attachEvent("on" + eventType, listener);
 227              };
 228          } else {
 229              fail("Document does not have required addEventListener or attachEvent method");
 230          }
 231  
 232          util.addListener = addListener;
 233      }
 234  
 235      var initListeners = [];
 236  
 237      function getErrorDesc(ex) {
 238          return ex.message || ex.description || String(ex);
 239      }
 240  
 241      // Initialization

 242      function init() {
 243          if (!isBrowser || api.initialized) {
 244              return;
 245          }
 246          var testRange;
 247          var implementsDomRange = false, implementsTextRange = false;
 248  
 249          // First, perform basic feature tests

 250  
 251          if (isHostMethod(document, "createRange")) {
 252              testRange = document.createRange();
 253              if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
 254                  implementsDomRange = true;
 255              }
 256          }
 257  
 258          var body = getBody(document);
 259          if (!body || body.nodeName.toLowerCase() != "body") {
 260              fail("No body element found");
 261              return;
 262          }
 263  
 264          if (body && isHostMethod(body, "createTextRange")) {
 265              testRange = body.createTextRange();
 266              if (isTextRange(testRange)) {
 267                  implementsTextRange = true;
 268              }
 269          }
 270  
 271          if (!implementsDomRange && !implementsTextRange) {
 272              fail("Neither Range nor TextRange are available");
 273              return;
 274          }
 275  
 276          api.initialized = true;
 277          api.features = {
 278              implementsDomRange: implementsDomRange,
 279              implementsTextRange: implementsTextRange
 280          };
 281  
 282          // Initialize modules

 283          var module, errorMessage;
 284          for (var moduleName in modules) {
 285              if ( (module = modules[moduleName]) instanceof Module ) {
 286                  module.init(module, api);
 287              }
 288          }
 289  
 290          // Call init listeners

 291          for (var i = 0, len = initListeners.length; i < len; ++i) {
 292              try {
 293                  initListeners[i](api);
 294              } catch (ex) {
 295                  errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
 296                  consoleLog(errorMessage);
 297              }
 298          }
 299      }
 300  
 301      function deprecationNotice(deprecated, replacement, module) {
 302          if (module) {
 303              deprecated += " in module " + module.name;
 304          }
 305          api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +
 306          replacement + " instead.");
 307      }
 308  
 309      function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {
 310          owner[deprecated] = function() {
 311              deprecationNotice(deprecated, replacement, module);
 312              return owner[replacement].apply(owner, util.toArray(arguments));
 313          };
 314      }
 315  
 316      util.deprecationNotice = deprecationNotice;
 317      util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;
 318  
 319      // Allow external scripts to initialize this library in case it's loaded after the document has loaded

 320      api.init = init;
 321  
 322      // Execute listener immediately if already initialized

 323      api.addInitListener = function(listener) {
 324          if (api.initialized) {
 325              listener(api);
 326          } else {
 327              initListeners.push(listener);
 328          }
 329      };
 330  
 331      var shimListeners = [];
 332  
 333      api.addShimListener = function(listener) {
 334          shimListeners.push(listener);
 335      };
 336  
 337      function shim(win) {
 338          win = win || window;
 339          init();
 340  
 341          // Notify listeners

 342          for (var i = 0, len = shimListeners.length; i < len; ++i) {
 343              shimListeners[i](win);
 344          }
 345      }
 346  
 347      if (isBrowser) {
 348          api.shim = api.createMissingNativeApi = shim;
 349          createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");
 350      }
 351  
 352      function Module(name, dependencies, initializer) {
 353          this.name = name;
 354          this.dependencies = dependencies;
 355          this.initialized = false;
 356          this.supported = false;
 357          this.initializer = initializer;
 358      }
 359  
 360      Module.prototype = {
 361          init: function() {
 362              var requiredModuleNames = this.dependencies || [];
 363              for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
 364                  moduleName = requiredModuleNames[i];
 365  
 366                  requiredModule = modules[moduleName];
 367                  if (!requiredModule || !(requiredModule instanceof Module)) {
 368                      throw new Error("required module '" + moduleName + "' not found");
 369                  }
 370  
 371                  requiredModule.init();
 372  
 373                  if (!requiredModule.supported) {
 374                      throw new Error("required module '" + moduleName + "' not supported");
 375                  }
 376              }
 377  
 378              // Now run initializer

 379              this.initializer(this);
 380          },
 381  
 382          fail: function(reason) {
 383              this.initialized = true;
 384              this.supported = false;
 385              throw new Error(reason);
 386          },
 387  
 388          warn: function(msg) {
 389              api.warn("Module " + this.name + ": " + msg);
 390          },
 391  
 392          deprecationNotice: function(deprecated, replacement) {
 393              api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +
 394                  replacement + " instead");
 395          },
 396  
 397          createError: function(msg) {
 398              return new Error("Error in Rangy " + this.name + " module: " + msg);
 399          }
 400      };
 401  
 402      function createModule(name, dependencies, initFunc) {
 403          var newModule = new Module(name, dependencies, function(module) {
 404              if (!module.initialized) {
 405                  module.initialized = true;
 406                  try {
 407                      initFunc(api, module);
 408                      module.supported = true;
 409                  } catch (ex) {
 410                      var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
 411                      consoleLog(errorMessage);
 412                      if (ex.stack) {
 413                          consoleLog(ex.stack);
 414                      }
 415                  }
 416              }
 417          });
 418          modules[name] = newModule;
 419          return newModule;
 420      }
 421  
 422      api.createModule = function(name) {
 423          // Allow 2 or 3 arguments (second argument is an optional array of dependencies)

 424          var initFunc, dependencies;
 425          if (arguments.length == 2) {
 426              initFunc = arguments[1];
 427              dependencies = [];
 428          } else {
 429              initFunc = arguments[2];
 430              dependencies = arguments[1];
 431          }
 432  
 433          var module = createModule(name, dependencies, initFunc);
 434  
 435          // Initialize the module immediately if the core is already initialized

 436          if (api.initialized && api.supported) {
 437              module.init();
 438          }
 439      };
 440  
 441      api.createCoreModule = function(name, dependencies, initFunc) {
 442          createModule(name, dependencies, initFunc);
 443      };
 444  
 445      /*----------------------------------------------------------------------------------------------------------------*/

 446  
 447      // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately

 448  
 449      function RangePrototype() {}
 450      api.RangePrototype = RangePrototype;
 451      api.rangePrototype = new RangePrototype();
 452  
 453      function SelectionPrototype() {}
 454      api.selectionPrototype = new SelectionPrototype();
 455  
 456      /*----------------------------------------------------------------------------------------------------------------*/

 457  
 458      // DOM utility methods used by Rangy
 459      api.createCoreModule("DomUtil", [], function(api, module) {
 460          var UNDEF = "undefined";
 461          var util = api.util;
 462          var getBody = util.getBody;
 463  
 464          // Perform feature tests
 465          if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
 466              module.fail("document missing a Node creation method");
 467          }
 468  
 469          if (!util.isHostMethod(document, "getElementsByTagName")) {
 470              module.fail("document missing getElementsByTagName method");
 471          }
 472  
 473          var el = document.createElement("div");
 474          if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
 475                  !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
 476              module.fail("Incomplete Element implementation");
 477          }
 478  
 479          // innerHTML is required for Range's createContextualFragment method
 480          if (!util.isHostProperty(el, "innerHTML")) {
 481              module.fail("Element is missing innerHTML property");
 482          }
 483  
 484          var textNode = document.createTextNode("test");
 485          if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
 486                  !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
 487                  !util.areHostProperties(textNode, ["data"]))) {
 488              module.fail("Incomplete Text Node implementation");
 489          }
 490  
 491          /*----------------------------------------------------------------------------------------------------------------*/
 492  
 493          // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
 494          // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
 495          // contains just the document as a single element and the value searched for is the document.
 496          var arrayContains = /*Array.prototype.indexOf ?
 497              function(arr, val) {
 498                  return arr.indexOf(val) > -1;
 499              }:*/
 500  
 501              function(arr, val) {
 502                  var i = arr.length;
 503                  while (i--) {
 504                      if (arr[i] === val) {
 505                          return true;
 506                      }
 507                  }
 508                  return false;
 509              };
 510  
 511          // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
 512          function isHtmlNamespace(node) {
 513              var ns;
 514              return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
 515          }
 516  
 517          function parentElement(node) {
 518              var parent = node.parentNode;
 519              return (parent.nodeType == 1) ? parent : null;
 520          }
 521  
 522          function getNodeIndex(node) {
 523              var i = 0;
 524              while( (node = node.previousSibling) ) {
 525                  ++i;
 526              }
 527              return i;
 528          }
 529  
 530          function getNodeLength(node) {
 531              switch (node.nodeType) {
 532                  case 7:
 533                  case 10:
 534                      return 0;
 535                  case 3:
 536                  case 8:
 537                      return node.length;
 538                  default:
 539                      return node.childNodes.length;
 540              }
 541          }
 542  
 543          function getCommonAncestor(node1, node2) {
 544              var ancestors = [], n;
 545              for (n = node1; n; n = n.parentNode) {
 546                  ancestors.push(n);
 547              }
 548  
 549              for (n = node2; n; n = n.parentNode) {
 550                  if (arrayContains(ancestors, n)) {
 551                      return n;
 552                  }
 553              }
 554  
 555              return null;
 556          }
 557  
 558          function isAncestorOf(ancestor, descendant, selfIsAncestor) {
 559              var n = selfIsAncestor ? descendant : descendant.parentNode;
 560              while (n) {
 561                  if (n === ancestor) {
 562                      return true;
 563                  } else {
 564                      n = n.parentNode;
 565                  }
 566              }
 567              return false;
 568          }
 569  
 570          function isOrIsAncestorOf(ancestor, descendant) {
 571              return isAncestorOf(ancestor, descendant, true);
 572          }
 573  
 574          function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
 575              var p, n = selfIsAncestor ? node : node.parentNode;
 576              while (n) {
 577                  p = n.parentNode;
 578                  if (p === ancestor) {
 579                      return n;
 580                  }
 581                  n = p;
 582              }
 583              return null;
 584          }
 585  
 586          function isCharacterDataNode(node) {
 587              var t = node.nodeType;
 588              return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
 589          }
 590  
 591          function isTextOrCommentNode(node) {
 592              if (!node) {
 593                  return false;
 594              }
 595              var t = node.nodeType;
 596              return t == 3 || t == 8 ; // Text or Comment
 597          }
 598  
 599          function insertAfter(node, precedingNode) {
 600              var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
 601              if (nextNode) {
 602                  parent.insertBefore(node, nextNode);
 603              } else {
 604                  parent.appendChild(node);
 605              }
 606              return node;
 607          }
 608  
 609          // Note that we cannot use splitText() because it is bugridden in IE 9.
 610          function splitDataNode(node, index, positionsToPreserve) {
 611              var newNode = node.cloneNode(false);
 612              newNode.deleteData(0, index);
 613              node.deleteData(index, node.length - index);
 614              insertAfter(newNode, node);
 615  
 616              // Preserve positions
 617              if (positionsToPreserve) {
 618                  for (var i = 0, position; position = positionsToPreserve[i++]; ) {
 619                      // Handle case where position was inside the portion of node after the split point
 620                      if (position.node == node && position.offset > index) {
 621                          position.node = newNode;
 622                          position.offset -= index;
 623                      }
 624                      // Handle the case where the position is a node offset within node's parent
 625                      else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
 626                          ++position.offset;
 627                      }
 628                  }
 629              }
 630              return newNode;
 631          }
 632  
 633          function getDocument(node) {
 634              if (node.nodeType == 9) {
 635                  return node;
 636              } else if (typeof node.ownerDocument != UNDEF) {
 637                  return node.ownerDocument;
 638              } else if (typeof node.document != UNDEF) {
 639                  return node.document;
 640              } else if (node.parentNode) {
 641                  return getDocument(node.parentNode);
 642              } else {
 643                  throw module.createError("getDocument: no document found for node");
 644              }
 645          }
 646  
 647          function getWindow(node) {
 648              var doc = getDocument(node);
 649              if (typeof doc.defaultView != UNDEF) {
 650                  return doc.defaultView;
 651              } else if (typeof doc.parentWindow != UNDEF) {
 652                  return doc.parentWindow;
 653              } else {
 654                  throw module.createError("Cannot get a window object for node");
 655              }
 656          }
 657  
 658          function getIframeDocument(iframeEl) {
 659              if (typeof iframeEl.contentDocument != UNDEF) {
 660                  return iframeEl.contentDocument;
 661              } else if (typeof iframeEl.contentWindow != UNDEF) {
 662                  return iframeEl.contentWindow.document;
 663              } else {
 664                  throw module.createError("getIframeDocument: No Document object found for iframe element");
 665              }
 666          }
 667  
 668          function getIframeWindow(iframeEl) {
 669              if (typeof iframeEl.contentWindow != UNDEF) {
 670                  return iframeEl.contentWindow;
 671              } else if (typeof iframeEl.contentDocument != UNDEF) {
 672                  return iframeEl.contentDocument.defaultView;
 673              } else {
 674                  throw module.createError("getIframeWindow: No Window object found for iframe element");
 675              }
 676          }
 677  
 678          // This looks bad. Is it worth it?
 679          function isWindow(obj) {
 680              return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
 681          }
 682  
 683          function getContentDocument(obj, module, methodName) {
 684              var doc;
 685  
 686              if (!obj) {
 687                  doc = document;
 688              }
 689  
 690              // Test if a DOM node has been passed and obtain a document object for it if so
 691              else if (util.isHostProperty(obj, "nodeType")) {
 692                  doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
 693                      getIframeDocument(obj) : getDocument(obj);
 694              }
 695  
 696              // Test if the doc parameter appears to be a Window object
 697              else if (isWindow(obj)) {
 698                  doc = obj.document;
 699              }
 700  
 701              if (!doc) {
 702                  throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
 703              }
 704  
 705              return doc;
 706          }
 707  
 708          function getRootContainer(node) {
 709              var parent;
 710              while ( (parent = node.parentNode) ) {
 711                  node = parent;
 712              }
 713              return node;
 714          }
 715  
 716          function comparePoints(nodeA, offsetA, nodeB, offsetB) {
 717              // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
 718              var nodeC, root, childA, childB, n;
 719              if (nodeA == nodeB) {
 720                  // Case 1: nodes are the same
 721                  return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
 722              } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
 723                  // Case 2: node C (container B or an ancestor) is a child node of A
 724                  return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
 725              } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
 726                  // Case 3: node C (container A or an ancestor) is a child node of B
 727                  return getNodeIndex(nodeC) < offsetB  ? -1 : 1;
 728              } else {
 729                  root = getCommonAncestor(nodeA, nodeB);
 730                  if (!root) {
 731                      throw new Error("comparePoints error: nodes have no common ancestor");
 732                  }
 733  
 734                  // Case 4: containers are siblings or descendants of siblings
 735                  childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
 736                  childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
 737  
 738                  if (childA === childB) {
 739                      // This shouldn't be possible
 740                      throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
 741                  } else {
 742                      n = root.firstChild;
 743                      while (n) {
 744                          if (n === childA) {
 745                              return -1;
 746                          } else if (n === childB) {
 747                              return 1;
 748                          }
 749                          n = n.nextSibling;
 750                      }
 751                  }
 752              }
 753          }
 754  
 755          /*----------------------------------------------------------------------------------------------------------------*/
 756  
 757          // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
 758          var crashyTextNodes = false;
 759  
 760          function isBrokenNode(node) {
 761              var n;
 762              try {
 763                  n = node.parentNode;
 764                  return false;
 765              } catch (e) {
 766                  return true;
 767              }
 768          }
 769  
 770          (function() {
 771              var el = document.createElement("b");
 772              el.innerHTML = "1";
 773              var textNode = el.firstChild;
 774              el.innerHTML = "<br />";
 775              crashyTextNodes = isBrokenNode(textNode);
 776  
 777              api.features.crashyTextNodes = crashyTextNodes;
 778          })();
 779  
 780          /*----------------------------------------------------------------------------------------------------------------*/
 781  
 782          function inspectNode(node) {
 783              if (!node) {
 784                  return "[No node]";
 785              }
 786              if (crashyTextNodes && isBrokenNode(node)) {
 787                  return "[Broken node]";
 788              }
 789              if (isCharacterDataNode(node)) {
 790                  return '"' + node.data + '"';
 791              }
 792              if (node.nodeType == 1) {
 793                  var idAttr = node.id ? ' id="' + node.id + '"' : "";
 794                  return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
 795              }
 796              return node.nodeName;
 797          }
 798  
 799          function fragmentFromNodeChildren(node) {
 800              var fragment = getDocument(node).createDocumentFragment(), child;
 801              while ( (child = node.firstChild) ) {
 802                  fragment.appendChild(child);
 803              }
 804              return fragment;
 805          }
 806  
 807          var getComputedStyleProperty;
 808          if (typeof window.getComputedStyle != UNDEF) {
 809              getComputedStyleProperty = function(el, propName) {
 810                  return getWindow(el).getComputedStyle(el, null)[propName];
 811              };
 812          } else if (typeof document.documentElement.currentStyle != UNDEF) {
 813              getComputedStyleProperty = function(el, propName) {
 814                  return el.currentStyle ? el.currentStyle[propName] : "";
 815              };
 816          } else {
 817              module.fail("No means of obtaining computed style properties found");
 818          }
 819  
 820          function createTestElement(doc, html, contentEditable) {
 821              var body = getBody(doc);
 822              var el = doc.createElement("div");
 823              el.contentEditable = "" + !!contentEditable;
 824              if (html) {
 825                  el.innerHTML = html;
 826              }
 827  
 828              // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
 829              var bodyFirstChild = body.firstChild;
 830              if (bodyFirstChild) {
 831                  body.insertBefore(el, bodyFirstChild);
 832              } else {
 833                  body.appendChild(el);
 834              }
 835  
 836              return el;
 837          }
 838  
 839          function removeNode(node) {
 840              return node.parentNode.removeChild(node);
 841          }
 842  
 843          function NodeIterator(root) {
 844              this.root = root;
 845              this._next = root;
 846          }
 847  
 848          NodeIterator.prototype = {
 849              _current: null,
 850  
 851              hasNext: function() {
 852                  return !!this._next;
 853              },
 854  
 855              next: function() {
 856                  var n = this._current = this._next;
 857                  var child, next;
 858                  if (this._current) {
 859                      child = n.firstChild;
 860                      if (child) {
 861                          this._next = child;
 862                      } else {
 863                          next = null;
 864                          while ((n !== this.root) && !(next = n.nextSibling)) {
 865                              n = n.parentNode;
 866                          }
 867                          this._next = next;
 868                      }
 869                  }
 870                  return this._current;
 871              },
 872  
 873              detach: function() {
 874                  this._current = this._next = this.root = null;
 875              }
 876          };
 877  
 878          function createIterator(root) {
 879              return new NodeIterator(root);
 880          }
 881  
 882          function DomPosition(node, offset) {
 883              this.node = node;
 884              this.offset = offset;
 885          }
 886  
 887          DomPosition.prototype = {
 888              equals: function(pos) {
 889                  return !!pos && this.node === pos.node && this.offset == pos.offset;
 890              },
 891  
 892              inspect: function() {
 893                  return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
 894              },
 895  
 896              toString: function() {
 897                  return this.inspect();
 898              }
 899          };
 900  
 901          function DOMException(codeName) {
 902              this.code = this[codeName];
 903              this.codeName = codeName;
 904              this.message = "DOMException: " + this.codeName;
 905          }
 906  
 907          DOMException.prototype = {
 908              INDEX_SIZE_ERR: 1,
 909              HIERARCHY_REQUEST_ERR: 3,
 910              WRONG_DOCUMENT_ERR: 4,
 911              NO_MODIFICATION_ALLOWED_ERR: 7,
 912              NOT_FOUND_ERR: 8,
 913              NOT_SUPPORTED_ERR: 9,
 914              INVALID_STATE_ERR: 11,
 915              INVALID_NODE_TYPE_ERR: 24
 916          };
 917  
 918          DOMException.prototype.toString = function() {
 919              return this.message;
 920          };
 921  
 922          api.dom = {
 923              arrayContains: arrayContains,
 924              isHtmlNamespace: isHtmlNamespace,
 925              parentElement: parentElement,
 926              getNodeIndex: getNodeIndex,
 927              getNodeLength: getNodeLength,
 928              getCommonAncestor: getCommonAncestor,
 929              isAncestorOf: isAncestorOf,
 930              isOrIsAncestorOf: isOrIsAncestorOf,
 931              getClosestAncestorIn: getClosestAncestorIn,
 932              isCharacterDataNode: isCharacterDataNode,
 933              isTextOrCommentNode: isTextOrCommentNode,
 934              insertAfter: insertAfter,
 935              splitDataNode: splitDataNode,
 936              getDocument: getDocument,
 937              getWindow: getWindow,
 938              getIframeWindow: getIframeWindow,
 939              getIframeDocument: getIframeDocument,
 940              getBody: getBody,
 941              isWindow: isWindow,
 942              getContentDocument: getContentDocument,
 943              getRootContainer: getRootContainer,
 944              comparePoints: comparePoints,
 945              isBrokenNode: isBrokenNode,
 946              inspectNode: inspectNode,
 947              getComputedStyleProperty: getComputedStyleProperty,
 948              createTestElement: createTestElement,
 949              removeNode: removeNode,
 950              fragmentFromNodeChildren: fragmentFromNodeChildren,
 951              createIterator: createIterator,
 952              DomPosition: DomPosition
 953          };
 954  
 955          api.DOMException = DOMException;
 956      });
 957  
 958      /*----------------------------------------------------------------------------------------------------------------*/

 959  
 960      // Pure JavaScript implementation of DOM Range
 961      api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
 962          var dom = api.dom;
 963          var util = api.util;
 964          var DomPosition = dom.DomPosition;
 965          var DOMException = api.DOMException;
 966  
 967          var isCharacterDataNode = dom.isCharacterDataNode;
 968          var getNodeIndex = dom.getNodeIndex;
 969          var isOrIsAncestorOf = dom.isOrIsAncestorOf;
 970          var getDocument = dom.getDocument;
 971          var comparePoints = dom.comparePoints;
 972          var splitDataNode = dom.splitDataNode;
 973          var getClosestAncestorIn = dom.getClosestAncestorIn;
 974          var getNodeLength = dom.getNodeLength;
 975          var arrayContains = dom.arrayContains;
 976          var getRootContainer = dom.getRootContainer;
 977          var crashyTextNodes = api.features.crashyTextNodes;
 978  
 979          var removeNode = dom.removeNode;
 980  
 981          /*----------------------------------------------------------------------------------------------------------------*/
 982  
 983          // Utility functions
 984  
 985          function isNonTextPartiallySelected(node, range) {
 986              return (node.nodeType != 3) &&
 987                     (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
 988          }
 989  
 990          function getRangeDocument(range) {
 991              return range.document || getDocument(range.startContainer);
 992          }
 993  
 994          function getRangeRoot(range) {
 995              return getRootContainer(range.startContainer);
 996          }
 997  
 998          function getBoundaryBeforeNode(node) {
 999              return new DomPosition(node.parentNode, getNodeIndex(node));
1000          }
1001  
1002          function getBoundaryAfterNode(node) {
1003              return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
1004          }
1005  
1006          function insertNodeAtPosition(node, n, o) {
1007              var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
1008              if (isCharacterDataNode(n)) {
1009                  if (o == n.length) {
1010                      dom.insertAfter(node, n);
1011                  } else {
1012                      n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
1013                  }
1014              } else if (o >= n.childNodes.length) {
1015                  n.appendChild(node);
1016              } else {
1017                  n.insertBefore(node, n.childNodes[o]);
1018              }
1019              return firstNodeInserted;
1020          }
1021  
1022          function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
1023              assertRangeValid(rangeA);
1024              assertRangeValid(rangeB);
1025  
1026              if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
1027                  throw new DOMException("WRONG_DOCUMENT_ERR");
1028              }
1029  
1030              var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
1031                  endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
1032  
1033              return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1034          }
1035  
1036          function cloneSubtree(iterator) {
1037              var partiallySelected;
1038              for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1039                  partiallySelected = iterator.isPartiallySelectedSubtree();
1040                  node = node.cloneNode(!partiallySelected);
1041                  if (partiallySelected) {
1042                      subIterator = iterator.getSubtreeIterator();
1043                      node.appendChild(cloneSubtree(subIterator));
1044                      subIterator.detach();
1045                  }
1046  
1047                  if (node.nodeType == 10) { // DocumentType
1048                      throw new DOMException("HIERARCHY_REQUEST_ERR");
1049                  }
1050                  frag.appendChild(node);
1051              }
1052              return frag;
1053          }
1054  
1055          function iterateSubtree(rangeIterator, func, iteratorState) {
1056              var it, n;
1057              iteratorState = iteratorState || { stop: false };
1058              for (var node, subRangeIterator; node = rangeIterator.next(); ) {
1059                  if (rangeIterator.isPartiallySelectedSubtree()) {
1060                      if (func(node) === false) {
1061                          iteratorState.stop = true;
1062                          return;
1063                      } else {
1064                          // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
1065                          // the node selected by the Range.
1066                          subRangeIterator = rangeIterator.getSubtreeIterator();
1067                          iterateSubtree(subRangeIterator, func, iteratorState);
1068                          subRangeIterator.detach();
1069                          if (iteratorState.stop) {
1070                              return;
1071                          }
1072                      }
1073                  } else {
1074                      // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
1075                      // descendants
1076                      it = dom.createIterator(node);
1077                      while ( (n = it.next()) ) {
1078                          if (func(n) === false) {
1079                              iteratorState.stop = true;
1080                              return;
1081                          }
1082                      }
1083                  }
1084              }
1085          }
1086  
1087          function deleteSubtree(iterator) {
1088              var subIterator;
1089              while (iterator.next()) {
1090                  if (iterator.isPartiallySelectedSubtree()) {
1091                      subIterator = iterator.getSubtreeIterator();
1092                      deleteSubtree(subIterator);
1093                      subIterator.detach();
1094                  } else {
1095                      iterator.remove();
1096                  }
1097              }
1098          }
1099  
1100          function extractSubtree(iterator) {
1101              for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1102  
1103                  if (iterator.isPartiallySelectedSubtree()) {
1104                      node = node.cloneNode(false);
1105                      subIterator = iterator.getSubtreeIterator();
1106                      node.appendChild(extractSubtree(subIterator));
1107                      subIterator.detach();
1108                  } else {
1109                      iterator.remove();
1110                  }
1111                  if (node.nodeType == 10) { // DocumentType
1112                      throw new DOMException("HIERARCHY_REQUEST_ERR");
1113                  }
1114                  frag.appendChild(node);
1115              }
1116              return frag;
1117          }
1118  
1119          function getNodesInRange(range, nodeTypes, filter) {
1120              var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
1121              var filterExists = !!filter;
1122              if (filterNodeTypes) {
1123                  regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
1124              }
1125  
1126              var nodes = [];
1127              iterateSubtree(new RangeIterator(range, false), function(node) {
1128                  if (filterNodeTypes && !regex.test(node.nodeType)) {
1129                      return;
1130                  }
1131                  if (filterExists && !filter(node)) {
1132                      return;
1133                  }
1134                  // Don't include a boundary container if it is a character data node and the range does not contain any
1135                  // of its character data. See issue 190.
1136                  var sc = range.startContainer;
1137                  if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
1138                      return;
1139                  }
1140  
1141                  var ec = range.endContainer;
1142                  if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
1143                      return;
1144                  }
1145  
1146                  nodes.push(node);
1147              });
1148              return nodes;
1149          }
1150  
1151          function inspect(range) {
1152              var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
1153              return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
1154                      dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
1155          }
1156  
1157          /*----------------------------------------------------------------------------------------------------------------*/
1158  
1159          // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
1160  
1161          function RangeIterator(range, clonePartiallySelectedTextNodes) {
1162              this.range = range;
1163              this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
1164  
1165  
1166              if (!range.collapsed) {
1167                  this.sc = range.startContainer;
1168                  this.so = range.startOffset;
1169                  this.ec = range.endContainer;
1170                  this.eo = range.endOffset;
1171                  var root = range.commonAncestorContainer;
1172  
1173                  if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
1174                      this.isSingleCharacterDataNode = true;
1175                      this._first = this._last = this._next = this.sc;
1176                  } else {
1177                      this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
1178                          this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
1179                      this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
1180                          this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
1181                  }
1182              }
1183          }
1184  
1185          RangeIterator.prototype = {
1186              _current: null,
1187              _next: null,
1188              _first: null,
1189              _last: null,
1190              isSingleCharacterDataNode: false,
1191  
1192              reset: function() {
1193                  this._current = null;
1194                  this._next = this._first;
1195              },
1196  
1197              hasNext: function() {
1198                  return !!this._next;
1199              },
1200  
1201              next: function() {
1202                  // Move to next node
1203                  var current = this._current = this._next;
1204                  if (current) {
1205                      this._next = (current !== this._last) ? current.nextSibling : null;
1206  
1207                      // Check for partially selected text nodes
1208                      if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
1209                          if (current === this.ec) {
1210                              (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
1211                          }
1212                          if (this._current === this.sc) {
1213                              (current = current.cloneNode(true)).deleteData(0, this.so);
1214                          }
1215                      }
1216                  }
1217  
1218                  return current;
1219              },
1220  
1221              remove: function() {
1222                  var current = this._current, start, end;
1223  
1224                  if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
1225                      start = (current === this.sc) ? this.so : 0;
1226                      end = (current === this.ec) ? this.eo : current.length;
1227                      if (start != end) {
1228                          current.deleteData(start, end - start);
1229                      }
1230                  } else {
1231                      if (current.parentNode) {
1232                          removeNode(current);
1233                      } else {
1234                      }
1235                  }
1236              },
1237  
1238              // Checks if the current node is partially selected
1239              isPartiallySelectedSubtree: function() {
1240                  var current = this._current;
1241                  return isNonTextPartiallySelected(current, this.range);
1242              },
1243  
1244              getSubtreeIterator: function() {
1245                  var subRange;
1246                  if (this.isSingleCharacterDataNode) {
1247                      subRange = this.range.cloneRange();
1248                      subRange.collapse(false);
1249                  } else {
1250                      subRange = new Range(getRangeDocument(this.range));
1251                      var current = this._current;
1252                      var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
1253  
1254                      if (isOrIsAncestorOf(current, this.sc)) {
1255                          startContainer = this.sc;
1256                          startOffset = this.so;
1257                      }
1258                      if (isOrIsAncestorOf(current, this.ec)) {
1259                          endContainer = this.ec;
1260                          endOffset = this.eo;
1261                      }
1262  
1263                      updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
1264                  }
1265                  return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
1266              },
1267  
1268              detach: function() {
1269                  this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
1270              }
1271          };
1272  
1273          /*----------------------------------------------------------------------------------------------------------------*/
1274  
1275          var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
1276          var rootContainerNodeTypes = [2, 9, 11];
1277          var readonlyNodeTypes = [5, 6, 10, 12];
1278          var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
1279          var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
1280  
1281          function createAncestorFinder(nodeTypes) {
1282              return function(node, selfIsAncestor) {
1283                  var t, n = selfIsAncestor ? node : node.parentNode;
1284                  while (n) {
1285                      t = n.nodeType;
1286                      if (arrayContains(nodeTypes, t)) {
1287                          return n;
1288                      }
1289                      n = n.parentNode;
1290                  }
1291                  return null;
1292              };
1293          }
1294  
1295          var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1296          var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1297          var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
1298  
1299          function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1300              if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1301                  throw new DOMException("INVALID_NODE_TYPE_ERR");
1302              }
1303          }
1304  
1305          function assertValidNodeType(node, invalidTypes) {
1306              if (!arrayContains(invalidTypes, node.nodeType)) {
1307                  throw new DOMException("INVALID_NODE_TYPE_ERR");
1308              }
1309          }
1310  
1311          function assertValidOffset(node, offset) {
1312              if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1313                  throw new DOMException("INDEX_SIZE_ERR");
1314              }
1315          }
1316  
1317          function assertSameDocumentOrFragment(node1, node2) {
1318              if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1319                  throw new DOMException("WRONG_DOCUMENT_ERR");
1320              }
1321          }
1322  
1323          function assertNodeNotReadOnly(node) {
1324              if (getReadonlyAncestor(node, true)) {
1325                  throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1326              }
1327          }
1328  
1329          function assertNode(node, codeName) {
1330              if (!node) {
1331                  throw new DOMException(codeName);
1332              }
1333          }
1334  
1335          function isValidOffset(node, offset) {
1336              return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
1337          }
1338  
1339          function isRangeValid(range) {
1340              return (!!range.startContainer && !!range.endContainer &&
1341                      !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
1342                      getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
1343                      isValidOffset(range.startContainer, range.startOffset) &&
1344                      isValidOffset(range.endContainer, range.endOffset));
1345          }
1346  
1347          function assertRangeValid(range) {
1348              if (!isRangeValid(range)) {
1349                  throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
1350              }
1351          }
1352  
1353          /*----------------------------------------------------------------------------------------------------------------*/
1354  
1355          // Test the browser's innerHTML support to decide how to implement createContextualFragment
1356          var styleEl = document.createElement("style");
1357          var htmlParsingConforms = false;
1358          try {
1359              styleEl.innerHTML = "<b>x</b>";
1360              htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
1361          } catch (e) {
1362              // IE 6 and 7 throw
1363          }
1364  
1365          api.features.htmlParsingConforms = htmlParsingConforms;
1366  
1367          var createContextualFragment = htmlParsingConforms ?
1368  
1369              // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
1370              // discussion and base code for this implementation at issue 67.
1371              // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
1372              // Thanks to Aleks Williams.
1373              function(fragmentStr) {
1374                  // "Let node the context object's start's node."
1375                  var node = this.startContainer;
1376                  var doc = getDocument(node);
1377  
1378                  // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1379                  // exception and abort these steps."
1380                  if (!node) {
1381                      throw new DOMException("INVALID_STATE_ERR");
1382                  }
1383  
1384                  // "Let element be as follows, depending on node's interface:"
1385                  // Document, Document Fragment: null
1386                  var el = null;
1387  
1388                  // "Element: node"
1389                  if (node.nodeType == 1) {
1390                      el = node;
1391  
1392                  // "Text, Comment: node's parentElement"
1393                  } else if (isCharacterDataNode(node)) {
1394                      el = dom.parentElement(node);
1395                  }
1396  
1397                  // "If either element is null or element's ownerDocument is an HTML document
1398                  // and element's local name is "html" and element's namespace is the HTML
1399                  // namespace"
1400                  if (el === null || (
1401                      el.nodeName == "HTML" &&
1402                      dom.isHtmlNamespace(getDocument(el).documentElement) &&
1403                      dom.isHtmlNamespace(el)
1404                  )) {
1405  
1406                  // "let element be a new Element with "body" as its local name and the HTML
1407                  // namespace as its namespace.""
1408                      el = doc.createElement("body");
1409                  } else {
1410                      el = el.cloneNode(false);
1411                  }
1412  
1413                  // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
1414                  // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
1415                  // "In either case, the algorithm must be invoked with fragment as the input
1416                  // and element as the context element."
1417                  el.innerHTML = fragmentStr;
1418  
1419                  // "If this raises an exception, then abort these steps. Otherwise, let new
1420                  // children be the nodes returned."
1421  
1422                  // "Let fragment be a new DocumentFragment."
1423                  // "Append all new children to fragment."
1424                  // "Return fragment."
1425                  return dom.fragmentFromNodeChildren(el);
1426              } :
1427  
1428              // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
1429              // previous versions of Rangy used (with the exception of using a body element rather than a div)
1430              function(fragmentStr) {
1431                  var doc = getRangeDocument(this);
1432                  var el = doc.createElement("body");
1433                  el.innerHTML = fragmentStr;
1434  
1435                  return dom.fragmentFromNodeChildren(el);
1436              };
1437  
1438          function splitRangeBoundaries(range, positionsToPreserve) {
1439              assertRangeValid(range);
1440  
1441              var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
1442              var startEndSame = (sc === ec);
1443  
1444              if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1445                  splitDataNode(ec, eo, positionsToPreserve);
1446              }
1447  
1448              if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1449                  sc = splitDataNode(sc, so, positionsToPreserve);
1450                  if (startEndSame) {
1451                      eo -= so;
1452                      ec = sc;
1453                  } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
1454                      eo++;
1455                  }
1456                  so = 0;
1457              }
1458              range.setStartAndEnd(sc, so, ec, eo);
1459          }
1460  
1461          function rangeToHtml(range) {
1462              assertRangeValid(range);
1463              var container = range.commonAncestorContainer.parentNode.cloneNode(false);
1464              container.appendChild( range.cloneContents() );
1465              return container.innerHTML;
1466          }
1467  
1468          /*----------------------------------------------------------------------------------------------------------------*/
1469  
1470          var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1471              "commonAncestorContainer"];
1472  
1473          var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
1474          var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
1475  
1476          util.extend(api.rangePrototype, {
1477              compareBoundaryPoints: function(how, range) {
1478                  assertRangeValid(this);
1479                  assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1480  
1481                  var nodeA, offsetA, nodeB, offsetB;
1482                  var prefixA = (how == e2s || how == s2s) ? "start" : "end";
1483                  var prefixB = (how == s2e || how == s2s) ? "start" : "end";
1484                  nodeA = this[prefixA + "Container"];
1485                  offsetA = this[prefixA + "Offset"];
1486                  nodeB = range[prefixB + "Container"];
1487                  offsetB = range[prefixB + "Offset"];
1488                  return comparePoints(nodeA, offsetA, nodeB, offsetB);
1489              },
1490  
1491              insertNode: function(node) {
1492                  assertRangeValid(this);
1493                  assertValidNodeType(node, insertableNodeTypes);
1494                  assertNodeNotReadOnly(this.startContainer);
1495  
1496                  if (isOrIsAncestorOf(node, this.startContainer)) {
1497                      throw new DOMException("HIERARCHY_REQUEST_ERR");
1498                  }
1499  
1500                  // No check for whether the container of the start of the Range is of a type that does not allow
1501                  // children of the type of node: the browser's DOM implementation should do this for us when we attempt
1502                  // to add the node
1503  
1504                  var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1505                  this.setStartBefore(firstNodeInserted);
1506              },
1507  
1508              cloneContents: function() {
1509                  assertRangeValid(this);
1510  
1511                  var clone, frag;
1512                  if (this.collapsed) {
1513                      return getRangeDocument(this).createDocumentFragment();
1514                  } else {
1515                      if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
1516                          clone = this.startContainer.cloneNode(true);
1517                          clone.data = clone.data.slice(this.startOffset, this.endOffset);
1518                          frag = getRangeDocument(this).createDocumentFragment();
1519                          frag.appendChild(clone);
1520                          return frag;
1521                      } else {
1522                          var iterator = new RangeIterator(this, true);
1523                          clone = cloneSubtree(iterator);
1524                          iterator.detach();
1525                      }
1526                      return clone;
1527                  }
1528              },
1529  
1530              canSurroundContents: function() {
1531                  assertRangeValid(this);
1532                  assertNodeNotReadOnly(this.startContainer);
1533                  assertNodeNotReadOnly(this.endContainer);
1534  
1535                  // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1536                  // no non-text nodes.
1537                  var iterator = new RangeIterator(this, true);
1538                  var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1539                          (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1540                  iterator.detach();
1541                  return !boundariesInvalid;
1542              },
1543  
1544              surroundContents: function(node) {
1545                  assertValidNodeType(node, surroundNodeTypes);
1546  
1547                  if (!this.canSurroundContents()) {
1548                      throw new DOMException("INVALID_STATE_ERR");
1549                  }
1550  
1551                  // Extract the contents
1552                  var content = this.extractContents();
1553  
1554                  // Clear the children of the node
1555                  if (node.hasChildNodes()) {
1556                      while (node.lastChild) {
1557                          node.removeChild(node.lastChild);
1558                      }
1559                  }
1560  
1561                  // Insert the new node and add the extracted contents
1562                  insertNodeAtPosition(node, this.startContainer, this.startOffset);
1563                  node.appendChild(content);
1564  
1565                  this.selectNode(node);
1566              },
1567  
1568              cloneRange: function() {
1569                  assertRangeValid(this);
1570                  var range = new Range(getRangeDocument(this));
1571                  var i = rangeProperties.length, prop;
1572                  while (i--) {
1573                      prop = rangeProperties[i];
1574                      range[prop] = this[prop];
1575                  }
1576                  return range;
1577              },
1578  
1579              toString: function() {
1580                  assertRangeValid(this);
1581                  var sc = this.startContainer;
1582                  if (sc === this.endContainer && isCharacterDataNode(sc)) {
1583                      return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
1584                  } else {
1585                      var textParts = [], iterator = new RangeIterator(this, true);
1586                      iterateSubtree(iterator, function(node) {
1587                          // Accept only text or CDATA nodes, not comments
1588                          if (node.nodeType == 3 || node.nodeType == 4) {
1589                              textParts.push(node.data);
1590                          }
1591                      });
1592                      iterator.detach();
1593                      return textParts.join("");
1594                  }
1595              },
1596  
1597              // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
1598              // been removed from Mozilla.
1599  
1600              compareNode: function(node) {
1601                  assertRangeValid(this);
1602  
1603                  var parent = node.parentNode;
1604                  var nodeIndex = getNodeIndex(node);
1605  
1606                  if (!parent) {
1607                      throw new DOMException("NOT_FOUND_ERR");
1608                  }
1609  
1610                  var startComparison = this.comparePoint(parent, nodeIndex),
1611                      endComparison = this.comparePoint(parent, nodeIndex + 1);
1612  
1613                  if (startComparison < 0) { // Node starts before
1614                      return (endComparison > 0) ? n_b_a : n_b;
1615                  } else {
1616                      return (endComparison > 0) ? n_a : n_i;
1617                  }
1618              },
1619  
1620              comparePoint: function(node, offset) {
1621                  assertRangeValid(this);
1622                  assertNode(node, "HIERARCHY_REQUEST_ERR");
1623                  assertSameDocumentOrFragment(node, this.startContainer);
1624  
1625                  if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
1626                      return -1;
1627                  } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
1628                      return 1;
1629                  }
1630                  return 0;
1631              },
1632  
1633              createContextualFragment: createContextualFragment,
1634  
1635              toHtml: function() {
1636                  return rangeToHtml(this);
1637              },
1638  
1639              // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
1640              // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
1641              intersectsNode: function(node, touchingIsIntersecting) {
1642                  assertRangeValid(this);
1643                  if (getRootContainer(node) != getRangeRoot(this)) {
1644                      return false;
1645                  }
1646  
1647                  var parent = node.parentNode, offset = getNodeIndex(node);
1648                  if (!parent) {
1649                      return true;
1650                  }
1651  
1652                  var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
1653                      endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
1654  
1655                  return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1656              },
1657  
1658              isPointInRange: function(node, offset) {
1659                  assertRangeValid(this);
1660                  assertNode(node, "HIERARCHY_REQUEST_ERR");
1661                  assertSameDocumentOrFragment(node, this.startContainer);
1662  
1663                  return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
1664                         (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
1665              },
1666  
1667              // The methods below are non-standard and invented by me.
1668  
1669              // Sharing a boundary start-to-end or end-to-start does not count as intersection.
1670              intersectsRange: function(range) {
1671                  return rangesIntersect(this, range, false);
1672              },
1673  
1674              // Sharing a boundary start-to-end or end-to-start does count as intersection.
1675              intersectsOrTouchesRange: function(range) {
1676                  return rangesIntersect(this, range, true);
1677              },
1678  
1679              intersection: function(range) {
1680                  if (this.intersectsRange(range)) {
1681                      var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
1682                          endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
1683  
1684                      var intersectionRange = this.cloneRange();
1685                      if (startComparison == -1) {
1686                          intersectionRange.setStart(range.startContainer, range.startOffset);
1687                      }
1688                      if (endComparison == 1) {
1689                          intersectionRange.setEnd(range.endContainer, range.endOffset);
1690                      }
1691                      return intersectionRange;
1692                  }
1693                  return null;
1694              },
1695  
1696              union: function(range) {
1697                  if (this.intersectsOrTouchesRange(range)) {
1698                      var unionRange = this.cloneRange();
1699                      if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
1700                          unionRange.setStart(range.startContainer, range.startOffset);
1701                      }
1702                      if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
1703                          unionRange.setEnd(range.endContainer, range.endOffset);
1704                      }
1705                      return unionRange;
1706                  } else {
1707                      throw new DOMException("Ranges do not intersect");
1708                  }
1709              },
1710  
1711              containsNode: function(node, allowPartial) {
1712                  if (allowPartial) {
1713                      return this.intersectsNode(node, false);
1714                  } else {
1715                      return this.compareNode(node) == n_i;
1716                  }
1717              },
1718  
1719              containsNodeContents: function(node) {
1720                  return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
1721              },
1722  
1723              containsRange: function(range) {
1724                  var intersection = this.intersection(range);
1725                  return intersection !== null && range.equals(intersection);
1726              },
1727  
1728              containsNodeText: function(node) {
1729                  var nodeRange = this.cloneRange();
1730                  nodeRange.selectNode(node);
1731                  var textNodes = nodeRange.getNodes([3]);
1732                  if (textNodes.length > 0) {
1733                      nodeRange.setStart(textNodes[0], 0);
1734                      var lastTextNode = textNodes.pop();
1735                      nodeRange.setEnd(lastTextNode, lastTextNode.length);
1736                      return this.containsRange(nodeRange);
1737                  } else {
1738                      return this.containsNodeContents(node);
1739                  }
1740              },
1741  
1742              getNodes: function(nodeTypes, filter) {
1743                  assertRangeValid(this);
1744                  return getNodesInRange(this, nodeTypes, filter);
1745              },
1746  
1747              getDocument: function() {
1748                  return getRangeDocument(this);
1749              },
1750  
1751              collapseBefore: function(node) {
1752                  this.setEndBefore(node);
1753                  this.collapse(false);
1754              },
1755  
1756              collapseAfter: function(node) {
1757                  this.setStartAfter(node);
1758                  this.collapse(true);
1759              },
1760  
1761              getBookmark: function(containerNode) {
1762                  var doc = getRangeDocument(this);
1763                  var preSelectionRange = api.createRange(doc);
1764                  containerNode = containerNode || dom.getBody(doc);
1765                  preSelectionRange.selectNodeContents(containerNode);
1766                  var range = this.intersection(preSelectionRange);
1767                  var start = 0, end = 0;
1768                  if (range) {
1769                      preSelectionRange.setEnd(range.startContainer, range.startOffset);
1770                      start = preSelectionRange.toString().length;
1771                      end = start + range.toString().length;
1772                  }
1773  
1774                  return {
1775                      start: start,
1776                      end: end,
1777                      containerNode: containerNode
1778                  };
1779              },
1780  
1781              moveToBookmark: function(bookmark) {
1782                  var containerNode = bookmark.containerNode;
1783                  var charIndex = 0;
1784                  this.setStart(containerNode, 0);
1785                  this.collapse(true);
1786                  var nodeStack = [containerNode], node, foundStart = false, stop = false;
1787                  var nextCharIndex, i, childNodes;
1788  
1789                  while (!stop && (node = nodeStack.pop())) {
1790                      if (node.nodeType == 3) {
1791                          nextCharIndex = charIndex + node.length;
1792                          if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
1793                              this.setStart(node, bookmark.start - charIndex);
1794                              foundStart = true;
1795                          }
1796                          if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
1797                              this.setEnd(node, bookmark.end - charIndex);
1798                              stop = true;
1799                          }
1800                          charIndex = nextCharIndex;
1801                      } else {
1802                          childNodes = node.childNodes;
1803                          i = childNodes.length;
1804                          while (i--) {
1805                              nodeStack.push(childNodes[i]);
1806                          }
1807                      }
1808                  }
1809              },
1810  
1811              getName: function() {
1812                  return "DomRange";
1813              },
1814  
1815              equals: function(range) {
1816                  return Range.rangesEqual(this, range);
1817              },
1818  
1819              isValid: function() {
1820                  return isRangeValid(this);
1821              },
1822  
1823              inspect: function() {
1824                  return inspect(this);
1825              },
1826  
1827              detach: function() {
1828                  // In DOM4, detach() is now a no-op.
1829              }
1830          });
1831  
1832          function copyComparisonConstantsToObject(obj) {
1833              obj.START_TO_START = s2s;
1834              obj.START_TO_END = s2e;
1835              obj.END_TO_END = e2e;
1836              obj.END_TO_START = e2s;
1837  
1838              obj.NODE_BEFORE = n_b;
1839              obj.NODE_AFTER = n_a;
1840              obj.NODE_BEFORE_AND_AFTER = n_b_a;
1841              obj.NODE_INSIDE = n_i;
1842          }
1843  
1844          function copyComparisonConstants(constructor) {
1845              copyComparisonConstantsToObject(constructor);
1846              copyComparisonConstantsToObject(constructor.prototype);
1847          }
1848  
1849          function createRangeContentRemover(remover, boundaryUpdater) {
1850              return function() {
1851                  assertRangeValid(this);
1852  
1853                  var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
1854  
1855                  var iterator = new RangeIterator(this, true);
1856  
1857                  // Work out where to position the range after content removal
1858                  var node, boundary;
1859                  if (sc !== root) {
1860                      node = getClosestAncestorIn(sc, root, true);
1861                      boundary = getBoundaryAfterNode(node);
1862                      sc = boundary.node;
1863                      so = boundary.offset;
1864                  }
1865  
1866                  // Check none of the range is read-only
1867                  iterateSubtree(iterator, assertNodeNotReadOnly);
1868  
1869                  iterator.reset();
1870  
1871                  // Remove the content
1872                  var returnValue = remover(iterator);
1873                  iterator.detach();
1874  
1875                  // Move to the new position
1876                  boundaryUpdater(this, sc, so, sc, so);
1877  
1878                  return returnValue;
1879              };
1880          }
1881  
1882          function createPrototypeRange(constructor, boundaryUpdater) {
1883              function createBeforeAfterNodeSetter(isBefore, isStart) {
1884                  return function(node) {
1885                      assertValidNodeType(node, beforeAfterNodeTypes);
1886                      assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
1887  
1888                      var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
1889                      (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
1890                  };
1891              }
1892  
1893              function setRangeStart(range, node, offset) {
1894                  var ec = range.endContainer, eo = range.endOffset;
1895                  if (node !== range.startContainer || offset !== range.startOffset) {
1896                      // Check the root containers of the range and the new boundary, and also check whether the new boundary
1897                      // is after the current end. In either case, collapse the range to the new position
1898                      if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
1899                          ec = node;
1900                          eo = offset;
1901                      }
1902                      boundaryUpdater(range, node, offset, ec, eo);
1903                  }
1904              }
1905  
1906              function setRangeEnd(range, node, offset) {
1907                  var sc = range.startContainer, so = range.startOffset;
1908                  if (node !== range.endContainer || offset !== range.endOffset) {
1909                      // Check the root containers of the range and the new boundary, and also check whether the new boundary
1910                      // is after the current end. In either case, collapse the range to the new position
1911                      if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
1912                          sc = node;
1913                          so = offset;
1914                      }
1915                      boundaryUpdater(range, sc, so, node, offset);
1916                  }
1917              }
1918  
1919              // Set up inheritance
1920              var F = function() {};
1921              F.prototype = api.rangePrototype;
1922              constructor.prototype = new F();
1923  
1924              util.extend(constructor.prototype, {
1925                  setStart: function(node, offset) {
1926                      assertNoDocTypeNotationEntityAncestor(node, true);
1927                      assertValidOffset(node, offset);
1928  
1929                      setRangeStart(this, node, offset);
1930                  },
1931  
1932                  setEnd: function(node, offset) {
1933                      assertNoDocTypeNotationEntityAncestor(node, true);
1934                      assertValidOffset(node, offset);
1935  
1936                      setRangeEnd(this, node, offset);
1937                  },
1938  
1939                  /**
1940                   * Convenience method to set a range's start and end boundaries. Overloaded as follows:
1941                   * - Two parameters (node, offset) creates a collapsed range at that position
1942                   * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
1943                   *   startOffset and ending at endOffset
1944                   * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
1945                   *   startNode and ending at endOffset in endNode
1946                   */
1947                  setStartAndEnd: function() {
1948                      var args = arguments;
1949                      var sc = args[0], so = args[1], ec = sc, eo = so;
1950  
1951                      switch (args.length) {
1952                          case 3:
1953                              eo = args[2];
1954                              break;
1955                          case 4:
1956                              ec = args[2];
1957                              eo = args[3];
1958                              break;
1959                      }
1960  
1961                      boundaryUpdater(this, sc, so, ec, eo);
1962                  },
1963  
1964                  setBoundary: function(node, offset, isStart) {
1965                      this["set" + (isStart ? "Start" : "End")](node, offset);
1966                  },
1967  
1968                  setStartBefore: createBeforeAfterNodeSetter(true, true),
1969                  setStartAfter: createBeforeAfterNodeSetter(false, true),
1970                  setEndBefore: createBeforeAfterNodeSetter(true, false),
1971                  setEndAfter: createBeforeAfterNodeSetter(false, false),
1972  
1973                  collapse: function(isStart) {
1974                      assertRangeValid(this);
1975                      if (isStart) {
1976                          boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
1977                      } else {
1978                          boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
1979                      }
1980                  },
1981  
1982                  selectNodeContents: function(node) {
1983                      assertNoDocTypeNotationEntityAncestor(node, true);
1984  
1985                      boundaryUpdater(this, node, 0, node, getNodeLength(node));
1986                  },
1987  
1988                  selectNode: function(node) {
1989                      assertNoDocTypeNotationEntityAncestor(node, false);
1990                      assertValidNodeType(node, beforeAfterNodeTypes);
1991  
1992                      var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
1993                      boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
1994                  },
1995  
1996                  extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
1997  
1998                  deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
1999  
2000                  canSurroundContents: function() {
2001                      assertRangeValid(this);
2002                      assertNodeNotReadOnly(this.startContainer);
2003                      assertNodeNotReadOnly(this.endContainer);
2004  
2005                      // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
2006                      // no non-text nodes.
2007                      var iterator = new RangeIterator(this, true);
2008                      var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
2009                              (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
2010                      iterator.detach();
2011                      return !boundariesInvalid;
2012                  },
2013  
2014                  splitBoundaries: function() {
2015                      splitRangeBoundaries(this);
2016                  },
2017  
2018                  splitBoundariesPreservingPositions: function(positionsToPreserve) {
2019                      splitRangeBoundaries(this, positionsToPreserve);
2020                  },
2021  
2022                  normalizeBoundaries: function() {
2023                      assertRangeValid(this);
2024  
2025                      var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
2026  
2027                      var mergeForward = function(node) {
2028                          var sibling = node.nextSibling;
2029                          if (sibling && sibling.nodeType == node.nodeType) {
2030                              ec = node;
2031                              eo = node.length;
2032                              node.appendData(sibling.data);
2033                              removeNode(sibling);
2034                          }
2035                      };
2036  
2037                      var mergeBackward = function(node) {
2038                          var sibling = node.previousSibling;
2039                          if (sibling && sibling.nodeType == node.nodeType) {
2040                              sc = node;
2041                              var nodeLength = node.length;
2042                              so = sibling.length;
2043                              node.insertData(0, sibling.data);
2044                              removeNode(sibling);
2045                              if (sc == ec) {
2046                                  eo += so;
2047                                  ec = sc;
2048                              } else if (ec == node.parentNode) {
2049                                  var nodeIndex = getNodeIndex(node);
2050                                  if (eo == nodeIndex) {
2051                                      ec = node;
2052                                      eo = nodeLength;
2053                                  } else if (eo > nodeIndex) {
2054                                      eo--;
2055                                  }
2056                              }
2057                          }
2058                      };
2059  
2060                      var normalizeStart = true;
2061                      var sibling;
2062  
2063                      if (isCharacterDataNode(ec)) {
2064                          if (eo == ec.length) {
2065                              mergeForward(ec);
2066                          } else if (eo == 0) {
2067                              sibling = ec.previousSibling;
2068                              if (sibling && sibling.nodeType == ec.nodeType) {
2069                                  eo = sibling.length;
2070                                  if (sc == ec) {
2071                                      normalizeStart = false;
2072                                  }
2073                                  sibling.appendData(ec.data);
2074                                  removeNode(ec);
2075                                  ec = sibling;
2076                              }
2077                          }
2078                      } else {
2079                          if (eo > 0) {
2080                              var endNode = ec.childNodes[eo - 1];
2081                              if (endNode && isCharacterDataNode(endNode)) {
2082                                  mergeForward(endNode);
2083                              }
2084                          }
2085                          normalizeStart = !this.collapsed;
2086                      }
2087  
2088                      if (normalizeStart) {
2089                          if (isCharacterDataNode(sc)) {
2090                              if (so == 0) {
2091                                  mergeBackward(sc);
2092                              } else if (so == sc.length) {
2093                                  sibling = sc.nextSibling;
2094                                  if (sibling && sibling.nodeType == sc.nodeType) {
2095                                      if (ec == sibling) {
2096                                          ec = sc;
2097                                          eo += sc.length;
2098                                      }
2099                                      sc.appendData(sibling.data);
2100                                      removeNode(sibling);
2101                                  }
2102                              }
2103                          } else {
2104                              if (so < sc.childNodes.length) {
2105                                  var startNode = sc.childNodes[so];
2106                                  if (startNode && isCharacterDataNode(startNode)) {
2107                                      mergeBackward(startNode);
2108                                  }
2109                              }
2110                          }
2111                      } else {
2112                          sc = ec;
2113                          so = eo;
2114                      }
2115  
2116                      boundaryUpdater(this, sc, so, ec, eo);
2117                  },
2118  
2119                  collapseToPoint: function(node, offset) {
2120                      assertNoDocTypeNotationEntityAncestor(node, true);
2121                      assertValidOffset(node, offset);
2122                      this.setStartAndEnd(node, offset);
2123                  }
2124              });
2125  
2126              copyComparisonConstants(constructor);
2127          }
2128  
2129          /*----------------------------------------------------------------------------------------------------------------*/
2130  
2131          // Updates commonAncestorContainer and collapsed after boundary change
2132          function updateCollapsedAndCommonAncestor(range) {
2133              range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2134              range.commonAncestorContainer = range.collapsed ?
2135                  range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
2136          }
2137  
2138          function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
2139              range.startContainer = startContainer;
2140              range.startOffset = startOffset;
2141              range.endContainer = endContainer;
2142              range.endOffset = endOffset;
2143              range.document = dom.getDocument(startContainer);
2144  
2145              updateCollapsedAndCommonAncestor(range);
2146          }
2147  
2148          function Range(doc) {
2149              this.startContainer = doc;
2150              this.startOffset = 0;
2151              this.endContainer = doc;
2152              this.endOffset = 0;
2153              this.document = doc;
2154              updateCollapsedAndCommonAncestor(this);
2155          }
2156  
2157          createPrototypeRange(Range, updateBoundaries);
2158  
2159          util.extend(Range, {
2160              rangeProperties: rangeProperties,
2161              RangeIterator: RangeIterator,
2162              copyComparisonConstants: copyComparisonConstants,
2163              createPrototypeRange: createPrototypeRange,
2164              inspect: inspect,
2165              toHtml: rangeToHtml,
2166              getRangeDocument: getRangeDocument,
2167              rangesEqual: function(r1, r2) {
2168                  return r1.startContainer === r2.startContainer &&
2169                      r1.startOffset === r2.startOffset &&
2170                      r1.endContainer === r2.endContainer &&
2171                      r1.endOffset === r2.endOffset;
2172              }
2173          });
2174  
2175          api.DomRange = Range;
2176      });
2177  
2178      /*----------------------------------------------------------------------------------------------------------------*/

2179  
2180      // Wrappers for the browser's native DOM Range and/or TextRange implementation
2181      api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
2182          var WrappedRange, WrappedTextRange;
2183          var dom = api.dom;
2184          var util = api.util;
2185          var DomPosition = dom.DomPosition;
2186          var DomRange = api.DomRange;
2187          var getBody = dom.getBody;
2188          var getContentDocument = dom.getContentDocument;
2189          var isCharacterDataNode = dom.isCharacterDataNode;
2190  
2191  
2192          /*----------------------------------------------------------------------------------------------------------------*/
2193  
2194          if (api.features.implementsDomRange) {
2195              // This is a wrapper around the browser's native DOM Range. It has two aims:
2196              // - Provide workarounds for specific browser bugs
2197              // - provide convenient extensions, which are inherited from Rangy's DomRange
2198  
2199              (function() {
2200                  var rangeProto;
2201                  var rangeProperties = DomRange.rangeProperties;
2202  
2203                  function updateRangeProperties(range) {
2204                      var i = rangeProperties.length, prop;
2205                      while (i--) {
2206                          prop = rangeProperties[i];
2207                          range[prop] = range.nativeRange[prop];
2208                      }
2209                      // Fix for broken collapsed property in IE 9.
2210                      range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2211                  }
2212  
2213                  function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
2214                      var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
2215                      var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
2216                      var nativeRangeDifferent = !range.equals(range.nativeRange);
2217  
2218                      // Always set both boundaries for the benefit of IE9 (see issue 35)
2219                      if (startMoved || endMoved || nativeRangeDifferent) {
2220                          range.setEnd(endContainer, endOffset);
2221                          range.setStart(startContainer, startOffset);
2222                      }
2223                  }
2224  
2225                  var createBeforeAfterNodeSetter;
2226  
2227                  WrappedRange = function(range) {
2228                      if (!range) {
2229                          throw module.createError("WrappedRange: Range must be specified");
2230                      }
2231                      this.nativeRange = range;
2232                      updateRangeProperties(this);
2233                  };
2234  
2235                  DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
2236  
2237                  rangeProto = WrappedRange.prototype;
2238  
2239                  rangeProto.selectNode = function(node) {
2240                      this.nativeRange.selectNode(node);
2241                      updateRangeProperties(this);
2242                  };
2243  
2244                  rangeProto.cloneContents = function() {
2245                      return this.nativeRange.cloneContents();
2246                  };
2247  
2248                  // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
2249                  // insertNode() is never delegated to the native range.
2250  
2251                  rangeProto.surroundContents = function(node) {
2252                      this.nativeRange.surroundContents(node);
2253                      updateRangeProperties(this);
2254                  };
2255  
2256                  rangeProto.collapse = function(isStart) {
2257                      this.nativeRange.collapse(isStart);
2258                      updateRangeProperties(this);
2259                  };
2260  
2261                  rangeProto.cloneRange = function() {
2262                      return new WrappedRange(this.nativeRange.cloneRange());
2263                  };
2264  
2265                  rangeProto.refresh = function() {
2266                      updateRangeProperties(this);
2267                  };
2268  
2269                  rangeProto.toString = function() {
2270                      return this.nativeRange.toString();
2271                  };
2272  
2273                  // Create test range and node for feature detection
2274  
2275                  var testTextNode = document.createTextNode("test");
2276                  getBody(document).appendChild(testTextNode);
2277                  var range = document.createRange();
2278  
2279                  /*--------------------------------------------------------------------------------------------------------*/
2280  
2281                  // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
2282                  // correct for it
2283  
2284                  range.setStart(testTextNode, 0);
2285                  range.setEnd(testTextNode, 0);
2286  
2287                  try {
2288                      range.setStart(testTextNode, 1);
2289  
2290                      rangeProto.setStart = function(node, offset) {
2291                          this.nativeRange.setStart(node, offset);
2292                          updateRangeProperties(this);
2293                      };
2294  
2295                      rangeProto.setEnd = function(node, offset) {
2296                          this.nativeRange.setEnd(node, offset);
2297                          updateRangeProperties(this);
2298                      };
2299  
2300                      createBeforeAfterNodeSetter = function(name) {
2301                          return function(node) {
2302                              this.nativeRange[name](node);
2303                              updateRangeProperties(this);
2304                          };
2305                      };
2306  
2307                  } catch(ex) {
2308  
2309                      rangeProto.setStart = function(node, offset) {
2310                          try {
2311                              this.nativeRange.setStart(node, offset);
2312                          } catch (ex) {
2313                              this.nativeRange.setEnd(node, offset);
2314                              this.nativeRange.setStart(node, offset);
2315                          }
2316                          updateRangeProperties(this);
2317                      };
2318  
2319                      rangeProto.setEnd = function(node, offset) {
2320                          try {
2321                              this.nativeRange.setEnd(node, offset);
2322                          } catch (ex) {
2323                              this.nativeRange.setStart(node, offset);
2324                              this.nativeRange.setEnd(node, offset);
2325                          }
2326                          updateRangeProperties(this);
2327                      };
2328  
2329                      createBeforeAfterNodeSetter = function(name, oppositeName) {
2330                          return function(node) {
2331                              try {
2332                                  this.nativeRange[name](node);
2333                              } catch (ex) {
2334                                  this.nativeRange[oppositeName](node);
2335                                  this.nativeRange[name](node);
2336                              }
2337                              updateRangeProperties(this);
2338                          };
2339                      };
2340                  }
2341  
2342                  rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
2343                  rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
2344                  rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
2345                  rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
2346  
2347                  /*--------------------------------------------------------------------------------------------------------*/
2348  
2349                  // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
2350                  // whether the native implementation can be trusted
2351                  rangeProto.selectNodeContents = function(node) {
2352                      this.setStartAndEnd(node, 0, dom.getNodeLength(node));
2353                  };
2354  
2355                  /*--------------------------------------------------------------------------------------------------------*/
2356  
2357                  // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
2358                  // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
2359  
2360                  range.selectNodeContents(testTextNode);
2361                  range.setEnd(testTextNode, 3);
2362  
2363                  var range2 = document.createRange();
2364                  range2.selectNodeContents(testTextNode);
2365                  range2.setEnd(testTextNode, 4);
2366                  range2.setStart(testTextNode, 2);
2367  
2368                  if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
2369                          range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
2370                      // This is the wrong way round, so correct for it
2371  
2372                      rangeProto.compareBoundaryPoints = function(type, range) {
2373                          range = range.nativeRange || range;
2374                          if (type == range.START_TO_END) {
2375                              type = range.END_TO_START;
2376                          } else if (type == range.END_TO_START) {
2377                              type = range.START_TO_END;
2378                          }
2379                          return this.nativeRange.compareBoundaryPoints(type, range);
2380                      };
2381                  } else {
2382                      rangeProto.compareBoundaryPoints = function(type, range) {
2383                          return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
2384                      };
2385                  }
2386  
2387                  /*--------------------------------------------------------------------------------------------------------*/
2388  
2389                  // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
2390  
2391                  var el = document.createElement("div");
2392                  el.innerHTML = "123";
2393                  var textNode = el.firstChild;
2394                  var body = getBody(document);
2395                  body.appendChild(el);
2396  
2397                  range.setStart(textNode, 1);
2398                  range.setEnd(textNode, 2);
2399                  range.deleteContents();
2400  
2401                  if (textNode.data == "13") {
2402                      // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
2403                      // extractContents()
2404                      rangeProto.deleteContents = function() {
2405                          this.nativeRange.deleteContents();
2406                          updateRangeProperties(this);
2407                      };
2408  
2409                      rangeProto.extractContents = function() {
2410                          var frag = this.nativeRange.extractContents();
2411                          updateRangeProperties(this);
2412                          return frag;
2413                      };
2414                  } else {
2415                  }
2416  
2417                  body.removeChild(el);
2418                  body = null;
2419  
2420                  /*--------------------------------------------------------------------------------------------------------*/
2421  
2422                  // Test for existence of createContextualFragment and delegate to it if it exists
2423                  if (util.isHostMethod(range, "createContextualFragment")) {
2424                      rangeProto.createContextualFragment = function(fragmentStr) {
2425                          return this.nativeRange.createContextualFragment(fragmentStr);
2426                      };
2427                  }
2428  
2429                  /*--------------------------------------------------------------------------------------------------------*/
2430  
2431                  // Clean up
2432                  getBody(document).removeChild(testTextNode);
2433  
2434                  rangeProto.getName = function() {
2435                      return "WrappedRange";
2436                  };
2437  
2438                  api.WrappedRange = WrappedRange;
2439  
2440                  api.createNativeRange = function(doc) {
2441                      doc = getContentDocument(doc, module, "createNativeRange");
2442                      return doc.createRange();
2443                  };
2444              })();
2445          }
2446  
2447          if (api.features.implementsTextRange) {
2448              /*
2449              This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
2450              method. For example, in the following (where pipes denote the selection boundaries):
2451  
2452              <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
2453  
2454              var range = document.selection.createRange();
2455              alert(range.parentElement().id); // Should alert "ul" but alerts "b"
2456  
2457              This method returns the common ancestor node of the following:
2458              - the parentElement() of the textRange
2459              - the parentElement() of the textRange after calling collapse(true)
2460              - the parentElement() of the textRange after calling collapse(false)
2461              */
2462              var getTextRangeContainerElement = function(textRange) {
2463                  var parentEl = textRange.parentElement();
2464                  var range = textRange.duplicate();
2465                  range.collapse(true);
2466                  var startEl = range.parentElement();
2467                  range = textRange.duplicate();
2468                  range.collapse(false);
2469                  var endEl = range.parentElement();
2470                  var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
2471  
2472                  return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
2473              };
2474  
2475              var textRangeIsCollapsed = function(textRange) {
2476                  return textRange.compareEndPoints("StartToEnd", textRange) == 0;
2477              };
2478  
2479              // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
2480              // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
2481              // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
2482              // bugs, handling for inputs and images, plus optimizations.
2483              var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
2484                  var workingRange = textRange.duplicate();
2485                  workingRange.collapse(isStart);
2486                  var containerElement = workingRange.parentElement();
2487  
2488                  // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
2489                  // check for that
2490                  if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
2491                      containerElement = wholeRangeContainerElement;
2492                  }
2493  
2494  
2495                  // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
2496                  // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
2497                  if (!containerElement.canHaveHTML) {
2498                      var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
2499                      return {
2500                          boundaryPosition: pos,
2501                          nodeInfo: {
2502                              nodeIndex: pos.offset,
2503                              containerElement: pos.node
2504                          }
2505                      };
2506                  }
2507  
2508                  var workingNode = dom.getDocument(containerElement).createElement("span");
2509  
2510                  // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
2511                  // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
2512                  if (workingNode.parentNode) {
2513                      dom.removeNode(workingNode);
2514                  }
2515  
2516                  var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
2517                  var previousNode, nextNode, boundaryPosition, boundaryNode;
2518                  var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
2519                  var childNodeCount = containerElement.childNodes.length;
2520                  var end = childNodeCount;
2521  
2522                  // Check end first. Code within the loop assumes that the endth child node of the container is definitely
2523                  // after the range boundary.
2524                  var nodeIndex = end;
2525  
2526                  while (true) {
2527                      if (nodeIndex == childNodeCount) {
2528                          containerElement.appendChild(workingNode);
2529                      } else {
2530                          containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
2531                      }
2532                      workingRange.moveToElementText(workingNode);
2533                      comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
2534                      if (comparison == 0 || start == end) {
2535                          break;
2536                      } else if (comparison == -1) {
2537                          if (end == start + 1) {
2538                              // We know the endth child node is after the range boundary, so we must be done.
2539                              break;
2540                          } else {
2541                              start = nodeIndex;
2542                          }
2543                      } else {
2544                          end = (end == start + 1) ? start : nodeIndex;
2545                      }
2546                      nodeIndex = Math.floor((start + end) / 2);
2547                      containerElement.removeChild(workingNode);
2548                  }
2549  
2550  
2551                  // We've now reached or gone past the boundary of the text range we're interested in
2552                  // so have identified the node we want
2553                  boundaryNode = workingNode.nextSibling;
2554  
2555                  if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
2556                      // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
2557                      // the node containing the text range's boundary, so we move the end of the working range to the
2558                      // boundary point and measure the length of its text to get the boundary's offset within the node.
2559                      workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
2560  
2561                      var offset;
2562  
2563                      if (/[\r\n]/.test(boundaryNode.data)) {
2564                          /*
2565                          For the particular case of a boundary within a text node containing rendered line breaks (within a
2566                          <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
2567                          IE. The facts:
2568  
2569                          - Each line break is represented as \r in the text node's data/nodeValue properties
2570                          - Each line break is represented as \r\n in the TextRange's 'text' property
2571                          - The 'text' property of the TextRange does not contain trailing line breaks
2572  
2573                          To get round the problem presented by the final fact above, we can use the fact that TextRange's
2574                          moveStart() and moveEnd() methods return the actual number of characters moved, which is not
2575                          necessarily the same as the number of characters it was instructed to move. The simplest approach is
2576                          to use this to store the characters moved when moving both the start and end of the range to the
2577                          start of the document body and subtracting the start offset from the end offset (the
2578                          "move-negative-gazillion" method). However, this is extremely slow when the document is large and
2579                          the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
2580                          the end of the document) has the same problem.
2581  
2582                          Another approach that works is to use moveStart() to move the start boundary of the range up to the
2583                          end boundary one character at a time and incrementing a counter with the value returned by the
2584                          moveStart() call. However, the check for whether the start boundary has reached the end boundary is
2585                          expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
2586                          by the location of the range within the document).
2587  
2588                          The approach used below is a hybrid of the two methods above. It uses the fact that a string
2589                          containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
2590                          be longer than the text of the TextRange, so the start of the range is moved that length initially
2591                          and then a character at a time to make up for any trailing line breaks not contained in the 'text'
2592                          property. This has good performance in most situations compared to the previous two methods.
2593                          */
2594                          var tempRange = workingRange.duplicate();
2595                          var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
2596  
2597                          offset = tempRange.moveStart("character", rangeLength);
2598                          while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
2599                              offset++;
2600                              tempRange.moveStart("character", 1);
2601                          }
2602                      } else {
2603                          offset = workingRange.text.length;
2604                      }
2605                      boundaryPosition = new DomPosition(boundaryNode, offset);
2606                  } else {
2607  
2608                      // If the boundary immediately follows a character data node and this is the end boundary, we should favour
2609                      // a position within that, and likewise for a start boundary preceding a character data node
2610                      previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
2611                      nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
2612                      if (nextNode && isCharacterDataNode(nextNode)) {
2613                          boundaryPosition = new DomPosition(nextNode, 0);
2614                      } else if (previousNode && isCharacterDataNode(previousNode)) {
2615                          boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
2616                      } else {
2617                          boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
2618                      }
2619                  }
2620  
2621                  // Clean up
2622                  dom.removeNode(workingNode);
2623  
2624                  return {
2625                      boundaryPosition: boundaryPosition,
2626                      nodeInfo: {
2627                          nodeIndex: nodeIndex,
2628                          containerElement: containerElement
2629                      }
2630                  };
2631              };
2632  
2633              // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
2634              // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
2635              // (http://code.google.com/p/ierange/)
2636              var createBoundaryTextRange = function(boundaryPosition, isStart) {
2637                  var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
2638                  var doc = dom.getDocument(boundaryPosition.node);
2639                  var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
2640                  var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
2641  
2642                  if (nodeIsDataNode) {
2643                      boundaryNode = boundaryPosition.node;
2644                      boundaryParent = boundaryNode.parentNode;
2645                  } else {
2646                      childNodes = boundaryPosition.node.childNodes;
2647                      boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
2648                      boundaryParent = boundaryPosition.node;
2649                  }
2650  
2651                  // Position the range immediately before the node containing the boundary
2652                  workingNode = doc.createElement("span");
2653  
2654                  // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
2655                  // the element rather than immediately before or after it
2656                  workingNode.innerHTML = "&#feff;";
2657  
2658                  // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
2659                  // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
2660                  if (boundaryNode) {
2661                      boundaryParent.insertBefore(workingNode, boundaryNode);
2662                  } else {
2663                      boundaryParent.appendChild(workingNode);
2664                  }
2665  
2666                  workingRange.moveToElementText(workingNode);
2667                  workingRange.collapse(!isStart);
2668  
2669                  // Clean up
2670                  boundaryParent.removeChild(workingNode);
2671  
2672                  // Move the working range to the text offset, if required
2673                  if (nodeIsDataNode) {
2674                      workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
2675                  }
2676  
2677                  return workingRange;
2678              };
2679  
2680              /*------------------------------------------------------------------------------------------------------------*/
2681  
2682              // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
2683              // prototype
2684  
2685              WrappedTextRange = function(textRange) {
2686                  this.textRange = textRange;
2687                  this.refresh();
2688              };
2689  
2690              WrappedTextRange.prototype = new DomRange(document);
2691  
2692              WrappedTextRange.prototype.refresh = function() {
2693                  var start, end, startBoundary;
2694  
2695                  // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
2696                  var rangeContainerElement = getTextRangeContainerElement(this.textRange);
2697  
2698                  if (textRangeIsCollapsed(this.textRange)) {
2699                      end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
2700                          true).boundaryPosition;
2701                  } else {
2702                      startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
2703                      start = startBoundary.boundaryPosition;
2704  
2705                      // An optimization used here is that if the start and end boundaries have the same parent element, the
2706                      // search scope for the end boundary can be limited to exclude the portion of the element that precedes
2707                      // the start boundary
2708                      end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
2709                          startBoundary.nodeInfo).boundaryPosition;
2710                  }
2711  
2712                  this.setStart(start.node, start.offset);
2713                  this.setEnd(end.node, end.offset);
2714              };
2715  
2716              WrappedTextRange.prototype.getName = function() {
2717                  return "WrappedTextRange";
2718              };
2719  
2720              DomRange.copyComparisonConstants(WrappedTextRange);
2721  
2722              var rangeToTextRange = function(range) {
2723                  if (range.collapsed) {
2724                      return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2725                  } else {
2726                      var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2727                      var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
2728                      var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
2729                      textRange.setEndPoint("StartToStart", startRange);
2730                      textRange.setEndPoint("EndToEnd", endRange);
2731                      return textRange;
2732                  }
2733              };
2734  
2735              WrappedTextRange.rangeToTextRange = rangeToTextRange;
2736  
2737              WrappedTextRange.prototype.toTextRange = function() {
2738                  return rangeToTextRange(this);
2739              };
2740  
2741              api.WrappedTextRange = WrappedTextRange;
2742  
2743              // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
2744              // implementation to use by default.
2745              if (!api.features.implementsDomRange || api.config.preferTextRange) {
2746                  // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
2747                  var globalObj = (function(f) { return f("return this;")(); })(Function);
2748                  if (typeof globalObj.Range == "undefined") {
2749                      globalObj.Range = WrappedTextRange;
2750                  }
2751  
2752                  api.createNativeRange = function(doc) {
2753                      doc = getContentDocument(doc, module, "createNativeRange");
2754                      return getBody(doc).createTextRange();
2755                  };
2756  
2757                  api.WrappedRange = WrappedTextRange;
2758              }
2759          }
2760  
2761          api.createRange = function(doc) {
2762              doc = getContentDocument(doc, module, "createRange");
2763              return new api.WrappedRange(api.createNativeRange(doc));
2764          };
2765  
2766          api.createRangyRange = function(doc) {
2767              doc = getContentDocument(doc, module, "createRangyRange");
2768              return new DomRange(doc);
2769          };
2770  
2771          util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
2772          util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
2773  
2774          api.addShimListener(function(win) {
2775              var doc = win.document;
2776              if (typeof doc.createRange == "undefined") {
2777                  doc.createRange = function() {
2778                      return api.createRange(doc);
2779                  };
2780              }
2781              doc = win = null;
2782          });
2783      });
2784  
2785      /*----------------------------------------------------------------------------------------------------------------*/

2786  
2787      // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
2788      // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
2789      api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
2790          api.config.checkSelectionRanges = true;
2791  
2792          var BOOLEAN = "boolean";
2793          var NUMBER = "number";
2794          var dom = api.dom;
2795          var util = api.util;
2796          var isHostMethod = util.isHostMethod;
2797          var DomRange = api.DomRange;
2798          var WrappedRange = api.WrappedRange;
2799          var DOMException = api.DOMException;
2800          var DomPosition = dom.DomPosition;
2801          var getNativeSelection;
2802          var selectionIsCollapsed;
2803          var features = api.features;
2804          var CONTROL = "Control";
2805          var getDocument = dom.getDocument;
2806          var getBody = dom.getBody;
2807          var rangesEqual = DomRange.rangesEqual;
2808  
2809  
2810          // Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
2811          // "forward" or "forwards") or a Boolean (true for backwards).
2812          function isDirectionBackward(dir) {
2813              return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
2814          }
2815  
2816          function getWindow(win, methodName) {
2817              if (!win) {
2818                  return window;
2819              } else if (dom.isWindow(win)) {
2820                  return win;
2821              } else if (win instanceof WrappedSelection) {
2822                  return win.win;
2823              } else {
2824                  var doc = dom.getContentDocument(win, module, methodName);
2825                  return dom.getWindow(doc);
2826              }
2827          }
2828  
2829          function getWinSelection(winParam) {
2830              return getWindow(winParam, "getWinSelection").getSelection();
2831          }
2832  
2833          function getDocSelection(winParam) {
2834              return getWindow(winParam, "getDocSelection").document.selection;
2835          }
2836  
2837          function winSelectionIsBackward(sel) {
2838              var backward = false;
2839              if (sel.anchorNode) {
2840                  backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
2841              }
2842              return backward;
2843          }
2844  
2845          // Test for the Range/TextRange and Selection features required
2846          // Test for ability to retrieve selection
2847          var implementsWinGetSelection = isHostMethod(window, "getSelection"),
2848              implementsDocSelection = util.isHostObject(document, "selection");
2849  
2850          features.implementsWinGetSelection = implementsWinGetSelection;
2851          features.implementsDocSelection = implementsDocSelection;
2852  
2853          var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
2854  
2855          if (useDocumentSelection) {
2856              getNativeSelection = getDocSelection;
2857              api.isSelectionValid = function(winParam) {
2858                  var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
2859  
2860                  // Check whether the selection TextRange is actually contained within the correct document
2861                  return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
2862              };
2863          } else if (implementsWinGetSelection) {
2864              getNativeSelection = getWinSelection;
2865              api.isSelectionValid = function() {
2866                  return true;
2867              };
2868          } else {
2869              module.fail("Neither document.selection or window.getSelection() detected.");
2870              return false;
2871          }
2872  
2873          api.getNativeSelection = getNativeSelection;
2874  
2875          var testSelection = getNativeSelection();
2876  
2877          // In Firefox, the selection is null in an iframe with display: none. See issue #138.
2878          if (!testSelection) {
2879              module.fail("Native selection was null (possibly issue 138?)");
2880              return false;
2881          }
2882  
2883          var testRange = api.createNativeRange(document);
2884          var body = getBody(document);
2885  
2886          // Obtaining a range from a selection
2887          var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
2888              ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
2889  
2890          features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
2891  
2892          // Test for existence of native selection extend() method
2893          var selectionHasExtend = isHostMethod(testSelection, "extend");
2894          features.selectionHasExtend = selectionHasExtend;
2895  
2896          // Test if rangeCount exists
2897          var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
2898          features.selectionHasRangeCount = selectionHasRangeCount;
2899  
2900          var selectionSupportsMultipleRanges = false;
2901          var collapsedNonEditableSelectionsSupported = true;
2902  
2903          var addRangeBackwardToNative = selectionHasExtend ?
2904              function(nativeSelection, range) {
2905                  var doc = DomRange.getRangeDocument(range);
2906                  var endRange = api.createRange(doc);
2907                  endRange.collapseToPoint(range.endContainer, range.endOffset);
2908                  nativeSelection.addRange(getNativeRange(endRange));
2909                  nativeSelection.extend(range.startContainer, range.startOffset);
2910              } : null;
2911  
2912          if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
2913                  typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
2914  
2915              (function() {
2916                  // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
2917                  // performed on the current document's selection. See issue 109.
2918  
2919                  // Note also that if a selection previously existed, it is wiped and later restored by these tests. This
2920                  // will result in the selection direction begin reversed if the original selection was backwards and the
2921                  // browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
2922                  var sel = window.getSelection();
2923                  if (sel) {
2924                      // Store the current selection
2925                      var originalSelectionRangeCount = sel.rangeCount;
2926                      var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
2927                      var originalSelectionRanges = [];
2928                      var originalSelectionBackward = winSelectionIsBackward(sel);
2929                      for (var i = 0; i < originalSelectionRangeCount; ++i) {
2930                          originalSelectionRanges[i] = sel.getRangeAt(i);
2931                      }
2932  
2933                      // Create some test elements
2934                      var testEl = dom.createTestElement(document, "", false);
2935                      var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
2936  
2937                      // Test whether the native selection will allow a collapsed selection within a non-editable element
2938                      var r1 = document.createRange();
2939  
2940                      r1.setStart(textNode, 1);
2941                      r1.collapse(true);
2942                      sel.removeAllRanges();
2943                      sel.addRange(r1);
2944                      collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
2945                      sel.removeAllRanges();
2946  
2947                      // Test whether the native selection is capable of supporting multiple ranges.
2948                      if (!selectionHasMultipleRanges) {
2949                          // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
2950                          // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
2951                          // nothing we can do about this while retaining the feature test so we have to resort to a browser
2952                          // sniff. I'm not happy about it. See
2953                          // https://code.google.com/p/chromium/issues/detail?id=399791
2954                          var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
2955                          if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
2956                              selectionSupportsMultipleRanges = false;
2957                          } else {
2958                              var r2 = r1.cloneRange();
2959                              r1.setStart(textNode, 0);
2960                              r2.setEnd(textNode, 3);
2961                              r2.setStart(textNode, 2);
2962                              sel.addRange(r1);
2963                              sel.addRange(r2);
2964                              selectionSupportsMultipleRanges = (sel.rangeCount == 2);
2965                          }
2966                      }
2967  
2968                      // Clean up
2969                      dom.removeNode(testEl);
2970                      sel.removeAllRanges();
2971  
2972                      for (i = 0; i < originalSelectionRangeCount; ++i) {
2973                          if (i == 0 && originalSelectionBackward) {
2974                              if (addRangeBackwardToNative) {
2975                                  addRangeBackwardToNative(sel, originalSelectionRanges[i]);
2976                              } else {
2977                                  api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
2978                                  sel.addRange(originalSelectionRanges[i]);
2979                              }
2980                          } else {
2981                              sel.addRange(originalSelectionRanges[i]);
2982                          }
2983                      }
2984                  }
2985              })();
2986          }
2987  
2988          features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
2989          features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
2990  
2991          // ControlRanges
2992          var implementsControlRange = false, testControlRange;
2993  
2994          if (body && isHostMethod(body, "createControlRange")) {
2995              testControlRange = body.createControlRange();
2996              if (util.areHostProperties(testControlRange, ["item", "add"])) {
2997                  implementsControlRange = true;
2998              }
2999          }
3000          features.implementsControlRange = implementsControlRange;
3001  
3002          // Selection collapsedness
3003          if (selectionHasAnchorAndFocus) {
3004              selectionIsCollapsed = function(sel) {
3005                  return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
3006              };
3007          } else {
3008              selectionIsCollapsed = function(sel) {
3009                  return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
3010              };
3011          }
3012  
3013          function updateAnchorAndFocusFromRange(sel, range, backward) {
3014              var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
3015              sel.anchorNode = range[anchorPrefix + "Container"];
3016              sel.anchorOffset = range[anchorPrefix + "Offset"];
3017              sel.focusNode = range[focusPrefix + "Container"];
3018              sel.focusOffset = range[focusPrefix + "Offset"];
3019          }
3020  
3021          function updateAnchorAndFocusFromNativeSelection(sel) {
3022              var nativeSel = sel.nativeSelection;
3023              sel.anchorNode = nativeSel.anchorNode;
3024              sel.anchorOffset = nativeSel.anchorOffset;
3025              sel.focusNode = nativeSel.focusNode;
3026              sel.focusOffset = nativeSel.focusOffset;
3027          }
3028  
3029          function updateEmptySelection(sel) {
3030              sel.anchorNode = sel.focusNode = null;
3031              sel.anchorOffset = sel.focusOffset = 0;
3032              sel.rangeCount = 0;
3033              sel.isCollapsed = true;
3034              sel._ranges.length = 0;
3035          }
3036  
3037          function getNativeRange(range) {
3038              var nativeRange;
3039              if (range instanceof DomRange) {
3040                  nativeRange = api.createNativeRange(range.getDocument());
3041                  nativeRange.setEnd(range.endContainer, range.endOffset);
3042                  nativeRange.setStart(range.startContainer, range.startOffset);
3043              } else if (range instanceof WrappedRange) {
3044                  nativeRange = range.nativeRange;
3045              } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
3046                  nativeRange = range;
3047              }
3048              return nativeRange;
3049          }
3050  
3051          function rangeContainsSingleElement(rangeNodes) {
3052              if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
3053                  return false;
3054              }
3055              for (var i = 1, len = rangeNodes.length; i < len; ++i) {
3056                  if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
3057                      return false;
3058                  }
3059              }
3060              return true;
3061          }
3062  
3063          function getSingleElementFromRange(range) {
3064              var nodes = range.getNodes();
3065              if (!rangeContainsSingleElement(nodes)) {
3066                  throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
3067              }
3068              return nodes[0];
3069          }
3070  
3071          // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
3072          function isTextRange(range) {
3073              return !!range && typeof range.text != "undefined";
3074          }
3075  
3076          function updateFromTextRange(sel, range) {
3077              // Create a Range from the selected TextRange
3078              var wrappedRange = new WrappedRange(range);
3079              sel._ranges = [wrappedRange];
3080  
3081              updateAnchorAndFocusFromRange(sel, wrappedRange, false);
3082              sel.rangeCount = 1;
3083              sel.isCollapsed = wrappedRange.collapsed;
3084          }
3085  
3086          function updateControlSelection(sel) {
3087              // Update the wrapped selection based on what's now in the native selection
3088              sel._ranges.length = 0;
3089              if (sel.docSelection.type == "None") {
3090                  updateEmptySelection(sel);
3091              } else {
3092                  var controlRange = sel.docSelection.createRange();
3093                  if (isTextRange(controlRange)) {
3094                      // This case (where the selection type is "Control" and calling createRange() on the selection returns
3095                      // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
3096                      // ControlRange have been removed from the ControlRange and removed from the document.
3097                      updateFromTextRange(sel, controlRange);
3098                  } else {
3099                      sel.rangeCount = controlRange.length;
3100                      var range, doc = getDocument(controlRange.item(0));
3101                      for (var i = 0; i < sel.rangeCount; ++i) {
3102                          range = api.createRange(doc);
3103                          range.selectNode(controlRange.item(i));
3104                          sel._ranges.push(range);
3105                      }
3106                      sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
3107                      updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
3108                  }
3109              }
3110          }
3111  
3112          function addRangeToControlSelection(sel, range) {
3113              var controlRange = sel.docSelection.createRange();
3114              var rangeElement = getSingleElementFromRange(range);
3115  
3116              // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
3117              // contained by the supplied range
3118              var doc = getDocument(controlRange.item(0));
3119              var newControlRange = getBody(doc).createControlRange();
3120              for (var i = 0, len = controlRange.length; i < len; ++i) {
3121                  newControlRange.add(controlRange.item(i));
3122              }
3123              try {
3124                  newControlRange.add(rangeElement);
3125              } catch (ex) {
3126                  throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
3127              }
3128              newControlRange.select();
3129  
3130              // Update the wrapped selection based on what's now in the native selection
3131              updateControlSelection(sel);
3132          }
3133  
3134          var getSelectionRangeAt;
3135  
3136          if (isHostMethod(testSelection, "getRangeAt")) {
3137              // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
3138              // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
3139              // lesson to us all, especially me.
3140              getSelectionRangeAt = function(sel, index) {
3141                  try {
3142                      return sel.getRangeAt(index);
3143                  } catch (ex) {
3144                      return null;
3145                  }
3146              };
3147          } else if (selectionHasAnchorAndFocus) {
3148              getSelectionRangeAt = function(sel) {
3149                  var doc = getDocument(sel.anchorNode);
3150                  var range = api.createRange(doc);
3151                  range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
3152  
3153                  // Handle the case when the selection was selected backwards (from the end to the start in the
3154                  // document)
3155                  if (range.collapsed !== this.isCollapsed) {
3156                      range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
3157                  }
3158  
3159                  return range;
3160              };
3161          }
3162  
3163          function WrappedSelection(selection, docSelection, win) {
3164              this.nativeSelection = selection;
3165              this.docSelection = docSelection;
3166              this._ranges = [];
3167              this.win = win;
3168              this.refresh();
3169          }
3170  
3171          WrappedSelection.prototype = api.selectionPrototype;
3172  
3173          function deleteProperties(sel) {
3174              sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
3175              sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
3176              sel.detached = true;
3177          }
3178  
3179          var cachedRangySelections = [];
3180  
3181          function actOnCachedSelection(win, action) {
3182              var i = cachedRangySelections.length, cached, sel;
3183              while (i--) {
3184                  cached = cachedRangySelections[i];
3185                  sel = cached.selection;
3186                  if (action == "deleteAll") {
3187                      deleteProperties(sel);
3188                  } else if (cached.win == win) {
3189                      if (action == "delete") {
3190                          cachedRangySelections.splice(i, 1);
3191                          return true;
3192                      } else {
3193                          return sel;
3194                      }
3195                  }
3196              }
3197              if (action == "deleteAll") {
3198                  cachedRangySelections.length = 0;
3199              }
3200              return null;
3201          }
3202  
3203          var getSelection = function(win) {
3204              // Check if the parameter is a Rangy Selection object
3205              if (win && win instanceof WrappedSelection) {
3206                  win.refresh();
3207                  return win;
3208              }
3209  
3210              win = getWindow(win, "getNativeSelection");
3211  
3212              var sel = actOnCachedSelection(win);
3213              var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
3214              if (sel) {
3215                  sel.nativeSelection = nativeSel;
3216                  sel.docSelection = docSel;
3217                  sel.refresh();
3218              } else {
3219                  sel = new WrappedSelection(nativeSel, docSel, win);
3220                  cachedRangySelections.push( { win: win, selection: sel } );
3221              }
3222              return sel;
3223          };
3224  
3225          api.getSelection = getSelection;
3226  
3227          util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
3228  
3229          var selProto = WrappedSelection.prototype;
3230  
3231          function createControlSelection(sel, ranges) {
3232              // Ensure that the selection becomes of type "Control"
3233              var doc = getDocument(ranges[0].startContainer);
3234              var controlRange = getBody(doc).createControlRange();
3235              for (var i = 0, el, len = ranges.length; i < len; ++i) {
3236                  el = getSingleElementFromRange(ranges[i]);
3237                  try {
3238                      controlRange.add(el);
3239                  } catch (ex) {
3240                      throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
3241                  }
3242              }
3243              controlRange.select();
3244  
3245              // Update the wrapped selection based on what's now in the native selection
3246              updateControlSelection(sel);
3247          }
3248  
3249          // Selecting a range
3250          if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
3251              selProto.removeAllRanges = function() {
3252                  this.nativeSelection.removeAllRanges();
3253                  updateEmptySelection(this);
3254              };
3255  
3256              var addRangeBackward = function(sel, range) {
3257                  addRangeBackwardToNative(sel.nativeSelection, range);
3258                  sel.refresh();
3259              };
3260  
3261              if (selectionHasRangeCount) {
3262                  selProto.addRange = function(range, direction) {
3263                      if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3264                          addRangeToControlSelection(this, range);
3265                      } else {
3266                          if (isDirectionBackward(direction) && selectionHasExtend) {
3267                              addRangeBackward(this, range);
3268                          } else {
3269                              var previousRangeCount;
3270                              if (selectionSupportsMultipleRanges) {
3271                                  previousRangeCount = this.rangeCount;
3272                              } else {
3273                                  this.removeAllRanges();
3274                                  previousRangeCount = 0;
3275                              }
3276                              // Clone the native range so that changing the selected range does not affect the selection.
3277                              // This is contrary to the spec but is the only way to achieve consistency between browsers. See
3278                              // issue 80.
3279                              var clonedNativeRange = getNativeRange(range).cloneRange();
3280                              try {
3281                                  this.nativeSelection.addRange(clonedNativeRange);
3282                              } catch (ex) {
3283                              }
3284  
3285                              // Check whether adding the range was successful
3286                              this.rangeCount = this.nativeSelection.rangeCount;
3287  
3288                              if (this.rangeCount == previousRangeCount + 1) {
3289                                  // The range was added successfully
3290  
3291                                  // Check whether the range that we added to the selection is reflected in the last range extracted from
3292                                  // the selection
3293                                  if (api.config.checkSelectionRanges) {
3294                                      var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
3295                                      if (nativeRange && !rangesEqual(nativeRange, range)) {
3296                                          // Happens in WebKit with, for example, a selection placed at the start of a text node
3297                                          range = new WrappedRange(nativeRange);
3298                                      }
3299                                  }
3300                                  this._ranges[this.rangeCount - 1] = range;
3301                                  updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
3302                                  this.isCollapsed = selectionIsCollapsed(this);
3303                              } else {
3304                                  // The range was not added successfully. The simplest thing is to refresh
3305                                  this.refresh();
3306                              }
3307                          }
3308                      }
3309                  };
3310              } else {
3311                  selProto.addRange = function(range, direction) {
3312                      if (isDirectionBackward(direction) && selectionHasExtend) {
3313                          addRangeBackward(this, range);
3314                      } else {
3315                          this.nativeSelection.addRange(getNativeRange(range));
3316                          this.refresh();
3317                      }
3318                  };
3319              }
3320  
3321              selProto.setRanges = function(ranges) {
3322                  if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
3323                      createControlSelection(this, ranges);
3324                  } else {
3325                      this.removeAllRanges();
3326                      for (var i = 0, len = ranges.length; i < len; ++i) {
3327                          this.addRange(ranges[i]);
3328                      }
3329                  }
3330              };
3331          } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
3332                     implementsControlRange && useDocumentSelection) {
3333  
3334              selProto.removeAllRanges = function() {
3335                  // Added try/catch as fix for issue #21
3336                  try {
3337                      this.docSelection.empty();
3338  
3339                      // Check for empty() not working (issue #24)
3340                      if (this.docSelection.type != "None") {
3341                          // Work around failure to empty a control selection by instead selecting a TextRange and then
3342                          // calling empty()
3343                          var doc;
3344                          if (this.anchorNode) {
3345                              doc = getDocument(this.anchorNode);
3346                          } else if (this.docSelection.type == CONTROL) {
3347                              var controlRange = this.docSelection.createRange();
3348                              if (controlRange.length) {
3349                                  doc = getDocument( controlRange.item(0) );
3350                              }
3351                          }
3352                          if (doc) {
3353                              var textRange = getBody(doc).createTextRange();
3354                              textRange.select();
3355                              this.docSelection.empty();
3356                          }
3357                      }
3358                  } catch(ex) {}
3359                  updateEmptySelection(this);
3360              };
3361  
3362              selProto.addRange = function(range) {
3363                  if (this.docSelection.type == CONTROL) {
3364                      addRangeToControlSelection(this, range);
3365                  } else {
3366                      api.WrappedTextRange.rangeToTextRange(range).select();
3367                      this._ranges[0] = range;
3368                      this.rangeCount = 1;
3369                      this.isCollapsed = this._ranges[0].collapsed;
3370                      updateAnchorAndFocusFromRange(this, range, false);
3371                  }
3372              };
3373  
3374              selProto.setRanges = function(ranges) {
3375                  this.removeAllRanges();
3376                  var rangeCount = ranges.length;
3377                  if (rangeCount > 1) {
3378                      createControlSelection(this, ranges);
3379                  } else if (rangeCount) {
3380                      this.addRange(ranges[0]);
3381                  }
3382              };
3383          } else {
3384              module.fail("No means of selecting a Range or TextRange was found");
3385              return false;
3386          }
3387  
3388          selProto.getRangeAt = function(index) {
3389              if (index < 0 || index >= this.rangeCount) {
3390                  throw new DOMException("INDEX_SIZE_ERR");
3391              } else {
3392                  // Clone the range to preserve selection-range independence. See issue 80.
3393                  return this._ranges[index].cloneRange();
3394              }
3395          };
3396  
3397          var refreshSelection;
3398  
3399          if (useDocumentSelection) {
3400              refreshSelection = function(sel) {
3401                  var range;
3402                  if (api.isSelectionValid(sel.win)) {
3403                      range = sel.docSelection.createRange();
3404                  } else {
3405                      range = getBody(sel.win.document).createTextRange();
3406                      range.collapse(true);
3407                  }
3408  
3409                  if (sel.docSelection.type == CONTROL) {
3410                      updateControlSelection(sel);
3411                  } else if (isTextRange(range)) {
3412                      updateFromTextRange(sel, range);
3413                  } else {
3414                      updateEmptySelection(sel);
3415                  }
3416              };
3417          } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
3418              refreshSelection = function(sel) {
3419                  if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
3420                      updateControlSelection(sel);
3421                  } else {
3422                      sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
3423                      if (sel.rangeCount) {
3424                          for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3425                              sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
3426                          }
3427                          updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
3428                          sel.isCollapsed = selectionIsCollapsed(sel);
3429                      } else {
3430                          updateEmptySelection(sel);
3431                      }
3432                  }
3433              };
3434          } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
3435              refreshSelection = function(sel) {
3436                  var range, nativeSel = sel.nativeSelection;
3437                  if (nativeSel.anchorNode) {
3438                      range = getSelectionRangeAt(nativeSel, 0);
3439                      sel._ranges = [range];
3440                      sel.rangeCount = 1;
3441                      updateAnchorAndFocusFromNativeSelection(sel);
3442                      sel.isCollapsed = selectionIsCollapsed(sel);
3443                  } else {
3444                      updateEmptySelection(sel);
3445                  }
3446              };
3447          } else {
3448              module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
3449              return false;
3450          }
3451  
3452          selProto.refresh = function(checkForChanges) {
3453              var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
3454              var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
3455  
3456              refreshSelection(this);
3457              if (checkForChanges) {
3458                  // Check the range count first
3459                  var i = oldRanges.length;
3460                  if (i != this._ranges.length) {
3461                      return true;
3462                  }
3463  
3464                  // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
3465                  // ranges after this
3466                  if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
3467                      return true;
3468                  }
3469  
3470                  // Finally, compare each range in turn
3471                  while (i--) {
3472                      if (!rangesEqual(oldRanges[i], this._ranges[i])) {
3473                          return true;
3474                      }
3475                  }
3476                  return false;
3477              }
3478          };
3479  
3480          // Removal of a single range
3481          var removeRangeManually = function(sel, range) {
3482              var ranges = sel.getAllRanges();
3483              sel.removeAllRanges();
3484              for (var i = 0, len = ranges.length; i < len; ++i) {
3485                  if (!rangesEqual(range, ranges[i])) {
3486                      sel.addRange(ranges[i]);
3487                  }
3488              }
3489              if (!sel.rangeCount) {
3490                  updateEmptySelection(sel);
3491              }
3492          };
3493  
3494          if (implementsControlRange && implementsDocSelection) {
3495              selProto.removeRange = function(range) {
3496                  if (this.docSelection.type == CONTROL) {
3497                      var controlRange = this.docSelection.createRange();
3498                      var rangeElement = getSingleElementFromRange(range);
3499  
3500                      // Create a new ControlRange containing all the elements in the selected ControlRange minus the
3501                      // element contained by the supplied range
3502                      var doc = getDocument(controlRange.item(0));
3503                      var newControlRange = getBody(doc).createControlRange();
3504                      var el, removed = false;
3505                      for (var i = 0, len = controlRange.length; i < len; ++i) {
3506                          el = controlRange.item(i);
3507                          if (el !== rangeElement || removed) {
3508                              newControlRange.add(controlRange.item(i));
3509                          } else {
3510                              removed = true;
3511                          }
3512                      }
3513                      newControlRange.select();
3514  
3515                      // Update the wrapped selection based on what's now in the native selection
3516                      updateControlSelection(this);
3517                  } else {
3518                      removeRangeManually(this, range);
3519                  }
3520              };
3521          } else {
3522              selProto.removeRange = function(range) {
3523                  removeRangeManually(this, range);
3524              };
3525          }
3526  
3527          // Detecting if a selection is backward
3528          var selectionIsBackward;
3529          if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
3530              selectionIsBackward = winSelectionIsBackward;
3531  
3532              selProto.isBackward = function() {
3533                  return selectionIsBackward(this);
3534              };
3535          } else {
3536              selectionIsBackward = selProto.isBackward = function() {
3537                  return false;
3538              };
3539          }
3540  
3541          // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
3542          selProto.isBackwards = selProto.isBackward;
3543  
3544          // Selection stringifier
3545          // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
3546          // The current spec does not yet define this method.
3547          selProto.toString = function() {
3548              var rangeTexts = [];
3549              for (var i = 0, len = this.rangeCount; i < len; ++i) {
3550                  rangeTexts[i] = "" + this._ranges[i];
3551              }
3552              return rangeTexts.join("");
3553          };
3554  
3555          function assertNodeInSameDocument(sel, node) {
3556              if (sel.win.document != getDocument(node)) {
3557                  throw new DOMException("WRONG_DOCUMENT_ERR");
3558              }
3559          }
3560  
3561          // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
3562          selProto.collapse = function(node, offset) {
3563              assertNodeInSameDocument(this, node);
3564              var range = api.createRange(node);
3565              range.collapseToPoint(node, offset);
3566              this.setSingleRange(range);
3567              this.isCollapsed = true;
3568          };
3569  
3570          selProto.collapseToStart = function() {
3571              if (this.rangeCount) {
3572                  var range = this._ranges[0];
3573                  this.collapse(range.startContainer, range.startOffset);
3574              } else {
3575                  throw new DOMException("INVALID_STATE_ERR");
3576              }
3577          };
3578  
3579          selProto.collapseToEnd = function() {
3580              if (this.rangeCount) {
3581                  var range = this._ranges[this.rangeCount - 1];
3582                  this.collapse(range.endContainer, range.endOffset);
3583              } else {
3584                  throw new DOMException("INVALID_STATE_ERR");
3585              }
3586          };
3587  
3588          // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
3589          // specified so the native implementation is never used by Rangy.
3590          selProto.selectAllChildren = function(node) {
3591              assertNodeInSameDocument(this, node);
3592              var range = api.createRange(node);
3593              range.selectNodeContents(node);
3594              this.setSingleRange(range);
3595          };
3596  
3597          selProto.deleteFromDocument = function() {
3598              // Sepcial behaviour required for IE's control selections
3599              if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3600                  var controlRange = this.docSelection.createRange();
3601                  var element;
3602                  while (controlRange.length) {
3603                      element = controlRange.item(0);
3604                      controlRange.remove(element);
3605                      dom.removeNode(element);
3606                  }
3607                  this.refresh();
3608              } else if (this.rangeCount) {
3609                  var ranges = this.getAllRanges();
3610                  if (ranges.length) {
3611                      this.removeAllRanges();
3612                      for (var i = 0, len = ranges.length; i < len; ++i) {
3613                          ranges[i].deleteContents();
3614                      }
3615                      // The spec says nothing about what the selection should contain after calling deleteContents on each
3616                      // range. Firefox moves the selection to where the final selected range was, so we emulate that
3617                      this.addRange(ranges[len - 1]);
3618                  }
3619              }
3620          };
3621  
3622          // The following are non-standard extensions
3623          selProto.eachRange = function(func, returnValue) {
3624              for (var i = 0, len = this._ranges.length; i < len; ++i) {
3625                  if ( func( this.getRangeAt(i) ) ) {
3626                      return returnValue;
3627                  }
3628              }
3629          };
3630  
3631          selProto.getAllRanges = function() {
3632              var ranges = [];
3633              this.eachRange(function(range) {
3634                  ranges.push(range);
3635              });
3636              return ranges;
3637          };
3638  
3639          selProto.setSingleRange = function(range, direction) {
3640              this.removeAllRanges();
3641              this.addRange(range, direction);
3642          };
3643  
3644          selProto.callMethodOnEachRange = function(methodName, params) {
3645              var results = [];
3646              this.eachRange( function(range) {
3647                  results.push( range[methodName].apply(range, params || []) );
3648              } );
3649              return results;
3650          };
3651  
3652          function createStartOrEndSetter(isStart) {
3653              return function(node, offset) {
3654                  var range;
3655                  if (this.rangeCount) {
3656                      range = this.getRangeAt(0);
3657                      range["set" + (isStart ? "Start" : "End")](node, offset);
3658                  } else {
3659                      range = api.createRange(this.win.document);
3660                      range.setStartAndEnd(node, offset);
3661                  }
3662                  this.setSingleRange(range, this.isBackward());
3663              };
3664          }
3665  
3666          selProto.setStart = createStartOrEndSetter(true);
3667          selProto.setEnd = createStartOrEndSetter(false);
3668  
3669          // Add select() method to Range prototype. Any existing selection will be removed.
3670          api.rangePrototype.select = function(direction) {
3671              getSelection( this.getDocument() ).setSingleRange(this, direction);
3672          };
3673  
3674          selProto.changeEachRange = function(func) {
3675              var ranges = [];
3676              var backward = this.isBackward();
3677  
3678              this.eachRange(function(range) {
3679                  func(range);
3680                  ranges.push(range);
3681              });
3682  
3683              this.removeAllRanges();
3684              if (backward && ranges.length == 1) {
3685                  this.addRange(ranges[0], "backward");
3686              } else {
3687                  this.setRanges(ranges);
3688              }
3689          };
3690  
3691          selProto.containsNode = function(node, allowPartial) {
3692              return this.eachRange( function(range) {
3693                  return range.containsNode(node, allowPartial);
3694              }, true ) || false;
3695          };
3696  
3697          selProto.getBookmark = function(containerNode) {
3698              return {
3699                  backward: this.isBackward(),
3700                  rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
3701              };
3702          };
3703  
3704          selProto.moveToBookmark = function(bookmark) {
3705              var selRanges = [];
3706              for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
3707                  range = api.createRange(this.win);
3708                  range.moveToBookmark(rangeBookmark);
3709                  selRanges.push(range);
3710              }
3711              if (bookmark.backward) {
3712                  this.setSingleRange(selRanges[0], "backward");
3713              } else {
3714                  this.setRanges(selRanges);
3715              }
3716          };
3717  
3718          selProto.saveRanges = function() {
3719              return {
3720                  backward: this.isBackward(),
3721                  ranges: this.callMethodOnEachRange("cloneRange")
3722              };
3723          };
3724  
3725          selProto.restoreRanges = function(selRanges) {
3726              this.removeAllRanges();
3727              for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
3728                  this.addRange(range, (selRanges.backward && i == 0));
3729              }
3730          };
3731  
3732          selProto.toHtml = function() {
3733              var rangeHtmls = [];
3734              this.eachRange(function(range) {
3735                  rangeHtmls.push( DomRange.toHtml(range) );
3736              });
3737              return rangeHtmls.join("");
3738          };
3739  
3740          if (features.implementsTextRange) {
3741              selProto.getNativeTextRange = function() {
3742                  var sel, textRange;
3743                  if ( (sel = this.docSelection) ) {
3744                      var range = sel.createRange();
3745                      if (isTextRange(range)) {
3746                          return range;
3747                      } else {
3748                          throw module.createError("getNativeTextRange: selection is a control selection");
3749                      }
3750                  } else if (this.rangeCount > 0) {
3751                      return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
3752                  } else {
3753                      throw module.createError("getNativeTextRange: selection contains no range");
3754                  }
3755              };
3756          }
3757  
3758          function inspect(sel) {
3759              var rangeInspects = [];
3760              var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
3761              var focus = new DomPosition(sel.focusNode, sel.focusOffset);
3762              var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
3763  
3764              if (typeof sel.rangeCount != "undefined") {
3765                  for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3766                      rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
3767                  }
3768              }
3769              return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
3770                      ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
3771          }
3772  
3773          selProto.getName = function() {
3774              return "WrappedSelection";
3775          };
3776  
3777          selProto.inspect = function() {
3778              return inspect(this);
3779          };
3780  
3781          selProto.detach = function() {
3782              actOnCachedSelection(this.win, "delete");
3783              deleteProperties(this);
3784          };
3785  
3786          WrappedSelection.detachAll = function() {
3787              actOnCachedSelection(null, "deleteAll");
3788          };
3789  
3790          WrappedSelection.inspect = inspect;
3791          WrappedSelection.isDirectionBackward = isDirectionBackward;
3792  
3793          api.Selection = WrappedSelection;
3794  
3795          api.selectionPrototype = selProto;
3796  
3797          api.addShimListener(function(win) {
3798              if (typeof win.getSelection == "undefined") {
3799                  win.getSelection = function() {
3800                      return getSelection(win);
3801                  };
3802              }
3803              win = null;
3804          });
3805      });
3806      
3807  
3808      /*----------------------------------------------------------------------------------------------------------------*/

3809  
3810      // Wait for document to load before initializing

3811      var docReady = false;
3812  
3813      var loadHandler = function(e) {
3814          if (!docReady) {
3815              docReady = true;
3816              if (!api.initialized && api.config.autoInitialize) {
3817                  init();
3818              }
3819          }
3820      };
3821  
3822      if (isBrowser) {
3823          // Test whether the document has already been loaded and initialize immediately if so

3824          if (document.readyState == "complete") {
3825              loadHandler();
3826          } else {
3827              if (isHostMethod(document, "addEventListener")) {
3828                  document.addEventListener("DOMContentLoaded", loadHandler, false);
3829              }
3830  
3831              // Add a fallback in case the DOMContentLoaded event isn't supported

3832              addListener(window, "load", loadHandler);
3833          }
3834      }
3835  
3836      return api;
3837  }, this);


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1