// Local js definitions: /* global addClass, getSettingValue, hasClass, updateLocalStorage */ /* global onEachLazy, removeClass, getVar */ "use strict"; // The amount of time that the cursor must remain still over a hover target before // revealing a tooltip. // // https://www.nngroup.com/articles/timing-exposing-content/ window.RUSTDOC_TOOLTIP_HOVER_MS = 300; window.RUSTDOC_TOOLTIP_HOVER_EXIT_MS = 450; /** * Given a basename (e.g. "storage") and an extension (e.g. ".js"), return a URL * for a resource under the root-path, with the resource-suffix. * * @param {string} basename * @param {string} extension */ function resourcePath(basename, extension) { return getVar("root-path") + basename + getVar("resource-suffix") + extension; } function hideMain() { addClass(document.getElementById(MAIN_ID), "hidden"); const toggle = document.getElementById("toggle-all-docs"); if (toggle) { toggle.setAttribute("disabled", "disabled"); } } function showMain() { const main = document.getElementById(MAIN_ID); if (!main) { return; } removeClass(main, "hidden"); const mainHeading = main.querySelector(".main-heading"); if (mainHeading && window.searchState.rustdocToolbar) { if (window.searchState.rustdocToolbar.parentElement) { window.searchState.rustdocToolbar.parentElement.removeChild( window.searchState.rustdocToolbar, ); } mainHeading.appendChild(window.searchState.rustdocToolbar); } const toggle = document.getElementById("toggle-all-docs"); if (toggle) { toggle.removeAttribute("disabled"); } } window.rootPath = getVar("root-path"); window.currentCrate = getVar("current-crate"); function setMobileTopbar() { // FIXME: It would be nicer to generate this text content directly in HTML, // but with the current code it's hard to get the right information in the right place. const mobileTopbar = document.querySelector(".mobile-topbar"); const locationTitle = document.querySelector(".sidebar h2.location"); if (mobileTopbar) { const mobileTitle = document.createElement("h2"); mobileTitle.className = "location"; if (hasClass(document.querySelector(".rustdoc"), "crate")) { mobileTitle.innerHTML = `Crate ${window.currentCrate}`; } else if (locationTitle) { mobileTitle.innerHTML = locationTitle.innerHTML; } mobileTopbar.appendChild(mobileTitle); } } /** * Gets the human-readable string for the virtual-key code of the * given KeyboardEvent, ev. * * This function is meant as a polyfill for KeyboardEvent#key, * since it is not supported in IE 11 or Chrome for Android. We also test for * KeyboardEvent#keyCode because the handleShortcut handler is * also registered for the keydown event, because Blink doesn't fire * keypress on hitting the Escape key. * * So I guess you could say things are getting pretty interoperable. * * @param {KeyboardEvent} ev */ function getVirtualKey(ev) { if ("key" in ev && typeof ev.key !== "undefined") { return ev.key; } const c = ev.charCode || ev.keyCode; if (c === 27) { return "Escape"; } return String.fromCharCode(c); } const MAIN_ID = "main-content"; const SETTINGS_BUTTON_ID = "settings-menu"; const ALTERNATIVE_DISPLAY_ID = "alternative-display"; const NOT_DISPLAYED_ID = "not-displayed"; const HELP_BUTTON_ID = "help-button"; function getSettingsButton() { return document.getElementById(SETTINGS_BUTTON_ID); } function getHelpButton() { return document.getElementById(HELP_BUTTON_ID); } // Returns the current URL without any query parameter or hash. function getNakedUrl() { return window.location.href.split("?")[0].split("#")[0]; } /** * This function inserts `newNode` after `referenceNode`. It doesn't work if `referenceNode` * doesn't have a parent node. * * @param {HTMLElement} newNode * @param {HTMLElement & { parentNode: HTMLElement }} referenceNode */ function insertAfter(newNode, referenceNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); } /** * This function creates a new `
` with the given `id` and `classes` if it doesn't already * exist. * * More information about this in `switchDisplayedElement` documentation. * * @param {string} id * @param {string} classes */ function getOrCreateSection(id, classes) { let el = document.getElementById(id); if (!el) { el = document.createElement("section"); el.id = id; el.className = classes; // @ts-expect-error insertAfter(el, document.getElementById(MAIN_ID)); } return el; } /** * Returns the `
` element which contains the displayed element. * * @return {HTMLElement} */ function getAlternativeDisplayElem() { return getOrCreateSection(ALTERNATIVE_DISPLAY_ID, "content hidden"); } /** * Returns the `
` element which contains the not-displayed elements. * * @return {HTMLElement} */ function getNotDisplayedElem() { return getOrCreateSection(NOT_DISPLAYED_ID, "hidden"); } /** * To nicely switch between displayed "extra" elements (such as search results or settings menu) * and to alternate between the displayed and not displayed elements, we hold them in two different * `
` elements. They work in pair: one holds the hidden elements while the other * contains the displayed element (there can be only one at the same time!). So basically, we switch * elements between the two `
` elements. * * @param {HTMLElement|null} elemToDisplay */ function switchDisplayedElement(elemToDisplay) { const el = getAlternativeDisplayElem(); if (el.children.length > 0) { // @ts-expect-error getNotDisplayedElem().appendChild(el.firstElementChild); } if (elemToDisplay === null) { addClass(el, "hidden"); showMain(); return; } el.appendChild(elemToDisplay); hideMain(); removeClass(el, "hidden"); const mainHeading = elemToDisplay.querySelector(".main-heading"); if (mainHeading && window.searchState.rustdocToolbar) { if (window.searchState.rustdocToolbar.parentElement) { window.searchState.rustdocToolbar.parentElement.removeChild( window.searchState.rustdocToolbar, ); } mainHeading.appendChild(window.searchState.rustdocToolbar); } } function browserSupportsHistoryApi() { return window.history && typeof window.history.pushState === "function"; } /** * Download CSS from the web without making it the active stylesheet. * We use this in the settings popover so that you don't get FOUC when switching. * * @param {string} cssUrl */ function preLoadCss(cssUrl) { // https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload const link = document.createElement("link"); link.href = cssUrl; link.rel = "preload"; link.as = "style"; document.getElementsByTagName("head")[0].appendChild(link); } (function() { const isHelpPage = window.location.pathname.endsWith("/help.html"); /** * Run a JavaScript file asynchronously. * @param {string} url * @param {function(): any} errorCallback */ function loadScript(url, errorCallback) { const script = document.createElement("script"); script.src = url; if (errorCallback !== undefined) { script.onerror = errorCallback; } document.head.append(script); } const settingsButton = getSettingsButton(); if (settingsButton) { settingsButton.onclick = event => { if (event.ctrlKey || event.altKey || event.metaKey) { return; } window.hideAllModals(false); addClass(getSettingsButton(), "rotate"); event.preventDefault(); // Sending request for the CSS and the JS files at the same time so it will // hopefully be loaded when the JS will generate the settings content. // @ts-expect-error loadScript(getVar("static-root-path") + getVar("settings-js")); // Pre-load all theme CSS files, so that switching feels seamless. // // When loading settings.html as a standalone page, the equivalent HTML is // generated in context.rs. setTimeout(() => { // @ts-expect-error const themes = getVar("themes").split(","); for (const theme of themes) { // if there are no themes, do nothing // "".split(",") == [""] if (theme !== "") { preLoadCss(getVar("root-path") + theme + ".css"); } } }, 0); }; } window.searchState = { rustdocToolbar: document.querySelector("rustdoc-toolbar"), loadingText: "Loading search results...", // This will always be an HTMLInputElement, but tsc can't see that // @ts-expect-error input: document.getElementsByClassName("search-input")[0], outputElement: () => { let el = document.getElementById("search"); if (!el) { el = document.createElement("section"); el.id = "search"; getNotDisplayedElem().appendChild(el); } return el; }, title: document.title, titleBeforeSearch: document.title, timeout: null, // On the search screen, so you remain on the last tab you opened. // // 0 for "In Names" // 1 for "In Parameters" // 2 for "In Return Types" currentTab: 0, // tab and back preserves the element that was focused. focusedByTab: [null, null, null], clearInputTimeout: () => { if (window.searchState.timeout !== null) { clearTimeout(window.searchState.timeout); window.searchState.timeout = null; } }, isDisplayed: () => { const outputElement = window.searchState.outputElement(); return !!outputElement && !!outputElement.parentElement && outputElement.parentElement.id === ALTERNATIVE_DISPLAY_ID; }, // Sets the focus on the search bar at the top of the page focus: () => { window.searchState.input && window.searchState.input.focus(); }, // Removes the focus from the search bar. defocus: () => { window.searchState.input && window.searchState.input.blur(); }, showResults: search => { if (search === null || typeof search === "undefined") { search = window.searchState.outputElement(); } switchDisplayedElement(search); document.title = window.searchState.title; }, removeQueryParameters: () => { // We change the document title. document.title = window.searchState.titleBeforeSearch; if (browserSupportsHistoryApi()) { history.replaceState(null, "", getNakedUrl() + window.location.hash); } }, hideResults: () => { switchDisplayedElement(null); // We also remove the query parameter from the URL. window.searchState.removeQueryParameters(); }, getQueryStringParams: () => { /** @type {Object.} */ const params = {}; window.location.search.substring(1).split("&"). map(s => { // https://github.com/rust-lang/rust/issues/119219 const pair = s.split("=").map(x => x.replace(/\+/g, " ")); params[decodeURIComponent(pair[0])] = typeof pair[1] === "undefined" ? null : decodeURIComponent(pair[1]); }); return params; }, setup: () => { const search_input = window.searchState.input; if (!search_input) { return; } let searchLoaded = false; // If you're browsing the nightly docs, the page might need to be refreshed for the // search to work because the hash of the JS scripts might have changed. function sendSearchForm() { // @ts-expect-error document.getElementsByClassName("search-form")[0].submit(); } function loadSearch() { if (!searchLoaded) { searchLoaded = true; // @ts-expect-error loadScript(getVar("static-root-path") + getVar("search-js"), sendSearchForm); loadScript(resourcePath("search-index", ".js"), sendSearchForm); } } search_input.addEventListener("focus", () => { window.searchState.origPlaceholder = search_input.placeholder; search_input.placeholder = "Type your search here."; loadSearch(); }); if (search_input.value !== "") { loadSearch(); } const params = window.searchState.getQueryStringParams(); if (params.search !== undefined) { window.searchState.setLoadingSearch(); loadSearch(); } }, setLoadingSearch: () => { const search = window.searchState.outputElement(); if (!search) { return; } search.innerHTML = "

" + window.searchState.loadingText + "

"; window.searchState.showResults(search); }, descShards: new Map(), loadDesc: async function({descShard, descIndex}) { if (descShard.promise === null) { descShard.promise = new Promise((resolve, reject) => { // The `resolve` callback is stored in the `descShard` // object, which is itself stored in `this.descShards` map. // It is called in `loadedDescShard` by the // search.desc script. descShard.resolve = resolve; const ds = descShard; const fname = `${ds.crate}-desc-${ds.shard}-`; const url = resourcePath( `search.desc/${descShard.crate}/${fname}`, ".js", ); loadScript(url, reject); }); } const list = await descShard.promise; return list[descIndex]; }, loadedDescShard: function(crate, shard, data) { // If loadedDescShard gets called, then the library must have been declared. // @ts-expect-error this.descShards.get(crate)[shard].resolve(data.split("\n")); }, }; const toggleAllDocsId = "toggle-all-docs"; let savedHash = ""; /** * @param {HashChangeEvent|null} ev */ function handleHashes(ev) { if (ev !== null && window.searchState.isDisplayed() && ev.newURL) { // This block occurs when clicking on an element in the navbar while // in a search. switchDisplayedElement(null); const hash = ev.newURL.slice(ev.newURL.indexOf("#") + 1); if (browserSupportsHistoryApi()) { // `window.location.search`` contains all the query parameters, not just `search`. history.replaceState(null, "", getNakedUrl() + window.location.search + "#" + hash); } const elem = document.getElementById(hash); if (elem) { elem.scrollIntoView(); } } // This part is used in case an element is not visible. const pageId = window.location.hash.replace(/^#/, ""); if (savedHash !== pageId) { savedHash = pageId; if (pageId !== "") { expandSection(pageId); } } if (savedHash.startsWith("impl-")) { // impl-disambiguated links, used by the search engine // format: impl-X[-for-Y]/method.WHATEVER // turn this into method.WHATEVER[-NUMBER] const splitAt = savedHash.indexOf("/"); if (splitAt !== -1) { const implId = savedHash.slice(0, splitAt); const assocId = savedHash.slice(splitAt + 1); const implElems = document.querySelectorAll( `details > summary > section[id^="${implId}"]`, ); onEachLazy(implElems, implElem => { const numbered = /^(.+?)-([0-9]+)$/.exec(implElem.id); if (implElem.id !== implId && (!numbered || numbered[1] !== implId)) { return false; } return onEachLazy(implElem.parentElement.parentElement.querySelectorAll( `[id^="${assocId}"]`), item => { const numbered = /^(.+?)-([0-9]+)$/.exec(item.id); if (item.id === assocId || (numbered && numbered[1] === assocId)) { openParentDetails(item); item.scrollIntoView(); // Let the section expand itself before trying to highlight setTimeout(() => { window.location.replace("#" + item.id); }, 0); return true; } }, ); }); } } } /** * @param {HashChangeEvent|null} ev */ function onHashChange(ev) { // If we're in mobile mode, we should hide the sidebar in any case. hideSidebar(); handleHashes(ev); } /** * @param {HTMLElement|null} elem */ function openParentDetails(elem) { while (elem) { if (elem.tagName === "DETAILS") { // @ts-expect-error elem.open = true; } elem = elem.parentElement; } } /** * @param {string} id */ function expandSection(id) { openParentDetails(document.getElementById(id)); } /** * @param {KeyboardEvent} ev */ function handleEscape(ev) { window.searchState.clearInputTimeout(); window.searchState.hideResults(); ev.preventDefault(); window.searchState.defocus(); window.hideAllModals(true); // true = reset focus for tooltips } /** * @param {KeyboardEvent} ev */ function handleShortcut(ev) { // Don't interfere with browser shortcuts const disableShortcuts = getSettingValue("disable-shortcuts") === "true"; if (ev.ctrlKey || ev.altKey || ev.metaKey || disableShortcuts) { return; } if (document.activeElement && document.activeElement.tagName === "INPUT" && // @ts-expect-error document.activeElement.type !== "checkbox" && // @ts-expect-error document.activeElement.type !== "radio") { switch (getVirtualKey(ev)) { case "Escape": handleEscape(ev); break; } } else { switch (getVirtualKey(ev)) { case "Escape": handleEscape(ev); break; case "s": case "S": case "/": ev.preventDefault(); window.searchState.focus(); break; case "+": ev.preventDefault(); expandAllDocs(); break; case "-": ev.preventDefault(); collapseAllDocs(); break; case "?": showHelp(); break; default: break; } } } document.addEventListener("keypress", handleShortcut); document.addEventListener("keydown", handleShortcut); function addSidebarItems() { if (!window.SIDEBAR_ITEMS) { return; } const sidebar = document.getElementById("rustdoc-modnav"); /** * Append to the sidebar a "block" of links - a heading along with a list (`