« get me outta code hell

smooth hash link scrolling & anchor scroll offset - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/static/client.js
diff options
context:
space:
mode:
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
commit8a24f02782b7f2454ef67d6ece0b62b808175a21 (patch)
treeb68e615d493133bd0d4639ead73f2abb23ecb5fb /src/static/client.js
parent999fc0c58485649aee8e4deb3ea6419aa687c228 (diff)
smooth hash link scrolling & anchor scroll offset
Diffstat (limited to 'src/static/client.js')
-rw-r--r--src/static/client.js43
1 files changed, 42 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;
         }