@abreen/tada 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +290 -0
- package/bin/tada.js +361 -0
- package/config/authors.json +1 -0
- package/config/nav.json +28 -0
- package/content/index.md +19 -0
- package/content/lectures/01/Pair.java.md +296 -0
- package/content/lectures/01/Rectangle.java +80 -0
- package/content/lectures/01/demo.py +9 -0
- package/content/lectures/01/index.md +39 -0
- package/content/lectures/01/lecture1.pdf +0 -0
- package/content/lectures/index.md +25 -0
- package/content/markdown.md +379 -0
- package/content/problem_sets/index.md +6 -0
- package/fonts/google-sans-code/GoogleSansCodeVariable-Italic.ttf +0 -0
- package/fonts/google-sans-code/GoogleSansCodeVariable.ttf +0 -0
- package/fonts/google-sans-code/LICENSE.txt +93 -0
- package/fonts/inter/InterVariable-Italic.ttf +0 -0
- package/fonts/inter/InterVariable.ttf +0 -0
- package/fonts/inter/LICENSE.txt +92 -0
- package/package.json +70 -0
- package/public/avatars/alex.jpg +0 -0
- package/public/test.txt +1 -0
- package/src/_mixins.scss +4 -0
- package/src/anchor/README.md +6 -0
- package/src/anchor/index.ts +34 -0
- package/src/anchor/style.scss +48 -0
- package/src/code/README.md +5 -0
- package/src/code/index.ts +113 -0
- package/src/code/style.scss +101 -0
- package/src/code.scss +54 -0
- package/src/header/README.md +8 -0
- package/src/header/index.ts +43 -0
- package/src/header/style.scss +228 -0
- package/src/index.ts +73 -0
- package/src/layout.scss +144 -0
- package/src/literate/style.scss +60 -0
- package/src/print/README.md +4 -0
- package/src/print/index.ts +32 -0
- package/src/print/style.scss +82 -0
- package/src/question/README.md +3 -0
- package/src/question/index.ts +25 -0
- package/src/question/style.scss +116 -0
- package/src/search/README.md +6 -0
- package/src/search/index.ts +574 -0
- package/src/search/style.scss +217 -0
- package/src/style.scss +815 -0
- package/src/timezone/index.test.ts +100 -0
- package/src/timezone/index.ts +298 -0
- package/src/timezone/style.scss +16 -0
- package/src/timezone/timezones.json +58 -0
- package/src/toc/README.md +3 -0
- package/src/toc/index.ts +322 -0
- package/src/toc/style.scss +203 -0
- package/src/top/README.md +4 -0
- package/src/top/index.ts +75 -0
- package/src/util.ts +122 -0
- package/templates/_author.html +27 -0
- package/templates/_bottom.html +3 -0
- package/templates/_download.html +1 -0
- package/templates/_heading.html +19 -0
- package/templates/_nav.html +18 -0
- package/templates/_theme.scss +97 -0
- package/templates/_top.html +87 -0
- package/templates/authors.schema.json +13 -0
- package/templates/code.html +31 -0
- package/templates/default.html +13 -0
- package/templates/literate.html +16 -0
- package/templates/nav.schema.json +27 -0
- package/tsconfig.json +15 -0
- package/types/dev.ts +3 -0
- package/types/sass.d.ts +1 -0
- package/types/site-variables.d.ts +16 -0
- package/webpack/apply-base-path-plugin.js +78 -0
- package/webpack/build-state.js +97 -0
- package/webpack/code.test.js +162 -0
- package/webpack/colors.js +15 -0
- package/webpack/config.base.js +147 -0
- package/webpack/config.dev.js +23 -0
- package/webpack/config.prod.js +32 -0
- package/webpack/content-watch-plugin.js +153 -0
- package/webpack/deflist-id-plugin.js +62 -0
- package/webpack/external-links-plugin.js +37 -0
- package/webpack/features.js +5 -0
- package/webpack/flair.json +1 -0
- package/webpack/generate-content-assets-plugin.js +308 -0
- package/webpack/generate-favicon-plugin.js +198 -0
- package/webpack/generate-fonts-plugin.js +69 -0
- package/webpack/generate-manifest-plugin.js +116 -0
- package/webpack/globals.js +74 -0
- package/webpack/heading-subtitle-plugin.js +80 -0
- package/webpack/json-schema.js +19 -0
- package/webpack/log.js +143 -0
- package/webpack/markdown-plugins.test.js +203 -0
- package/webpack/pagefind-plugin.js +379 -0
- package/webpack/pagefind-plugin.test.js +131 -0
- package/webpack/pdf-text.js +163 -0
- package/webpack/print-flair-plugin.js +22 -0
- package/webpack/reachability.js +273 -0
- package/webpack/reachability.test.js +80 -0
- package/webpack/serve.js +104 -0
- package/webpack/site-variables.js +53 -0
- package/webpack/site.schema.json +67 -0
- package/webpack/templates.js +128 -0
- package/webpack/text-to-id.js +8 -0
- package/webpack/toc-plugin.js +167 -0
- package/webpack/util.js +49 -0
- package/webpack/utils/code.js +439 -0
- package/webpack/utils/content-files.js +147 -0
- package/webpack/utils/define-plugin.js +20 -0
- package/webpack/utils/file-types.js +26 -0
- package/webpack/utils/front-matter.js +57 -0
- package/webpack/utils/jdi-runner/LiterateRunner.class +0 -0
- package/webpack/utils/jdi-runner/LiterateRunner.java +241 -0
- package/webpack/utils/literate-java.js +153 -0
- package/webpack/utils/markdown.js +244 -0
- package/webpack/utils/parse-hsl.js +8 -0
- package/webpack/utils/paths.js +58 -0
- package/webpack/utils/render.js +466 -0
- package/webpack/utils/shiki-highlighter.js +26 -0
- package/webpack/validate-internal-links-plugin.js +155 -0
- package/webpack/watch-reachability-state.js +273 -0
- package/webpack/watch-reachability-state.test.js +198 -0
- package/webpack/watch-reload-client.js +54 -0
- package/webpack/watch.js +166 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import { getElement, applyBasePath } from '../util';
|
|
2
|
+
|
|
3
|
+
const MAX_RESULTS = 24;
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
let pagefind: any = null;
|
|
7
|
+
|
|
8
|
+
type SubResult = { title: string; url: string; excerpt: string };
|
|
9
|
+
type Result = {
|
|
10
|
+
title: string;
|
|
11
|
+
url: string;
|
|
12
|
+
excerpt: string;
|
|
13
|
+
score: number;
|
|
14
|
+
subResults: SubResult[];
|
|
15
|
+
pageNumber: number | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type State = {
|
|
19
|
+
value: string;
|
|
20
|
+
showResults: boolean;
|
|
21
|
+
results: Result[];
|
|
22
|
+
totalResults: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function getPdfPageNumber(
|
|
26
|
+
url: string,
|
|
27
|
+
pageMeta: string | undefined,
|
|
28
|
+
): number | null {
|
|
29
|
+
const fromMeta = Number.parseInt(pageMeta ?? '', 10);
|
|
30
|
+
if (Number.isInteger(fromMeta) && fromMeta > 0) {
|
|
31
|
+
return fromMeta;
|
|
32
|
+
}
|
|
33
|
+
const match = url.match(/#(?:.*&)?page=(\d+)\b/i);
|
|
34
|
+
if (!match) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const fromUrl = Number.parseInt(match[1], 10);
|
|
38
|
+
return Number.isInteger(fromUrl) && fromUrl >= 1 ? fromUrl : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getPdfBaseUrl(url: string): string | null {
|
|
42
|
+
const hashIndex = url.indexOf('#');
|
|
43
|
+
const baseUrl = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
|
|
44
|
+
return baseUrl.toLowerCase().endsWith('.pdf') ? baseUrl : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function groupPdfResults(results: Result[]): Result[] {
|
|
48
|
+
const pdfGroups = new Map<string, Result[]>();
|
|
49
|
+
const other: Result[] = [];
|
|
50
|
+
|
|
51
|
+
for (const result of results) {
|
|
52
|
+
const baseUrl = getPdfBaseUrl(result.url);
|
|
53
|
+
if (!baseUrl || result.pageNumber == null) {
|
|
54
|
+
other.push(result);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!pdfGroups.has(baseUrl)) {
|
|
58
|
+
pdfGroups.set(baseUrl, []);
|
|
59
|
+
}
|
|
60
|
+
pdfGroups.get(baseUrl)!.push(result);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const grouped: Result[] = [...other];
|
|
64
|
+
|
|
65
|
+
pdfGroups.forEach((pages, baseUrl) => {
|
|
66
|
+
const primary = pages
|
|
67
|
+
.slice()
|
|
68
|
+
.sort(
|
|
69
|
+
(a, b) =>
|
|
70
|
+
b.score - a.score || (a.pageNumber ?? 0) - (b.pageNumber ?? 0),
|
|
71
|
+
)[0];
|
|
72
|
+
const subResults = pages
|
|
73
|
+
.slice()
|
|
74
|
+
.sort((a, b) => (a.pageNumber ?? 0) - (b.pageNumber ?? 0))
|
|
75
|
+
.map(page => ({
|
|
76
|
+
title: `Page ${page.pageNumber ?? '?'}`,
|
|
77
|
+
url: page.url,
|
|
78
|
+
excerpt: page.excerpt,
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
grouped.push({
|
|
82
|
+
title: primary.title,
|
|
83
|
+
url: baseUrl,
|
|
84
|
+
excerpt: primary.excerpt,
|
|
85
|
+
score: primary.score,
|
|
86
|
+
subResults,
|
|
87
|
+
pageNumber: null,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
grouped.sort((a, b) => b.score - a.score);
|
|
92
|
+
return grouped;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function doSearch(state: State) {
|
|
96
|
+
if (pagefind == null) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!state.value) {
|
|
101
|
+
state.results = [];
|
|
102
|
+
state.totalResults = 0;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const search = await pagefind.search(state.value);
|
|
107
|
+
const slice = search.results.slice(0, MAX_RESULTS);
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
+
const data = await Promise.all(slice.map((r: any) => r.data()));
|
|
110
|
+
|
|
111
|
+
const titlePostfix = window.siteVariables.titlePostfix ?? '';
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
const results: Result[] = data.map((d: any) => {
|
|
114
|
+
let title: string = d.meta?.title ?? d.url;
|
|
115
|
+
if (titlePostfix && title.endsWith(titlePostfix)) {
|
|
116
|
+
title = title.slice(0, -titlePostfix.length);
|
|
117
|
+
}
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
119
|
+
const subResults: SubResult[] = (d.sub_results ?? [])
|
|
120
|
+
.filter((s: any) => {
|
|
121
|
+
try {
|
|
122
|
+
return new URL(s.url, window.location.href).hash !== '';
|
|
123
|
+
} catch {
|
|
124
|
+
return s.url !== d.url;
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
128
|
+
.map((s: any) => ({
|
|
129
|
+
title: s.title ?? '',
|
|
130
|
+
url: s.url,
|
|
131
|
+
excerpt: s.excerpt ?? '',
|
|
132
|
+
}));
|
|
133
|
+
return {
|
|
134
|
+
title,
|
|
135
|
+
url: d.url,
|
|
136
|
+
excerpt: d.excerpt,
|
|
137
|
+
score: d.score ?? 0,
|
|
138
|
+
subResults,
|
|
139
|
+
pageNumber: getPdfPageNumber(d.url, d.meta?.page),
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const grouped = groupPdfResults(results);
|
|
144
|
+
state.totalResults = grouped.length;
|
|
145
|
+
state.results = grouped;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function applyHighlight(
|
|
149
|
+
resultsContainer: HTMLElement,
|
|
150
|
+
focusedEl: HTMLElement | null,
|
|
151
|
+
) {
|
|
152
|
+
const options = Array.from(
|
|
153
|
+
resultsContainer.querySelectorAll('[role="option"]'),
|
|
154
|
+
) as HTMLElement[];
|
|
155
|
+
options.forEach(opt => {
|
|
156
|
+
const selected = focusedEl !== null && opt.contains(focusedEl);
|
|
157
|
+
opt.setAttribute('aria-selected', selected ? 'true' : 'false');
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function render(
|
|
162
|
+
input: HTMLInputElement,
|
|
163
|
+
resultsContainer: HTMLElement,
|
|
164
|
+
state: State,
|
|
165
|
+
) {
|
|
166
|
+
if (state.showResults) {
|
|
167
|
+
resultsContainer.removeAttribute('inert');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let resultsDiv = resultsContainer.querySelector(
|
|
171
|
+
'.results',
|
|
172
|
+
) as HTMLElement | null;
|
|
173
|
+
if (!resultsDiv) {
|
|
174
|
+
resultsDiv = document.createElement('div');
|
|
175
|
+
resultsDiv.className = 'results';
|
|
176
|
+
resultsContainer.appendChild(resultsDiv);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const ol = document.createElement('ol');
|
|
180
|
+
ol.id = `${input.name}-results`;
|
|
181
|
+
ol.role = 'listbox';
|
|
182
|
+
ol.tabIndex = -1;
|
|
183
|
+
ol.setAttribute('aria-label', 'Search results');
|
|
184
|
+
|
|
185
|
+
const totalVisible = Math.min(state.results.length, MAX_RESULTS);
|
|
186
|
+
|
|
187
|
+
state.results.slice(0, MAX_RESULTS).forEach((result, i) => {
|
|
188
|
+
const a = document.createElement('a');
|
|
189
|
+
a.id = `result-${i}`;
|
|
190
|
+
a.className = 'result';
|
|
191
|
+
a.href = result.url;
|
|
192
|
+
a.tabIndex = 0;
|
|
193
|
+
|
|
194
|
+
const titleEl = document.createElement('div');
|
|
195
|
+
titleEl.id = `title-${i}`;
|
|
196
|
+
titleEl.className = 'title';
|
|
197
|
+
titleEl.textContent = result.title;
|
|
198
|
+
a.appendChild(titleEl);
|
|
199
|
+
|
|
200
|
+
const subtitle = document.createElement('div');
|
|
201
|
+
subtitle.className = 'subtitle';
|
|
202
|
+
subtitle.innerText = result.url;
|
|
203
|
+
a.appendChild(subtitle);
|
|
204
|
+
|
|
205
|
+
const excerpt = document.createElement('div');
|
|
206
|
+
excerpt.className = 'excerpt';
|
|
207
|
+
excerpt.innerHTML = result.excerpt;
|
|
208
|
+
a.appendChild(excerpt);
|
|
209
|
+
|
|
210
|
+
const li = document.createElement('li');
|
|
211
|
+
li.id = `option-${i}`;
|
|
212
|
+
li.setAttribute('role', 'option');
|
|
213
|
+
li.setAttribute('aria-selected', 'false');
|
|
214
|
+
li.setAttribute('aria-setsize', String(totalVisible));
|
|
215
|
+
li.setAttribute('aria-posinset', String(i + 1));
|
|
216
|
+
li.setAttribute('aria-labelledby', `title-${i}`);
|
|
217
|
+
li.appendChild(a);
|
|
218
|
+
|
|
219
|
+
const subsToShow = result.subResults.slice(0, 5);
|
|
220
|
+
if (subsToShow.length > 0) {
|
|
221
|
+
const subList = document.createElement('ul');
|
|
222
|
+
subList.className = 'sub-results';
|
|
223
|
+
for (const sub of subsToShow) {
|
|
224
|
+
const subA = document.createElement('a');
|
|
225
|
+
subA.href = sub.url;
|
|
226
|
+
subA.className = 'sub-result';
|
|
227
|
+
|
|
228
|
+
const subTitle = document.createElement('div');
|
|
229
|
+
subTitle.className = 'title';
|
|
230
|
+
subTitle.textContent = sub.title;
|
|
231
|
+
subA.appendChild(subTitle);
|
|
232
|
+
|
|
233
|
+
if (sub.excerpt) {
|
|
234
|
+
const subExcerpt = document.createElement('div');
|
|
235
|
+
subExcerpt.className = 'excerpt';
|
|
236
|
+
subExcerpt.innerHTML = sub.excerpt;
|
|
237
|
+
subA.appendChild(subExcerpt);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const subLi = document.createElement('li');
|
|
241
|
+
subLi.appendChild(subA);
|
|
242
|
+
subList.appendChild(subLi);
|
|
243
|
+
}
|
|
244
|
+
li.appendChild(subList);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
ol.appendChild(li);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
resultsDiv.replaceChildren(ol);
|
|
251
|
+
|
|
252
|
+
let infoSpan = resultsDiv.querySelector(
|
|
253
|
+
'.results-info',
|
|
254
|
+
) as HTMLSpanElement | null;
|
|
255
|
+
if (!infoSpan) {
|
|
256
|
+
infoSpan = document.createElement('span');
|
|
257
|
+
infoSpan.className = 'results-info';
|
|
258
|
+
|
|
259
|
+
const hint = document.createElement('span');
|
|
260
|
+
hint.className = 'search-hint';
|
|
261
|
+
const kbd1 = document.createElement('kbd');
|
|
262
|
+
kbd1.textContent = 'Ctrl';
|
|
263
|
+
const kbd2 = document.createElement('kbd');
|
|
264
|
+
kbd2.textContent = 'Space';
|
|
265
|
+
hint.append(kbd1, '+', kbd2, '\u00a0to search');
|
|
266
|
+
infoSpan.appendChild(hint);
|
|
267
|
+
|
|
268
|
+
resultsDiv.insertBefore(infoSpan, resultsDiv.firstChild);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let countSpan = infoSpan.querySelector(
|
|
272
|
+
'.results-count',
|
|
273
|
+
) as HTMLSpanElement | null;
|
|
274
|
+
if (!countSpan) {
|
|
275
|
+
countSpan = document.createElement('span');
|
|
276
|
+
countSpan.className = 'results-count';
|
|
277
|
+
infoSpan.insertBefore(countSpan, infoSpan.firstChild);
|
|
278
|
+
}
|
|
279
|
+
const n = state.totalResults;
|
|
280
|
+
if (n === 0) {
|
|
281
|
+
countSpan.textContent = 'No results';
|
|
282
|
+
} else if (n === 1) {
|
|
283
|
+
countSpan.textContent = 'One result';
|
|
284
|
+
} else if (n <= MAX_RESULTS) {
|
|
285
|
+
countSpan.textContent = `${n} results`;
|
|
286
|
+
} else {
|
|
287
|
+
countSpan.textContent = `Showing first ${MAX_RESULTS} results`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (state.showResults) {
|
|
291
|
+
resultsContainer.classList.add('is-showing');
|
|
292
|
+
resultsContainer.setAttribute('aria-hidden', 'false');
|
|
293
|
+
} else {
|
|
294
|
+
resultsContainer.classList.remove('is-showing');
|
|
295
|
+
resultsContainer.setAttribute('aria-hidden', 'true');
|
|
296
|
+
resultsContainer.setAttribute('inert', '');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
input.setAttribute('aria-expanded', String(state.showResults));
|
|
300
|
+
input.setAttribute('aria-controls', ol.id);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export default (window: Window) => {
|
|
304
|
+
const input = document.querySelector(
|
|
305
|
+
'input.quick-search',
|
|
306
|
+
) as HTMLInputElement | null;
|
|
307
|
+
if (!input) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const header = input.closest('header') as HTMLElement;
|
|
312
|
+
const resultsContainer = getElement(header, '.results-container');
|
|
313
|
+
const resultsDiv = getElement(resultsContainer, '.results');
|
|
314
|
+
|
|
315
|
+
// Unhide (hidden via inline style in template to prevent FOUC)
|
|
316
|
+
resultsDiv.style.display = '';
|
|
317
|
+
|
|
318
|
+
let entryETag: string | null = null;
|
|
319
|
+
let entryLastModified: string | null = null;
|
|
320
|
+
let lastIndexCheck = 0;
|
|
321
|
+
let indexCheckInFlight = false;
|
|
322
|
+
|
|
323
|
+
async function loadPagefind() {
|
|
324
|
+
if (pagefind) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// @ts-ignore pagefind.js is generated post-build, not resolvable at compile time
|
|
328
|
+
pagefind = await import(
|
|
329
|
+
/* webpackIgnore: true */ applyBasePath('/pagefind/pagefind.js')
|
|
330
|
+
);
|
|
331
|
+
await pagefind.init();
|
|
332
|
+
const res = await fetch(applyBasePath('/pagefind/pagefind-entry.json'), {
|
|
333
|
+
cache: 'no-cache',
|
|
334
|
+
});
|
|
335
|
+
if (res.ok) {
|
|
336
|
+
entryETag = res.headers.get('ETag');
|
|
337
|
+
entryLastModified = res.headers.get('Last-Modified');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function checkForIndexUpdate() {
|
|
342
|
+
if (indexCheckInFlight || Date.now() - lastIndexCheck < 3000) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
lastIndexCheck = Date.now();
|
|
346
|
+
indexCheckInFlight = true;
|
|
347
|
+
try {
|
|
348
|
+
const res = await fetch(applyBasePath('/pagefind/pagefind-entry.json'), {
|
|
349
|
+
method: 'HEAD',
|
|
350
|
+
cache: 'no-cache',
|
|
351
|
+
});
|
|
352
|
+
if (!res.ok) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const newETag = res.headers.get('ETag');
|
|
356
|
+
const newLastModified = res.headers.get('Last-Modified');
|
|
357
|
+
const changed =
|
|
358
|
+
(newETag !== null && newETag !== entryETag) ||
|
|
359
|
+
(newETag === null &&
|
|
360
|
+
newLastModified !== null &&
|
|
361
|
+
newLastModified !== entryLastModified);
|
|
362
|
+
if (changed) {
|
|
363
|
+
pagefind = null;
|
|
364
|
+
await loadPagefind();
|
|
365
|
+
await update();
|
|
366
|
+
}
|
|
367
|
+
} catch {
|
|
368
|
+
// best-effort
|
|
369
|
+
} finally {
|
|
370
|
+
indexCheckInFlight = false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
loadPagefind().catch(err => {
|
|
375
|
+
console.log(`failed to load Pagefind: ${err}`);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const state: State = {
|
|
379
|
+
value: '',
|
|
380
|
+
showResults: false,
|
|
381
|
+
results: [],
|
|
382
|
+
totalResults: 0,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
async function update() {
|
|
386
|
+
await doSearch(state);
|
|
387
|
+
render(input!, resultsContainer, state);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function hide() {
|
|
391
|
+
state.showResults = false;
|
|
392
|
+
render(input!, resultsContainer, state);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function handleInput(e: Event) {
|
|
396
|
+
const value = (e.target as HTMLInputElement).value;
|
|
397
|
+
if (value === state.value) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
state.value = value;
|
|
401
|
+
state.showResults = true;
|
|
402
|
+
update().catch(() => {});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function handleWindowPointerMove() {
|
|
406
|
+
resultsContainer.classList.remove('is-typing');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let previousFocus: HTMLElement | null = null;
|
|
410
|
+
|
|
411
|
+
function handleFocus(e: FocusEvent) {
|
|
412
|
+
const previous = e.relatedTarget as HTMLElement | null;
|
|
413
|
+
if (previous && !resultsContainer.contains(previous)) {
|
|
414
|
+
previousFocus = previous;
|
|
415
|
+
}
|
|
416
|
+
checkForIndexUpdate().catch(() => {});
|
|
417
|
+
if (!state.showResults) {
|
|
418
|
+
state.showResults = true;
|
|
419
|
+
update().catch(() => {});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let pointerDownInResults = false;
|
|
424
|
+
|
|
425
|
+
function handleBlur(e: FocusEvent) {
|
|
426
|
+
if (!state.showResults) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const related = e.relatedTarget as HTMLElement | null;
|
|
430
|
+
if (related && resultsContainer.contains(related)) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (pointerDownInResults) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
hide();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function handlePointerDown() {
|
|
440
|
+
pointerDownInResults = true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function handleWindowPointerUp(e: PointerEvent) {
|
|
444
|
+
if (!pointerDownInResults) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
// If released inside results, let the click handler hide instead
|
|
448
|
+
if (resultsContainer.contains(e.target as Node)) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
pointerDownInResults = false;
|
|
452
|
+
hide();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function handleResultClick() {
|
|
456
|
+
pointerDownInResults = false;
|
|
457
|
+
hide();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function handleInputKeyDown(e: KeyboardEvent) {
|
|
461
|
+
if (e.key === 'Escape') {
|
|
462
|
+
e.preventDefault();
|
|
463
|
+
if (state.showResults) {
|
|
464
|
+
hide();
|
|
465
|
+
} else {
|
|
466
|
+
previousFocus?.focus();
|
|
467
|
+
}
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!state.showResults) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const links = Array.from(
|
|
476
|
+
resultsContainer.querySelectorAll('a.result, a.sub-result'),
|
|
477
|
+
) as HTMLElement[];
|
|
478
|
+
|
|
479
|
+
if (e.key === 'ArrowDown') {
|
|
480
|
+
e.preventDefault();
|
|
481
|
+
links[0]?.focus();
|
|
482
|
+
} else if (e.key === 'ArrowUp') {
|
|
483
|
+
e.preventDefault();
|
|
484
|
+
// no-op: already at the top of the widget
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function handleResultsKeyDown(e: KeyboardEvent) {
|
|
489
|
+
const links = Array.from(
|
|
490
|
+
resultsContainer.querySelectorAll('a.result, a.sub-result'),
|
|
491
|
+
) as HTMLElement[];
|
|
492
|
+
const focused = document.activeElement as HTMLElement;
|
|
493
|
+
const idx = links.indexOf(focused);
|
|
494
|
+
|
|
495
|
+
if (e.key === 'ArrowDown') {
|
|
496
|
+
e.preventDefault();
|
|
497
|
+
if (idx < 0) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
links[(idx + 1) % links.length].focus();
|
|
501
|
+
} else if (e.key === 'ArrowUp') {
|
|
502
|
+
e.preventDefault();
|
|
503
|
+
if (idx < 0) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (idx === 0) {
|
|
507
|
+
input!.focus();
|
|
508
|
+
} else {
|
|
509
|
+
links[idx - 1].focus();
|
|
510
|
+
}
|
|
511
|
+
} else if (e.key === 'Escape') {
|
|
512
|
+
e.preventDefault();
|
|
513
|
+
hide();
|
|
514
|
+
input!.focus();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function handleResultsFocusIn(e: FocusEvent) {
|
|
519
|
+
applyHighlight(resultsContainer, e.target as HTMLElement);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function handleResultsFocusOut(e: FocusEvent) {
|
|
523
|
+
const related = e.relatedTarget as HTMLElement | null;
|
|
524
|
+
if (related && (resultsContainer.contains(related) || related === input)) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
applyHighlight(resultsContainer, null);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function handleWindowPointerDown(e: PointerEvent) {
|
|
531
|
+
const target = e.target as Node;
|
|
532
|
+
if (!input!.contains(target) && !resultsContainer.contains(target)) {
|
|
533
|
+
hide();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function handleWindowKeyDown(e: KeyboardEvent) {
|
|
538
|
+
resultsContainer.classList.add('is-typing');
|
|
539
|
+
if (e.key === ' ' && e.ctrlKey && !e.altKey && !e.metaKey) {
|
|
540
|
+
e.preventDefault();
|
|
541
|
+
input!.focus();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
input.addEventListener('input', handleInput);
|
|
546
|
+
input.addEventListener('focus', handleFocus);
|
|
547
|
+
input.addEventListener('blur', handleBlur);
|
|
548
|
+
input.addEventListener('keydown', handleInputKeyDown);
|
|
549
|
+
resultsContainer.addEventListener('pointerdown', handlePointerDown);
|
|
550
|
+
resultsContainer.addEventListener('click', handleResultClick);
|
|
551
|
+
resultsContainer.addEventListener('keydown', handleResultsKeyDown);
|
|
552
|
+
resultsContainer.addEventListener('focusin', handleResultsFocusIn);
|
|
553
|
+
resultsContainer.addEventListener('focusout', handleResultsFocusOut);
|
|
554
|
+
window.addEventListener('pointerup', handleWindowPointerUp);
|
|
555
|
+
window.addEventListener('pointerdown', handleWindowPointerDown);
|
|
556
|
+
window.addEventListener('pointermove', handleWindowPointerMove);
|
|
557
|
+
window.addEventListener('keydown', handleWindowKeyDown);
|
|
558
|
+
|
|
559
|
+
return () => {
|
|
560
|
+
window.removeEventListener('keydown', handleWindowKeyDown);
|
|
561
|
+
window.removeEventListener('pointermove', handleWindowPointerMove);
|
|
562
|
+
window.removeEventListener('pointerdown', handleWindowPointerDown);
|
|
563
|
+
window.removeEventListener('pointerup', handleWindowPointerUp);
|
|
564
|
+
resultsContainer.removeEventListener('focusout', handleResultsFocusOut);
|
|
565
|
+
resultsContainer.removeEventListener('focusin', handleResultsFocusIn);
|
|
566
|
+
resultsContainer.removeEventListener('keydown', handleResultsKeyDown);
|
|
567
|
+
resultsContainer.removeEventListener('click', handleResultClick);
|
|
568
|
+
resultsContainer.removeEventListener('pointerdown', handlePointerDown);
|
|
569
|
+
input!.removeEventListener('keydown', handleInputKeyDown);
|
|
570
|
+
input!.removeEventListener('blur', handleBlur);
|
|
571
|
+
input!.removeEventListener('focus', handleFocus);
|
|
572
|
+
input!.removeEventListener('input', handleInput);
|
|
573
|
+
};
|
|
574
|
+
};
|