Skip to content

Commit 5c6d7bb

Browse files
yashrajbhartiyashrajbharticybtekk
andauthored
Add Search Functionality to Filter Sample List (#524)
## Summary This PR introduces a search bar to the WebGPU samples page, enabling users to filter the list of available samples in real time based on their query. New <input type="search"> added to the page. • Input value synced with the ?q= query parameter in the URL. • Filtering logic applies on both initial load and live input changes. • Filters against: • Sample title • tocName • Category title Fixes #379 Open to suggestions on implementation, UX, or improvements! https://github.com/user-attachments/assets/00284135-a95e-42af-998d-11516bfaa7b6 --------- Co-authored-by: yashrajbharticybtekk <[email protected]>
1 parent b28ddf7 commit 5c6d7bb

File tree

3 files changed

+145
-34
lines changed

3 files changed

+145
-34
lines changed

index.html

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,27 @@
3333
<h1>
3434
<a href="./">WebGPU Samples</a>
3535
</h1>
36+
<search>
37+
<hr />
38+
<label>
39+
<svg
40+
width="24px"
41+
height="24px"
42+
viewBox="0 0 1024 1024"
43+
class="icon"
44+
version="1.1"
45+
xmlns="http://www.w3.org/2000/svg"
46+
>
47+
<title>Search</title>
48+
<path
49+
d="M448 768A320 320 0 1 0 448 128a320 320 0 0 0 0 640z m297.344-76.992l214.592 214.592-54.336 54.336-214.592-214.592a384 384 0 1 1 54.336-54.336z"
50+
fill="currentColor"
51+
/>
52+
</svg>
53+
<input type="search" name="search" />
54+
</label>
55+
<hr />
56+
</search>
3657
<input type="checkbox" id="menuToggle">
3758
<label class="expand" for="menuToggle"></label>
3859
<div class="panelContents">

public/css/MainLayout.css

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,47 @@ main {
6464
overflow: auto;
6565
}
6666

67+
search {
68+
inline-size: 100%;
69+
margin-block-end: 20px;
70+
71+
& > hr {
72+
border-color: var(--tooltip-border);
73+
margin-block: 0;
74+
}
75+
76+
& > label {
77+
display: flex;
78+
justify-content: flex-start;
79+
align-items: center;
80+
gap: 10px;
81+
inline-size: 100%;
82+
block-size: 50px;
83+
84+
&:has(input:focus) > svg {
85+
display: none;
86+
}
87+
88+
& > svg {
89+
margin-inline-start: 10px;
90+
}
91+
92+
& > input {
93+
opacity: 0;
94+
border: none;
95+
block-size: 100%;
96+
flex: 1;
97+
padding-inline: 20px;
98+
background-color: var(--panel-background);
99+
font-size: 1rem;
100+
101+
&:is(:focus) {
102+
opacity: 1;
103+
}
104+
}
105+
}
106+
}
107+
67108
@media only screen and (max-width: 768px) {
68109
.wrapper {
69110
flex-direction: column;

src/main.ts

Lines changed: 83 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -273,47 +273,96 @@ function setSampleIFrameURL(e: PointerEvent, sampleInfo: SampleInfo) {
273273
setSampleIFrame(sampleInfo);
274274
}
275275

276+
const searchInput = document.querySelector(
277+
'input[type="search"]'
278+
) as HTMLInputElement;
279+
280+
let debounceTimer: number | undefined;
281+
282+
searchInput.addEventListener('input', () => {
283+
clearTimeout(debounceTimer);
284+
debounceTimer = window.setTimeout(() => {
285+
const q = searchInput.value.trim();
286+
const url = new URL(window.location.href);
287+
288+
if (q) {
289+
url.searchParams.set('q', q);
290+
} else {
291+
url.searchParams.delete('q');
292+
}
293+
294+
// Do NOT touch 'sample' param — we preserve it
295+
history.replaceState(null, '', url.toString());
296+
297+
// Re-render the list
298+
sampleListElem.innerHTML = '';
299+
samplesByKey.clear();
300+
renderSampleList();
301+
}, 200); // debounce delay
302+
});
303+
276304
// Samples are looked up by `?sample=key` so this is a map
277305
// from those keys to each sample.
278306
const samplesByKey = new Map<string, SampleInfo>();
279307

308+
renderSampleList();
309+
280310
// Generate the list of samples
281-
for (const { title, description, samples } of pageCategories) {
282-
for (const [key, sampleInfo] of Object.entries(samples)) {
283-
samplesByKey.set(key, sampleInfo);
284-
}
311+
function renderSampleList() {
312+
const url = new URL(window.location.href);
313+
const q = url.searchParams.get('q')?.toLowerCase().trim() || '';
314+
315+
sampleListElem.innerHTML = '';
316+
samplesByKey.clear();
317+
318+
for (const { title, description, samples } of pageCategories) {
319+
const filteredSamples = Object.entries(samples).filter(([, sample]) => {
320+
if (!q) return true;
321+
const name = sample.name?.toLowerCase() || '';
322+
const tocName = sample.tocName?.toLowerCase() || '';
323+
const titleMatch = title.toLowerCase().includes(q);
324+
return name.includes(q) || tocName.includes(q) || titleMatch;
325+
});
285326

286-
sampleListElem.appendChild(
287-
el('ul', { className: 'exampleList' }, [
288-
el('div', {}, [
289-
el('div', { className: 'sampleCategory' }, [
290-
el('h3', {
291-
style: { 'margin-top': '5px' },
292-
textContent: title,
293-
dataset: { tooltip: description },
294-
}),
295-
]),
296-
...Object.entries(samples).map(([key, sampleInfo]) =>
297-
el('li', {}, [
298-
el('a', {
299-
href: sampleInfo.external
300-
? sampleInfo.external.url
301-
: sampleInfo.filename,
302-
...(!sampleInfo.openInNewTab && {
303-
onClick: (e: PointerEvent) => {
304-
setSampleIFrameURL(e, sampleInfo);
305-
},
306-
}),
307-
textContent: `${sampleInfo.tocName || key}${
308-
sampleInfo.openInNewTab ? ' ↗️' : ''
309-
}`,
310-
...(sampleInfo.openInNewTab && { target: '_blank' }),
327+
// Skip categories with no matches
328+
if (filteredSamples.length === 0) continue;
329+
330+
for (const [key, sampleInfo] of filteredSamples) {
331+
samplesByKey.set(key, sampleInfo);
332+
}
333+
334+
sampleListElem.appendChild(
335+
el('ul', { className: 'exampleList' }, [
336+
el('div', {}, [
337+
el('div', { className: 'sampleCategory' }, [
338+
el('h3', {
339+
style: { 'margin-top': '5px' },
340+
textContent: title,
341+
dataset: { tooltip: description },
311342
}),
312-
])
313-
),
314-
]),
315-
])
316-
);
343+
]),
344+
...filteredSamples.map(([key, sampleInfo]) =>
345+
el('li', {}, [
346+
el('a', {
347+
href: sampleInfo.external
348+
? sampleInfo.external.url
349+
: sampleInfo.filename,
350+
...(!sampleInfo.openInNewTab && {
351+
onClick: (e: PointerEvent) => {
352+
setSampleIFrameURL(e, sampleInfo);
353+
},
354+
}),
355+
textContent: `${sampleInfo.tocName || key}${
356+
sampleInfo.openInNewTab ? ' ↗️' : ''
357+
}`,
358+
...(sampleInfo.openInNewTab && { target: '_blank' }),
359+
}),
360+
])
361+
),
362+
]),
363+
])
364+
);
365+
}
317366
}
318367

319368
sourceLElem.addEventListener('click', () => switchToRelativeTab(-1).click());

0 commit comments

Comments
 (0)