Skip to content
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

Merged
merged 10 commits into from Aug 8, 2018
Merged

ScrollSpy for table of contents #1557

merged 10 commits into from Aug 8, 2018

Conversation

edemaine
Copy link
Member

@edemaine edemaine commented Aug 6, 2018

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

scripts: ['https://buttons.github.io/buttons.js'],
scripts: [
'https://buttons.github.io/buttons.js',
baseUrl + 'js/scrollspy.js',
Copy link
Member

@ylemkimon ylemkimon Aug 7, 2018

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.

Copy link
Member

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.

document.addEventListener('scroll', onScroll);
document.addEventListener('resize', onScroll);
document.addEventListener('ready', onScroll);
onScroll();
Copy link
Member

@ylemkimon ylemkimon Aug 7, 2018

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)).

Copy link
Member

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.

};
document.addEventListener('scroll', onScroll);
document.addEventListener('resize', onScroll);
document.addEventListener('ready', onScroll);
Copy link
Member

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.

@@ -0,0 +1,37 @@
const epsilon = 10;
Copy link
Member

@ylemkimon ylemkimon Aug 7, 2018

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)

}
timer = setTimeout(() => {
let found = false;
const headings = document.querySelectorAll('.toc-headings > li > a');
Copy link
Member

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.

Copy link
Member

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)

Copy link
Member Author

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.

// 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scrollBottom is not used.

// Some computations based on
// https://github.com/makotot/scrollspy/blob/master/src/js/modules/scrollspy.js
const scrollTop = document.documentElement.scrollTop ||
document.body.scrollTop;
Copy link
Member

@ylemkimon ylemkimon Aug 7, 2018

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).

@edemaine
Copy link
Member Author

edemaine commented Aug 7, 2018

@ylemkimon Thanks for all the comments and pointers. I've implemented them all, except for caching header positions.

@@ -0,0 +1,44 @@
// Based in part on
// https://github.com/makotot/scrollspy/blob/master/src/js/modules/scrollspy.js
Copy link
Member

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.

Copy link
Member Author

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...

const next = headings[i + 1].href.split('#')[1];
const nextHeader = document.getElementById(next);
const top = nextHeader.getBoundingClientRect().top;
current = top > OFFSET;
Copy link
Member

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;

Copy link
Member Author

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 scrollTops cancel.

let timer;
let headingsCache;
const findHeadings = () =>
document.querySelectorAll('.toc-headings > li > a');
Copy link
Member

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');.

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member

@ylemkimon ylemkimon left a 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
Copy link

codecov bot commented Aug 8, 2018

Codecov Report

Merging #1557 into master will not change coverage.
The diff coverage is n/a.

Impacted file tree graph

@@           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.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 48e6d9d...96b02a7. Read the comment docs.

if (timer) { // throttle
clearTimeout(timer);
}
timer = setTimeout(() => {
Copy link
Member

@ylemkimon ylemkimon Aug 8, 2018

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)

Copy link
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ScrollSpy in documentation
2 participants