PageSourceSearch

https://learninpublic.org/assets/reader/search.js

js learninpublic.org collected 2026-10-02 06:14:26 UTC 6,857 bytes, 138 lines download raw bytes

1(() => {
2  "use strict";
3
4  // Keep Quarto's autocomplete, ranking, search.json, and shareable search URLs.
5  const loadIndex = window.readSearchData;
6  if (!loadIndex) return;
7  let pendingIndex;
8  let failed = false;
9  let overlay;
10  let background = [];
11
12  window.readSearchData = () => {
13    if (!pendingIndex) {
14      failed = false;
15      document.body.classList.add("book-search-loading");
16      pendingIndex = loadIndex().catch(() => {
17        failed = true;
18        pendingIndex = null;
19        // The native collection remains empty, but our error state replaces
20        // "No results" and leaves useful links available. Next input retries.
21        return new window.Fuse([]);
22      }).finally(() => requestAnimationFrame(() => {
23        document.body.classList.remove("book-search-loading");
24        enhance();
25      }));
26    }
27    return pendingIndex;
28  };
29
30  function category(href) {
31    const slug = new URL(href, location.href).pathname.split("/").at(-1);
32    for (const [prefix, label] of [["careers-", "Guide"], ["principles-", "Principle"], ["strategy-", "Strategy"], ["tactics-", "Tactic"], ["concepts-", "Concept"]]) {
33      if (slug.startsWith(prefix)) return label;
34    }
35    return "Book";
36  }
37
38  function mountOverlay(container) {
39    container.setAttribute("role", "dialog");
40    container.setAttribute("aria-modal", "true");
41    container.setAttribute("aria-label", "Search the book");
42    const input = container.querySelector(".aa-Input");
43    input.removeAttribute("aria-labelledby");
44    input.setAttribute("aria-label", "Search chapters and ideas");
45    input.setAttribute("role", "combobox");
46    input.setAttribute("aria-expanded", "true");
47    input.setAttribute("aria-haspopup", "listbox");
48
49    if (!container.querySelector(".book-search-destinations")) {
50      const status = document.createElement("p");
51      status.className = "book-search-status";
52      status.setAttribute("role", "status");
53      container.querySelector(".aa-DetachedFormContainer").after(status);
54      const destinations = document.createElement("nav");
55      destinations.className = "book-search-destinations";
56      destinations.setAttribute("aria-label", "Start reading");
57      const heading = document.createElement("p");
58      heading.textContent = "Start reading";
59      destinations.append(heading);
60      // Select recommendations from Quarto's authoritative book navigation.
61      for (const suffix of ["/quickstart.html", "/chapters/careers-beyond-coding.html", "/chapters/principles-learn-in-public.html", "/chapters/strategy-megatrends.html", "/chapters/tactics-marketing-yourself.html", "/audiobook.html"]) {
62        const source = [...document.querySelectorAll("#quarto-sidebar .sidebar-item a[href]")].find(link => new URL(link.href).pathname === suffix);
63        if (!source) continue;
64        const link = document.createElement("a");
65        link.href = source.href;
66        link.textContent = source.querySelector(".menu-text")?.textContent.trim().replace(/^\d+\s*/, "") || source.textContent.trim();
67        destinations.append(link);
68      }
69      container.append(destinations);
70    }
71    background = [...document.querySelectorAll("#quarto-content, #quarto-sidebar, .book-masthead, .audiobook-player, .nav-footer")].map(element => [element, element.inert]);
72    for (const [element] of background) element.inert = true;
73  }
74
75  function enhance() {
76    const container = document.querySelector(".aa-DetachedContainer");
77    if (container !== overlay) {
78      for (const [element, wasInert] of background) element.inert = wasInert;
79      background = [];
80      overlay = container;
81      if (container) mountOverlay(container);
82    }
83    if (!container) return;
84    const input = container.querySelector(".aa-Input");
85    // Autocomplete rewrites this association while rendering collections.
86    input.removeAttribute("aria-labelledby");
87    const query = input.value.trim();
88    const loading = document.body.classList.contains("book-search-loading");
89    const message = failed ? "Search couldn’t load. Check your connection and try typing again, or use a link below." : loading && query ? "Searching the book…" : "";
90    const status = container.querySelector(".book-search-status");
91    if (status.textContent !== message) status.textContent = message;
92    status.hidden = !message;
93    container.classList.toggle("book-search-failed", failed);
94    container.classList.toggle("book-search-empty", !query);
95    container.querySelector(".book-search-destinations").hidden = Boolean(query) && !failed;
96    for (const result of container.querySelectorAll(".search-result-doc")) {
97      let badge = result.querySelector(".reader-search-category");
98      if (!badge) {
99        badge = document.createElement("span");
100        badge.className = "reader-search-category";
101        result.querySelector(".search-result-title-container").prepend(badge);
102      }
103      const label = category(result.querySelector("a[href]").href);
104      // Autocomplete can reuse a result node for a different chapter.
105      if (badge.textContent !== label) badge.textContent = label;
106    }
107  }
108
109  new MutationObserver(enhance).observe(document.body, { childList: true, subtree: true });
110  document.addEventListener("input", event => {
111    if (event.target.matches?.(".aa-Input")) enhance();
112  });
113  document.addEventListener("click", event => {
114    if (event.target.closest?.("[data-book-search], .book-panel-search")) window.readSearchData();
115  }, { capture: true });
116  document.addEventListener("keydown", event => {
117    if (event.isComposing || event.keyCode === 229) return;
118    if (overlay && event.key === "Tab") {
119      const controls = [...overlay.querySelectorAll("input, button, a[href]")].filter(element => !element.disabled && element.tabIndex >= 0 && element.getClientRects().length);
120      const first = controls[0];
121      const last = controls.at(-1);
122      if (event.shiftKey && (document.activeElement === first || !control
122s.includes(document.activeElement))) {
123        event.preventDefault(); last?.focus();
124      } else if (!event.shiftKey && document.activeElement === last) {
125        event.preventDefault(); first?.focus();
126      }
127      return;
128    }
129    const editable = event.target.closest?.("input, textarea, select, [contenteditable]:not([contenteditable='false']), [role='textbox']");
130    const dialog = document.querySelector("dialog[open], [role='dialog'][aria-modal='true']");
131    const slash = event.key === "/" && !event.metaKey && !event.ctrlKey && !event.altKey;
132    const command = event.key.toLowerCase() === "k" && (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey;
133    if ((!slash && !command) || editable || dialog || event.defaultPrevented) return;
134    event.preventDefault();
135    document.querySelector("[data-book-search]")?.click();
136  }, { capture: true });
137  enhance();
138})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.