mirror of
https://github.com/CaiJimmy/hugo-theme-stack.git
synced 2025-04-29 20:13:31 +08:00
Scrollspy actually works now
This commit is contained in:
parent
6d8d55b6d0
commit
dfad51cdec
@ -11,6 +11,7 @@ import menu from 'ts/menu';
|
|||||||
import createElement from 'ts/createElement';
|
import createElement from 'ts/createElement';
|
||||||
import StackColorScheme from 'ts/colorScheme';
|
import StackColorScheme from 'ts/colorScheme';
|
||||||
import { setupScrollspy } from 'ts/scrollspy';
|
import { setupScrollspy } from 'ts/scrollspy';
|
||||||
|
import { setupSmoothAnchors } from "ts/smoothAnchors";
|
||||||
|
|
||||||
let Stack = {
|
let Stack = {
|
||||||
init: () => {
|
init: () => {
|
||||||
@ -22,7 +23,8 @@ let Stack = {
|
|||||||
const articleContent = document.querySelector('.article-content') as HTMLElement;
|
const articleContent = document.querySelector('.article-content') as HTMLElement;
|
||||||
if (articleContent) {
|
if (articleContent) {
|
||||||
new StackGallery(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")
|
setupSmoothAnchors();
|
||||||
|
setupScrollspy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
// Implements a scroll spy system for the ToC, displaying the current section with an indicator and scrolling to it when needed.
|
||||||
|
|
||||||
// While solutions for debouncing like the ones in https://gomakethings.com/debouncing-your-javascript-events/ could work,
|
// 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.
|
// we do need an actual debouncing of scroll events in order to only capture the "end" of the scroll.
|
||||||
function debounced(func: Function) {
|
function debounced(func: Function) {
|
||||||
@ -17,14 +19,39 @@ function debounced(func: Function) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupScrollspy(headersQuery: string, tocQuery: string, navigationQuery: string, activeClass: string) {
|
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" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLinkForSectionId(sectionId: string, navigation: NodeListOf<Element>): HTMLElement | undefined {
|
||||||
|
for (let i = 0; i < navigation.length; i++) {
|
||||||
|
let link = navigation[i].querySelector("a");
|
||||||
|
if (link.getAttribute("href") === "#" + sectionId) {
|
||||||
|
return navigation[i] as HTMLElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupScrollspy() {
|
||||||
let headers = document.querySelectorAll(headersQuery);
|
let headers = document.querySelectorAll(headersQuery);
|
||||||
if (!headers) {
|
if (!headers) {
|
||||||
console.warn("No header matched query", headers);
|
console.warn("No header matched query", headers);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let scrollableNavigation = document.querySelector(tocQuery);
|
let scrollableNavigation = document.querySelector(tocQuery) as HTMLElement | undefined;
|
||||||
if (!scrollableNavigation) {
|
if (!scrollableNavigation) {
|
||||||
console.warn("No toc matched query", tocQuery);
|
console.warn("No toc matched query", tocQuery);
|
||||||
return;
|
return;
|
||||||
@ -41,36 +68,37 @@ function setupScrollspy(headersQuery: string, tocQuery: string, navigationQuery:
|
|||||||
headers.forEach((header: HTMLElement) => { sectionsOffsets.push({ id: header.id, offset: header.offsetTop }) });
|
headers.forEach((header: HTMLElement) => { sectionsOffsets.push({ id: header.id, offset: header.offsetTop }) });
|
||||||
sectionsOffsets.sort((a, b) => a.offset - b.offset);
|
sectionsOffsets.sort((a, b) => a.offset - b.offset);
|
||||||
|
|
||||||
let activeSectionLink: Element;
|
// 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;
|
let tocHovered: boolean = false;
|
||||||
|
scrollableNavigation.addEventListener("mouseenter", debounced(() => tocHovered = true));
|
||||||
|
scrollableNavigation.addEventListener("mouseleave", debounced(() => tocHovered = false));
|
||||||
|
|
||||||
function hoverHandler(isHovered: boolean) {
|
let activeSectionLink: Element;
|
||||||
tocHovered = isHovered;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollHandler(e: Event) {
|
function scrollHandler(e: Event) {
|
||||||
let scrollPosition = document.documentElement.scrollTop || document.body.scrollTop;
|
let scrollPosition = document.documentElement.scrollTop || document.body.scrollTop;
|
||||||
|
|
||||||
let newActiveSection: HTMLElement;
|
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) => {
|
sectionsOffsets.forEach((section) => {
|
||||||
if (scrollPosition >= section.offset - 20) {
|
if (scrollPosition >= section.offset - 20) {
|
||||||
newActiveSection = document.getElementById(section.id);
|
newActiveSection = document.getElementById(section.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let newActiveSectionLink: HTMLElement;
|
// 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) {
|
if (newActiveSection) {
|
||||||
for (let i = 0; i < navigation.length; i++) {
|
newActiveSectionLink = findLinkForSectionId(newActiveSection.id, navigation);
|
||||||
let link = navigation[i].querySelector("a");
|
|
||||||
if (link.getAttribute("href") === "#" + newActiveSection.id) {
|
|
||||||
newActiveSectionLink = navigation[i] as HTMLElement;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newActiveSection && !newActiveSectionLink) {
|
if (newActiveSection && !newActiveSectionLink) {
|
||||||
|
// The active section does not have a link in the ToC, so we can't scroll to it.
|
||||||
console.warn("No link found for section", newActiveSection);
|
console.warn("No link found for section", newActiveSection);
|
||||||
} else if (newActiveSectionLink !== activeSectionLink) {
|
} else if (newActiveSectionLink !== activeSectionLink) {
|
||||||
if (activeSectionLink)
|
if (activeSectionLink)
|
||||||
@ -79,12 +107,7 @@ function setupScrollspy(headersQuery: string, tocQuery: string, navigationQuery:
|
|||||||
newActiveSectionLink.classList.add(activeClass);
|
newActiveSectionLink.classList.add(activeClass);
|
||||||
if (!tocHovered) {
|
if (!tocHovered) {
|
||||||
// Scroll so that newActiveSectionLink is in the middle of scrollableNavigation, except when it's from a manual click (hence the tocHovered check)
|
// 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;
|
scrollToTocElement(newActiveSectionLink, scrollableNavigation);
|
||||||
let scrollTop = newActiveSectionLink.offsetTop - scrollableNavigation.offsetHeight / 2 + textHeight / 2 - scrollableNavigation.offsetTop;
|
|
||||||
if (scrollTop < 0) {
|
|
||||||
scrollTop = 0;
|
|
||||||
}
|
|
||||||
scrollableNavigation.scrollTo({ top: scrollTop, behavior: "auto" });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activeSectionLink = newActiveSectionLink;
|
activeSectionLink = newActiveSectionLink;
|
||||||
@ -92,8 +115,6 @@ function setupScrollspy(headersQuery: string, tocQuery: string, navigationQuery:
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("scroll", debounced(scrollHandler));
|
window.addEventListener("scroll", debounced(scrollHandler));
|
||||||
scrollableNavigation.addEventListener("mouseenter", debounced(() => hoverHandler(true)));
|
|
||||||
scrollableNavigation.addEventListener("mouseleave", debounced(() => hoverHandler(false)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { setupScrollspy };
|
export { setupScrollspy };
|
30
assets/ts/smoothAnchors.ts
Normal file
30
assets/ts/smoothAnchors.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Implements smooth scrolling when clicking on an anchor link.
|
||||||
|
// This is required instead of using modern CSS because Chromium does not currently support scrolling
|
||||||
|
// one element with scrollTo while another element is scrolled because of a click on a link. This would thus not work with the ToC scrollspy.
|
||||||
|
|
||||||
|
const anchorLinksQuery = "a[href]";
|
||||||
|
|
||||||
|
function setupSmoothAnchors() {
|
||||||
|
document.querySelectorAll(anchorLinksQuery).forEach(aElement => {
|
||||||
|
let href = aElement.getAttribute("href");
|
||||||
|
if (!href.startsWith("#")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
aElement.addEventListener("click", clickEvent => {
|
||||||
|
clickEvent.preventDefault();
|
||||||
|
|
||||||
|
let targetId = aElement.getAttribute("href").substring(1);
|
||||||
|
let target = document.querySelector(`#${targetId.replace(":", "\\:")}`) as HTMLElement;
|
||||||
|
|
||||||
|
//let tocLink = findLinkForSectionId(targetId, navigation);
|
||||||
|
//if (tocLink) {
|
||||||
|
// scrollToTocElement(tocLink, scrollableNavigation);
|
||||||
|
//}
|
||||||
|
|
||||||
|
window.history.pushState({}, "", aElement.getAttribute("href"));
|
||||||
|
scrollTo({ top: target.offsetTop, behavior: "smooth" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { setupSmoothAnchors };
|
Loading…
Reference in New Issue
Block a user