Skip to content

Instantly share code, notes, and snippets.

@zadeviggers
Last active February 22, 2022 03:21
Show Gist options
  • Save zadeviggers/b52b968346fbb20b6f3f858576dc499d to your computer and use it in GitHub Desktop.
Save zadeviggers/b52b968346fbb20b6f3f858576dc499d to your computer and use it in GitHub Desktop.
The code for my search bar (https://publictransportforum.nz/articles). Made in SolidJS (with FuseJS for fuzzy matching) to be used in an Astro site. Note that this code is ripped straight from my source so it might need a bit of tweaking to make it work for you.
// Here are some snippets from the file that loads all my article's markdown files and does processing on them
// including generating the json file for search
import * as fs from "fs";
import path from "path";
const publicDirectory = path.join(process.cwd(), "public");
// At the bottom of the function that populates the cache of articles...
const searchFile = JSON.stringify(
articles.map((article) => ({
t: article.meta.unsafeTitle,
d: article.meta.unsafeDescription,
u: article.URL.linkHref,
ts: article.meta.tags.map((tag) => tag.formattedText).join(" "),
e: !!article.external,
})),
);
fs.writeFileSync(path.join(publicDirectory, "search.json"), searchFile);
// Search bar component, made in SolidJS
// Make sure to add @withastro/renderer-solid to the renders key in your Astro config
// Also make sire to install solid-js and fuse.js
import {
For,
createEffect,
createResource,
createSignal,
onCleanup,
onMount,
} from "solid-js";
import Fuse from "fuse.js";
const ExternalLinkIconSVG = () => (
<svg width="1em" height="1em" viewBox="0 0 24 24" class="external-link-icon">
<g
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</g>
</svg>
);
const highlightResult = (
key: string,
text: string,
rawMatches?: { key: string; indices: [number, number][] }[],
): string => {
if (!rawMatches) return text;
let highlighted = [];
let matches = rawMatches.find((m) => m.key === key)?.indices || [];
let pair = matches.shift();
for (let i = 0; i < text.length; i++) {
const char = text.charAt(i);
if (pair && i == pair[0]) {
highlighted.push("<strong>");
}
highlighted.push(char);
if (pair && i == pair[1]) {
highlighted.push("</strong>");
pair = matches.shift();
}
}
return highlighted.join("");
};
export default function SearchBar() {
const [value, setValue] = createSignal("");
const [wrapper, setWrapper] = createSignal<HTMLDivElement>();
const [open, setOpen] = createSignal(false);
const [items] = createResource<
{
t: string;
d: string;
u: string;
ts: string;
e: boolean;
}[]
>(async () => {
const res = await fetch("/search.json");
const data = await res.json();
return data;
});
const fuse = () =>
items.loading
? null
: new Fuse(items(), {
keys: [
{ name: "t", weight: 0.8 },
{ name: "ts", weight: 0.5 },
{ name: "d", weight: 0.4 },
],
includeMatches: true,
threshold: 0.7,
useExtendedSearch: true,
});
// Close on click outside
const onDocumentClick = (e) => {
if (!wrapper().contains(e.currentTarget)) {
setOpen(false);
}
};
onMount(() => document.addEventListener("click", onDocumentClick));
onCleanup(() => document.removeEventListener("click", onDocumentClick));
const onInput = (event) => {
setValue(event.target.value);
};
createEffect(() => {
if (value().length > 0) {
setOpen(true);
} else {
setOpen(false);
}
});
const searchResults = () => fuse()?.search(value()).slice(0, 11);
return (
<div class="searchbar-wrapper" id="searchbar-wrapper" ref={setWrapper}>
<label for="searchbar" class="searchbar">
Search:{" "}
<input
value={value()}
onInput={onInput}
type="text"
id="searchbar"
aria-expanded={open() ? "true" : "false"}
aria-controls="search-results"
/>
</label>
<div
id="search-results"
class="results"
style={`display: ${open() ? "block" : "none"};`}>
<ul>
<For
each={searchResults()}
fallback={
<li>
<p>No results found </p>
</li>
}
children={(result) => (
<li>
<a
href={result.item.u}
target={result.item.e ? "_blank" : null}>
{result.item.e && <ExternalLinkIconSVG />}
<span
innerHTML={highlightResult(
"t",
result.item.t,
result.matches as any,
)}
/>
</a>
</li>
)}
/>
</ul>
</div>
</div>
);
}
/* The styles for the searchbar */
/* --spacing is set to 8px on :root */
.searchbar-wrapper {
margin-top: calc(var(--spacing) * 4);
--width: min(90vw, calc(var(--spacing) * 40));
--wider-width: min(90vw, calc(var(--spacing) * 60));
--highlight-colour: var(--rust);
--background-colour: var(--slate);
--text-colour: var(--white);
--hover-colour: var(--light-slate);
width: var(--width);
position: relative;
}
@media (prefers-color-scheme: dark) {
.searchbar-wrapper {
--highlight-colour: var(--blue);
--background-colour: var(--slate);
--text-colour: var(--white);
--hover-colour: var(--light-slate);
}
}
.searchbar-wrapper > .searchbar,
.searchbar-wrapper > .results {
background-color: var(--background-colour);
border-radius: var(--spacing);
padding-top: var(--spacing);
padding-bottom: var(--spacing);
color: var(--text-colour);
}
.searchbar-wrapper > .searchbar {
width: var(--width);
display: flex;
padding-left: var(--spacing);
padding-right: var(--spacing);
border: 2px solid transparent;
}
.searchbar-wrapper > .searchbar:focus-within {
border-color: var(--highlight-colour);
}
.searchbar-wrapper > .searchbar input {
margin-left: var(--spacing);
background-color: transparent;
color: var(--text-colour);
flex: 1;
}
.searchbar-wrapper > .searchbar input:focus {
outline: none;
}
.searchbar-wrapper > .results {
width: var(--wider-width);
overflow: hidden;
position: absolute;
top: calc(var(--spacing) * 6);
left: calc(-1 * calc(calc(var(--wider-width) - var(--width)) / 2));
}
.searchbar-wrapper > .results > ul {
list-style-type: none;
display: flex;
flex-direction: column;
gap: var(--spacing);
}
.searchbar-wrapper > .results > ul > li {
margin: 0;
display: flex;
flex-direction: column;
}
.searchbar-wrapper > .results > ul > li::before {
display: none;
}
.searchbar-wrapper > .results > ul > li > * {
color: var(--text-colour);
display: flex;
align-items: center;
gap: var(--spacing);
padding: var(--spacing);
}
.searchbar-wrapper > .results > ul > li > a > .external-link-icon {
stroke: var(--text-colour);
stroke-width: 2px;
}
.searchbar-wrapper > .results > ul > li > a:hover,
.searchbar-wrapper > .results > ul > li > a:focus {
background-color: var(--hover-colour);
text-decoration: none;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment