Browse Source

Improve navbar scrolling in the live version of the docs (#3368)

Yuri Sizov 5 years ago
parent
commit
4f7ee00821
2 changed files with 121 additions and 61 deletions
  1. 11 0
      _static/css/custom.css
  2. 110 61
      _static/js/custom.js

+ 11 - 0
_static/css/custom.css

@@ -843,6 +843,11 @@ kbd, .kbd {
     margin-right: 0;
 }
 
+/* Banner panel in sidebar */
+.wy-nav-side .ethical-rtd.fixed {
+    position: fixed
+}
+
 /* Version selector (only visible on Read the Docs) */
 
 .rst-versions {
@@ -895,8 +900,14 @@ kbd, .kbd {
 }
 
 /* Allows the navbar's scrollbar to be shown */
+@media only screen and (min-width: 769px) {
+    .wy-side-scroll {
+        overflow: hidden;
+    }
+}
 .wy-menu.wy-menu-vertical {
     overflow-y: auto;
+    overflow-x: hidden;
     max-height: calc(100% - 348px);
 }
 @media screen and (max-width: 768px) {

+ 110 - 61
_static/js/custom.js

@@ -1,71 +1,120 @@
-// The number of pixels the user must scroll by before the logo is hidden.
-const scrollTopPixels = 234;
-
-// The margin to apply to the menu when the search bar is made fixed.
-// Should roughly match the logo's height as to not hide the top menu items
-// behind it.
-const menuTopMargin = 90;
-const menuHeightOffset_default = 338;
-const menuHeightOffset_fixed = 102;
-const menuHeightOffset_diff = (menuHeightOffset_default - menuHeightOffset_fixed);
-
-// Hide the navigation bar logo when scrolling down on desktop platforms.
-// The logo is quite tall, so this helps make the rest of the navigation bar
-// more readable.
-function registerOnScrollEvent(mediaQuery) {
-  const $window = $(window);
-  const $menu = $('.wy-menu-vertical');
-  const $search = $('.wy-side-nav-search');
-
-  if (mediaQuery.matches) {
-    // We're on desktop; register the scroll event.
-    const handleEarlyScroll = (currentScroll) => {
-      $search.css('margin-top', `-${currentScroll}px`);
-      $menu.css('margin-top', `${menuTopMargin + (scrollTopPixels - currentScroll)}px`);
-
-      if (currentScroll > 0) {
-        const scrolledPercent = (scrollTopPixels - currentScroll) / scrollTopPixels;
-        const offsetValue = menuHeightOffset_fixed + menuHeightOffset_diff * scrolledPercent;
-        $menu.css('max-height', `calc(100% - ${offsetValue}px)`);
-      } else {
-        $menu.css('max-height', `calc(100% - ${menuHeightOffset_default}px)`);
-      }
-    };
+// Handle page scroll and adjust sidebar accordingly.
 
-    $search.addClass('fixed');
-    $window.scroll(function() {
-      const currentScroll = window.scrollY;
+// Each page has two scrolls: the main scroll, which is moving the content of the page;
+// and the sidebar scroll, which is moving the navigation in the sidebar.
+// We want the logo to gradually disappear as the main content is scrolled, giving
+// more room to the navigation on the left. This means adjusting the height
+// available to the navigation on the fly. There is also a banner below the navigation
+// that must be dealt with simultaneously.
+const registerOnScrollEvent = (function(){
+  // Configuration.
 
-      if (currentScroll >= scrollTopPixels) {
-        $search.css('margin-top', `-${scrollTopPixels}px`);
-        $menu.css('margin-top', `${menuTopMargin}px`);
+  // The number of pixels the user must scroll by before the logo is completely hidden.
+  const scrollTopPixels = 234;
+  // The target margin to be applied to the navigation bar when the logo is hidden.
+  const menuTopMargin = 90;
+  // The max-height offset when the logo is completely visible.
+  const menuHeightOffset_default = 338;
+  // The max-height offset when the logo is completely hidden.
+  const menuHeightOffset_fixed = 102;
+  // The distance between the two max-height offset values above; used for intermediate values.
+  const menuHeightOffset_diff = (menuHeightOffset_default - menuHeightOffset_fixed);
 
-        $menu.css('max-height', `calc(100% - ${menuHeightOffset_fixed}px)`);
-      }
-      else {
-        handleEarlyScroll(currentScroll);
-      }
-    });
+  // Media query handler.
+  return function(mediaQuery) {
+    // We only apply this logic to the "desktop" resolution (defined by a media query at the bottom).
+    // This handler is executed when the result of the query evaluation changes, which means that 
+    // the page has moved between "desktop" and "mobile" states.
+
+    // When entering the "desktop" state, we register scroll events and adjust elements on the page.
+    // When entering the "mobile" state, we clean up any registered events and restore elements on the page
+    // to their initial state.
+
+    const $window = $(window);
+    const $menu = $('.wy-menu-vertical');
+    const $search = $('.wy-side-nav-search');
+    const $ethical = $('.ethical-rtd');
+
+    if (mediaQuery.matches) {
+      // Entering the "desktop" state.
+
+      // The scroll event handler.
+      // Executed as the page is scrolled and once immediatelly as the page enters this state.
+      const handleScroll = (currentScroll) => {
+        if (currentScroll >= scrollTopPixels) {
+          // After the page is scrolled below the threshold, we fix everything in place.
+          $search.css('margin-top', `-${scrollTopPixels}px`);
+          $menu.css('margin-top', `${menuTopMargin}px`);
+          $menu.css('max-height', `calc(100% - ${menuHeightOffset_fixed}px)`);
+        }
+        else {
+          // Between the top of the page and the threshold we calculate intermediate values 
+          // to guarantee a smooth transition.
+          $search.css('margin-top', `-${currentScroll}px`);
+          $menu.css('margin-top', `${menuTopMargin + (scrollTopPixels - currentScroll)}px`);
+
+          if (currentScroll > 0) {
+            const scrolledPercent = (scrollTopPixels - currentScroll) / scrollTopPixels;
+            const offsetValue = menuHeightOffset_fixed + menuHeightOffset_diff * scrolledPercent;
+            $menu.css('max-height', `calc(100% - ${offsetValue}px)`);
+          } else {
+            $menu.css('max-height', `calc(100% - ${menuHeightOffset_default}px)`);
+          }
+        }
+      };
 
-    $menu.scroll(function() {
-      if ($(this).scrollTop() > 0) {
-        $search.addClass('fixed-and-scrolled');
+      $search.addClass('fixed');
+      $ethical.addClass('fixed');
+
+      // Adjust the inner height of navigation so that the banner can be overlaid there later.
+      const ethicalOffsetBottom = $ethical.height() || 0;
+      if (ethicalOffsetBottom) {
+        $menu.css('padding-bottom', `${ethicalOffsetBottom}px`);
       } else {
-        $search.removeClass('fixed-and-scrolled');
+        $menu.css('padding-bottom', `0px`);
       }
-    })
-
-    handleEarlyScroll(window.scrollY);
-  } else {
-    // We're on mobile; unregister the scroll event so the logo isn't hidden
-    // when scrolling and clean up any changed properties.
-    $window.unbind('scroll');
-    $search.removeClass('fixed');
-    $search.css('margin-top', `0px`);
-    $menu.css('margin-top', `0px`);
-    $menu.css('max-height', 'initial');
+
+      $window.scroll(function() {
+        handleScroll(window.scrollY);
+      });
+
+      $menu.scroll(function() {
+        const menuScrollTop = $(this).scrollTop();
+        const menuScrollBottom = this.scrollHeight - (menuScrollTop + this.offsetHeight);
+
+        // As the navigation is scrolled we add a shadow to the top bar hanging over it.
+        if (menuScrollTop > 0) {
+          $search.addClass('fixed-and-scrolled');
+        } else {
+          $search.removeClass('fixed-and-scrolled');
+        }
+
+        // Near the bottom we start moving the sidebar banner into view.
+        if (menuScrollBottom < ethicalOffsetBottom) {
+          $ethical.css('margin-top', `-${ethicalOffsetBottom - menuScrollBottom}px`);
+        } else {
+          $ethical.css('margin-top', '0px');
+        }
+      })
+
+      handleScroll(window.scrollY);
+    } else {
+      // Entering the "mobile" state.
+
+      $window.unbind('scroll');
+      $menu.unbind('scroll');
+
+      $search.removeClass('fixed');
+      $ethical.removeClass('fixed');
+
+      $search.css('margin-top', `0px`);
+      $menu.css('margin-top', `0px`);
+      $menu.css('padding-bottom', `0px`);
+      $menu.css('max-height', 'initial');
+      $ethical.css('margin-top', '0px');
+    }
   }
-}
+})();
 
 $(document).ready(() => {
   const mediaQuery = window.matchMedia('only screen and (min-width: 769px)');