From 29b19bcf8bc8f97069be00a9012c4cf1b7dbfb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Cab=C3=A9?= Date: Tue, 5 Dec 2023 15:17:47 +0100 Subject: [PATCH] doc: sphinx: Tweak page scroll behavior to maximize real estate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This picks up custom javscript from Godot documentation and uses CSS rules we already had in place (only they were not used) to tweak the page scroll behavior. As folks scroll down the page, the Zephyr logo in the top right corner gradually disappears, leaving more room for the navigation menu. Also, when scrolling in the navigation pane, the UI there is slightly adapted to make it more clear that the search box is "fixed", plus, when one reaches the bottom of the navigation tree and continues scrolling, the "main" page scrolls down. Signed-off-by: Benjamin Cabé --- doc/_static/js/custom.js | 122 +++++++++++++++++++++++++++++++++++++++ doc/conf.py | 1 + 2 files changed, 123 insertions(+) create mode 100644 doc/_static/js/custom.js diff --git a/doc/_static/js/custom.js b/doc/_static/js/custom.js new file mode 100644 index 00000000000..52f254b38c7 --- /dev/null +++ b/doc/_static/js/custom.js @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2020-2023, The Godot community + * Copyright (c) 2023, Benjamin Cabé + * SPDX-License-Identifier: CC-BY-3.0 + */ + + +// Handle page scroll and adjust sidebar accordingly. + +// 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. +const registerOnScrollEvent = (function(){ + // Configuration. + + // The number of pixels the user must scroll by before the logo is completely hidden. + const scrollTopPixels = 156; + // The target margin to be applied to the navigation bar when the logo is hidden. + const menuTopMargin = 54; + // The max-height offset when the logo is completely visible. + const menuHeightOffset_default = 210; + // The max-height offset when the logo is completely hidden. + const menuHeightOffset_fixed = 63; + // The distance between the two max-height offset values above; used for intermediate values. + const menuHeightOffset_diff = (menuHeightOffset_default - menuHeightOffset_fixed); + + // 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 $sidebar = $('.wy-side-scroll'); + const $search = $sidebar.children('.wy-side-nav-search'); + const $menu = $sidebar.children('.wy-menu-vertical'); + + if (mediaQuery.matches) { + // Entering the "desktop" state. + + // The main scroll event handler. + // Executed as the page is scrolled and once immediately as the page enters this state. + const handleMainScroll = (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)`); + } + } + }; + + // The sidebar scroll event handler. + // Executed as the sidebar is scrolled as well as after the main scroll. This is needed + // because the main scroll can affect the scrollable area of the sidebar. + const handleSidebarScroll = () => { + const menuElement = $menu.get(0); + const menuScrollTop = $menu.scrollTop(); + const menuScrollBottom = menuElement.scrollHeight - (menuScrollTop + menuElement.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'); + } + }; + + $search.addClass('fixed'); + + $window.scroll(function() { + handleMainScroll(window.scrollY); + handleSidebarScroll(); + }); + + $menu.scroll(function() { + handleSidebarScroll(); + }); + + handleMainScroll(window.scrollY); + handleSidebarScroll(); + } else { + // Entering the "mobile" state. + + $window.unbind('scroll'); + $menu.unbind('scroll'); + + $search.removeClass('fixed'); + + $search.css('margin-top', `0px`); + $menu.css('margin-top', `0px`); + $menu.css('max-height', 'initial'); + } + }; + })(); + + $(document).ready(() => { + // Initialize handlers for page scrolling and our custom sidebar. + const mediaQuery = window.matchMedia('only screen and (min-width: 769px)'); + + registerOnScrollEvent(mediaQuery); + mediaQuery.addListener(registerOnScrollEvent); + }); diff --git a/doc/conf.py b/doc/conf.py index c0ce3c07235..35afc15af16 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -339,4 +339,5 @@ linkcheck_anchors = False def setup(app): # theme customizations app.add_css_file("css/custom.css") + app.add_js_file("js/custom.js") app.add_js_file("js/dark-mode-toggle.min.mjs", type="module")