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