1 // Local js definitions:
2 /* global addClass, getSettingValue, hasClass, searchState */
3 /* global onEach, onEachLazy, removeClass */
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");
12 return el.attributes["data-" + name].value;
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;
25 addClass(document.getElementById(MAIN_ID), "hidden");
29 removeClass(document.getElementById(MAIN_ID), "hidden");
32 function elemIsInParent(elem, parent) {
33 while (elem && elem !== document.body) {
34 if (elem === parent) {
37 elem = elem.parentElement;
42 function blurHandler(event, parentElem, hideCallback) {
43 if (!elemIsInParent(document.activeElement, parentElem) &&
44 !elemIsInParent(event.relatedTarget, parentElem)
51 window.rootPath = getVar("root-path");
52 window.currentCrate = getVar("current-crate");
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;
65 // Gets the human-readable string for the virtual-key code of the
66 // given KeyboardEvent, ev.
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.
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") {
80 const c = ev.charCode || ev.keyCode;
84 return String.fromCharCode(c);
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";
93 function getSettingsButton() {
94 return document.getElementById(SETTINGS_BUTTON_ID);
97 function getHelpButton() {
98 return document.getElementById(HELP_BUTTON_ID);
101 // Returns the current URL without any query parameter or hash.
102 function getNakedUrl() {
103 return window.location.href.split("?")[0].split("#")[0];
107 * This function inserts `newNode` after `referenceNode`. It doesn't work if `referenceNode`
108 * doesn't have a parent node.
110 * @param {HTMLElement} newNode
111 * @param {HTMLElement} referenceNode
113 function insertAfter(newNode, referenceNode) {
114 referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
118 * This function creates a new `<section>` with the given `id` and `classes` if it doesn't already
121 * More information about this in `switchDisplayedElement` documentation.
124 * @param {string} classes
126 function getOrCreateSection(id, classes) {
127 let el = document.getElementById(id);
130 el = document.createElement("section");
132 el.className = classes;
133 insertAfter(el, document.getElementById(MAIN_ID));
139 * Returns the `<section>` element which contains the displayed element.
141 * @return {HTMLElement}
143 function getAlternativeDisplayElem() {
144 return getOrCreateSection(ALTERNATIVE_DISPLAY_ID, "content hidden");
148 * Returns the `<section>` element which contains the not-displayed elements.
150 * @return {HTMLElement}
152 function getNotDisplayedElem() {
153 return getOrCreateSection(NOT_DISPLAYED_ID, "hidden");
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.
163 * @param {HTMLElement} elemToDisplay
165 function switchDisplayedElement(elemToDisplay) {
166 const el = getAlternativeDisplayElem();
168 if (el.children.length > 0) {
169 getNotDisplayedElem().appendChild(el.firstElementChild);
171 if (elemToDisplay === null) {
172 addClass(el, "hidden");
176 el.appendChild(elemToDisplay);
178 removeClass(el, "hidden");
181 function browserSupportsHistoryApi() {
182 return window.history && typeof window.history.pushState === "function";
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);
195 function loadScript(url) {
196 const script = document.createElement("script");
198 document.head.append(script);
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.
207 loadScript(resourcePath("settings", ".js"));
210 window.searchState = {
211 loadingText: "Loading search results...",
212 input: document.getElementsByClassName("search-input")[0],
213 outputElement: () => {
214 let el = document.getElementById("search");
216 el = document.createElement("section");
218 getNotDisplayedElem().appendChild(el);
222 title: document.title,
223 titleBeforeSearch: document.title,
225 // On the search screen, so you remain on the last tab you opened.
228 // 1 for "In Parameters"
229 // 2 for "In Return Types"
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;
239 isDisplayed: () => searchState.outputElement().parentElement.id === ALTERNATIVE_DISPLAY_ID,
240 // Sets the focus on the search bar at the top of the page
242 searchState.input.focus();
244 // Removes the focus from the search bar.
246 searchState.input.blur();
248 showResults: search => {
249 if (search === null || typeof search === "undefined") {
250 search = searchState.outputElement();
252 switchDisplayedElement(search);
253 searchState.mouseMovedAfterSearch = false;
254 document.title = searchState.title;
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);
265 getQueryStringParams: () => {
267 window.location.search.substring(1).split("&").
269 const pair = s.split("=");
270 params[decodeURIComponent(pair[0])] =
271 typeof pair[1] === "undefined" ? null : decodeURIComponent(pair[1]);
276 const search_input = searchState.input;
277 if (!searchState.input) {
280 let searchLoaded = false;
281 function loadSearch() {
284 loadScript(resourcePath("search", ".js"));
285 loadScript(resourcePath("search-index", ".js"));
289 search_input.addEventListener("focus", () => {
290 search_input.origPlaceholder = search_input.placeholder;
291 search_input.placeholder = "Type your search here.";
295 if (search_input.value !== "") {
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);
310 function getPageId() {
311 if (window.location.hash) {
312 const tmp = window.location.hash.replace(/^#/, "");
313 if (tmp.length > 0) {
320 const toggleAllDocsId = "toggle-all-docs";
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
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);
334 const elem = document.getElementById(hash);
336 elem.scrollIntoView();
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) {
345 expandSection(savedHash.slice(1)); // we remove the '#'
349 function onHashChange(ev) {
350 // If we're in mobile mode, we should hide the sidebar in any case.
355 function openParentDetails(elem) {
357 if (elem.tagName === "DETAILS") {
360 elem = elem.parentNode;
364 function expandSection(id) {
365 openParentDetails(document.getElementById(id));
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);
376 searchState.defocus();
377 window.hidePopoverMenus();
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) {
387 if (document.activeElement.tagName === "INPUT" &&
388 document.activeElement.type !== "checkbox") {
389 switch (getVirtualKey(ev)) {
395 switch (getVirtualKey(ev)) {
422 document.addEventListener("keypress", handleShortcut);
423 document.addEventListener("keydown", handleShortcut);
425 function addSidebarItems() {
426 if (!window.SIDEBAR_ITEMS) {
429 const sidebar = document.getElementsByClassName("sidebar-elems")[0];
432 * Append to the sidebar a "block" of links - a heading along with a list (`<ul>`) of items.
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".
439 function block(shortty, id, longty) {
440 const filtered = window.SIDEBAR_ITEMS[shortty];
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>`;
450 const ul = document.createElement("ul");
452 for (const item of filtered) {
453 const name = item[0];
454 const desc = item[1]; // can be null
458 if (shortty === "mod") {
459 path = name + "/index.html";
461 path = shortty + "." + name + ".html";
463 const current_page = document.location.href.split("/").pop();
464 if (path === current_page) {
467 const link = document.createElement("a");
470 link.className = klass;
471 link.textContent = name;
472 const li = document.createElement("li");
473 li.appendChild(link);
477 sidebar.appendChild(div);
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");
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();
504 const SYNTHETIC_IDX = 1;
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.
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");
518 aliases.split(",").forEach(alias => {
519 inlined_types.add(alias);
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) {
538 const structs = imp[lib];
541 for (const struct of structs) {
542 const list = struct[SYNTHETIC_IDX] ? synthetic_implementors : implementors;
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;
551 inlined_types.add(struct_type);
555 const code = document.createElement("h3");
556 code.innerHTML = struct[TEXT_IDX];
557 addClass(code, "code-header");
558 addClass(code, "in-band");
560 onEachLazy(code.getElementsByTagName("a"), elem => {
561 const href = elem.getAttribute("href");
563 if (href && href.indexOf("http") !== 0) {
564 elem.setAttribute("href", window.rootPath + href);
568 const currentId = baseIdName + currentNbImpls;
569 const anchor = document.createElement("a");
570 anchor.href = "#" + currentId;
571 addClass(anchor, "anchor");
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);
583 if (window.pending_implementors) {
584 window.register_implementors(window.pending_implementors);
587 function addSidebarCrates() {
588 if (!window.ALL_CRATES) {
591 const sidebarElems = document.getElementsByClassName("sidebar-elems")[0];
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");
602 for (const crate of window.ALL_CRATES) {
604 if (window.rootPath !== "./" && crate === window.currentCrate) {
607 const link = document.createElement("a");
608 link.href = window.rootPath + crate + "/index.html";
609 link.className = klass;
610 link.textContent = crate;
612 const li = document.createElement("li");
613 li.appendChild(link);
616 sidebarElems.appendChild(div);
620 function labelForToggleButton(sectionIsCollapsed) {
621 if (sectionIsCollapsed) {
622 // button will expand the section
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
630 function toggleAllDocs() {
631 const innerToggle = document.getElementById(toggleAllDocsId);
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")) {
643 innerToggle.title = "collapse all docs";
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"))
654 sectionIsCollapsed = true;
655 innerToggle.title = "expand all docs";
657 innerToggle.children[0].innerText = labelForToggleButton(sectionIsCollapsed);
661 const toggles = document.getElementById(toggleAllDocsId);
663 toggles.onclick = toggleAllDocs;
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";
670 function setImplementorsTogglesOpen(id, open) {
671 const list = document.getElementById(id);
673 onEachLazy(list.getElementsByClassName("implementors-toggle"), e => {
679 if (hideImplementations) {
680 setImplementorsTogglesOpen("trait-implementations-list", false);
681 setImplementorsTogglesOpen("blanket-implementations-list", false);
684 onEachLazy(document.getElementsByClassName("rustdoc-toggle"), e => {
685 if (!hideLargeItemContents && hasClass(e, "type-contents-toggle")) {
688 if (hideMethodDocs && hasClass(e, "method-toggle")) {
694 const pageId = getPageId();
695 if (pageId !== null) {
696 expandSection(pageId);
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;
706 for (let i = 0; i < count; ++i) {
709 const node = document.createElement("pre");
710 addClass(node, "line-number");
711 node.innerHTML = elems.join("\n");
712 x.parentNode.insertBefore(node, x);
717 let oldSidebarScrollPosition = null;
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";
729 oldSidebarScrollPosition = null;
731 const sidebar = document.getElementsByClassName("sidebar")[0];
732 addClass(sidebar, "shown");
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;
748 const sidebar = document.getElementsByClassName("sidebar")[0];
749 removeClass(sidebar, "shown");
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.
761 function handleClick(id, f) {
762 const elem = document.getElementById(id);
764 elem.addEventListener("click", f);
767 handleClick(MAIN_ID, () => {
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.
776 el.addEventListener("click", () => {
777 expandSection(el.hash.slice(1));
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") {
791 onEachLazy(document.getElementsByClassName("notable-traits"), e => {
792 e.onclick = function() {
793 this.getElementsByClassName("notable-traits-tooltiptext")[0]
794 .classList.toggle("force-tooltip");
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")) {
810 function helpBlurHandler(event) {
811 blurHandler(event, getHelpButton(), window.hidePopoverMenus);
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>.";
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 ["⏎", "Go to active search result"],
827 ["+", "Expand all sections"],
828 ["-", "Collapse all sections"],
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>";
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 -> usize</code> or \
844 <code>-> 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;
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);
861 const container = document.createElement("div");
862 container.className = "popover";
863 container.style.display = "none";
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);
870 container.appendChild(book_info);
871 container.appendChild(side_by_side);
872 container.appendChild(rustdoc_version);
874 const help_button = getHelpButton();
875 help_button.appendChild(container);
877 container.onblur = helpBlurHandler;
878 container.onclick = event => {
879 event.preventDefault();
881 help_button.onblur = helpBlurHandler;
882 help_button.children[0].onblur = helpBlurHandler;
888 * Hide all the popover menus.
890 window.hidePopoverMenus = function() {
891 onEachLazy(document.querySelectorAll(".search-container .popover"), elem => {
892 elem.style.display = "none";
897 * Returns the help menu element (not the button).
899 * @param {boolean} buildNeeded - If this argument is `false`, the help menu element won't be
900 * built if it doesn't exist.
902 * @return {HTMLElement}
904 function getHelpMenu(buildNeeded) {
905 let menu = getHelpButton().querySelector(".popover");
906 if (!menu && buildNeeded) {
907 menu = buildHelpMenu();
913 * Show the help popup menu.
915 function showHelp() {
916 const menu = getHelpMenu(true);
917 if (menu.style.display === "none") {
918 window.hidePopoverMenus();
919 menu.style.display = "";
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) {
928 const menu = getHelpMenu(true);
929 const shouldShowHelp = menu.style.display === "none";
930 if (shouldShowHelp) {
933 window.hidePopoverMenus();
941 window.addEventListener("hashchange", onHashChange);
946 let reset_button_timeout = null;
948 window.copy_path = but => {
949 const parent = but.parentElement;
952 onEach(parent.childNodes, child => {
953 if (child.tagName === "A") {
954 path.push(child.textContent);
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";
965 document.body.appendChild(el);
967 document.execCommand("copy");
968 document.body.removeChild(el);
970 // There is always one children, but multiple childNodes.
971 but.children[0].style.display = "none";
974 if (but.childNodes.length < 2) {
975 tmp = document.createTextNode("✓");
976 but.appendChild(tmp);
978 onEachLazy(but.childNodes, e => {
979 if (e.nodeType === Node.TEXT_NODE) {
984 tmp.textContent = "✓";
987 if (reset_button_timeout !== null) {
988 window.clearTimeout(reset_button_timeout);
991 function reset_button() {
992 tmp.textContent = "";
993 reset_button_timeout = null;
994 but.children[0].style.display = "";
997 reset_button_timeout = window.setTimeout(reset_button, 1000);