Bug 674871 - [highlighter] there is something wrong with iframes; r=mihai.sucan,gavin.sharp
authorPanos Astithas <past@mozilla.com>
Fri, 16 Sep 2011 12:01:38 +0300
changeset 76960 31153dfd43070444318a813a6979ee01328dc445
parent 76959 01ef83aa3f02aa7606eeda54b6381148da059324
child 76961 8e3e1c5f348da4bb38895b513f42c40d7f5eb087
push id169
push usermihai.sucan@gmail.com
push dateFri, 16 Sep 2011 09:20:06 +0000
treeherderfx-team@8e3e1c5f348d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmihai.sucan, gavin.sharp
bugs674871
milestone9.0a1
Bug 674871 - [highlighter] there is something wrong with iframes; r=mihai.sucan,gavin.sharp
browser/base/content/inspector.js
browser/base/content/test/inspector/Makefile.in
browser/base/content/test/inspector/browser_inspector_bug_674871.js
--- a/browser/base/content/inspector.js
+++ b/browser/base/content/inspector.js
@@ -248,60 +248,78 @@ Highlighter.prototype = {
    */
   highlight: function Highlighter_highlight(aScroll)
   {
     // node is not set or node is not highlightable, bail
     if (!this.node || !this.isNodeHighlightable()) {
       return;
     }
 
+    if (aScroll) {
+      this.node.scrollIntoView();
+    }
+
     let clientRect = this.node.getBoundingClientRect();
 
+    // Go up in the tree of frames to determine the correct rectangle.
     // clientRect is read-only, we need to be able to change properties.
     let rect = {top: clientRect.top,
                 left: clientRect.left,
                 width: clientRect.width,
                 height: clientRect.height};
 
-    if (aScroll) {
-      this.node.scrollIntoView();
-    }
+    let frameWin = this.node.ownerDocument.defaultView;
+
+    // We iterate through all the parent windows.
+    while (true) {
+
+      // Does the selection overflow on the right of its window?
+      let diffx = frameWin.innerWidth - (rect.left + rect.width);
+      if (diffx < 0) {
+        rect.width += diffx;
+      }
 
-    // Go up in the tree of frames to determine the correct rectangle
-    // coordinates and size.
-    let frameWin = this.node.ownerDocument.defaultView;
-    do {
-      let frameRect = frameWin.frameElement ?
-                      frameWin.frameElement.getBoundingClientRect() :
-                      {top: 0, left: 0};
+      // Does the selection overflow on the bottom of its window?
+      let diffy = frameWin.innerHeight - (rect.top + rect.height);
+      if (diffy < 0) {
+        rect.height += diffy;
+      }
 
+      // Does the selection overflow on the left of its window?
+      if (rect.left < 0) {
+        rect.width += rect.left;
+        rect.left = 0;
+      }
+
+      // Does the selection overflow on the top of its window?
       if (rect.top < 0) {
         rect.height += rect.top;
         rect.top = 0;
       }
 
-      if (rect.left < 0) {
-        rect.width += rect.left;
-        rect.left = 0;
+      // Selection has been clipped to fit in its own window.
+
+      // Are we in the top-level window?
+      if (frameWin.parent === frameWin || !frameWin.frameElement) {
+        break;
       }
 
-      let diffx = frameWin.innerWidth - rect.left - rect.width;
-      if (diffx < 0) {
-        rect.width += diffx;
-      }
-      let diffy = frameWin.innerHeight - rect.top - rect.height;
-      if (diffy < 0) {
-        rect.height += diffy;
-      }
+      // We are in an iframe.
+      // We take into account the parent iframe position and its
+      // offset (borders and padding).
+      let frameRect = frameWin.frameElement.getBoundingClientRect();
 
-      rect.left += frameRect.left;
-      rect.top += frameRect.top;
+      let [offsetTop, offsetLeft] =
+        InspectorUI.getIframeContentOffset(frameWin.frameElement);
+
+      rect.top += frameRect.top + offsetTop;
+      rect.left += frameRect.left + offsetLeft;
 
       frameWin = frameWin.parent;
-    } while (frameWin != this.win);
+    }
 
     this.highlightRectangle(rect);
 
     if (this._highlighting) {
       Services.obs.notifyObservers(null,
         INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, null);
     }
   },
@@ -1373,36 +1391,72 @@ var InspectorUI = {
    * @param integer aX
    * @param integer aY
    * @returns Node|null the element node found at the given coordinates.
    */
   elementFromPoint: function IUI_elementFromPoint(aDocument, aX, aY)
   {
     let node = aDocument.elementFromPoint(aX, aY);
     if (node && node.contentDocument) {
-      switch (node.nodeName.toLowerCase()) {
-        case "iframe":
-          let rect = node.getBoundingClientRect();
-          aX -= rect.left;
-          aY -= rect.top;
+      if (node instanceof HTMLIFrameElement) {
+        let rect = node.getBoundingClientRect();
+
+        // Gap between the iframe and its content window.
+        let [offsetTop, offsetLeft] = this.getIframeContentOffset(node);
+
+        aX -= rect.left + offsetLeft;
+        aY -= rect.top + offsetTop;
 
-        case "frame":
-          let subnode = this.elementFromPoint(node.contentDocument, aX, aY);
-          if (subnode) {
-            node = subnode;
-          }
+        if (aX < 0 || aY < 0) {
+          // Didn't reach the content document, still over the iframe.
+          return node;
+        }
+      }
+      if (node instanceof HTMLIFrameElement ||
+          node instanceof HTMLFrameElement) {
+        let subnode = this.elementFromPoint(node.contentDocument, aX, aY);
+        if (subnode) {
+          node = subnode;
+        }
       }
     }
     return node;
   },
 
   ///////////////////////////////////////////////////////////////////////////
   //// Utility functions
 
   /**
+   * Returns iframe content offset (iframe border + padding).
+   * Note: this function shouldn't need to exist, had the platform provided a
+   * suitable API for determining the offset between the iframe's content and
+   * its bounding client rect. Bug 626359 should provide us with such an API.
+   *
+   * @param aIframe
+   *        The iframe.
+   * @returns array [offsetTop, offsetLeft]
+   *          offsetTop is the distance from the top of the iframe and the
+   *            top of the content document.
+   *          offsetLeft is the distance from the left of the iframe and the
+   *            left of the content document.
+   */
+  getIframeContentOffset: function IUI_getIframeContentOffset(aIframe)
+  {
+    let style = aIframe.contentWindow.getComputedStyle(aIframe, null);
+
+    let paddingTop = parseInt(style.getPropertyValue("padding-top"));
+    let paddingLeft = parseInt(style.getPropertyValue("padding-left"));
+
+    let borderTop = parseInt(style.getPropertyValue("border-top-width"));
+    let borderLeft = parseInt(style.getPropertyValue("border-left-width"));
+
+    return [borderTop + paddingTop, borderLeft + paddingLeft];
+  },
+
+  /**
    * Does the given object have a class attribute?
    * @param aNode
    *        the DOM node.
    * @param aClass
    *        The class string.
    * @returns boolean
    */
   hasClass: function IUI_hasClass(aNode, aClass)
--- a/browser/base/content/test/inspector/Makefile.in
+++ b/browser/base/content/test/inspector/Makefile.in
@@ -51,14 +51,15 @@ include $(topsrcdir)/config/rules.mk
 		browser_inspector_scrolling.js \
 		browser_inspector_store.js \
 		browser_inspector_tab_switch.js \
 		browser_inspector_treePanel_output.js \
 		browser_inspector_treePanel_input.html \
 		browser_inspector_treePanel_result.html \
 		browser_inspector_registertools.js \
 		browser_inspector_bug_665880.js \
+		browser_inspector_bug_674871.js \
 		browser_inspector_editor.js \
 		browser_inspector_bug_566084_location_changed.js \
 		$(NULL)
 
 libs::	$(_BROWSER_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/inspector/browser_inspector_bug_674871.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+function test()
+{
+  waitForExplicitFinish();
+
+  let doc;
+  let iframeNode, iframeBodyNode;
+
+  let iframeSrc = "<style>" +
+                  "body {" +
+                  "margin:0;" +
+                  "height:100%;" +
+                  "background-color:red" +
+                  "}" +
+                  "</style>" +
+                  "<body></body>";
+  let docSrc = "<style>" +
+               "iframe {" +
+               "height:200px;" +
+               "border: 11px solid black;" +
+               "padding: 13px;" +
+               "}" +
+               "body,iframe {" +
+               "margin:0" +
+               "}" +
+               "</style>" +
+               "<body>" +
+               "<iframe src='data:text/html," + iframeSrc + "'></iframe>" +
+               "</body>";
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    doc = content.document;
+    waitForFocus(setupTest, content);
+  }, true);
+
+  content.location = "data:text/html," + docSrc;
+
+  function setupTest()
+  {
+    iframeNode = doc.querySelector("iframe");
+    iframeBodyNode = iframeNode.contentDocument.querySelector("body");
+    ok(iframeNode, "we have the iframe node");
+    ok(iframeBodyNode, "we have the body node");
+    Services.obs.addObserver(runTests,
+      INSPECTOR_NOTIFICATIONS.OPENED, false);
+    InspectorUI.toggleInspectorUI();
+  }
+
+  function runTests()
+  {
+    Services.obs.removeObserver(runTests,
+      INSPECTOR_NOTIFICATIONS.OPENED);
+
+    executeSoon(function() {
+      Services.obs.addObserver(isTheIframeSelected,
+        INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+
+      moveMouseOver(iframeNode, 1, 1);
+    });
+  }
+
+  function isTheIframeSelected()
+  {
+    Services.obs.removeObserver(isTheIframeSelected,
+      INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+
+    is(InspectorUI.selection, iframeNode, "selection matches node");
+    iframeNode.style.marginBottom = doc.defaultView.innerHeight + "px";
+    doc.defaultView.scrollBy(0, 40);
+
+    executeSoon(function() {
+      Services.obs.addObserver(isTheIframeContentSelected,
+        INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+
+      moveMouseOver(iframeNode, 40, 40);
+    });
+  }
+
+  function isTheIframeContentSelected()
+  {
+    Services.obs.removeObserver(isTheIframeContentSelected,
+      INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+
+    is(InspectorUI.selection, iframeBodyNode, "selection matches node");
+    // 184 == 200 + 11(border) + 13(padding) - 40(scroll)
+    is(InspectorUI.highlighter._highlightRect.height, 184,
+      "highlighter height");
+
+    Services.obs.addObserver(finishUp,
+      INSPECTOR_NOTIFICATIONS.CLOSED, false);
+    InspectorUI.closeInspectorUI();
+  }
+
+  function finishUp() {
+    Services.obs.removeObserver(finishUp, INSPECTOR_NOTIFICATIONS.CLOSED);
+    doc = iframeNode = iframeBodyNode = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+
+
+  function moveMouseOver(aElement, x, y)
+  {
+    EventUtils.synthesizeMouse(aElement, x, y, {type: "mousemove"},
+                               aElement.ownerDocument.defaultView);
+  }
+
+}
+