diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-03 10:56:21 +0300 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-03 10:56:21 +0300 |
| commit | 456cf011b36de91c9936994b1fa45703adcd309b (patch) | |
| tree | 8e60daf998f731ac50d100fa490eaecae1168042 /src/components/Search.astro | |
Initial fork of chrismwilliams/astro-theme-cactus theme
Diffstat (limited to 'src/components/Search.astro')
| -rw-r--r-- | src/components/Search.astro | 162 |
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> |
