From ea6424fed021f3daa6f25e89fe2e86b93d61dec1 Mon Sep 17 00:00:00 2001 From: Aaron Pham Date: Wed, 6 Nov 2024 22:26:12 -0500 Subject: [PATCH] chore: update latest Signed-off-by: Aaron Pham --- quartz/components/scripts/sidenotes.inline.ts | 111 +++++++++--------- quartz/components/styles/sidenotes.scss | 28 ++++- 2 files changed, 84 insertions(+), 55 deletions(-) diff --git a/quartz/components/scripts/sidenotes.inline.ts b/quartz/components/scripts/sidenotes.inline.ts index b52c943fd..2f56d9c34 100644 --- a/quartz/components/scripts/sidenotes.inline.ts +++ b/quartz/components/scripts/sidenotes.inline.ts @@ -4,18 +4,6 @@ const ARTICLE_CONTENT_SELECTOR = ".center" const FOOTNOTE_SECTION_SELECTOR = "section[data-footnotes] > ol" const INDIVIDUAL_FOOTNOTE_SELECTOR = "li[id^='user-content-fn-']" -// Computes an offset such that setting `top` on elemToAlign will put it -// in vertical alignment with targetAlignment. -function computeOffsetForAlignment(elemToAlign: HTMLElement, targetAlignment: HTMLElement) { - const offsetParentTop = elemToAlign.offsetParent!.getBoundingClientRect().top - return targetAlignment.getBoundingClientRect().top - offsetParentTop -} - -// Clamp value between min and max -function clamp(value: number, min: number, max: number): number { - return Math.max(min, Math.min(value, max)) -} - function isInViewport(element: HTMLElement, buffer: number = 100) { const rect = element.getBoundingClientRect() return ( @@ -24,13 +12,17 @@ function isInViewport(element: HTMLElement, buffer: number = 100) { ) } +function computeOffsetForAlignment(elemToAlign: HTMLElement, targetAlignment: HTMLElement) { + const elemRect = elemToAlign.getBoundingClientRect() + const targetRect = targetAlignment.getBoundingClientRect() + const parentRect = elemToAlign.parentElement?.getBoundingClientRect() || elemRect + return targetRect.top - parentRect.top +} + // Get bounds for the sidenote positioning -function getSidenoteBounds( - sideContainer: HTMLElement, - sidenote: HTMLElement, -): { min: number; max: number } { - const containerRect = sideContainer.getBoundingClientRect() - const sidenoteRect = sidenote.getBoundingClientRect() +function getBounds(parent: HTMLElement, child: HTMLElement): { min: number; max: number } { + const containerRect = parent.getBoundingClientRect() + const sidenoteRect = child.getBoundingClientRect() return { min: 0, @@ -38,37 +30,40 @@ function getSidenoteBounds( } } -function updateSidenotes( - articleContent: HTMLElement, - sideContainer: HTMLElement, - footnoteElements: NodeListOf, -) { - footnoteElements.forEach((sidenote) => { +function updatePosition(ref: HTMLElement, child: HTMLElement, parent: HTMLElement) { + // Calculate ideal position + let referencePosition = computeOffsetForAlignment(child, ref) + + // Get bounds for this sidenote + const bounds = getBounds(parent, child) + + // Clamp the position within bounds + referencePosition = Math.max(referencePosition, Math.min(bounds.min, bounds.max)) + + // Apply position + child.style.top = `${referencePosition}px` +} + +function updateSidenotes() { + const articleContent = document.querySelector(ARTICLE_CONTENT_SELECTOR) as HTMLElement + const sideContainer = document.querySelector(".sidenotes") as HTMLElement + if (!articleContent || !sideContainer) return + + const sidenotes = sideContainer.querySelectorAll(".sidenote-element") as NodeListOf + for (const sidenote of sidenotes) { const sideId = sidenote.id.replace("sidebar-", "") const intextLink = articleContent.querySelector(`a[href="#${sideId}"]`) as HTMLElement if (!intextLink) return - // Calculate ideal position - let referencePosition = computeOffsetForAlignment(sidenote, intextLink) - - // Get bounds for this sidenote - const bounds = getSidenoteBounds(sideContainer, sidenote) - - // Clamp the position within bounds - referencePosition = clamp(referencePosition, bounds.min, bounds.max) - - // Apply position - sidenote.style.top = `${referencePosition}px` - - // Update visibility state if (isInViewport(intextLink)) { sidenote.classList.add("in-view") intextLink.classList.add("active") + updatePosition(intextLink, sidenote, sideContainer) } else { sidenote.classList.remove("in-view") intextLink.classList.remove("active") } - }) + } } function debounce(fn: Function, delay: number) { @@ -81,8 +76,11 @@ function debounce(fn: Function, delay: number) { document.addEventListener("nav", () => { const articleContent = document.querySelector(ARTICLE_CONTENT_SELECTOR) as HTMLElement - const footnoteSection = document.querySelector(FOOTNOTE_SECTION_SELECTOR) - if (!footnoteSection || !articleContent) return + const footnoteSections = Array.from(document.querySelectorAll(FOOTNOTE_SECTION_SELECTOR)) + if (footnoteSections.length == 0 || !articleContent) return + + const lastIdx = footnoteSections.length - 1 + const footnoteSection = footnoteSections[lastIdx] as HTMLElement const sideContainer = document.querySelector(".sidenotes") as HTMLElement if (!sideContainer) return @@ -101,7 +99,7 @@ document.addEventListener("nav", () => { INDIVIDUAL_FOOTNOTE_SELECTOR, ) as NodeListOf - footnotes.forEach((footnote) => { + for (const footnote of footnotes) { const footnoteId = footnote.id const intextLink = articleContent.querySelector(`a[href="#${footnoteId}"]`) as HTMLElement if (!intextLink) return @@ -109,28 +107,33 @@ document.addEventListener("nav", () => { const sidenote = document.createElement("li") sidenote.classList.add("sidenote-element") sidenote.style.position = "absolute" + const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) + sidenote.style.maxWidth = `${sideContainer.offsetWidth - rootFontSize}px` sidenote.id = `sidebar-${footnoteId}` const cloned = footnote.cloneNode(true) as HTMLElement + const backref = cloned.querySelector("a[data-footnote-backref]") + backref?.remove() sidenote.append(...cloned.children) + // create inner child container + let innerContainer = sidenote.querySelector(".sidenote-inner") + if (!innerContainer) { + innerContainer = document.createElement("div") as HTMLDivElement + innerContainer.className = "sidenote-inner" + while (sidenote.firstChild) { + innerContainer.appendChild(sidenote.firstChild) + } + sidenote.appendChild(innerContainer) + } + ol.appendChild(sidenote) - }) + } - // Get all sidenotes for updates - const sidenotes = sideContainer.querySelectorAll(".sidenote-element") as NodeListOf - - // Initial position update - updateSidenotes(articleContent, sideContainer, sidenotes) + updateSidenotes() // Update on scroll with debouncing - const debouncedUpdate = debounce( - () => updateSidenotes(articleContent, sideContainer, sidenotes), - 16, // ~60fps - ) + const debouncedUpdate = debounce(updateSidenotes, 2) - // Add scroll listener document.addEventListener("scroll", debouncedUpdate, { passive: true }) - - // Add resize listener window.addEventListener("resize", debouncedUpdate, { passive: true }) // Cleanup diff --git a/quartz/components/styles/sidenotes.scss b/quartz/components/styles/sidenotes.scss index 0998d03ec..9ae9b3b87 100644 --- a/quartz/components/styles/sidenotes.scss +++ b/quartz/components/styles/sidenotes.scss @@ -4,12 +4,38 @@ } & .sidenote-element { - transition: opacity 0.2s ease; + position: absolute; + transition: opacity 0.2s ease-in-out; opacity: 0; + display: block; margin-bottom: 1rem; + border: 1px solid var(--gray); + counter-increment: sidenote-counter; + background-color: var(--light); + + &::before { + content: counter(sidenote-counter); + background-color: var(--light); + font-size: 0.8em; + font-weight: bold; + margin-right: 0.5rem; + position: absolute; + top: -12px; + left: 12px; + padding: 1px 8px; + border: 1px solid var(--tertiary); + } &.in-view { opacity: 1; } + + & .sidenote-inner { + max-height: 200px; + overflow-y: auto; + width: 100%; + box-sizing: border-box; + padding: 0.2rem 1rem; + } } }