bug 667243 - use caretPositionFromPoint for controlling text selection r=mfinkle
authorBrad Lassey <blassey@mozilla.com>
Thu, 18 Aug 2011 16:54:34 -0400
changeset 75507 636d0e651d5e610c0c30a511e16d0ac98cc49223
parent 75506 70b03a8b5a957b3b191774b1f8ef73a4c7ead5e9
child 75508 00f9b3304811a191188521cc29ff1cc9034470c2
push id21031
push usermak77@bonardo.net
push dateFri, 19 Aug 2011 09:40:40 +0000
treeherdermozilla-central@1881f9b5f8b5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs667243
milestone9.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 667243 - use caretPositionFromPoint for controlling text selection r=mfinkle
mobile/chrome/content/content.js
--- a/mobile/chrome/content/content.js
+++ b/mobile/chrome/content/content.js
@@ -1348,61 +1348,65 @@ var SelectionHandler = {
   contentWindow: null,
   
   init: function sh_init() {
     addMessageListener("Browser:SelectionStart", this);
     addMessageListener("Browser:SelectionEnd", this);
     addMessageListener("Browser:SelectionMove", this);
   },
 
+  getCurrentWindowAndOffset: function(x, y, offset) {
+    let utils = Util.getWindowUtils(content);
+    let elem = utils.elementFromPoint(x, y, true, false);
+    while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
+      // adjust client coordinates' origin to be top left of iframe viewport
+      let rect = elem.getBoundingClientRect();
+      scrollOffset = ContentScroll.getScrollOffset(elem.ownerDocument.defaultView);
+      offset.x += rect.left;
+      x -= rect.left;
+      
+      offset.y += rect.top + scrollOffset.y;
+      y -= rect.top + scrollOffset.y;
+      utils = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+      elem = utils.elementFromPoint(x, y, true, false);
+    }
+    if (!elem)
+      return;
+    
+    return { contentWindow: elem.ownerDocument.defaultView, offset: offset };
+  },
+
   receiveMessage: function sh_receiveMessage(aMessage) {
     let scrollOffset = ContentScroll.getScrollOffset(content);
     let utils = Util.getWindowUtils(content);
     let json = aMessage.json;
 
     switch (aMessage.name) {
       case "Browser:SelectionStart": {
         // Clear out the text cache
         this.selectedText = "";
 
         // if this is an iframe, dig down to find the document that was clicked
         let x = json.x - scrollOffset.x;
         let y = json.y - scrollOffset.y;
-        let offset = scrollOffset;
-        let elem = utils.elementFromPoint(x, y, true, false);
-        while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
-          // adjust client coordinates' origin to be top left of iframe viewport
-          let rect = elem.getBoundingClientRect();
-          scrollOffset = ContentScroll.getScrollOffset(elem.ownerDocument.defaultView);
-          offset.x += rect.left;
-          x -= rect.left;
-
-          offset.y += rect.top + scrollOffset.y;
-          y -= rect.top + scrollOffset.y;
-          utils = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
-          elem = utils.elementFromPoint(x, y, true, false);
-        }
-        if (!elem)
-          return;
-
-        let contentWindow = elem.ownerDocument.defaultView;
+        let { contentWindow: contentWindow, offset: offset } = this.getCurrentWindowAndOffset(x, y, scrollOffset);
         let currentDocShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell);
 
         // Remove any previous selected or created ranges. Tapping anywhere on a
         // page will create an empty range.
         let selection = contentWindow.getSelection();
         selection.removeAllRanges();
 
-        // Position the caret using a fake mouse click
-        utils.sendMouseEventToWindow("mousedown", x, y, 0, 1, 0, true);
-        utils.sendMouseEventToWindow("mouseup", x, y, 0, 1, 0, true);
+        try {
+          let caretPos = contentWindow.document.caretPositionFromPoint(json.x - scrollOffset.x, json.y - scrollOffset.y);
+          let selcon = currentDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay).QueryInterface(Ci.nsISelectionController);
+          let sel = selcon.getSelection(1);
+          sel.collapse(caretPos.offsetNode, caretPos.offset);
 
-        // Select the word nearest the caret
-        try {
-          let selcon = currentDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay).QueryInterface(Ci.nsISelectionController);
+          // Select the word nearest the caret
           selcon.wordMove(false, false);
           selcon.wordMove(true, true);
         } catch(e) {
           // If we couldn't select the word at the given point, bail
           return;
         }
 
         // Find the selected text rect and send it back so the handles can position correctly
@@ -1433,59 +1437,65 @@ var SelectionHandler = {
         let tap = { x: json.x - this.cache.offset.x, y: json.y - this.cache.offset.y };
         pointInSelection = (tap.x > this.cache.rect.left && tap.x < this.cache.rect.right) && (tap.y > this.cache.rect.top && tap.y < this.cache.rect.bottom);
 
         try {
           // The selection might already be gone
           if (this.contentWindow)
             this.contentWindow.getSelection().removeAllRanges();
           this.contentWindow = null;
-        } catch(e) {}
+        } catch(e) {
+          Cu.reportError(e);
+        }
 
         if (pointInSelection && this.selectedText.length) {
           let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
           clipboard.copyString(this.selectedText);
           sendAsyncMessage("Browser:SelectionCopied", { succeeded: true });
         } else {
           sendAsyncMessage("Browser:SelectionCopied", { succeeded: false });
         }
         break;
       }
 
       case "Browser:SelectionMove":
         if (!this.contentWindow)
           return;
 
-        // Hack to avoid setting focus in a textbox [Bugs 654352 & 667243]
-        let elemUnder = elementFromPoint(json.x - scrollOffset.x, json.y - scrollOffset.y);
-        if (elemUnder && elemUnder instanceof Ci.nsIDOMHTMLInputElement || elemUnder instanceof Ci.nsIDOMHTMLTextAreaElement)
-          return;
+        let x = json.x - scrollOffset.x;
+        let y = json.y - scrollOffset.y;
 
-        // Limit the selection to the initial content window (don't leave or enter iframes)
-        if (elemUnder && elemUnder.ownerDocument.defaultView != this.contentWindow)
-          return;
+        try {
+          let caretPos = this.contentWindow.document.caretPositionFromPoint(x, y);
+          if (caretPos.offsetNode == null ||
+              caretPos.offsetNode instanceof Ci.nsIDOMHTMLInputElement || 
+              caretPos.offsetNode instanceof Ci.nsIDOMHTMLTextAreaElement ||
+              caretPos.offsetNode.ownerDocument.defaultView != this.contentWindow)
+            return;
 
-        // Use fake mouse events to update the selection
-        if (json.type == "end") {
-          // Keep the cache in "client" coordinates, but translate for the mouse event
-          this.cache.end = { x: json.x, y: json.y };
-          let end = { x: this.cache.end.x - scrollOffset.x, y: this.cache.end.y - scrollOffset.y };
-          utils.sendMouseEventToWindow("mousedown", end.x, end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
-          utils.sendMouseEventToWindow("mouseup", end.x, end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
-        } else {
-          // Keep the cache in "client" coordinates, but translate for the mouse event
-          this.cache.start = { x: json.x, y: json.y };
-          let start = { x: this.cache.start.x - scrollOffset.x, y: this.cache.start.y - scrollOffset.y };
-          let end = { x: this.cache.end.x - scrollOffset.x, y: this.cache.end.y - scrollOffset.y };
+          // Keep the cache in "client" coordinates
+          if (json.type == "end")
+            this.cache.end = { x: json.x, y: json.y };
+          else
+            this.cache.start = { x: json.x, y: json.y };
 
-          utils.sendMouseEventToWindow("mousedown", start.x, start.y, 0, 0, 0, true);
-          utils.sendMouseEventToWindow("mouseup", start.x, start.y, 0, 0, 0, true);
-
-          utils.sendMouseEventToWindow("mousedown", end.x, end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
-          utils.sendMouseEventToWindow("mouseup", end.x, end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
+          let currentDocShell = this.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell);
+          let selcon = currentDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay).QueryInterface(Ci.nsISelectionController);
+          let sel = selcon.getSelection(1);
+          if (json.type != "end") {
+            let focusOffset = sel.focusOffset;
+            let focusNode = sel.focusNode;
+            sel.collapse(caretPos.offsetNode, caretPos.offset);
+            sel.extend(focusNode, focusOffset);
+          } else {
+            sel.extend(caretPos.offsetNode, caretPos.offset);
+          }
+        } catch(e) {
+          Cu.reportError(e);
+          return;
         }
 
         // Cache the selected text since the selection might be gone by the time we get the "end" message
         let selection = this.contentWindow.getSelection()
         this.selectedText = selection.toString().trim();
 
         // Update the rect we use to test when finishing the clipboard operation
         let range = selection.getRangeAt(0).QueryInterface(Ci.nsIDOMNSRange);