New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ScrollSpy for table of contents #1557
Conversation
website/siteConfig.js
Outdated
scripts: ['https://buttons.github.io/buttons.js'], | ||
scripts: [ | ||
'https://buttons.github.io/buttons.js', | ||
baseUrl + 'js/scrollspy.js', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use /js/scrollspy.js
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@edemaine Sorry, it turns out baseUrl
is needed for scripts path. I'll open a PR.
website/static/js/scrollspy.js
Outdated
document.addEventListener('scroll', onScroll); | ||
document.addEventListener('resize', onScroll); | ||
document.addEventListener('ready', onScroll); | ||
onScroll(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrap this in a closure (IIFE) to prevent polluting global scope. Or wrap in a function and attach to DOMContentLoaded
to cache elements and positions (#1557 (comment)).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The script is included in the <head>
, so calling onScroll
here has no effect.
website/static/js/scrollspy.js
Outdated
}; | ||
document.addEventListener('scroll', onScroll); | ||
document.addEventListener('resize', onScroll); | ||
document.addEventListener('ready', onScroll); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ready
is only available on jQuery. You should use DOMContentLoaded
.
website/static/js/scrollspy.js
Outdated
@@ -0,0 +1,37 @@ | |||
const epsilon = 10; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the name OFFSET
is better. (All caps, as it's a constant)
website/static/js/scrollspy.js
Outdated
} | ||
timer = setTimeout(() => { | ||
let found = false; | ||
const headings = document.querySelectorAll('.toc-headings > li > a'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can cache (pre-calculate) this value after DOMContentLoaded
. You can also probably cache positions of their headers, but I'm not sure DOMContentLoaded
waits for fonts to be loaded and rendered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can cache positions of headers based on the scrollHeight
. i.e., update cached positions if scrollHeight
has been changed. (AFAIK, modern browsers support document.documentElement.scrollHeight
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added the query cache (though if we ever load pages via AJAX, this will break). I don't think we need to cache the positions of the headers -- these should be effectively cached by the web browser. This is also standard for scrollspy.
website/static/js/scrollspy.js
Outdated
// https://github.com/makotot/scrollspy/blob/master/src/js/modules/scrollspy.js | ||
const scrollTop = document.documentElement.scrollTop || | ||
document.body.scrollTop; | ||
const scrollBottom = scrollTop + window.innerHeight; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scrollBottom
is not used.
website/static/js/scrollspy.js
Outdated
// Some computations based on | ||
// https://github.com/makotot/scrollspy/blob/master/src/js/modules/scrollspy.js | ||
const scrollTop = document.documentElement.scrollTop || | ||
document.body.scrollTop; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All browsers except IE<9, which Docusaurus and we don't support, support window.pageYOffset
(https://developer.mozilla.org/en-US/docs/Web/API/Window/pageYOffset).
@ylemkimon Thanks for all the comments and pointers. I've implemented them all, except for caching header positions. |
website/static/js/scrollspy.js
Outdated
@@ -0,0 +1,44 @@ | |||
// Based in part on | |||
// https://github.com/makotot/scrollspy/blob/master/src/js/modules/scrollspy.js |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is no longer based on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe "inspired by"? I could remove the link and just mention that ScrollSpy is a thing. I also wonder whether this file should be called "scrollspy.js" or something else...
website/static/js/scrollspy.js
Outdated
const next = headings[i + 1].href.split('#')[1]; | ||
const nextHeader = document.getElementById(next); | ||
const top = nextHeader.getBoundingClientRect().top; | ||
current = top > OFFSET; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be current = top > scrollTop + OFFSET;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It used to be top = nextHeader.getBoundingClientRect().top + scrollTop
and then current = top + scrollTop > scrollTop + OFFSET
. The scrollTop
s cancel.
website/static/js/scrollspy.js
Outdated
let timer; | ||
let headingsCache; | ||
const findHeadings = () => | ||
document.querySelectorAll('.toc-headings > li > a'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can check whether cache is available here, i.e., const findHeadings = () => headingsCache || document.querySelectorAll('.toc-headings > li > a');
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. In my experience, it takes multiple seconds before the DOM content is fully loaded, and someone could easily scroll during that time. We don't want to accidentally cache at this time or we might miss some of the elements...
Alternatively, I could bind the scroll
etc. events only after DOM content is fully loaded. But I like that the current code does its best effort while the page is still loading, in the case that someone scrolls early.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, I see now that you were suggesting the cache could be read here, not written. I'll do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please fix lint errors. I'll fix them on my end.
Codecov Report
@@ Coverage Diff @@
## master #1557 +/- ##
=======================================
Coverage 84.49% 84.49%
=======================================
Files 79 79
Lines 4619 4619
Branches 807 807
=======================================
Hits 3903 3903
Misses 619 619
Partials 97 97 Continue to review full report at Codecov.
|
website/static/js/scrollspy.js
Outdated
if (timer) { // throttle | ||
clearTimeout(timer); | ||
} | ||
timer = setTimeout(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think throttling is more suitable than debounce. (This will debounce, i.e., run only once after repeated scrolling)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I've switched back to throttling. Thanks for the lint fixes! I also tweaked the comment now that scrollTop
is no longer defined.
Fix #1537 by adding automatic highlighting of currently visible topmost table of contents item.
This sometimes doesn't do an initial highlight, I guess because the table of contents isn't rendered at the beginning? Is there a way to fix this? But it works when scrolling and resizing.Based on #1537 (comment) and https://github.com/makotot/scrollspy/blob/master/src/js/modules/scrollspy.js