scripts.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. /**
  2. * Site-wide JS that sets up:
  3. *
  4. * [1] MathJax rendering on navigation
  5. * [2] Sidebar toggling
  6. * [3] Sidebar scroll preserving
  7. * [4] Keyboard navigation
  8. * [5] Right sidebar scroll highlighting / navbar show
  9. */
  10. const togglerId = 'js-sidebar-toggle'
  11. const textbookId = 'js-textbook'
  12. const togglerActiveClass = 'is-active'
  13. const textbookActiveClass = 'js-show-sidebar'
  14. const mathRenderedClass = 'js-mathjax-rendered'
  15. const icon_path = document.location.origin + `${site_basename}assets`;
  16. const getToggler = () => document.getElementById(togglerId)
  17. const getTextbook = () => document.getElementById(textbookId)
  18. // [1] Run MathJax when Turbolinks navigates to a page.
  19. // When Turbolinks caches a page, it also saves the MathJax rendering. We mark
  20. // each page with a CSS class after rendering to prevent double renders when
  21. // navigating back to a cached page.
  22. document.addEventListener('turbolinks:load', () => {
  23. const textbook = getTextbook()
  24. if (window.MathJax && !textbook.classList.contains(mathRenderedClass)) {
  25. MathJax.Hub.Queue(['Typeset', MathJax.Hub])
  26. textbook.classList.add(mathRenderedClass)
  27. }
  28. })
  29. /**
  30. * [2] Toggles sidebar and menu icon
  31. */
  32. const toggleSidebar = () => {
  33. const toggler = getToggler()
  34. const textbook = getTextbook()
  35. if (textbook.classList.contains(textbookActiveClass)) {
  36. textbook.classList.remove(textbookActiveClass)
  37. toggler.classList.remove(togglerActiveClass)
  38. } else {
  39. textbook.classList.add(textbookActiveClass)
  40. toggler.classList.add(togglerActiveClass)
  41. }
  42. }
  43. /**
  44. * Keep the variable below in sync with the tablet breakpoint value in
  45. * _sass/inuitcss/tools/_tools.mq.scss
  46. *
  47. */
  48. const autoCloseSidebarBreakpoint = 769
  49. // Set up event listener for sidebar toggle button
  50. const sidebarButtonHandler = () => {
  51. getToggler().addEventListener('click', toggleSidebar)
  52. /**
  53. * Auto-close sidebar on smaller screens after page load.
  54. *
  55. * Having the sidebar be open by default then closing it on page load for
  56. * small screens gives the illusion that the sidebar closes in response
  57. * to selecting a page in the sidebar. However, it does cause a bit of jank
  58. * on the first page load.
  59. *
  60. * Since we don't want to persist state in between page navigation, this is
  61. * the best we can do while optimizing for larger screens where most
  62. * viewers will read the textbook.
  63. *
  64. * The code below assumes that the sidebar is open by default.
  65. */
  66. if (window.innerWidth < autoCloseSidebarBreakpoint) toggleSidebar()
  67. }
  68. initFunction(sidebarButtonHandler);
  69. /**
  70. * [3] Preserve sidebar scroll when navigating between pages
  71. */
  72. let sidebarScrollTop = 0
  73. const getSidebar = () => document.getElementById('js-sidebar')
  74. document.addEventListener('turbolinks:before-visit', () => {
  75. sidebarScrollTop = getSidebar().scrollTop
  76. })
  77. document.addEventListener('turbolinks:load', () => {
  78. getSidebar().scrollTop = sidebarScrollTop
  79. })
  80. /**
  81. * Focus textbook page by default so that user can scroll with spacebar
  82. */
  83. const focusPage = () => {
  84. document.querySelector('.c-textbook__page').focus()
  85. }
  86. initFunction(focusPage);
  87. /**
  88. * [4] Use left and right arrow keys to navigate forward and backwards.
  89. */
  90. const LEFT_ARROW_KEYCODE = 37
  91. const RIGHT_ARROW_KEYCODE = 39
  92. const getPrevUrl = () => document.getElementById('js-page__nav__prev').href
  93. const getNextUrl = () => document.getElementById('js-page__nav__next').href
  94. const initPageNav = (event) => {
  95. const keycode = event.which
  96. if (keycode === LEFT_ARROW_KEYCODE) {
  97. Turbolinks.visit(getPrevUrl())
  98. } else if (keycode === RIGHT_ARROW_KEYCODE) {
  99. Turbolinks.visit(getNextUrl())
  100. }
  101. };
  102. var keyboardListener = false;
  103. const initListener = () => {
  104. if (keyboardListener === false) {
  105. document.addEventListener('keydown', initPageNav)
  106. keyboardListener = true;
  107. }
  108. }
  109. initFunction(initListener);
  110. /**
  111. * [5] Scrolling functions:
  112. * * Right sidebar scroll highlighting
  113. * * Top navbar hiding for scrolling
  114. */
  115. var didScroll;
  116. initScrollFunc = function() {
  117. var content = document.querySelector('.c-textbook__page');
  118. var topbar = document.getElementById("top-navbar");
  119. var prevScrollpos = content.scrollTop; // Initializing
  120. scrollFunc = function() {
  121. // This is the function that does all the stuff when scrolling happens
  122. var position = content.scrollTop; // Because we use this differently for sidebar
  123. // Decide to show the navbar
  124. var currentScrollPos = content.scrollTop;
  125. var delta = 10;
  126. var scrollDiff = prevScrollpos - currentScrollPos;
  127. if (scrollDiff >= delta) {
  128. // If we scrolled down, consider showing the menu
  129. topbar.classList.remove("hidetop")
  130. } else if (Math.abs(scrollDiff) >= delta) {
  131. // If we scrolled up, consider hiding the menu
  132. topbar.classList.add("hidetop")
  133. } else {
  134. // Do nothing because we didn't scroll enough
  135. }
  136. prevScrollpos = currentScrollPos;
  137. // Highlight the right sidebar section
  138. position = position + (window.innerHeight / 4); // + Manual offset
  139. content.querySelectorAll('h2, h3').forEach((header, index) => {
  140. // Highlight based on location from the top of the screen
  141. var target = header.getBoundingClientRect().top
  142. var pixelOffset = 300; // Number of pixels from top to be highlighted
  143. var id = header.id;
  144. if (target < pixelOffset) {
  145. var query = 'ul.toc__menu a[href="#' + id + '"]';
  146. document.querySelectorAll('ul.toc__menu li').forEach((item) => {item.classList.remove('active')});
  147. document.querySelectorAll(query).forEach((item) => {item.parentElement.classList.add('active')});
  148. }
  149. });
  150. }
  151. // Our event listener just sets "yep, I scrolled" to true.
  152. // The interval function will set it to false after it runs.
  153. content.addEventListener('scroll', () => {didScroll = true;});
  154. scrollWait = 250;
  155. setInterval(() => {
  156. if (didScroll) {
  157. scrollFunc();
  158. didScroll = false;
  159. }
  160. }, scrollWait)
  161. }
  162. initFunction(initScrollFunc);