« get me outta code hell

sticky subheadings - 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:
author(quasar) nebula <qznebula@protonmail.com>2022-12-03 21:28:15 -0400
committer(quasar) nebula <qznebula@protonmail.com>2022-12-03 21:28:15 -0400
commit4a1997b0edd7de4b124c17e3cdeb1a47ecea1095 (patch)
tree57665b4938178c054c5e2c634fd2d3772c013285 /src/static
parent690a7b53a72ac71f9f76260fa50c634566c4e984 (diff)
sticky subheadings
Diffstat (limited to 'src/static')
-rw-r--r--src/static/client.js87
-rw-r--r--src/static/site2.css70
2 files changed, 154 insertions, 3 deletions
diff --git a/src/static/client.js b/src/static/client.js
index b5ed6868..ebe8604f 100644
--- a/src/static/client.js
+++ b/src/static/client.js
@@ -443,3 +443,90 @@ function addInfoCardLinkHandlers(type) {
 if (localStorage.tryInfoCards) {
   addInfoCardLinkHandlers('track');
 }
+
+// Sticky content heading ---------------------------------
+
+const stickyHeadingInfo = Array.from(document.querySelectorAll('.content-sticky-heading-container'))
+  .map(stickyContainer => {
+    const {parentElement: contentContainer} = stickyContainer;
+    const stickySubheading = stickyContainer.querySelector('.content-sticky-subheading');
+    const contentHeadings = Array.from(contentContainer.querySelectorAll('.content-heading'));
+
+    return {
+      contentContainer,
+      contentHeadings,
+      stickyContainer,
+      stickySubheading,
+      state: {
+        displayedHeading: null,
+      },
+    };
+  });
+
+const topOfViewInside = (el, scroll = window.scrollY) => (
+  scroll > el.offsetTop &&
+  scroll < el.offsetTop + el.offsetHeight
+);
+
+function updateStickyHeading() {
+  for (const {
+    contentContainer,
+    contentHeadings,
+    stickyContainer,
+    stickySubheading,
+    state,
+  } of stickyHeadingInfo) {
+    let closestHeading = null;
+
+    if (topOfViewInside(contentContainer)) {
+      if (stickySubheading.childNodes.length === 0) {
+        // &nbsp; to ensure correct basic line height
+        stickySubheading.appendChild(document.createTextNode('\xA0'));
+      }
+
+      const stickyRect = stickyContainer.getBoundingClientRect();
+      const subheadingRect = stickySubheading.getBoundingClientRect();
+      const stickyBottom = stickyRect.bottom + subheadingRect.height;
+
+      // This array is reversed so that we're starting from the bottom when
+      // iterating over it.
+      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) {
+          closestHeading = heading;
+          break;
+        }
+      }
+    }
+
+    if (state.displayedHeading !== closestHeading) {
+      if (closestHeading) {
+        // Array.from needed to iterate over a live array with for..of
+        for (const child of Array.from(stickySubheading.childNodes)) {
+          child.remove();
+        }
+
+        for (const child of closestHeading.childNodes) {
+          if (child.tagName === 'A') {
+            for (const grandchild of child.childNodes) {
+              stickySubheading.appendChild(grandchild.cloneNode(true));
+            }
+          } else {
+            stickySubheading.appendChild(child.cloneNode(true));
+          }
+        }
+
+        stickySubheading.classList.add('visible');
+      } else {
+        stickySubheading.classList.remove('visible');
+      }
+
+      state.displayedHeading = closestHeading;
+    }
+  }
+}
+
+document.addEventListener('scroll', updateStickyHeading);
+
+updateStickyHeading();
diff --git a/src/static/site2.css b/src/static/site2.css
index 33553e84..070d89ee 100644
--- a/src/static/site2.css
+++ b/src/static/site2.css
@@ -983,11 +983,15 @@ li > ul {
 
 /* sticky headers */
 
-#content:not(.no-sticky-heading) h1:first-of-type,
+#content:not(.no-sticky-heading) > h1:first-of-type,
 .sidebar:not(.no-sticky-heading) h1:first-of-type {
   position: sticky;
   top: 0;
+}
 
+#content .content-sticky-heading-container h1,
+#content:not(.no-sticky-heading) > h1:first-of-type,
+.sidebar:not(.no-sticky-heading) h1:first-of-type {
   margin: calc(-1 * var(--content-padding));
   margin-bottom: calc(0.5 * var(--content-padding));
   padding:
@@ -997,14 +1001,74 @@ li > ul {
     20px;
 
   background: var(--bg-black-color);
-  border-bottom: 1px dotted rgba(180, 180, 180, 0.4);
+  border-bottom: 1px dotted rgba(220, 220, 220, 0.4);
 
   -webkit-backdrop-filter: blur(6px);
           backdrop-filter: blur(6px);
 }
 
-#content:not(.no-sticky-heading) h1:first-of-type {
+#content .content-sticky-heading-container {
+  position: sticky;
+  top: 0;
+
+  /* Safari doesn't always play nicely with position: sticky,
+   * this seems to fix images sometimes displaying above the
+   * position: absolute subheading (h2) child
+   *
+   * See also: https://stackoverflow.com/questions/50224855/
+   */
+  transform: translate3d(0, 0, 0);
   z-index: 1;
+}
+
+#content .content-sticky-heading-container h1 {
+  margin-bottom: 0;
+}
+
+#content .content-sticky-heading-container h2 {
+  position: absolute;
+  margin: 0 calc(-1 * var(--content-padding));
+  width: 100%;
+  padding: 10px 40px 5px 20px;
+
+  font-size: 0.9em;
+  font-weight: normal;
+  font-style: oblique;
+  color: #eee;
+
+  background: var(--bg-black-color);
+  border-bottom: 1px dotted rgba(220, 220, 220, 0.4);
+
+  -webkit-backdrop-filter: blur(3px);
+          backdrop-filter: blur(3px);
+
+  transition: margin-top 0.35s, opacity 0.25s;
+}
+
+#content .content-sticky-heading-container h2:not(.visible) {
+  margin-top: -20px;
+  opacity: 0;
+}
+
+#content .content-sticky-heading-container h2.visible {
+  margin-top: 0;
+  opacity: 1;
+}
+
+#content:not(.no-sticky-heading) > h1:first-of-type {
+  z-index: 1;
+  box-shadow:
+    inset 0 10px 10px -5px var(--shadow-color),
+    0 4px 4px rgba(0, 0, 0, 0.8);
+}
+
+#content .content-sticky-heading-container h1 {
+  box-shadow:
+    inset 0 10px 10px -5px var(--shadow-color),
+    0 4px 4px rgba(0, 0, 0, 0.8);
+}
+
+#content .content-sticky-heading-container h2.visible {
   box-shadow:
     inset 0 10px 10px -5px var(--shadow-color),
     0 4px 4px rgba(0, 0, 0, 0.8);