Coves frontend - a photon fork
at main 121 lines 3.1 kB view raw
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>