|
<script lang="ts"> |
|
import Search_Worker from "./search-worker?worker"; |
|
import SearchIcon from "./SearchIcon.svelte"; |
|
import { onNavigate } from "$app/navigation"; |
|
import type { Result } from "./search"; |
|
import { browser } from "$app/environment"; |
|
|
|
let search: "idle" | "load" | "ready" = "idle"; |
|
let search_term = ""; |
|
let results: Result[] = []; |
|
let search_worker: Worker; |
|
|
|
function initialize() { |
|
open = true; |
|
if (search === "ready") return; |
|
search = "load"; |
|
search_worker = new Search_Worker(); |
|
search_worker.addEventListener("message", (e) => { |
|
const { type, payload } = e.data; |
|
type === "ready" && (search = "ready"); |
|
type === "results" && (results = payload.results); |
|
}); |
|
search_worker.postMessage({ type: "load" }); |
|
} |
|
|
|
let open: boolean = false; |
|
|
|
onNavigate(() => { |
|
open = false; |
|
}); |
|
|
|
$: if (search === "ready") { |
|
search_worker.postMessage({ type: "search", payload: { search_term } }); |
|
} |
|
|
|
$: if (search_term && !open) { |
|
search_term = ""; |
|
} |
|
|
|
let content_elem: HTMLElement; |
|
let search_button_elem: HTMLElement; |
|
|
|
function focus_input(el: HTMLInputElement) { |
|
el.focus(); |
|
} |
|
|
|
function get_os(): string { |
|
|
|
if (typeof navigator !== "undefined") { |
|
const userAgent = navigator.userAgent.toLowerCase(); |
|
if (userAgent.indexOf("win") > -1) return "Windows"; |
|
if (userAgent.indexOf("mac") > -1) return "MacOS"; |
|
if (userAgent.indexOf("linux") > -1) return "Linux"; |
|
if (userAgent.indexOf("android") > -1) return "Android"; |
|
if (userAgent.indexOf("iphone") > -1 || userAgent.indexOf("ipad") > -1) |
|
return "iOS"; |
|
} |
|
return "Unknown"; |
|
} |
|
|
|
let meta_key = "⌘"; |
|
|
|
$: if (browser && navigator) { |
|
let os = get_os(); |
|
meta_key = os === "MacOS" || os === "iOS" ? "⌘" : "CTRL+"; |
|
} |
|
|
|
function handle_key_down(e: KeyboardEvent): void { |
|
if (e.ctrlKey || e.metaKey) { |
|
if (e.key === "k" || e.key === "K") { |
|
e.preventDefault(); |
|
initialize(); |
|
} |
|
} |
|
if (e.key === "Escape") { |
|
open = false; |
|
} |
|
if ((e.key === "ArrowUp" || e.key === "ArrowDown") && open) { |
|
e.preventDefault(); |
|
const current = document.activeElement; |
|
const items = [...document.getElementsByClassName("res-block")]; |
|
|
|
const current_index = current ? items.indexOf(current) : -1; |
|
let new_index; |
|
if (current_index === -1) { |
|
new_index = 1; |
|
} else { |
|
if (e.key === "ArrowUp") { |
|
new_index = (current_index + items.length - 1) % items.length; |
|
} else { |
|
new_index = (current_index + 1) % items.length; |
|
} |
|
} |
|
|
|
(current as HTMLElement).blur(); |
|
|
|
const newElement = items[new_index] as HTMLElement; |
|
if (newElement) { |
|
newElement.focus(); |
|
document |
|
.querySelectorAll(".res-block") |
|
.forEach((el) => el.classList.remove("first-res")); |
|
} |
|
} |
|
const search_input = document.getElementById( |
|
"search-input" |
|
) as HTMLInputElement; |
|
const first_result = document.querySelector(".res-block") as HTMLElement; |
|
if (e.key === "Enter" && document.activeElement === search_input) { |
|
first_result.click(); |
|
} |
|
} |
|
|
|
function on_click(e: MouseEvent) { |
|
if (content_elem) { |
|
if (!content_elem.contains(e.target as Node) && open) { |
|
open = false; |
|
} |
|
} else { |
|
if (search_button_elem.contains(e.target as Node)) { |
|
initialize(); |
|
} |
|
} |
|
} |
|
|
|
$: if (browser && document.querySelector(".res-block")) { |
|
document.querySelector(".res-block")?.classList.add("first-res"); |
|
} |
|
</script> |
|
|
|
<svelte:window on:keydown={handle_key_down} on:click={on_click} /> |
|
|
|
<button class="search-button" bind:this={search_button_elem}> |
|
<SearchIcon /> |
|
<span class="pl-1 pr-5">Search</span> |
|
<div class="shortcut"> |
|
<div class="text-sm"> |
|
{meta_key}K |
|
</div> |
|
</div> |
|
</button> |
|
|
|
{#if open} |
|
<div class="overlay" /> |
|
<div class="content" bind:this={content_elem}> |
|
<div class="search-bar"> |
|
{#if search === "load"} |
|
<div class="loader"></div> |
|
{:else} |
|
<SearchIcon /> |
|
{/if} |
|
<input |
|
bind:value={search_term} |
|
placeholder="What are you searching for?" |
|
autocomplete="off" |
|
autocorrect="off" |
|
autocapitalize="off" |
|
enterkeyhint="go" |
|
maxlength="64" |
|
spellcheck="false" |
|
type="search" |
|
use:focus_input |
|
id="search-input" |
|
/> |
|
<button |
|
on:click={() => { |
|
open = false; |
|
}} |
|
class="text-xs font-semibold rounded-md p-1 border-gray-300 border" |
|
> |
|
ESC |
|
</button> |
|
</div> |
|
<div class="results"> |
|
{#if results.length} |
|
<ul> |
|
{#each results as result, i} |
|
{#if result.content.length > 0} |
|
<li> |
|
<a |
|
class="res-block" |
|
class:first-res={i === 0} |
|
href={result.slug} |
|
> |
|
<p |
|
class:text-green-700={result.type == "DOCS"} |
|
class:bg-green-100={result.type == "DOCS"} |
|
class:text-orange-700={result.type == "GUIDE"} |
|
class:bg-orange-100={result.type == "GUIDE"} |
|
class="float-left text-xs font-semibold rounded-md p-1 px-2 mx-1 mt-[3px]" |
|
> |
|
{result.type} |
|
</p> |
|
<div class="float-right"> |
|
<div class="enter">↵</div> |
|
</div> |
|
<p>{@html result.title}</p> |
|
<ol> |
|
{#each result.content as content} |
|
<li class="res-content">{@html content}</li> |
|
{/each} |
|
</ol> |
|
</a> |
|
</li> |
|
{/if} |
|
{/each} |
|
</ul> |
|
{:else} |
|
{#if search_term} |
|
{#if search === "load"} |
|
<p class="mx-auto w-fit text-gray-500">Searching for results...</p> |
|
{:else} |
|
<p class="mx-auto w-fit text-gray-500"> |
|
No results found. Try using a different term. |
|
</p> |
|
{/if} |
|
{/if} |
|
<ul> |
|
<p class="">Suggestions</p> |
|
<li> |
|
<a class="res-block first-res" href="/quickstart"> |
|
<p |
|
class="float-left text-xs mx-1 font-semibold text-orange-700 bg-orange-100 rounded-md p-1 px-2 mt-[3px]" |
|
> |
|
GUIDE |
|
</p> |
|
<div class="float-right"> |
|
<div class="enter">↵</div> |
|
</div> |
|
<p>Quickstart</p> |
|
</a> |
|
</li> |
|
<li> |
|
<a class="res-block" href="/docs/gradio/interface"> |
|
<p |
|
class="float-left text-xs font-semibold text-green-700 bg-green-100 rounded-md p-1 px-2 mx-1 mt-[3px]" |
|
> |
|
DOCS |
|
</p> |
|
<div class="float-right"> |
|
<div class="enter">↵</div> |
|
</div> |
|
<p>Interface</p> |
|
</a> |
|
</li> |
|
<li> |
|
<a class="res-block" href="/docs/gradio/blocks"> |
|
<p |
|
class="float-left text-xs font-semibold text-green-700 bg-green-100 rounded-md p-1 px-2 mx-1 mt-[3px]" |
|
> |
|
DOCS |
|
</p> |
|
<div class="float-right"> |
|
<div class="enter">↵</div> |
|
</div> |
|
<p>Blocks</p> |
|
</a> |
|
</li> |
|
</ul> |
|
{/if} |
|
</div> |
|
</div> |
|
{/if} |
|
|
|
<style> |
|
.overlay { |
|
@apply fixed inset-0 z-30 backdrop-blur-sm bg-black/20; |
|
} |
|
|
|
.search-bar { |
|
@apply font-sans text-lg z-10 px-4 relative flex flex-none items-center border-b border-gray-100 text-gray-500; |
|
} |
|
|
|
.search-bar input { |
|
@apply text-lg appearance-none h-14 text-black mx-1 flex-auto min-w-0 border-none cursor-text; |
|
outline: none; |
|
box-shadow: none; |
|
} |
|
|
|
.content { |
|
@apply fixed left-1/2 top-[7%] -translate-x-1/2 mx-auto w-[95vw] max-w-3xl flex flex-col min-h-0 rounded-lg shadow-2xl bg-white z-40; |
|
} |
|
|
|
.results { |
|
@apply p-5 overflow-y-auto; |
|
max-height: 60vh; |
|
scrollbar-width: thin; |
|
|
|
& ol { |
|
margin-block-start: 2px; |
|
} |
|
|
|
& li:not(:last-child) { |
|
margin-block-end: 4px; |
|
padding-block-end: 4px; |
|
} |
|
|
|
& a { |
|
display: block; |
|
} |
|
} |
|
|
|
.search-button { |
|
@apply flex flex-row rounded-full items-center cursor-pointer px-2 text-gray-400 border-gray-300 border text-lg outline-none font-sans; |
|
} |
|
|
|
:global(.res-content .mark) { |
|
color: #ff7c00; |
|
text-decoration: underline; |
|
} |
|
:global(.res-content) { |
|
@apply text-gray-500; |
|
} |
|
:global(.res-block) { |
|
@apply m-2 p-2 border border-gray-100 rounded-md bg-gray-50 hover:bg-gray-100 hover:scale-[1.01] focus:bg-gray-100 focus:scale-[1.01] focus:outline-none; |
|
} |
|
:global(.first-res) { |
|
@apply bg-gray-100 scale-[1.01]; |
|
} |
|
|
|
:global(.res-block:focus .enter) { |
|
display: block !important; |
|
} |
|
:global(.first-res .enter) { |
|
display: block !important; |
|
} |
|
|
|
.enter { |
|
display: none; |
|
} |
|
|
|
.enter { |
|
@apply text-xs font-semibold rounded-md p-1 border-gray-300 border text-gray-500 font-sans bg-white; |
|
} |
|
|
|
.loader { |
|
border: 1px solid #d0cfcf; |
|
border-top: 2px solid #475469; |
|
border-radius: 50%; |
|
width: 15px; |
|
height: 15px; |
|
animation: spin 1.2s linear infinite; |
|
} |
|
|
|
@keyframes spin { |
|
0% { |
|
transform: rotate(0deg); |
|
} |
|
100% { |
|
transform: rotate(360deg); |
|
} |
|
} |
|
</style> |
|
|