table-of-contents.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. const getPageToc = () => document.getElementsByClassName('pagetoc')[0];
  2. const pageToc = getPageToc();
  3. const pageTocChildren = [...pageToc.children];
  4. const headers = [...document.getElementsByClassName('header')];
  5. // Select highlighted item in ToC when clicking an item
  6. pageTocChildren.forEach(child => {
  7. child.addEventHandler('click', () => {
  8. pageTocChildren.forEach(child => {
  9. child.classList.remove('active');
  10. });
  11. child.classList.add('active');
  12. });
  13. });
  14. /**
  15. * Test whether a node is in the viewport
  16. */
  17. function isInViewport(node) {
  18. const rect = node.getBoundingClientRect();
  19. return rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth);
  20. }
  21. /**
  22. * Set a new ToC entry.
  23. * Clear any previously highlighted ToC items, set the new one,
  24. * and adjust the ToC scroll position.
  25. */
  26. function setTocEntry() {
  27. let activeEntry;
  28. const pageTocChildren = [...getPageToc().children];
  29. // Calculate which header is the current one at the top of screen
  30. headers.forEach(header => {
  31. if (window.pageYOffset >= header.offsetTop) {
  32. activeEntry = header;
  33. }
  34. });
  35. // Update selected item in ToC when scrolling
  36. pageTocChildren.forEach(child => {
  37. if (activeEntry.href.localeCompare(child.href) === 0) {
  38. child.classList.add('active');
  39. } else {
  40. child.classList.remove('active');
  41. }
  42. });
  43. let tocEntryForLocation = document.querySelector(`nav a[href="${activeEntry.href}"]`);
  44. if (tocEntryForLocation) {
  45. const headingForLocation = document.querySelector(activeEntry.hash);
  46. if (headingForLocation && isInViewport(headingForLocation)) {
  47. // Update ToC scroll
  48. const nav = getPageToc();
  49. const content = document.querySelector('html');
  50. if (content.scrollTop !== 0) {
  51. nav.scrollTo({
  52. top: tocEntryForLocation.offsetTop - 100,
  53. left: 0,
  54. behavior: 'smooth',
  55. });
  56. } else {
  57. nav.scrollTop = 0;
  58. }
  59. }
  60. }
  61. }
  62. /**
  63. * Populate sidebar on load
  64. */
  65. window.addEventListener('load', () => {
  66. // Prevent rendering the table of contents of the "print book" page, as it
  67. // will end up being rendered into the output (in a broken-looking way)
  68. // Get the name of the current page (i.e. 'print.html')
  69. const pageNameExtension = window.location.pathname.split('/').pop();
  70. // Split off the extension (as '.../print' is also a valid page name), which
  71. // should result in 'print'
  72. const pageName = pageNameExtension.split('.')[0];
  73. if (pageName === "print") {
  74. // Don't render the table of contents on this page
  75. return;
  76. }
  77. // Only create table of contents if there is more than one header on the page
  78. if (headers.length <= 1) {
  79. return;
  80. }
  81. // Create an entry in the page table of contents for each header in the document
  82. headers.forEach((header, index) => {
  83. const link = document.createElement('a');
  84. // Indent shows hierarchy
  85. let indent = '0px';
  86. switch (header.parentElement.tagName) {
  87. case 'H1':
  88. indent = '5px';
  89. break;
  90. case 'H2':
  91. indent = '20px';
  92. break;
  93. case 'H3':
  94. indent = '30px';
  95. break;
  96. case 'H4':
  97. indent = '40px';
  98. break;
  99. case 'H5':
  100. indent = '50px';
  101. break;
  102. case 'H6':
  103. indent = '60px';
  104. break;
  105. default:
  106. break;
  107. }
  108. let tocEntry;
  109. if (index == 0) {
  110. // Create a bolded title for the first element
  111. tocEntry = document.createElement("strong");
  112. tocEntry.innerHTML = header.text;
  113. } else {
  114. // All other elements are non-bold
  115. tocEntry = document.createTextNode(header.text);
  116. }
  117. link.appendChild(tocEntry);
  118. link.style.paddingLeft = indent;
  119. link.href = header.href;
  120. pageToc.appendChild(link);
  121. });
  122. setTocEntry.call();
  123. });
  124. // Handle active headers on scroll, if there is more than one header on the page
  125. if (headers.length > 1) {
  126. window.addEventListener('scroll', setTocEntry);
  127. }