text: // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
  // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
  rangy.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
      api.config.checkSelectionRanges = true;
  
      var BOOLEAN = "boolean";
      var NUMBER = "number";
      var dom = api.dom;
      var util = api.util;
      var isHostMethod = util.isHostMethod;
      var DomRange = api.DomRange;
      var WrappedRange = api.WrappedRange;
      var DOMException = api.DOMException;
      var DomPosition = dom.DomPosition;
      var getNativeSelection;
      var selectionIsCollapsed;
      var features = api.features;
      var CONTROL = "Control";
      var getDocument = dom.getDocument;
      var getBody = dom.getBody;
      var rangesEqual = DomRange.rangesEqual;
  
      var log = log4javascript.getLogger("rangy.WrappedSelection");
  
      // Utility function to support direction parameters in the API that may be a string ("backward" or "forward") or a
      // Boolean (true for backwards).
      function isDirectionBackward(dir) {
          return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
      }
  
      function getWindow(win, methodName) {
          if (!win) {
              return window;
          } else if (dom.isWindow(win)) {
              return win;
          } else if (win instanceof WrappedSelection) {
              return win.win;
          } else {
              var doc = dom.getContentDocument(win, module, methodName);
              return dom.getWindow(doc);
          }
      }
  
      function getWinSelection(winParam) {
          return getWindow(winParam, "getWinSelection").getSelection();
      }
  
      function getDocSelection(winParam) {
          return getWindow(winParam, "getDocSelection").document.selection;
      }
      
      function winSelectionIsBackward(sel) {
          var backward = false;
          if (sel.anchorNode) {
              backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
          }
          return backward;
      }
  
      // Test for the Range/TextRange and Selection features required
      // Test for ability to retrieve selection
      var implementsWinGetSelection = isHostMethod(window, "getSelection"),
          implementsDocSelection = util.isHostObject(document, "selection");
  
      features.implementsWinGetSelection = implementsWinGetSelection;
      features.implementsDocSelection = implementsDocSelection;
  
      var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
  
      if (useDocumentSelection) {
          getNativeSelection = getDocSelection;
          api.isSelectionValid = function(winParam) {
              var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
  
              // Check whether the selection TextRange is actually contained within the correct document
              return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
          };
      } else if (implementsWinGetSelection) {
          getNativeSelection = getWinSelection;
          api.isSelectionValid = function() {
              return true;
          };
      } else {
          module.fail("Neither document.selection or window.getSelection() detected.");
      }
  
      api.getNativeSelection = getNativeSelection;
  
      var testSelection = getNativeSelection();
      var testRange = api.createNativeRange(document);
      var body = getBody(document);
  
      // Obtaining a range from a selection
      var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
          ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
  
      features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
  
      // Test for existence of native selection extend() method
      var selectionHasExtend = isHostMethod(testSelection, "extend");
      features.selectionHasExtend = selectionHasExtend;
      
      // Test if rangeCount exists
      var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
      features.selectionHasRangeCount = selectionHasRangeCount;
  
      var selectionSupportsMultipleRanges = false;
      var collapsedNonEditableSelectionsSupported = true;
  
      var addRangeBackwardToNative = selectionHasExtend ?
          function(nativeSelection, range) {
              var doc = DomRange.getRangeDocument(range);
              var endRange = api.createRange(doc);
              endRange.collapseToPoint(range.endContainer, range.endOffset);
              nativeSelection.addRange(getNativeRange(endRange));
              nativeSelection.extend(range.startContainer, range.startOffset);
          } : null;
  
      if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
              typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
  
          (function() {
              // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
              // performed on the current document's selection. See issue 109.
  
              // Note also that if a selection previously existed, it is wiped by these tests. This should usually be fine
              // because initialization usually happens when the document loads, but could be a problem for a script that
              // loads and initializes Rangy later. If anyone complains, code could be added to save and restore the
              // selection.
              var sel = window.getSelection();
              if (sel) {
                  // Store the current selection
                  var originalSelectionRangeCount = sel.rangeCount;
                  var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
                  var originalSelectionRanges = [];
                  var originalSelectionBackward = winSelectionIsBackward(sel); 
                  for (var i = 0; i < originalSelectionRangeCount; ++i) {
                      originalSelectionRanges[i] = sel.getRangeAt(i);
                  }
                  
                  // Create some test elements
                  var body = getBody(document);
                  var testEl = body.appendChild( document.createElement("div") );
                  testEl.contentEditable = "false";
                  var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
  
                  // Test whether the native selection will allow a collapsed selection within a non-editable element
                  var r1 = document.createRange();
  
                  r1.setStart(textNode, 1);
                  r1.collapse(true);
                  sel.addRange(r1);
                  collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
                  sel.removeAllRanges();
  
                  // Test whether the native selection is capable of supporting multiple ranges
                  if (!selectionHasMultipleRanges) {
                      var r2 = r1.cloneRange();
                      r1.setStart(textNode, 0);
                      r2.setEnd(textNode, 3);
                      r2.setStart(textNode, 2);
                      sel.addRange(r1);
                      sel.addRange(r2);
  
                      selectionSupportsMultipleRanges = (sel.rangeCount == 2);
                      r2.detach();
                  }
  
                  // Clean up
                  body.removeChild(testEl);
                  sel.removeAllRanges();
                  r1.detach();
  
                  for (i = 0; i < originalSelectionRangeCount; ++i) {
                      if (i == 0 && originalSelectionBackward) {
                          if (addRangeBackwardToNative) {
                              addRangeBackwardToNative(sel, originalSelectionRanges[i]);
                          } else {
                              api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because browser does not support Selection.extend");
                              sel.addRange(originalSelectionRanges[i])
                          }
                      } else {
                          sel.addRange(originalSelectionRanges[i])
                      }
                  }
              }
          })();
      }
  
      features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
      features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
  
      // ControlRanges
      var implementsControlRange = false, testControlRange;
  
      if (body && isHostMethod(body, "createControlRange")) {
          testControlRange = body.createControlRange();
          if (util.areHostProperties(testControlRange, ["item", "add"])) {
              implementsControlRange = true;
          }
      }
      features.implementsControlRange = implementsControlRange;
  
      // Selection collapsedness
      if (selectionHasAnchorAndFocus) {
          selectionIsCollapsed = function(sel) {
              return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
          };
      } else {
          selectionIsCollapsed = function(sel) {
              return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
          };
      }
  
      function updateAnchorAndFocusFromRange(sel, range, backward) {
          var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
          sel.anchorNode = range[anchorPrefix + "Container"];
          sel.anchorOffset = range[anchorPrefix + "Offset"];
          sel.focusNode = range[focusPrefix + "Container"];
          sel.focusOffset = range[focusPrefix + "Offset"];
      }
  
      function updateAnchorAndFocusFromNativeSelection(sel) {
          var nativeSel = sel.nativeSelection;
          sel.anchorNode = nativeSel.anchorNode;
          sel.anchorOffset = nativeSel.anchorOffset;
          sel.focusNode = nativeSel.focusNode;
          sel.focusOffset = nativeSel.focusOffset;
      }
  
      function updateEmptySelection(sel) {
          sel.anchorNode = sel.focusNode = null;
          sel.anchorOffset = sel.focusOffset = 0;
          sel.rangeCount = 0;
          sel.isCollapsed = true;
          sel._ranges.length = 0;
      }
  
      function getNativeRange(range) {
          var nativeRange;
          if (range instanceof DomRange) {
              nativeRange = api.createNativeRange(range.getDocument());
              nativeRange.setEnd(range.endContainer, range.endOffset);
              nativeRange.setStart(range.startContainer, range.startOffset);
          } else if (range instanceof WrappedRange) {
              nativeRange = range.nativeRange;
          } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
              nativeRange = range;
          }
          return nativeRange;
      }
  
      function rangeContainsSingleElement(rangeNodes) {
          if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
              return false;
          }
          for (var i = 1, len = rangeNodes.length; i < len; ++i) {
              if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
                  return false;
              }
          }
          return true;
      }
  
      function getSingleElementFromRange(range) {
          var nodes = range.getNodes();
          if (!rangeContainsSingleElement(nodes)) {
              throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
          }
          return nodes[0];
      }
  
      // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
      function isTextRange(range) {
          return !!range && typeof range.text != "undefined";
      }
  
      function updateFromTextRange(sel, range) {
          // Create a Range from the selected TextRange
          var wrappedRange = new WrappedRange(range);
          sel._ranges = [wrappedRange];
  
          updateAnchorAndFocusFromRange(sel, wrappedRange, false);
          sel.rangeCount = 1;
          sel.isCollapsed = wrappedRange.collapsed;
      }
  
      function updateControlSelection(sel) {
          // Update the wrapped selection based on what's now in the native selection
          sel._ranges.length = 0;
          if (sel.docSelection.type == "None") {
              updateEmptySelection(sel);
          } else {
              var controlRange = sel.docSelection.createRange();
              if (isTextRange(controlRange)) {
                  // This case (where the selection type is "Control" and calling createRange() on the selection returns
                  // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
                  // ControlRange have been removed from the ControlRange and removed from the document.
                  updateFromTextRange(sel, controlRange);
              } else {
                  sel.rangeCount = controlRange.length;
                  var range, doc = getDocument(controlRange.item(0));
                  for (var i = 0; i < sel.rangeCount; ++i) {
                      range = api.createRange(doc);
                      range.selectNode(controlRange.item(i));
                      sel._ranges.push(range);
                  }
                  sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
                  updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
              }
          }
      }
  
      function addRangeToControlSelection(sel, range) {
          var controlRange = sel.docSelection.createRange();
          var rangeElement = getSingleElementFromRange(range);
  
          // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
          // contained by the supplied range
          var doc = getDocument(controlRange.item(0));
          var newControlRange = getBody(doc).createControlRange();
          for (var i = 0, len = controlRange.length; i < len; ++i) {
              newControlRange.add(controlRange.item(i));
          }
          try {
              newControlRange.add(rangeElement);
          } catch (ex) {
              throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
          }
          newControlRange.select();
  
          // Update the wrapped selection based on what's now in the native selection
          updateControlSelection(sel);
      }
  
      var getSelectionRangeAt;
  
      if (isHostMethod(testSelection, "getRangeAt")) {
          // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
          // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
          // lesson to us all, especially me.
          getSelectionRangeAt = function(sel, index) {
              try {
                  return sel.getRangeAt(index);
              } catch (ex) {
                  return null;
              }
          };
      } else if (selectionHasAnchorAndFocus) {
          getSelectionRangeAt = function(sel) {
              var doc = getDocument(sel.anchorNode);
              var range = api.createRange(doc);
              range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
  
              // Handle the case when the selection was selected backwards (from the end to the start in the
              // document)
              if (range.collapsed !== this.isCollapsed) {
                  range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
              }
  
              return range;
          };
      }
  
      function WrappedSelection(selection, docSelection, win) {
          this.nativeSelection = selection;
          this.docSelection = docSelection;
          this._ranges = [];
          this.win = win;
          this.refresh();
      }
  
      WrappedSelection.prototype = api.selectionPrototype;
  
      function deleteProperties(sel) {
          sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
          sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
          sel.detached = true;
      }
  
      var cachedRangySelections = [];
  
      function actOnCachedSelection(win, action) {
          var i = cachedRangySelections.length, cached, sel;
          while (i--) {
              cached = cachedRangySelections[i];
              sel = cached.selection;
              if (action == "deleteAll") {
                  deleteProperties(sel);
              } else if (cached.win == win) {
                  if (action == "delete") {
                      cachedRangySelections.splice(i, 1);
                      return true;
                  } else {
                      return sel;
                  }
              }
          }
          if (action == "deleteAll") {
              cachedRangySelections.length = 0;
          }
          return null;
      }
  
      var getSelection = function(win) {
          // Check if the parameter is a Rangy Selection object
          if (win && win instanceof WrappedSelection) {
              win.refresh();
              return win;
          }
  
          win = getWindow(win, "getNativeSelection");
  
          var sel = actOnCachedSelection(win);
          var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
          if (sel) {
              sel.nativeSelection = nativeSel;
              sel.docSelection = docSel;
              sel.refresh();
          } else {
              sel = new WrappedSelection(nativeSel, docSel, win);
              cachedRangySelections.push( { win: win, selection: sel } );
          }
          return sel;
      };
  
      api.getSelection = getSelection;
  
      api.getIframeSelection = function(iframeEl) {
          module.deprecationNotice("getIframeSelection()", "getSelection(iframeEl)");
          return api.getSelection(dom.getIframeWindow(iframeEl));
      };
  
      var selProto = WrappedSelection.prototype;
  
      function createControlSelection(sel, ranges) {
          // Ensure that the selection becomes of type "Control"
          var doc = getDocument(ranges[0].startContainer);
          var controlRange = getBody(doc).createControlRange();
          for (var i = 0, el, len = ranges.length; i < len; ++i) {
              el = getSingleElementFromRange(ranges[i]);
              try {
                  controlRange.add(el);
              } catch (ex) {
                  throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
              }
          }
          controlRange.select();
  
          // Update the wrapped selection based on what's now in the native selection
          updateControlSelection(sel);
      }
  
      // Selecting a range
      if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
          selProto.removeAllRanges = function() {
              this.nativeSelection.removeAllRanges();
              updateEmptySelection(this);
          };
  
          var addRangeBackward = function(sel, range) {
              addRangeBackwardToNative(sel.nativeSelection, range);
              sel.refresh();
          };
  
          if (selectionHasRangeCount) {
              selProto.addRange = function(range, direction) {
                  if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
                      addRangeToControlSelection(this, range);
                  } else {
                      if (isDirectionBackward(direction) && selectionHasExtend) {
                          addRangeBackward(this, range);
                      } else {
                          var previousRangeCount;
                          if (selectionSupportsMultipleRanges) {
                              previousRangeCount = this.rangeCount;
                          } else {
                              this.removeAllRanges();
                              previousRangeCount = 0;
                          }
                          // Clone the native range so that changing the selected range does not affect the selection.
                          // This is contrary to the spec but is the only way to achieve consistency between browsers. See
                          // issue 80.
                          this.nativeSelection.addRange(getNativeRange(range).cloneRange());
  
                          // Check whether adding the range was successful
                          this.rangeCount = this.nativeSelection.rangeCount;
  
                          if (this.rangeCount == previousRangeCount + 1) {
                              // The range was added successfully
  
                              // Check whether the range that we added to the selection is reflected in the last range extracted from
                              // the selection
                              if (api.config.checkSelectionRanges) {
                                  var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
                                  if (nativeRange && !rangesEqual(nativeRange, range)) {
                                      // Happens in WebKit with, for example, a selection placed at the start of a text node
                                      range = new WrappedRange(nativeRange);
                                  }
                              }
                              this._ranges[this.rangeCount - 1] = range;
                              updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
                              this.isCollapsed = selectionIsCollapsed(this);
                          } else {
                              // The range was not added successfully. The simplest thing is to refresh
                              this.refresh();
                          }
                      }
                  }
              };
          } else {
              selProto.addRange = function(range, direction) {
                  if (isDirectionBackward(direction) && selectionHasExtend) {
                      addRangeBackward(this, range);
                  } else {
                      this.nativeSelection.addRange(getNativeRange(range));
                      this.refresh();
                  }
              };
          }
  
          selProto.setRanges = function(ranges) {
              if (implementsControlRange && ranges.length > 1) {
                  createControlSelection(this, ranges);
              } else {
                  this.removeAllRanges();
                  for (var i = 0, len = ranges.length; i < len; ++i) {
                      this.addRange(ranges[i]);
                  }
              }
          };
      } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
                 implementsControlRange && useDocumentSelection) {
  
          selProto.removeAllRanges = function() {
              // Added try/catch as fix for issue #21
              try {
                  this.docSelection.empty();
  
                  // Check for empty() not working (issue #24)
                  if (this.docSelection.type != "None") {
                      // Work around failure to empty a control selection by instead selecting a TextRange and then
                      // calling empty()
                      var doc;
                      if (this.anchorNode) {
                          doc = getDocument(this.anchorNode);
                      } else if (this.docSelection.type == CONTROL) {
                          var controlRange = this.docSelection.createRange();
                          if (controlRange.length) {
                              doc = getDocument( controlRange.item(0) );
                          }
                      }
                      if (doc) {
                          var textRange = getBody(doc).createTextRange();
                          textRange.select();
                          this.docSelection.empty();
                      }
                  }
              } catch(ex) {}
              updateEmptySelection(this);
          };
  
          selProto.addRange = function(range) {
              if (this.docSelection.type == CONTROL) {
                  addRangeToControlSelection(this, range);
              } else {
                  api.WrappedTextRange.rangeToTextRange(range).select();
                  this._ranges[0] = range;
                  this.rangeCount = 1;
                  this.isCollapsed = this._ranges[0].collapsed;
                  updateAnchorAndFocusFromRange(this, range, false);
              }
          };
  
          selProto.setRanges = function(ranges) {
              this.removeAllRanges();
              var rangeCount = ranges.length;
              if (rangeCount > 1) {
                  createControlSelection(this, ranges);
              } else if (rangeCount) {
                  this.addRange(ranges[0]);
              }
          };
      } else {
          module.fail("No means of selecting a Range or TextRange was found");
          return false;
      }
  
      selProto.getRangeAt = function(index) {
          if (index < 0 || index >= this.rangeCount) {
              throw new DOMException("INDEX_SIZE_ERR");
          } else {
              // Clone the range to preserve selection-range independence. See issue 80.
              return this._ranges[index].cloneRange();
          }
      };
  
      var refreshSelection;
  
      if (useDocumentSelection) {
          refreshSelection = function(sel) {
              var range;
              if (api.isSelectionValid(sel.win)) {
                  range = sel.docSelection.createRange();
              } else {
                  range = getBody(sel.win.document).createTextRange();
                  range.collapse(true);
              }
              log.warn("selection refresh called, selection type: " + sel.docSelection.type);
  
              if (sel.docSelection.type == CONTROL) {
                  updateControlSelection(sel);
              } else if (isTextRange(range)) {
                  updateFromTextRange(sel, range);
              } else {
                  updateEmptySelection(sel);
              }
          };
      } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
          refreshSelection = function(sel) {
              if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
                  updateControlSelection(sel);
              } else {
                  sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
                  if (sel.rangeCount) {
                      for (var i = 0, len = sel.rangeCount; i < len; ++i) {
                          sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
                      }
                      updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
                      sel.isCollapsed = selectionIsCollapsed(sel);
                  } else {
                      updateEmptySelection(sel);
                  }
              }
          };
      } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
          refreshSelection = function(sel) {
              var range, nativeSel = sel.nativeSelection;
              if (nativeSel.anchorNode) {
                  range = getSelectionRangeAt(nativeSel, 0);
                  sel._ranges = [range];
                  sel.rangeCount = 1;
                  updateAnchorAndFocusFromNativeSelection(sel);
                  sel.isCollapsed = selectionIsCollapsed(sel);
              } else {
                  updateEmptySelection(sel);
              }
          };
      } else {
          module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
          return false;
      }
  
      selProto.refresh = function(checkForChanges) {
          var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
          var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
  
          refreshSelection(this);
          if (checkForChanges) {
              // Check the range count first
              var i = oldRanges.length;
              if (i != this._ranges.length) {
                  log.debug("Selection.refresh: Range count has changed: was " + i + ", is now " + this._ranges.length);
                  return true;
              }
  
              // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
              // ranges after this
              if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
                  log.debug("Selection.refresh: anchor different, so selection has changed");
                  return true;
              }
  
              // Finally, compare each range in turn
              while (i--) {
                  if (!rangesEqual(oldRanges[i], this._ranges[i])) {
                      log.debug("Selection.refresh: Range at index " + i + " has changed: was " + oldRanges[i].inspect() + ", is now " + this._ranges[i].inspect());
                      return true;
                  }
              }
              return false;
          }
      };
  
      // Removal of a single range
      var removeRangeManually = function(sel, range) {
          var ranges = sel.getAllRanges();
          sel.removeAllRanges();
          for (var i = 0, len = ranges.length; i < len; ++i) {
              if (!rangesEqual(range, ranges[i])) {
                  sel.addRange(ranges[i]);
              }
          }
          if (!sel.rangeCount) {
              updateEmptySelection(sel);
          }
      };
  
      if (implementsControlRange) {
          selProto.removeRange = function(range) {
              if (this.docSelection.type == CONTROL) {
                  var controlRange = this.docSelection.createRange();
                  var rangeElement = getSingleElementFromRange(range);
  
                  // Create a new ControlRange containing all the elements in the selected ControlRange minus the
                  // element contained by the supplied range
                  var doc = getDocument(controlRange.item(0));
                  var newControlRange = getBody(doc).createControlRange();
                  var el, removed = false;
                  for (var i = 0, len = controlRange.length; i < len; ++i) {
                      el = controlRange.item(i);
                      if (el !== rangeElement || removed) {
                          newControlRange.add(controlRange.item(i));
                      } else {
                          removed = true;
                      }
                  }
                  newControlRange.select();
  
                  // Update the wrapped selection based on what's now in the native selection
                  updateControlSelection(this);
              } else {
                  removeRangeManually(this, range);
              }
          };
      } else {
          selProto.removeRange = function(range) {
              removeRangeManually(this, range);
          };
      }
  
      // Detecting if a selection is backward
      var selectionIsBackward;
      if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
          selectionIsBackward = winSelectionIsBackward;
  
          selProto.isBackward = function() {
              return selectionIsBackward(this);
          };
      } else {
          selectionIsBackward = selProto.isBackward = function() {
              return false;
          };
      }
  
      // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
      selProto.isBackwards = selProto.isBackward;
  
      // Selection stringifier
      // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
      // The current spec does not yet define this method.
      selProto.toString = function() {
          log.debug("selection toString called");
          var rangeTexts = [];
          for (var i = 0, len = this.rangeCount; i < len; ++i) {
              rangeTexts[i] = "" + this._ranges[i];
          }
          return rangeTexts.join("");
      };
  
      function assertNodeInSameDocument(sel, node) {
          if (sel.win.document != getDocument(node)) {
              throw new DOMException("WRONG_DOCUMENT_ERR");
          }
      }
  
      // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
      selProto.collapse = function(node, offset) {
          assertNodeInSameDocument(this, node);
          var range = api.createRange(node);
          range.collapseToPoint(node, offset);
          this.setSingleRange(range);
          this.isCollapsed = true;
      };
  
      selProto.collapseToStart = function() {
          if (this.rangeCount) {
              var range = this._ranges[0];
              this.collapse(range.startContainer, range.startOffset);
          } else {
              throw new DOMException("INVALID_STATE_ERR");
          }
      };
  
      selProto.collapseToEnd = function() {
          if (this.rangeCount) {
              var range = this._ranges[this.rangeCount - 1];
              this.collapse(range.endContainer, range.endOffset);
          } else {
              throw new DOMException("INVALID_STATE_ERR");
          }
      };
  
      // The spec is very specific on how selectAllChildren should be implemented so the native implementation is
      // never used by Rangy.
      selProto.selectAllChildren = function(node) {
          assertNodeInSameDocument(this, node);
          var range = api.createRange(node);
          range.selectNodeContents(node);
          this.setSingleRange(range);
      };
  
      selProto.deleteFromDocument = function() {
          // Sepcial behaviour required for IE's control selections
          if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
              var controlRange = this.docSelection.createRange();
              var element;
              while (controlRange.length) {
                  element = controlRange.item(0);
                  controlRange.remove(element);
                  element.parentNode.removeChild(element);
              }
              this.refresh();
          } else if (this.rangeCount) {
              var ranges = this.getAllRanges();
              if (ranges.length) {
                  this.removeAllRanges();
                  for (var i = 0, len = ranges.length; i < len; ++i) {
                      ranges[i].deleteContents();
                  }
                  // The spec says nothing about what the selection should contain after calling deleteContents on each
                  // range. Firefox moves the selection to where the final selected range was, so we emulate that
                  this.addRange(ranges[len - 1]);
              }
          }
      };
  
      // The following are non-standard extensions
      selProto.eachRange = function(func, returnValue) {
          for (var i = 0, len = this._ranges.length; i < len; ++i) {
              if ( func( this.getRangeAt(i) ) ) {
                  return returnValue;
              }
          }
      };
  
      selProto.getAllRanges = function() {
          var ranges = [];
          this.eachRange(function(range) {
              ranges.push(range);
          });
          return ranges;
      };
  
      selProto.setSingleRange = function(range, direction) {
          this.removeAllRanges();
          this.addRange(range, direction);
      };
  
      selProto.callMethodOnEachRange = function(methodName, params) {
          var results = [];
          this.eachRange( function(range) {
              results.push( range[methodName].apply(range, params) );
          } );
          return results;
      };
      
      function createStartOrEndSetter(isStart) {
          return function(node, offset) {
              var range;
              if (this.rangeCount) {
                  range = this.getRangeAt(0);
                  range["set" + (isStart ? "Start" : "End")](node, offset);
              } else {
                  range = api.createRange(this.win.document);
                  range.setStartAndEnd(node, offset);
              }
              this.setSingleRange(range, this.isBackward());
          };
      }
  
      selProto.setStart = createStartOrEndSetter(true);
      selProto.setEnd = createStartOrEndSetter(false);
      
      // Add select() method to Range prototype. Any existing selection will be removed.
      api.rangePrototype.select = function(direction) {
          getSelection( this.getDocument() ).setSingleRange(this, direction);
      };
  
      selProto.changeEachRange = function(func) {
          var ranges = [];
          var backward = this.isBackward();
  
          this.eachRange(function(range) {
              func(range);
              ranges.push(range);
          });
  
          this.removeAllRanges();
          if (backward && ranges.length == 1) {
              this.addRange(ranges[0], "backward");
          } else {
              this.setRanges(ranges);
          }
      };
  
      selProto.containsNode = function(node, allowPartial) {
          return this.eachRange( function(range) {
              return range.containsNode(node, allowPartial);
          }, true );
      };
  
      selProto.getBookmark = function(containerNode) {
          return {
              backward: this.isBackward(),
              rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
          };
      };
  
      selProto.moveToBookmark = function(bookmark) {
          var selRanges = [];
          for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
              range = api.createRange(this.win);
              range.moveToBookmark(rangeBookmark);
              selRanges.push(range);
          }
          if (bookmark.backward) {
              this.setSingleRange(selRanges[0], "backward");
          } else {
              this.setRanges(selRanges);
          }
      };
  
      selProto.toHtml = function() {
          return this.callMethodOnEachRange("toHtml").join("");
      };
  
      function inspect(sel) {
          var rangeInspects = [];
          var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
          var focus = new DomPosition(sel.focusNode, sel.focusOffset);
          var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
  
          if (typeof sel.rangeCount != "undefined") {
              for (var i = 0, len = sel.rangeCount; i < len; ++i) {
                  rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
              }
          }
          return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
                  ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
      }
  
      selProto.getName = function() {
          return "WrappedSelection";
      };
  
      selProto.inspect = function() {
          return inspect(this);
      };
  
      selProto.detach = function() {
          actOnCachedSelection(this.win, "delete");
          deleteProperties(this);
      };
  
      WrappedSelection.detachAll = function() {
          actOnCachedSelection(null, "deleteAll");
      };
  
      WrappedSelection.inspect = inspect;
      WrappedSelection.isDirectionBackward = isDirectionBackward;
  
      api.Selection = WrappedSelection;
  
      api.selectionPrototype = selProto;
  
      api.addCreateMissingNativeApiListener(function(win) {
          if (typeof win.getSelection == "undefined") {
              win.getSelection = function() {
                  return getSelection(win);
              };
          }
          win = null;
      });
  });
  