diff options
Diffstat (limited to 'themes/hugo-theme-stack/assets/ts/scrollspy.ts')
-rw-r--r-- | themes/hugo-theme-stack/assets/ts/scrollspy.ts | 131 |
1 files changed, 0 insertions, 131 deletions
diff --git a/themes/hugo-theme-stack/assets/ts/scrollspy.ts b/themes/hugo-theme-stack/assets/ts/scrollspy.ts deleted file mode 100644 index 8a14085..0000000 --- a/themes/hugo-theme-stack/assets/ts/scrollspy.ts +++ /dev/null @@ -1,131 +0,0 @@ -// Implements a scroll spy system for the ToC, displaying the current section with an indicator and scrolling to it when needed. - -// Inspired from https://gomakethings.com/debouncing-your-javascript-events/ -function debounced(func: Function) { - let timeout; - return () => { - if (timeout) { - window.cancelAnimationFrame(timeout); - } - - timeout = window.requestAnimationFrame(() => func()); - } -} - -const headersQuery = ".article-content h1[id], .article-content h2[id], .article-content h3[id], .article-content h4[id], .article-content h5[id], .article-content h6[id]"; -const tocQuery = "#TableOfContents"; -const navigationQuery = "#TableOfContents li"; -const activeClass = "active-class"; - -function scrollToTocElement(tocElement: HTMLElement, scrollableNavigation: HTMLElement) { - let textHeight = tocElement.querySelector("a").offsetHeight; - let scrollTop = tocElement.offsetTop - scrollableNavigation.offsetHeight / 2 + textHeight / 2 - scrollableNavigation.offsetTop; - if (scrollTop < 0) { - scrollTop = 0; - } - scrollableNavigation.scrollTo({ top: scrollTop, behavior: "smooth" }); -} - -type IdToElementMap = { [key: string]: HTMLElement }; - -function buildIdToNavigationElementMap(navigation: NodeListOf<Element>): IdToElementMap { - const sectionLinkRef: IdToElementMap = {}; - navigation.forEach((navigationElement: HTMLElement) => { - const link = navigationElement.querySelector("a"); - const href = link.getAttribute("href"); - if (href.startsWith("#")) { - sectionLinkRef[href.slice(1)] = navigationElement; - } - }); - - return sectionLinkRef; -} - -function computeOffsets(headers: NodeListOf<Element>) { - let sectionsOffsets = []; - headers.forEach((header: HTMLElement) => { sectionsOffsets.push({ id: header.id, offset: header.offsetTop }) }); - sectionsOffsets.sort((a, b) => a.offset - b.offset); - return sectionsOffsets; -} - -function setupScrollspy() { - let headers = document.querySelectorAll(headersQuery); - if (!headers) { - console.warn("No header matched query", headers); - return; - } - - let scrollableNavigation = document.querySelector(tocQuery) as HTMLElement | undefined; - if (!scrollableNavigation) { - console.warn("No toc matched query", tocQuery); - return; - } - - let navigation = document.querySelectorAll(navigationQuery); - if (!navigation) { - console.warn("No navigation matched query", navigationQuery); - return; - } - - let sectionsOffsets = computeOffsets(headers); - - // We need to avoid scrolling when the user is actively interacting with the ToC. Otherwise, if the user clicks on a link in the ToC, - // we would scroll their view, which is not optimal usability-wise. - let tocHovered: boolean = false; - scrollableNavigation.addEventListener("mouseenter", debounced(() => tocHovered = true)); - scrollableNavigation.addEventListener("mouseleave", debounced(() => tocHovered = false)); - - let activeSectionLink: Element; - - let idToNavigationElement: IdToElementMap = buildIdToNavigationElementMap(navigation); - - function scrollHandler() { - let scrollPosition = document.documentElement.scrollTop || document.body.scrollTop; - - let newActiveSection: HTMLElement | undefined; - - // Find the section that is currently active. - // It is possible for no section to be active, so newActiveSection may be undefined. - sectionsOffsets.forEach((section) => { - if (scrollPosition >= section.offset - 20) { - newActiveSection = document.getElementById(section.id); - } - }); - - // Find the link for the active section. Once again, there are a few edge cases: - // - No active section = no link => undefined - // - No active section but the link does not exist in toc (e.g. because it is outside of the applicable ToC levels) => undefined - let newActiveSectionLink: HTMLElement | undefined - if (newActiveSection) { - newActiveSectionLink = idToNavigationElement[newActiveSection.id]; - } - - if (newActiveSection && !newActiveSectionLink) { - // The active section does not have a link in the ToC, so we can't scroll to it. - console.debug("No link found for section", newActiveSection); - } else if (newActiveSectionLink !== activeSectionLink) { - if (activeSectionLink) - activeSectionLink.classList.remove(activeClass); - if (newActiveSectionLink) { - newActiveSectionLink.classList.add(activeClass); - if (!tocHovered) { - // Scroll so that newActiveSectionLink is in the middle of scrollableNavigation, except when it's from a manual click (hence the tocHovered check) - scrollToTocElement(newActiveSectionLink, scrollableNavigation); - } - } - activeSectionLink = newActiveSectionLink; - } - } - - window.addEventListener("scroll", debounced(scrollHandler)); - - // Resizing may cause the offset values to change: recompute them. - function resizeHandler() { - sectionsOffsets = computeOffsets(headers); - scrollHandler(); - } - - window.addEventListener("resize", debounced(resizeHandler)); -} - -export { setupScrollspy };
\ No newline at end of file |