diff --git a/.gitignore b/.gitignore index 9ebefdf..9ff142d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ public resources -assets/jsconfig.json \ No newline at end of file +assets/jsconfig.json +.hugo_build.lock \ No newline at end of file diff --git a/assets/jsconfig.json b/assets/jsconfig.json index 9321136..377218c 100644 --- a/assets/jsconfig.json +++ b/assets/jsconfig.json @@ -1,10 +1,10 @@ { - "compilerOptions": { - "baseUrl": ".", - "paths": { - "*": [ - "*" - ] - }, - } + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": [ + "*" + ] + } + } } \ No newline at end of file diff --git a/assets/scss/partials/base.scss b/assets/scss/partials/base.scss index ab3bf42..e7aefe6 100644 --- a/assets/scss/partials/base.scss +++ b/assets/scss/partials/base.scss @@ -1,7 +1,6 @@ html { font-size: 62.5%; overflow-y: scroll; - scroll-behavior: smooth; } * { diff --git a/assets/scss/partials/layout/article.scss b/assets/scss/partials/layout/article.scss index 7059853..9420ace 100644 --- a/assets/scss/partials/layout/article.scss +++ b/assets/scss/partials/layout/article.scss @@ -122,7 +122,6 @@ } .article-page.has-toc { - scroll-behavior: smooth; .left-sidebar { display: none; @@ -193,6 +192,10 @@ color: var(--card-text-color-main); overflow: hidden; + ::-webkit-scrollbar-thumb { + background-color: var(--card-separator-color); + } + #TableOfContents { overflow-x: auto; max-height: 75vh; @@ -220,7 +223,7 @@ } li { - margin: 15px 20px; + margin: 15px 0 15px 20px; padding: 5px; & > ol, @@ -234,6 +237,25 @@ } } } + li.active-class > a { + border-left: var(--heading-border-size) solid var(--accent-color); + font-weight: bold; + display: block; + } + & > ul > li.active-class > a { + margin-left: calc(-25px - 1em); + padding-left: calc(25px + 1em - var(--heading-border-size)); + } + + & > ul > li > ul > li.active-class > a { + margin-left: calc(-60px - 1em); + padding-left: calc(60px + 1em - var(--heading-border-size)); + } + + & > ul > li > ul > li > ul > li.active-class > a { + margin-left: calc(-95px - 1em); + padding-left: calc(95px + 1em - var(--heading-border-size)); + } } } diff --git a/assets/ts/main.ts b/assets/ts/main.ts index d79c127..0fd6d4e 100644 --- a/assets/ts/main.ts +++ b/assets/ts/main.ts @@ -10,6 +10,7 @@ import { getColor } from 'ts/color'; import menu from 'ts/menu'; import createElement from 'ts/createElement'; import StackColorScheme from 'ts/colorScheme'; +import { setupScrollspy } from 'ts/scrollspy'; let Stack = { init: () => { @@ -21,6 +22,7 @@ let Stack = { const articleContent = document.querySelector('.article-content') as HTMLElement; if (articleContent) { new StackGallery(articleContent); + setupScrollspy(".article-content h1[id], .article-content h2[id], .article-content h3[id], .article-content h4[id], .article-content h5[id], .article-content h6[id]", "#TableOfContents", "#TableOfContents li", "active-class") } /** diff --git a/assets/ts/scrollspy.ts b/assets/ts/scrollspy.ts new file mode 100644 index 0000000..6b34248 --- /dev/null +++ b/assets/ts/scrollspy.ts @@ -0,0 +1,99 @@ +// While solutions for debouncing like the ones in https://gomakethings.com/debouncing-your-javascript-events/ could work, +// we do need an actual debouncing of scroll events in order to only capture the "end" of the scroll. +function debounced(func: Function) { + let timeout; + return (e: Event) => { + /* + if (timeout) { + clearTimeout(timeout); + } + timeout = window.requestAnimationFrame(func); + */ + if (timeout) { + window.cancelAnimationFrame(timeout); + } + + timeout = window.requestAnimationFrame(() => func(e)); + } +} + +function setupScrollspy(headersQuery: string, tocQuery: string, navigationQuery: string, activeClass: string) { + let headers = document.querySelectorAll(headersQuery); + if (!headers) { + console.warn("No header matched query", headers); + return; + } + + let scrollableNavigation = document.querySelector(tocQuery); + 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 = []; + + headers.forEach((header: HTMLElement) => { sectionsOffsets.push({ id: header.id, offset: header.offsetTop }) }); + sectionsOffsets.sort((a, b) => a.offset - b.offset); + + let activeSectionLink: Element; + let tocHovered: boolean = false; + + function hoverHandler(isHovered: boolean) { + tocHovered = isHovered; + } + + function scrollHandler(e: Event) { + let scrollPosition = document.documentElement.scrollTop || document.body.scrollTop; + + let newActiveSection: HTMLElement; + + sectionsOffsets.forEach((section) => { + if (scrollPosition >= section.offset - 20) { + newActiveSection = document.getElementById(section.id); + } + }); + + let newActiveSectionLink: HTMLElement; + if (newActiveSection) { + for (let i = 0; i < navigation.length; i++) { + let link = navigation[i].querySelector("a"); + if (link.getAttribute("href") === "#" + newActiveSection.id) { + newActiveSectionLink = navigation[i] as HTMLElement; + break; + } + } + } + + if (newActiveSection && !newActiveSectionLink) { + console.warn("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) + let textHeight = newActiveSectionLink.querySelector("a").offsetHeight; + let scrollTop = newActiveSectionLink.offsetTop - scrollableNavigation.offsetHeight / 2 + textHeight / 2 - scrollableNavigation.offsetTop; + if (scrollTop < 0) { + scrollTop = 0; + } + scrollableNavigation.scrollTo({ top: scrollTop, behavior: "auto" }); + } + } + activeSectionLink = newActiveSectionLink; + } + } + + window.addEventListener("scroll", debounced(scrollHandler)); + scrollableNavigation.addEventListener("mouseenter", debounced(() => hoverHandler(true))); + scrollableNavigation.addEventListener("mouseleave", debounced(() => hoverHandler(false))); +} + +export { setupScrollspy }; \ No newline at end of file