Thread viewer for Bluesky
at 2.0 280 lines 6.2 kB view raw
1<script lang="ts"> 2 import { api } from '../api.js'; 3 4 export type AutocompleteUser = { 5 did: string; 6 handle: string; 7 avatar?: string; 8 displayName?: string; 9 } 10 11 let { selectedUsers = $bindable([]) }: { selectedUsers: AutocompleteUser[] } = $props(); 12 13 let typedValue = $state(''); 14 let autocompleteResults: AutocompleteUser[] = $state([]); 15 let autocompleteIndex = $state(-1); 16 17 let selectedUserDIDs: string[] = $derived(selectedUsers.map(u => u.did)); 18 let autocompleteVisible = $derived(autocompleteResults.length > 0); 19 let autocompleteVerticalOffset = $state(0); 20 21 let autocompleteTimer: number | undefined; 22 23 $effect(() => { 24 let html = document.body.parentNode! 25 html.addEventListener('click', hideAutocomplete); 26 27 return () => { 28 html.removeEventListener('click', hideAutocomplete); 29 }; 30 }); 31 32 function onTextInput() { 33 if (autocompleteTimer) { 34 clearTimeout(autocompleteTimer); 35 } 36 37 let query = typedValue.trim(); 38 39 if (query.length > 0) { 40 autocompleteTimer = setTimeout(() => fetchAutocomplete(query), 100); 41 } else { 42 hideAutocomplete(); 43 autocompleteTimer = undefined; 44 } 45 } 46 47 function onKeyPress(e: KeyboardEvent) { 48 if (e.key == 'Enter') { 49 e.preventDefault(); 50 51 if (autocompleteIndex >= 0) { 52 selectUser(autocompleteIndex); 53 } 54 } else if (e.key == 'Escape') { 55 hideAutocomplete(); 56 } else if (e.key == 'ArrowDown' && autocompleteResults.length > 0) { 57 e.preventDefault(); 58 moveAutocomplete(+1); 59 } else if (e.key == 'ArrowUp' && autocompleteResults.length > 0) { 60 e.preventDefault(); 61 moveAutocomplete(-1); 62 } 63 } 64 65 async function fetchAutocomplete(query: string) { 66 let users = await api.autocompleteUsers(query) as AutocompleteUser[]; 67 68 let selectedDIDs = new Set(selectedUserDIDs); 69 users = users.filter(u => !selectedDIDs.has(u.did)); 70 71 if (users.length > 0) { 72 autocompleteResults = users; 73 autocompleteIndex = 0; 74 } else { 75 hideAutocomplete(); 76 } 77 } 78 79 function hideAutocomplete() { 80 autocompleteResults = []; 81 autocompleteIndex = -1; 82 } 83 84 function moveAutocomplete(change: 1 | -1) { 85 if (autocompleteResults.length == 0) { 86 return; 87 } 88 89 let newIndex = autocompleteIndex + change; 90 91 if (newIndex < 0) { 92 newIndex = autocompleteResults.length - 1; 93 } else if (newIndex >= autocompleteResults.length) { 94 newIndex = 0; 95 } 96 97 autocompleteIndex = newIndex; 98 } 99 100 function selectAutocomplete(e: MouseEvent, index: number) { 101 e.preventDefault(); 102 selectUser(index); 103 } 104 105 function selectUser(index: number) { 106 let user = autocompleteResults[index]; 107 108 if (!user) { 109 return; 110 } 111 112 selectedUsers.push(user); 113 typedValue = ''; 114 hideAutocomplete(); 115 } 116 117 function removeUser(e: MouseEvent, index: number) { 118 e.preventDefault(); 119 selectedUsers.splice(index, 1); 120 } 121</script> 122 123<div class="user-choice"> 124 <input type="text" placeholder="Add user" autocomplete="off" autofocus 125 oninput={onTextInput} 126 onkeydown={onKeyPress} 127 bind:value={typedValue} 128 bind:offsetHeight={autocompleteVerticalOffset}> 129 130 {#if autocompleteVisible} 131 <div class="autocomplete" 132 style:display={autocompleteVisible ? 'block' : 'none'} 133 style:top="{autocompleteVerticalOffset}px"> 134 135 {#each autocompleteResults as user, i (user.did)} 136 <div class="user-row" 137 class:highlighted={autocompleteIndex == i} 138 onmouseenter={() => { autocompleteIndex = i }} 139 onmousedown={(e) => { selectAutocomplete(e, i) }}> 140 {@render userRow(user)} 141 </div> 142 {/each} 143 </div> 144 {/if} 145 146 <div class="selected-users"> 147 {#each selectedUsers as user, i (user.did)} 148 <div class="user-row"> 149 {@render userRow(user)} 150 <a class="remove" href="#" onclick={(e) => { removeUser(e, i) }}>✕</a> 151 </div> 152 {/each} 153 </div> 154</div> 155 156{#snippet userRow(user: AutocompleteUser)} 157 <img class="avatar" alt="Avatar" src={user.avatar}> 158 <span class="name">{user.displayName || '–'}</span> 159 <span class="handle">{user.handle}</span> 160{/snippet} 161 162<style> 163 .user-choice { 164 position: relative; 165 } 166 167 input { 168 width: 260px; 169 font-size: 11pt; 170 } 171 172 .autocomplete { 173 position: absolute; 174 left: 0; 175 top: 0; 176 margin-top: 4px; 177 width: 350px; 178 max-height: 250px; 179 overflow-y: auto; 180 background-color: white; 181 border: 1px solid #ccc; 182 z-index: 10; 183 } 184 185 .selected-users { 186 width: 275px; 187 height: 150px; 188 overflow-y: auto; 189 border: 1px solid #aaa; 190 padding: 4px; 191 margin-top: 20px; 192 } 193 194 .user-row { 195 position: relative; 196 padding: 2px 4px 2px 37px; 197 cursor: pointer; 198 } 199 200 .user-row .avatar { 201 position: absolute; 202 left: 6px; 203 top: 8px; 204 width: 24px; 205 border-radius: 12px; 206 } 207 208 .user-row span { 209 display: block; 210 overflow-x: hidden; 211 text-overflow: ellipsis; 212 } 213 214 .user-row .name { 215 font-size: 11pt; 216 margin-top: 1px; 217 margin-bottom: 1px; 218 } 219 220 .user-row .handle { 221 font-size: 10pt; 222 margin-bottom: 2px; 223 color: #666; 224 } 225 226 .autocomplete .user-row { 227 cursor: pointer; 228 } 229 230 .autocomplete .user-row.highlighted { 231 background-color: hsl(207, 100%, 85%); 232 } 233 234 .selected-users .user-row span { 235 padding-right: 14px; 236 } 237 238 .selected-users .user-row .remove { 239 position: absolute; 240 right: 4px; 241 top: 11px; 242 padding: 0px 4px; 243 color: #333; 244 line-height: 17px; 245 } 246 247 .selected-users .user-row .remove:hover { 248 text-decoration: none; 249 background-color: #ddd; 250 border-radius: 8px; 251 } 252 253 @media (prefers-color-scheme: dark) { 254 .autocomplete { 255 background-color: hsl(210, 5%, 18%); 256 border-color: #4b4b4b; 257 } 258 259 .selected-users { 260 border-color: #666; 261 } 262 263 .user-row .handle { 264 color: #888; 265 } 266 267 .autocomplete .user-row.highlighted { 268 background-color: hsl(207, 90%, 25%); 269 } 270 271 .selected-users .user-row .remove { 272 color: #aaa; 273 } 274 275 .selected-users .user-row .remove:hover { 276 background-color: #555; 277 color: #bbb; 278 } 279 } 280</style>