custom.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. // Handle page scroll and adjust sidebar accordingly.
  2. // Each page has two scrolls: the main scroll, which is moving the content of the page;
  3. // and the sidebar scroll, which is moving the navigation in the sidebar.
  4. // We want the logo to gradually disappear as the main content is scrolled, giving
  5. // more room to the navigation on the left. This means adjusting the height
  6. // available to the navigation on the fly. There is also a banner below the navigation
  7. // that must be dealt with simultaneously.
  8. const registerOnScrollEvent = (function(){
  9. // Configuration.
  10. // The number of pixels the user must scroll by before the logo is completely hidden.
  11. const scrollTopPixels = 234;
  12. // The target margin to be applied to the navigation bar when the logo is hidden.
  13. const menuTopMargin = 90;
  14. // The max-height offset when the logo is completely visible.
  15. const menuHeightOffset_default = 338;
  16. // The max-height offset when the logo is completely hidden.
  17. const menuHeightOffset_fixed = 102;
  18. // The distance between the two max-height offset values above; used for intermediate values.
  19. const menuHeightOffset_diff = (menuHeightOffset_default - menuHeightOffset_fixed);
  20. // Media query handler.
  21. return function(mediaQuery) {
  22. // We only apply this logic to the "desktop" resolution (defined by a media query at the bottom).
  23. // This handler is executed when the result of the query evaluation changes, which means that
  24. // the page has moved between "desktop" and "mobile" states.
  25. // When entering the "desktop" state, we register scroll events and adjust elements on the page.
  26. // When entering the "mobile" state, we clean up any registered events and restore elements on the page
  27. // to their initial state.
  28. const $window = $(window);
  29. const $sidebar = $('.wy-side-scroll');
  30. const $search = $sidebar.children('.wy-side-nav-search');
  31. const $menu = $sidebar.children('.wy-menu-vertical');
  32. const $ethical = $sidebar.children('.ethical-rtd');
  33. // This padding is needed to correctly adjust the height of the scrollable area in the sidebar.
  34. // It has to have the same height as the ethical block, if there is one.
  35. let $menuPadding = $menu.children('.wy-menu-ethical-padding');
  36. if ($menuPadding.length == 0) {
  37. $menuPadding = $('<div class="wy-menu-ethical-padding"></div>');
  38. $menu.append($menuPadding);
  39. }
  40. if (mediaQuery.matches) {
  41. // Entering the "desktop" state.
  42. // The main scroll event handler.
  43. // Executed as the page is scrolled and once immediatelly as the page enters this state.
  44. const handleMainScroll = (currentScroll) => {
  45. if (currentScroll >= scrollTopPixels) {
  46. // After the page is scrolled below the threshold, we fix everything in place.
  47. $search.css('margin-top', `-${scrollTopPixels}px`);
  48. $menu.css('margin-top', `${menuTopMargin}px`);
  49. $menu.css('max-height', `calc(100% - ${menuHeightOffset_fixed}px)`);
  50. }
  51. else {
  52. // Between the top of the page and the threshold we calculate intermediate values
  53. // to guarantee a smooth transition.
  54. $search.css('margin-top', `-${currentScroll}px`);
  55. $menu.css('margin-top', `${menuTopMargin + (scrollTopPixels - currentScroll)}px`);
  56. if (currentScroll > 0) {
  57. const scrolledPercent = (scrollTopPixels - currentScroll) / scrollTopPixels;
  58. const offsetValue = menuHeightOffset_fixed + menuHeightOffset_diff * scrolledPercent;
  59. $menu.css('max-height', `calc(100% - ${offsetValue}px)`);
  60. } else {
  61. $menu.css('max-height', `calc(100% - ${menuHeightOffset_default}px)`);
  62. }
  63. }
  64. };
  65. // The sidebar scroll event handler.
  66. // Executed as the sidebar is scrolled as well as after the main scroll. This is needed
  67. // because the main scroll can affect the scrollable area of the sidebar.
  68. const handleSidebarScroll = () => {
  69. const menuElement = $menu.get(0);
  70. const menuScrollTop = $menu.scrollTop();
  71. const menuScrollBottom = menuElement.scrollHeight - (menuScrollTop + menuElement.offsetHeight);
  72. // As the navigation is scrolled we add a shadow to the top bar hanging over it.
  73. if (menuScrollTop > 0) {
  74. $search.addClass('fixed-and-scrolled');
  75. } else {
  76. $search.removeClass('fixed-and-scrolled');
  77. }
  78. // Near the bottom we start moving the sidebar banner into view.
  79. if (menuScrollBottom < ethicalOffsetBottom) {
  80. $ethical.css('display', 'block');
  81. $ethical.css('margin-top', `-${ethicalOffsetBottom - menuScrollBottom}px`);
  82. } else {
  83. $ethical.css('display', 'none');
  84. $ethical.css('margin-top', '0px');
  85. }
  86. };
  87. $search.addClass('fixed');
  88. $ethical.addClass('fixed');
  89. // Adjust the inner height of navigation so that the banner can be overlaid there later.
  90. const ethicalOffsetBottom = $ethical.height() || 0;
  91. if (ethicalOffsetBottom) {
  92. $menuPadding.css('height', `${ethicalOffsetBottom}px`);
  93. } else {
  94. $menuPadding.css('height', `0px`);
  95. }
  96. $window.scroll(function() {
  97. handleMainScroll(window.scrollY);
  98. handleSidebarScroll();
  99. });
  100. $menu.scroll(function() {
  101. handleSidebarScroll();
  102. });
  103. handleMainScroll(window.scrollY);
  104. handleSidebarScroll();
  105. } else {
  106. // Entering the "mobile" state.
  107. $window.unbind('scroll');
  108. $menu.unbind('scroll');
  109. $search.removeClass('fixed');
  110. $ethical.removeClass('fixed');
  111. $search.css('margin-top', `0px`);
  112. $menu.css('margin-top', `0px`);
  113. $menu.css('max-height', 'initial');
  114. $menuPadding.css('height', `0px`);
  115. $ethical.css('margin-top', '0px');
  116. $ethical.css('display', 'block');
  117. }
  118. };
  119. })();
  120. // Subscribe to DOM changes in the sidebar container, because there is a
  121. // banner that gets added at a later point, that we might not catch otherwise.
  122. const registerSidebarObserver = (function(){
  123. return function(callback) {
  124. const sidebarContainer = document.querySelector('.wy-side-scroll');
  125. let sidebarEthical = null;
  126. const registerEthicalObserver = () => {
  127. if (sidebarEthical) {
  128. // Do it only once.
  129. return;
  130. }
  131. sidebarEthical = sidebarContainer.querySelector('.ethical-rtd');
  132. if (!sidebarEthical) {
  133. // Do it only after we have the element there.
  134. return;
  135. }
  136. // This observer watches over the ethical block in sidebar, and all of its subtree.
  137. const ethicalObserverConfig = { childList: true, subtree: true };
  138. const ethicalObserverCallback = (mutationsList, observer) => {
  139. for (let mutation of mutationsList) {
  140. if (mutation.type !== 'childList') {
  141. continue;
  142. }
  143. callback();
  144. }
  145. };
  146. const ethicalObserver = new MutationObserver(ethicalObserverCallback);
  147. ethicalObserver.observe(sidebarEthical, ethicalObserverConfig);
  148. };
  149. registerEthicalObserver();
  150. // This observer watches over direct children of the main sidebar container.
  151. const observerConfig = { childList: true };
  152. const observerCallback = (mutationsList, observer) => {
  153. for (let mutation of mutationsList) {
  154. if (mutation.type !== 'childList') {
  155. continue;
  156. }
  157. callback();
  158. registerEthicalObserver();
  159. }
  160. };
  161. const observer = new MutationObserver(observerCallback);
  162. observer.observe(sidebarContainer, observerConfig);
  163. };
  164. })();
  165. $(document).ready(() => {
  166. const mediaQuery = window.matchMedia('only screen and (min-width: 769px)');
  167. registerOnScrollEvent(mediaQuery);
  168. mediaQuery.addListener(registerOnScrollEvent);
  169. registerSidebarObserver(() => {
  170. registerOnScrollEvent(mediaQuery);
  171. });
  172. // Load instant.page to prefetch pages upon hovering. This makes navigation feel
  173. // snappier. The script is dynamically appended as Read the Docs doesn't have
  174. // a way to add scripts with a "module" attribute.
  175. const instantPageScript = document.createElement('script');
  176. instantPageScript.toggleAttribute('module');
  177. /*! instant.page v5.1.0 - (C) 2019-2020 Alexandre Dieulot - https://instant.page/license */
  178. instantPageScript.innerText = 'let t,e;const n=new Set,o=document.createElement("link"),i=o.relList&&o.relList.supports&&o.relList.supports("prefetch")&&window.IntersectionObserver&&"isIntersecting"in IntersectionObserverEntry.prototype,s="instantAllowQueryString"in document.body.dataset,a="instantAllowExternalLinks"in document.body.dataset,r="instantWhitelist"in document.body.dataset,c="instantMousedownShortcut"in document.body.dataset,d=1111;let l=65,u=!1,f=!1,m=!1;if("instantIntensity"in document.body.dataset){const t=document.body.dataset.instantIntensity;if("mousedown"==t.substr(0,"mousedown".length))u=!0,"mousedown-only"==t&&(f=!0);else if("viewport"==t.substr(0,"viewport".length))navigator.connection&&(navigator.connection.saveData||navigator.connection.effectiveType&&navigator.connection.effectiveType.includes("2g"))||("viewport"==t?document.documentElement.clientWidth*document.documentElement.clientHeight<45e4&&(m=!0):"viewport-all"==t&&(m=!0));else{const e=parseInt(t);isNaN(e)||(l=e)}}if(i){const n={capture:!0,passive:!0};if(f||document.addEventListener("touchstart",function(t){e=performance.now();const n=t.target.closest("a");if(!h(n))return;v(n.href)},n),u?c||document.addEventListener("mousedown",function(t){const e=t.target.closest("a");if(!h(e))return;v(e.href)},n):document.addEventListener("mouseover",function(n){if(performance.now()-e<d)return;const o=n.target.closest("a");if(!h(o))return;o.addEventListener("mouseout",p,{passive:!0}),t=setTimeout(()=>{v(o.href),t=void 0},l)},n),c&&document.addEventListener("mousedown",function(t){if(performance.now()-e<d)return;const n=t.target.closest("a");if(t.which>1||t.metaKey||t.ctrlKey)return;if(!n)return;n.addEventListener("click",function(t){1337!=t.detail&&t.preventDefault()},{capture:!0,passive:!1,once:!0});const o=new MouseEvent("click",{view:window,bubbles:!0,cancelable:!1,detail:1337});n.dispatchEvent(o)},n),m){let t;(t=window.requestIdleCallback?t=>{requestIdleCallback(t,{timeout:1500})}:t=>{t()})(()=>{const t=new IntersectionObserver(e=>{e.forEach(e=>{if(e.isIntersecting){const n=e.target;t.unobserve(n),v(n.href)}})});document.querySelectorAll("a").forEach(e=>{h(e)&&t.observe(e)})})}}function p(e){e.relatedTarget&&e.target.closest("a")==e.relatedTarget.closest("a")||t&&(clearTimeout(t),t=void 0)}function h(t){if(t&&t.href&&(!r||"instant"in t.dataset)&&(a||t.origin==location.origin||"instant"in t.dataset)&&["http:","https:"].includes(t.protocol)&&("http:"!=t.protocol||"https:"!=location.protocol)&&(s||!t.search||"instant"in t.dataset)&&!(t.hash&&t.pathname+t.search==location.pathname+location.search||"noInstant"in t.dataset))return!0}function v(t){if(n.has(t))return;const e=document.createElement("link");e.rel="prefetch",e.href=t,document.head.appendChild(e),n.add(t)}';
  179. document.head.appendChild(instantPageScript);
  180. });