Bug 724585 - We need a way to scroll a page to center an element if the element is not visible; r=rcampbell
authorThaddee Tyl <thaddee.tyl@gmail.com>
Tue, 12 Jun 2012 13:43:00 +0300
changeset 101799 1dd1770cc77eaeed188db07da8a8254475767296
parent 101798 960b6d4ea73b52450320ae7c00702f3c233f523d
child 101800 95852ec078fb49ba547bed20abb4b886efb1f5d8
push id1316
push userakeybl@mozilla.com
push dateMon, 27 Aug 2012 22:37:00 +0000
treeherdermozilla-beta@db4b09302ee2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcampbell
bugs724585
milestone16.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 724585 - We need a way to scroll a page to center an element if the element is not visible; r=rcampbell
browser/devtools/highlighter/InsideOutBox.jsm
browser/devtools/shared/LayoutHelpers.jsm
browser/devtools/shared/test/Makefile.in
browser/devtools/shared/test/browser_layoutHelpers.html
browser/devtools/shared/test/browser_layoutHelpers.js
browser/devtools/shared/test/browser_layoutHelpers_iframe.html
--- a/browser/devtools/highlighter/InsideOutBox.jsm
+++ b/browser/devtools/highlighter/InsideOutBox.jsm
@@ -123,16 +123,19 @@ InsideOutBoxView = {
  *        The view requiring the InsideOutBox.
  * @param aBox
  *        The box object containing the InsideOutBox. Required to add/remove
  *        children during box manipulation (toggling opened or closed).
  */
 
 var EXPORTED_SYMBOLS = ["InsideOutBox"];
 
+const Cu = Components.utils;
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+
 function InsideOutBox(aView, aBox)
 {
   this.view = aView;
   this.box = aBox;
 
   this.rootObject = null;
 
   this.rootObjectBox = null;
@@ -207,17 +210,19 @@ InsideOutBox.prototype =
     let objectBox = this.createObjectBox(aObject);
     if (!objectBox) {
       return null;
     }
     this.selectObjectBox(objectBox, forceOpen);
     if (makeBoxVisible) {
       this.openObjectBox(objectBox);
       if (scrollIntoView) {
-        objectBox.scrollIntoView(true);
+        // We want to center the label of the element, not the whole tag
+        // (which includes all of its children, and is vertically huge).
+        LayoutHelpers.scrollIntoViewIfNeeded(objectBox.firstElementChild);
       }
     }
     return objectBox;
   },
 
   /**
    * Expands/contracts the given object, depending on its state.
    * @param aObject
--- a/browser/devtools/shared/LayoutHelpers.jsm
+++ b/browser/devtools/shared/LayoutHelpers.jsm
@@ -181,18 +181,17 @@ LayoutHelpers = {
    * Find an element from the given coordinates. This method descends through
    * frames to find the element the user clicked inside frames.
    *
    * @param DOMDocument aDocument the document to look into.
    * @param integer aX
    * @param integer aY
    * @returns Node|null the element node found at the given coordinates.
    */
-  getElementFromPoint: function LH_elementFromPoint(aDocument, aX, aY)
-  {
+  getElementFromPoint: function LH_elementFromPoint(aDocument, aX, aY) {
     let node = aDocument.elementFromPoint(aX, aY);
     if (node && node.contentDocument) {
       if (node instanceof Ci.nsIDOMHTMLIFrameElement) {
         let rect = node.getBoundingClientRect();
 
         // Gap between the iframe and its content window.
         let [offsetTop, offsetLeft] = LayoutHelpers.getIframeContentOffset(node);
 
@@ -209,9 +208,87 @@ LayoutHelpers = {
         let subnode = this.getElementFromPoint(node.contentDocument, aX, aY);
         if (subnode) {
           node = subnode;
         }
       }
     }
     return node;
   },
+
+  /**
+   * Scroll the document so that the element "elem" appears in the viewport.
+   *
+   * @param Element elem the element that needs to appear in the viewport.
+   * @param bool centered true if you want it centered, false if you want it to
+   * appear on the top of the viewport. It is true by default, and that is
+   * usually what you want.
+   */
+  scrollIntoViewIfNeeded:
+  function LH_scrollIntoViewIfNeeded(elem, centered) {
+    // We want to default to centering the element in the page,
+    // so as to keep the context of the element.
+    centered = centered === undefined? true: !!centered;
+
+    let win = elem.ownerDocument.defaultView;
+    let clientRect = elem.getBoundingClientRect();
+
+    // The following are always from the {top, bottom, left, right}
+    // of the viewport, to the {top, …} of the box.
+    // Think of them as geometrical vectors, it helps.
+    // The origin is at the top left.
+
+    let topToBottom = clientRect.bottom;
+    let bottomToTop = clientRect.top - win.innerHeight;
+    let leftToRight = clientRect.right;
+    let rightToLeft = clientRect.left - win.innerWidth;
+    let xAllowed = true;  // We allow one translation on the x axis,
+    let yAllowed = true;  // and one on the y axis.
+
+    // Whatever `centered` is, the behavior is the same if the box is
+    // (even partially) visible.
+
+    if ((topToBottom > 0 || !centered) && topToBottom <= elem.offsetHeight) {
+      win.scrollBy(0, topToBottom - elem.offsetHeight);
+      yAllowed = false;
+    } else
+    if ((bottomToTop < 0 || !centered) && bottomToTop >= -elem.offsetHeight) {
+      win.scrollBy(0, bottomToTop + elem.offsetHeight);
+      yAllowed = false;
+    }
+
+    if ((leftToRight > 0 || !centered) && leftToRight <= elem.offsetWidth) {
+      if (xAllowed) {
+        win.scrollBy(leftToRight - elem.offsetWidth, 0);
+        xAllowed = false;
+      }
+    } else
+    if ((rightToLeft < 0 || !centered) && rightToLeft >= -elem.offsetWidth) {
+      if (xAllowed) {
+        win.scrollBy(rightToLeft + elem.offsetWidth, 0);
+        xAllowed = false;
+      }
+    }
+
+    // If we want it centered, and the box is completely hidden,
+    // then we center it explicitly.
+
+    if (centered) {
+
+      if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) {
+        win.scroll(win.scrollX,
+                   win.scrollY + clientRect.top
+                   - (win.innerHeight - elem.offsetHeight) / 2);
+      }
+
+      if (xAllowed && (leftToRight <= 0 || rightToLeft <= 0)) {
+        win.scroll(win.scrollX + clientRect.left
+                   - (win.innerWidth - elem.offsetWidth) / 2,
+                   win.scrollY);
+      }
+    }
+
+    if (win.parent !== win) {
+      // We are inside an iframe.
+      LH_scrollIntoViewIfNeeded(win.frameElement, centered);
+    }
+  },
 };
--- a/browser/devtools/shared/test/Makefile.in
+++ b/browser/devtools/shared/test/Makefile.in
@@ -15,22 +15,25 @@ include $(topsrcdir)/config/rules.mk
 _BROWSER_TEST_FILES = \
   browser_browser_basic.js \
   browser_promise_basic.js \
   browser_require_basic.js \
   browser_templater_basic.js \
   browser_toolbar_basic.js \
   browser_toolbar_tooltip.js \
   browser_toolbar_webconsole_errors_count.js \
+  browser_layoutHelpers.js \
   head.js \
   $(NULL)
 
 _BROWSER_TEST_PAGES = \
   browser_templater_basic.html \
   browser_toolbar_basic.html \
   browser_toolbar_webconsole_errors_count.html \
+  browser_layoutHelpers.html \
+  browser_layoutHelpers_iframe.html \
   $(NULL)
 
 libs:: $(_BROWSER_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
 
 libs:: $(_BROWSER_TEST_PAGES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_layoutHelpers.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset=utf-8>
+<title> Layout Helpers </title>
+
+<style>
+  html {
+    height: 300%;
+    width: 300%;
+  }
+  div#some {
+    position: absolute;
+    background: black;
+    width: 2px;
+    height: 2px;
+  }
+  iframe {
+    position: absolute;
+    width: 40px;
+    height: 40px;
+    border: 0;
+  }
+</style>
+
+<div id=some></div>
+<iframe id=frame src='./browser_layoutHelpers_iframe.html'></iframe>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_layoutHelpers.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that scrollIntoViewIfNeeded works properly.
+
+let imported = {};
+Components.utils.import("resource:///modules/devtools/LayoutHelpers.jsm",
+    imported);
+registerCleanupFunction(function () {
+  imported = {};
+});
+
+let LayoutHelpers = imported.LayoutHelpers;
+
+const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_layoutHelpers.html";
+
+function test() {
+  addTab(TEST_URI, function(browser, tab) {
+    info("Starting browser_layoutHelpers.js");
+    let doc = browser.contentDocument;
+    runTest(doc.defaultView, doc.getElementById('some'));
+    gBrowser.removeCurrentTab();
+    finish();
+  });
+}
+
+function runTest(win, some) {
+  some.style.top = win.innerHeight + 'px';
+  some.style.left = win.innerWidth + 'px';
+  // The tests start with a black 2x2 pixels square below bottom right.
+  // Do not resize the window during the tests.
+
+  win.scroll(win.innerWidth / 2, win.innerHeight + 2);  // Above the viewport.
+  LayoutHelpers.scrollIntoViewIfNeeded(some);
+  is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+     'Element completely hidden above should appear centered.');
+
+  win.scroll(win.innerWidth / 2, win.innerHeight + 1);  // On the top edge.
+  LayoutHelpers.scrollIntoViewIfNeeded(some);
+  is(win.scrollY, win.innerHeight,
+     'Element partially visible above should appear above.');
+
+  win.scroll(win.innerWidth / 2, 0);  // Just below the viewport.
+  LayoutHelpers.scrollIntoViewIfNeeded(some);
+  is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+     'Element completely hidden below should appear centered.');
+
+  win.scroll(win.innerWidth / 2, 1);  // On the bottom edge.
+  LayoutHelpers.scrollIntoViewIfNeeded(some);
+  is(win.scrollY, 2,
+     'Element partially visible below should appear below.');
+
+
+  win.scroll(win.innerWidth / 2, win.innerHeight + 2);  // Above the viewport.
+  LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+  is(win.scrollY, win.innerHeight,
+     'Element completely hidden above should appear above ' +
+     'if parameter is false.');
+
+  win.scroll(win.innerWidth / 2, win.innerHeight + 1);  // On the top edge.
+  LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+  is(win.scrollY, win.innerHeight,
+     'Element partially visible above should appear above ' +
+     'if parameter is false.');
+
+  win.scroll(win.innerWidth / 2, 0);  // Below the viewport.
+  LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+  is(win.scrollY, 2,
+     'Element completely hidden below should appear below ' +
+     'if parameter is false.');
+
+  win.scroll(win.innerWidth / 2, 1);  // On the bottom edge.
+  LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+  is(win.scrollY, 2,
+     'Element partially visible below should appear below ' +
+     'if parameter is false.');
+
+  // The case of iframes.
+  win.scroll(0, 0);
+
+  let frame = win.document.getElementById('frame');
+  let fwin = frame.contentWindow;
+
+  frame.style.top = win.innerHeight + 'px';
+  frame.style.left = win.innerWidth + 'px';
+
+  fwin.addEventListener('load', function frameLoad() {
+    let some = fwin.document.getElementById('some');
+    LayoutHelpers.scrollIntoViewIfNeeded(some);
+    is(win.scrollX, Math.floor(win.innerWidth / 2) + 20,
+       'Scrolling from an iframe should center the iframe vertically.');
+    is(win.scrollY, Math.floor(win.innerHeight / 2) + 20,
+       'Scrolling from an iframe should center the iframe horizontally.');
+    is(fwin.scrollX, Math.floor(fwin.innerWidth / 2) + 1,
+       'Scrolling from an iframe should center the element vertically.');
+    is(fwin.scrollY, Math.floor(fwin.innerHeight / 2) + 1,
+       'Scrolling from an iframe should center the element horizontally.');
+  }, false);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_layoutHelpers_iframe.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<meta charset=utf-8>
+<title> Layout Helpers </title>
+
+<style>
+  html {
+    height: 300%;
+    width: 300%;
+  }
+  div#some {
+    position: absolute;
+    background: black;
+    width: 2px;
+    height: 2px;
+  }
+</style>
+
+<div id=some></div>
+