custom.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. // Set this to `true` when the `latest` branch is significantly incompatible with the
  2. // current `stable` branch, which can lead to confusion for users that land on
  3. // `latest` instead of `stable`.
  4. const inDev = true;
  5. // Handle page scroll and adjust sidebar accordingly.
  6. // Each page has two scrolls: the main scroll, which is moving the content of the page;
  7. // and the sidebar scroll, which is moving the navigation in the sidebar.
  8. // We want the logo to gradually disappear as the main content is scrolled, giving
  9. // more room to the navigation on the left. This means adjusting the height
  10. // available to the navigation on the fly. There is also a banner below the navigation
  11. // that must be dealt with simultaneously.
  12. const registerOnScrollEvent = (function(){
  13. // Configuration.
  14. // The number of pixels the user must scroll by before the logo is completely hidden.
  15. const scrollTopPixels = 84;
  16. // The target margin to be applied to the navigation bar when the logo is hidden.
  17. const menuTopMargin = 88;
  18. // The max-height offset when the logo is completely visible.
  19. const menuHeightOffset_default = 180;
  20. // The max-height offset when the logo is completely hidden.
  21. const menuHeightOffset_fixed = 98;
  22. // The distance between the two max-height offset values above; used for intermediate values.
  23. const menuHeightOffset_diff = (menuHeightOffset_default - menuHeightOffset_fixed);
  24. // Media query handler.
  25. return function(mediaQuery) {
  26. // We only apply this logic to the "desktop" resolution (defined by a media query at the bottom).
  27. // This handler is executed when the result of the query evaluation changes, which means that
  28. // the page has moved between "desktop" and "mobile" states.
  29. // When entering the "desktop" state, we register scroll events and adjust elements on the page.
  30. // When entering the "mobile" state, we clean up any registered events and restore elements on the page
  31. // to their initial state.
  32. const $window = $(window);
  33. const $sidebar = $('.wy-side-scroll');
  34. const $search = $sidebar.children('.wy-side-nav-search');
  35. const $menu = $sidebar.children('.wy-menu-vertical');
  36. const $ethical = $sidebar.children('.ethical-rtd');
  37. // This padding is needed to correctly adjust the height of the scrollable area in the sidebar.
  38. // It has to have the same height as the ethical block, if there is one.
  39. let $menuPadding = $menu.children('.wy-menu-ethical-padding');
  40. if ($menuPadding.length == 0) {
  41. $menuPadding = $('<div class="wy-menu-ethical-padding"></div>');
  42. $menu.append($menuPadding);
  43. }
  44. if (mediaQuery.matches) {
  45. // Entering the "desktop" state.
  46. // The main scroll event handler.
  47. // Executed as the page is scrolled and once immediately as the page enters this state.
  48. const handleMainScroll = (currentScroll) => {
  49. if (currentScroll >= scrollTopPixels) {
  50. // After the page is scrolled below the threshold, we fix everything in place.
  51. $search.css('margin-top', `-${scrollTopPixels}px`);
  52. $menu.css('margin-top', `${menuTopMargin}px`);
  53. $menu.css('max-height', `calc(100% - ${menuHeightOffset_fixed}px)`);
  54. }
  55. else {
  56. // Between the top of the page and the threshold we calculate intermediate values
  57. // to guarantee a smooth transition.
  58. $search.css('margin-top', `-${currentScroll}px`);
  59. $menu.css('margin-top', `${menuTopMargin + (scrollTopPixels - currentScroll)}px`);
  60. if (currentScroll > 0) {
  61. const scrolledPercent = (scrollTopPixels - currentScroll) / scrollTopPixels;
  62. const offsetValue = menuHeightOffset_fixed + menuHeightOffset_diff * scrolledPercent;
  63. $menu.css('max-height', `calc(100% - ${offsetValue}px)`);
  64. } else {
  65. $menu.css('max-height', `calc(100% - ${menuHeightOffset_default}px)`);
  66. }
  67. }
  68. };
  69. // The sidebar scroll event handler.
  70. // Executed as the sidebar is scrolled as well as after the main scroll. This is needed
  71. // because the main scroll can affect the scrollable area of the sidebar.
  72. const handleSidebarScroll = () => {
  73. const menuElement = $menu.get(0);
  74. const menuScrollTop = $menu.scrollTop();
  75. const menuScrollBottom = menuElement.scrollHeight - (menuScrollTop + menuElement.offsetHeight);
  76. // As the navigation is scrolled we add a shadow to the top bar hanging over it.
  77. if (menuScrollTop > 0) {
  78. $search.addClass('fixed-and-scrolled');
  79. } else {
  80. $search.removeClass('fixed-and-scrolled');
  81. }
  82. // Near the bottom we start moving the sidebar banner into view.
  83. if (menuScrollBottom < ethicalOffsetBottom) {
  84. $ethical.css('display', 'block');
  85. $ethical.css('margin-top', `-${ethicalOffsetBottom - menuScrollBottom}px`);
  86. } else {
  87. $ethical.css('display', 'none');
  88. $ethical.css('margin-top', '0px');
  89. }
  90. };
  91. $search.addClass('fixed');
  92. $ethical.addClass('fixed');
  93. // Adjust the inner height of navigation so that the banner can be overlaid there later.
  94. const ethicalOffsetBottom = $ethical.height() || 0;
  95. if (ethicalOffsetBottom) {
  96. $menuPadding.css('height', `${ethicalOffsetBottom}px`);
  97. } else {
  98. $menuPadding.css('height', `0px`);
  99. }
  100. $window.scroll(function() {
  101. handleMainScroll(window.scrollY);
  102. handleSidebarScroll();
  103. });
  104. $menu.scroll(function() {
  105. handleSidebarScroll();
  106. });
  107. handleMainScroll(window.scrollY);
  108. handleSidebarScroll();
  109. } else {
  110. // Entering the "mobile" state.
  111. $window.unbind('scroll');
  112. $menu.unbind('scroll');
  113. $search.removeClass('fixed');
  114. $ethical.removeClass('fixed');
  115. $search.css('margin-top', `0px`);
  116. $menu.css('margin-top', `0px`);
  117. $menu.css('max-height', 'initial');
  118. $menuPadding.css('height', `0px`);
  119. $ethical.css('margin-top', '0px');
  120. $ethical.css('display', 'block');
  121. }
  122. };
  123. })();
  124. // Subscribe to DOM changes in the sidebar container, because there is a
  125. // banner that gets added at a later point, that we might not catch otherwise.
  126. const registerSidebarObserver = (function(){
  127. return function(callback) {
  128. const sidebarContainer = document.querySelector('.wy-side-scroll');
  129. let sidebarEthical = null;
  130. const registerEthicalObserver = () => {
  131. if (sidebarEthical) {
  132. // Do it only once.
  133. return;
  134. }
  135. sidebarEthical = sidebarContainer.querySelector('.ethical-rtd');
  136. if (!sidebarEthical) {
  137. // Do it only after we have the element there.
  138. return;
  139. }
  140. // This observer watches over the ethical block in sidebar, and all of its subtree.
  141. const ethicalObserverConfig = { childList: true, subtree: true };
  142. const ethicalObserverCallback = (mutationsList, observer) => {
  143. for (let mutation of mutationsList) {
  144. if (mutation.type !== 'childList') {
  145. continue;
  146. }
  147. callback();
  148. }
  149. };
  150. const ethicalObserver = new MutationObserver(ethicalObserverCallback);
  151. ethicalObserver.observe(sidebarEthical, ethicalObserverConfig);
  152. };
  153. registerEthicalObserver();
  154. // This observer watches over direct children of the main sidebar container.
  155. const observerConfig = { childList: true };
  156. const observerCallback = (mutationsList, observer) => {
  157. for (let mutation of mutationsList) {
  158. if (mutation.type !== 'childList') {
  159. continue;
  160. }
  161. callback();
  162. registerEthicalObserver();
  163. }
  164. };
  165. const observer = new MutationObserver(observerCallback);
  166. observer.observe(sidebarContainer, observerConfig);
  167. // Default TOC tree has links that immediately navigate to the selected page. Our
  168. // theme adds an extra button to fold and unfold the tree without navigating away.
  169. // But that means that the buttons are added after the initial load, so we need to
  170. // improvise to detect clicks on these buttons.
  171. const registerLinkHandler = (linkChildren) => {
  172. linkChildren.forEach(it => {
  173. if (it.nodeType === Node.ELEMENT_NODE && it.classList.contains('toctree-expand')) {
  174. it.addEventListener('click', () => {
  175. callback();
  176. });
  177. }
  178. });
  179. }
  180. const navigationSections = document.querySelectorAll('.wy-menu-vertical ul');
  181. navigationSections.forEach(it => {
  182. if (typeof it.previousSibling === 'undefined' || it.previousSibling.tagName != 'A') {
  183. return;
  184. }
  185. const navigationLink = it.previousSibling;
  186. registerLinkHandler(navigationLink.childNodes);
  187. const linkObserverConfig = { childList: true };
  188. const linkObserverCallback = (mutationsList, observer) => {
  189. for (let mutation of mutationsList) {
  190. registerLinkHandler(mutation.addedNodes);
  191. }
  192. };
  193. const linkObserver = new MutationObserver(linkObserverCallback);
  194. linkObserver.observe(navigationLink, linkObserverConfig);
  195. });
  196. };
  197. })();
  198. $(document).ready(() => {
  199. // Remove the search match highlights from the page, and adjust the URL in the
  200. // navigation history.
  201. const url = new URL(location.href);
  202. if (url.searchParams.has('highlight')) {
  203. Documentation.hideSearchWords();
  204. }
  205. // Initialize handlers for page scrolling and our custom sidebar.
  206. const mediaQuery = window.matchMedia('only screen and (min-width: 769px)');
  207. registerOnScrollEvent(mediaQuery);
  208. mediaQuery.addListener(registerOnScrollEvent);
  209. registerSidebarObserver(() => {
  210. registerOnScrollEvent(mediaQuery);
  211. });
  212. if (inDev) {
  213. // Add a compatibility notice using JavaScript so it doesn't end up in the
  214. // automatically generated `meta description` tag.
  215. const baseUrl = [location.protocol, '//', location.host, location.pathname].join('');
  216. // These lines only work as expected in the production environment, can't test this locally.
  217. const fallbackUrl = baseUrl.replace('/latest/', '/stable/');
  218. const homeUrl = baseUrl.split('/latest/')[0] + '/stable/';
  219. const searchUrl = homeUrl + 'search.html?q=';
  220. // Insert the base notice with a placeholder to display as we're making a request.
  221. document.querySelector('div[itemprop="articleBody"]').insertAdjacentHTML('afterbegin', `
  222. <div class="admonition attention latest-notice">
  223. <p class="first admonition-title">Attention</p>
  224. <p>
  225. You are reading the <code class="docutils literal notranslate"><span class="pre">latest</span></code>
  226. (unstable) version of this documentation, which may document features not available
  227. or compatible with Godot 3.x.
  228. </p>
  229. <p class="last latest-notice-link">
  230. Checking the <a class="reference" href="${homeUrl}">stable version</a>
  231. of the documentation...
  232. </p>
  233. </div>
  234. `);
  235. const noticeLink = document.querySelector('.latest-notice-link');
  236. // Make a HEAD request to the possible stable URL to check if the page exists.
  237. fetch(fallbackUrl, { method: 'HEAD' })
  238. .then((res) => {
  239. // We only check the HTTP status, which should tell us if the link is valid or not.
  240. if (res.status === 200) {
  241. noticeLink.innerHTML = `
  242. See the <a class="reference" href="${fallbackUrl}">stable version</a>
  243. of this documentation page instead.
  244. `;
  245. } else {
  246. // Err, just to fallthrough to catch.
  247. throw Error('Bad request');
  248. }
  249. })
  250. .catch((err) => {
  251. let message = `
  252. This page does not exist in the <a class="reference" href="${homeUrl}">stable version</a>
  253. of the documentation.
  254. `;
  255. // Also suggest a search query using the page's title. It should work with translations as well.
  256. // Note that we can't use the title tag as it has a permanent suffix. OG title doesn't, though.
  257. const titleMeta = document.querySelector('meta[property="og:title"]');
  258. if (typeof titleMeta !== 'undefined') {
  259. const pageTitle = titleMeta.getAttribute('content');
  260. message += `
  261. You can try searching for "<a class="reference" href="${searchUrl + encodeURIComponent(pageTitle)}">${pageTitle}</a>" instead.
  262. `;
  263. }
  264. noticeLink.innerHTML = message;
  265. });
  266. }
  267. // Load instant.page to prefetch pages upon hovering. This makes navigation feel
  268. // snappier. The script is dynamically appended as Read the Docs doesn't have
  269. // a way to add scripts with a "module" attribute.
  270. const instantPageScript = document.createElement('script');
  271. instantPageScript.toggleAttribute('module');
  272. /*! instant.page v5.1.0 - (C) 2019-2020 Alexandre Dieulot - https://instant.page/license */
  273. 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)}';
  274. document.head.appendChild(instantPageScript);
  275. // Make sections in the sidebar togglable.
  276. let hasCurrent = false;
  277. let menuHeaders = document.querySelectorAll('.wy-menu-vertical .caption[role=heading]');
  278. menuHeaders.forEach(it => {
  279. let connectedMenu = it.nextElementSibling;
  280. // Enable toggling.
  281. it.addEventListener('click', () => {
  282. if (connectedMenu.classList.contains('active')) {
  283. connectedMenu.classList.remove('active');
  284. it.classList.remove('active');
  285. } else {
  286. connectedMenu.classList.add('active');
  287. it.classList.add('active');
  288. }
  289. // Hide other sections.
  290. menuHeaders.forEach(ot => {
  291. if (ot !== it && ot.classList.contains('active')) {
  292. ot.nextElementSibling.classList.remove('active');
  293. ot.classList.remove('active');
  294. }
  295. });
  296. registerOnScrollEvent(mediaQuery);
  297. }, true);
  298. // Set the default state, expand our current section.
  299. if (connectedMenu.classList.contains('current')) {
  300. connectedMenu.classList.add('active');
  301. it.classList.add('active');
  302. hasCurrent = true;
  303. }
  304. });
  305. // Unfold the first (general information) section on the home page.
  306. if (!hasCurrent && menuHeaders.length > 0) {
  307. menuHeaders[0].classList.add('active');
  308. menuHeaders[0].nextElementSibling.classList.add('active');
  309. registerOnScrollEvent(mediaQuery);
  310. }
  311. });
  312. // Override the default implementation from doctools.js to avoid this behavior.
  313. Documentation.highlightSearchWords = function() {
  314. // Nope.
  315. }