diff options
author | (quasar) nebula <qznebula@protonmail.com> | 2023-02-27 10:30:12 -0400 |
---|---|---|
committer | (quasar) nebula <qznebula@protonmail.com> | 2023-02-27 10:30:12 -0400 |
commit | 8a24f02782b7f2454ef67d6ece0b62b808175a21 (patch) | |
tree | b68e615d493133bd0d4639ead73f2abb23ecb5fb | |
parent | 999fc0c58485649aee8e4deb3ea6419aa687c228 (diff) |
smooth hash link scrolling & anchor scroll offset
-rw-r--r-- | src/static/client.js | 43 | ||||
-rw-r--r-- | src/static/site3.css | 10 |
2 files changed, 52 insertions, 1 deletions
diff --git a/src/static/client.js b/src/static/client.js index 15f21fdb..87b74004 100644 --- a/src/static/client.js +++ b/src/static/client.js @@ -444,6 +444,47 @@ if (localStorage.tryInfoCards) { addInfoCardLinkHandlers('track'); } +// Custom hash links -------------------------------------- + +function addHashLinkHandlers() { + // Instead of defining a scroll offset (to account for the sticky heading) + // in JavaScript, we interface with the CSS property 'scroll-margin-top'. + // This lets the scroll offset be consolidated where it makes sense, and + // sets an appropriate offset when (re)loading a page with hash for free! + + for (const a of document.links) { + const href = a.getAttribute('href'); + if (!href || !href.startsWith('#')) { + continue; + } + + a.addEventListener('click', handleHashLinkClicked); + } + + function handleHashLinkClicked(evt) { + if (evt.metaKey || evt.shiftKey || evt.ctrlKey || evt.altKey) { + return; + } + + const href = evt.target.getAttribute('href'); + const id = href.slice(1); + const linked = document.getElementById(id); + const box = linked.getBoundingClientRect(); + const style = window.getComputedStyle(linked); + + const scrollY = + window.scrollY + + box.top + - style['scroll-margin-top'].replace('px', ''); + + evt.preventDefault(); + history.pushState({}, '', href); + window.scrollTo({top: scrollY, behavior: 'smooth'}); + } +} + +addHashLinkHandlers(); + // Sticky content heading --------------------------------- const stickyHeadingInfo = Array.from(document.querySelectorAll('.content-sticky-heading-container')) @@ -510,7 +551,7 @@ function updateStickyHeading() { for (let i = contentHeadings.length - 1; i >= 0; i--) { const heading = contentHeadings[i]; const headingRect = heading.getBoundingClientRect(); - if (headingRect.y + headingRect.height / 1.5 < stickyBottom) { + if (headingRect.y + headingRect.height / 1.5 < stickyBottom + 20) { closestHeading = heading; break; } diff --git a/src/static/site3.css b/src/static/site3.css index cc853b65..22a92f84 100644 --- a/src/static/site3.css +++ b/src/static/site3.css @@ -1140,6 +1140,16 @@ html[data-url-key="localized.home"] .carousel-container { /* Sticky heading */ +#content [id] { + /* Adjust scroll margin. */ + scroll-margin-top: calc( + 74px /* Sticky heading */ + + 33px /* Sticky subheading */ + - 1em /* One line of text (align bottom) */ + - 4px /* Padding for hanging letters */ + ); +} + .content-sticky-heading-container { position: sticky; top: 0; |