tangled
alpha
login
or
join now
moth11.net
/
xcvr
2
fork
atom
frontend for xcvr appview
2
fork
atom
overview
issues
pulls
pipelines
some emoji list stuff
moth11.net
6 months ago
9d62a529
b10e3bd2
+105
-9
1 changed file
expand all
collapse all
unified
split
src
lib
components
AutoGrowTextArea.svelte
+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
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
27
-
let curemoji: [string, number, number] | null = $state(null);
28
28
+
let curemojiresults: [EmojiSearchResults, number, number] | null =
29
29
+
$state(null);
30
30
+
let curemojinumber: null | number = $state(null);
31
31
+
let curemoji: null | string = $derived(
32
32
+
curemojiresults && curemojinumber
33
33
+
? curemojiresults[0][curemojinumber]
34
34
+
: null,
35
35
+
);
28
36
29
37
let inputEl: HTMLTextAreaElement;
38
38
+
let emojilist: HTMLElement | undefined = $state();
30
39
function adjust(event: Event) {
31
40
onInputEl?.(inputEl);
32
32
-
curemoji = checkAndSearch();
41
41
+
curemojiresults = checkAndSearch();
42
42
+
if (curemojiresults !== null) {
43
43
+
curemojinumber = 0;
44
44
+
}
33
45
}
34
46
35
47
function bi(event: InputEvent) {
···
79
91
return [res, colonPos, selectionStart];
80
92
}
81
93
const fuseOptions = {
94
94
+
includeMatches: true,
82
95
keys: ["keywords"],
83
96
};
84
97
const fuse = new Fuse(emojis, fuseOptions);
85
85
-
function searchEmoji(s: string): string | null {
98
98
+
type RangeTuple = [number, number];
99
99
+
100
100
+
type FuseResultMatch = {
101
101
+
indices: ReadonlyArray<RangeTuple>;
102
102
+
key?: string;
103
103
+
refIndex?: number;
104
104
+
value?: string;
105
105
+
};
106
106
+
type FuseResult<T> = {
107
107
+
item: T;
108
108
+
refIndex: number;
109
109
+
score?: number;
110
110
+
matches?: ReadonlyArray<FuseResultMatch>;
111
111
+
};
112
112
+
type EmojiSearchResults = FuseResult<{
113
113
+
emoji: string;
114
114
+
keywords: string[];
115
115
+
}>[];
116
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
90
-
return results[0].item.emoji;
121
121
+
return results;
91
122
}
92
92
-
function checkAndSearch(): [string, number, number] | null {
123
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
102
-
if (curemoji === null) {
133
133
+
if (
134
134
+
curemojiresults === null ||
135
135
+
curemojinumber === null ||
136
136
+
curemoji === null
137
137
+
) {
103
138
return;
104
139
}
105
140
switch (e.key) {
···
107
142
e.preventDefault();
108
143
e.stopPropagation();
109
144
inputEl.value =
110
110
-
inputEl.value.slice(0, curemoji[1]) +
111
111
-
curemoji[0] +
112
112
-
inputEl.value.slice(curemoji[2]);
145
145
+
inputEl.value.slice(0, curemojiresults[1]) +
146
146
+
curemoji +
147
147
+
inputEl.value.slice(curemojiresults[2]);
113
148
onInputEl?.(inputEl);
149
149
+
curemoji = null;
150
150
+
curemojiresults = null;
151
151
+
curemojinumber = null;
152
152
+
return;
153
153
+
case "ArrowDown":
154
154
+
e.preventDefault();
155
155
+
e.stopPropagation();
156
156
+
curemojinumber = curemojinumber + 1;
157
157
+
if (curemojinumber > curemojiresults[0].length - 1)
158
158
+
curemojinumber = 0;
159
159
+
return;
160
160
+
case "ArrowUp":
161
161
+
e.preventDefault();
162
162
+
e.stopPropagation();
163
163
+
curemojinumber = curemojinumber - 1;
164
164
+
if (curemojinumber < 0)
165
165
+
curemojinumber = curemojiresults[0].length - 1;
114
166
return;
115
167
}
116
168
}
169
169
+
$effect(() => {
170
170
+
if (inputEl && emojilist) {
171
171
+
computePosition(inputEl, emojilist, {
172
172
+
placement: "top",
173
173
+
middleware: [flip()],
174
174
+
}).then(({ x, y }) => {
175
175
+
if (emojilist !== undefined) {
176
176
+
Object.assign(emojilist.style, {
177
177
+
left: `${x}px`,
178
178
+
top: `${y}px`,
179
179
+
});
180
180
+
}
181
181
+
});
182
182
+
}
183
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
200
+
{#if curemojiresults !== null && curemojinumber !== null}
201
201
+
<div bind:this={emojilist} class="emoji-selector">
202
202
+
{#each curemojiresults[0] as result, idx}
203
203
+
<div
204
204
+
class={curemojinumber === idx
205
205
+
? "selected emoji-result"
206
206
+
: "emoji-result"}
207
207
+
>
208
208
+
{result.item.emoji}
209
209
+
{#if result.matches && result.matches[0] !== null}{result
210
210
+
.matches[0].value}{/if}
211
211
+
</div>
212
212
+
{/each}
213
213
+
</div>
214
214
+
{/if}
133
215
</div>
134
216
135
217
<style>
218
218
+
.emoji-selector {
219
219
+
width: max-content;
220
220
+
position: absolute;
221
221
+
top: 0;
222
222
+
left: 0;
223
223
+
}
224
224
+
.selected.emoji-result {
225
225
+
background: var(--fg);
226
226
+
color: var(--bg);
227
227
+
}
228
228
+
.emoji-result {
229
229
+
color: var(--fg);
230
230
+
background: var(--bg);
231
231
+
}
136
232
textarea {
137
233
width: 100%;
138
234
font: inherit;