Bug 858206 - Part 1: Swap start and end monocles when they overlap. r=jimm
authorAleh Zasypkin <aleh.zasypkin@gmail.com>
Wed, 05 Mar 2014 12:41:14 +0100
changeset 191345 3c9c98992742be0349f65c340fdf53057b2c4fa1
parent 191344 c6fc27e68f455521abf328b28555c1ed97556160
child 191346 1b636faa318773541a796c0081f641cbd03ca8bf
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimm
bugs858206
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 858206 - Part 1: Swap start and end monocles when they overlap. r=jimm
browser/metro/base/content/contenthandlers/SelectionHandler.js
browser/metro/base/content/helperui/ChromeSelectionHandler.js
browser/metro/base/content/helperui/SelectionHelperUI.js
browser/metro/base/content/library/SelectionPrototype.js
--- a/browser/metro/base/content/contenthandlers/SelectionHandler.js
+++ b/browser/metro/base/content/contenthandlers/SelectionHandler.js
@@ -173,49 +173,34 @@ var SelectionHandler = {
       return;
     }
 
     if (!this._selectionMoveActive) {
       this._onFail("mouse isn't down for drag move?");
       return;
     }
 
-    // Update selection in the doc
-    let pos = null;
-    if (aMsg.change == "start") {
-      pos = aMsg.start;
-    } else {
-      pos = aMsg.end;
-    }
-    this._handleSelectionPoint(aMsg.change, pos, false);
+    this._handleSelectionPoint(aMsg, false);
   },
 
   /*
    * Selection monocle move finished event handler
    */
   _onSelectionMoveEnd: function _onSelectionMoveComplete(aMsg) {
     if (!this._contentWindow) {
       this._onFail("_onSelectionMove was called without proper view set up");
       return;
     }
 
     if (!this._selectionMoveActive) {
       this._onFail("mouse isn't down for drag move?");
       return;
     }
 
-    // Update selection in the doc
-    let pos = null;
-    if (aMsg.change == "start") {
-      pos = aMsg.start;
-    } else {
-      pos = aMsg.end;
-    }
-
-    this._handleSelectionPoint(aMsg.change, pos, true);
+    this._handleSelectionPoint(aMsg, true);
     this._selectionMoveActive = false;
     
     // _handleSelectionPoint may set a scroll timer, so this must
     // be reset after the last call.
     this._clearTimers();
 
     // Update the position of our selection monocles
     this._updateSelectionUI("end", true, true);
--- a/browser/metro/base/content/helperui/ChromeSelectionHandler.js
+++ b/browser/metro/base/content/helperui/ChromeSelectionHandler.js
@@ -107,49 +107,34 @@ var ChromeSelectionHandler = {
       return;
     }
 
     if (!this._selectionMoveActive) {
       this._onFail("mouse isn't down for drag move?");
       return;
     }
 
-    // Update selection in the doc
-    let pos = null;
-    if (aMsg.change == "start") {
-      pos = aMsg.start;
-    } else {
-      pos = aMsg.end;
-    }
-    this._handleSelectionPoint(aMsg.change, pos, false);
+    this._handleSelectionPoint(aMsg, false);
   },
 
   /*
    * Selection monocle move finished event handler
    */
   _onSelectionMoveEnd: function _onSelectionMoveComplete(aMsg) {
     if (!this.targetIsEditable) {
       this._onFail("_onSelectionMoveEnd with bad targetElement.");
       return;
     }
 
     if (!this._selectionMoveActive) {
       this._onFail("mouse isn't down for drag move?");
       return;
     }
 
-    // Update selection in the doc
-    let pos = null;
-    if (aMsg.change == "start") {
-      pos = aMsg.start;
-    } else {
-      pos = aMsg.end;
-    }
-
-    this._handleSelectionPoint(aMsg.change, pos, true);
+    this._handleSelectionPoint(aMsg, true);
     this._selectionMoveActive = false;
     
     // Clear any existing scroll timers
     this._clearTimers();
 
     // Update the position of our selection monocles
     this._updateSelectionUI("end", true, true);
   },
--- a/browser/metro/base/content/helperui/SelectionHelperUI.js
+++ b/browser/metro/base/content/helperui/SelectionHelperUI.js
@@ -106,21 +106,22 @@ Marker.prototype = {
   _selectionHelperUI: null,
   _xPos: 0,
   _yPos: 0,
   _xDrag: 0,
   _yDrag: 0,
   _tag: "",
   _hPlane: 0,
   _vPlane: 0,
+  _restrictedToBounds: false,
 
   // Tweak me if the monocle graphics change in any way
   _monocleRadius: 8,
   _monocleXHitTextAdjust: -2, 
-  _monocleYHitTextAdjust: -10, 
+  _monocleYHitTextAdjust: -10,
 
   get xPos() {
     return this._xPos;
   },
 
   get yPos() {
     return this._yPos;
   },
@@ -132,16 +133,23 @@ Marker.prototype = {
   set tag(aVal) {
     this._tag = aVal;
   },
 
   get dragging() {
     return this._element.customDragger.dragging;
   },
 
+  // Indicates that marker's position doesn't reflect real selection boundary
+  // but rather boundary of input control while actual selection boundaries are
+  // not visible (ex. due scrolled content).
+  get restrictedToBounds() {
+    return this._restrictedToBounds;
+  },
+
   shutdown: function shutdown() {
     this._element.hidden = true;
     this._element.customDragger.shutdown = true;
     delete this._element.customDragger;
     this._selectionHelperUI = null;
     this._element = null;
   },
 
@@ -158,19 +166,20 @@ Marker.prototype = {
   hide: function hide() {
     this._element.hidden = true;
   },
 
   get visible() {
     return this._element.hidden == false;
   },
 
-  position: function position(aX, aY) {
+  position: function position(aX, aY, aRestrictedToBounds) {
     this._xPos = aX;
     this._yPos = aY;
+    this._restrictedToBounds = !!aRestrictedToBounds;
     this._setPosition();
   },
 
   _setPosition: function _setPosition() {
     this._element.left = this._xPos + "px";
     this._element.top = this._yPos + "px";
   },
 
@@ -578,16 +587,17 @@ var SelectionHelperUI = {
 
     // SelectionHandler messages
     messageManager.addMessageListener("Content:SelectionRange", this);
     messageManager.addMessageListener("Content:SelectionCopied", this);
     messageManager.addMessageListener("Content:SelectionFail", this);
     messageManager.addMessageListener("Content:SelectionDebugRect", this);
     messageManager.addMessageListener("Content:HandlerShutdown", this);
     messageManager.addMessageListener("Content:SelectionHandlerPong", this);
+    messageManager.addMessageListener("Content:SelectionSwap", this);
 
     // capture phase
     window.addEventListener("keypress", this, true);
     window.addEventListener("MozPrecisePointer", this, true);
     window.addEventListener("MozDeckOffsetChanging", this, true);
     window.addEventListener("MozDeckOffsetChanged", this, true);
     window.addEventListener("KeyboardChanged", this, true);
 
@@ -607,16 +617,17 @@ var SelectionHelperUI = {
 
   _shutdown: function _shutdown() {
     messageManager.removeMessageListener("Content:SelectionRange", this);
     messageManager.removeMessageListener("Content:SelectionCopied", this);
     messageManager.removeMessageListener("Content:SelectionFail", this);
     messageManager.removeMessageListener("Content:SelectionDebugRect", this);
     messageManager.removeMessageListener("Content:HandlerShutdown", this);
     messageManager.removeMessageListener("Content:SelectionHandlerPong", this);
+    messageManager.removeMessageListener("Content:SelectionSwap", this);
 
     window.removeEventListener("keypress", this, true);
     window.removeEventListener("MozPrecisePointer", this, true);
     window.removeEventListener("MozDeckOffsetChanging", this, true);
     window.removeEventListener("MozDeckOffsetChanged", this, true);
     window.removeEventListener("KeyboardChanged", this, true);
 
     window.removeEventListener("click", this, false);
@@ -932,37 +943,43 @@ var SelectionHelperUI = {
     this.overlay.addDebugRect(aMsg.left, aMsg.top, aMsg.right, aMsg.bottom,
                               aMsg.color, aMsg.fill, aMsg.id);
   },
 
   _selectionHandlerShutdown: function _selectionHandlerShutdown() {
     this._shutdown();
   },
 
+  _selectionSwap: function _selectionSwap() {
+    [this.startMark.tag, this.endMark.tag] = [this.endMark.tag,
+        this.startMark.tag];
+    [this._startMark, this._endMark] = [this.endMark, this.startMark];
+  },
+
   /*
    * Message handlers
    */
 
   _onSelectionCopied: function _onSelectionCopied(json) {
     this.closeEditSession(true);
   },
 
   _onSelectionRangeChange: function _onSelectionRangeChange(json) {
     let haveSelectionRect = true;
 
     if (json.updateStart) {
       let x = this._msgTarget.btocx(json.start.xPos, true);
       let y = this._msgTarget.btocy(json.start.yPos, true);
-      this.startMark.position(x, y);
+      this.startMark.position(x, y, json.start.restrictedToBounds);
     }
 
     if (json.updateEnd) {
       let x = this._msgTarget.btocx(json.end.xPos, true);
       let y = this._msgTarget.btocy(json.end.yPos, true);
-      this.endMark.position(x, y);
+      this.endMark.position(x, y, json.end.restrictedToBounds);
     }
 
     if (json.updateCaret) {
       let x = this._msgTarget.btocx(json.caret.xPos, true);
       let y = this._msgTarget.btocy(json.caret.yPos, true);
       // If selectionRangeFound is set SelectionHelper found a range we can
       // attach to. If not, there's no text in the control, and hence no caret
       // position information we can use.
@@ -1085,78 +1102,81 @@ var SelectionHelperUI = {
         this._onSelectionCopied(json);
         break;
       case "Content:SelectionDebugRect":
         this._onDebugRectRequest(json);
         break;
       case "Content:HandlerShutdown":
         this._selectionHandlerShutdown();
         break;
+      case "Content:SelectionSwap":
+        this._selectionSwap();
+        break;
       case "Content:SelectionHandlerPong":
         this._onPong(json.id);
         break;
     }
   },
 
   /*
    * Callbacks from markers
    */
 
   _getMarkerBaseMessage: function _getMarkerBaseMessage(aMarkerTag) {
     return {
       change: aMarkerTag,
       start: {
         xPos: this._msgTarget.ctobx(this.startMark.xPos, true),
-        yPos: this._msgTarget.ctoby(this.startMark.yPos, true)
+        yPos: this._msgTarget.ctoby(this.startMark.yPos, true),
+        restrictedToBounds: this.startMark.restrictedToBounds
       },
       end: {
         xPos: this._msgTarget.ctobx(this.endMark.xPos, true),
-        yPos: this._msgTarget.ctoby(this.endMark.yPos, true)
+        yPos: this._msgTarget.ctoby(this.endMark.yPos, true),
+        restrictedToBounds: this.endMark.restrictedToBounds
       },
       caret: {
         xPos: this._msgTarget.ctobx(this.caretMark.xPos, true),
         yPos: this._msgTarget.ctoby(this.caretMark.yPos, true)
       },
     };
   },
 
   markerDragStart: function markerDragStart(aMarker) {
     let json = this._getMarkerBaseMessage(aMarker.tag);
     if (aMarker.tag == "caret") {
-      this._cachedCaretPos = null;
-      this._sendAsyncMessage("Browser:CaretMove", json);
+      // Cache for when we start the drag in _transitionFromCaretToSelection.
+      if (!this._cachedCaretPos) {
+        this._cachedCaretPos = this._getMarkerBaseMessage(aMarker.tag).caret;
+      }
       return;
     }
     this._sendAsyncMessage("Browser:SelectionMoveStart", json);
   },
 
   markerDragStop: function markerDragStop(aMarker) {
     let json = this._getMarkerBaseMessage(aMarker.tag);
     if (aMarker.tag == "caret") {
-      this._sendAsyncMessage("Browser:CaretUpdate", json);
+      this._cachedCaretPos = null;
       return;
     }
     this._sendAsyncMessage("Browser:SelectionMoveEnd", json);
   },
 
   markerDragMove: function markerDragMove(aMarker, aDirection) {
     if (aMarker.tag == "caret") {
       // If direction is "tbd" the drag monocle hasn't determined which
       // direction the user is dragging.
       if (aDirection != "tbd") {
         // We are going to transition from caret browsing mode to selection
         // mode on drag. So swap the caret monocle for a start or end monocle
         // depending on the direction of the drag, and start selecting text.
         this._transitionFromCaretToSelection(aDirection);
         return false;
       }
-      // Cache for when we start the drag in _transitionFromCaretToSelection.
-      if (!this._cachedCaretPos) {
-        this._cachedCaretPos = this._getMarkerBaseMessage(aMarker.tag).caret;
-      }
       return true;
     }
     this._cachedCaretPos = null;
 
     // We'll re-display these after the drag is complete.
     this._hideMonocles();
 
     let json = this._getMarkerBaseMessage(aMarker.tag);
--- a/browser/metro/base/content/library/SelectionPrototype.js
+++ b/browser/metro/base/content/library/SelectionPrototype.js
@@ -237,55 +237,117 @@ SelectionPrototype.prototype = {
     this._cache.updateCaret = aUpdateCaret || false;
     this._cache.targetIsEditable = this._targetIsEditable;
 
     // Get monocles positioned correctly
     this.sendAsync("Content:SelectionRange", this._cache);
   },
 
   /*
-   * _handleSelectionPoint(aMarker, aPoint, aEndOfSelection) 
+   * _handleSelectionPoint(aSelectionInfo, aEndOfSelection)
    *
    * After a monocle moves to a new point in the document, determines
    * what the target is and acts on its selection accordingly. If the
    * monocle is within the bounds of the target, adds or subtracts selection
    * at the monocle coordinates appropriately and then merges selection ranges
    * into a single continuous selection. If the monocle is outside the bounds
    * of the target and the underlying target is editable, uses the selection
    * controller to advance selection and visibility within the control.
    */
-  _handleSelectionPoint: function _handleSelectionPoint(aMarker, aClientPoint,
+  _handleSelectionPoint: function _handleSelectionPoint(aSelectionInfo,
                                                         aEndOfSelection) {
     let selection = this._getSelection();
 
-    let clientPoint = { xPos: aClientPoint.xPos, yPos: aClientPoint.yPos };
+    let markerToChange = aSelectionInfo.change == "start" ?
+        aSelectionInfo.start : aSelectionInfo.end;
 
     if (selection.rangeCount == 0) {
       this._onFail("selection.rangeCount == 0");
       return;
     }
 
     // We expect continuous selection ranges.
     if (selection.rangeCount > 1) {
       this._setContinuousSelection();
     }
 
     // Adjust our y position up such that we are sending coordinates on
     // the text line vs. below it where the monocle is positioned.
-    let halfLineHeight = this._queryHalfLineHeight(aMarker, selection);
-    clientPoint.yPos -= halfLineHeight;
+    let halfLineHeight = this._queryHalfLineHeight(aSelectionInfo.start,
+        selection);
+    aSelectionInfo.start.yPos -= halfLineHeight;
+    aSelectionInfo.end.yPos -= halfLineHeight;
+
+    let isSwapNeeded = false;
+    if (this._isSelectionSwapNeeded(aSelectionInfo.start, aSelectionInfo.end,
+        halfLineHeight)) {
+      [aSelectionInfo.start, aSelectionInfo.end] =
+          [aSelectionInfo.end, aSelectionInfo.start];
+
+      isSwapNeeded = true;
+      this.sendAsync("Content:SelectionSwap");
+    }
 
     // Modify selection based on monocle movement
     if (this._targetIsEditable && !Util.isEditableContent(this._targetElement)) {
-      this._adjustEditableSelection(aMarker, clientPoint, aEndOfSelection);
+      if (isSwapNeeded) {
+        this._adjustEditableSelection("start", aSelectionInfo.start,
+            aEndOfSelection);
+        this._adjustEditableSelection("end", aSelectionInfo.end,
+            aEndOfSelection);
+      } else {
+        this._adjustEditableSelection(aSelectionInfo.change, markerToChange,
+            aEndOfSelection);
+      }
     } else {
-      this._adjustSelectionAtPoint(aMarker, clientPoint, aEndOfSelection);
+      if (isSwapNeeded) {
+        this._adjustSelectionAtPoint("start", aSelectionInfo.start,
+            aEndOfSelection, true);
+        this._adjustSelectionAtPoint("end", aSelectionInfo.end,
+            aEndOfSelection, true);
+      } else {
+        this._adjustSelectionAtPoint(aSelectionInfo.change, markerToChange,
+            aEndOfSelection);
+      }
     }
   },
 
+  /**
+   * Checks whether we need to swap start and end markers depending the target
+   * element and monocle position.
+   * @param aStart Start monocle coordinates.
+   * @param aEnd End monocle coordinates
+   * @param aYThreshold Y-coordinate threshold used to eliminate slight
+   * differences in monocle vertical positions.
+   */
+  _isSelectionSwapNeeded: function(aStart, aEnd, aYThreshold) {
+    let isSwapNeededByX = aStart.xPos > aEnd.xPos;
+    let isSwapNeededByY = aStart.yPos - aEnd.yPos > aYThreshold;
+    let onTheSameLine = Math.abs(aStart.yPos - aEnd.yPos) <= aYThreshold;
+
+    if (this._targetIsEditable &&
+        !Util.isEditableContent(this._targetElement)) {
+
+      // If one of the markers is restricted to edit bounds, then we shouldn't
+      // swap it until we know its real position
+      if (aStart.restrictedToBounds && aEnd.restrictedToBounds) {
+        return false;
+      }
+
+      // For multi line we should respect Y-coordinate
+      if (Util.isMultilineInput(this._targetElement)) {
+        return isSwapNeededByY || (isSwapNeededByX && onTheSameLine);
+      }
+
+      return isSwapNeededByX;
+    }
+
+    return isSwapNeededByY || (isSwapNeededByX && onTheSameLine);
+  },
+
   /*
    * _handleSelectionPoint helper methods
    */
 
   /*
    * _adjustEditableSelection
    *
    * Based on a monocle marker and position, adds or subtracts from the
@@ -370,19 +432,22 @@ SelectionPrototype.prototype = {
    * Note: we are trying to move away from this api due to the overhead. 
    *
    * @param the marker currently being manipulated
    * @param aClientPoint the point designating the new start or end
    * position for the selection.
    * @param aEndOfSelection indicates if this is the end of a selection
    * move, in which case we may want to snap to the end of a word or
    * sentence.
+   * @param suppressSelectionUIUpdate Indicates that we don't want to update
+   * static monocle automatically as it's going to be be updated explicitly like
+   * in case with monocle swapping.
    */
-  _adjustSelectionAtPoint: function _adjustSelectionAtPoint(aMarker, aClientPoint,
-                                                            aEndOfSelection) {
+  _adjustSelectionAtPoint: function _adjustSelectionAtPoint(aMarker,
+      aClientPoint, aEndOfSelection, suppressSelectionUIUpdate) {
     // Make a copy of the existing range, we may need to reset it.
     this._backupRangeList();
 
     // shrinkSelectionFromPoint takes sub-frame relative coordinates.
     let framePoint = this._clientPointToFramePoint(aClientPoint);
 
     // Tests to see if the user is trying to shrink the selection, and if so
     // collapses it down to the appropriate side such that our calls below
@@ -392,20 +457,41 @@ SelectionPrototype.prototype = {
     let selectResult = false;
     try {
       // If we're at the end of a selection (touchend) snap to the word.
       let type = ((aEndOfSelection && this._snap) ?
         Ci.nsIDOMWindowUtils.SELECT_WORD :
         Ci.nsIDOMWindowUtils.SELECT_CHARACTER);
 
       // Select a character at the point.
-      selectResult = 
-        this._domWinUtils.selectAtPoint(framePoint.xPos,
-                                        framePoint.yPos,
-                                        type);
+      selectResult = this._domWinUtils.selectAtPoint(framePoint.xPos,
+          framePoint.yPos, type);
+
+      // selectAtPoint selects char back and forward and apparently can select
+      // content that is beyond selection boundaries that we had before it was
+      // shrunk that forces selection to always move forward or backward
+      // preventing monocle swapping.
+      if (selectResult && shrunk && this._rangeBackup) {
+        let selection = this._getSelection();
+
+        let currentSelection = this._extractUIRects(
+            selection.getRangeAt(selection.rangeCount - 1)).selection;
+        let previousSelection = this._extractUIRects(
+            this._rangeBackup[0]).selection;
+
+        if (aMarker == "start" &&
+            currentSelection.right > previousSelection.right) {
+          selectResult = false;
+        }
+
+        if (aMarker == "end"  &&
+            currentSelection.left < previousSelection.left) {
+          selectResult = false;
+        }
+      }
     } catch (ex) {
     }
 
     // If selectAtPoint failed (which can happen if there's nothing to select)
     // reset our range back before we shrunk it.
     if (!selectResult) {
       this._restoreRangeList();
     }
@@ -413,17 +499,19 @@ SelectionPrototype.prototype = {
     this._freeRangeList();
 
     // Smooth over the selection between all existing ranges.
     this._setContinuousSelection();
 
     // Update the other monocle's position. We do this because the dragging
     // monocle may reset the static monocle to a new position if the dragging
     // monocle drags ahead or behind the other.
-    this._updateSelectionUI("update", aMarker == "end", aMarker == "start");
+    this._updateSelectionUI("update",
+      aMarker == "end" && !suppressSelectionUIUpdate,
+      aMarker == "start" && !suppressSelectionUIUpdate);
   },
 
   /*
    * _backupRangeList, _restoreRangeList, and _freeRangeList
    *
    * Utilities that manage a cloned copy of the existing selection.
    */
 
@@ -432,16 +520,20 @@ SelectionPrototype.prototype = {
     for (let idx = 0; idx < this._getSelection().rangeCount; idx++) {
       this._rangeBackup.push(this._getSelection().getRangeAt(idx).cloneRange());
     }
   },
 
   _restoreRangeList: function _restoreRangeList() {
     if (this._rangeBackup == null)
       return;
+
+    // Remove every previously created selection range
+    this._getSelection().removeAllRanges();
+
     for (let idx = 0; idx < this._rangeBackup.length; idx++) {
       this._getSelection().addRange(this._rangeBackup[idx]);
     }
     this._freeRangeList();
   },
 
   _freeRangeList: function _restoreRangeList() {
     this._rangeBackup = null;
@@ -813,51 +905,60 @@ SelectionPrototype.prototype = {
     seldata.element.bottom = r.bottom + this._contentOffset.y;
 
     // If we don't have a range we can attach to let SelectionHelperUI know.
     seldata.selectionRangeFound = !!rects.length;
 
     return seldata;
   },
 
+  /**
+   * Updates point's coordinate with max\min available in accordance with the
+   * bounding rectangle. Returns true if point coordinates were actually updated,
+   * and false - otherwise.
+   * @param aPoint Target point which coordinates will be analyzed.
+   * @param aRectangle Target rectangle to bound to.
+   */
+  _restrictPointToRectangle: function(aPoint, aRectangle) {
+    let restrictionWasRequired = false;
+
+    if (aPoint.xPos < aRectangle.left) {
+      aPoint.xPos = aRectangle.left;
+      restrictionWasRequired = true;
+    } else if (aPoint.xPos > aRectangle.right) {
+      aPoint.xPos = aRectangle.right;
+      restrictionWasRequired = true;
+    }
+
+    if (aPoint.yPos < aRectangle.top) {
+      aPoint.yPos = aRectangle.top;
+      restrictionWasRequired = true;
+    } else if (aPoint.yPos > aRectangle.bottom) {
+      aPoint.yPos = aRectangle.bottom;
+      restrictionWasRequired = true;
+    }
+
+    return restrictionWasRequired;
+  },
+
   /*
    * Selection bounds will fall outside the bound of a control if the control
    * can scroll. Clip UI cache data to the bounds of the target so monocles
    * don't draw outside the control.
    */
   _restrictSelectionRectToEditBounds: function _restrictSelectionRectToEditBounds() {
     if (!this._targetIsEditable)
       return;
 
-    let bounds = this._getTargetBrowserRect();
-    if (this._cache.start.xPos < bounds.left)
-      this._cache.start.xPos = bounds.left;
-    if (this._cache.end.xPos < bounds.left)
-      this._cache.end.xPos = bounds.left;
-    if (this._cache.caret.xPos < bounds.left)
-      this._cache.caret.xPos = bounds.left;
-    if (this._cache.start.xPos > bounds.right)
-      this._cache.start.xPos = bounds.right;
-    if (this._cache.end.xPos > bounds.right)
-      this._cache.end.xPos = bounds.right;
-    if (this._cache.caret.xPos > bounds.right)
-      this._cache.caret.xPos = bounds.right;
-
-    if (this._cache.start.yPos < bounds.top)
-      this._cache.start.yPos = bounds.top;
-    if (this._cache.end.yPos < bounds.top)
-      this._cache.end.yPos = bounds.top;
-    if (this._cache.caret.yPos < bounds.top)
-      this._cache.caret.yPos = bounds.top;
-    if (this._cache.start.yPos > bounds.bottom)
-      this._cache.start.yPos = bounds.bottom;
-    if (this._cache.end.yPos > bounds.bottom)
-      this._cache.end.yPos = bounds.bottom;
-    if (this._cache.caret.yPos > bounds.bottom)
-      this._cache.caret.yPos = bounds.bottom;
+    let targetRectangle = this._getTargetBrowserRect();
+    this._cache.start.restrictedToBounds = this._restrictPointToRectangle(
+        this._cache.start, targetRectangle);
+    this._cache.end.restrictedToBounds = this._restrictPointToRectangle(
+        this._cache.end, targetRectangle);
+    this._restrictPointToRectangle(this._cache.caret, targetRectangle);
   },
 
   _restrictCoordinateToEditBounds: function _restrictCoordinateToEditBounds(aX, aY) {
     let result = {
       xPos: aX,
       yPos: aY
     };
     if (!this._targetIsEditable)