dotfiles
My PC Dotfiles & Websitegit clone https://github.com/maydayv7/dotfiles
site/static/js/search.js (5146B)
1(() => {
2 const input = document.getElementById("search");
3 if (!input) return;
4 const container = input.closest(".search-container");
5 const panel = document.getElementById("search-results");
6 const items = panel.querySelector(".search-results__items");
7 const status = panel.querySelector('[role="status"]');
8 let engine;
9 let library;
10 let timer;
11 let revision = 0;
12
13 function loadEngine() {
14 if (engine) return engine;
15 if (!library) {
16 library = new Promise((resolve, reject) => {
17 if (window.Fuse) return resolve();
18 const script = document.createElement("script");
19 script.src = input.dataset.library;
20 script.onload = resolve;
21 script.onerror = () => {
22 script.remove();
23 library = null;
24 reject(new Error("Search library unavailable"));
25 };
26 document.head.appendChild(script);
27 });
28 }
29 engine = Promise.all([
30 library,
31 fetch(input.dataset.index).then((response) => {
32 if (!response.ok) throw new Error("Search index unavailable");
33 return response.json();
34 }),
35 ])
36 .then(
37 ([, index]) =>
38 new Fuse(index, {
39 keys: [
40 { name: "title", weight: 2 },
41 { name: "body", weight: 1 },
42 ],
43 minMatchCharLength: 2,
44 threshold: 0.3,
45 ignoreLocation: true,
46 }),
47 )
48 .catch((error) => {
49 engine = null;
50 throw error;
51 });
52 return engine;
53 }
54
55 function show(visible) {
56 panel.style.display = visible ? "block" : "none";
57 }
58
59 function close() {
60 revision++;
61 clearTimeout(timer);
62 panel.removeAttribute("aria-busy");
63 show(false);
64 }
65
66 function teaser(body, terms) {
67 const lower = body.toLowerCase();
68 const matches = terms
69 .map((term) => lower.indexOf(term.toLowerCase()))
70 .filter((index) => index >= 0);
71 const start = Math.max(0, (matches.length ? Math.min(...matches) : 0) - 40);
72 const text =
73 (start ? "…" : "") +
74 body.slice(start, start + 200) +
75 (body.length > start + 200 ? "…" : "");
76 const paragraph = document.createElement("div");
77 const pattern = new RegExp(
78 terms
79 .map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
80 .join("|"),
81 "gi",
82 );
83 let position = 0;
84 for (const match of text.matchAll(pattern)) {
85 paragraph.append(text.slice(position, match.index));
86 const bold = document.createElement("b");
87 bold.textContent = match[0];
88 paragraph.append(bold);
89 position = match.index + match[0].length;
90 }
91 paragraph.append(text.slice(position));
92 return paragraph;
93 }
94
95 async function search() {
96 const term = input.value.trim();
97 const request = ++revision;
98 items.replaceChildren();
99 if (!term) return show(false);
100 show(true);
101 status.textContent = "Searching…";
102 panel.setAttribute("aria-busy", "true");
103 try {
104 const fuse = await loadEngine();
105 if (request !== revision) return;
106 const results = fuse.search(term);
107 status.textContent = results.length
108 ? `${results.length} ${results.length === 1 ? "result" : "results"}${results.length > 10 ? " (showing 10)" : ""}`
109 : "No results found.";
110 const fragment = document.createDocumentFragment();
111 for (const { item } of results.slice(0, 10)) {
112 const url = new URL(item.url, location.href);
113 if (url.origin !== location.origin) continue;
114 const row = document.createElement("li");
115 row.className = "search-results__item";
116 const title = document.createElement("h2");
117 title.className = "title";
118 const link = document.createElement("a");
119 link.href = url.href;
120 link.textContent = item.title;
121 title.append(link);
122 row.append(
123 title,
124 teaser((item.body || "").replace(/§/g, ""), term.split(/\s+/)),
125 );
126 const more = document.createElement("a");
127 more.href = url.href;
128 const label = document.createElement("i");
129 label.textContent = "Read More →";
130 more.append(label);
131 row.append(more);
132 fragment.append(row);
133 }
134 items.append(fragment);
135 } catch {
136 if (request === revision)
137 status.textContent = "Search could not load. Try typing again.";
138 } finally {
139 if (request === revision) panel.removeAttribute("aria-busy");
140 }
141 }
142
143 input.addEventListener("focus", () => {
144 loadEngine().catch(() => {});
145 if (input.value.trim()) search();
146 });
147 input.addEventListener("input", () => {
148 revision++;
149 clearTimeout(timer);
150 if (!input.value.trim()) return close();
151 timer = setTimeout(search, 150);
152 });
153 container.addEventListener("keydown", (event) => {
154 if (event.key === "Escape") {
155 input.focus();
156 close();
157 }
158 });
159 document.addEventListener("click", (event) => {
160 if (!container.contains(event.target)) close();
161 });
162 container.addEventListener("focusout", (event) => {
163 if (!container.contains(event.relatedTarget)) close();
164 });
165})();