Skip to content

Commit 76234f6

Browse files
committed
Add method/type index filter
1 parent c34a4c1 commit 76234f6

5 files changed

Lines changed: 239 additions & 1 deletion

File tree

_config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ const pagefindUi = {
540540
debounceTimeoutMs: 150,
541541
translations: {
542542
placeholder: "Search...",
543-
clear_search: "Clear search",
543+
clear_search: "Clear Search",
544544
search_label: "Search documentation",
545545
zero_results: "No results were found for “[SEARCH_TERM]”.",
546546
},

src/_includes/layout.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default (
1616
walkthroughProgress.step <= walkthroughProgress.total
1717
? walkthroughProgress
1818
: undefined;
19+
const referenceIndex = url === "/methods/" ? "methods" : url === "/types/" ? "types" : undefined;
1920
const searchable = !url.startsWith("/gh/") && !url.startsWith("/404") && url !== "/source-map/";
2021
const searchKind = url.startsWith("/methods/") ? "Methods" : url.startsWith("/types/") ? "Types" : url.startsWith("/server/") ? "Server" : "Guides";
2122

@@ -214,6 +215,32 @@ export default (
214215
</div>
215216
</div>
216217
)}
218+
{referenceIndex && (
219+
<>
220+
<div class="reference-filter" role="search" aria-label={`Filter ${referenceIndex}`} data-reference-filter={referenceIndex} data-pagefind-ignore="all">
221+
<label class="reference-filter-label" for="reference-filter-input">
222+
Filter {referenceIndex}
223+
</label>
224+
<div class="reference-filter-field">
225+
<input
226+
id="reference-filter-input"
227+
class="reference-filter-input"
228+
type="search"
229+
placeholder={`Filter ${referenceIndex}...`}
230+
autocomplete="off"
231+
autocapitalize="none"
232+
spellcheck="false"
233+
/>
234+
<button type="button" class="reference-filter-clear" aria-label="Clear filter" hidden>
235+
<svg viewBox="0 0 16 16" aria-hidden="true">
236+
<path d="m4 4 8 8m0-8-8 8" />
237+
</svg>
238+
</button>
239+
</div>
240+
</div>
241+
<p class="reference-filter-empty" data-reference-filter-empty hidden data-pagefind-ignore="all"></p>
242+
</>
243+
)}
217244
{children}
218245
{(next || prev) && (
219246
<nav class="bottom-nav" aria-label="Documentation pagination" data-pagefind-ignore="all">

src/_includes/style/content.scss

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,111 @@
225225
.no-margin * {
226226
margin: 0;
227227
}
228+
229+
.descr-list[hidden],
230+
.descr-list > [hidden],
231+
> h2[hidden],
232+
.reference-filter-empty[hidden] {
233+
display: none !important;
234+
}
235+
}
236+
237+
.reference-filter {
238+
display: flex;
239+
margin: 1rem 0 1.25rem;
240+
align-items: center;
241+
gap: 0.75rem;
242+
}
243+
244+
.reference-filter-label {
245+
position: absolute;
246+
width: 1px;
247+
height: 1px;
248+
padding: 0;
249+
overflow: hidden;
250+
clip: rect(0, 0, 0, 0);
251+
clip-path: inset(50%);
252+
border: 0;
253+
white-space: nowrap;
254+
}
255+
256+
.reference-filter-field {
257+
position: relative;
258+
min-width: 0;
259+
flex: 1;
260+
}
261+
262+
.reference-filter-input {
263+
width: 100%;
264+
min-width: 0;
265+
height: 2.35rem;
266+
padding: 0.5rem 0.65rem;
267+
appearance: none;
268+
border: 1px solid var(--border);
269+
border-radius: 0;
270+
background: var(--surface);
271+
color: var(--fg);
272+
font: inherit;
273+
font-size: 0.8125rem;
274+
font-weight: 400;
275+
line-height: 1.2;
276+
277+
&::placeholder {
278+
color: var(--muted);
279+
opacity: 1;
280+
}
281+
282+
&:focus {
283+
border-color: var(--kr);
284+
outline: 2px solid color-mix(in srgb, var(--kr) 22%, transparent);
285+
outline-offset: 1px;
286+
}
287+
}
288+
289+
.reference-filter-field:has(.reference-filter-clear:not([hidden])) .reference-filter-input {
290+
padding-right: 2.75rem;
291+
}
292+
293+
.reference-filter-clear {
294+
position: absolute;
295+
top: 0;
296+
right: 0;
297+
display: grid;
298+
width: 2.35rem;
299+
height: 2.35rem;
300+
padding: 0;
301+
place-items: center;
302+
border: 0;
303+
border-left: 1px solid var(--border);
304+
border-radius: 0;
305+
background: transparent;
306+
color: var(--muted);
307+
cursor: pointer;
308+
309+
&[hidden] {
310+
display: none;
311+
}
312+
313+
&:hover {
314+
color: var(--fg);
315+
}
316+
317+
> svg {
318+
width: 0.85rem;
319+
height: 0.85rem;
320+
fill: none;
321+
stroke: currentColor;
322+
stroke-linecap: round;
323+
stroke-width: 1.5;
324+
}
325+
}
326+
327+
.content .reference-filter-empty {
328+
margin: 2rem 0;
329+
color: var(--muted);
330+
font-size: 0.8125rem;
331+
font-weight: 400;
332+
text-align: center;
228333
}
229334

230335
@media (max-width: 36rem) {

src/_includes/style/toc.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@
108108
}
109109
}
110110

111+
.toc[hidden] {
112+
display: none !important;
113+
}
114+
111115
.toc-mobile-toggle {
112116
display: none;
113117
}

src/static/script.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,106 @@ function initCodeGroups() {
5858

5959
initCodeGroups();
6060

61+
function initReferenceFilter() {
62+
const filter = document.querySelector("[data-reference-filter]");
63+
const input = filter?.querySelector(".reference-filter-input");
64+
const clearButton = filter?.querySelector(".reference-filter-clear");
65+
const content = filter?.closest(".content");
66+
const empty = content?.querySelector("[data-reference-filter-empty]");
67+
const kind = filter?.dataset.referenceFilter;
68+
69+
if (
70+
!(input instanceof HTMLInputElement) ||
71+
!(clearButton instanceof HTMLButtonElement) ||
72+
!(empty instanceof HTMLElement) ||
73+
!content ||
74+
(kind !== "methods" && kind !== "types")
75+
) return;
76+
77+
function normalize(value) {
78+
return value.normalize("NFKD").toLocaleLowerCase().replace(/[\p{P}\p{S}\s]+/gu, "");
79+
}
80+
81+
const groups = Array.from(content.querySelectorAll(".descr-list"), (list) => {
82+
const heading = list.previousElementSibling?.matches("h2") ? list.previousElementSibling : undefined;
83+
const children = Array.from(list.children);
84+
const entries = [];
85+
86+
for (let index = 0; index < children.length; index++) {
87+
const nameNode = children[index];
88+
const link = nameNode.matches("a[href]") ? nameNode : nameNode.matches("p") ? nameNode.querySelector(":scope > a[href]:only-child") : null;
89+
const description = children[index + 1];
90+
const divider = children[index + 2];
91+
if (!link || !description || !divider?.classList.contains("descr-list-border")) continue;
92+
93+
entries.push({
94+
nodes: [nameNode, description, divider],
95+
name: normalize(link.textContent),
96+
});
97+
index += 2;
98+
}
99+
100+
return { heading, list, entries };
101+
}).filter((group) => group.entries.length);
102+
const total = groups.reduce((sum, group) => sum + group.entries.length, 0);
103+
const toc = document.querySelector(".toc");
104+
let updateFrame;
105+
106+
if (!total) {
107+
filter.hidden = true;
108+
return;
109+
}
110+
111+
function update() {
112+
updateFrame = undefined;
113+
const rawQuery = input.value.trim();
114+
const terms = rawQuery.split(/\s+/).map(normalize).filter(Boolean);
115+
let matches = 0;
116+
117+
for (const group of groups) {
118+
let groupMatches = 0;
119+
for (const entry of group.entries) {
120+
const visible = terms.every((term) => entry.name.includes(term));
121+
for (const node of entry.nodes) node.hidden = !visible;
122+
if (visible) {
123+
matches++;
124+
groupMatches++;
125+
}
126+
}
127+
128+
const groupVisible = groupMatches > 0;
129+
group.list.hidden = !groupVisible;
130+
if (group.heading) group.heading.hidden = !groupVisible;
131+
}
132+
133+
clearButton.hidden = rawQuery.length === 0;
134+
empty.hidden = matches > 0;
135+
empty.textContent = matches ? "" : `No ${kind} match “${rawQuery}”.`;
136+
if (toc) toc.hidden = terms.length > 0;
137+
globalThis.dispatchEvent(new Event("referencefilterchange"));
138+
}
139+
140+
function scheduleUpdate() {
141+
globalThis.cancelAnimationFrame(updateFrame);
142+
updateFrame = globalThis.requestAnimationFrame(update);
143+
}
144+
145+
input.addEventListener("input", scheduleUpdate);
146+
input.addEventListener("keydown", (event) => {
147+
if (event.key !== "Escape" || !input.value) return;
148+
event.preventDefault();
149+
input.value = "";
150+
update();
151+
});
152+
clearButton.addEventListener("click", () => {
153+
input.value = "";
154+
update();
155+
input.focus({ preventScroll: true });
156+
});
157+
158+
update();
159+
}
160+
61161
function initIndexSubsections() {
62162
const panels = Array.from(document.querySelectorAll(".index-subsections"));
63163
const content = panels[0]?.closest(".content");
@@ -612,6 +712,7 @@ function initToc() {
612712
globalThis.addEventListener("resize", relayout, { passive: true });
613713
globalThis.addEventListener("load", relayout);
614714
globalThis.addEventListener("hashchange", onHashChange);
715+
globalThis.addEventListener("referencefilterchange", relayout);
615716
globalThis.addEventListener("wheel", releaseLockedEntry, { passive: true });
616717
globalThis.addEventListener("touchstart", releaseLockedEntry, { passive: true });
617718
globalThis.addEventListener("pointerdown", releaseLockedEntry, { passive: true });
@@ -627,4 +728,5 @@ function initToc() {
627728
}
628729

629730
initIndexSubsections();
731+
initReferenceFilter();
630732
initToc();

0 commit comments

Comments
 (0)