@portabletext/plugin-emoji-picker 3.0.23 → 3.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -9
- package/src/create-match-emojis.ts +0 -105
- package/src/emoji-picker-machine.tsx +0 -1080
- package/src/emoji-picker.feature +0 -264
- package/src/emoji-picker.test.tsx +0 -88
- package/src/global.d.ts +0 -4
- package/src/index.ts +0 -3
- package/src/match-emojis.ts +0 -22
- package/src/use-emoji-picker.ts +0 -187
|
@@ -1,1080 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ChildPath,
|
|
3
|
-
Editor,
|
|
4
|
-
EditorSelector,
|
|
5
|
-
EditorSnapshot,
|
|
6
|
-
PortableTextSpan,
|
|
7
|
-
} from '@portabletext/editor'
|
|
8
|
-
import {
|
|
9
|
-
defineBehavior,
|
|
10
|
-
effect,
|
|
11
|
-
forward,
|
|
12
|
-
raise,
|
|
13
|
-
} from '@portabletext/editor/behaviors'
|
|
14
|
-
import {
|
|
15
|
-
getFocusSpan,
|
|
16
|
-
getMarkState,
|
|
17
|
-
getNextSpan,
|
|
18
|
-
getPreviousSpan,
|
|
19
|
-
isPointAfterSelection,
|
|
20
|
-
isPointBeforeSelection,
|
|
21
|
-
type MarkState,
|
|
22
|
-
} from '@portabletext/editor/selectors'
|
|
23
|
-
import {
|
|
24
|
-
isEqualSelectionPoints,
|
|
25
|
-
isSelectionCollapsed,
|
|
26
|
-
} from '@portabletext/editor/utils'
|
|
27
|
-
import {createKeyboardShortcut} from '@portabletext/keyboard-shortcuts'
|
|
28
|
-
import {
|
|
29
|
-
defineInputRule,
|
|
30
|
-
defineInputRuleBehavior,
|
|
31
|
-
type InputRuleMatch,
|
|
32
|
-
} from '@portabletext/plugin-input-rule'
|
|
33
|
-
import {
|
|
34
|
-
assertEvent,
|
|
35
|
-
assign,
|
|
36
|
-
fromCallback,
|
|
37
|
-
not,
|
|
38
|
-
sendTo,
|
|
39
|
-
setup,
|
|
40
|
-
type AnyEventObject,
|
|
41
|
-
type CallbackLogicFunction,
|
|
42
|
-
} from 'xstate'
|
|
43
|
-
import type {BaseEmojiMatch, MatchEmojis} from './match-emojis'
|
|
44
|
-
|
|
45
|
-
/*******************
|
|
46
|
-
* Keyboard shortcuts
|
|
47
|
-
*******************/
|
|
48
|
-
const arrowUpShortcut = createKeyboardShortcut({
|
|
49
|
-
default: [{key: 'ArrowUp'}],
|
|
50
|
-
})
|
|
51
|
-
const arrowDownShortcut = createKeyboardShortcut({
|
|
52
|
-
default: [{key: 'ArrowDown'}],
|
|
53
|
-
})
|
|
54
|
-
const enterShortcut = createKeyboardShortcut({
|
|
55
|
-
default: [{key: 'Enter'}],
|
|
56
|
-
})
|
|
57
|
-
const tabShortcut = createKeyboardShortcut({
|
|
58
|
-
default: [{key: 'Tab'}],
|
|
59
|
-
})
|
|
60
|
-
const escapeShortcut = createKeyboardShortcut({
|
|
61
|
-
default: [{key: 'Escape'}],
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
const getTriggerState: EditorSelector<
|
|
65
|
-
| {
|
|
66
|
-
focusSpan: {
|
|
67
|
-
node: PortableTextSpan
|
|
68
|
-
path: ChildPath
|
|
69
|
-
}
|
|
70
|
-
markState: MarkState
|
|
71
|
-
focusSpanTextBefore: string
|
|
72
|
-
focusSpanTextAfter: string
|
|
73
|
-
previousSpan:
|
|
74
|
-
| {
|
|
75
|
-
node: PortableTextSpan
|
|
76
|
-
path: ChildPath
|
|
77
|
-
}
|
|
78
|
-
| undefined
|
|
79
|
-
nextSpan:
|
|
80
|
-
| {
|
|
81
|
-
node: PortableTextSpan
|
|
82
|
-
path: ChildPath
|
|
83
|
-
}
|
|
84
|
-
| undefined
|
|
85
|
-
}
|
|
86
|
-
| undefined
|
|
87
|
-
> = (snapshot) => {
|
|
88
|
-
const focusSpan = getFocusSpan(snapshot)
|
|
89
|
-
const markState = getMarkState(snapshot)
|
|
90
|
-
|
|
91
|
-
if (!focusSpan || !markState || !snapshot.context.selection) {
|
|
92
|
-
return undefined
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const focusSpanTextBefore = focusSpan.node.text.slice(
|
|
96
|
-
0,
|
|
97
|
-
snapshot.context.selection.focus.offset,
|
|
98
|
-
)
|
|
99
|
-
const focusSpanTextAfter = focusSpan.node.text.slice(
|
|
100
|
-
snapshot.context.selection.focus.offset,
|
|
101
|
-
)
|
|
102
|
-
const previousSpan = getPreviousSpan(snapshot)
|
|
103
|
-
const nextSpan = getNextSpan(snapshot)
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
focusSpan,
|
|
107
|
-
markState,
|
|
108
|
-
focusSpanTextBefore,
|
|
109
|
-
focusSpanTextAfter,
|
|
110
|
-
previousSpan,
|
|
111
|
-
nextSpan,
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function createTriggerActions({
|
|
116
|
-
snapshot,
|
|
117
|
-
payload,
|
|
118
|
-
keywordState,
|
|
119
|
-
}: {
|
|
120
|
-
snapshot: EditorSnapshot
|
|
121
|
-
payload: ReturnType<typeof getTriggerState> & {lastMatch: InputRuleMatch}
|
|
122
|
-
keywordState: 'partial' | 'complete'
|
|
123
|
-
}) {
|
|
124
|
-
if (payload.markState.state === 'unchanged') {
|
|
125
|
-
const focusSpan = {
|
|
126
|
-
node: {
|
|
127
|
-
_key: payload.focusSpan.node._key,
|
|
128
|
-
_type: payload.focusSpan.node._type,
|
|
129
|
-
text: `${payload.focusSpanTextBefore}${payload.lastMatch.text}${payload.focusSpanTextAfter}`,
|
|
130
|
-
marks: payload.markState.marks,
|
|
131
|
-
},
|
|
132
|
-
path: payload.focusSpan.path,
|
|
133
|
-
textBefore: payload.focusSpanTextBefore,
|
|
134
|
-
textAfter: payload.focusSpanTextAfter,
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (keywordState === 'complete') {
|
|
138
|
-
return [
|
|
139
|
-
raise(
|
|
140
|
-
createKeywordFoundEvent({
|
|
141
|
-
focusSpan,
|
|
142
|
-
}),
|
|
143
|
-
),
|
|
144
|
-
]
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return [
|
|
148
|
-
raise(
|
|
149
|
-
createTriggerFoundEvent({
|
|
150
|
-
focusSpan,
|
|
151
|
-
}),
|
|
152
|
-
),
|
|
153
|
-
]
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const newSpan = {
|
|
157
|
-
_key: snapshot.context.keyGenerator(),
|
|
158
|
-
_type: payload.focusSpan.node._type,
|
|
159
|
-
text: payload.lastMatch.text,
|
|
160
|
-
marks: payload.markState.marks,
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
let focusSpan = {
|
|
164
|
-
node: {
|
|
165
|
-
_key: newSpan._key,
|
|
166
|
-
_type: newSpan._type,
|
|
167
|
-
text: `${newSpan.text}${payload.nextSpan?.node.text ?? payload.focusSpanTextAfter}`,
|
|
168
|
-
marks: payload.markState.marks,
|
|
169
|
-
},
|
|
170
|
-
path: [
|
|
171
|
-
{_key: payload.focusSpan.path[0]._key},
|
|
172
|
-
'children',
|
|
173
|
-
{_key: newSpan._key},
|
|
174
|
-
] satisfies ChildPath,
|
|
175
|
-
textBefore: '',
|
|
176
|
-
textAfter: payload.nextSpan?.node.text ?? payload.focusSpanTextAfter,
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
payload.previousSpan &&
|
|
181
|
-
payload.focusSpanTextBefore.length === 0 &&
|
|
182
|
-
JSON.stringify(payload.previousSpan.node.marks ?? []) ===
|
|
183
|
-
JSON.stringify(payload.markState.marks)
|
|
184
|
-
) {
|
|
185
|
-
// The text will be inserted into the previous span, so we'll treat that
|
|
186
|
-
// as the focus span
|
|
187
|
-
|
|
188
|
-
focusSpan = {
|
|
189
|
-
node: {
|
|
190
|
-
_key: payload.previousSpan.node._key,
|
|
191
|
-
_type: newSpan._type,
|
|
192
|
-
text: `${payload.previousSpan.node.text}${newSpan.text}`,
|
|
193
|
-
marks: newSpan.marks,
|
|
194
|
-
},
|
|
195
|
-
path: payload.previousSpan.path,
|
|
196
|
-
textBefore: payload.previousSpan.node.text,
|
|
197
|
-
textAfter: '',
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return [
|
|
202
|
-
raise({type: 'select', at: payload.lastMatch.targetOffsets}),
|
|
203
|
-
raise({type: 'delete', at: payload.lastMatch.targetOffsets}),
|
|
204
|
-
raise({type: 'insert.child', child: newSpan}),
|
|
205
|
-
...(keywordState === 'complete'
|
|
206
|
-
? [
|
|
207
|
-
raise(
|
|
208
|
-
createKeywordFoundEvent({
|
|
209
|
-
focusSpan,
|
|
210
|
-
}),
|
|
211
|
-
),
|
|
212
|
-
]
|
|
213
|
-
: [
|
|
214
|
-
raise(
|
|
215
|
-
createTriggerFoundEvent({
|
|
216
|
-
focusSpan,
|
|
217
|
-
}),
|
|
218
|
-
),
|
|
219
|
-
]),
|
|
220
|
-
]
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/*******************
|
|
224
|
-
* Input Rules
|
|
225
|
-
*******************/
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Listen for a single colon insertion
|
|
229
|
-
*/
|
|
230
|
-
const triggerRule = defineInputRule({
|
|
231
|
-
on: /:/,
|
|
232
|
-
guard: ({snapshot, event}) => {
|
|
233
|
-
const lastMatch = event.matches.at(-1)
|
|
234
|
-
|
|
235
|
-
if (lastMatch === undefined) {
|
|
236
|
-
return false
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const triggerState = getTriggerState(snapshot)
|
|
240
|
-
|
|
241
|
-
if (!triggerState) {
|
|
242
|
-
return false
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return {
|
|
246
|
-
lastMatch,
|
|
247
|
-
...triggerState,
|
|
248
|
-
}
|
|
249
|
-
},
|
|
250
|
-
actions: [
|
|
251
|
-
({snapshot}, payload) =>
|
|
252
|
-
createTriggerActions({snapshot, payload, keywordState: 'partial'}),
|
|
253
|
-
],
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
type TriggerFoundEvent = ReturnType<typeof createTriggerFoundEvent>
|
|
257
|
-
|
|
258
|
-
function createTriggerFoundEvent(payload: {
|
|
259
|
-
focusSpan: {
|
|
260
|
-
node: PortableTextSpan
|
|
261
|
-
path: ChildPath
|
|
262
|
-
textBefore: string
|
|
263
|
-
textAfter: string
|
|
264
|
-
}
|
|
265
|
-
}) {
|
|
266
|
-
return {
|
|
267
|
-
type: 'custom.trigger found',
|
|
268
|
-
...payload,
|
|
269
|
-
} as const
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Listen for a partial keyword like ":joy"
|
|
274
|
-
*/
|
|
275
|
-
const partialKeywordRule = defineInputRule({
|
|
276
|
-
on: /:[\S]+/,
|
|
277
|
-
guard: ({snapshot, event}) => {
|
|
278
|
-
const lastMatch = event.matches.at(-1)
|
|
279
|
-
|
|
280
|
-
if (lastMatch === undefined) {
|
|
281
|
-
return false
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (lastMatch.targetOffsets.anchor.offset < event.textBefore.length) {
|
|
285
|
-
return false
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const triggerState = getTriggerState(snapshot)
|
|
289
|
-
|
|
290
|
-
if (!triggerState) {
|
|
291
|
-
return false
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
...triggerState,
|
|
296
|
-
lastMatch,
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
actions: [
|
|
300
|
-
({snapshot}, payload) =>
|
|
301
|
-
createTriggerActions({snapshot, payload, keywordState: 'partial'}),
|
|
302
|
-
],
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Listen for a complete keyword like ":joy:"
|
|
307
|
-
*/
|
|
308
|
-
const keywordRule = defineInputRule({
|
|
309
|
-
on: /:[\S]+:/,
|
|
310
|
-
guard: ({snapshot, event}) => {
|
|
311
|
-
const lastMatch = event.matches.at(-1)
|
|
312
|
-
|
|
313
|
-
if (lastMatch === undefined) {
|
|
314
|
-
return false
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (lastMatch.targetOffsets.anchor.offset < event.textBefore.length) {
|
|
318
|
-
return false
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const triggerState = getTriggerState(snapshot)
|
|
322
|
-
|
|
323
|
-
if (!triggerState) {
|
|
324
|
-
return false
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return {
|
|
328
|
-
...triggerState,
|
|
329
|
-
lastMatch,
|
|
330
|
-
}
|
|
331
|
-
},
|
|
332
|
-
actions: [
|
|
333
|
-
({snapshot}, payload) =>
|
|
334
|
-
createTriggerActions({snapshot, payload, keywordState: 'complete'}),
|
|
335
|
-
],
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
type KeywordFoundEvent = ReturnType<typeof createKeywordFoundEvent>
|
|
339
|
-
|
|
340
|
-
function createKeywordFoundEvent(payload: {
|
|
341
|
-
focusSpan: {
|
|
342
|
-
node: PortableTextSpan
|
|
343
|
-
path: ChildPath
|
|
344
|
-
textBefore: string
|
|
345
|
-
textAfter: string
|
|
346
|
-
}
|
|
347
|
-
}) {
|
|
348
|
-
return {
|
|
349
|
-
type: 'custom.keyword found',
|
|
350
|
-
...payload,
|
|
351
|
-
} as const
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
type EmojiPickerContext = {
|
|
355
|
-
editor: Editor
|
|
356
|
-
matches: ReadonlyArray<BaseEmojiMatch>
|
|
357
|
-
matchEmojis: MatchEmojis<BaseEmojiMatch>
|
|
358
|
-
selectedIndex: number
|
|
359
|
-
focusSpan:
|
|
360
|
-
| {
|
|
361
|
-
node: PortableTextSpan
|
|
362
|
-
path: ChildPath
|
|
363
|
-
textBefore: string
|
|
364
|
-
textAfter: string
|
|
365
|
-
}
|
|
366
|
-
| undefined
|
|
367
|
-
incompleteKeywordRegex: RegExp
|
|
368
|
-
keyword: string
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
type EmojiPickerEvent =
|
|
372
|
-
| TriggerFoundEvent
|
|
373
|
-
| KeywordFoundEvent
|
|
374
|
-
| {
|
|
375
|
-
type: 'selection changed'
|
|
376
|
-
}
|
|
377
|
-
| {
|
|
378
|
-
type: 'dismiss'
|
|
379
|
-
}
|
|
380
|
-
| {
|
|
381
|
-
type: 'navigate down'
|
|
382
|
-
}
|
|
383
|
-
| {
|
|
384
|
-
type: 'navigate up'
|
|
385
|
-
}
|
|
386
|
-
| {
|
|
387
|
-
type: 'navigate to'
|
|
388
|
-
index: number
|
|
389
|
-
}
|
|
390
|
-
| {
|
|
391
|
-
type: 'insert selected match'
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const triggerListenerCallback: CallbackLogicFunction<
|
|
395
|
-
AnyEventObject,
|
|
396
|
-
EmojiPickerEvent,
|
|
397
|
-
{editor: Editor}
|
|
398
|
-
> = ({sendBack, input}) => {
|
|
399
|
-
const unregisterBehaviors = [
|
|
400
|
-
input.editor.registerBehavior({
|
|
401
|
-
behavior: defineInputRuleBehavior({
|
|
402
|
-
rules: [keywordRule, partialKeywordRule, triggerRule],
|
|
403
|
-
}),
|
|
404
|
-
}),
|
|
405
|
-
input.editor.registerBehavior({
|
|
406
|
-
behavior: defineBehavior<KeywordFoundEvent, KeywordFoundEvent['type']>({
|
|
407
|
-
on: 'custom.keyword found',
|
|
408
|
-
actions: [
|
|
409
|
-
({event}) => [
|
|
410
|
-
effect(() => {
|
|
411
|
-
sendBack(event)
|
|
412
|
-
}),
|
|
413
|
-
],
|
|
414
|
-
],
|
|
415
|
-
}),
|
|
416
|
-
}),
|
|
417
|
-
input.editor.registerBehavior({
|
|
418
|
-
behavior: defineBehavior<TriggerFoundEvent, TriggerFoundEvent['type']>({
|
|
419
|
-
on: 'custom.trigger found',
|
|
420
|
-
actions: [
|
|
421
|
-
({event}) => [
|
|
422
|
-
effect(() => {
|
|
423
|
-
sendBack(event)
|
|
424
|
-
}),
|
|
425
|
-
],
|
|
426
|
-
],
|
|
427
|
-
}),
|
|
428
|
-
}),
|
|
429
|
-
]
|
|
430
|
-
|
|
431
|
-
return () => {
|
|
432
|
-
for (const unregister of unregisterBehaviors) {
|
|
433
|
-
unregister()
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const escapeListenerCallback: CallbackLogicFunction<
|
|
439
|
-
AnyEventObject,
|
|
440
|
-
EmojiPickerEvent,
|
|
441
|
-
{editor: Editor}
|
|
442
|
-
> = ({sendBack, input}) => {
|
|
443
|
-
return input.editor.registerBehavior({
|
|
444
|
-
behavior: defineBehavior({
|
|
445
|
-
on: 'keyboard.keydown',
|
|
446
|
-
guard: ({event}) => escapeShortcut.guard(event.originEvent),
|
|
447
|
-
actions: [
|
|
448
|
-
() => [
|
|
449
|
-
effect(() => {
|
|
450
|
-
sendBack({type: 'dismiss'})
|
|
451
|
-
}),
|
|
452
|
-
],
|
|
453
|
-
],
|
|
454
|
-
}),
|
|
455
|
-
})
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const arrowListenerCallback: CallbackLogicFunction<
|
|
459
|
-
AnyEventObject,
|
|
460
|
-
EmojiPickerEvent,
|
|
461
|
-
{editor: Editor}
|
|
462
|
-
> = ({sendBack, input}) => {
|
|
463
|
-
const unregisterBehaviors = [
|
|
464
|
-
input.editor.registerBehavior({
|
|
465
|
-
behavior: defineBehavior({
|
|
466
|
-
on: 'keyboard.keydown',
|
|
467
|
-
guard: ({event}) => arrowDownShortcut.guard(event.originEvent),
|
|
468
|
-
actions: [
|
|
469
|
-
() => [
|
|
470
|
-
effect(() => {
|
|
471
|
-
sendBack({type: 'navigate down'})
|
|
472
|
-
}),
|
|
473
|
-
],
|
|
474
|
-
],
|
|
475
|
-
}),
|
|
476
|
-
}),
|
|
477
|
-
input.editor.registerBehavior({
|
|
478
|
-
behavior: defineBehavior({
|
|
479
|
-
on: 'keyboard.keydown',
|
|
480
|
-
guard: ({event}) => arrowUpShortcut.guard(event.originEvent),
|
|
481
|
-
actions: [
|
|
482
|
-
() => [
|
|
483
|
-
effect(() => {
|
|
484
|
-
sendBack({type: 'navigate up'})
|
|
485
|
-
}),
|
|
486
|
-
],
|
|
487
|
-
],
|
|
488
|
-
}),
|
|
489
|
-
}),
|
|
490
|
-
]
|
|
491
|
-
|
|
492
|
-
return () => {
|
|
493
|
-
for (const unregister of unregisterBehaviors) {
|
|
494
|
-
unregister()
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const emojiInsertListener: CallbackLogicFunction<
|
|
500
|
-
AnyEventObject,
|
|
501
|
-
EmojiPickerEvent,
|
|
502
|
-
{context: EmojiPickerContext}
|
|
503
|
-
> = ({sendBack, input}) => {
|
|
504
|
-
return input.context.editor.registerBehavior({
|
|
505
|
-
behavior: defineBehavior<{
|
|
506
|
-
emoji: string
|
|
507
|
-
focusSpan: {
|
|
508
|
-
node: PortableTextSpan
|
|
509
|
-
path: ChildPath
|
|
510
|
-
textBefore: string
|
|
511
|
-
textAfter: string
|
|
512
|
-
}
|
|
513
|
-
}>({
|
|
514
|
-
on: 'custom.insert emoji',
|
|
515
|
-
actions: [
|
|
516
|
-
({event}) => [
|
|
517
|
-
effect(() => {
|
|
518
|
-
sendBack({type: 'dismiss'})
|
|
519
|
-
}),
|
|
520
|
-
raise({
|
|
521
|
-
type: 'delete',
|
|
522
|
-
at: {
|
|
523
|
-
anchor: {
|
|
524
|
-
path: event.focusSpan.path,
|
|
525
|
-
offset: event.focusSpan.textBefore.length,
|
|
526
|
-
},
|
|
527
|
-
focus: {
|
|
528
|
-
path: event.focusSpan.path,
|
|
529
|
-
offset:
|
|
530
|
-
event.focusSpan.node.text.length -
|
|
531
|
-
event.focusSpan.textAfter.length,
|
|
532
|
-
},
|
|
533
|
-
},
|
|
534
|
-
}),
|
|
535
|
-
raise({
|
|
536
|
-
type: 'insert.text',
|
|
537
|
-
text: event.emoji,
|
|
538
|
-
}),
|
|
539
|
-
],
|
|
540
|
-
],
|
|
541
|
-
}),
|
|
542
|
-
})
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const submitListenerCallback: CallbackLogicFunction<
|
|
546
|
-
{type: 'context changed'; context: EmojiPickerContext},
|
|
547
|
-
EmojiPickerEvent,
|
|
548
|
-
{context: EmojiPickerContext}
|
|
549
|
-
> = ({sendBack, input, receive}) => {
|
|
550
|
-
let context = input.context
|
|
551
|
-
|
|
552
|
-
receive((event) => {
|
|
553
|
-
context = event.context
|
|
554
|
-
})
|
|
555
|
-
|
|
556
|
-
const unregisterBehaviors = [
|
|
557
|
-
input.context.editor.registerBehavior({
|
|
558
|
-
behavior: defineBehavior({
|
|
559
|
-
on: 'keyboard.keydown',
|
|
560
|
-
guard: ({event}) => {
|
|
561
|
-
if (
|
|
562
|
-
!enterShortcut.guard(event.originEvent) &&
|
|
563
|
-
!tabShortcut.guard(event.originEvent)
|
|
564
|
-
) {
|
|
565
|
-
return false
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const focusSpan = context.focusSpan
|
|
569
|
-
const match = context.matches[context.selectedIndex]
|
|
570
|
-
|
|
571
|
-
return match && focusSpan ? {focusSpan, emoji: match.emoji} : false
|
|
572
|
-
},
|
|
573
|
-
actions: [
|
|
574
|
-
(_, {focusSpan, emoji}) => [
|
|
575
|
-
raise({
|
|
576
|
-
type: 'custom.insert emoji',
|
|
577
|
-
emoji,
|
|
578
|
-
focusSpan,
|
|
579
|
-
}),
|
|
580
|
-
],
|
|
581
|
-
],
|
|
582
|
-
}),
|
|
583
|
-
}),
|
|
584
|
-
input.context.editor.registerBehavior({
|
|
585
|
-
behavior: defineBehavior({
|
|
586
|
-
on: 'keyboard.keydown',
|
|
587
|
-
guard: ({event}) =>
|
|
588
|
-
(enterShortcut.guard(event.originEvent) ||
|
|
589
|
-
tabShortcut.guard(event.originEvent)) &&
|
|
590
|
-
context.keyword.length === 1,
|
|
591
|
-
actions: [
|
|
592
|
-
({event}) => [
|
|
593
|
-
forward(event),
|
|
594
|
-
effect(() => {
|
|
595
|
-
sendBack({type: 'dismiss'})
|
|
596
|
-
}),
|
|
597
|
-
],
|
|
598
|
-
],
|
|
599
|
-
}),
|
|
600
|
-
}),
|
|
601
|
-
input.context.editor.registerBehavior({
|
|
602
|
-
behavior: defineBehavior({
|
|
603
|
-
on: 'keyboard.keydown',
|
|
604
|
-
guard: ({event}) =>
|
|
605
|
-
(enterShortcut.guard(event.originEvent) ||
|
|
606
|
-
tabShortcut.guard(event.originEvent)) &&
|
|
607
|
-
context.keyword.length > 1,
|
|
608
|
-
actions: [
|
|
609
|
-
() => [
|
|
610
|
-
effect(() => {
|
|
611
|
-
sendBack({type: 'dismiss'})
|
|
612
|
-
}),
|
|
613
|
-
],
|
|
614
|
-
],
|
|
615
|
-
}),
|
|
616
|
-
}),
|
|
617
|
-
]
|
|
618
|
-
|
|
619
|
-
return () => {
|
|
620
|
-
for (const unregister of unregisterBehaviors) {
|
|
621
|
-
unregister()
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
const selectionListenerCallback: CallbackLogicFunction<
|
|
627
|
-
AnyEventObject,
|
|
628
|
-
EmojiPickerEvent,
|
|
629
|
-
{editor: Editor}
|
|
630
|
-
> = ({sendBack, input}) => {
|
|
631
|
-
const subscription = input.editor.on('selection', () => {
|
|
632
|
-
sendBack({type: 'selection changed'})
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
return subscription.unsubscribe
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const textInsertionListenerCallback: CallbackLogicFunction<
|
|
639
|
-
{type: 'context changed'; context: EmojiPickerContext},
|
|
640
|
-
EmojiPickerEvent,
|
|
641
|
-
{context: EmojiPickerContext}
|
|
642
|
-
> = ({sendBack, input, receive}) => {
|
|
643
|
-
let context = input.context
|
|
644
|
-
|
|
645
|
-
receive((event) => {
|
|
646
|
-
context = event.context
|
|
647
|
-
})
|
|
648
|
-
|
|
649
|
-
return input.context.editor.registerBehavior({
|
|
650
|
-
behavior: defineBehavior({
|
|
651
|
-
on: 'insert.text',
|
|
652
|
-
guard: ({snapshot}) => {
|
|
653
|
-
if (!context.focusSpan) {
|
|
654
|
-
return false
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
if (!snapshot.context.selection) {
|
|
658
|
-
return false
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
const keywordAnchor = {
|
|
662
|
-
path: context.focusSpan.path,
|
|
663
|
-
offset: context.focusSpan.textBefore.length,
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
return isEqualSelectionPoints(
|
|
667
|
-
snapshot.context.selection.focus,
|
|
668
|
-
keywordAnchor,
|
|
669
|
-
)
|
|
670
|
-
},
|
|
671
|
-
actions: [
|
|
672
|
-
({event}) => [
|
|
673
|
-
forward(event),
|
|
674
|
-
effect(() => {
|
|
675
|
-
sendBack({type: 'dismiss'})
|
|
676
|
-
}),
|
|
677
|
-
],
|
|
678
|
-
],
|
|
679
|
-
}),
|
|
680
|
-
})
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
export const emojiPickerMachine = setup({
|
|
684
|
-
types: {
|
|
685
|
-
context: {} as EmojiPickerContext,
|
|
686
|
-
input: {} as {
|
|
687
|
-
editor: Editor
|
|
688
|
-
matchEmojis: MatchEmojis
|
|
689
|
-
},
|
|
690
|
-
events: {} as EmojiPickerEvent,
|
|
691
|
-
},
|
|
692
|
-
actors: {
|
|
693
|
-
'emoji insert listener': fromCallback(emojiInsertListener),
|
|
694
|
-
'submit listener': fromCallback(submitListenerCallback),
|
|
695
|
-
'arrow listener': fromCallback(arrowListenerCallback),
|
|
696
|
-
'trigger listener': fromCallback(triggerListenerCallback),
|
|
697
|
-
'escape listener': fromCallback(escapeListenerCallback),
|
|
698
|
-
'selection listener': fromCallback(selectionListenerCallback),
|
|
699
|
-
'text insertion listener': fromCallback(textInsertionListenerCallback),
|
|
700
|
-
},
|
|
701
|
-
actions: {
|
|
702
|
-
'set focus span': assign({
|
|
703
|
-
focusSpan: ({context, event}) => {
|
|
704
|
-
if (
|
|
705
|
-
event.type !== 'custom.trigger found' &&
|
|
706
|
-
event.type !== 'custom.keyword found'
|
|
707
|
-
) {
|
|
708
|
-
return context.focusSpan
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
return event.focusSpan
|
|
712
|
-
},
|
|
713
|
-
}),
|
|
714
|
-
'update focus span': assign({
|
|
715
|
-
focusSpan: ({context}) => {
|
|
716
|
-
if (!context.focusSpan) {
|
|
717
|
-
return undefined
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
const snapshot = context.editor.getSnapshot()
|
|
721
|
-
const focusSpan = getFocusSpan(snapshot)
|
|
722
|
-
|
|
723
|
-
if (!snapshot.context.selection) {
|
|
724
|
-
return undefined
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
if (!focusSpan) {
|
|
728
|
-
return undefined
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
const nextSpan = getNextSpan({
|
|
732
|
-
...snapshot,
|
|
733
|
-
context: {
|
|
734
|
-
...snapshot.context,
|
|
735
|
-
selection: {
|
|
736
|
-
anchor: {
|
|
737
|
-
path: context.focusSpan.path,
|
|
738
|
-
offset: 0,
|
|
739
|
-
},
|
|
740
|
-
focus: {
|
|
741
|
-
path: context.focusSpan.path,
|
|
742
|
-
offset: 0,
|
|
743
|
-
},
|
|
744
|
-
},
|
|
745
|
-
},
|
|
746
|
-
})
|
|
747
|
-
|
|
748
|
-
if (
|
|
749
|
-
JSON.stringify(focusSpan.path) !==
|
|
750
|
-
JSON.stringify(context.focusSpan.path)
|
|
751
|
-
) {
|
|
752
|
-
if (
|
|
753
|
-
nextSpan &&
|
|
754
|
-
context.focusSpan.textAfter.length === 0 &&
|
|
755
|
-
snapshot.context.selection.focus.offset === 0 &&
|
|
756
|
-
isSelectionCollapsed(snapshot.context.selection)
|
|
757
|
-
) {
|
|
758
|
-
// This is an edge case where the caret is moved from the end of
|
|
759
|
-
// the focus span to the start of the next span.
|
|
760
|
-
return context.focusSpan
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
return undefined
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
if (!focusSpan.node.text.startsWith(context.focusSpan.textBefore)) {
|
|
767
|
-
return undefined
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
if (!focusSpan.node.text.endsWith(context.focusSpan.textAfter)) {
|
|
771
|
-
return undefined
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
const keywordAnchor = {
|
|
775
|
-
path: focusSpan.path,
|
|
776
|
-
offset: context.focusSpan.textBefore.length,
|
|
777
|
-
}
|
|
778
|
-
const keywordFocus = {
|
|
779
|
-
path: focusSpan.path,
|
|
780
|
-
offset:
|
|
781
|
-
focusSpan.node.text.length - context.focusSpan.textAfter.length,
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
const selectionIsBeforeKeyword =
|
|
785
|
-
isPointAfterSelection(keywordAnchor)(snapshot)
|
|
786
|
-
|
|
787
|
-
const selectionIsAfterKeyword =
|
|
788
|
-
isPointBeforeSelection(keywordFocus)(snapshot)
|
|
789
|
-
|
|
790
|
-
if (selectionIsBeforeKeyword || selectionIsAfterKeyword) {
|
|
791
|
-
return undefined
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
return {
|
|
795
|
-
node: focusSpan.node,
|
|
796
|
-
path: focusSpan.path,
|
|
797
|
-
textBefore: context.focusSpan.textBefore,
|
|
798
|
-
textAfter: context.focusSpan.textAfter,
|
|
799
|
-
}
|
|
800
|
-
},
|
|
801
|
-
}),
|
|
802
|
-
'update keyword': assign({
|
|
803
|
-
keyword: ({context}) => {
|
|
804
|
-
if (!context.focusSpan) {
|
|
805
|
-
return ''
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
if (
|
|
809
|
-
context.focusSpan.textBefore.length > 0 &&
|
|
810
|
-
context.focusSpan.textAfter.length > 0
|
|
811
|
-
) {
|
|
812
|
-
return context.focusSpan.node.text.slice(
|
|
813
|
-
context.focusSpan.textBefore.length,
|
|
814
|
-
-context.focusSpan.textAfter.length,
|
|
815
|
-
)
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
if (context.focusSpan.textBefore.length > 0) {
|
|
819
|
-
return context.focusSpan.node.text.slice(
|
|
820
|
-
context.focusSpan.textBefore.length,
|
|
821
|
-
)
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
if (context.focusSpan.textAfter.length > 0) {
|
|
825
|
-
return context.focusSpan.node.text.slice(
|
|
826
|
-
0,
|
|
827
|
-
-context.focusSpan.textAfter.length,
|
|
828
|
-
)
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
return context.focusSpan.node.text
|
|
832
|
-
},
|
|
833
|
-
}),
|
|
834
|
-
'update matches': assign({
|
|
835
|
-
matches: ({context}) => {
|
|
836
|
-
// Strip leading colon
|
|
837
|
-
let rawKeyword = context.keyword.startsWith(':')
|
|
838
|
-
? context.keyword.slice(1)
|
|
839
|
-
: context.keyword
|
|
840
|
-
// Strip trailing colon
|
|
841
|
-
rawKeyword =
|
|
842
|
-
rawKeyword.length > 1 && rawKeyword.endsWith(':')
|
|
843
|
-
? rawKeyword.slice(0, -1)
|
|
844
|
-
: rawKeyword
|
|
845
|
-
|
|
846
|
-
if (rawKeyword === undefined) {
|
|
847
|
-
return []
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
return context.matchEmojis({keyword: rawKeyword})
|
|
851
|
-
},
|
|
852
|
-
}),
|
|
853
|
-
'reset selected index': assign({
|
|
854
|
-
selectedIndex: 0,
|
|
855
|
-
}),
|
|
856
|
-
'increment selected index': assign({
|
|
857
|
-
selectedIndex: ({context}) => {
|
|
858
|
-
if (context.selectedIndex === context.matches.length - 1) {
|
|
859
|
-
return 0
|
|
860
|
-
}
|
|
861
|
-
return context.selectedIndex + 1
|
|
862
|
-
},
|
|
863
|
-
}),
|
|
864
|
-
'decrement selected index': assign({
|
|
865
|
-
selectedIndex: ({context}) => {
|
|
866
|
-
if (context.selectedIndex === 0) {
|
|
867
|
-
return context.matches.length - 1
|
|
868
|
-
}
|
|
869
|
-
return context.selectedIndex - 1
|
|
870
|
-
},
|
|
871
|
-
}),
|
|
872
|
-
'set selected index': assign({
|
|
873
|
-
selectedIndex: ({event}) => {
|
|
874
|
-
assertEvent(event, 'navigate to')
|
|
875
|
-
|
|
876
|
-
return event.index
|
|
877
|
-
},
|
|
878
|
-
}),
|
|
879
|
-
'update submit listener context': sendTo(
|
|
880
|
-
'submit listener',
|
|
881
|
-
({context}) => ({
|
|
882
|
-
type: 'context changed',
|
|
883
|
-
context,
|
|
884
|
-
}),
|
|
885
|
-
),
|
|
886
|
-
'update text insertion listener context': sendTo(
|
|
887
|
-
'text insertion listener',
|
|
888
|
-
({context}) => ({
|
|
889
|
-
type: 'context changed',
|
|
890
|
-
context,
|
|
891
|
-
}),
|
|
892
|
-
),
|
|
893
|
-
'insert selected match': ({context}) => {
|
|
894
|
-
const match = context.matches[context.selectedIndex]
|
|
895
|
-
|
|
896
|
-
if (!match || !context.focusSpan) {
|
|
897
|
-
return
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
context.editor.send({
|
|
901
|
-
type: 'custom.insert emoji',
|
|
902
|
-
emoji: match.emoji,
|
|
903
|
-
focusSpan: context.focusSpan,
|
|
904
|
-
})
|
|
905
|
-
},
|
|
906
|
-
'reset': assign({
|
|
907
|
-
focusSpan: undefined,
|
|
908
|
-
keyword: '',
|
|
909
|
-
matches: [],
|
|
910
|
-
selectedIndex: 0,
|
|
911
|
-
}),
|
|
912
|
-
},
|
|
913
|
-
guards: {
|
|
914
|
-
'no focus span': ({context}) => {
|
|
915
|
-
return !context.focusSpan
|
|
916
|
-
},
|
|
917
|
-
'has matches': ({context}) => {
|
|
918
|
-
return context.matches.length > 0
|
|
919
|
-
},
|
|
920
|
-
'no matches': not('has matches'),
|
|
921
|
-
'keyword is malformed': ({context}) => {
|
|
922
|
-
return !context.incompleteKeywordRegex.test(context.keyword)
|
|
923
|
-
},
|
|
924
|
-
'keyword is direct match': ({context}) => {
|
|
925
|
-
const fullKeywordRegex = /^:[\S]+:$/
|
|
926
|
-
|
|
927
|
-
if (!fullKeywordRegex.test(context.keyword)) {
|
|
928
|
-
return false
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const match = context.matches.at(context.selectedIndex)
|
|
932
|
-
|
|
933
|
-
if (!match || match.type !== 'exact') {
|
|
934
|
-
return false
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
return true
|
|
938
|
-
},
|
|
939
|
-
},
|
|
940
|
-
}).createMachine({
|
|
941
|
-
id: 'emoji picker',
|
|
942
|
-
context: ({input}) => ({
|
|
943
|
-
editor: input.editor,
|
|
944
|
-
keyword: '',
|
|
945
|
-
focusSpan: undefined,
|
|
946
|
-
matchEmojis: input.matchEmojis,
|
|
947
|
-
incompleteKeywordRegex: /^:[\S]*$/,
|
|
948
|
-
matches: [],
|
|
949
|
-
selectedIndex: 0,
|
|
950
|
-
}),
|
|
951
|
-
initial: 'idle',
|
|
952
|
-
invoke: [
|
|
953
|
-
{
|
|
954
|
-
src: 'emoji insert listener',
|
|
955
|
-
id: 'emoji insert listener',
|
|
956
|
-
input: ({context}) => ({context}),
|
|
957
|
-
},
|
|
958
|
-
],
|
|
959
|
-
states: {
|
|
960
|
-
idle: {
|
|
961
|
-
entry: ['reset'],
|
|
962
|
-
invoke: {
|
|
963
|
-
src: 'trigger listener',
|
|
964
|
-
input: ({context}) => ({editor: context.editor}),
|
|
965
|
-
},
|
|
966
|
-
on: {
|
|
967
|
-
'custom.trigger found': {
|
|
968
|
-
target: 'searching',
|
|
969
|
-
actions: ['set focus span', 'update keyword'],
|
|
970
|
-
},
|
|
971
|
-
'custom.keyword found': {
|
|
972
|
-
actions: [
|
|
973
|
-
'set focus span',
|
|
974
|
-
'update keyword',
|
|
975
|
-
'update matches',
|
|
976
|
-
'insert selected match',
|
|
977
|
-
],
|
|
978
|
-
target: 'idle',
|
|
979
|
-
reenter: true,
|
|
980
|
-
},
|
|
981
|
-
},
|
|
982
|
-
},
|
|
983
|
-
searching: {
|
|
984
|
-
invoke: [
|
|
985
|
-
{
|
|
986
|
-
src: 'submit listener',
|
|
987
|
-
id: 'submit listener',
|
|
988
|
-
input: ({context}) => ({context}),
|
|
989
|
-
},
|
|
990
|
-
{
|
|
991
|
-
src: 'escape listener',
|
|
992
|
-
input: ({context}) => ({editor: context.editor}),
|
|
993
|
-
},
|
|
994
|
-
{
|
|
995
|
-
src: 'selection listener',
|
|
996
|
-
input: ({context}) => ({editor: context.editor}),
|
|
997
|
-
},
|
|
998
|
-
{
|
|
999
|
-
src: 'text insertion listener',
|
|
1000
|
-
id: 'text insertion listener',
|
|
1001
|
-
input: ({context}) => ({context}),
|
|
1002
|
-
},
|
|
1003
|
-
],
|
|
1004
|
-
on: {
|
|
1005
|
-
'dismiss': {
|
|
1006
|
-
target: 'idle',
|
|
1007
|
-
},
|
|
1008
|
-
'selection changed': [
|
|
1009
|
-
{
|
|
1010
|
-
actions: [
|
|
1011
|
-
'update focus span',
|
|
1012
|
-
'update keyword',
|
|
1013
|
-
'update matches',
|
|
1014
|
-
'reset selected index',
|
|
1015
|
-
'update submit listener context',
|
|
1016
|
-
'update text insertion listener context',
|
|
1017
|
-
],
|
|
1018
|
-
},
|
|
1019
|
-
],
|
|
1020
|
-
},
|
|
1021
|
-
always: [
|
|
1022
|
-
{
|
|
1023
|
-
guard: 'no focus span',
|
|
1024
|
-
target: 'idle',
|
|
1025
|
-
},
|
|
1026
|
-
{
|
|
1027
|
-
guard: 'keyword is malformed',
|
|
1028
|
-
target: 'idle',
|
|
1029
|
-
},
|
|
1030
|
-
{
|
|
1031
|
-
guard: 'keyword is direct match',
|
|
1032
|
-
actions: ['insert selected match'],
|
|
1033
|
-
target: 'idle',
|
|
1034
|
-
},
|
|
1035
|
-
],
|
|
1036
|
-
initial: 'no matches showing',
|
|
1037
|
-
states: {
|
|
1038
|
-
'no matches showing': {
|
|
1039
|
-
entry: ['reset selected index'],
|
|
1040
|
-
always: {
|
|
1041
|
-
guard: 'has matches',
|
|
1042
|
-
target: 'showing matches',
|
|
1043
|
-
},
|
|
1044
|
-
},
|
|
1045
|
-
'showing matches': {
|
|
1046
|
-
invoke: {
|
|
1047
|
-
src: 'arrow listener',
|
|
1048
|
-
input: ({context}) => ({editor: context.editor}),
|
|
1049
|
-
},
|
|
1050
|
-
always: [
|
|
1051
|
-
{
|
|
1052
|
-
guard: 'no matches',
|
|
1053
|
-
target: 'no matches showing',
|
|
1054
|
-
},
|
|
1055
|
-
],
|
|
1056
|
-
on: {
|
|
1057
|
-
'navigate down': {
|
|
1058
|
-
actions: [
|
|
1059
|
-
'increment selected index',
|
|
1060
|
-
'update submit listener context',
|
|
1061
|
-
],
|
|
1062
|
-
},
|
|
1063
|
-
'navigate up': {
|
|
1064
|
-
actions: [
|
|
1065
|
-
'decrement selected index',
|
|
1066
|
-
'update submit listener context',
|
|
1067
|
-
],
|
|
1068
|
-
},
|
|
1069
|
-
'navigate to': {
|
|
1070
|
-
actions: ['set selected index', 'update submit listener context'],
|
|
1071
|
-
},
|
|
1072
|
-
'insert selected match': {
|
|
1073
|
-
actions: ['insert selected match'],
|
|
1074
|
-
},
|
|
1075
|
-
},
|
|
1076
|
-
},
|
|
1077
|
-
},
|
|
1078
|
-
},
|
|
1079
|
-
},
|
|
1080
|
-
})
|