]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/static/js/main.js
Rollup merge of #101641 - GuillaumeGomez:update-browser-ui-test, r=Dylan-DPC
[rust.git] / src / librustdoc / html / static / js / main.js
1 // Local js definitions:
2 /* global addClass, getSettingValue, hasClass, searchState */
3 /* global onEach, onEachLazy, removeClass */
4
5 "use strict";
6
7 // Get a value from the rustdoc-vars div, which is used to convey data from
8 // Rust to the JS. If there is no such element, return null.
9 function getVar(name) {
10     const el = document.getElementById("rustdoc-vars");
11     if (el) {
12         return el.attributes["data-" + name].value;
13     } else {
14         return null;
15     }
16 }
17
18 // Given a basename (e.g. "storage") and an extension (e.g. ".js"), return a URL
19 // for a resource under the root-path, with the resource-suffix.
20 function resourcePath(basename, extension) {
21     return getVar("root-path") + basename + getVar("resource-suffix") + extension;
22 }
23
24 function hideMain() {
25     addClass(document.getElementById(MAIN_ID), "hidden");
26 }
27
28 function showMain() {
29     removeClass(document.getElementById(MAIN_ID), "hidden");
30 }
31
32 function elemIsInParent(elem, parent) {
33     while (elem && elem !== document.body) {
34         if (elem === parent) {
35             return true;
36         }
37         elem = elem.parentElement;
38     }
39     return false;
40 }
41
42 function blurHandler(event, parentElem, hideCallback) {
43     if (!elemIsInParent(document.activeElement, parentElem) &&
44         !elemIsInParent(event.relatedTarget, parentElem)
45     ) {
46         hideCallback();
47     }
48 }
49
50 (function() {
51     window.rootPath = getVar("root-path");
52     window.currentCrate = getVar("current-crate");
53 }());
54
55 function setMobileTopbar() {
56     // FIXME: It would be nicer to generate this text content directly in HTML,
57     // but with the current code it's hard to get the right information in the right place.
58     const mobileLocationTitle = document.querySelector(".mobile-topbar h2.location");
59     const locationTitle = document.querySelector(".sidebar h2.location");
60     if (mobileLocationTitle && locationTitle) {
61         mobileLocationTitle.innerHTML = locationTitle.innerHTML;
62     }
63 }
64
65 // Gets the human-readable string for the virtual-key code of the
66 // given KeyboardEvent, ev.
67 //
68 // This function is meant as a polyfill for KeyboardEvent#key,
69 // since it is not supported in IE 11 or Chrome for Android. We also test for
70 // KeyboardEvent#keyCode because the handleShortcut handler is
71 // also registered for the keydown event, because Blink doesn't fire
72 // keypress on hitting the Escape key.
73 //
74 // So I guess you could say things are getting pretty interoperable.
75 function getVirtualKey(ev) {
76     if ("key" in ev && typeof ev.key !== "undefined") {
77         return ev.key;
78     }
79
80     const c = ev.charCode || ev.keyCode;
81     if (c === 27) {
82         return "Escape";
83     }
84     return String.fromCharCode(c);
85 }
86
87 const MAIN_ID = "main-content";
88 const SETTINGS_BUTTON_ID = "settings-menu";
89 const ALTERNATIVE_DISPLAY_ID = "alternative-display";
90 const NOT_DISPLAYED_ID = "not-displayed";
91 const HELP_BUTTON_ID = "help-button";
92
93 function getSettingsButton() {
94     return document.getElementById(SETTINGS_BUTTON_ID);
95 }
96
97 function getHelpButton() {
98     return document.getElementById(HELP_BUTTON_ID);
99 }
100
101 // Returns the current URL without any query parameter or hash.
102 function getNakedUrl() {
103     return window.location.href.split("?")[0].split("#")[0];
104 }
105
106 /**
107  * This function inserts `newNode` after `referenceNode`. It doesn't work if `referenceNode`
108  * doesn't have a parent node.
109  *
110  * @param {HTMLElement} newNode
111  * @param {HTMLElement} referenceNode
112  */
113 function insertAfter(newNode, referenceNode) {
114     referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
115 }
116
117 /**
118  * This function creates a new `<section>` with the given `id` and `classes` if it doesn't already
119  * exist.
120  *
121  * More information about this in `switchDisplayedElement` documentation.
122  *
123  * @param {string} id
124  * @param {string} classes
125  */
126 function getOrCreateSection(id, classes) {
127     let el = document.getElementById(id);
128
129     if (!el) {
130         el = document.createElement("section");
131         el.id = id;
132         el.className = classes;
133         insertAfter(el, document.getElementById(MAIN_ID));
134     }
135     return el;
136 }
137
138 /**
139  * Returns the `<section>` element which contains the displayed element.
140  *
141  * @return {HTMLElement}
142  */
143 function getAlternativeDisplayElem() {
144     return getOrCreateSection(ALTERNATIVE_DISPLAY_ID, "content hidden");
145 }
146
147 /**
148  * Returns the `<section>` element which contains the not-displayed elements.
149  *
150  * @return {HTMLElement}
151  */
152 function getNotDisplayedElem() {
153     return getOrCreateSection(NOT_DISPLAYED_ID, "hidden");
154 }
155
156 /**
157  * To nicely switch between displayed "extra" elements (such as search results or settings menu)
158  * and to alternate between the displayed and not displayed elements, we hold them in two different
159  * `<section>` elements. They work in pair: one holds the hidden elements while the other
160  * contains the displayed element (there can be only one at the same time!). So basically, we switch
161  * elements between the two `<section>` elements.
162  *
163  * @param {HTMLElement} elemToDisplay
164  */
165 function switchDisplayedElement(elemToDisplay) {
166     const el = getAlternativeDisplayElem();
167
168     if (el.children.length > 0) {
169         getNotDisplayedElem().appendChild(el.firstElementChild);
170     }
171     if (elemToDisplay === null) {
172         addClass(el, "hidden");
173         showMain();
174         return;
175     }
176     el.appendChild(elemToDisplay);
177     hideMain();
178     removeClass(el, "hidden");
179 }
180
181 function browserSupportsHistoryApi() {
182     return window.history && typeof window.history.pushState === "function";
183 }
184
185 // eslint-disable-next-line no-unused-vars
186 function loadCss(cssFileName) {
187     const link = document.createElement("link");
188     link.href = resourcePath(cssFileName, ".css");
189     link.type = "text/css";
190     link.rel = "stylesheet";
191     document.getElementsByTagName("head")[0].appendChild(link);
192 }
193
194 (function() {
195     function loadScript(url) {
196         const script = document.createElement("script");
197         script.src = url;
198         document.head.append(script);
199     }
200
201     getSettingsButton().onclick = event => {
202         addClass(getSettingsButton(), "rotate");
203         event.preventDefault();
204         // Sending request for the CSS and the JS files at the same time so it will
205         // hopefully be loaded when the JS will generate the settings content.
206         loadCss("settings");
207         loadScript(resourcePath("settings", ".js"));
208     };
209
210     window.searchState = {
211         loadingText: "Loading search results...",
212         input: document.getElementsByClassName("search-input")[0],
213         outputElement: () => {
214             let el = document.getElementById("search");
215             if (!el) {
216                 el = document.createElement("section");
217                 el.id = "search";
218                 getNotDisplayedElem().appendChild(el);
219             }
220             return el;
221         },
222         title: document.title,
223         titleBeforeSearch: document.title,
224         timeout: null,
225         // On the search screen, so you remain on the last tab you opened.
226         //
227         // 0 for "In Names"
228         // 1 for "In Parameters"
229         // 2 for "In Return Types"
230         currentTab: 0,
231         // tab and back preserves the element that was focused.
232         focusedByTab: [null, null, null],
233         clearInputTimeout: () => {
234             if (searchState.timeout !== null) {
235                 clearTimeout(searchState.timeout);
236                 searchState.timeout = null;
237             }
238         },
239         isDisplayed: () => searchState.outputElement().parentElement.id === ALTERNATIVE_DISPLAY_ID,
240         // Sets the focus on the search bar at the top of the page
241         focus: () => {
242             searchState.input.focus();
243         },
244         // Removes the focus from the search bar.
245         defocus: () => {
246             searchState.input.blur();
247         },
248         showResults: search => {
249             if (search === null || typeof search === "undefined") {
250                 search = searchState.outputElement();
251             }
252             switchDisplayedElement(search);
253             searchState.mouseMovedAfterSearch = false;
254             document.title = searchState.title;
255         },
256         hideResults: () => {
257             switchDisplayedElement(null);
258             document.title = searchState.titleBeforeSearch;
259             // We also remove the query parameter from the URL.
260             if (browserSupportsHistoryApi()) {
261                 history.replaceState(null, window.currentCrate + " - Rust",
262                     getNakedUrl() + window.location.hash);
263             }
264         },
265         getQueryStringParams: () => {
266             const params = {};
267             window.location.search.substring(1).split("&").
268                 map(s => {
269                     const pair = s.split("=");
270                     params[decodeURIComponent(pair[0])] =
271                         typeof pair[1] === "undefined" ? null : decodeURIComponent(pair[1]);
272                 });
273             return params;
274         },
275         setup: () => {
276             const search_input = searchState.input;
277             if (!searchState.input) {
278                 return;
279             }
280             let searchLoaded = false;
281             function loadSearch() {
282                 if (!searchLoaded) {
283                     searchLoaded = true;
284                     loadScript(resourcePath("search", ".js"));
285                     loadScript(resourcePath("search-index", ".js"));
286                 }
287             }
288
289             search_input.addEventListener("focus", () => {
290                 search_input.origPlaceholder = search_input.placeholder;
291                 search_input.placeholder = "Type your search here.";
292                 loadSearch();
293             });
294
295             if (search_input.value !== "") {
296                 loadSearch();
297             }
298
299             const params = searchState.getQueryStringParams();
300             if (params.search !== undefined) {
301                 const search = searchState.outputElement();
302                 search.innerHTML = "<h3 class=\"search-loading\">" +
303                     searchState.loadingText + "</h3>";
304                 searchState.showResults(search);
305                 loadSearch();
306             }
307         },
308     };
309
310     function getPageId() {
311         if (window.location.hash) {
312             const tmp = window.location.hash.replace(/^#/, "");
313             if (tmp.length > 0) {
314                 return tmp;
315             }
316         }
317         return null;
318     }
319
320     const toggleAllDocsId = "toggle-all-docs";
321     let savedHash = "";
322
323     function handleHashes(ev) {
324         if (ev !== null && searchState.isDisplayed() && ev.newURL) {
325             // This block occurs when clicking on an element in the navbar while
326             // in a search.
327             switchDisplayedElement(null);
328             const hash = ev.newURL.slice(ev.newURL.indexOf("#") + 1);
329             if (browserSupportsHistoryApi()) {
330                 // `window.location.search`` contains all the query parameters, not just `search`.
331                 history.replaceState(null, "",
332                     getNakedUrl() + window.location.search + "#" + hash);
333             }
334             const elem = document.getElementById(hash);
335             if (elem) {
336                 elem.scrollIntoView();
337             }
338         }
339         // This part is used in case an element is not visible.
340         if (savedHash !== window.location.hash) {
341             savedHash = window.location.hash;
342             if (savedHash.length === 0) {
343                 return;
344             }
345             expandSection(savedHash.slice(1)); // we remove the '#'
346         }
347     }
348
349     function onHashChange(ev) {
350         // If we're in mobile mode, we should hide the sidebar in any case.
351         hideSidebar();
352         handleHashes(ev);
353     }
354
355     function openParentDetails(elem) {
356         while (elem) {
357             if (elem.tagName === "DETAILS") {
358                 elem.open = true;
359             }
360             elem = elem.parentNode;
361         }
362     }
363
364     function expandSection(id) {
365         openParentDetails(document.getElementById(id));
366     }
367
368     function handleEscape(ev) {
369         searchState.clearInputTimeout();
370         switchDisplayedElement(null);
371         if (browserSupportsHistoryApi()) {
372             history.replaceState(null, window.currentCrate + " - Rust",
373                 getNakedUrl() + window.location.hash);
374         }
375         ev.preventDefault();
376         searchState.defocus();
377         window.hidePopoverMenus();
378     }
379
380     function handleShortcut(ev) {
381         // Don't interfere with browser shortcuts
382         const disableShortcuts = getSettingValue("disable-shortcuts") === "true";
383         if (ev.ctrlKey || ev.altKey || ev.metaKey || disableShortcuts) {
384             return;
385         }
386
387         if (document.activeElement.tagName === "INPUT" &&
388             document.activeElement.type !== "checkbox") {
389             switch (getVirtualKey(ev)) {
390             case "Escape":
391                 handleEscape(ev);
392                 break;
393             }
394         } else {
395             switch (getVirtualKey(ev)) {
396             case "Escape":
397                 handleEscape(ev);
398                 break;
399
400             case "s":
401             case "S":
402                 ev.preventDefault();
403                 searchState.focus();
404                 break;
405
406             case "+":
407             case "-":
408                 ev.preventDefault();
409                 toggleAllDocs();
410                 break;
411
412             case "?":
413                 showHelp();
414                 break;
415
416             default:
417                 break;
418             }
419         }
420     }
421
422     document.addEventListener("keypress", handleShortcut);
423     document.addEventListener("keydown", handleShortcut);
424
425     function addSidebarItems() {
426         if (!window.SIDEBAR_ITEMS) {
427             return;
428         }
429         const sidebar = document.getElementsByClassName("sidebar-elems")[0];
430
431         /**
432          * Append to the sidebar a "block" of links - a heading along with a list (`<ul>`) of items.
433          *
434          * @param {string} shortty - A short type name, like "primitive", "mod", or "macro"
435          * @param {string} id - The HTML id of the corresponding section on the module page.
436          * @param {string} longty - A long, capitalized, plural name, like "Primitive Types",
437          *                          "Modules", or "Macros".
438          */
439         function block(shortty, id, longty) {
440             const filtered = window.SIDEBAR_ITEMS[shortty];
441             if (!filtered) {
442                 return;
443             }
444
445             const div = document.createElement("div");
446             div.className = "block " + shortty;
447             const h3 = document.createElement("h3");
448             h3.innerHTML = `<a href="index.html#${id}">${longty}</a>`;
449             div.appendChild(h3);
450             const ul = document.createElement("ul");
451
452             for (const item of filtered) {
453                 const name = item[0];
454                 const desc = item[1]; // can be null
455
456                 let klass = shortty;
457                 let path;
458                 if (shortty === "mod") {
459                     path = name + "/index.html";
460                 } else {
461                     path = shortty + "." + name + ".html";
462                 }
463                 const current_page = document.location.href.split("/").pop();
464                 if (path === current_page) {
465                     klass += " current";
466                 }
467                 const link = document.createElement("a");
468                 link.href = path;
469                 link.title = desc;
470                 link.className = klass;
471                 link.textContent = name;
472                 const li = document.createElement("li");
473                 li.appendChild(link);
474                 ul.appendChild(li);
475             }
476             div.appendChild(ul);
477             sidebar.appendChild(div);
478         }
479
480         if (sidebar) {
481             block("primitive", "primitives", "Primitive Types");
482             block("mod", "modules", "Modules");
483             block("macro", "macros", "Macros");
484             block("struct", "structs", "Structs");
485             block("enum", "enums", "Enums");
486             block("union", "unions", "Unions");
487             block("constant", "constants", "Constants");
488             block("static", "static", "Statics");
489             block("trait", "traits", "Traits");
490             block("fn", "functions", "Functions");
491             block("type", "types", "Type Definitions");
492             block("foreigntype", "foreign-types", "Foreign Types");
493             block("keyword", "keywords", "Keywords");
494             block("traitalias", "trait-aliases", "Trait Aliases");
495         }
496     }
497
498     window.register_implementors = imp => {
499         const implementors = document.getElementById("implementors-list");
500         const synthetic_implementors = document.getElementById("synthetic-implementors-list");
501         const inlined_types = new Set();
502
503         const TEXT_IDX = 0;
504         const SYNTHETIC_IDX = 1;
505         const TYPES_IDX = 2;
506
507         if (synthetic_implementors) {
508             // This `inlined_types` variable is used to avoid having the same implementation
509             // showing up twice. For example "String" in the "Sync" doc page.
510             //
511             // By the way, this is only used by and useful for traits implemented automatically
512             // (like "Send" and "Sync").
513             onEachLazy(synthetic_implementors.getElementsByClassName("impl"), el => {
514                 const aliases = el.getAttribute("data-aliases");
515                 if (!aliases) {
516                     return;
517                 }
518                 aliases.split(",").forEach(alias => {
519                     inlined_types.add(alias);
520                 });
521             });
522         }
523
524         let currentNbImpls = implementors.getElementsByClassName("impl").length;
525         const traitName = document.querySelector("h1.fqn > .in-band > .trait").textContent;
526         const baseIdName = "impl-" + traitName + "-";
527         const libs = Object.getOwnPropertyNames(imp);
528         // We don't want to include impls from this JS file, when the HTML already has them.
529         // The current crate should always be ignored. Other crates that should also be
530         // ignored are included in the attribute `data-ignore-extern-crates`.
531         const script = document
532             .querySelector("script[data-ignore-extern-crates]");
533         const ignoreExternCrates = script ? script.getAttribute("data-ignore-extern-crates") : "";
534         for (const lib of libs) {
535             if (lib === window.currentCrate || ignoreExternCrates.indexOf(lib) !== -1) {
536                 continue;
537             }
538             const structs = imp[lib];
539
540             struct_loop:
541             for (const struct of structs) {
542                 const list = struct[SYNTHETIC_IDX] ? synthetic_implementors : implementors;
543
544                 // The types list is only used for synthetic impls.
545                 // If this changes, `main.js` and `write_shared.rs` both need changed.
546                 if (struct[SYNTHETIC_IDX]) {
547                     for (const struct_type of struct[TYPES_IDX]) {
548                         if (inlined_types.has(struct_type)) {
549                             continue struct_loop;
550                         }
551                         inlined_types.add(struct_type);
552                     }
553                 }
554
555                 const code = document.createElement("h3");
556                 code.innerHTML = struct[TEXT_IDX];
557                 addClass(code, "code-header");
558                 addClass(code, "in-band");
559
560                 onEachLazy(code.getElementsByTagName("a"), elem => {
561                     const href = elem.getAttribute("href");
562
563                     if (href && href.indexOf("http") !== 0) {
564                         elem.setAttribute("href", window.rootPath + href);
565                     }
566                 });
567
568                 const currentId = baseIdName + currentNbImpls;
569                 const anchor = document.createElement("a");
570                 anchor.href = "#" + currentId;
571                 addClass(anchor, "anchor");
572
573                 const display = document.createElement("div");
574                 display.id = currentId;
575                 addClass(display, "impl");
576                 display.appendChild(anchor);
577                 display.appendChild(code);
578                 list.appendChild(display);
579                 currentNbImpls += 1;
580             }
581         }
582     };
583     if (window.pending_implementors) {
584         window.register_implementors(window.pending_implementors);
585     }
586
587     function addSidebarCrates() {
588         if (!window.ALL_CRATES) {
589             return;
590         }
591         const sidebarElems = document.getElementsByClassName("sidebar-elems")[0];
592         if (!sidebarElems) {
593             return;
594         }
595         // Draw a convenient sidebar of known crates if we have a listing
596         const div = document.createElement("div");
597         div.className = "block crate";
598         div.innerHTML = "<h3>Crates</h3>";
599         const ul = document.createElement("ul");
600         div.appendChild(ul);
601
602         for (const crate of window.ALL_CRATES) {
603             let klass = "crate";
604             if (window.rootPath !== "./" && crate === window.currentCrate) {
605                 klass += " current";
606             }
607             const link = document.createElement("a");
608             link.href = window.rootPath + crate + "/index.html";
609             link.className = klass;
610             link.textContent = crate;
611
612             const li = document.createElement("li");
613             li.appendChild(link);
614             ul.appendChild(li);
615         }
616         sidebarElems.appendChild(div);
617     }
618
619
620     function labelForToggleButton(sectionIsCollapsed) {
621         if (sectionIsCollapsed) {
622             // button will expand the section
623             return "+";
624         }
625         // button will collapse the section
626         // note that this text is also set in the HTML template in ../render/mod.rs
627         return "\u2212"; // "\u2212" is "−" minus sign
628     }
629
630     function toggleAllDocs() {
631         const innerToggle = document.getElementById(toggleAllDocsId);
632         if (!innerToggle) {
633             return;
634         }
635         let sectionIsCollapsed = false;
636         if (hasClass(innerToggle, "will-expand")) {
637             removeClass(innerToggle, "will-expand");
638             onEachLazy(document.getElementsByClassName("rustdoc-toggle"), e => {
639                 if (!hasClass(e, "type-contents-toggle")) {
640                     e.open = true;
641                 }
642             });
643             innerToggle.title = "collapse all docs";
644         } else {
645             addClass(innerToggle, "will-expand");
646             onEachLazy(document.getElementsByClassName("rustdoc-toggle"), e => {
647                 if (e.parentNode.id !== "implementations-list" ||
648                     (!hasClass(e, "implementors-toggle") &&
649                      !hasClass(e, "type-contents-toggle"))
650                 ) {
651                     e.open = false;
652                 }
653             });
654             sectionIsCollapsed = true;
655             innerToggle.title = "expand all docs";
656         }
657         innerToggle.children[0].innerText = labelForToggleButton(sectionIsCollapsed);
658     }
659
660     (function() {
661         const toggles = document.getElementById(toggleAllDocsId);
662         if (toggles) {
663             toggles.onclick = toggleAllDocs;
664         }
665
666         const hideMethodDocs = getSettingValue("auto-hide-method-docs") === "true";
667         const hideImplementations = getSettingValue("auto-hide-trait-implementations") === "true";
668         const hideLargeItemContents = getSettingValue("auto-hide-large-items") !== "false";
669
670         function setImplementorsTogglesOpen(id, open) {
671             const list = document.getElementById(id);
672             if (list !== null) {
673                 onEachLazy(list.getElementsByClassName("implementors-toggle"), e => {
674                     e.open = open;
675                 });
676             }
677         }
678
679         if (hideImplementations) {
680             setImplementorsTogglesOpen("trait-implementations-list", false);
681             setImplementorsTogglesOpen("blanket-implementations-list", false);
682         }
683
684         onEachLazy(document.getElementsByClassName("rustdoc-toggle"), e => {
685             if (!hideLargeItemContents && hasClass(e, "type-contents-toggle")) {
686                 e.open = true;
687             }
688             if (hideMethodDocs && hasClass(e, "method-toggle")) {
689                 e.open = false;
690             }
691
692         });
693
694         const pageId = getPageId();
695         if (pageId !== null) {
696             expandSection(pageId);
697         }
698     }());
699
700     (function() {
701         // To avoid checking on "rustdoc-line-numbers" value on every loop...
702         if (getSettingValue("line-numbers") === "true") {
703             onEachLazy(document.getElementsByClassName("rust-example-rendered"), x => {
704                 const count = x.textContent.split("\n").length;
705                 const elems = [];
706                 for (let i = 0; i < count; ++i) {
707                     elems.push(i + 1);
708                 }
709                 const node = document.createElement("pre");
710                 addClass(node, "line-number");
711                 node.innerHTML = elems.join("\n");
712                 x.parentNode.insertBefore(node, x);
713             });
714         }
715     }());
716
717     let oldSidebarScrollPosition = null;
718
719     function showSidebar() {
720         if (window.innerWidth < window.RUSTDOC_MOBILE_BREAKPOINT) {
721             // This is to keep the scroll position on mobile.
722             oldSidebarScrollPosition = window.scrollY;
723             document.body.style.width = `${document.body.offsetWidth}px`;
724             document.body.style.position = "fixed";
725             document.body.style.top = `-${oldSidebarScrollPosition}px`;
726             document.querySelector(".mobile-topbar").style.top = `${oldSidebarScrollPosition}px`;
727             document.querySelector(".mobile-topbar").style.position = "relative";
728         } else {
729             oldSidebarScrollPosition = null;
730         }
731         const sidebar = document.getElementsByClassName("sidebar")[0];
732         addClass(sidebar, "shown");
733     }
734
735     function hideSidebar() {
736         if (oldSidebarScrollPosition !== null) {
737             // This is to keep the scroll position on mobile.
738             document.body.style.width = "";
739             document.body.style.position = "";
740             document.body.style.top = "";
741             document.querySelector(".mobile-topbar").style.top = "";
742             document.querySelector(".mobile-topbar").style.position = "";
743             // The scroll position is lost when resetting the style, hence why we store it in
744             // `oldSidebarScrollPosition`.
745             window.scrollTo(0, oldSidebarScrollPosition);
746             oldSidebarScrollPosition = null;
747         }
748         const sidebar = document.getElementsByClassName("sidebar")[0];
749         removeClass(sidebar, "shown");
750     }
751
752     window.addEventListener("resize", () => {
753         if (window.innerWidth >= window.RUSTDOC_MOBILE_BREAKPOINT &&
754             oldSidebarScrollPosition !== null) {
755             // If the user opens the sidebar in "mobile" mode, and then grows the browser window,
756             // we need to switch away from mobile mode and make the main content area scrollable.
757             hideSidebar();
758         }
759     });
760
761     function handleClick(id, f) {
762         const elem = document.getElementById(id);
763         if (elem) {
764             elem.addEventListener("click", f);
765         }
766     }
767     handleClick(MAIN_ID, () => {
768         hideSidebar();
769     });
770
771     onEachLazy(document.getElementsByTagName("a"), el => {
772         // For clicks on internal links (<A> tags with a hash property), we expand the section we're
773         // jumping to *before* jumping there. We can't do this in onHashChange, because it changes
774         // the height of the document so we wind up scrolled to the wrong place.
775         if (el.hash) {
776             el.addEventListener("click", () => {
777                 expandSection(el.hash.slice(1));
778                 hideSidebar();
779             });
780         }
781     });
782
783     onEachLazy(document.querySelectorAll(".rustdoc-toggle > summary:not(.hideme)"), el => {
784         el.addEventListener("click", e => {
785             if (e.target.tagName !== "SUMMARY" && e.target.tagName !== "A") {
786                 e.preventDefault();
787             }
788         });
789     });
790
791     onEachLazy(document.getElementsByClassName("notable-traits"), e => {
792         e.onclick = function() {
793             this.getElementsByClassName("notable-traits-tooltiptext")[0]
794                 .classList.toggle("force-tooltip");
795         };
796     });
797
798     const sidebar_menu_toggle = document.getElementsByClassName("sidebar-menu-toggle")[0];
799     if (sidebar_menu_toggle) {
800         sidebar_menu_toggle.addEventListener("click", () => {
801             const sidebar = document.getElementsByClassName("sidebar")[0];
802             if (!hasClass(sidebar, "shown")) {
803                 showSidebar();
804             } else {
805                 hideSidebar();
806             }
807         });
808     }
809
810     function helpBlurHandler(event) {
811         blurHandler(event, getHelpButton(), window.hidePopoverMenus);
812     }
813
814     function buildHelpMenu() {
815         const book_info = document.createElement("span");
816         book_info.className = "top";
817         book_info.innerHTML = "You can find more information in \
818             <a href=\"https://doc.rust-lang.org/rustdoc/\">the rustdoc book</a>.";
819
820         const shortcuts = [
821             ["?", "Show this help dialog"],
822             ["S", "Focus the search field"],
823             ["↑", "Move up in search results"],
824             ["↓", "Move down in search results"],
825             ["← / →", "Switch result tab (when results focused)"],
826             ["&#9166;", "Go to active search result"],
827             ["+", "Expand all sections"],
828             ["-", "Collapse all sections"],
829         ].map(x => "<dt>" +
830             x[0].split(" ")
831                 .map((y, index) => ((index & 1) === 0 ? "<kbd>" + y + "</kbd>" : " " + y + " "))
832                 .join("") + "</dt><dd>" + x[1] + "</dd>").join("");
833         const div_shortcuts = document.createElement("div");
834         addClass(div_shortcuts, "shortcuts");
835         div_shortcuts.innerHTML = "<h2>Keyboard Shortcuts</h2><dl>" + shortcuts + "</dl></div>";
836
837         const infos = [
838             "Prefix searches with a type followed by a colon (e.g., <code>fn:</code>) to \
839              restrict the search to a given item kind.",
840             "Accepted kinds are: <code>fn</code>, <code>mod</code>, <code>struct</code>, \
841              <code>enum</code>, <code>trait</code>, <code>type</code>, <code>macro</code>, \
842              and <code>const</code>.",
843             "Search functions by type signature (e.g., <code>vec -&gt; usize</code> or \
844              <code>-&gt; vec</code>)",
845             "Search multiple things at once by splitting your query with comma (e.g., \
846              <code>str,u8</code> or <code>String,struct:Vec,test</code>)",
847             "You can look for items with an exact name by putting double quotes around \
848              your request: <code>\"string\"</code>",
849             "Look for items inside another one by searching for a path: <code>vec::Vec</code>",
850         ].map(x => "<p>" + x + "</p>").join("");
851         const div_infos = document.createElement("div");
852         addClass(div_infos, "infos");
853         div_infos.innerHTML = "<h2>Search Tricks</h2>" + infos;
854
855         const rustdoc_version = document.createElement("span");
856         rustdoc_version.className = "bottom";
857         const rustdoc_version_code = document.createElement("code");
858         rustdoc_version_code.innerText = "rustdoc " + getVar("rustdoc-version");
859         rustdoc_version.appendChild(rustdoc_version_code);
860
861         const container = document.createElement("div");
862         container.className = "popover";
863         container.style.display = "none";
864
865         const side_by_side = document.createElement("div");
866         side_by_side.className = "side-by-side";
867         side_by_side.appendChild(div_shortcuts);
868         side_by_side.appendChild(div_infos);
869
870         container.appendChild(book_info);
871         container.appendChild(side_by_side);
872         container.appendChild(rustdoc_version);
873
874         const help_button = getHelpButton();
875         help_button.appendChild(container);
876
877         container.onblur = helpBlurHandler;
878         container.onclick = event => {
879             event.preventDefault();
880         };
881         help_button.onblur = helpBlurHandler;
882         help_button.children[0].onblur = helpBlurHandler;
883
884         return container;
885     }
886
887     /**
888      * Hide all the popover menus.
889      */
890     window.hidePopoverMenus = function() {
891         onEachLazy(document.querySelectorAll(".search-container .popover"), elem => {
892             elem.style.display = "none";
893         });
894     };
895
896     /**
897      * Returns the help menu element (not the button).
898      *
899      * @param {boolean} buildNeeded - If this argument is `false`, the help menu element won't be
900      *                                built if it doesn't exist.
901      *
902      * @return {HTMLElement}
903      */
904     function getHelpMenu(buildNeeded) {
905         let menu = getHelpButton().querySelector(".popover");
906         if (!menu && buildNeeded) {
907             menu = buildHelpMenu();
908         }
909         return menu;
910     }
911
912     /**
913      * Show the help popup menu.
914      */
915     function showHelp() {
916         const menu = getHelpMenu(true);
917         if (menu.style.display === "none") {
918             window.hidePopoverMenus();
919             menu.style.display = "";
920         }
921     }
922
923     document.querySelector(`#${HELP_BUTTON_ID} > button`).addEventListener("click", event => {
924         const target = event.target;
925         if (target.tagName !== "BUTTON" || target.parentElement.id !== HELP_BUTTON_ID) {
926             return;
927         }
928         const menu = getHelpMenu(true);
929         const shouldShowHelp = menu.style.display === "none";
930         if (shouldShowHelp) {
931             showHelp();
932         } else {
933             window.hidePopoverMenus();
934         }
935     });
936
937     setMobileTopbar();
938     addSidebarItems();
939     addSidebarCrates();
940     onHashChange(null);
941     window.addEventListener("hashchange", onHashChange);
942     searchState.setup();
943 }());
944
945 (function() {
946     let reset_button_timeout = null;
947
948     window.copy_path = but => {
949         const parent = but.parentElement;
950         const path = [];
951
952         onEach(parent.childNodes, child => {
953             if (child.tagName === "A") {
954                 path.push(child.textContent);
955             }
956         });
957
958         const el = document.createElement("textarea");
959         el.value = path.join("::");
960         el.setAttribute("readonly", "");
961         // To not make it appear on the screen.
962         el.style.position = "absolute";
963         el.style.left = "-9999px";
964
965         document.body.appendChild(el);
966         el.select();
967         document.execCommand("copy");
968         document.body.removeChild(el);
969
970         // There is always one children, but multiple childNodes.
971         but.children[0].style.display = "none";
972
973         let tmp;
974         if (but.childNodes.length < 2) {
975             tmp = document.createTextNode("✓");
976             but.appendChild(tmp);
977         } else {
978             onEachLazy(but.childNodes, e => {
979                 if (e.nodeType === Node.TEXT_NODE) {
980                     tmp = e;
981                     return true;
982                 }
983             });
984             tmp.textContent = "✓";
985         }
986
987         if (reset_button_timeout !== null) {
988             window.clearTimeout(reset_button_timeout);
989         }
990
991         function reset_button() {
992             tmp.textContent = "";
993             reset_button_timeout = null;
994             but.children[0].style.display = "";
995         }
996
997         reset_button_timeout = window.setTimeout(reset_button, 1000);
998     };
999 }());