toc-highlighter.ts 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. /**
  2. * Highlights the current section in the table of contents when scrolling.
  3. */
  4. export class TocHighlighter {
  5. constructor(private tocElement: HTMLElement) {}
  6. highlight() {
  7. const article = document.querySelector('article');
  8. if (this.tocElement && article) {
  9. const headers: HTMLHeadingElement[] = Array.from(
  10. article.querySelectorAll('h1[id],h2[id],h3[id],h4[id]'),
  11. );
  12. window.addEventListener('scroll', (e) => {
  13. this.highlightCurrentSection(headers);
  14. });
  15. this.highlightCurrentSection(headers);
  16. }
  17. }
  18. private highlightCurrentSection(headers: HTMLElement[]) {
  19. const locationHash = location.hash;
  20. Array.from(this.tocElement.querySelectorAll('.active')).forEach((el) =>
  21. el.classList.remove('active'),
  22. );
  23. // tslint:disable:prefer-for-of
  24. for (let i = 0; i < headers.length; i++) {
  25. const header = headers[i];
  26. const nextHeader = headers[i + 1];
  27. const id = header.getAttribute('id') as string;
  28. if (
  29. !nextHeader ||
  30. (nextHeader && window.scrollY + window.innerHeight - 200 < nextHeader.offsetTop)
  31. ) {
  32. this.highlightItem(id);
  33. return;
  34. }
  35. const isCurrentTarget = `#${id}` === locationHash;
  36. const currentTargetOffset = isCurrentTarget ? 90 : 0;
  37. if (header.offsetTop + currentTargetOffset >= window.scrollY) {
  38. this.highlightItem(id);
  39. return;
  40. }
  41. }
  42. }
  43. private highlightItem(id: string) {
  44. const tocItem = this.tocElement.querySelector(`[href="#${id}"]`) as HTMLAnchorElement;
  45. if (tocItem) {
  46. tocItem.classList.add('active');
  47. // ensure the highlighted item is scrolled into view in the TOC menu
  48. const padding = 12;
  49. const tocHeight = this.tocElement.offsetHeight;
  50. const tocScrollTop = this.tocElement.scrollTop;
  51. const outOfRangeTop = tocItem?.offsetTop < tocScrollTop + padding;
  52. const outofRangeBottom = tocHeight + tocScrollTop < tocItem.offsetTop + padding;
  53. if (outOfRangeTop) {
  54. // console.log('¬TOP');
  55. this.tocElement.scrollTo({ top: tocItem.offsetTop - tocItem.offsetHeight - padding });
  56. }
  57. if (outofRangeBottom) {
  58. // console.log('$BOTTOm');
  59. const delta = tocItem.offsetTop - (tocHeight + tocScrollTop);
  60. this.tocElement.scrollTo({ top: tocScrollTop + delta + tocItem.offsetHeight + padding });
  61. }
  62. }
  63. }
  64. }