frontend for xcvr appview

some emoji list stuff

+105 -9
+105 -9
src/lib/components/AutoGrowTextArea.svelte
··· 2 2 import { onMount } from "svelte"; 3 3 import Fuse from "fuse.js"; 4 4 import emojis from "$lib/keyword-emojis.json"; 5 + import { computePosition, flip } from "@floating-ui/dom"; 5 6 6 7 interface Props { 7 8 onBeforeInput?: (event: InputEvent) => void; ··· 24 25 color, 25 26 fs, 26 27 }: Props = $props(); 27 - let curemoji: [string, number, number] | null = $state(null); 28 + let curemojiresults: [EmojiSearchResults, number, number] | null = 29 + $state(null); 30 + let curemojinumber: null | number = $state(null); 31 + let curemoji: null | string = $derived( 32 + curemojiresults && curemojinumber 33 + ? curemojiresults[0][curemojinumber] 34 + : null, 35 + ); 28 36 29 37 let inputEl: HTMLTextAreaElement; 38 + let emojilist: HTMLElement | undefined = $state(); 30 39 function adjust(event: Event) { 31 40 onInputEl?.(inputEl); 32 - curemoji = checkAndSearch(); 41 + curemojiresults = checkAndSearch(); 42 + if (curemojiresults !== null) { 43 + curemojinumber = 0; 44 + } 33 45 } 34 46 35 47 function bi(event: InputEvent) { ··· 79 91 return [res, colonPos, selectionStart]; 80 92 } 81 93 const fuseOptions = { 94 + includeMatches: true, 82 95 keys: ["keywords"], 83 96 }; 84 97 const fuse = new Fuse(emojis, fuseOptions); 85 - function searchEmoji(s: string): string | null { 98 + type RangeTuple = [number, number]; 99 + 100 + type FuseResultMatch = { 101 + indices: ReadonlyArray<RangeTuple>; 102 + key?: string; 103 + refIndex?: number; 104 + value?: string; 105 + }; 106 + type FuseResult<T> = { 107 + item: T; 108 + refIndex: number; 109 + score?: number; 110 + matches?: ReadonlyArray<FuseResultMatch>; 111 + }; 112 + type EmojiSearchResults = FuseResult<{ 113 + emoji: string; 114 + keywords: string[]; 115 + }>[]; 116 + function searchEmoji(s: string): null | EmojiSearchResults { 86 117 const results = fuse.search(s); 87 118 if (results.length === 0) { 88 119 return null; 89 120 } 90 - return results[0].item.emoji; 121 + return results; 91 122 } 92 - function checkAndSearch(): [string, number, number] | null { 123 + function checkAndSearch(): [EmojiSearchResults, number, number] | null { 93 124 const query = checkEmoji(inputEl.selectionStart, inputEl.selectionEnd); 94 125 if (query === null) { 95 126 return null; ··· 99 130 return [emoji, query[1], query[2]]; 100 131 } 101 132 function emojifier(e: KeyboardEvent) { 102 - if (curemoji === null) { 133 + if ( 134 + curemojiresults === null || 135 + curemojinumber === null || 136 + curemoji === null 137 + ) { 103 138 return; 104 139 } 105 140 switch (e.key) { ··· 107 142 e.preventDefault(); 108 143 e.stopPropagation(); 109 144 inputEl.value = 110 - inputEl.value.slice(0, curemoji[1]) + 111 - curemoji[0] + 112 - inputEl.value.slice(curemoji[2]); 145 + inputEl.value.slice(0, curemojiresults[1]) + 146 + curemoji + 147 + inputEl.value.slice(curemojiresults[2]); 113 148 onInputEl?.(inputEl); 149 + curemoji = null; 150 + curemojiresults = null; 151 + curemojinumber = null; 152 + return; 153 + case "ArrowDown": 154 + e.preventDefault(); 155 + e.stopPropagation(); 156 + curemojinumber = curemojinumber + 1; 157 + if (curemojinumber > curemojiresults[0].length - 1) 158 + curemojinumber = 0; 159 + return; 160 + case "ArrowUp": 161 + e.preventDefault(); 162 + e.stopPropagation(); 163 + curemojinumber = curemojinumber - 1; 164 + if (curemojinumber < 0) 165 + curemojinumber = curemojiresults[0].length - 1; 114 166 return; 115 167 } 116 168 } 169 + $effect(() => { 170 + if (inputEl && emojilist) { 171 + computePosition(inputEl, emojilist, { 172 + placement: "top", 173 + middleware: [flip()], 174 + }).then(({ x, y }) => { 175 + if (emojilist !== undefined) { 176 + Object.assign(emojilist.style, { 177 + left: `${x}px`, 178 + top: `${y}px`, 179 + }); 180 + } 181 + }); 182 + } 183 + }); 117 184 </script> 118 185 119 186 <div class="autogrowwrapper"> ··· 130 197 style:font-size={fs ?? "1rem"} 131 198 {placeholder} 132 199 ></textarea> 200 + {#if curemojiresults !== null && curemojinumber !== null} 201 + <div bind:this={emojilist} class="emoji-selector"> 202 + {#each curemojiresults[0] as result, idx} 203 + <div 204 + class={curemojinumber === idx 205 + ? "selected emoji-result" 206 + : "emoji-result"} 207 + > 208 + {result.item.emoji} 209 + {#if result.matches && result.matches[0] !== null}{result 210 + .matches[0].value}{/if} 211 + </div> 212 + {/each} 213 + </div> 214 + {/if} 133 215 </div> 134 216 135 217 <style> 218 + .emoji-selector { 219 + width: max-content; 220 + position: absolute; 221 + top: 0; 222 + left: 0; 223 + } 224 + .selected.emoji-result { 225 + background: var(--fg); 226 + color: var(--bg); 227 + } 228 + .emoji-result { 229 + color: var(--fg); 230 + background: var(--bg); 231 + } 136 232 textarea { 137 233 width: 100%; 138 234 font: inherit;