Coves frontend - a photon fork
1<script lang="ts" module>
2 import { Menu, MenuButton, Spinner, TextInput } from 'mono-svelte'
3 import type { TextInputProps } from 'mono-svelte/forms/TextInput.svelte'
4 import { debounce } from 'mono-svelte/util/time'
5 import { Icon, MagnifyingGlass } from 'svelte-hero-icons/dist'
6
7 interface Props<T> extends Omit<TextInputProps, 'onselect' | 'children'> {
8 query?: string
9 selected?: T | undefined
10 search: (query: string) => Promise<T[]>
11 extractName: (item: T) => string
12 select?: (item: T) => void
13 input?: import('svelte').Snippet
14 noresults?: import('svelte').Snippet
15 required?: boolean
16 children?: import('svelte').Snippet<
17 [
18 {
19 select: (item: T) => void
20 item: T
21 extractName: (item: T) => string
22 },
23 ]
24 >
25 onselect?: (value?: T) => void
26 oninput?: TextInputProps['oninput']
27 }
28
29 export type { Props as SearchProps }
30</script>
31
32<script lang="ts" generics="T">
33 let items: T[] = $state([])
34
35 /**
36 * This is here so that the menu doesn't open as soon as it's mounted.
37 */
38 let openMenu = $state(false)
39 let searching = $state(false)
40
41 let {
42 query = $bindable(''),
43 selected = $bindable(undefined),
44 search,
45 extractName,
46 select = (item: T) => {
47 selected = item
48 query = extractName(item)
49 onselect?.(item)
50 },
51 required,
52 input,
53 noresults,
54 children,
55 onselect,
56 oninput,
57 ...rest
58 }: Props<T> = $props()
59
60 const debounceFunc = debounce(async () => {
61 searching = true
62 openMenu = true
63 items = await search(query)
64 searching = false
65 })
66</script>
67
68<div class="relative">
69 <Menu bind:open={openMenu}>
70 {#snippet target(attachment)}
71 {#if input}{@render input()}{:else}
72 <TextInput
73 {@attach attachment}
74 bind:value={query}
75 oninput={(e) => {
76 searching = true
77 openMenu = true
78 oninput?.(e)
79 debounceFunc()
80 }}
81 onfocus={(e) => {
82 searching = true
83 openMenu = true
84 oninput?.(e)
85 debounceFunc()
86 }}
87 {required}
88 {...rest}
89 inlineAffixes
90 >
91 {#snippet prefix()}
92 <div class="h-5 flex items-center">
93 <Icon src={MagnifyingGlass} mini size="16" />
94 </div>
95 {/snippet}
96 </TextInput>
97 {/if}
98 {/snippet}
99 {#if searching}
100 <div class="w-full h-24 grid place-items-center">
101 <Spinner width={24} />
102 </div>
103 {:else if items.length == 0}
104 <div class="text-center h-24 grid place-items-center">
105 {#if noresults}{@render noresults()}{:else}No results found.{/if}
106 </div>
107 {:else}
108 {#each items as item (item)}
109 {#if children}{@render children({
110 extractName,
111 item,
112 select,
113 })}{:else}
114 <MenuButton onclick={() => select(item)}>
115 {extractName(item)}
116 </MenuButton>
117 {/if}
118 {/each}
119 {/if}
120 </Menu>
121</div>