1// Local js definitions: 2/* global addClass, getSettingValue, hasClass, searchState */ 3/* global onEach, onEachLazy, removeClass, getVar */ 4 5"use strict"; 6 7// The amount of time that the cursor must remain still over a hover target before 8// revealing a tooltip. 9// 10// https://www.nngroup.com/articles/timing-exposing-content/ 11window.RUSTDOC_TOOLTIP_HOVER_MS = 300; 12window.RUSTDOC_TOOLTIP_HOVER_EXIT_MS = 450; 13 14// Given a basename (e.g. "storage") and an extension (e.g. ".js"), return a URL 15// for a resource under the root-path, with the resource-suffix. 16function resourcePath(basename, extension) { 17 return getVar("root-path") + basename + getVar("resource-suffix") + extension; 18} 19 20function hideMain() { 21 addClass(document.getElementById(MAIN_ID), "hidden"); 22} 23 24function showMain() { 25 removeClass(document.getElementById(MAIN_ID), "hidden"); 26} 27 28function elemIsInParent(elem, parent) { 29 while (elem && elem !== document.body) { 30 if (elem === parent) { 31 return true; 32 } 33 elem = elem.parentElement; 34 } 35 return false; 36} 37 38function blurHandler(event, parentElem, hideCallback) { 39 if (!elemIsInParent(document.activeElement, parentElem) && 40 !elemIsInParent(event.relatedTarget, parentElem) 41 ) { 42 hideCallback(); 43 } 44} 45 46window.rootPath = getVar("root-path"); 47window.currentCrate = getVar("current-crate"); 48 49function setMobileTopbar() { 50 // FIXME: It would be nicer to generate this text content directly in HTML, 51 // but with the current code it's hard to get the right information in the right place. 52 const mobileLocationTitle = document.querySelector(".mobile-topbar h2"); 53 const locationTitle = document.querySelector(".sidebar h2.location"); 54 if (mobileLocationTitle && locationTitle) { 55 mobileLocationTitle.innerHTML = locationTitle.innerHTML; 56 } 57} 58 59// Gets the human-readable string for the virtual-key code of the 60// given KeyboardEvent, ev. 61// 62// This function is meant as a polyfill for KeyboardEvent#key, 63// since it is not supported in IE 11 or Chrome for Android. We also test for 64// KeyboardEvent#keyCode because the handleShortcut handler is 65// also registered for the keydown event, because Blink doesn't fire 66// keypress on hitting the Escape key. 67// 68// So I guess you could say things are getting pretty interoperable. 69function getVirtualKey(ev) { 70 if ("key" in ev && typeof ev.key !== "undefined") { 71 return ev.key; 72 } 73 74 const c = ev.charCode || ev.keyCode; 75 if (c === 27) { 76 return "Escape"; 77 } 78 return String.fromCharCode(c); 79} 80 81const MAIN_ID = "main-content"; 82const SETTINGS_BUTTON_ID = "settings-menu"; 83const ALTERNATIVE_DISPLAY_ID = "alternative-display"; 84const NOT_DISPLAYED_ID = "not-displayed"; 85const HELP_BUTTON_ID = "help-button"; 86 87function getSettingsButton() { 88 return document.getElementById(SETTINGS_BUTTON_ID); 89} 90 91function getHelpButton() { 92 return document.getElementById(HELP_BUTTON_ID); 93} 94 95// Returns the current URL without any query parameter or hash. 96function getNakedUrl() { 97 return window.location.href.split("?")[0].split("#")[0]; 98} 99 100/** 101 * This function inserts `newNode` after `referenceNode`. It doesn't work if `referenceNode` 102 * doesn't have a parent node. 103 * 104 * @param {HTMLElement} newNode 105 * @param {HTMLElement} referenceNode 106 */ 107function insertAfter(newNode, referenceNode) { 108 referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); 109} 110 111/** 112 * This function creates a new `<section>` with the given `id` and `classes` if it doesn't already 113 * exist. 114 * 115 * More information about this in `switchDisplayedElement` documentation. 116 * 117 * @param {string} id 118 * @param {string} classes 119 */ 120function getOrCreateSection(id, classes) { 121 let el = document.getElementById(id); 122 123 if (!el) { 124 el = document.createElement("section"); 125 el.id = id; 126 el.className = classes; 127 insertAfter(el, document.getElementById(MAIN_ID)); 128 } 129 return el; 130} 131 132/** 133 * Returns the `<section>` element which contains the displayed element. 134 * 135 * @return {HTMLElement} 136 */ 137function getAlternativeDisplayElem() { 138 return getOrCreateSection(ALTERNATIVE_DISPLAY_ID, "content hidden"); 139} 140 141/** 142 * Returns the `<section>` element which contains the not-displayed elements. 143 * 144 * @return {HTMLElement} 145 */ 146function getNotDisplayedElem() { 147 return getOrCreateSection(NOT_DISPLAYED_ID, "hidden"); 148} 149 150/** 151 * To nicely switch between displayed "extra" elements (such as search results or settings menu) 152 * and to alternate between the displayed and not displayed elements, we hold them in two different 153 * `<section>` elements. They work in pair: one holds the hidden elements while the other 154 * contains the displayed element (there can be only one at the same time!). So basically, we switch 155 * elements between the two `<section>` elements. 156 * 157 * @param {HTMLElement} elemToDisplay 158 */ 159function switchDisplayedElement(elemToDisplay) { 160 const el = getAlternativeDisplayElem(); 161 162 if (el.children.length > 0) { 163 getNotDisplayedElem().appendChild(el.firstElementChild); 164 } 165 if (elemToDisplay === null) { 166 addClass(el, "hidden"); 167 showMain(); 168 return; 169 } 170 el.appendChild(elemToDisplay); 171 hideMain(); 172 removeClass(el, "hidden"); 173} 174 175function browserSupportsHistoryApi() { 176 return window.history && typeof window.history.pushState === "function"; 177} 178 179function loadCss(cssUrl) { 180 const link = document.createElement("link"); 181 link.href = cssUrl; 182 link.rel = "stylesheet"; 183 document.getElementsByTagName("head")[0].appendChild(link); 184} 185 186function preLoadCss(cssUrl) { 187 // https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload 188 const link = document.createElement("link"); 189 link.href = cssUrl; 190 link.rel = "preload"; 191 link.as = "style"; 192 document.getElementsByTagName("head")[0].appendChild(link); 193} 194 195(function() { 196 const isHelpPage = window.location.pathname.endsWith("/help.html"); 197 198 function loadScript(url) { 199 const script = document.createElement("script"); 200 script.src = url; 201 document.head.append(script); 202 } 203 204 getSettingsButton().onclick = event => { 205 if (event.ctrlKey || event.altKey || event.metaKey) { 206 return; 207 } 208 window.hideAllModals(false); 209 addClass(getSettingsButton(), "rotate"); 210 event.preventDefault(); 211 // Sending request for the CSS and the JS files at the same time so it will 212 // hopefully be loaded when the JS will generate the settings content. 213 loadCss(getVar("static-root-path") + getVar("settings-css")); 214 loadScript(getVar("static-root-path") + getVar("settings-js")); 215 preLoadCss(getVar("static-root-path") + getVar("theme-light-css")); 216 preLoadCss(getVar("static-root-path") + getVar("theme-dark-css")); 217 preLoadCss(getVar("static-root-path") + getVar("theme-ayu-css")); 218 // Pre-load all theme CSS files, so that switching feels seamless. 219 // 220 // When loading settings.html as a standalone page, the equivalent HTML is 221 // generated in context.rs. 222 setTimeout(() => { 223 const themes = getVar("themes").split(","); 224 for (const theme of themes) { 225 // if there are no themes, do nothing 226 // "".split(",") == [""] 227 if (theme !== "") { 228 preLoadCss(getVar("root-path") + theme + ".css"); 229 } 230 } 231 }, 0); 232 }; 233 234 window.searchState = { 235 loadingText: "Loading search results...", 236 input: document.getElementsByClassName("search-input")[0], 237 outputElement: () => { 238 let el = document.getElementById("search"); 239 if (!el) { 240 el = document.createElement("section"); 241 el.id = "search"; 242 getNotDisplayedElem().appendChild(el); 243 } 244 return el; 245 }, 246 title: document.title, 247 titleBeforeSearch: document.title, 248 timeout: null, 249 // On the search screen, so you remain on the last tab you opened. 250 // 251 // 0 for "In Names" 252 // 1 for "In Parameters" 253 // 2 for "In Return Types" 254 currentTab: 0, 255 // tab and back preserves the element that was focused. 256 focusedByTab: [null, null, null], 257 clearInputTimeout: () => { 258 if (searchState.timeout !== null) { 259 clearTimeout(searchState.timeout); 260 searchState.timeout = null; 261 } 262 }, 263 isDisplayed: () => searchState.outputElement().parentElement.id === ALTERNATIVE_DISPLAY_ID, 264 // Sets the focus on the search bar at the top of the page 265 focus: () => { 266 searchState.input.focus(); 267 }, 268 // Removes the focus from the search bar. 269 defocus: () => { 270 searchState.input.blur(); 271 }, 272 showResults: search => { 273 if (search === null || typeof search === "undefined") { 274 search = searchState.outputElement(); 275 } 276 switchDisplayedElement(search); 277 searchState.mouseMovedAfterSearch = false; 278 document.title = searchState.title; 279 }, 280 removeQueryParameters: () => { 281 // We change the document title. 282 document.title = searchState.titleBeforeSearch; 283 if (browserSupportsHistoryApi()) { 284 history.replaceState(null, "", getNakedUrl() + window.location.hash); 285 } 286 }, 287 hideResults: () => { 288 switchDisplayedElement(null); 289 // We also remove the query parameter from the URL. 290 searchState.removeQueryParameters(); 291 }, 292 getQueryStringParams: () => { 293 const params = {}; 294 window.location.search.substring(1).split("&"). 295 map(s => { 296 const pair = s.split("="); 297 params[decodeURIComponent(pair[0])] = 298 typeof pair[1] === "undefined" ? null : decodeURIComponent(pair[1]); 299 }); 300 return params; 301 }, 302 setup: () => { 303 const search_input = searchState.input; 304 if (!searchState.input) { 305 return; 306 } 307 let searchLoaded = false; 308 function loadSearch() { 309 if (!searchLoaded) { 310 searchLoaded = true; 311 loadScript(getVar("static-root-path") + getVar("search-js")); 312 loadScript(resourcePath("search-index", ".js")); 313 } 314 } 315 316 search_input.addEventListener("focus", () => { 317 search_input.origPlaceholder = search_input.placeholder; 318 search_input.placeholder = "Type your search here."; 319 loadSearch(); 320 }); 321 322 if (search_input.value !== "") { 323 loadSearch(); 324 } 325 326 const params = searchState.getQueryStringParams(); 327 if (params.search !== undefined) { 328 searchState.setLoadingSearch(); 329 loadSearch(); 330 } 331 }, 332 setLoadingSearch: () => { 333 const search = searchState.outputElement(); 334 search.innerHTML = "<h3 class=\"search-loading\">" + searchState.loadingText + "</h3>"; 335 searchState.showResults(search); 336 }, 337 }; 338 339 const toggleAllDocsId = "toggle-all-docs"; 340 let savedHash = ""; 341 342 function handleHashes(ev) { 343 if (ev !== null && searchState.isDisplayed() && ev.newURL) { 344 // This block occurs when clicking on an element in the navbar while 345 // in a search. 346 switchDisplayedElement(null); 347 const hash = ev.newURL.slice(ev.newURL.indexOf("#") + 1); 348 if (browserSupportsHistoryApi()) { 349 // `window.location.search`` contains all the query parameters, not just `search`. 350 history.replaceState(null, "", 351 getNakedUrl() + window.location.search + "#" + hash); 352 } 353 const elem = document.getElementById(hash); 354 if (elem) { 355 elem.scrollIntoView(); 356 } 357 } 358 // This part is used in case an element is not visible. 359 const pageId = window.location.hash.replace(/^#/, ""); 360 if (savedHash !== pageId) { 361 savedHash = pageId; 362 if (pageId !== "") { 363 expandSection(pageId); 364 } 365 } 366 } 367 368 function onHashChange(ev) { 369 // If we're in mobile mode, we should hide the sidebar in any case. 370 hideSidebar(); 371 handleHashes(ev); 372 } 373 374 function openParentDetails(elem) { 375 while (elem) { 376 if (elem.tagName === "DETAILS") { 377 elem.open = true; 378 } 379 elem = elem.parentNode; 380 } 381 } 382 383 function expandSection(id) { 384 openParentDetails(document.getElementById(id)); 385 } 386 387 function handleEscape(ev) { 388 searchState.clearInputTimeout(); 389 searchState.hideResults(); 390 ev.preventDefault(); 391 searchState.defocus(); 392 window.hideAllModals(true); // true = reset focus for tooltips 393 } 394 395 function handleShortcut(ev) { 396 // Don't interfere with browser shortcuts 397 const disableShortcuts = getSettingValue("disable-shortcuts") === "true"; 398 if (ev.ctrlKey || ev.altKey || ev.metaKey || disableShortcuts) { 399 return; 400 } 401 402 if (document.activeElement.tagName === "INPUT" && 403 document.activeElement.type !== "checkbox" && 404 document.activeElement.type !== "radio") { 405 switch (getVirtualKey(ev)) { 406 case "Escape": 407 handleEscape(ev); 408 break; 409 } 410 } else { 411 switch (getVirtualKey(ev)) { 412 case "Escape": 413 handleEscape(ev); 414 break; 415 416 case "s": 417 case "S": 418 ev.preventDefault(); 419 searchState.focus(); 420 break; 421 422 case "+": 423 ev.preventDefault(); 424 expandAllDocs(); 425 break; 426 case "-": 427 ev.preventDefault(); 428 collapseAllDocs(); 429 break; 430 431 case "?": 432 showHelp(); 433 break; 434 435 default: 436 break; 437 } 438 } 439 } 440 441 document.addEventListener("keypress", handleShortcut); 442 document.addEventListener("keydown", handleShortcut); 443 444 function addSidebarItems() { 445 if (!window.SIDEBAR_ITEMS) { 446 return; 447 } 448 const sidebar = document.getElementsByClassName("sidebar-elems")[0]; 449 450 /** 451 * Append to the sidebar a "block" of links - a heading along with a list (`<ul>`) of items. 452 * 453 * @param {string} shortty - A short type name, like "primitive", "mod", or "macro" 454 * @param {string} id - The HTML id of the corresponding section on the module page. 455 * @param {string} longty - A long, capitalized, plural name, like "Primitive Types", 456 * "Modules", or "Macros". 457 */ 458 function block(shortty, id, longty) { 459 const filtered = window.SIDEBAR_ITEMS[shortty]; 460 if (!filtered) { 461 return; 462 } 463 464 const h3 = document.createElement("h3"); 465 h3.innerHTML = `<a href="index.html#${id}">${longty}</a>`; 466 const ul = document.createElement("ul"); 467 ul.className = "block " + shortty; 468 469 for (const name of filtered) { 470 let path; 471 if (shortty === "mod") { 472 path = name + "/index.html"; 473 } else { 474 path = shortty + "." + name + ".html"; 475 } 476 const current_page = document.location.href.split("/").pop(); 477 const link = document.createElement("a"); 478 link.href = path; 479 if (path === current_page) { 480 link.className = "current"; 481 } 482 link.textContent = name; 483 const li = document.createElement("li"); 484 li.appendChild(link); 485 ul.appendChild(li); 486 } 487 sidebar.appendChild(h3); 488 sidebar.appendChild(ul); 489 } 490 491 if (sidebar) { 492 block("primitive", "primitives", "Primitive Types"); 493 block("mod", "modules", "Modules"); 494 block("macro", "macros", "Macros"); 495 block("struct", "structs", "Structs"); 496 block("enum", "enums", "Enums"); 497 block("union", "unions", "Unions"); 498 block("constant", "constants", "Constants"); 499 block("static", "static", "Statics"); 500 block("trait", "traits", "Traits"); 501 block("fn", "functions", "Functions"); 502 block("type", "types", "Type Definitions"); 503 block("foreigntype", "foreign-types", "Foreign Types"); 504 block("keyword", "keywords", "Keywords"); 505 block("traitalias", "trait-aliases", "Trait Aliases"); 506 } 507 } 508 509 window.register_implementors = imp => { 510 const implementors = document.getElementById("implementors-list"); 511 const synthetic_implementors = document.getElementById("synthetic-implementors-list"); 512 const inlined_types = new Set(); 513 514 const TEXT_IDX = 0; 515 const SYNTHETIC_IDX = 1; 516 const TYPES_IDX = 2; 517 518 if (synthetic_implementors) { 519 // This `inlined_types` variable is used to avoid having the same implementation 520 // showing up twice. For example "String" in the "Sync" doc page. 521 // 522 // By the way, this is only used by and useful for traits implemented automatically 523 // (like "Send" and "Sync"). 524 onEachLazy(synthetic_implementors.getElementsByClassName("impl"), el => { 525 const aliases = el.getAttribute("data-aliases"); 526 if (!aliases) { 527 return; 528 } 529 aliases.split(",").forEach(alias => { 530 inlined_types.add(alias); 531 }); 532 }); 533 } 534 535 let currentNbImpls = implementors.getElementsByClassName("impl").length; 536 const traitName = document.querySelector(".main-heading h1 > .trait").textContent; 537 const baseIdName = "impl-" + traitName + "-"; 538 const libs = Object.getOwnPropertyNames(imp); 539 // We don't want to include impls from this JS file, when the HTML already has them. 540 // The current crate should always be ignored. Other crates that should also be 541 // ignored are included in the attribute `data-ignore-extern-crates`. 542 const script = document 543 .querySelector("script[data-ignore-extern-crates]"); 544 const ignoreExternCrates = new Set( 545 (script ? script.getAttribute("data-ignore-extern-crates") : "").split(",") 546 ); 547 for (const lib of libs) { 548 if (lib === window.currentCrate || ignoreExternCrates.has(lib)) { 549 continue; 550 } 551 const structs = imp[lib]; 552 553 struct_loop: 554 for (const struct of structs) { 555 const list = struct[SYNTHETIC_IDX] ? synthetic_implementors : implementors; 556 557 // The types list is only used for synthetic impls. 558 // If this changes, `main.js` and `write_shared.rs` both need changed. 559 if (struct[SYNTHETIC_IDX]) { 560 for (const struct_type of struct[TYPES_IDX]) { 561 if (inlined_types.has(struct_type)) { 562 continue struct_loop; 563 } 564 inlined_types.add(struct_type); 565 } 566 } 567 568 const code = document.createElement("h3"); 569 code.innerHTML = struct[TEXT_IDX]; 570 addClass(code, "code-header"); 571 572 onEachLazy(code.getElementsByTagName("a"), elem => { 573 const href = elem.getAttribute("href"); 574 575 if (href && !/^(?:[a-z+]+:)?\/\//.test(href)) { 576 elem.setAttribute("href", window.rootPath + href); 577 } 578 }); 579 580 const currentId = baseIdName + currentNbImpls; 581 const anchor = document.createElement("a"); 582 anchor.href = "#" + currentId; 583 addClass(anchor, "anchor"); 584 585 const display = document.createElement("div"); 586 display.id = currentId; 587 addClass(display, "impl"); 588 display.appendChild(anchor); 589 display.appendChild(code); 590 list.appendChild(display); 591 currentNbImpls += 1; 592 } 593 } 594 }; 595 if (window.pending_implementors) { 596 window.register_implementors(window.pending_implementors); 597 } 598 599 function addSidebarCrates() { 600 if (!window.ALL_CRATES) { 601 return; 602 } 603 const sidebarElems = document.getElementsByClassName("sidebar-elems")[0]; 604 if (!sidebarElems) { 605 return; 606 } 607 // Draw a convenient sidebar of known crates if we have a listing 608 const h3 = document.createElement("h3"); 609 h3.innerHTML = "Crates"; 610 const ul = document.createElement("ul"); 611 ul.className = "block crate"; 612 613 for (const crate of window.ALL_CRATES) { 614 const link = document.createElement("a"); 615 link.href = window.rootPath + crate + "/index.html"; 616 if (window.rootPath !== "./" && crate === window.currentCrate) { 617 link.className = "current"; 618 } 619 link.textContent = crate; 620 621 const li = document.createElement("li"); 622 li.appendChild(link); 623 ul.appendChild(li); 624 } 625 sidebarElems.appendChild(h3); 626 sidebarElems.appendChild(ul); 627 } 628 629 function expandAllDocs() { 630 const innerToggle = document.getElementById(toggleAllDocsId); 631 removeClass(innerToggle, "will-expand"); 632 onEachLazy(document.getElementsByClassName("toggle"), e => { 633 if (!hasClass(e, "type-contents-toggle") && !hasClass(e, "more-examples-toggle")) { 634 e.open = true; 635 } 636 }); 637 innerToggle.title = "collapse all docs"; 638 innerToggle.children[0].innerText = "\u2212"; // "\u2212" is "−" minus sign 639 } 640 641 function collapseAllDocs() { 642 const innerToggle = document.getElementById(toggleAllDocsId); 643 addClass(innerToggle, "will-expand"); 644 onEachLazy(document.getElementsByClassName("toggle"), e => { 645 if (e.parentNode.id !== "implementations-list" || 646 (!hasClass(e, "implementors-toggle") && 647 !hasClass(e, "type-contents-toggle")) 648 ) { 649 e.open = false; 650 } 651 }); 652 innerToggle.title = "expand all docs"; 653 innerToggle.children[0].innerText = "+"; 654 } 655 656 function toggleAllDocs() { 657 const innerToggle = document.getElementById(toggleAllDocsId); 658 if (!innerToggle) { 659 return; 660 } 661 if (hasClass(innerToggle, "will-expand")) { 662 expandAllDocs(); 663 } else { 664 collapseAllDocs(); 665 } 666 } 667 668 (function() { 669 const toggles = document.getElementById(toggleAllDocsId); 670 if (toggles) { 671 toggles.onclick = toggleAllDocs; 672 } 673 674 const hideMethodDocs = getSettingValue("auto-hide-method-docs") === "true"; 675 const hideImplementations = getSettingValue("auto-hide-trait-implementations") === "true"; 676 const hideLargeItemContents = getSettingValue("auto-hide-large-items") !== "false"; 677 678 function setImplementorsTogglesOpen(id, open) { 679 const list = document.getElementById(id); 680 if (list !== null) { 681 onEachLazy(list.getElementsByClassName("implementors-toggle"), e => { 682 e.open = open; 683 }); 684 } 685 } 686 687 if (hideImplementations) { 688 setImplementorsTogglesOpen("trait-implementations-list", false); 689 setImplementorsTogglesOpen("blanket-implementations-list", false); 690 } 691 692 onEachLazy(document.getElementsByClassName("toggle"), e => { 693 if (!hideLargeItemContents && hasClass(e, "type-contents-toggle")) { 694 e.open = true; 695 } 696 if (hideMethodDocs && hasClass(e, "method-toggle")) { 697 e.open = false; 698 } 699 700 }); 701 }()); 702 703 window.rustdoc_add_line_numbers_to_examples = () => { 704 onEachLazy(document.getElementsByClassName("rust-example-rendered"), x => { 705 const parent = x.parentNode; 706 const line_numbers = parent.querySelectorAll(".example-line-numbers"); 707 if (line_numbers.length > 0) { 708 return; 709 } 710 const count = x.textContent.split("\n").length; 711 const elems = []; 712 for (let i = 0; i < count; ++i) { 713 elems.push(i + 1); 714 } 715 const node = document.createElement("pre"); 716 addClass(node, "example-line-numbers"); 717 node.innerHTML = elems.join("\n"); 718 parent.insertBefore(node, x); 719 }); 720 }; 721 722 window.rustdoc_remove_line_numbers_from_examples = () => { 723 onEachLazy(document.getElementsByClassName("rust-example-rendered"), x => { 724 const parent = x.parentNode; 725 const line_numbers = parent.querySelectorAll(".example-line-numbers"); 726 for (const node of line_numbers) { 727 parent.removeChild(node); 728 } 729 }); 730 }; 731 732 if (getSettingValue("line-numbers") === "true") { 733 window.rustdoc_add_line_numbers_to_examples(); 734 } 735 736 function showSidebar() { 737 window.hideAllModals(false); 738 const sidebar = document.getElementsByClassName("sidebar")[0]; 739 addClass(sidebar, "shown"); 740 } 741 742 function hideSidebar() { 743 const sidebar = document.getElementsByClassName("sidebar")[0]; 744 removeClass(sidebar, "shown"); 745 } 746 747 window.addEventListener("resize", () => { 748 if (window.CURRENT_TOOLTIP_ELEMENT) { 749 // As a workaround to the behavior of `contains: layout` used in doc togglers, 750 // tooltip popovers are positioned using javascript. 751 // 752 // This means when the window is resized, we need to redo the layout. 753 const base = window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE; 754 const force_visible = base.TOOLTIP_FORCE_VISIBLE; 755 hideTooltip(false); 756 if (force_visible) { 757 showTooltip(base); 758 base.TOOLTIP_FORCE_VISIBLE = true; 759 } 760 } 761 }); 762 763 const mainElem = document.getElementById(MAIN_ID); 764 if (mainElem) { 765 mainElem.addEventListener("click", hideSidebar); 766 } 767 768 onEachLazy(document.querySelectorAll("a[href^='#']"), el => { 769 // For clicks on internal links (<A> tags with a hash property), we expand the section we're 770 // jumping to *before* jumping there. We can't do this in onHashChange, because it changes 771 // the height of the document so we wind up scrolled to the wrong place. 772 el.addEventListener("click", () => { 773 expandSection(el.hash.slice(1)); 774 hideSidebar(); 775 }); 776 }); 777 778 onEachLazy(document.querySelectorAll(".toggle > summary:not(.hideme)"), el => { 779 el.addEventListener("click", e => { 780 if (e.target.tagName !== "SUMMARY" && e.target.tagName !== "A") { 781 e.preventDefault(); 782 } 783 }); 784 }); 785 786 /** 787 * Show a tooltip immediately. 788 * 789 * @param {DOMElement} e - The tooltip's anchor point. The DOM is consulted to figure 790 * out what the tooltip should contain, and where it should be 791 * positioned. 792 */ 793 function showTooltip(e) { 794 const notable_ty = e.getAttribute("data-notable-ty"); 795 if (!window.NOTABLE_TRAITS && notable_ty) { 796 const data = document.getElementById("notable-traits-data"); 797 if (data) { 798 window.NOTABLE_TRAITS = JSON.parse(data.innerText); 799 } else { 800 throw new Error("showTooltip() called with notable without any notable traits!"); 801 } 802 } 803 // Make this function idempotent. If the tooltip is already shown, avoid doing extra work 804 // and leave it alone. 805 if (window.CURRENT_TOOLTIP_ELEMENT && window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE === e) { 806 clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT); 807 return; 808 } 809 window.hideAllModals(false); 810 const wrapper = document.createElement("div"); 811 if (notable_ty) { 812 wrapper.innerHTML = "<div class=\"content\">" + 813 window.NOTABLE_TRAITS[notable_ty] + "</div>"; 814 } else { 815 // Replace any `title` attribute with `data-title` to avoid double tooltips. 816 if (e.getAttribute("title") !== null) { 817 e.setAttribute("data-title", e.getAttribute("title")); 818 e.removeAttribute("title"); 819 } 820 if (e.getAttribute("data-title") !== null) { 821 const titleContent = document.createElement("div"); 822 titleContent.className = "content"; 823 titleContent.appendChild(document.createTextNode(e.getAttribute("data-title"))); 824 wrapper.appendChild(titleContent); 825 } 826 } 827 wrapper.className = "tooltip popover"; 828 const focusCatcher = document.createElement("div"); 829 focusCatcher.setAttribute("tabindex", "0"); 830 focusCatcher.onfocus = hideTooltip; 831 wrapper.appendChild(focusCatcher); 832 const pos = e.getBoundingClientRect(); 833 // 5px overlap so that the mouse can easily travel from place to place 834 wrapper.style.top = (pos.top + window.scrollY + pos.height) + "px"; 835 wrapper.style.left = 0; 836 wrapper.style.right = "auto"; 837 wrapper.style.visibility = "hidden"; 838 const body = document.getElementsByTagName("body")[0]; 839 body.appendChild(wrapper); 840 const wrapperPos = wrapper.getBoundingClientRect(); 841 // offset so that the arrow points at the center of the "(i)" 842 const finalPos = pos.left + window.scrollX - wrapperPos.width + 24; 843 if (finalPos > 0) { 844 wrapper.style.left = finalPos + "px"; 845 } else { 846 wrapper.style.setProperty( 847 "--popover-arrow-offset", 848 (wrapperPos.right - pos.right + 4) + "px" 849 ); 850 } 851 wrapper.style.visibility = ""; 852 window.CURRENT_TOOLTIP_ELEMENT = wrapper; 853 window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE = e; 854 clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT); 855 wrapper.onpointerenter = function(ev) { 856 // If this is a synthetic touch event, ignore it. A click event will be along shortly. 857 if (ev.pointerType !== "mouse") { 858 return; 859 } 860 clearTooltipHoverTimeout(e); 861 }; 862 wrapper.onpointerleave = function(ev) { 863 // If this is a synthetic touch event, ignore it. A click event will be along shortly. 864 if (ev.pointerType !== "mouse") { 865 return; 866 } 867 if (!e.TOOLTIP_FORCE_VISIBLE && !elemIsInParent(ev.relatedTarget, e)) { 868 // See "Tooltip pointer leave gesture" below. 869 setTooltipHoverTimeout(e, false); 870 addClass(wrapper, "fade-out"); 871 } 872 }; 873 } 874 875 /** 876 * Show or hide the tooltip after a timeout. If a timeout was already set before this function 877 * was called, that timeout gets cleared. If the tooltip is already in the requested state, 878 * this function will still clear any pending timeout, but otherwise do nothing. 879 * 880 * @param {DOMElement} element - The tooltip's anchor point. The DOM is consulted to figure 881 * out what the tooltip should contain, and where it should be 882 * positioned. 883 * @param {boolean} show - If true, the tooltip will be made visible. If false, it will 884 * be hidden. 885 */ 886 function setTooltipHoverTimeout(element, show) { 887 clearTooltipHoverTimeout(element); 888 if (!show && !window.CURRENT_TOOLTIP_ELEMENT) { 889 // To "hide" an already hidden element, just cancel its timeout. 890 return; 891 } 892 if (show && window.CURRENT_TOOLTIP_ELEMENT) { 893 // To "show" an already visible element, just cancel its timeout. 894 return; 895 } 896 if (window.CURRENT_TOOLTIP_ELEMENT && 897 window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE !== element) { 898 // Don't do anything if another tooltip is already visible. 899 return; 900 } 901 element.TOOLTIP_HOVER_TIMEOUT = setTimeout(() => { 902 if (show) { 903 showTooltip(element); 904 } else if (!element.TOOLTIP_FORCE_VISIBLE) { 905 hideTooltip(false); 906 } 907 }, show ? window.RUSTDOC_TOOLTIP_HOVER_MS : window.RUSTDOC_TOOLTIP_HOVER_EXIT_MS); 908 } 909 910 /** 911 * If a show/hide timeout was set by `setTooltipHoverTimeout`, cancel it. If none exists, 912 * do nothing. 913 * 914 * @param {DOMElement} element - The tooltip's anchor point, 915 * as passed to `setTooltipHoverTimeout`. 916 */ 917 function clearTooltipHoverTimeout(element) { 918 if (element.TOOLTIP_HOVER_TIMEOUT !== undefined) { 919 removeClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out"); 920 clearTimeout(element.TOOLTIP_HOVER_TIMEOUT); 921 delete element.TOOLTIP_HOVER_TIMEOUT; 922 } 923 } 924 925 function tooltipBlurHandler(event) { 926 if (window.CURRENT_TOOLTIP_ELEMENT && 927 !elemIsInParent(document.activeElement, window.CURRENT_TOOLTIP_ELEMENT) && 928 !elemIsInParent(event.relatedTarget, window.CURRENT_TOOLTIP_ELEMENT) && 929 !elemIsInParent(document.activeElement, window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE) && 930 !elemIsInParent(event.relatedTarget, window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE) 931 ) { 932 // Work around a difference in the focus behaviour between Firefox, Chrome, and Safari. 933 // When I click the button on an already-opened tooltip popover, Safari 934 // hides the popover and then immediately shows it again, while everyone else hides it 935 // and it stays hidden. 936 // 937 // To work around this, make sure the click finishes being dispatched before 938 // hiding the popover. Since `hideTooltip()` is idempotent, this makes Safari behave 939 // consistently with the other two. 940 setTimeout(() => hideTooltip(false), 0); 941 } 942 } 943 944 /** 945 * Hide the current tooltip immediately. 946 * 947 * @param {boolean} focus - If set to `true`, move keyboard focus to the tooltip anchor point. 948 * If set to `false`, leave keyboard focus alone. 949 */ 950 function hideTooltip(focus) { 951 if (window.CURRENT_TOOLTIP_ELEMENT) { 952 if (window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE) { 953 if (focus) { 954 window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.focus(); 955 } 956 window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE = false; 957 } 958 const body = document.getElementsByTagName("body")[0]; 959 body.removeChild(window.CURRENT_TOOLTIP_ELEMENT); 960 clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT); 961 window.CURRENT_TOOLTIP_ELEMENT = null; 962 } 963 } 964 965 onEachLazy(document.getElementsByClassName("tooltip"), e => { 966 e.onclick = function() { 967 this.TOOLTIP_FORCE_VISIBLE = this.TOOLTIP_FORCE_VISIBLE ? false : true; 968 if (window.CURRENT_TOOLTIP_ELEMENT && !this.TOOLTIP_FORCE_VISIBLE) { 969 hideTooltip(true); 970 } else { 971 showTooltip(this); 972 window.CURRENT_TOOLTIP_ELEMENT.setAttribute("tabindex", "0"); 973 window.CURRENT_TOOLTIP_ELEMENT.focus(); 974 window.CURRENT_TOOLTIP_ELEMENT.onblur = tooltipBlurHandler; 975 } 976 return false; 977 }; 978 e.onpointerenter = function(ev) { 979 // If this is a synthetic touch event, ignore it. A click event will be along shortly. 980 if (ev.pointerType !== "mouse") { 981 return; 982 } 983 setTooltipHoverTimeout(this, true); 984 }; 985 e.onpointermove = function(ev) { 986 // If this is a synthetic touch event, ignore it. A click event will be along shortly. 987 if (ev.pointerType !== "mouse") { 988 return; 989 } 990 setTooltipHoverTimeout(this, true); 991 }; 992 e.onpointerleave = function(ev) { 993 // If this is a synthetic touch event, ignore it. A click event will be along shortly. 994 if (ev.pointerType !== "mouse") { 995 return; 996 } 997 if (!this.TOOLTIP_FORCE_VISIBLE && 998 !elemIsInParent(ev.relatedTarget, window.CURRENT_TOOLTIP_ELEMENT)) { 999 // Tooltip pointer leave gesture: 1000 // 1001 // Designing a good hover microinteraction is a matter of guessing user 1002 // intent from what are, literally, vague gestures. In this case, guessing if 1003 // hovering in or out of the tooltip base is intentional or not. 1004 // 1005 // To figure this out, a few different techniques are used: 1006 // 1007 // * When the mouse pointer enters a tooltip anchor point, its hitbox is grown 1008 // on the bottom, where the popover is/will appear. Search "hover tunnel" in 1009 // rustdoc.css for the implementation. 1010 // * There's a delay when the mouse pointer enters the popover base anchor, in 1011 // case the mouse pointer was just passing through and the user didn't want 1012 // to open it. 1013 // * Similarly, a delay is added when exiting the anchor, or the popover 1014 // itself, before hiding it. 1015 // * A fade-out animation is layered onto the pointer exit delay to immediately 1016 // inform the user that they successfully dismissed the popover, while still 1017 // providing a way for them to cancel it if it was a mistake and they still 1018 // wanted to interact with it. 1019 // * No animation is used for revealing it, because we don't want people to try 1020 // to interact with an element while it's in the middle of fading in: either 1021 // they're allowed to interact with it while it's fading in, meaning it can't 1022 // serve as mistake-proofing for the popover, or they can't, but 1023 // they might try and be frustrated. 1024 // 1025 // See also: 1026 // * https://www.nngroup.com/articles/timing-exposing-content/ 1027 // * https://www.nngroup.com/articles/tooltip-guidelines/ 1028 // * https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown 1029 setTooltipHoverTimeout(e, false); 1030 addClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out"); 1031 } 1032 }; 1033 }); 1034 1035 const sidebar_menu_toggle = document.getElementsByClassName("sidebar-menu-toggle")[0]; 1036 if (sidebar_menu_toggle) { 1037 sidebar_menu_toggle.addEventListener("click", () => { 1038 const sidebar = document.getElementsByClassName("sidebar")[0]; 1039 if (!hasClass(sidebar, "shown")) { 1040 showSidebar(); 1041 } else { 1042 hideSidebar(); 1043 } 1044 }); 1045 } 1046 1047 function helpBlurHandler(event) { 1048 blurHandler(event, getHelpButton(), window.hidePopoverMenus); 1049 } 1050 1051 function buildHelpMenu() { 1052 const book_info = document.createElement("span"); 1053 const channel = getVar("channel"); 1054 book_info.className = "top"; 1055 book_info.innerHTML = `You can find more information in \ 1056<a href="https://doc.rust-lang.org/${channel}/rustdoc/">the rustdoc book</a>.`; 1057 1058 const shortcuts = [ 1059 ["?", "Show this help dialog"], 1060 ["S", "Focus the search field"], 1061 ["↑", "Move up in search results"], 1062 ["↓", "Move down in search results"], 1063 ["← / →", "Switch result tab (when results focused)"], 1064 ["⏎", "Go to active search result"], 1065 ["+", "Expand all sections"], 1066 ["-", "Collapse all sections"], 1067 ].map(x => "<dt>" + 1068 x[0].split(" ") 1069 .map((y, index) => ((index & 1) === 0 ? "<kbd>" + y + "</kbd>" : " " + y + " ")) 1070 .join("") + "</dt><dd>" + x[1] + "</dd>").join(""); 1071 const div_shortcuts = document.createElement("div"); 1072 addClass(div_shortcuts, "shortcuts"); 1073 div_shortcuts.innerHTML = "<h2>Keyboard Shortcuts</h2><dl>" + shortcuts + "</dl></div>"; 1074 1075 const infos = [ 1076 `For a full list of all search features, take a look <a \ 1077href="https://doc.rust-lang.org/${channel}/rustdoc/how-to-read-rustdoc.html\ 1078#the-search-interface">here</a>.`, 1079 "Prefix searches with a type followed by a colon (e.g., <code>fn:</code>) to \ 1080 restrict the search to a given item kind.", 1081 "Accepted kinds are: <code>fn</code>, <code>mod</code>, <code>struct</code>, \ 1082 <code>enum</code>, <code>trait</code>, <code>type</code>, <code>macro</code>, \ 1083 and <code>const</code>.", 1084 "Search functions by type signature (e.g., <code>vec -> usize</code> or \ 1085 <code>-> vec</code> or <code>String, enum:Cow -> bool</code>)", 1086 "You can look for items with an exact name by putting double quotes around \ 1087 your request: <code>\"string\"</code>", 1088 "Look for functions that accept or return \ 1089 <a href=\"https://doc.rust-lang.org/std/primitive.slice.html\">slices</a> and \ 1090 <a href=\"https://doc.rust-lang.org/std/primitive.array.html\">arrays</a> by writing \ 1091 square brackets (e.g., <code>-> [u8]</code> or <code>[] -> Option</code>)", 1092 "Look for items inside another one by searching for a path: <code>vec::Vec</code>", 1093 ].map(x => "<p>" + x + "</p>").join(""); 1094 const div_infos = document.createElement("div"); 1095 addClass(div_infos, "infos"); 1096 div_infos.innerHTML = "<h2>Search Tricks</h2>" + infos; 1097 1098 const rustdoc_version = document.createElement("span"); 1099 rustdoc_version.className = "bottom"; 1100 const rustdoc_version_code = document.createElement("code"); 1101 rustdoc_version_code.innerText = "rustdoc " + getVar("rustdoc-version"); 1102 rustdoc_version.appendChild(rustdoc_version_code); 1103 1104 const container = document.createElement("div"); 1105 if (!isHelpPage) { 1106 container.className = "popover"; 1107 } 1108 container.id = "help"; 1109 container.style.display = "none"; 1110 1111 const side_by_side = document.createElement("div"); 1112 side_by_side.className = "side-by-side"; 1113 side_by_side.appendChild(div_shortcuts); 1114 side_by_side.appendChild(div_infos); 1115 1116 container.appendChild(book_info); 1117 container.appendChild(side_by_side); 1118 container.appendChild(rustdoc_version); 1119 1120 if (isHelpPage) { 1121 const help_section = document.createElement("section"); 1122 help_section.appendChild(container); 1123 document.getElementById("main-content").appendChild(help_section); 1124 container.style.display = "block"; 1125 } else { 1126 const help_button = getHelpButton(); 1127 help_button.appendChild(container); 1128 1129 container.onblur = helpBlurHandler; 1130 help_button.onblur = helpBlurHandler; 1131 help_button.children[0].onblur = helpBlurHandler; 1132 } 1133 1134 return container; 1135 } 1136 1137 /** 1138 * Hide popover menus, clickable tooltips, and the sidebar (if applicable). 1139 * 1140 * Pass "true" to reset focus for tooltip popovers. 1141 */ 1142 window.hideAllModals = function(switchFocus) { 1143 hideSidebar(); 1144 window.hidePopoverMenus(); 1145 hideTooltip(switchFocus); 1146 }; 1147 1148 /** 1149 * Hide all the popover menus. 1150 */ 1151 window.hidePopoverMenus = function() { 1152 onEachLazy(document.querySelectorAll(".search-form .popover"), elem => { 1153 elem.style.display = "none"; 1154 }); 1155 }; 1156 1157 /** 1158 * Returns the help menu element (not the button). 1159 * 1160 * @param {boolean} buildNeeded - If this argument is `false`, the help menu element won't be 1161 * built if it doesn't exist. 1162 * 1163 * @return {HTMLElement} 1164 */ 1165 function getHelpMenu(buildNeeded) { 1166 let menu = getHelpButton().querySelector(".popover"); 1167 if (!menu && buildNeeded) { 1168 menu = buildHelpMenu(); 1169 } 1170 return menu; 1171 } 1172 1173 /** 1174 * Show the help popup menu. 1175 */ 1176 function showHelp() { 1177 // Prevent `blur` events from being dispatched as a result of closing 1178 // other modals. 1179 getHelpButton().querySelector("a").focus(); 1180 const menu = getHelpMenu(true); 1181 if (menu.style.display === "none") { 1182 window.hideAllModals(); 1183 menu.style.display = ""; 1184 } 1185 } 1186 1187 if (isHelpPage) { 1188 showHelp(); 1189 document.querySelector(`#${HELP_BUTTON_ID} > a`).addEventListener("click", event => { 1190 // Already on the help page, make help button a no-op. 1191 const target = event.target; 1192 if (target.tagName !== "A" || 1193 target.parentElement.id !== HELP_BUTTON_ID || 1194 event.ctrlKey || 1195 event.altKey || 1196 event.metaKey) { 1197 return; 1198 } 1199 event.preventDefault(); 1200 }); 1201 } else { 1202 document.querySelector(`#${HELP_BUTTON_ID} > a`).addEventListener("click", event => { 1203 // By default, have help button open docs in a popover. 1204 // If user clicks with a moderator, though, use default browser behavior, 1205 // probably opening in a new window or tab. 1206 const target = event.target; 1207 if (target.tagName !== "A" || 1208 target.parentElement.id !== HELP_BUTTON_ID || 1209 event.ctrlKey || 1210 event.altKey || 1211 event.metaKey) { 1212 return; 1213 } 1214 event.preventDefault(); 1215 const menu = getHelpMenu(true); 1216 const shouldShowHelp = menu.style.display === "none"; 1217 if (shouldShowHelp) { 1218 showHelp(); 1219 } else { 1220 window.hidePopoverMenus(); 1221 } 1222 }); 1223 } 1224 1225 setMobileTopbar(); 1226 addSidebarItems(); 1227 addSidebarCrates(); 1228 onHashChange(null); 1229 window.addEventListener("hashchange", onHashChange); 1230 searchState.setup(); 1231}()); 1232 1233(function() { 1234 let reset_button_timeout = null; 1235 1236 const but = document.getElementById("copy-path"); 1237 if (!but) { 1238 return; 1239 } 1240 but.onclick = () => { 1241 const parent = but.parentElement; 1242 const path = []; 1243 1244 onEach(parent.childNodes, child => { 1245 if (child.tagName === "A") { 1246 path.push(child.textContent); 1247 } 1248 }); 1249 1250 const el = document.createElement("textarea"); 1251 el.value = path.join("::"); 1252 el.setAttribute("readonly", ""); 1253 // To not make it appear on the screen. 1254 el.style.position = "absolute"; 1255 el.style.left = "-9999px"; 1256 1257 document.body.appendChild(el); 1258 el.select(); 1259 document.execCommand("copy"); 1260 document.body.removeChild(el); 1261 1262 // There is always one children, but multiple childNodes. 1263 but.children[0].style.display = "none"; 1264 1265 let tmp; 1266 if (but.childNodes.length < 2) { 1267 tmp = document.createTextNode("✓"); 1268 but.appendChild(tmp); 1269 } else { 1270 onEachLazy(but.childNodes, e => { 1271 if (e.nodeType === Node.TEXT_NODE) { 1272 tmp = e; 1273 return true; 1274 } 1275 }); 1276 tmp.textContent = "✓"; 1277 } 1278 1279 if (reset_button_timeout !== null) { 1280 window.clearTimeout(reset_button_timeout); 1281 } 1282 1283 function reset_button() { 1284 tmp.textContent = ""; 1285 reset_button_timeout = null; 1286 but.children[0].style.display = ""; 1287 } 1288 1289 reset_button_timeout = window.setTimeout(reset_button, 1000); 1290 }; 1291}()); 1292