diff --git a/_static/css/custom.css b/_static/css/custom.css index ee234a8eb..0c0b3e45a 100644 --- a/_static/css/custom.css +++ b/_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) { diff --git a/_static/js/custom.js b/_static/js/custom.js index 5e75863af..bcc07a0f0 100644 --- a/_static/js/custom.js +++ b/_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; +// Handle page scroll and adjust sidebar accordingly. -// 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); +// 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. -// 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'); + // 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); - 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`); + // 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. - if (currentScroll > 0) { - const scrolledPercent = (scrollTopPixels - currentScroll) / scrollTopPixels; - const offsetValue = menuHeightOffset_fixed + menuHeightOffset_diff * scrolledPercent; - $menu.css('max-height', `calc(100% - ${offsetValue}px)`); + // 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)`); + } + } + }; + + $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 { - $menu.css('max-height', `calc(100% - ${menuHeightOffset_default}px)`); + $menu.css('padding-bottom', `0px`); } - }; - $search.addClass('fixed'); - $window.scroll(function() { - const currentScroll = window.scrollY; + $window.scroll(function() { + handleScroll(window.scrollY); + }); - if (currentScroll >= scrollTopPixels) { - $search.css('margin-top', `-${scrollTopPixels}px`); - $menu.css('margin-top', `${menuTopMargin}px`); + $menu.scroll(function() { + const menuScrollTop = $(this).scrollTop(); + const menuScrollBottom = this.scrollHeight - (menuScrollTop + this.offsetHeight); - $menu.css('max-height', `calc(100% - ${menuHeightOffset_fixed}px)`); - } - else { - handleEarlyScroll(currentScroll); - } - }); + // 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'); + } - $menu.scroll(function() { - if ($(this).scrollTop() > 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'); + } + }) - 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'); + 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)');