bug 667243 - use caretPositionFromPoint for controlling text selection r=mfinkle
authorBrad Lassey <blassey@mozilla.com>
Thu, 18 Aug 2011 16:54:34 -0400
changeset 75509 636d0e651d5e610c0c30a511e16d0ac98cc49223
parent 75508 70b03a8b5a957b3b191774b1f8ef73a4c7ead5e9
child 75510 00f9b3304811a191188521cc29ff1cc9034470c2
push id2
push userbsmedberg@mozilla.com
push dateFri, 19 Aug 2011 14:38:13 +0000
reviewersmfinkle
bugs667243
milestone9.0a1
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);