| // Local js definitions: | |
| /* global addClass, getSettingValue, hasClass, updateLocalStorage */ | |
| /* global onEachLazy, removeClass, getVar, nonnull */ | |
| "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"); | |
| /** | |
| * 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 | |
| * @returns {string} | |
| */ | |
| 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 ALTERNATIVE_DISPLAY_ID = "alternative-display"; | |
| const NOT_DISPLAYED_ID = "not-displayed"; | |
| // 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 `<section>` 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 `<section>` element which contains the displayed element. | |
| * | |
| * @return {HTMLElement} | |
| */ | |
| function getAlternativeDisplayElem() { | |
| return getOrCreateSection(ALTERNATIVE_DISPLAY_ID, "content hidden"); | |
| } | |
| /** | |
| * Returns the `<section>` 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 | |
| * `<section>` 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 `<section>` elements. | |
| * | |
| * @param {Element|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); | |
| } | |
| onEachLazy(document.querySelectorAll(".settings-menu"), settingsMenu => { | |
| /** @param {MouseEvent} event */ | |
| settingsMenu.querySelector("a").onclick = event => { | |
| if (event.ctrlKey || event.altKey || event.metaKey) { | |
| return; | |
| } | |
| window.hideAllModals(false); | |
| addClass(settingsMenu, "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(() => { | |
| 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...", | |
| inputElement: () => { | |
| let el = document.getElementsByClassName("search-input")[0]; | |
| if (!el) { | |
| const out = nonnull(nonnull(window.searchState.outputElement()).parentElement); | |
| const hdr = document.createElement("div"); | |
| hdr.className = "main-heading search-results-main-heading"; | |
| const params = window.searchState.getQueryStringParams(); | |
| const autofocusParam = params.search === "" ? "autofocus" : ""; | |
| hdr.innerHTML = `<nav class="sub"> | |
| <form class="search-form loading"> | |
| <span></span> <!-- This empty span is a hacky fix for Safari: see #93184 --> | |
| <input | |
| ${autofocusParam} | |
| class="search-input" | |
| name="search" | |
| aria-label="Run search in the documentation" | |
| autocomplete="off" | |
| spellcheck="false" | |
| placeholder="Type ‘S’ or ‘/’ to search, ‘?’ for more options…" | |
| type="search"> | |
| </form> | |
| </nav><div class="search-switcher"></div>`; | |
| out.insertBefore(hdr, window.searchState.outputElement()); | |
| el = document.getElementsByClassName("search-input")[0]; | |
| } | |
| if (el instanceof HTMLInputElement) { | |
| return el; | |
| } | |
| return null; | |
| }, | |
| containerElement: () => { | |
| let el = document.getElementById("search"); | |
| if (!el) { | |
| el = document.createElement("section"); | |
| el.id = "search"; | |
| getNotDisplayedElem().appendChild(el); | |
| } | |
| return el; | |
| }, | |
| outputElement: () => { | |
| const container = window.searchState.containerElement(); | |
| if (!container) { | |
| return null; | |
| } | |
| let el = container.querySelector(".search-out"); | |
| if (!el) { | |
| el = document.createElement("div"); | |
| el.className = "search-out"; | |
| container.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 container = window.searchState.containerElement(); | |
| if (!container) { | |
| return false; | |
| } | |
| return !!container.parentElement && container.parentElement.id === | |
| ALTERNATIVE_DISPLAY_ID; | |
| }, | |
| // Sets the focus on the search bar at the top of the page | |
| focus: () => { | |
| const inputElement = window.searchState.inputElement(); | |
| window.searchState.showResults(); | |
| if (inputElement) { | |
| inputElement.focus(); | |
| // Avoid glitch if something focuses the search button after clicking. | |
| requestAnimationFrame(() => inputElement.focus()); | |
| } | |
| }, | |
| // Removes the focus from the search bar. | |
| defocus: () => { | |
| nonnull(window.searchState.inputElement()).blur(); | |
| }, | |
| toggle: () => { | |
| if (window.searchState.isDisplayed()) { | |
| window.searchState.defocus(); | |
| window.searchState.hideResults(); | |
| } else { | |
| window.searchState.focus(); | |
| } | |
| }, | |
| showResults: () => { | |
| document.title = window.searchState.title; | |
| if (window.searchState.isDisplayed()) { | |
| return; | |
| } | |
| const search = window.searchState.containerElement(); | |
| switchDisplayedElement(search); | |
| const btn = document.querySelector("#search-button a"); | |
| if (browserSupportsHistoryApi() && btn instanceof HTMLAnchorElement && | |
| window.searchState.getQueryStringParams().search === undefined | |
| ) { | |
| history.pushState(null, "", btn.href); | |
| } | |
| const btnLabel = document.querySelector("#search-button a span.label"); | |
| if (btnLabel) { | |
| btnLabel.innerHTML = "Exit"; | |
| } | |
| }, | |
| 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(); | |
| const btnLabel = document.querySelector("#search-button a span.label"); | |
| if (btnLabel) { | |
| btnLabel.innerHTML = "Search"; | |
| } | |
| }, | |
| getQueryStringParams: () => { | |
| /** @type {Object.<any, string>} */ | |
| 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: () => { | |
| let searchLoaded = false; | |
| const search_input = window.searchState.inputElement(); | |
| if (!search_input) { | |
| return; | |
| } | |
| // 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; | |
| window.rr_ = data => { | |
| window.searchIndex = data; | |
| }; | |
| if (!window.StringdexOnload) { | |
| window.StringdexOnload = []; | |
| } | |
| window.StringdexOnload.push(() => { | |
| loadScript( | |
| getVar("static-root-path") + getVar("search-js"), | |
| sendSearchForm, | |
| ); | |
| }); | |
| loadScript(getVar("static-root-path") + getVar("stringdex-js"), sendSearchForm); | |
| loadScript(resourcePath("search.index/root", ".js"), sendSearchForm); | |
| } | |
| } | |
| search_input.addEventListener("focus", () => { | |
| loadSearch(); | |
| }); | |
| const btn = document.getElementById("search-button"); | |
| if (btn) { | |
| btn.onclick = event => { | |
| if (event.ctrlKey || event.altKey || event.metaKey) { | |
| return; | |
| } | |
| event.preventDefault(); | |
| window.searchState.toggle(); | |
| loadSearch(); | |
| }; | |
| } | |
| // Push and pop states are used to add search results to the browser | |
| // history. | |
| if (browserSupportsHistoryApi()) { | |
| // Store the previous <title> so we can revert back to it later. | |
| const previousTitle = document.title; | |
| window.addEventListener("popstate", e => { | |
| const params = window.searchState.getQueryStringParams(); | |
| // Revert to the previous title manually since the History | |
| // API ignores the title parameter. | |
| document.title = previousTitle; | |
| // Synchronize search bar with query string state and | |
| // perform the search. This will empty the bar if there's | |
| // nothing there, which lets you really go back to a | |
| // previous state with nothing in the bar. | |
| const inputElement = window.searchState.inputElement(); | |
| if (params.search !== undefined && inputElement !== null) { | |
| loadSearch(); | |
| inputElement.value = params.search; | |
| // Some browsers fire "onpopstate" for every page load | |
| // (Chrome), while others fire the event only when actually | |
| // popping a state (Firefox), which is why search() is | |
| // called both here and at the end of the startSearch() | |
| // function. | |
| e.preventDefault(); | |
| window.searchState.showResults(); | |
| if (params.search === "") { | |
| window.searchState.focus(); | |
| } | |
| } else { | |
| // When browsing back from search results the main page | |
| // visibility must be reset. | |
| window.searchState.hideResults(); | |
| } | |
| }); | |
| } | |
| // This is required in firefox to avoid this problem: Navigating to a search result | |
| // with the keyboard, hitting enter, and then hitting back would take you back to | |
| // the doc page, rather than the search that should overlay it. | |
| // This was an interaction between the back-forward cache and our handlers | |
| // that try to sync state between the URL and the search input. To work around it, | |
| // do a small amount of re-init on page show. | |
| window.onpageshow = () => { | |
| const inputElement = window.searchState.inputElement(); | |
| const qSearch = window.searchState.getQueryStringParams().search; | |
| if (qSearch !== undefined && inputElement !== null) { | |
| if (inputElement.value === "") { | |
| inputElement.value = qSearch; | |
| } | |
| window.searchState.showResults(); | |
| if (qSearch === "") { | |
| loadSearch(); | |
| window.searchState.focus(); | |
| } | |
| } else { | |
| window.searchState.hideResults(); | |
| } | |
| }; | |
| const params = window.searchState.getQueryStringParams(); | |
| if (params.search !== undefined) { | |
| window.searchState.setLoadingSearch(); | |
| loadSearch(); | |
| } | |
| }, | |
| setLoadingSearch: () => { | |
| const search = window.searchState.outputElement(); | |
| nonnull(search).innerHTML = "<h3 class=\"search-loading\">" + | |
| window.searchState.loadingText + "</h3>"; | |
| window.searchState.showResults(); | |
| }, | |
| 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 instanceof HTMLDetailsElement) { | |
| 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 instanceof HTMLInputElement && | |
| document.activeElement.type !== "checkbox" && | |
| 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 "+": | |
| case "=": | |
| ev.preventDefault(); | |
| expandAllDocs(); | |
| break; | |
| case "-": | |
| ev.preventDefault(); | |
| collapseAllDocs(false); | |
| break; | |
| case "_": | |
| ev.preventDefault(); | |
| collapseAllDocs(true); | |
| 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 (`<ul>`) of items. | |
| * | |
| * @param {string} shortty - A short type name, like "primitive", "mod", or "macro" | |
| * @param {string} id - The HTML id of the corresponding section on the module page. | |
| * @param {string} longty - A long, capitalized, plural name, like "Primitive Types", | |
| * "Modules", or "Macros". | |
| */ | |
| function block(shortty, id, longty) { | |
| // @ts-expect-error | |
| const filtered = window.SIDEBAR_ITEMS[shortty]; | |
| if (!filtered) { | |
| return; | |
| } | |
| const modpath = hasClass(document.querySelector(".rustdoc"), "mod") ? "../" : ""; | |
| const h3 = document.createElement("h3"); | |
| h3.innerHTML = `<a href="${modpath}index.html#${id}">${longty}</a>`; | |
| const ul = document.createElement("ul"); | |
| ul.className = "block " + shortty; | |
| for (const item of filtered) { | |
| const [name, isMacro] = Array.isArray(item) ? [item[0], true] : [item, false]; | |
| let path; | |
| if (shortty === "mod") { | |
| path = `${modpath}${name}/index.html`; | |
| } else if (!isMacro) { | |
| path = `${modpath}${shortty}.${name}.html`; | |
| } else { | |
| path = `${modpath}macro.${name}.html`; | |
| } | |
| let current_page = document.location.href.toString(); | |
| if (current_page.endsWith("/")) { | |
| current_page += "index.html"; | |
| } | |
| const link = document.createElement("a"); | |
| link.href = path; | |
| link.textContent = name; | |
| const li = document.createElement("li"); | |
| // Don't "optimize" this to just use `path`. | |
| // We want the browser to normalize this into an absolute URL. | |
| if (link.href === current_page) { | |
| li.classList.add("current"); | |
| } | |
| li.appendChild(link); | |
| ul.appendChild(li); | |
| } | |
| // @ts-expect-error | |
| sidebar.appendChild(h3); | |
| // @ts-expect-error | |
| sidebar.appendChild(ul); | |
| } | |
| if (sidebar) { | |
| // keep this synchronized with ItemSection::ALL in html/render/mod.rs | |
| // Re-exports aren't shown here, because they don't have child pages | |
| //block("reexport", "reexports", "Re-exports"); | |
| block("primitive", "primitives", "Primitive Types"); | |
| block("mod", "modules", "Modules"); | |
| block("macro", "macros", "Macros"); | |
| block("struct", "structs", "Structs"); | |
| block("enum", "enums", "Enums"); | |
| block("constant", "constants", "Constants"); | |
| block("static", "static", "Statics"); | |
| block("trait", "traits", "Traits"); | |
| block("fn", "functions", "Functions"); | |
| block("type", "types", "Type Aliases"); | |
| block("union", "unions", "Unions"); | |
| // No point, because these items don't appear in modules | |
| //block("impl", "impls", "Implementations"); | |
| //block("tymethod", "tymethods", "Type Methods"); | |
| //block("method", "methods", "Methods"); | |
| //block("structfield", "fields", "Fields"); | |
| //block("variant", "variants", "Variants"); | |
| //block("associatedtype", "associated-types", "Associated Types"); | |
| //block("associatedconstant", "associated-consts", "Associated Constants"); | |
| block("foreigntype", "foreign-types", "Foreign Types"); | |
| block("keyword", "keywords", "Keywords"); | |
| block("attribute", "attribute-docs", "Attributes"); | |
| block("attr", "attributes", "Attribute Macros"); | |
| block("derive", "derives", "Derive Macros"); | |
| block("traitalias", "trait-aliases", "Trait Aliases"); | |
| } | |
| } | |
| // <https://github.com/search?q=repo%3Arust-lang%2Frust+[RUSTDOCIMPL]+trait.impl&type=code> | |
| window.register_implementors = imp => { | |
| /** Takes an ID as input and returns a list of two elements. The first element is the DOM | |
| * element with the given ID and the second is the "negative marker", meaning the location | |
| * between the negative and non-negative impls. | |
| * | |
| * @param {string} id: ID of the DOM element. | |
| * | |
| * @return {[HTMLElement|null, HTMLElement|null]} | |
| */ | |
| function implementorsElems(id) { | |
| const elem = document.getElementById(id); | |
| return [elem, elem ? elem.querySelector(".negative-marker") : null]; | |
| } | |
| const implementors = implementorsElems("implementors-list"); | |
| const syntheticImplementors = implementorsElems("synthetic-implementors-list"); | |
| const inlined_types = new Set(); | |
| const TEXT_IDX = 0; | |
| const IS_NEG_IDX = 1; | |
| const SYNTHETIC_IDX = 2; | |
| const TYPES_IDX = 3; | |
| if (syntheticImplementors[0]) { | |
| // This `inlined_types` variable is used to avoid having the same implementation | |
| // showing up twice. For example "String" in the "Sync" doc page. | |
| // | |
| // By the way, this is only used by and useful for traits implemented automatically | |
| // (like "Send" and "Sync"). | |
| onEachLazy(syntheticImplementors[0].getElementsByClassName("impl"), el => { | |
| const aliases = el.getAttribute("data-aliases"); | |
| if (!aliases) { | |
| return; | |
| } | |
| // @ts-expect-error | |
| aliases.split(",").forEach(alias => { | |
| inlined_types.add(alias); | |
| }); | |
| }); | |
| } | |
| // @ts-expect-error | |
| let currentNbImpls = implementors[0].getElementsByClassName("impl").length; | |
| // @ts-expect-error | |
| const traitName = document.querySelector(".main-heading h1 > .trait").textContent; | |
| const baseIdName = "impl-" + traitName + "-"; | |
| const libs = Object.getOwnPropertyNames(imp); | |
| // We don't want to include impls from this JS file, when the HTML already has them. | |
| // The current crate should always be ignored. Other crates that should also be | |
| // ignored are included in the attribute `data-ignore-extern-crates`. | |
| const script = document | |
| .querySelector("script[data-ignore-extern-crates]"); | |
| const ignoreExternCrates = new Set( | |
| // @ts-expect-error | |
| (script ? script.getAttribute("data-ignore-extern-crates") : "").split(","), | |
| ); | |
| for (const lib of libs) { | |
| if (lib === window.currentCrate || ignoreExternCrates.has(lib)) { | |
| continue; | |
| } | |
| const structs = imp[lib]; | |
| struct_loop: | |
| for (const struct of structs) { | |
| const list = struct[SYNTHETIC_IDX] ? syntheticImplementors : implementors; | |
| // The types list is only used for synthetic impls. | |
| // If this changes, `main.js` and `write_shared.rs` both need changed. | |
| if (struct[SYNTHETIC_IDX]) { | |
| for (const struct_type of struct[TYPES_IDX]) { | |
| if (inlined_types.has(struct_type)) { | |
| continue struct_loop; | |
| } | |
| inlined_types.add(struct_type); | |
| } | |
| } | |
| const code = document.createElement("h3"); | |
| code.innerHTML = struct[TEXT_IDX]; | |
| addClass(code, "code-header"); | |
| onEachLazy(code.getElementsByTagName("a"), elem => { | |
| const href = elem.getAttribute("href"); | |
| if (href && !href.startsWith("#") && !/^(?:[a-z+]+:)?\/\//.test(href)) { | |
| elem.setAttribute("href", window.rootPath + href); | |
| } | |
| }); | |
| const currentId = baseIdName + currentNbImpls; | |
| const anchor = document.createElement("a"); | |
| anchor.href = "#" + currentId; | |
| addClass(anchor, "anchor"); | |
| const display = document.createElement("div"); | |
| display.id = currentId; | |
| addClass(display, "impl"); | |
| display.appendChild(anchor); | |
| display.appendChild(code); | |
| // If this is a negative implementor, we put it into the right location (just | |
| // before the negative impl marker). | |
| if (struct[IS_NEG_IDX]) { | |
| // @ts-expect-error | |
| list[1].before(display); | |
| } else { | |
| // @ts-expect-error | |
| list[0].appendChild(display); | |
| } | |
| currentNbImpls += 1; | |
| } | |
| } | |
| if (implementors[0]) { | |
| implementors[0].classList.add("loaded"); | |
| } | |
| if (syntheticImplementors[0]) { | |
| syntheticImplementors[0].classList.add("loaded"); | |
| } | |
| }; | |
| if (window.pending_implementors) { | |
| window.register_implementors(window.pending_implementors); | |
| } | |
| /** | |
| * <https://github.com/search?q=repo%3Arust-lang%2Frust+[RUSTDOCIMPL]+type.impl&type=code> | |
| * | |
| * [RUSTDOCIMPL] type.impl | |
| * | |
| * This code inlines implementations into the type alias docs at runtime. It's done at | |
| * runtime because some crates have many type aliases and many methods, and we don't want | |
| * to generate *O*`(types*methods)` HTML text. The data inside is mostly HTML fragments, | |
| * wrapped in JSON. | |
| * | |
| * - It only includes docs generated for the current crate. This function accepts an | |
| * object mapping crate names to the set of impls. | |
| * | |
| * - It filters down to the set of applicable impls. The Rust type checker is used to | |
| * tag each HTML blob with the set of type aliases that can actually use it, so the | |
| * JS only needs to consult the attached list of type aliases. | |
| * | |
| * - It renames the ID attributes, to avoid conflicting IDs in the resulting DOM. | |
| * | |
| * - It adds the necessary items to the sidebar. If it's an inherent impl, that means | |
| * adding methods, associated types, and associated constants. If it's a trait impl, | |
| * that means adding it to the trait impl sidebar list. | |
| * | |
| * - It adds the HTML block itself. If it's an inherent impl, it goes after the type | |
| * alias's own inherent impls. If it's a trait impl, it goes in the Trait | |
| * Implementations section. | |
| * | |
| * - After processing all of the impls, it sorts the sidebar items by name. | |
| * | |
| * @param {rustdoc.TypeImpls} imp | |
| */ | |
| window.register_type_impls = imp => { | |
| // @ts-expect-error | |
| if (!imp || !imp[window.currentCrate]) { | |
| return; | |
| } | |
| window.pending_type_impls = undefined; | |
| const idMap = new Map(); | |
| let implementations = document.getElementById("implementations-list"); | |
| let trait_implementations = document.getElementById("trait-implementations-list"); | |
| let trait_implementations_header = document.getElementById("trait-implementations"); | |
| // We want to include the current type alias's impls, and no others. | |
| const script = document.querySelector("script[data-self-path]"); | |
| const selfPath = script ? script.getAttribute("data-self-path") : null; | |
| // These sidebar blocks need filled in, too. | |
| const mainContent = nonnull(document.querySelector("#main-content")); | |
| const sidebarSection = nonnull(document.querySelector(".sidebar section")); | |
| let methods = document.querySelector(".sidebar .block.method"); | |
| let associatedTypes = document.querySelector(".sidebar .block.associatedtype"); | |
| let associatedConstants = document.querySelector(".sidebar .block.associatedconstant"); | |
| let sidebarTraitList = document.querySelector(".sidebar .block.trait-implementation"); | |
| for (const impList of imp[nonnull(window.currentCrate)]) { | |
| const types = impList.slice(2); | |
| const text = impList[0]; | |
| const traitName = impList[1]; | |
| const isTrait = typeof traitName === "string"; | |
| // @ts-expect-error | |
| if (types.indexOf(selfPath) === -1) { | |
| continue; | |
| } | |
| let outputList = isTrait ? trait_implementations : implementations; | |
| if (outputList === null) { | |
| const outputListName = isTrait ? "Trait Implementations" : "Implementations"; | |
| const outputListId = isTrait ? | |
| "trait-implementations-list" : | |
| "implementations-list"; | |
| const outputListHeaderId = isTrait ? "trait-implementations" : "implementations"; | |
| const outputListHeader = document.createElement("h2"); | |
| outputListHeader.id = outputListHeaderId; | |
| outputListHeader.innerText = outputListName; | |
| outputList = document.createElement("div"); | |
| outputList.id = outputListId; | |
| if (isTrait) { | |
| const link = document.createElement("a"); | |
| link.href = `#${outputListHeaderId}`; | |
| link.innerText = "Trait Implementations"; | |
| const h = document.createElement("h3"); | |
| h.appendChild(link); | |
| trait_implementations = outputList; | |
| trait_implementations_header = outputListHeader; | |
| sidebarSection.appendChild(h); | |
| sidebarTraitList = document.createElement("ul"); | |
| sidebarTraitList.className = "block trait-implementation"; | |
| sidebarSection.appendChild(sidebarTraitList); | |
| mainContent.appendChild(outputListHeader); | |
| mainContent.appendChild(outputList); | |
| } else { | |
| implementations = outputList; | |
| if (trait_implementations) { | |
| mainContent.insertBefore(outputListHeader, trait_implementations_header); | |
| mainContent.insertBefore(outputList, trait_implementations_header); | |
| } else { | |
| mainContent.appendChild(outputListHeader); | |
| mainContent.appendChild(outputList); | |
| } | |
| } | |
| } | |
| const template = document.createElement("template"); | |
| template.innerHTML = text; | |
| onEachLazy(template.content.querySelectorAll("a"), elem => { | |
| const href = elem.getAttribute("href"); | |
| if (href && !href.startsWith("#") && !/^(?:[a-z+]+:)?\/\//.test(href)) { | |
| elem.setAttribute("href", window.rootPath + href); | |
| } | |
| }); | |
| onEachLazy(template.content.querySelectorAll("[id]"), el => { | |
| let i = 0; | |
| if (idMap.has(el.id)) { | |
| i = idMap.get(el.id); | |
| } else if (document.getElementById(el.id)) { | |
| i = 1; | |
| while (document.getElementById(`${el.id}-${2 * i}`)) { | |
| i = 2 * i; | |
| } | |
| while (document.getElementById(`${el.id}-${i}`)) { | |
| i += 1; | |
| } | |
| } | |
| if (i !== 0) { | |
| const oldHref = `#${el.id}`; | |
| const newHref = `#${el.id}-${i}`; | |
| el.id = `${el.id}-${i}`; | |
| onEachLazy(template.content.querySelectorAll("a[href]"), link => { | |
| if (link.getAttribute("href") === oldHref) { | |
| link.href = newHref; | |
| } | |
| }); | |
| } | |
| idMap.set(el.id, i + 1); | |
| }); | |
| const templateAssocItems = template.content.querySelectorAll("section.tymethod, " + | |
| "section.method, section.associatedtype, section.associatedconstant"); | |
| if (isTrait) { | |
| const li = document.createElement("li"); | |
| const a = document.createElement("a"); | |
| a.href = `#${nonnull(template.content.querySelector(".impl")).id}`; | |
| a.textContent = traitName; | |
| li.appendChild(a); | |
| // @ts-expect-error | |
| sidebarTraitList.append(li); | |
| } else { | |
| onEachLazy(templateAssocItems, item => { | |
| let block = hasClass(item, "associatedtype") ? associatedTypes : ( | |
| hasClass(item, "associatedconstant") ? associatedConstants : ( | |
| methods)); | |
| if (!block) { | |
| const blockTitle = hasClass(item, "associatedtype") ? "Associated Types" : ( | |
| hasClass(item, "associatedconstant") ? "Associated Constants" : ( | |
| "Methods")); | |
| const blockClass = hasClass(item, "associatedtype") ? "associatedtype" : ( | |
| hasClass(item, "associatedconstant") ? "associatedconstant" : ( | |
| "method")); | |
| const blockHeader = document.createElement("h3"); | |
| const blockLink = document.createElement("a"); | |
| blockLink.href = "#implementations"; | |
| blockLink.innerText = blockTitle; | |
| blockHeader.appendChild(blockLink); | |
| block = document.createElement("ul"); | |
| block.className = `block ${blockClass}`; | |
| const insertionReference = methods || sidebarTraitList; | |
| if (insertionReference) { | |
| const insertionReferenceH = insertionReference.previousElementSibling; | |
| sidebarSection.insertBefore(blockHeader, insertionReferenceH); | |
| sidebarSection.insertBefore(block, insertionReferenceH); | |
| } else { | |
| sidebarSection.appendChild(blockHeader); | |
| sidebarSection.appendChild(block); | |
| } | |
| if (hasClass(item, "associatedtype")) { | |
| associatedTypes = block; | |
| } else if (hasClass(item, "associatedconstant")) { | |
| associatedConstants = block; | |
| } else { | |
| methods = block; | |
| } | |
| } | |
| const li = document.createElement("li"); | |
| const a = document.createElement("a"); | |
| a.innerText = item.id.split("-")[0].split(".")[1]; | |
| a.href = `#${item.id}`; | |
| li.appendChild(a); | |
| block.appendChild(li); | |
| }); | |
| } | |
| outputList.appendChild(template.content); | |
| } | |
| for (const list of [methods, associatedTypes, associatedConstants, sidebarTraitList]) { | |
| if (!list) { | |
| continue; | |
| } | |
| const newChildren = Array.prototype.slice.call(list.children); | |
| newChildren.sort((a, b) => { | |
| const aI = a.innerText; | |
| const bI = b.innerText; | |
| return aI < bI ? -1 : | |
| aI > bI ? 1 : | |
| 0; | |
| }); | |
| list.replaceChildren(...newChildren); | |
| } | |
| }; | |
| if (window.pending_type_impls) { | |
| window.register_type_impls(window.pending_type_impls); | |
| } | |
| function addSidebarCrates() { | |
| // @ts-expect-error | |
| if (!window.ALL_CRATES) { | |
| return; | |
| } | |
| const sidebarElems = document.getElementById("rustdoc-modnav"); | |
| if (!sidebarElems) { | |
| return; | |
| } | |
| // Draw a convenient sidebar of known crates if we have a listing | |
| const h3 = document.createElement("h3"); | |
| h3.innerHTML = "Crates"; | |
| const ul = document.createElement("ul"); | |
| ul.className = "block crate"; | |
| // @ts-expect-error | |
| for (const crate of window.ALL_CRATES) { | |
| const link = document.createElement("a"); | |
| link.href = window.rootPath + crate + "/index.html"; | |
| link.textContent = crate; | |
| const li = document.createElement("li"); | |
| if (window.rootPath !== "./" && crate === window.currentCrate) { | |
| li.className = "current"; | |
| } | |
| li.appendChild(link); | |
| ul.appendChild(li); | |
| } | |
| sidebarElems.appendChild(h3); | |
| sidebarElems.appendChild(ul); | |
| } | |
| function expandAllDocs() { | |
| const innerToggle = document.getElementById(toggleAllDocsId); | |
| removeClass(innerToggle, "will-expand"); | |
| onEachLazy(document.getElementsByClassName("toggle"), e => { | |
| if (!hasClass(e, "type-contents-toggle") && !hasClass(e, "more-examples-toggle")) { | |
| e.open = true; | |
| } | |
| }); | |
| // @ts-expect-error | |
| innerToggle.children[0].innerText = "Summary"; | |
| } | |
| /** | |
| * @param {boolean} collapseImpls - also collapse impl blocks if set to true | |
| */ | |
| function collapseAllDocs(collapseImpls) { | |
| const innerToggle = document.getElementById(toggleAllDocsId); | |
| addClass(innerToggle, "will-expand"); | |
| onEachLazy(document.getElementsByClassName("toggle"), e => { | |
| if ((collapseImpls || e.parentNode.id !== "implementations-list") || | |
| (!hasClass(e, "implementors-toggle") && | |
| !hasClass(e, "type-contents-toggle")) | |
| ) { | |
| e.open = false; | |
| } | |
| }); | |
| // @ts-expect-error | |
| innerToggle.children[0].innerText = "Show all"; | |
| } | |
| /** | |
| * @param {MouseEvent=} ev | |
| */ | |
| function toggleAllDocs(ev) { | |
| const innerToggle = document.getElementById(toggleAllDocsId); | |
| if (!innerToggle) { | |
| return; | |
| } | |
| if (hasClass(innerToggle, "will-expand")) { | |
| expandAllDocs(); | |
| } else { | |
| collapseAllDocs(ev !== undefined && ev.shiftKey); | |
| } | |
| } | |
| (function() { | |
| const toggles = document.getElementById(toggleAllDocsId); | |
| if (toggles) { | |
| toggles.onclick = toggleAllDocs; | |
| } | |
| const hideMethodDocs = getSettingValue("auto-hide-method-docs") === "true"; | |
| const hideImplementations = getSettingValue("auto-hide-trait-implementations") === "true"; | |
| const hideLargeItemContents = getSettingValue("auto-hide-large-items") !== "false"; | |
| // @ts-expect-error | |
| function setImplementorsTogglesOpen(id, open) { | |
| const list = document.getElementById(id); | |
| if (list !== null) { | |
| onEachLazy(list.getElementsByClassName("implementors-toggle"), e => { | |
| e.open = open; | |
| }); | |
| } | |
| } | |
| if (hideImplementations) { | |
| setImplementorsTogglesOpen("trait-implementations-list", false); | |
| setImplementorsTogglesOpen("blanket-implementations-list", false); | |
| } | |
| onEachLazy(document.getElementsByClassName("toggle"), e => { | |
| if (!hideLargeItemContents && hasClass(e, "type-contents-toggle")) { | |
| e.open = true; | |
| } | |
| if (hideMethodDocs && hasClass(e, "method-toggle")) { | |
| e.open = false; | |
| } | |
| }); | |
| }()); | |
| window.rustdoc_add_line_numbers_to_examples = () => { | |
| // @ts-expect-error | |
| function generateLine(nb) { | |
| return `<span data-nosnippet>${nb}</span>`; | |
| } | |
| onEachLazy(document.querySelectorAll( | |
| ".rustdoc:not(.src) :not(.scraped-example) > .example-wrap > pre > code", | |
| ), code => { | |
| if (hasClass(code.parentElement.parentElement, "hide-lines")) { | |
| removeClass(code.parentElement.parentElement, "hide-lines"); | |
| return; | |
| } | |
| const lines = code.innerHTML.split("\n"); | |
| const digits = (lines.length + "").length; | |
| // @ts-expect-error | |
| code.innerHTML = lines.map((line, index) => generateLine(index + 1) + line).join("\n"); | |
| addClass(code.parentElement.parentElement, `digits-${digits}`); | |
| }); | |
| }; | |
| window.rustdoc_remove_line_numbers_from_examples = () => { | |
| onEachLazy( | |
| document.querySelectorAll(".rustdoc:not(.src) :not(.scraped-example) > .example-wrap"), | |
| x => addClass(x, "hide-lines"), | |
| ); | |
| }; | |
| if (getSettingValue("line-numbers") === "true") { | |
| window.rustdoc_add_line_numbers_to_examples(); | |
| } | |
| function showSidebar() { | |
| window.hideAllModals(false); | |
| const sidebar = document.getElementsByClassName("sidebar")[0]; | |
| addClass(sidebar, "shown"); | |
| } | |
| function hideSidebar() { | |
| const sidebar = document.getElementsByClassName("sidebar")[0]; | |
| removeClass(sidebar, "shown"); | |
| } | |
| window.addEventListener("resize", () => { | |
| if (window.CURRENT_TOOLTIP_ELEMENT) { | |
| // As a workaround to the behavior of `contains: layout` used in doc togglers, | |
| // tooltip popovers are positioned using javascript. | |
| // | |
| // This means when the window is resized, we need to redo the layout. | |
| const base = window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE; | |
| const force_visible = base.TOOLTIP_FORCE_VISIBLE; | |
| hideTooltip(false); | |
| if (force_visible) { | |
| showTooltip(base); | |
| base.TOOLTIP_FORCE_VISIBLE = true; | |
| } | |
| } | |
| }); | |
| const mainElem = document.getElementById(MAIN_ID); | |
| if (mainElem) { | |
| mainElem.addEventListener("click", hideSidebar); | |
| } | |
| onEachLazy(document.querySelectorAll("a[href^='#']"), el => { | |
| // For clicks on internal links (<A> tags with a hash property), we expand the section we're | |
| // jumping to *before* jumping there. We can't do this in onHashChange, because it changes | |
| // the height of the document so we wind up scrolled to the wrong place. | |
| el.addEventListener("click", () => { | |
| expandSection(el.hash.slice(1)); | |
| hideSidebar(); | |
| }); | |
| }); | |
| onEachLazy(document.querySelectorAll(".toggle > summary:not(.hideme)"), el => { | |
| // @ts-expect-error | |
| // Clicking on the summary's contents should not collapse it, | |
| // but links within should still fire. | |
| el.addEventListener("click", e => { | |
| if (!e.target.matches("summary, a, a *")) { | |
| e.preventDefault(); | |
| } | |
| }); | |
| }); | |
| /** | |
| * Show a tooltip immediately. | |
| * | |
| * @param {HTMLElement} e - The tooltip's anchor point. The DOM is consulted to figure | |
| * out what the tooltip should contain, and where it should be | |
| * positioned. | |
| */ | |
| function showTooltip(e) { | |
| const notable_ty = e.getAttribute("data-notable-ty"); | |
| if (!window.NOTABLE_TRAITS && notable_ty) { | |
| const data = document.getElementById("notable-traits-data"); | |
| if (data) { | |
| window.NOTABLE_TRAITS = JSON.parse(data.innerText); | |
| } else { | |
| throw new Error("showTooltip() called with notable without any notable traits!"); | |
| } | |
| } | |
| // Make this function idempotent. If the tooltip is already shown, avoid doing extra work | |
| // and leave it alone. | |
| if (window.CURRENT_TOOLTIP_ELEMENT && window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE === e) { | |
| clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT); | |
| return; | |
| } | |
| window.hideAllModals(false); | |
| // use Object.assign to make sure the object has the correct type | |
| // with all of the correct fields before it is assigned to a variable, | |
| // as typescript has no way to change the type of a variable once it is initialized. | |
| const wrapper = Object.assign(document.createElement("div"), {TOOLTIP_BASE: e}); | |
| if (notable_ty) { | |
| wrapper.innerHTML = "<div class=\"content\">" + | |
| // @ts-expect-error | |
| window.NOTABLE_TRAITS[notable_ty] + "</div>"; | |
| } else { | |
| // Replace any `title` attribute with `data-title` to avoid double tooltips. | |
| const ttl = e.getAttribute("title"); | |
| if (ttl !== null) { | |
| e.setAttribute("data-title", ttl); | |
| e.removeAttribute("title"); | |
| } | |
| const dttl = e.getAttribute("data-title"); | |
| if (dttl !== null) { | |
| const titleContent = document.createElement("div"); | |
| titleContent.className = "content"; | |
| titleContent.appendChild(document.createTextNode(dttl)); | |
| wrapper.appendChild(titleContent); | |
| } | |
| } | |
| wrapper.className = "tooltip popover"; | |
| const focusCatcher = document.createElement("div"); | |
| focusCatcher.setAttribute("tabindex", "0"); | |
| // @ts-expect-error | |
| focusCatcher.onfocus = hideTooltip; | |
| wrapper.appendChild(focusCatcher); | |
| const pos = e.getBoundingClientRect(); | |
| // 5px overlap so that the mouse can easily travel from place to place | |
| wrapper.style.top = (pos.top + window.scrollY + pos.height) + "px"; | |
| // @ts-expect-error | |
| wrapper.style.left = 0; | |
| wrapper.style.right = "auto"; | |
| wrapper.style.visibility = "hidden"; | |
| document.body.appendChild(wrapper); | |
| const wrapperPos = wrapper.getBoundingClientRect(); | |
| // offset so that the arrow points at the center of the "(i)" | |
| const finalPos = pos.left + window.scrollX - wrapperPos.width + 24; | |
| if (finalPos > 0) { | |
| wrapper.style.left = finalPos + "px"; | |
| } else { | |
| wrapper.style.setProperty( | |
| "--popover-arrow-offset", | |
| (wrapperPos.right - pos.right + 4) + "px", | |
| ); | |
| } | |
| wrapper.style.visibility = ""; | |
| window.CURRENT_TOOLTIP_ELEMENT = wrapper; | |
| clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT); | |
| wrapper.onpointerenter = ev => { | |
| // If this is a synthetic touch event, ignore it. A click event will be along shortly. | |
| if (ev.pointerType !== "mouse") { | |
| return; | |
| } | |
| clearTooltipHoverTimeout(e); | |
| }; | |
| wrapper.onpointerleave = ev => { | |
| // If this is a synthetic touch event, ignore it. A click event will be along shortly. | |
| if (ev.pointerType !== "mouse" || !(ev.relatedTarget instanceof HTMLElement)) { | |
| return; | |
| } | |
| if (!e.TOOLTIP_FORCE_VISIBLE && !e.contains(ev.relatedTarget)) { | |
| // See "Tooltip pointer leave gesture" below. | |
| setTooltipHoverTimeout(e, false); | |
| addClass(wrapper, "fade-out"); | |
| } | |
| }; | |
| } | |
| /** | |
| * Show or hide the tooltip after a timeout. If a timeout was already set before this function | |
| * was called, that timeout gets cleared. If the tooltip is already in the requested state, | |
| * this function will still clear any pending timeout, but otherwise do nothing. | |
| * | |
| * @param {HTMLElement} element - The tooltip's anchor point. The DOM is consulted to figure | |
| * out what the tooltip should contain, and where it should be | |
| * positioned. | |
| * @param {boolean} show - If true, the tooltip will be made visible. If false, it will | |
| * be hidden. | |
| */ | |
| function setTooltipHoverTimeout(element, show) { | |
| clearTooltipHoverTimeout(element); | |
| if (!show && !window.CURRENT_TOOLTIP_ELEMENT) { | |
| // To "hide" an already hidden element, just cancel its timeout. | |
| return; | |
| } | |
| if (show && window.CURRENT_TOOLTIP_ELEMENT) { | |
| // To "show" an already visible element, just cancel its timeout. | |
| return; | |
| } | |
| if (window.CURRENT_TOOLTIP_ELEMENT && | |
| window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE !== element) { | |
| // Don't do anything if another tooltip is already visible. | |
| return; | |
| } | |
| element.TOOLTIP_HOVER_TIMEOUT = setTimeout(() => { | |
| if (show) { | |
| showTooltip(element); | |
| } else if (!element.TOOLTIP_FORCE_VISIBLE) { | |
| hideTooltip(false); | |
| } | |
| }, show ? window.RUSTDOC_TOOLTIP_HOVER_MS : window.RUSTDOC_TOOLTIP_HOVER_EXIT_MS); | |
| } | |
| /** | |
| * If a show/hide timeout was set by `setTooltipHoverTimeout`, cancel it. If none exists, | |
| * do nothing. | |
| * | |
| * @param {HTMLElement} element - The tooltip's anchor point, | |
| * as passed to `setTooltipHoverTimeout`. | |
| */ | |
| function clearTooltipHoverTimeout(element) { | |
| if (element.TOOLTIP_HOVER_TIMEOUT !== undefined) { | |
| removeClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out"); | |
| clearTimeout(element.TOOLTIP_HOVER_TIMEOUT); | |
| delete element.TOOLTIP_HOVER_TIMEOUT; | |
| } | |
| } | |
| /** | |
| * @param {Event & { relatedTarget: Node }} event | |
| */ | |
| function tooltipBlurHandler(event) { | |
| if (window.CURRENT_TOOLTIP_ELEMENT && | |
| !window.CURRENT_TOOLTIP_ELEMENT.contains(document.activeElement) && | |
| !window.CURRENT_TOOLTIP_ELEMENT.contains(event.relatedTarget) && | |
| !window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.contains(document.activeElement) && | |
| !window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.contains(event.relatedTarget) | |
| ) { | |
| // Work around a difference in the focus behaviour between Firefox, Chrome, and Safari. | |
| // When I click the button on an already-opened tooltip popover, Safari | |
| // hides the popover and then immediately shows it again, while everyone else hides it | |
| // and it stays hidden. | |
| // | |
| // To work around this, make sure the click finishes being dispatched before | |
| // hiding the popover. Since `hideTooltip()` is idempotent, this makes Safari behave | |
| // consistently with the other two. | |
| setTimeout(() => hideTooltip(false), 0); | |
| } | |
| } | |
| /** | |
| * Hide the current tooltip immediately. | |
| * | |
| * @param {boolean} focus - If set to `true`, move keyboard focus to the tooltip anchor point. | |
| * If set to `false`, leave keyboard focus alone. | |
| */ | |
| function hideTooltip(focus) { | |
| if (window.CURRENT_TOOLTIP_ELEMENT) { | |
| if (window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE) { | |
| if (focus) { | |
| window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.focus(); | |
| } | |
| window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE = false; | |
| } | |
| document.body.removeChild(window.CURRENT_TOOLTIP_ELEMENT); | |
| clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT); | |
| window.CURRENT_TOOLTIP_ELEMENT = undefined; | |
| } | |
| } | |
| onEachLazy(document.getElementsByClassName("tooltip"), e => { | |
| e.onclick = () => { | |
| e.TOOLTIP_FORCE_VISIBLE = e.TOOLTIP_FORCE_VISIBLE ? false : true; | |
| if (window.CURRENT_TOOLTIP_ELEMENT && !e.TOOLTIP_FORCE_VISIBLE) { | |
| hideTooltip(true); | |
| } else { | |
| showTooltip(e); | |
| // @ts-expect-error | |
| window.CURRENT_TOOLTIP_ELEMENT.setAttribute("tabindex", "0"); | |
| // @ts-expect-error | |
| window.CURRENT_TOOLTIP_ELEMENT.focus(); | |
| // @ts-expect-error | |
| window.CURRENT_TOOLTIP_ELEMENT.onblur = tooltipBlurHandler; | |
| } | |
| return false; | |
| }; | |
| // @ts-expect-error | |
| e.onpointerenter = ev => { | |
| // If this is a synthetic touch event, ignore it. A click event will be along shortly. | |
| if (ev.pointerType !== "mouse") { | |
| return; | |
| } | |
| setTooltipHoverTimeout(e, true); | |
| }; | |
| // @ts-expect-error | |
| e.onpointermove = ev => { | |
| // If this is a synthetic touch event, ignore it. A click event will be along shortly. | |
| if (ev.pointerType !== "mouse") { | |
| return; | |
| } | |
| setTooltipHoverTimeout(e, true); | |
| }; | |
| // @ts-expect-error | |
| e.onpointerleave = ev => { | |
| // If this is a synthetic touch event, ignore it. A click event will be along shortly. | |
| if (ev.pointerType !== "mouse") { | |
| return; | |
| } | |
| if (!e.TOOLTIP_FORCE_VISIBLE && window.CURRENT_TOOLTIP_ELEMENT && | |
| !window.CURRENT_TOOLTIP_ELEMENT.contains(ev.relatedTarget)) { | |
| // Tooltip pointer leave gesture: | |
| // | |
| // Designing a good hover microinteraction is a matter of guessing user | |
| // intent from what are, literally, vague gestures. In this case, guessing if | |
| // hovering in or out of the tooltip base is intentional or not. | |
| // | |
| // To figure this out, a few different techniques are used: | |
| // | |
| // * When the mouse pointer enters a tooltip anchor point, its hitbox is grown | |
| // on the bottom, where the popover is/will appear. Search "hover tunnel" in | |
| // rustdoc.css for the implementation. | |
| // * There's a delay when the mouse pointer enters the popover base anchor, in | |
| // case the mouse pointer was just passing through and the user didn't want | |
| // to open it. | |
| // * Similarly, a delay is added when exiting the anchor, or the popover | |
| // itself, before hiding it. | |
| // * A fade-out animation is layered onto the pointer exit delay to immediately | |
| // inform the user that they successfully dismissed the popover, while still | |
| // providing a way for them to cancel it if it was a mistake and they still | |
| // wanted to interact with it. | |
| // * No animation is used for revealing it, because we don't want people to try | |
| // to interact with an element while it's in the middle of fading in: either | |
| // they're allowed to interact with it while it's fading in, meaning it can't | |
| // serve as mistake-proofing for the popover, or they can't, but | |
| // they might try and be frustrated. | |
| // | |
| // See also: | |
| // * https://www.nngroup.com/articles/timing-exposing-content/ | |
| // * https://www.nngroup.com/articles/tooltip-guidelines/ | |
| // * https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown | |
| setTooltipHoverTimeout(e, false); | |
| addClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out"); | |
| } | |
| }; | |
| }); | |
| const sidebar_menu_toggle = document.getElementsByClassName("sidebar-menu-toggle")[0]; | |
| if (sidebar_menu_toggle) { | |
| sidebar_menu_toggle.addEventListener("click", () => { | |
| const sidebar = document.getElementsByClassName("sidebar")[0]; | |
| // @ts-expect-error | |
| if (!hasClass(sidebar, "shown")) { | |
| showSidebar(); | |
| } else { | |
| hideSidebar(); | |
| } | |
| }); | |
| } | |
| // @ts-expect-error | |
| function helpBlurHandler(event) { | |
| const isInPopover = onEachLazy( | |
| document.querySelectorAll(".settings-menu, .help-menu"), | |
| menu => { | |
| return menu.contains(document.activeElement) || menu.contains(event.relatedTarget); | |
| }, | |
| ); | |
| if (!isInPopover) { | |
| window.hidePopoverMenus(); | |
| } | |
| } | |
| function buildHelpMenu() { | |
| const book_info = document.createElement("span"); | |
| const drloChannel = `https://doc.rust-lang.org/${getVar("channel")}`; | |
| book_info.className = "top"; | |
| book_info.innerHTML = `You can find more information in \ | |
| <a href="${drloChannel}/rustdoc/">the rustdoc book</a>.`; | |
| const shortcuts = [ | |
| ["?", "Show this help dialog"], | |
| ["S / /", "Focus the search field"], | |
| ["↑", "Move up in search results"], | |
| ["↓", "Move down in search results"], | |
| ["← / →", "Switch result tab (when results focused)"], | |
| ["⏎", "Go to active search result"], | |
| ["+ / =", "Expand all sections"], | |
| ["-", "Collapse all sections"], | |
| // for the sake of brevity, we don't say "inherit impl blocks", | |
| // although that would be more correct, | |
| // since trait impl blocks are collapsed by - | |
| ["_", "Collapse all sections, including impl blocks"], | |
| ].map(x => "<dt>" + | |
| x[0].split(" ") | |
| .map((y, index) => ((index & 1) === 0 ? "<kbd>" + y + "</kbd>" : " " + y + " ")) | |
| .join("") + "</dt><dd>" + x[1] + "</dd>").join(""); | |
| const div_shortcuts = document.createElement("div"); | |
| addClass(div_shortcuts, "shortcuts"); | |
| div_shortcuts.innerHTML = "<h2>Keyboard Shortcuts</h2><dl>" + shortcuts + "</dl></div>"; | |
| const infos = [ | |
| `For a full list of all search features, take a look \ | |
| <a href="${drloChannel}/rustdoc/read-documentation/search.html">here</a>.`, | |
| "Prefix searches with a type followed by a colon (e.g., <code>fn:</code>) to \ | |
| restrict the search to a given item kind.", | |
| "Accepted kinds are: <code>fn</code>, <code>mod</code>, <code>struct</code>, \ | |
| <code>enum</code>, <code>trait</code>, <code>type</code>, <code>macro</code>, \ | |
| and <code>constant</code>.", | |
| "Search functions by type signature (e.g., <code>vec -> usize</code> or \ | |
| <code>-> vec</code> or <code>String, enum:Cow -> bool</code>)", | |
| "You can look for items with an exact name by putting double quotes around \ | |
| your request: <code>\"string\"</code>", | |
| `Look for functions that accept or return \ | |
| <a href="${drloChannel}/std/primitive.slice.html">slices</a> and \ | |
| <a href="${drloChannel}/std/primitive.array.html">arrays</a> by writing square \ | |
| brackets (e.g., <code>-> [u8]</code> or <code>[] -> Option</code>)`, | |
| "Look for items inside another one by searching for a path: <code>vec::Vec</code>", | |
| ].map(x => "<p>" + x + "</p>").join(""); | |
| const div_infos = document.createElement("div"); | |
| addClass(div_infos, "infos"); | |
| div_infos.innerHTML = "<h2>Search Tricks</h2>" + infos; | |
| const rustdoc_version = document.createElement("span"); | |
| rustdoc_version.className = "bottom"; | |
| const rustdoc_version_code = document.createElement("code"); | |
| rustdoc_version_code.innerText = "rustdoc " + getVar("rustdoc-version"); | |
| rustdoc_version.appendChild(rustdoc_version_code); | |
| const container = document.createElement("div"); | |
| if (!isHelpPage) { | |
| container.className = "popover"; | |
| } | |
| container.id = "help"; | |
| const side_by_side = document.createElement("div"); | |
| side_by_side.className = "side-by-side"; | |
| side_by_side.appendChild(div_shortcuts); | |
| side_by_side.appendChild(div_infos); | |
| const content = document.createElement("div"); | |
| content.className = "content"; | |
| content.appendChild(book_info); | |
| content.appendChild(side_by_side); | |
| content.appendChild(rustdoc_version); | |
| container.appendChild(content); | |
| if (isHelpPage) { | |
| const help_section = document.createElement("section"); | |
| help_section.appendChild(container); | |
| nonnull(document.getElementById("main-content")).appendChild(help_section); | |
| } else { | |
| onEachLazy(document.getElementsByClassName("help-menu"), menu => { | |
| if (menu.offsetWidth !== 0) { | |
| menu.appendChild(container); | |
| container.onblur = helpBlurHandler; | |
| menu.onblur = helpBlurHandler; | |
| menu.children[0].onblur = helpBlurHandler; | |
| return true; | |
| } | |
| }); | |
| } | |
| return container; | |
| } | |
| /** | |
| * Hide popover menus, clickable tooltips, and the sidebar (if applicable). | |
| * | |
| * Pass `true` to reset focus for tooltip popovers. | |
| */ | |
| window.hideAllModals = switchFocus => { | |
| hideSidebar(); | |
| window.hidePopoverMenus(); | |
| hideTooltip(switchFocus); | |
| }; | |
| /** | |
| * Hide all the popover menus. | |
| */ | |
| window.hidePopoverMenus = () => { | |
| onEachLazy(document.querySelectorAll(".settings-menu .popover"), elem => { | |
| elem.style.display = "none"; | |
| }); | |
| onEachLazy(document.querySelectorAll(".help-menu .popover"), elem => { | |
| elem.parentElement.removeChild(elem); | |
| }); | |
| }; | |
| /** | |
| * Show the help popup menu. | |
| */ | |
| function showHelp() { | |
| window.hideAllModals(false); | |
| // Prevent `blur` events from being dispatched as a result of closing | |
| // other modals. | |
| onEachLazy(document.querySelectorAll(".help-menu a"), menu => { | |
| if (menu.offsetWidth !== 0) { | |
| menu.focus(); | |
| return true; | |
| } | |
| }); | |
| buildHelpMenu(); | |
| } | |
| if (isHelpPage) { | |
| buildHelpMenu(); | |
| } else { | |
| onEachLazy(document.querySelectorAll(".help-menu > a"), helpLink => { | |
| helpLink.addEventListener( | |
| "click", | |
| /** @param {MouseEvent} event */ | |
| event => { | |
| // By default, have help button open docs in a popover. | |
| // If user clicks with a moderator, though, use default browser behavior, | |
| // probably opening in a new window or tab. | |
| if (event.ctrlKey || | |
| event.altKey || | |
| event.metaKey) { | |
| return; | |
| } | |
| event.preventDefault(); | |
| if (document.getElementById("help")) { | |
| window.hidePopoverMenus(); | |
| } else { | |
| showHelp(); | |
| } | |
| }, | |
| ); | |
| }); | |
| } | |
| addSidebarItems(); | |
| addSidebarCrates(); | |
| onHashChange(null); | |
| window.addEventListener("hashchange", onHashChange); | |
| window.searchState.setup(); | |
| }()); | |
| // Hide, show, and resize the sidebar | |
| // | |
| // The body class and CSS variable are initially set up in storage.js, | |
| // but in this file, we implement: | |
| // | |
| // * the show sidebar button, which appears if the sidebar is hidden | |
| // and, by clicking on it, will bring it back | |
| // * the sidebar resize handle, which appears only on large viewports | |
| // with a [fine precision pointer] to allow the user to change | |
| // the size of the sidebar | |
| // | |
| // [fine precision pointer]: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pointer | |
| (function() { | |
| // 100 is the size of the logo | |
| // don't let the sidebar get smaller than that, or it'll get squished | |
| const SIDEBAR_MIN = 100; | |
| // Don't let the sidebar get bigger than this | |
| const SIDEBAR_MAX = 500; | |
| // Don't let the body (including the gutter) get smaller than this | |
| // | |
| // WARNING: RUSTDOC_MOBILE_BREAKPOINT MEDIA QUERY | |
| // Acceptable values for BODY_MIN are constrained by the mobile breakpoint | |
| // (which is the minimum size of the whole page where the sidebar exists) | |
| // and the default sidebar width: | |
| // | |
| // BODY_MIN <= RUSTDOC_MOBILE_BREAKPOINT - DEFAULT_SIDEBAR_WIDTH | |
| // | |
| // At the time of this writing, the DEFAULT_SIDEBAR_WIDTH on src pages is | |
| // 300px, and the RUSTDOC_MOBILE_BREAKPOINT is 700px, so BODY_MIN must be | |
| // at most 400px. Otherwise, it would start out at the default size, then | |
| // grabbing the resize handle would suddenly cause it to jank to | |
| // its constraint-generated maximum. | |
| const RUSTDOC_MOBILE_BREAKPOINT = 700; | |
| const BODY_MIN = 400; | |
| // At half-way past the minimum size, vanish the sidebar entirely | |
| const SIDEBAR_VANISH_THRESHOLD = SIDEBAR_MIN / 2; | |
| // Toolbar button to show the sidebar. | |
| // | |
| // On small, "mobile-sized" viewports, it's not persistent and it | |
| // can only be activated by going into Settings and hiding the nav bar. | |
| // On larger, "desktop-sized" viewports (though that includes many | |
| // tablets), it's fixed-position, appears in the left side margin, | |
| // and it can be activated by resizing the sidebar into nothing. | |
| let sidebarButton = document.getElementById("sidebar-button"); | |
| const body = document.querySelector(".main-heading"); | |
| if (!sidebarButton && body) { | |
| sidebarButton = document.createElement("div"); | |
| sidebarButton.id = "sidebar-button"; | |
| const path = `${window.rootPath}${window.currentCrate}/all.html`; | |
| sidebarButton.innerHTML = `<a href="${path}" title="show sidebar"></a>`; | |
| body.insertBefore(sidebarButton, body.firstChild); | |
| } | |
| if (sidebarButton) { | |
| sidebarButton.addEventListener("click", e => { | |
| removeClass(document.documentElement, "hide-sidebar"); | |
| updateLocalStorage("hide-sidebar", "false"); | |
| if (window.rustdocToggleSrcSidebar) { | |
| window.rustdocToggleSrcSidebar(); | |
| } | |
| e.preventDefault(); | |
| }); | |
| } | |
| /** | |
| * Pointer capture. | |
| * | |
| * Resizing is a single-pointer gesture. Any secondary pointer is ignored | |
| * | |
| * @type {null|number} | |
| */ | |
| let currentPointerId = null; | |
| /** | |
| * "Desired" sidebar size. | |
| * | |
| * This is stashed here for window resizing. If the sidebar gets | |
| * shrunk to maintain BODY_MIN, and then the user grows the window again, | |
| * it gets the sidebar to restore its size. | |
| * | |
| * @type {null|number} | |
| */ | |
| let desiredSidebarSize = null; | |
| /** | |
| * Sidebar resize debouncer. | |
| * | |
| * The sidebar itself is resized instantly, but the body HTML can be too | |
| * big for that, causing reflow jank. To reduce this, we queue up a separate | |
| * animation frame and throttle it. | |
| * | |
| * @type {false|ReturnType<typeof setTimeout>} | |
| */ | |
| let pendingSidebarResizingFrame = false; | |
| /** @type {HTMLElement|null} */ | |
| const resizer = document.querySelector(".sidebar-resizer"); | |
| /** @type {HTMLElement|null} */ | |
| const sidebar = document.querySelector(".sidebar"); | |
| // If this page has no sidebar at all, bail out. | |
| if (!resizer || !sidebar) { | |
| return; | |
| } | |
| // src page and docs page use different variables, because the contents of | |
| // the sidebar are so different that it's reasonable to thing the user | |
| // would want them to have different sizes | |
| const isSrcPage = hasClass(document.body, "src"); | |
| // Call this function to hide the sidebar when using the resize handle | |
| // | |
| // This function also nulls out the sidebar width CSS variable and setting, | |
| // causing it to return to its default. This does not happen if you do it | |
| // from settings.js, which uses a separate function. It's done here because | |
| // the minimum sidebar size is rather uncomfortable, and it must pass | |
| // through that size when using the shrink-to-nothing gesture. | |
| const hideSidebar = function() { | |
| if (isSrcPage) { | |
| window.rustdocCloseSourceSidebar(); | |
| updateLocalStorage("src-sidebar-width", null); | |
| // [RUSTDOCIMPL] CSS variable fast path | |
| // | |
| // The sidebar width variable is attached to the <html> element by | |
| // storage.js, because the sidebar and resizer don't exist yet. | |
| // But the resize code, in `resize()`, sets the property on the | |
| // sidebar and resizer elements (which are the only elements that | |
| // use the variable) to avoid recalculating CSS on the entire | |
| // document on every frame. | |
| // | |
| // So, to clear it, we need to clear all three. | |
| document.documentElement.style.removeProperty("--src-sidebar-width"); | |
| sidebar.style.removeProperty("--src-sidebar-width"); | |
| resizer.style.removeProperty("--src-sidebar-width"); | |
| } else { | |
| addClass(document.documentElement, "hide-sidebar"); | |
| updateLocalStorage("hide-sidebar", "true"); | |
| updateLocalStorage("desktop-sidebar-width", null); | |
| document.documentElement.style.removeProperty("--desktop-sidebar-width"); | |
| sidebar.style.removeProperty("--desktop-sidebar-width"); | |
| resizer.style.removeProperty("--desktop-sidebar-width"); | |
| } | |
| }; | |
| // Call this function to show the sidebar from the resize handle. | |
| // On docs pages, this can only happen if the user has grabbed the resize | |
| // handle, shrunk the sidebar down to nothing, and then pulls back into | |
| // the visible range without releasing it. You can, however, grab the | |
| // resize handle on a source page with the sidebar closed, because it | |
| // remains visible all the time on there. | |
| const showSidebar = function() { | |
| if (isSrcPage) { | |
| window.rustdocShowSourceSidebar(); | |
| } else { | |
| removeClass(document.documentElement, "hide-sidebar"); | |
| updateLocalStorage("hide-sidebar", "false"); | |
| } | |
| }; | |
| /** | |
| * Call this to set the correct CSS variable and setting. | |
| * This function doesn't enforce size constraints. Do that before calling it! | |
| * | |
| * @param {number} size - CSS px width of the sidebar. | |
| */ | |
| const changeSidebarSize = function(size) { | |
| if (isSrcPage) { | |
| updateLocalStorage("src-sidebar-width", size.toString()); | |
| // [RUSTDOCIMPL] CSS variable fast path | |
| // | |
| // While this property is set on the HTML element at load time, | |
| // because the sidebar isn't actually loaded yet, | |
| // we scope this update to the sidebar to avoid hitting a slow | |
| // path in WebKit. | |
| sidebar.style.setProperty("--src-sidebar-width", size + "px"); | |
| resizer.style.setProperty("--src-sidebar-width", size + "px"); | |
| } else { | |
| updateLocalStorage("desktop-sidebar-width", size.toString()); | |
| sidebar.style.setProperty("--desktop-sidebar-width", size + "px"); | |
| resizer.style.setProperty("--desktop-sidebar-width", size + "px"); | |
| } | |
| }; | |
| // Check if the sidebar is hidden. Since src pages and doc pages have | |
| // different settings, this function has to check that. | |
| const isSidebarHidden = function() { | |
| return isSrcPage ? | |
| !hasClass(document.documentElement, "src-sidebar-expanded") : | |
| hasClass(document.documentElement, "hide-sidebar"); | |
| }; | |
| /** | |
| * Respond to the resize handle event. | |
| * This function enforces size constraints, and implements the | |
| * shrink-to-nothing gesture based on thresholds defined above. | |
| * | |
| * @param {PointerEvent} e | |
| */ | |
| const resize = function(e) { | |
| if (currentPointerId === null || currentPointerId !== e.pointerId) { | |
| return; | |
| } | |
| e.preventDefault(); | |
| const pos = e.clientX - 3; | |
| if (pos < SIDEBAR_VANISH_THRESHOLD) { | |
| hideSidebar(); | |
| } else if (pos >= SIDEBAR_MIN) { | |
| if (isSidebarHidden()) { | |
| showSidebar(); | |
| } | |
| // don't let the sidebar get wider than SIDEBAR_MAX, or the body narrower | |
| // than BODY_MIN | |
| const constrainedPos = Math.min(pos, window.innerWidth - BODY_MIN, SIDEBAR_MAX); | |
| changeSidebarSize(constrainedPos); | |
| desiredSidebarSize = constrainedPos; | |
| if (pendingSidebarResizingFrame !== false) { | |
| clearTimeout(pendingSidebarResizingFrame); | |
| } | |
| pendingSidebarResizingFrame = setTimeout(() => { | |
| if (currentPointerId === null || pendingSidebarResizingFrame === false) { | |
| return; | |
| } | |
| pendingSidebarResizingFrame = false; | |
| document.documentElement.style.setProperty( | |
| "--resizing-sidebar-width", | |
| desiredSidebarSize + "px", | |
| ); | |
| }, 100); | |
| } | |
| }; | |
| // Respond to the window resize event. | |
| window.addEventListener("resize", () => { | |
| if (window.innerWidth < RUSTDOC_MOBILE_BREAKPOINT) { | |
| return; | |
| } | |
| stopResize(); | |
| if (desiredSidebarSize !== null && desiredSidebarSize >= (window.innerWidth - BODY_MIN)) { | |
| changeSidebarSize(window.innerWidth - BODY_MIN); | |
| } else if (desiredSidebarSize !== null && desiredSidebarSize > SIDEBAR_MIN) { | |
| changeSidebarSize(desiredSidebarSize); | |
| } | |
| }); | |
| /** | |
| * @param {PointerEvent=} e | |
| */ | |
| const stopResize = function(e) { | |
| if (currentPointerId === null) { | |
| return; | |
| } | |
| if (e) { | |
| e.preventDefault(); | |
| } | |
| desiredSidebarSize = sidebar.getBoundingClientRect().width; | |
| removeClass(resizer, "active"); | |
| window.removeEventListener("pointermove", resize, false); | |
| window.removeEventListener("pointerup", stopResize, false); | |
| removeClass(document.documentElement, "sidebar-resizing"); | |
| document.documentElement.style.removeProperty( "--resizing-sidebar-width"); | |
| if (resizer.releasePointerCapture) { | |
| resizer.releasePointerCapture(currentPointerId); | |
| currentPointerId = null; | |
| } | |
| }; | |
| /** | |
| * @param {PointerEvent} e | |
| */ | |
| const initResize = function(e) { | |
| if (currentPointerId !== null || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) { | |
| return; | |
| } | |
| if (resizer.setPointerCapture) { | |
| resizer.setPointerCapture(e.pointerId); | |
| if (!resizer.hasPointerCapture(e.pointerId)) { | |
| // unable to capture pointer; something else has it | |
| // on iOS, this usually means you long-clicked a link instead | |
| resizer.releasePointerCapture(e.pointerId); | |
| return; | |
| } | |
| currentPointerId = e.pointerId; | |
| } | |
| window.hideAllModals(false); | |
| e.preventDefault(); | |
| window.addEventListener("pointermove", resize, false); | |
| window.addEventListener("pointercancel", stopResize, false); | |
| window.addEventListener("pointerup", stopResize, false); | |
| addClass(resizer, "active"); | |
| addClass(document.documentElement, "sidebar-resizing"); | |
| const pos = e.clientX - sidebar.offsetLeft - 3; | |
| document.documentElement.style.setProperty( "--resizing-sidebar-width", pos + "px"); | |
| desiredSidebarSize = null; | |
| }; | |
| resizer.addEventListener("pointerdown", initResize, false); | |
| }()); | |
| // This section handles the copy button that appears next to the path breadcrumbs | |
| // and the copy buttons on the code examples. | |
| (function() { | |
| // Common functions to copy buttons. | |
| /** | |
| * @param {string|null} content | |
| */ | |
| function copyContentToClipboard(content) { | |
| if (content === null) { | |
| return; | |
| } | |
| const el = document.createElement("textarea"); | |
| el.value = content; | |
| el.setAttribute("readonly", ""); | |
| // To not make it appear on the screen. | |
| el.style.position = "absolute"; | |
| el.style.left = "-9999px"; | |
| document.body.appendChild(el); | |
| el.select(); | |
| document.execCommand("copy"); | |
| document.body.removeChild(el); | |
| } | |
| /** | |
| * @param {HTMLElement & {reset_button_timeout?: ReturnType<typeof setTimeout>}} button | |
| */ | |
| function copyButtonAnimation(button) { | |
| button.classList.add("clicked"); | |
| if (button.reset_button_timeout !== undefined) { | |
| clearTimeout(button.reset_button_timeout); | |
| } | |
| button.reset_button_timeout = setTimeout(() => { | |
| button.reset_button_timeout = undefined; | |
| button.classList.remove("clicked"); | |
| }, 1000); | |
| } | |
| // Copy button that appears next to the path breadcrumbs. | |
| const but = document.getElementById("copy-path"); | |
| if (!but) { | |
| return; | |
| } | |
| but.onclick = () => { | |
| // We get the path from the "breadcrumbs" and the actual item name. | |
| let path = ""; | |
| // @ts-expect-error | |
| const heading = document.getElementById(MAIN_ID).querySelector(".main-heading"); | |
| if (heading) { | |
| const breadcrumbs = heading.querySelector(".rustdoc-breadcrumbs"); | |
| if (breadcrumbs) { | |
| // @ts-expect-error | |
| path = breadcrumbs.innerText; | |
| if (path.length > 0) { | |
| path += "::"; | |
| } | |
| } | |
| // @ts-expect-error | |
| path += heading.querySelector("h1 > span").innerText; | |
| copyContentToClipboard(path); | |
| copyButtonAnimation(but); | |
| } | |
| }; | |
| /** | |
| * Copy buttons on code examples. | |
| * @param {HTMLElement|null} codeElem | |
| */ | |
| function copyCode(codeElem) { | |
| if (!codeElem) { | |
| // Should never happen, but the world is a dark and dangerous place. | |
| return; | |
| } | |
| let content = ""; | |
| for (const node of codeElem.childNodes) { | |
| // We exclude line numbers. | |
| if (node instanceof HTMLElement && node.hasAttribute("data-nosnippet")) { | |
| continue; | |
| } | |
| content += node.textContent; | |
| } | |
| copyContentToClipboard(content); | |
| } | |
| /** | |
| * @param {UIEvent} event | |
| * @returns {HTMLElement|null} | |
| */ | |
| function getExampleWrap(event) { | |
| const target = event.target; | |
| if (target instanceof HTMLElement) { | |
| /** @type {HTMLElement|null} */ | |
| let elem = target; | |
| while (elem !== null && !hasClass(elem, "example-wrap")) { | |
| if (elem === document.body || | |
| elem.tagName === "A" || | |
| elem.tagName === "BUTTON" || | |
| hasClass(elem, "docblock") | |
| ) { | |
| return null; | |
| } | |
| elem = elem.parentElement; | |
| } | |
| return elem; | |
| } else { | |
| return null; | |
| } | |
| } | |
| /** | |
| * @param {UIEvent} event | |
| */ | |
| function addCopyButton(event) { | |
| const elem = getExampleWrap(event); | |
| if (elem === null) { | |
| return; | |
| } | |
| // Since the button will be added, no need to keep this listener around. | |
| elem.removeEventListener("mouseover", addCopyButton); | |
| const parent = document.createElement("div"); | |
| parent.className = "button-holder"; | |
| const runButton = elem.querySelector(".test-arrow"); | |
| if (runButton !== null) { | |
| // If there is a run button, we move it into the same div. | |
| parent.appendChild(runButton); | |
| } | |
| elem.appendChild(parent); | |
| const copyButton = document.createElement("button"); | |
| copyButton.className = "copy-button"; | |
| copyButton.title = "Copy code to clipboard"; | |
| copyButton.addEventListener("click", () => { | |
| copyCode(elem.querySelector("pre > code")); | |
| copyButtonAnimation(copyButton); | |
| }); | |
| parent.appendChild(copyButton); | |
| if (!elem.parentElement || !elem.parentElement.classList.contains("scraped-example") || | |
| !window.updateScrapedExample) { | |
| return; | |
| } | |
| const scrapedWrapped = elem.parentElement; | |
| window.updateScrapedExample(scrapedWrapped, parent); | |
| } | |
| /** | |
| * @param {UIEvent} event | |
| */ | |
| function showHideCodeExampleButtons(event) { | |
| const elem = getExampleWrap(event); | |
| if (elem === null) { | |
| return; | |
| } | |
| let buttons = elem.querySelector(".button-holder"); | |
| if (buttons === null) { | |
| // On mobile, you can't hover an element so buttons need to be created on click | |
| // if they're not already there. | |
| addCopyButton(event); | |
| buttons = elem.querySelector(".button-holder"); | |
| if (buttons === null) { | |
| return; | |
| } | |
| } | |
| buttons.classList.toggle("keep-visible"); | |
| } | |
| onEachLazy(document.querySelectorAll(".docblock .example-wrap"), elem => { | |
| elem.addEventListener("mouseover", addCopyButton); | |
| elem.addEventListener("click", showHideCodeExampleButtons); | |
| }); | |
| }()); | |
| // Workaround for browser-specific bugs when copying code snippets. | |
| // | |
| // * In Firefox, copying text that includes elements with `user-select: none` | |
| // inserts extra blank lines. | |
| // - Firefox issue: https://bugzilla.mozilla.org/show_bug.cgi?id=1273836 | |
| // - Rust issue: https://github.com/rust-lang/rust/issues/141464 | |
| // | |
| // * In Chromium-based browsers, `document.getSelection()` includes elements | |
| // with `user-select: none`, causing unwanted line numbers to be copied. | |
| // - Chromium issue: https://issues.chromium.org/issues/446539520 | |
| // - Rust issue: https://github.com/rust-lang/rust/issues/146816 | |
| (function() { | |
| document.body.addEventListener("copy", event => { | |
| let target = nonnull(event.target); | |
| let isInsideCode = false; | |
| while (target && target !== document.body) { | |
| // @ts-expect-error | |
| if (target.tagName === "CODE") { | |
| isInsideCode = true; | |
| break; | |
| } | |
| // @ts-expect-error | |
| target = target.parentElement; | |
| } | |
| if (!isInsideCode) { | |
| return; | |
| } | |
| const selection = nonnull(document.getSelection()); | |
| const text = Array.from({ length: selection.rangeCount }, (_, i) => { | |
| const fragment = selection.getRangeAt(i).cloneContents(); | |
| fragment.querySelectorAll("[data-nosnippet]").forEach(el => el.remove()); | |
| return fragment.textContent; | |
| }).join(""); | |
| nonnull(event.clipboardData).setData("text/plain", text); | |
| event.preventDefault(); | |
| }); | |
| }()); |