1 // Local js definitions:
2 /* global addClass, getSettingValue, hasClass, searchState */
3 /* global onEach, onEachLazy, removeClass */
7 if (!String.prototype.startsWith) {
8 String.prototype.startsWith = function(searchString, position) {
9 position = position || 0;
10 return this.indexOf(searchString, position) === position;
13 if (!String.prototype.endsWith) {
14 String.prototype.endsWith = function(suffix, length) {
15 const l = length || this.length;
16 return this.indexOf(suffix, l - suffix.length) !== -1;
20 if (!DOMTokenList.prototype.add) {
21 DOMTokenList.prototype.add = function(className) {
22 if (className && !hasClass(this, className)) {
23 if (this.className && this.className.length > 0) {
24 this.className += " " + className;
26 this.className = className;
32 if (!DOMTokenList.prototype.remove) {
33 DOMTokenList.prototype.remove = function(className) {
34 if (className && this.className) {
35 this.className = (" " + this.className + " ").replace(" " + className + " ", " ")
41 // Get a value from the rustdoc-vars div, which is used to convey data from
42 // Rust to the JS. If there is no such element, return null.
43 function getVar(name) {
44 const el = document.getElementById("rustdoc-vars");
46 return el.attributes["data-" + name].value;
52 // Given a basename (e.g. "storage") and an extension (e.g. ".js"), return a URL
53 // for a resource under the root-path, with the resource-suffix.
54 function resourcePath(basename, extension) {
55 return getVar("root-path") + basename + getVar("resource-suffix") + extension;
59 addClass(document.getElementById(MAIN_ID), "hidden");
63 removeClass(document.getElementById(MAIN_ID), "hidden");
66 function elemIsInParent(elem, parent) {
67 while (elem && elem !== document.body) {
68 if (elem === parent) {
71 elem = elem.parentElement;
76 function blurHandler(event, parentElem, hideCallback) {
77 if (!elemIsInParent(document.activeElement, parentElem) &&
78 !elemIsInParent(event.relatedTarget, parentElem)
85 window.rootPath = getVar("root-path");
86 window.currentCrate = getVar("current-crate");
89 function setMobileTopbar() {
90 // FIXME: It would be nicer to generate this text content directly in HTML,
91 // but with the current code it's hard to get the right information in the right place.
92 const mobileLocationTitle = document.querySelector(".mobile-topbar h2.location");
93 const locationTitle = document.querySelector(".sidebar h2.location");
94 if (mobileLocationTitle && locationTitle) {
95 mobileLocationTitle.innerHTML = locationTitle.innerHTML;
99 // Gets the human-readable string for the virtual-key code of the
100 // given KeyboardEvent, ev.
102 // This function is meant as a polyfill for KeyboardEvent#key,
103 // since it is not supported in IE 11 or Chrome for Android. We also test for
104 // KeyboardEvent#keyCode because the handleShortcut handler is
105 // also registered for the keydown event, because Blink doesn't fire
106 // keypress on hitting the Escape key.
108 // So I guess you could say things are getting pretty interoperable.
109 function getVirtualKey(ev) {
110 if ("key" in ev && typeof ev.key !== "undefined") {
114 const c = ev.charCode || ev.keyCode;
118 return String.fromCharCode(c);
121 const MAIN_ID = "main-content";
122 const SETTINGS_BUTTON_ID = "settings-menu";
123 const ALTERNATIVE_DISPLAY_ID = "alternative-display";
124 const NOT_DISPLAYED_ID = "not-displayed";
125 const HELP_BUTTON_ID = "help-button";
127 function getSettingsButton() {
128 return document.getElementById(SETTINGS_BUTTON_ID);
131 function getHelpButton() {
132 return document.getElementById(HELP_BUTTON_ID);
135 // Returns the current URL without any query parameter or hash.
136 function getNakedUrl() {
137 return window.location.href.split("?")[0].split("#")[0];
141 * This function inserts `newNode` after `referenceNode`. It doesn't work if `referenceNode`
142 * doesn't have a parent node.
144 * @param {HTMLElement} newNode
145 * @param {HTMLElement} referenceNode
147 function insertAfter(newNode, referenceNode) {
148 referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
152 * This function creates a new `<section>` with the given `id` and `classes` if it doesn't already
155 * More information about this in `switchDisplayedElement` documentation.
158 * @param {string} classes
160 function getOrCreateSection(id, classes) {
161 let el = document.getElementById(id);
164 el = document.createElement("section");
166 el.className = classes;
167 insertAfter(el, document.getElementById(MAIN_ID));
173 * Returns the `<section>` element which contains the displayed element.
175 * @return {HTMLElement}
177 function getAlternativeDisplayElem() {
178 return getOrCreateSection(ALTERNATIVE_DISPLAY_ID, "content hidden");
182 * Returns the `<section>` element which contains the not-displayed elements.
184 * @return {HTMLElement}
186 function getNotDisplayedElem() {
187 return getOrCreateSection(NOT_DISPLAYED_ID, "hidden");
191 * To nicely switch between displayed "extra" elements (such as search results or settings menu)
192 * and to alternate between the displayed and not displayed elements, we hold them in two different
193 * `<section>` elements. They work in pair: one holds the hidden elements while the other
194 * contains the displayed element (there can be only one at the same time!). So basically, we switch
195 * elements between the two `<section>` elements.
197 * @param {HTMLElement} elemToDisplay
199 function switchDisplayedElement(elemToDisplay) {
200 const el = getAlternativeDisplayElem();
202 if (el.children.length > 0) {
203 getNotDisplayedElem().appendChild(el.firstElementChild);
205 if (elemToDisplay === null) {
206 addClass(el, "hidden");
210 el.appendChild(elemToDisplay);
212 removeClass(el, "hidden");
215 function browserSupportsHistoryApi() {
216 return window.history && typeof window.history.pushState === "function";
219 // eslint-disable-next-line no-unused-vars
220 function loadCss(cssFileName) {
221 const link = document.createElement("link");
222 link.href = resourcePath(cssFileName, ".css");
223 link.type = "text/css";
224 link.rel = "stylesheet";
225 document.getElementsByTagName("head")[0].appendChild(link);
229 function loadScript(url) {
230 const script = document.createElement("script");
232 document.head.append(script);
235 getSettingsButton().onclick = event => {
236 addClass(getSettingsButton(), "rotate");
237 event.preventDefault();
238 // Sending request for the CSS and the JS files at the same time so it will
239 // hopefully be loaded when the JS will generate the settings content.
241 loadScript(resourcePath("settings", ".js"));
244 window.searchState = {
245 loadingText: "Loading search results...",
246 input: document.getElementsByClassName("search-input")[0],
247 outputElement: () => {
248 let el = document.getElementById("search");
250 el = document.createElement("section");
252 getNotDisplayedElem().appendChild(el);
256 title: document.title,
257 titleBeforeSearch: document.title,
259 // On the search screen, so you remain on the last tab you opened.
262 // 1 for "In Parameters"
263 // 2 for "In Return Types"
265 // tab and back preserves the element that was focused.
266 focusedByTab: [null, null, null],
267 clearInputTimeout: () => {
268 if (searchState.timeout !== null) {
269 clearTimeout(searchState.timeout);
270 searchState.timeout = null;
273 isDisplayed: () => searchState.outputElement().parentElement.id === ALTERNATIVE_DISPLAY_ID,
274 // Sets the focus on the search bar at the top of the page
276 searchState.input.focus();
278 // Removes the focus from the search bar.
280 searchState.input.blur();
282 showResults: search => {
283 if (search === null || typeof search === "undefined") {
284 search = searchState.outputElement();
286 switchDisplayedElement(search);
287 searchState.mouseMovedAfterSearch = false;
288 document.title = searchState.title;
291 switchDisplayedElement(null);
292 document.title = searchState.titleBeforeSearch;
293 // We also remove the query parameter from the URL.
294 if (browserSupportsHistoryApi()) {
295 history.replaceState(null, window.currentCrate + " - Rust",
296 getNakedUrl() + window.location.hash);
299 getQueryStringParams: () => {
301 window.location.search.substring(1).split("&").
303 const pair = s.split("=");
304 params[decodeURIComponent(pair[0])] =
305 typeof pair[1] === "undefined" ? null : decodeURIComponent(pair[1]);
310 const search_input = searchState.input;
311 if (!searchState.input) {
314 let searchLoaded = false;
315 function loadSearch() {
318 loadScript(resourcePath("search", ".js"));
319 loadScript(resourcePath("search-index", ".js"));
323 search_input.addEventListener("focus", () => {
324 search_input.origPlaceholder = search_input.placeholder;
325 search_input.placeholder = "Type your search here.";
329 if (search_input.value !== "") {
333 const params = searchState.getQueryStringParams();
334 if (params.search !== undefined) {
335 const search = searchState.outputElement();
336 search.innerHTML = "<h3 class=\"search-loading\">" +
337 searchState.loadingText + "</h3>";
338 searchState.showResults(search);
344 function getPageId() {
345 if (window.location.hash) {
346 const tmp = window.location.hash.replace(/^#/, "");
347 if (tmp.length > 0) {
354 const toggleAllDocsId = "toggle-all-docs";
357 function handleHashes(ev) {
358 if (ev !== null && searchState.isDisplayed() && ev.newURL) {
359 // This block occurs when clicking on an element in the navbar while
361 switchDisplayedElement(null);
362 const hash = ev.newURL.slice(ev.newURL.indexOf("#") + 1);
363 if (browserSupportsHistoryApi()) {
364 // `window.location.search`` contains all the query parameters, not just `search`.
365 history.replaceState(null, "",
366 getNakedUrl() + window.location.search + "#" + hash);
368 const elem = document.getElementById(hash);
370 elem.scrollIntoView();
373 // This part is used in case an element is not visible.
374 if (savedHash !== window.location.hash) {
375 savedHash = window.location.hash;
376 if (savedHash.length === 0) {
379 expandSection(savedHash.slice(1)); // we remove the '#'
383 function onHashChange(ev) {
384 // If we're in mobile mode, we should hide the sidebar in any case.
385 const sidebar = document.getElementsByClassName("sidebar")[0];
386 removeClass(sidebar, "shown");
390 function openParentDetails(elem) {
392 if (elem.tagName === "DETAILS") {
395 elem = elem.parentNode;
399 function expandSection(id) {
400 openParentDetails(document.getElementById(id));
403 function handleEscape(ev) {
404 searchState.clearInputTimeout();
405 switchDisplayedElement(null);
406 if (browserSupportsHistoryApi()) {
407 history.replaceState(null, window.currentCrate + " - Rust",
408 getNakedUrl() + window.location.hash);
411 searchState.defocus();
412 window.hidePopoverMenus();
415 function handleShortcut(ev) {
416 // Don't interfere with browser shortcuts
417 const disableShortcuts = getSettingValue("disable-shortcuts") === "true";
418 if (ev.ctrlKey || ev.altKey || ev.metaKey || disableShortcuts) {
422 if (document.activeElement.tagName === "INPUT" &&
423 document.activeElement.type !== "checkbox") {
424 switch (getVirtualKey(ev)) {
430 switch (getVirtualKey(ev)) {
457 document.addEventListener("keypress", handleShortcut);
458 document.addEventListener("keydown", handleShortcut);
460 function addSidebarItems() {
461 if (!window.SIDEBAR_ITEMS) {
464 const sidebar = document.getElementsByClassName("sidebar-elems")[0];
467 * Append to the sidebar a "block" of links - a heading along with a list (`<ul>`) of items.
469 * @param {string} shortty - A short type name, like "primitive", "mod", or "macro"
470 * @param {string} id - The HTML id of the corresponding section on the module page.
471 * @param {string} longty - A long, capitalized, plural name, like "Primitive Types",
472 * "Modules", or "Macros".
474 function block(shortty, id, longty) {
475 const filtered = window.SIDEBAR_ITEMS[shortty];
480 const div = document.createElement("div");
481 div.className = "block " + shortty;
482 const h3 = document.createElement("h3");
483 h3.innerHTML = `<a href="index.html#${id}">${longty}</a>`;
485 const ul = document.createElement("ul");
487 for (const item of filtered) {
488 const name = item[0];
489 const desc = item[1]; // can be null
493 if (shortty === "mod") {
494 path = name + "/index.html";
496 path = shortty + "." + name + ".html";
498 const current_page = document.location.href.split("/").pop();
499 if (path === current_page) {
502 const link = document.createElement("a");
505 link.className = klass;
506 link.textContent = name;
507 const li = document.createElement("li");
508 li.appendChild(link);
512 sidebar.appendChild(div);
516 const isModule = hasClass(document.body, "mod");
518 block("primitive", "primitives", "Primitive Types");
519 block("mod", "modules", "Modules");
520 block("macro", "macros", "Macros");
521 block("struct", "structs", "Structs");
522 block("enum", "enums", "Enums");
523 block("union", "unions", "Unions");
524 block("constant", "constants", "Constants");
525 block("static", "static", "Statics");
526 block("trait", "traits", "Traits");
527 block("fn", "functions", "Functions");
528 block("type", "types", "Type Definitions");
529 block("foreigntype", "foreign-types", "Foreign Types");
530 block("keyword", "keywords", "Keywords");
531 block("traitalias", "trait-aliases", "Trait Aliases");
536 window.register_implementors = imp => {
537 const implementors = document.getElementById("implementors-list");
538 const synthetic_implementors = document.getElementById("synthetic-implementors-list");
539 const inlined_types = new Set();
541 if (synthetic_implementors) {
542 // This `inlined_types` variable is used to avoid having the same implementation
543 // showing up twice. For example "String" in the "Sync" doc page.
545 // By the way, this is only used by and useful for traits implemented automatically
546 // (like "Send" and "Sync").
547 onEachLazy(synthetic_implementors.getElementsByClassName("impl"), el => {
548 const aliases = el.getAttribute("data-aliases");
552 aliases.split(",").forEach(alias => {
553 inlined_types.add(alias);
558 let currentNbImpls = implementors.getElementsByClassName("impl").length;
559 const traitName = document.querySelector("h1.fqn > .in-band > .trait").textContent;
560 const baseIdName = "impl-" + traitName + "-";
561 const libs = Object.getOwnPropertyNames(imp);
562 // We don't want to include impls from this JS file, when the HTML already has them.
563 // The current crate should always be ignored. Other crates that should also be
564 // ignored are included in the attribute `data-ignore-extern-crates`.
565 const ignoreExternCrates = document
566 .querySelector("script[data-ignore-extern-crates]")
567 .getAttribute("data-ignore-extern-crates");
568 for (const lib of libs) {
569 if (lib === window.currentCrate || ignoreExternCrates.indexOf(lib) !== -1) {
572 const structs = imp[lib];
575 for (const struct of structs) {
576 const list = struct.synthetic ? synthetic_implementors : implementors;
578 if (struct.synthetic) {
579 for (const struct_type of struct.types) {
580 if (inlined_types.has(struct_type)) {
581 continue struct_loop;
583 inlined_types.add(struct_type);
587 const code = document.createElement("h3");
588 code.innerHTML = struct.text;
589 addClass(code, "code-header");
590 addClass(code, "in-band");
592 onEachLazy(code.getElementsByTagName("a"), elem => {
593 const href = elem.getAttribute("href");
595 if (href && href.indexOf("http") !== 0) {
596 elem.setAttribute("href", window.rootPath + href);
600 const currentId = baseIdName + currentNbImpls;
601 const anchor = document.createElement("a");
602 anchor.href = "#" + currentId;
603 addClass(anchor, "anchor");
605 const display = document.createElement("div");
606 display.id = currentId;
607 addClass(display, "impl");
608 display.appendChild(anchor);
609 display.appendChild(code);
610 list.appendChild(display);
615 if (window.pending_implementors) {
616 window.register_implementors(window.pending_implementors);
619 function addSidebarCrates() {
620 if (!window.ALL_CRATES) {
623 const sidebarElems = document.getElementsByClassName("sidebar-elems")[0];
627 // Draw a convenient sidebar of known crates if we have a listing
628 const div = document.createElement("div");
629 div.className = "block crate";
630 div.innerHTML = "<h3>Crates</h3>";
631 const ul = document.createElement("ul");
634 for (const crate of window.ALL_CRATES) {
636 if (window.rootPath !== "./" && crate === window.currentCrate) {
639 const link = document.createElement("a");
640 link.href = window.rootPath + crate + "/index.html";
641 link.className = klass;
642 link.textContent = crate;
644 const li = document.createElement("li");
645 li.appendChild(link);
648 sidebarElems.appendChild(div);
652 function labelForToggleButton(sectionIsCollapsed) {
653 if (sectionIsCollapsed) {
654 // button will expand the section
657 // button will collapse the section
658 // note that this text is also set in the HTML template in ../render/mod.rs
659 return "\u2212"; // "\u2212" is "−" minus sign
662 function toggleAllDocs() {
663 const innerToggle = document.getElementById(toggleAllDocsId);
667 let sectionIsCollapsed = false;
668 if (hasClass(innerToggle, "will-expand")) {
669 removeClass(innerToggle, "will-expand");
670 onEachLazy(document.getElementsByClassName("rustdoc-toggle"), e => {
671 if (!hasClass(e, "type-contents-toggle")) {
675 innerToggle.title = "collapse all docs";
677 addClass(innerToggle, "will-expand");
678 onEachLazy(document.getElementsByClassName("rustdoc-toggle"), e => {
679 if (e.parentNode.id !== "implementations-list" ||
680 (!hasClass(e, "implementors-toggle") &&
681 !hasClass(e, "type-contents-toggle"))
686 sectionIsCollapsed = true;
687 innerToggle.title = "expand all docs";
689 innerToggle.children[0].innerText = labelForToggleButton(sectionIsCollapsed);
693 const toggles = document.getElementById(toggleAllDocsId);
695 toggles.onclick = toggleAllDocs;
698 const hideMethodDocs = getSettingValue("auto-hide-method-docs") === "true";
699 const hideImplementations = getSettingValue("auto-hide-trait-implementations") === "true";
700 const hideLargeItemContents = getSettingValue("auto-hide-large-items") !== "false";
702 function setImplementorsTogglesOpen(id, open) {
703 const list = document.getElementById(id);
705 onEachLazy(list.getElementsByClassName("implementors-toggle"), e => {
711 if (hideImplementations) {
712 setImplementorsTogglesOpen("trait-implementations-list", false);
713 setImplementorsTogglesOpen("blanket-implementations-list", false);
716 onEachLazy(document.getElementsByClassName("rustdoc-toggle"), e => {
717 if (!hideLargeItemContents && hasClass(e, "type-contents-toggle")) {
720 if (hideMethodDocs && hasClass(e, "method-toggle")) {
726 const pageId = getPageId();
727 if (pageId !== null) {
728 expandSection(pageId);
733 // To avoid checking on "rustdoc-line-numbers" value on every loop...
734 let lineNumbersFunc = () => {};
735 if (getSettingValue("line-numbers") === "true") {
736 lineNumbersFunc = x => {
737 const count = x.textContent.split("\n").length;
739 for (let i = 0; i < count; ++i) {
742 const node = document.createElement("pre");
743 addClass(node, "line-number");
744 node.innerHTML = elems.join("\n");
745 x.parentNode.insertBefore(node, x);
748 onEachLazy(document.getElementsByClassName("rust-example-rendered"), e => {
749 if (hasClass(e, "compile_fail")) {
750 e.addEventListener("mouseover", function() {
751 this.parentElement.previousElementSibling.childNodes[0].style.color = "#f00";
753 e.addEventListener("mouseout", function() {
754 this.parentElement.previousElementSibling.childNodes[0].style.color = "";
756 } else if (hasClass(e, "ignore")) {
757 e.addEventListener("mouseover", function() {
758 this.parentElement.previousElementSibling.childNodes[0].style.color = "#ff9200";
760 e.addEventListener("mouseout", function() {
761 this.parentElement.previousElementSibling.childNodes[0].style.color = "";
768 function hideSidebar() {
769 const sidebar = document.getElementsByClassName("sidebar")[0];
770 removeClass(sidebar, "shown");
773 function handleClick(id, f) {
774 const elem = document.getElementById(id);
776 elem.addEventListener("click", f);
779 handleClick(MAIN_ID, () => {
783 onEachLazy(document.getElementsByTagName("a"), el => {
784 // For clicks on internal links (<A> tags with a hash property), we expand the section we're
785 // jumping to *before* jumping there. We can't do this in onHashChange, because it changes
786 // the height of the document so we wind up scrolled to the wrong place.
788 el.addEventListener("click", () => {
789 expandSection(el.hash.slice(1));
795 onEachLazy(document.querySelectorAll(".rustdoc-toggle > summary:not(.hideme)"), el => {
796 el.addEventListener("click", e => {
797 if (e.target.tagName !== "SUMMARY" && e.target.tagName !== "A") {
803 onEachLazy(document.getElementsByClassName("notable-traits"), e => {
804 e.onclick = function() {
805 this.getElementsByClassName("notable-traits-tooltiptext")[0]
806 .classList.toggle("force-tooltip");
810 const sidebar_menu_toggle = document.getElementsByClassName("sidebar-menu-toggle")[0];
811 if (sidebar_menu_toggle) {
812 sidebar_menu_toggle.addEventListener("click", () => {
813 const sidebar = document.getElementsByClassName("sidebar")[0];
814 if (!hasClass(sidebar, "shown")) {
815 addClass(sidebar, "shown");
817 removeClass(sidebar, "shown");
822 function helpBlurHandler(event) {
823 blurHandler(event, getHelpButton(), window.hidePopoverMenus);
826 function buildHelpMenu() {
827 const book_info = document.createElement("span");
828 book_info.className = "top";
829 book_info.innerHTML = "You can find more information in \
830 <a href=\"https://doc.rust-lang.org/rustdoc/\">the rustdoc book</a>.";
833 ["?", "Show this help dialog"],
834 ["S", "Focus the search field"],
835 ["↑", "Move up in search results"],
836 ["↓", "Move down in search results"],
837 ["← / →", "Switch result tab (when results focused)"],
838 ["⏎", "Go to active search result"],
839 ["+", "Expand all sections"],
840 ["-", "Collapse all sections"],
843 .map((y, index) => ((index & 1) === 0 ? "<kbd>" + y + "</kbd>" : " " + y + " "))
844 .join("") + "</dt><dd>" + x[1] + "</dd>").join("");
845 const div_shortcuts = document.createElement("div");
846 addClass(div_shortcuts, "shortcuts");
847 div_shortcuts.innerHTML = "<h2>Keyboard Shortcuts</h2><dl>" + shortcuts + "</dl></div>";
850 "Prefix searches with a type followed by a colon (e.g., <code>fn:</code>) to \
851 restrict the search to a given item kind.",
852 "Accepted kinds are: <code>fn</code>, <code>mod</code>, <code>struct</code>, \
853 <code>enum</code>, <code>trait</code>, <code>type</code>, <code>macro</code>, \
854 and <code>const</code>.",
855 "Search functions by type signature (e.g., <code>vec -> usize</code> or \
856 <code>* -> vec</code>)",
857 "Search multiple things at once by splitting your query with comma (e.g., \
858 <code>str,u8</code> or <code>String,struct:Vec,test</code>)",
859 "You can look for items with an exact name by putting double quotes around \
860 your request: <code>\"string\"</code>",
861 "Look for items inside another one by searching for a path: <code>vec::Vec</code>",
862 ].map(x => "<p>" + x + "</p>").join("");
863 const div_infos = document.createElement("div");
864 addClass(div_infos, "infos");
865 div_infos.innerHTML = "<h2>Search Tricks</h2>" + infos;
867 const rustdoc_version = document.createElement("span");
868 rustdoc_version.className = "bottom";
869 const rustdoc_version_code = document.createElement("code");
870 rustdoc_version_code.innerText = "rustdoc " + getVar("rustdoc-version");
871 rustdoc_version.appendChild(rustdoc_version_code);
873 const container = document.createElement("div");
874 container.className = "popover";
875 container.style.display = "none";
877 const side_by_side = document.createElement("div");
878 side_by_side.className = "side-by-side";
879 side_by_side.appendChild(div_shortcuts);
880 side_by_side.appendChild(div_infos);
882 container.appendChild(book_info);
883 container.appendChild(side_by_side);
884 container.appendChild(rustdoc_version);
886 const help_button = getHelpButton();
887 help_button.appendChild(container);
889 container.onblur = helpBlurHandler;
890 container.onclick = event => {
891 event.preventDefault();
893 help_button.onblur = helpBlurHandler;
894 help_button.children[0].onblur = helpBlurHandler;
900 * Hide all the popover menus.
902 window.hidePopoverMenus = function() {
903 onEachLazy(document.querySelectorAll(".search-container .popover"), elem => {
904 elem.style.display = "none";
909 * Returns the help menu element (not the button).
911 * @param {boolean} buildNeeded - If this argument is `false`, the help menu element won't be
912 * built if it doesn't exist.
914 * @return {HTMLElement}
916 function getHelpMenu(buildNeeded) {
917 let menu = getHelpButton().querySelector(".popover");
918 if (!menu && buildNeeded) {
919 menu = buildHelpMenu();
925 * Show the help popup menu.
927 function showHelp() {
928 const menu = getHelpMenu(true);
929 if (menu.style.display === "none") {
930 window.hidePopoverMenus();
931 menu.style.display = "";
935 document.querySelector(`#${HELP_BUTTON_ID} > button`).addEventListener("click", event => {
936 const target = event.target;
937 if (target.tagName !== "BUTTON" || target.parentElement.id !== HELP_BUTTON_ID) {
940 const menu = getHelpMenu(true);
941 const shouldShowHelp = menu.style.display === "none";
942 if (shouldShowHelp) {
945 window.hidePopoverMenus();
953 window.addEventListener("hashchange", onHashChange);
958 let reset_button_timeout = null;
960 window.copy_path = but => {
961 const parent = but.parentElement;
964 onEach(parent.childNodes, child => {
965 if (child.tagName === "A") {
966 path.push(child.textContent);
970 const el = document.createElement("textarea");
971 el.value = path.join("::");
972 el.setAttribute("readonly", "");
973 // To not make it appear on the screen.
974 el.style.position = "absolute";
975 el.style.left = "-9999px";
977 document.body.appendChild(el);
979 document.execCommand("copy");
980 document.body.removeChild(el);
982 // There is always one children, but multiple childNodes.
983 but.children[0].style.display = "none";
986 if (but.childNodes.length < 2) {
987 tmp = document.createTextNode("✓");
988 but.appendChild(tmp);
990 onEachLazy(but.childNodes, e => {
991 if (e.nodeType === Node.TEXT_NODE) {
996 tmp.textContent = "✓";
999 if (reset_button_timeout !== null) {
1000 window.clearTimeout(reset_button_timeout);
1003 function reset_button() {
1004 tmp.textContent = "";
1005 reset_button_timeout = null;
1006 but.children[0].style.display = "";
1009 reset_button_timeout = window.setTimeout(reset_button, 1000);