summaryrefslogtreecommitdiff
path: root/src/components/Search.astro
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/Search.astro')
-rw-r--r--src/components/Search.astro162
1 files changed, 162 insertions, 0 deletions
diff --git a/src/components/Search.astro b/src/components/Search.astro
new file mode 100644
index 0000000..7b98c1a
--- /dev/null
+++ b/src/components/Search.astro
@@ -0,0 +1,162 @@
+---
+// Heavy inspiration taken from Astro Starlight -> https://github.com/withastro/starlight/blob/main/packages/starlight/components/Search.astro
+
+import "@/styles/blocks/search.css";
+---
+
+<site-search class="ms-auto" id="search">
+ <button
+ class="hover:text-accent flex h-9 w-9 cursor-pointer items-center justify-center rounded-md"
+ aria-keyshortcuts="Control+K Meta+K"
+ data-open-modal
+ disabled
+ >
+ <svg
+ aria-hidden="true"
+ class="h-7 w-7"
+ fill="none"
+ height="16"
+ stroke="currentColor"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="1.5"
+ viewBox="0 0 24 24"
+ width="16"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path d="M0 0h24v24H0z" stroke="none"></path>
+ <path d="M3 10a7 7 0 1 0 14 0 7 7 0 1 0-14 0M21 21l-6-6"></path>
+ </svg>
+ <span class="sr-only">Open Search</span>
+ </button>
+ <dialog
+ aria-label="search"
+ class="bg-global-bg h-full max-h-full w-full max-w-full border border-zinc-400 shadow-sm backdrop:backdrop-blur-sm open:flex sm:mx-auto sm:mt-16 sm:mb-auto sm:h-max sm:max-h-[calc(100%-8rem)] sm:min-h-[15rem] sm:w-5/6 sm:max-w-[48rem] sm:rounded-md"
+ >
+ <div class="dialog-frame flex grow flex-col gap-4 p-6 pt-12 sm:pt-6">
+ <button
+ class="ms-auto cursor-pointer rounded-md bg-zinc-200 p-2 font-semibold dark:bg-zinc-700"
+ data-close-modal>Close</button
+ >
+ {
+ import.meta.env.DEV ? (
+ <div class="mx-auto text-center">
+ <p>
+ Search is only available in production builds. <br />
+ Try building and previewing the site to test it out locally.
+ </p>
+ </div>
+ ) : (
+ <div class="search-container">
+ <div id="cactus__search" />
+ </div>
+ )
+ }
+ </div>
+ </dialog>
+</site-search>
+
+<script>
+ class SiteSearch extends HTMLElement {
+ #closeBtn: HTMLButtonElement | null;
+ #dialog: HTMLDialogElement | null;
+ #dialogFrame: HTMLDivElement | null;
+ #openBtn: HTMLButtonElement | null;
+ #controller: AbortController;
+
+ constructor() {
+ super();
+ this.#openBtn = this.querySelector<HTMLButtonElement>("button[data-open-modal]");
+ this.#closeBtn = this.querySelector<HTMLButtonElement>("button[data-close-modal]");
+ this.#dialog = this.querySelector<HTMLDialogElement>("dialog");
+ this.#dialogFrame = this.querySelector(".dialog-frame");
+ this.#controller = new AbortController();
+
+ // Set up events
+ if (this.#openBtn) {
+ this.#openBtn.addEventListener("click", this.openModal);
+ this.#openBtn.disabled = false;
+ } else {
+ console.warn("Search button not found");
+ }
+
+ if (this.#closeBtn) {
+ this.#closeBtn.addEventListener("click", this.closeModal);
+ } else {
+ console.warn("Close button not found");
+ }
+
+ if (this.#dialog) {
+ this.#dialog.addEventListener("close", () => {
+ window.removeEventListener("click", this.onWindowClick);
+ });
+ } else {
+ console.warn("Dialog not found");
+ }
+
+ // only add pagefind in production
+ if (import.meta.env.DEV) return;
+ const onIdle = window.requestIdleCallback || ((cb) => setTimeout(cb, 1));
+ onIdle(async () => {
+ const { PagefindUI } = await import("@pagefind/default-ui");
+ new PagefindUI({
+ baseUrl: import.meta.env.BASE_URL,
+ bundlePath: import.meta.env.BASE_URL.replace(/\/$/, "") + "/pagefind/",
+ element: "#cactus__search",
+ showImages: false,
+ showSubResults: true,
+ });
+ });
+ }
+
+ connectedCallback() {
+ // window events, requires cleanup
+ window.addEventListener("keydown", this.onWindowKeydown, { signal: this.#controller.signal });
+ }
+
+ disconnectedCallback() {
+ this.#controller.abort();
+ }
+
+ openModal = (event?: MouseEvent) => {
+ if (!this.#dialog) {
+ console.warn("Dialog not found");
+ return;
+ }
+
+ this.#dialog.showModal();
+ this.querySelector("input")?.focus();
+ event?.stopPropagation();
+ window.addEventListener("click", this.onWindowClick, { signal: this.#controller.signal });
+ };
+
+ closeModal = () => this.#dialog?.close();
+
+ onWindowClick = (event: MouseEvent) => {
+ // check if it's a link
+ const isLink = "href" in (event.target || {});
+ // make sure the click is either a link or outside of the dialog
+ if (
+ isLink ||
+ (document.body.contains(event.target as Node) &&
+ !this.#dialogFrame?.contains(event.target as Node))
+ ) {
+ this.closeModal();
+ }
+ };
+
+ onWindowKeydown = (e: KeyboardEvent) => {
+ if (!this.#dialog) {
+ console.warn("Dialog not found");
+ return;
+ }
+ // check if it's the Control+K or ⌘+K shortcut
+ if ((e.metaKey === true || e.ctrlKey === true) && e.key === "k") {
+ this.#dialog.open ? this.closeModal() : this.openModal();
+ e.preventDefault();
+ }
+ };
+ }
+
+ customElements.define("site-search", SiteSearch);
+</script>