diff options
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 |
commit | 4a1997b0edd7de4b124c17e3cdeb1a47ecea1095 (patch) | |
tree | 57665b4938178c054c5e2c634fd2d3772c013285 /src/static | |
parent | 690a7b53a72ac71f9f76260fa50c634566c4e984 (diff) |
sticky subheadings
Diffstat (limited to 'src/static')
-rw-r--r-- | src/static/client.js | 87 | ||||
-rw-r--r-- | src/static/site2.css | 70 |
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) { + // 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); |