« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/static
diff options
context:
space:
mode:
Diffstat (limited to 'src/static')
-rw-r--r--src/static/client.js77
-rw-r--r--src/static/site3.css58
2 files changed, 131 insertions, 4 deletions
diff --git a/src/static/client.js b/src/static/client.js
index 9ae5510a..47936d82 100644
--- a/src/static/client.js
+++ b/src/static/client.js
@@ -444,6 +444,81 @@ 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!
+
+  let wasHighlighted;
+
+  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);
+
+    if (!linked) {
+      return;
+    }
+
+    // Hide skipper box right away, so the layout is updated on time for the
+    // math operations coming up next.
+    const skipper = document.getElementById('skippers');
+    skipper.style.display = 'none';
+    setTimeout(() => skipper.style.display = '');
+
+    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'});
+    linked.focus({preventScroll: true});
+
+    const maxScroll =
+        document.body.scrollHeight
+      - window.innerHeight;
+
+    if (scrollY > maxScroll && linked.classList.contains('content-heading')) {
+      if (wasHighlighted) {
+        wasHighlighted.classList.remove('highlight-hash-link');
+      }
+
+      wasHighlighted = linked;
+      linked.classList.add('highlight-hash-link');
+      linked.addEventListener('animationend', function handle(evt) {
+        if (evt.animationName === 'highlight-hash-link') {
+          linked.removeEventListener('animationend', handle);
+          linked.classList.remove('highlight-hash-link');
+          wasHighlighted = null;
+        }
+      });
+    }
+  }
+}
+
+addHashLinkHandlers();
+
 // Sticky content heading ---------------------------------
 
 const stickyHeadingInfo = Array.from(document.querySelectorAll('.content-sticky-heading-container'))
@@ -510,7 +585,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 7abb5351..449e6fad 100644
--- a/src/static/site3.css
+++ b/src/static/site3.css
@@ -208,7 +208,19 @@ body::before {
   box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
 }
 
-#skippers > .skipper:not(:last-child)::after {
+#skippers > * {
+  display: inline-block;
+}
+
+#skippers > .skipper-list:not(:last-child)::after {
+  display: inline-block;
+  content: "\00a0";
+  margin-left: 2px;
+  margin-right: -2px;
+  border-left: 1px dotted;
+}
+
+#skippers .skipper-list > .skipper:not(:last-child)::after {
   content: " \00b7 ";
   font-weight: 800;
 }
@@ -342,14 +354,13 @@ body::before {
 .sidebar > details summary {
   margin-top: 0.5em;
   padding-left: 5px;
-  user-select: none;
 }
 
 .sidebar > details summary .group-name {
   color: var(--primary-color);
 }
 
-.sidebar > details summary:hover {
+.sidebar > details summary > span:hover {
   cursor: pointer;
   text-decoration: underline;
   text-decoration-color: var(--primary-color);
@@ -1138,8 +1149,49 @@ html[data-url-key="localized.home"] .carousel-container {
   margin-bottom: 0;
 }
 
+/* Custom hash links */
+
+.content-heading {
+  border-bottom: 3px double transparent;
+  margin-bottom: -3px;
+}
+
+.content-heading.highlight-hash-link {
+  animation: highlight-hash-link 4s;
+  animation-delay: 125ms;
+}
+
+/* This animation's name is referenced in JavaScript */
+@keyframes highlight-hash-link {
+  0% {
+    border-bottom-color: transparent;
+  }
+
+  10% {
+    border-bottom-color: white;
+  }
+
+  25% {
+    border-bottom-color: white;
+  }
+
+  100% {
+    border-bottom-color: transparent;
+  }
+}
+
 /* Sticky heading */
 
+#content [id] {
+  /* Adjust scroll margin. */
+  scroll-margin-top: calc(
+      74px /* Sticky heading */
+    + 33px /* Sticky subheading */
+    - 1em  /* One line of text (align bottom) */
+    - 12px /* Padding for hanging letters & focus ring */
+  );
+}
+
 .content-sticky-heading-container {
   position: sticky;
   top: 0;