2141 lines
84 KiB
JavaScript
2141 lines
84 KiB
JavaScript
// 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 <a href="#">${window.currentCrate}</a>`;
|
|
} 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 `<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 {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.<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: () => {
|
|
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 = "<h3 class=\"search-loading\">" +
|
|
window.searchState.loadingText +
|
|
"</h3>";
|
|
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 (`<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 name of filtered) {
|
|
let path;
|
|
if (shortty === "mod") {
|
|
path = `${modpath}${name}/index.html`;
|
|
} else {
|
|
path = `${modpath}${shortty}.${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("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 => {
|
|
const implementors = document.getElementById("implementors-list");
|
|
const synthetic_implementors = document.getElementById("synthetic-implementors-list");
|
|
const inlined_types = new Set();
|
|
|
|
const TEXT_IDX = 0;
|
|
const SYNTHETIC_IDX = 1;
|
|
const TYPES_IDX = 2;
|
|
|
|
if (synthetic_implementors) {
|
|
// 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(synthetic_implementors.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.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] ? synthetic_implementors : 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);
|
|
// @ts-expect-error
|
|
list.appendChild(display);
|
|
currentNbImpls += 1;
|
|
}
|
|
}
|
|
};
|
|
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 = document.querySelector("#main-content");
|
|
const sidebarSection = 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");
|
|
|
|
// @ts-expect-error
|
|
for (const impList of imp[window.currentCrate]) {
|
|
const types = impList.slice(2);
|
|
const text = impList[0];
|
|
const isTrait = impList[1] !== 0;
|
|
const traitName = impList[1];
|
|
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;
|
|
// @ts-expect-error
|
|
sidebarSection.appendChild(h);
|
|
sidebarTraitList = document.createElement("ul");
|
|
sidebarTraitList.className = "block trait-implementation";
|
|
// @ts-expect-error
|
|
sidebarSection.appendChild(sidebarTraitList);
|
|
// @ts-expect-error
|
|
mainContent.appendChild(outputListHeader);
|
|
// @ts-expect-error
|
|
mainContent.appendChild(outputList);
|
|
} else {
|
|
implementations = outputList;
|
|
if (trait_implementations) {
|
|
// @ts-expect-error
|
|
mainContent.insertBefore(outputListHeader, trait_implementations_header);
|
|
// @ts-expect-error
|
|
mainContent.insertBefore(outputList, trait_implementations_header);
|
|
} else {
|
|
const mainContent = document.querySelector("#main-content");
|
|
// @ts-expect-error
|
|
mainContent.appendChild(outputListHeader);
|
|
// @ts-expect-error
|
|
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");
|
|
// @ts-expect-error
|
|
a.href = `#${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;
|
|
// @ts-expect-error
|
|
sidebarSection.insertBefore(blockHeader, insertionReferenceH);
|
|
// @ts-expect-error
|
|
sidebarSection.insertBefore(block, insertionReferenceH);
|
|
} else {
|
|
// @ts-expect-error
|
|
sidebarSection.appendChild(blockHeader);
|
|
// @ts-expect-error
|
|
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";
|
|
}
|
|
|
|
function collapseAllDocs() {
|
|
const innerToggle = document.getElementById(toggleAllDocsId);
|
|
addClass(innerToggle, "will-expand");
|
|
onEachLazy(document.getElementsByClassName("toggle"), e => {
|
|
if (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";
|
|
}
|
|
|
|
function toggleAllDocs() {
|
|
const innerToggle = document.getElementById(toggleAllDocsId);
|
|
if (!innerToggle) {
|
|
return;
|
|
}
|
|
if (hasClass(innerToggle, "will-expand")) {
|
|
expandAllDocs();
|
|
} else {
|
|
collapseAllDocs();
|
|
}
|
|
}
|
|
|
|
(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;
|
|
}
|
|
|
|
});
|
|
}());
|
|
|
|
// @ts-expect-error
|
|
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}`);
|
|
});
|
|
};
|
|
|
|
// @ts-expect-error
|
|
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") {
|
|
// @ts-expect-error
|
|
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", () => {
|
|
// @ts-expect-error
|
|
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.
|
|
// @ts-expect-error
|
|
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
|
|
el.addEventListener("click", e => {
|
|
if (e.target.tagName !== "SUMMARY" && e.target.tagName !== "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");
|
|
// @ts-expect-error
|
|
if (!window.NOTABLE_TRAITS && notable_ty) {
|
|
const data = document.getElementById("notable-traits-data");
|
|
if (data) {
|
|
// @ts-expect-error
|
|
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.
|
|
// @ts-expect-error
|
|
if (window.CURRENT_TOOLTIP_ELEMENT && window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE === e) {
|
|
// @ts-expect-error
|
|
clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
|
|
return;
|
|
}
|
|
window.hideAllModals(false);
|
|
const wrapper = document.createElement("div");
|
|
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 = "";
|
|
// @ts-expect-error
|
|
window.CURRENT_TOOLTIP_ELEMENT = wrapper;
|
|
// @ts-expect-error
|
|
window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE = e;
|
|
// @ts-expect-error
|
|
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);
|
|
// @ts-expect-error
|
|
if (!show && !window.CURRENT_TOOLTIP_ELEMENT) {
|
|
// To "hide" an already hidden element, just cancel its timeout.
|
|
return;
|
|
}
|
|
// @ts-expect-error
|
|
if (show && window.CURRENT_TOOLTIP_ELEMENT) {
|
|
// To "show" an already visible element, just cancel its timeout.
|
|
return;
|
|
}
|
|
// @ts-expect-error
|
|
if (window.CURRENT_TOOLTIP_ELEMENT &&
|
|
// @ts-expect-error
|
|
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) {
|
|
// @ts-expect-error
|
|
removeClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out");
|
|
clearTimeout(element.TOOLTIP_HOVER_TIMEOUT);
|
|
delete element.TOOLTIP_HOVER_TIMEOUT;
|
|
}
|
|
}
|
|
|
|
// @ts-expect-error
|
|
function tooltipBlurHandler(event) {
|
|
// @ts-expect-error
|
|
if (window.CURRENT_TOOLTIP_ELEMENT &&
|
|
// @ts-expect-error
|
|
!window.CURRENT_TOOLTIP_ELEMENT.contains(document.activeElement) &&
|
|
// @ts-expect-error
|
|
!window.CURRENT_TOOLTIP_ELEMENT.contains(event.relatedTarget) &&
|
|
// @ts-expect-error
|
|
!window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.contains(document.activeElement) &&
|
|
// @ts-expect-error
|
|
!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) {
|
|
// @ts-expect-error
|
|
if (window.CURRENT_TOOLTIP_ELEMENT) {
|
|
// @ts-expect-error
|
|
if (window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE) {
|
|
if (focus) {
|
|
// @ts-expect-error
|
|
window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.focus();
|
|
}
|
|
// @ts-expect-error
|
|
window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE = false;
|
|
}
|
|
// @ts-expect-error
|
|
document.body.removeChild(window.CURRENT_TOOLTIP_ELEMENT);
|
|
// @ts-expect-error
|
|
clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
|
|
// @ts-expect-error
|
|
window.CURRENT_TOOLTIP_ELEMENT = null;
|
|
}
|
|
}
|
|
|
|
onEachLazy(document.getElementsByClassName("tooltip"), e => {
|
|
e.onclick = () => {
|
|
e.TOOLTIP_FORCE_VISIBLE = e.TOOLTIP_FORCE_VISIBLE ? false : true;
|
|
// @ts-expect-error
|
|
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;
|
|
}
|
|
// @ts-expect-error
|
|
if (!e.TOOLTIP_FORCE_VISIBLE && window.CURRENT_TOOLTIP_ELEMENT &&
|
|
// @ts-expect-error
|
|
!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);
|
|
// @ts-expect-error
|
|
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) {
|
|
// @ts-expect-error
|
|
if (!getHelpButton().contains(document.activeElement) &&
|
|
// @ts-expect-error
|
|
!getHelpButton().contains(event.relatedTarget) &&
|
|
// @ts-expect-error
|
|
!getSettingsButton().contains(document.activeElement) &&
|
|
// @ts-expect-error
|
|
!getSettingsButton().contains(event.relatedTarget)
|
|
) {
|
|
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"],
|
|
].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>const</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";
|
|
container.style.display = "none";
|
|
|
|
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);
|
|
|
|
container.appendChild(book_info);
|
|
container.appendChild(side_by_side);
|
|
container.appendChild(rustdoc_version);
|
|
|
|
if (isHelpPage) {
|
|
const help_section = document.createElement("section");
|
|
help_section.appendChild(container);
|
|
// @ts-expect-error
|
|
document.getElementById("main-content").appendChild(help_section);
|
|
container.style.display = "block";
|
|
} else {
|
|
const help_button = getHelpButton();
|
|
// @ts-expect-error
|
|
help_button.appendChild(container);
|
|
|
|
container.onblur = helpBlurHandler;
|
|
// @ts-expect-error
|
|
help_button.onblur = helpBlurHandler;
|
|
// @ts-expect-error
|
|
help_button.children[0].onblur = helpBlurHandler;
|
|
}
|
|
|
|
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("rustdoc-toolbar .popover"), elem => {
|
|
elem.style.display = "none";
|
|
});
|
|
const button = getHelpButton();
|
|
if (button) {
|
|
removeClass(button, "help-open");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the help menu element (not the button).
|
|
*
|
|
* @param {boolean} buildNeeded - If this argument is `false`, the help menu element won't be
|
|
* built if it doesn't exist.
|
|
*
|
|
* @return {HTMLElement}
|
|
*/
|
|
function getHelpMenu(buildNeeded) {
|
|
// @ts-expect-error
|
|
let menu = getHelpButton().querySelector(".popover");
|
|
if (!menu && buildNeeded) {
|
|
menu = buildHelpMenu();
|
|
}
|
|
// @ts-expect-error
|
|
return menu;
|
|
}
|
|
|
|
/**
|
|
* Show the help popup menu.
|
|
*/
|
|
function showHelp() {
|
|
// Prevent `blur` events from being dispatched as a result of closing
|
|
// other modals.
|
|
const button = getHelpButton();
|
|
addClass(button, "help-open");
|
|
// @ts-expect-error
|
|
button.querySelector("a").focus();
|
|
const menu = getHelpMenu(true);
|
|
if (menu.style.display === "none") {
|
|
// @ts-expect-error
|
|
window.hideAllModals();
|
|
menu.style.display = "";
|
|
}
|
|
}
|
|
|
|
const helpLink = document.querySelector(`#${HELP_BUTTON_ID} > a`);
|
|
if (isHelpPage) {
|
|
buildHelpMenu();
|
|
} else if (helpLink) {
|
|
helpLink.addEventListener("click", 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 (!helpLink.contains(helpLink) ||
|
|
// @ts-expect-error
|
|
event.ctrlKey ||
|
|
// @ts-expect-error
|
|
event.altKey ||
|
|
// @ts-expect-error
|
|
event.metaKey) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
const menu = getHelpMenu(true);
|
|
const shouldShowHelp = menu.style.display === "none";
|
|
if (shouldShowHelp) {
|
|
showHelp();
|
|
} else {
|
|
window.hidePopoverMenus();
|
|
}
|
|
});
|
|
}
|
|
|
|
setMobileTopbar();
|
|
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 contraint-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.
|
|
const sidebarButton = document.getElementById("sidebar-button");
|
|
if (sidebarButton) {
|
|
sidebarButton.addEventListener("click", e => {
|
|
removeClass(document.documentElement, "hide-sidebar");
|
|
updateLocalStorage("hide-sidebar", "false");
|
|
if (document.querySelector(".rustdoc.src")) {
|
|
// @ts-expect-error
|
|
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 = () => {
|
|
// Most page titles are '<Item> in <path::to::module> - Rust', except
|
|
// modules (which don't have the first part) and keywords/primitives
|
|
// (which don't have a module path)
|
|
const titleElement = document.querySelector("title");
|
|
const title = titleElement && titleElement.textContent ?
|
|
titleElement.textContent.replace(" - Rust", "") : "";
|
|
const [item, module] = title.split(" in ");
|
|
const path = [item];
|
|
if (module !== undefined) {
|
|
path.unshift(module);
|
|
}
|
|
|
|
copyContentToClipboard(path.join("::"));
|
|
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;
|
|
}
|
|
copyContentToClipboard(codeElem.textContent);
|
|
}
|
|
|
|
/**
|
|
* @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);
|
|
});
|
|
}());
|