@portabletext/plugin-emoji-picker 1.0.3 → 1.0.4
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/dist/index.cjs +188 -293
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +190 -280
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/emoji-picker-machine.tsx +326 -389
- package/src/emoji-picker.feature +41 -12
- package/src/emoji-picker.test.tsx +1 -1
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
ChildPath,
|
|
3
3
|
Editor,
|
|
4
|
-
|
|
4
|
+
EditorSelector,
|
|
5
5
|
EditorSnapshot,
|
|
6
|
+
PortableTextSpan,
|
|
6
7
|
} from '@portabletext/editor'
|
|
8
|
+
import {defineBehavior, effect, raise} from '@portabletext/editor/behaviors'
|
|
7
9
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
getFocusSpan,
|
|
11
|
+
getMarkState,
|
|
12
|
+
getNextSpan,
|
|
13
|
+
isPointAfterSelection,
|
|
14
|
+
isPointBeforeSelection,
|
|
15
|
+
type MarkState,
|
|
16
|
+
} from '@portabletext/editor/selectors'
|
|
15
17
|
import {createKeyboardShortcut} from '@portabletext/keyboard-shortcuts'
|
|
16
18
|
import {
|
|
17
19
|
defineInputRule,
|
|
18
20
|
defineInputRuleBehavior,
|
|
21
|
+
type InputRuleMatch,
|
|
19
22
|
} from '@portabletext/plugin-input-rule'
|
|
20
23
|
import {
|
|
21
24
|
assertEvent,
|
|
22
25
|
assign,
|
|
23
26
|
fromCallback,
|
|
24
27
|
not,
|
|
25
|
-
or,
|
|
26
28
|
sendTo,
|
|
27
29
|
setup,
|
|
28
30
|
type AnyEventObject,
|
|
@@ -49,6 +51,134 @@ const escapeShortcut = createKeyboardShortcut({
|
|
|
49
51
|
default: [{key: 'Escape'}],
|
|
50
52
|
})
|
|
51
53
|
|
|
54
|
+
const getTriggerState: EditorSelector<
|
|
55
|
+
| {
|
|
56
|
+
focusSpan: {
|
|
57
|
+
node: PortableTextSpan
|
|
58
|
+
path: ChildPath
|
|
59
|
+
}
|
|
60
|
+
markState: MarkState
|
|
61
|
+
focusSpanTextBefore: string
|
|
62
|
+
focusSpanTextAfter: string
|
|
63
|
+
nextSpan:
|
|
64
|
+
| {
|
|
65
|
+
node: PortableTextSpan
|
|
66
|
+
path: ChildPath
|
|
67
|
+
}
|
|
68
|
+
| undefined
|
|
69
|
+
}
|
|
70
|
+
| undefined
|
|
71
|
+
> = (snapshot) => {
|
|
72
|
+
const focusSpan = getFocusSpan(snapshot)
|
|
73
|
+
const markState = getMarkState(snapshot)
|
|
74
|
+
|
|
75
|
+
if (!focusSpan || !markState || !snapshot.context.selection) {
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const focusSpanTextBefore = focusSpan.node.text.slice(
|
|
80
|
+
0,
|
|
81
|
+
snapshot.context.selection.focus.offset,
|
|
82
|
+
)
|
|
83
|
+
const focusSpanTextAfter = focusSpan.node.text.slice(
|
|
84
|
+
snapshot.context.selection.focus.offset,
|
|
85
|
+
)
|
|
86
|
+
const nextSpan = getNextSpan(snapshot)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
focusSpan,
|
|
90
|
+
markState,
|
|
91
|
+
focusSpanTextBefore,
|
|
92
|
+
focusSpanTextAfter,
|
|
93
|
+
nextSpan,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createTriggerActions({
|
|
98
|
+
snapshot,
|
|
99
|
+
payload,
|
|
100
|
+
keywordState,
|
|
101
|
+
}: {
|
|
102
|
+
snapshot: EditorSnapshot
|
|
103
|
+
payload: ReturnType<typeof getTriggerState> & {lastMatch: InputRuleMatch}
|
|
104
|
+
keywordState: 'partial' | 'complete'
|
|
105
|
+
}) {
|
|
106
|
+
if (payload.markState.state === 'unchanged') {
|
|
107
|
+
const focusSpan = {
|
|
108
|
+
node: {
|
|
109
|
+
_key: payload.focusSpan.node._key,
|
|
110
|
+
_type: payload.focusSpan.node._type,
|
|
111
|
+
text: `${payload.focusSpanTextBefore}${payload.lastMatch.text}${payload.focusSpanTextAfter}`,
|
|
112
|
+
marks: payload.markState.marks,
|
|
113
|
+
},
|
|
114
|
+
path: payload.focusSpan.path,
|
|
115
|
+
textBefore: payload.focusSpanTextBefore,
|
|
116
|
+
textAfter: payload.focusSpanTextAfter,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (keywordState === 'complete') {
|
|
120
|
+
return [
|
|
121
|
+
raise(
|
|
122
|
+
createKeywordFoundEvent({
|
|
123
|
+
focusSpan,
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return [
|
|
130
|
+
raise(
|
|
131
|
+
createTriggerFoundEvent({
|
|
132
|
+
focusSpan,
|
|
133
|
+
}),
|
|
134
|
+
),
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const newSpan = {
|
|
139
|
+
_key: snapshot.context.keyGenerator(),
|
|
140
|
+
_type: payload.focusSpan.node._type,
|
|
141
|
+
text: payload.lastMatch.text,
|
|
142
|
+
marks: payload.markState.marks,
|
|
143
|
+
}
|
|
144
|
+
const focusSpan = {
|
|
145
|
+
node: {
|
|
146
|
+
_key: newSpan._key,
|
|
147
|
+
_type: newSpan._type,
|
|
148
|
+
text: `${newSpan.text}${payload.nextSpan?.node.text ?? ''}`,
|
|
149
|
+
marks: payload.markState.marks,
|
|
150
|
+
},
|
|
151
|
+
path: [
|
|
152
|
+
{_key: payload.focusSpan.path[0]._key},
|
|
153
|
+
'children',
|
|
154
|
+
{_key: newSpan._key},
|
|
155
|
+
] satisfies ChildPath,
|
|
156
|
+
textBefore: '',
|
|
157
|
+
textAfter: payload.nextSpan?.node.text ?? '',
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return [
|
|
161
|
+
raise({type: 'select', at: payload.lastMatch.targetOffsets}),
|
|
162
|
+
raise({type: 'delete', at: payload.lastMatch.targetOffsets}),
|
|
163
|
+
raise({type: 'insert.child', child: newSpan}),
|
|
164
|
+
...(keywordState === 'complete'
|
|
165
|
+
? [
|
|
166
|
+
raise(
|
|
167
|
+
createKeywordFoundEvent({
|
|
168
|
+
focusSpan,
|
|
169
|
+
}),
|
|
170
|
+
),
|
|
171
|
+
]
|
|
172
|
+
: [
|
|
173
|
+
raise(
|
|
174
|
+
createTriggerFoundEvent({
|
|
175
|
+
focusSpan,
|
|
176
|
+
}),
|
|
177
|
+
),
|
|
178
|
+
]),
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
|
|
52
182
|
/*******************
|
|
53
183
|
* Input Rules
|
|
54
184
|
*******************/
|
|
@@ -58,34 +188,39 @@ const escapeShortcut = createKeyboardShortcut({
|
|
|
58
188
|
*/
|
|
59
189
|
const triggerRule = defineInputRule({
|
|
60
190
|
on: /:/,
|
|
61
|
-
guard: ({event}) => {
|
|
191
|
+
guard: ({snapshot, event}) => {
|
|
62
192
|
const lastMatch = event.matches.at(-1)
|
|
63
193
|
|
|
64
194
|
if (lastMatch === undefined) {
|
|
65
195
|
return false
|
|
66
196
|
}
|
|
67
197
|
|
|
198
|
+
const triggerState = getTriggerState(snapshot)
|
|
199
|
+
|
|
200
|
+
if (!triggerState) {
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
|
|
68
204
|
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
point: lastMatch.selection.anchor,
|
|
72
|
-
blockOffset: lastMatch.targetOffsets.anchor,
|
|
73
|
-
},
|
|
74
|
-
keywordFocus: lastMatch.targetOffsets.focus,
|
|
205
|
+
lastMatch,
|
|
206
|
+
...triggerState,
|
|
75
207
|
}
|
|
76
208
|
},
|
|
77
|
-
actions: [
|
|
209
|
+
actions: [
|
|
210
|
+
({snapshot}, payload) =>
|
|
211
|
+
createTriggerActions({snapshot, payload, keywordState: 'partial'}),
|
|
212
|
+
],
|
|
78
213
|
})
|
|
79
214
|
|
|
80
215
|
type TriggerFoundEvent = ReturnType<typeof createTriggerFoundEvent>
|
|
81
216
|
|
|
82
217
|
function createTriggerFoundEvent(payload: {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
218
|
+
focusSpan: {
|
|
219
|
+
node: PortableTextSpan
|
|
220
|
+
path: ChildPath
|
|
221
|
+
textBefore: string
|
|
222
|
+
textAfter: string
|
|
87
223
|
}
|
|
88
|
-
keywordFocus: BlockOffset
|
|
89
224
|
}) {
|
|
90
225
|
return {
|
|
91
226
|
type: 'custom.trigger found',
|
|
@@ -98,76 +233,76 @@ function createTriggerFoundEvent(payload: {
|
|
|
98
233
|
*/
|
|
99
234
|
const partialKeywordRule = defineInputRule({
|
|
100
235
|
on: /:[\S]+/,
|
|
101
|
-
guard: ({event}) => {
|
|
236
|
+
guard: ({snapshot, event}) => {
|
|
102
237
|
const lastMatch = event.matches.at(-1)
|
|
103
238
|
|
|
104
239
|
if (lastMatch === undefined) {
|
|
105
240
|
return false
|
|
106
241
|
}
|
|
107
242
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
point: lastMatch.selection.anchor,
|
|
111
|
-
blockOffset: lastMatch.targetOffsets.anchor,
|
|
243
|
+
if (lastMatch.targetOffsets.anchor.offset < event.textBefore.length) {
|
|
244
|
+
return false
|
|
112
245
|
}
|
|
113
|
-
const keywordFocus = lastMatch.targetOffsets.focus
|
|
114
246
|
|
|
115
|
-
|
|
116
|
-
},
|
|
117
|
-
actions: [(_, payload) => [raise(createPartialKeywordFoundEvent(payload))]],
|
|
118
|
-
})
|
|
247
|
+
const triggerState = getTriggerState(snapshot)
|
|
119
248
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
249
|
+
if (!triggerState) {
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
123
252
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
...payload,
|
|
135
|
-
} as const
|
|
136
|
-
}
|
|
253
|
+
return {
|
|
254
|
+
...triggerState,
|
|
255
|
+
lastMatch,
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
actions: [
|
|
259
|
+
({snapshot}, payload) =>
|
|
260
|
+
createTriggerActions({snapshot, payload, keywordState: 'partial'}),
|
|
261
|
+
],
|
|
262
|
+
})
|
|
137
263
|
|
|
138
264
|
/**
|
|
139
265
|
* Listen for a complete keyword like ":joy:"
|
|
140
266
|
*/
|
|
141
267
|
const keywordRule = defineInputRule({
|
|
142
268
|
on: /:[\S]+:/,
|
|
143
|
-
guard: ({event}) => {
|
|
269
|
+
guard: ({snapshot, event}) => {
|
|
144
270
|
const lastMatch = event.matches.at(-1)
|
|
145
271
|
|
|
146
272
|
if (lastMatch === undefined) {
|
|
147
273
|
return false
|
|
148
274
|
}
|
|
149
275
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
276
|
+
if (lastMatch.targetOffsets.anchor.offset < event.textBefore.length) {
|
|
277
|
+
return false
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const triggerState = getTriggerState(snapshot)
|
|
281
|
+
|
|
282
|
+
if (!triggerState) {
|
|
283
|
+
return false
|
|
154
284
|
}
|
|
155
|
-
const keywordFocus = lastMatch.targetOffsets.focus
|
|
156
285
|
|
|
157
|
-
return {
|
|
286
|
+
return {
|
|
287
|
+
...triggerState,
|
|
288
|
+
lastMatch,
|
|
289
|
+
}
|
|
158
290
|
},
|
|
159
|
-
actions: [
|
|
291
|
+
actions: [
|
|
292
|
+
({snapshot}, payload) =>
|
|
293
|
+
createTriggerActions({snapshot, payload, keywordState: 'complete'}),
|
|
294
|
+
],
|
|
160
295
|
})
|
|
161
296
|
|
|
162
297
|
type KeywordFoundEvent = ReturnType<typeof createKeywordFoundEvent>
|
|
163
298
|
|
|
164
299
|
function createKeywordFoundEvent(payload: {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
300
|
+
focusSpan: {
|
|
301
|
+
node: PortableTextSpan
|
|
302
|
+
path: ChildPath
|
|
303
|
+
textBefore: string
|
|
304
|
+
textAfter: string
|
|
169
305
|
}
|
|
170
|
-
keywordFocus: BlockOffset
|
|
171
306
|
}) {
|
|
172
307
|
return {
|
|
173
308
|
type: 'custom.keyword found',
|
|
@@ -180,38 +315,25 @@ type EmojiPickerContext = {
|
|
|
180
315
|
matches: ReadonlyArray<BaseEmojiMatch>
|
|
181
316
|
matchEmojis: MatchEmojis<BaseEmojiMatch>
|
|
182
317
|
selectedIndex: number
|
|
183
|
-
|
|
318
|
+
focusSpan:
|
|
184
319
|
| {
|
|
185
|
-
|
|
186
|
-
|
|
320
|
+
node: PortableTextSpan
|
|
321
|
+
path: ChildPath
|
|
322
|
+
textBefore: string
|
|
323
|
+
textAfter: string
|
|
187
324
|
}
|
|
188
325
|
| undefined
|
|
189
|
-
keywordFocus: BlockOffset | undefined
|
|
190
326
|
incompleteKeywordRegex: RegExp
|
|
191
327
|
keyword: string
|
|
192
328
|
}
|
|
193
329
|
|
|
194
330
|
type EmojiPickerEvent =
|
|
195
331
|
| TriggerFoundEvent
|
|
196
|
-
| PartialKeywordFoundEvent
|
|
197
332
|
| KeywordFoundEvent
|
|
198
333
|
| {
|
|
199
334
|
type: 'selection changed'
|
|
200
335
|
snapshot: EditorSnapshot
|
|
201
336
|
}
|
|
202
|
-
| {
|
|
203
|
-
type: 'insert.text'
|
|
204
|
-
focus: EditorSelectionPoint
|
|
205
|
-
text: string
|
|
206
|
-
}
|
|
207
|
-
| {
|
|
208
|
-
type: 'delete.backward'
|
|
209
|
-
focus: EditorSelectionPoint
|
|
210
|
-
}
|
|
211
|
-
| {
|
|
212
|
-
type: 'delete.forward'
|
|
213
|
-
focus: EditorSelectionPoint
|
|
214
|
-
}
|
|
215
337
|
| {
|
|
216
338
|
type: 'dismiss'
|
|
217
339
|
}
|
|
@@ -252,21 +374,6 @@ const triggerListenerCallback: CallbackLogicFunction<
|
|
|
252
374
|
],
|
|
253
375
|
}),
|
|
254
376
|
}),
|
|
255
|
-
input.editor.registerBehavior({
|
|
256
|
-
behavior: defineBehavior<
|
|
257
|
-
PartialKeywordFoundEvent,
|
|
258
|
-
PartialKeywordFoundEvent['type']
|
|
259
|
-
>({
|
|
260
|
-
on: 'custom.partial keyword found',
|
|
261
|
-
actions: [
|
|
262
|
-
({event}) => [
|
|
263
|
-
effect(() => {
|
|
264
|
-
sendBack(event)
|
|
265
|
-
}),
|
|
266
|
-
],
|
|
267
|
-
],
|
|
268
|
-
}),
|
|
269
|
-
}),
|
|
270
377
|
input.editor.registerBehavior({
|
|
271
378
|
behavior: defineBehavior<TriggerFoundEvent, TriggerFoundEvent['type']>({
|
|
272
379
|
on: 'custom.trigger found',
|
|
@@ -357,8 +464,12 @@ const emojiInsertListener: CallbackLogicFunction<
|
|
|
357
464
|
return input.context.editor.registerBehavior({
|
|
358
465
|
behavior: defineBehavior<{
|
|
359
466
|
emoji: string
|
|
360
|
-
|
|
361
|
-
|
|
467
|
+
focusSpan: {
|
|
468
|
+
node: PortableTextSpan
|
|
469
|
+
path: ChildPath
|
|
470
|
+
textBefore: string
|
|
471
|
+
textAfter: string
|
|
472
|
+
}
|
|
362
473
|
}>({
|
|
363
474
|
on: 'custom.insert emoji',
|
|
364
475
|
actions: [
|
|
@@ -367,8 +478,19 @@ const emojiInsertListener: CallbackLogicFunction<
|
|
|
367
478
|
sendBack({type: 'dismiss'})
|
|
368
479
|
}),
|
|
369
480
|
raise({
|
|
370
|
-
type: 'delete
|
|
371
|
-
at: {
|
|
481
|
+
type: 'delete',
|
|
482
|
+
at: {
|
|
483
|
+
anchor: {
|
|
484
|
+
path: event.focusSpan.path,
|
|
485
|
+
offset: event.focusSpan.textBefore.length,
|
|
486
|
+
},
|
|
487
|
+
focus: {
|
|
488
|
+
path: event.focusSpan.path,
|
|
489
|
+
offset:
|
|
490
|
+
event.focusSpan.node.text.length -
|
|
491
|
+
event.focusSpan.textAfter.length,
|
|
492
|
+
},
|
|
493
|
+
},
|
|
372
494
|
}),
|
|
373
495
|
raise({
|
|
374
496
|
type: 'insert.text',
|
|
@@ -403,21 +525,17 @@ const submitListenerCallback: CallbackLogicFunction<
|
|
|
403
525
|
return false
|
|
404
526
|
}
|
|
405
527
|
|
|
406
|
-
const
|
|
407
|
-
const focus = context.keywordFocus
|
|
528
|
+
const focusSpan = context.focusSpan
|
|
408
529
|
const match = context.matches[context.selectedIndex]
|
|
409
530
|
|
|
410
|
-
return match &&
|
|
411
|
-
? {anchor, focus, emoji: match.emoji}
|
|
412
|
-
: false
|
|
531
|
+
return match && focusSpan ? {focusSpan, emoji: match.emoji} : false
|
|
413
532
|
},
|
|
414
533
|
actions: [
|
|
415
|
-
(_, {
|
|
534
|
+
(_, {focusSpan, emoji}) => [
|
|
416
535
|
raise({
|
|
417
536
|
type: 'custom.insert emoji',
|
|
418
537
|
emoji,
|
|
419
|
-
|
|
420
|
-
focus,
|
|
538
|
+
focusSpan,
|
|
421
539
|
}),
|
|
422
540
|
],
|
|
423
541
|
],
|
|
@@ -438,31 +556,6 @@ const submitListenerCallback: CallbackLogicFunction<
|
|
|
438
556
|
],
|
|
439
557
|
}),
|
|
440
558
|
}),
|
|
441
|
-
input.context.editor.registerBehavior({
|
|
442
|
-
behavior: defineInputRuleBehavior({
|
|
443
|
-
rules: [keywordRule],
|
|
444
|
-
}),
|
|
445
|
-
}),
|
|
446
|
-
input.context.editor.registerBehavior({
|
|
447
|
-
behavior: defineBehavior<
|
|
448
|
-
KeywordFoundEvent,
|
|
449
|
-
KeywordFoundEvent['type'],
|
|
450
|
-
{
|
|
451
|
-
anchor: BlockOffset
|
|
452
|
-
focus: BlockOffset
|
|
453
|
-
emoji: string
|
|
454
|
-
}
|
|
455
|
-
>({
|
|
456
|
-
on: 'custom.keyword found',
|
|
457
|
-
actions: [
|
|
458
|
-
({event}) => [
|
|
459
|
-
effect(() => {
|
|
460
|
-
sendBack(event)
|
|
461
|
-
}),
|
|
462
|
-
],
|
|
463
|
-
],
|
|
464
|
-
}),
|
|
465
|
-
}),
|
|
466
559
|
]
|
|
467
560
|
|
|
468
561
|
return () => {
|
|
@@ -485,81 +578,6 @@ const selectionListenerCallback: CallbackLogicFunction<
|
|
|
485
578
|
return subscription.unsubscribe
|
|
486
579
|
}
|
|
487
580
|
|
|
488
|
-
const textChangeListener: CallbackLogicFunction<
|
|
489
|
-
AnyEventObject,
|
|
490
|
-
EmojiPickerEvent,
|
|
491
|
-
{editor: Editor}
|
|
492
|
-
> = ({sendBack, input}) => {
|
|
493
|
-
const unregisterBehaviors = [
|
|
494
|
-
input.editor.registerBehavior({
|
|
495
|
-
behavior: defineBehavior({
|
|
496
|
-
on: 'insert.text',
|
|
497
|
-
guard: ({snapshot}) =>
|
|
498
|
-
snapshot.context.selection
|
|
499
|
-
? {focus: snapshot.context.selection.focus}
|
|
500
|
-
: false,
|
|
501
|
-
actions: [
|
|
502
|
-
({event}, {focus}) => [
|
|
503
|
-
effect(() => {
|
|
504
|
-
sendBack({
|
|
505
|
-
...event,
|
|
506
|
-
focus,
|
|
507
|
-
})
|
|
508
|
-
}),
|
|
509
|
-
forward(event),
|
|
510
|
-
],
|
|
511
|
-
],
|
|
512
|
-
}),
|
|
513
|
-
}),
|
|
514
|
-
input.editor.registerBehavior({
|
|
515
|
-
behavior: defineBehavior({
|
|
516
|
-
on: 'delete.backward',
|
|
517
|
-
guard: ({snapshot, event}) =>
|
|
518
|
-
event.unit === 'character' && snapshot.context.selection
|
|
519
|
-
? {focus: snapshot.context.selection.focus}
|
|
520
|
-
: false,
|
|
521
|
-
actions: [
|
|
522
|
-
({event}, {focus}) => [
|
|
523
|
-
effect(() => {
|
|
524
|
-
sendBack({
|
|
525
|
-
type: 'delete.backward',
|
|
526
|
-
focus,
|
|
527
|
-
})
|
|
528
|
-
}),
|
|
529
|
-
forward(event),
|
|
530
|
-
],
|
|
531
|
-
],
|
|
532
|
-
}),
|
|
533
|
-
}),
|
|
534
|
-
input.editor.registerBehavior({
|
|
535
|
-
behavior: defineBehavior({
|
|
536
|
-
on: 'delete.forward',
|
|
537
|
-
guard: ({snapshot, event}) =>
|
|
538
|
-
event.unit === 'character' && snapshot.context.selection
|
|
539
|
-
? {focus: snapshot.context.selection.focus}
|
|
540
|
-
: false,
|
|
541
|
-
actions: [
|
|
542
|
-
({event}, {focus}) => [
|
|
543
|
-
effect(() => {
|
|
544
|
-
sendBack({
|
|
545
|
-
type: 'delete.forward',
|
|
546
|
-
focus,
|
|
547
|
-
})
|
|
548
|
-
}),
|
|
549
|
-
forward(event),
|
|
550
|
-
],
|
|
551
|
-
],
|
|
552
|
-
}),
|
|
553
|
-
}),
|
|
554
|
-
]
|
|
555
|
-
|
|
556
|
-
return () => {
|
|
557
|
-
for (const unregister of unregisterBehaviors) {
|
|
558
|
-
unregister()
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
581
|
export const emojiPickerMachine = setup({
|
|
564
582
|
types: {
|
|
565
583
|
context: {} as EmojiPickerContext,
|
|
@@ -576,96 +594,106 @@ export const emojiPickerMachine = setup({
|
|
|
576
594
|
'trigger listener': fromCallback(triggerListenerCallback),
|
|
577
595
|
'escape listener': fromCallback(escapeListenerCallback),
|
|
578
596
|
'selection listener': fromCallback(selectionListenerCallback),
|
|
579
|
-
'text change listener': fromCallback(textChangeListener),
|
|
580
597
|
},
|
|
581
598
|
actions: {
|
|
582
|
-
'
|
|
583
|
-
|
|
599
|
+
'set focus span': assign({
|
|
600
|
+
focusSpan: ({context, event}) => {
|
|
584
601
|
if (
|
|
585
602
|
event.type !== 'custom.trigger found' &&
|
|
586
|
-
event.type !== 'custom.partial keyword found' &&
|
|
587
603
|
event.type !== 'custom.keyword found'
|
|
588
604
|
) {
|
|
589
|
-
return context.
|
|
605
|
+
return context.focusSpan
|
|
590
606
|
}
|
|
591
607
|
|
|
592
|
-
return event.
|
|
608
|
+
return event.focusSpan
|
|
593
609
|
},
|
|
594
610
|
}),
|
|
595
|
-
'
|
|
596
|
-
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
)
|
|
602
|
-
|
|
611
|
+
'update focus span': assign({
|
|
612
|
+
focusSpan: ({context}) => {
|
|
613
|
+
if (!context.focusSpan) {
|
|
614
|
+
return undefined
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const snapshot = context.editor.getSnapshot()
|
|
618
|
+
const focusSpan = getFocusSpan(snapshot)
|
|
619
|
+
|
|
620
|
+
if (!focusSpan) {
|
|
621
|
+
return undefined
|
|
603
622
|
}
|
|
604
623
|
|
|
605
|
-
return event.keywordAnchor
|
|
606
|
-
},
|
|
607
|
-
}),
|
|
608
|
-
'set keyword focus': assign({
|
|
609
|
-
keywordFocus: ({context, event}) => {
|
|
610
624
|
if (
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
event.type !== 'custom.keyword found'
|
|
625
|
+
JSON.stringify(focusSpan.path) !==
|
|
626
|
+
JSON.stringify(context.focusSpan.path)
|
|
614
627
|
) {
|
|
615
|
-
return
|
|
628
|
+
return undefined
|
|
616
629
|
}
|
|
617
630
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
'update keyword focus': assign({
|
|
622
|
-
keywordFocus: ({context, event}) => {
|
|
623
|
-
assertEvent(event, ['insert.text', 'delete.backward', 'delete.forward'])
|
|
631
|
+
if (!focusSpan.node.text.startsWith(context.focusSpan.textBefore)) {
|
|
632
|
+
return undefined
|
|
633
|
+
}
|
|
624
634
|
|
|
625
|
-
if (!context.
|
|
626
|
-
return
|
|
635
|
+
if (!focusSpan.node.text.endsWith(context.focusSpan.textAfter)) {
|
|
636
|
+
return undefined
|
|
627
637
|
}
|
|
628
638
|
|
|
629
|
-
|
|
630
|
-
path:
|
|
639
|
+
const keywordAnchor = {
|
|
640
|
+
path: focusSpan.path,
|
|
641
|
+
offset: context.focusSpan.textBefore.length,
|
|
642
|
+
}
|
|
643
|
+
const keywordFocus = {
|
|
644
|
+
path: focusSpan.path,
|
|
631
645
|
offset:
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
646
|
+
focusSpan.node.text.length - context.focusSpan.textAfter.length,
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const selectionIsBeforeKeyword =
|
|
650
|
+
isPointAfterSelection(keywordAnchor)(snapshot)
|
|
651
|
+
|
|
652
|
+
const selectionIsAfterKeyword =
|
|
653
|
+
isPointBeforeSelection(keywordFocus)(snapshot)
|
|
654
|
+
|
|
655
|
+
if (selectionIsBeforeKeyword || selectionIsAfterKeyword) {
|
|
656
|
+
return undefined
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
node: focusSpan.node,
|
|
661
|
+
path: focusSpan.path,
|
|
662
|
+
textBefore: context.focusSpan.textBefore,
|
|
663
|
+
textAfter: context.focusSpan.textAfter,
|
|
638
664
|
}
|
|
639
665
|
},
|
|
640
666
|
}),
|
|
641
667
|
'update keyword': assign({
|
|
642
|
-
keyword: ({context
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (!context.keywordAnchor || !context.keywordFocus) {
|
|
668
|
+
keyword: ({context}) => {
|
|
669
|
+
if (!context.focusSpan) {
|
|
646
670
|
return ''
|
|
647
671
|
}
|
|
648
672
|
|
|
649
|
-
|
|
650
|
-
context
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
673
|
+
if (
|
|
674
|
+
context.focusSpan.textBefore.length > 0 &&
|
|
675
|
+
context.focusSpan.textAfter.length > 0
|
|
676
|
+
) {
|
|
677
|
+
return context.focusSpan.node.text.slice(
|
|
678
|
+
context.focusSpan.textBefore.length,
|
|
679
|
+
-context.focusSpan.textAfter.length,
|
|
680
|
+
)
|
|
681
|
+
}
|
|
654
682
|
|
|
655
|
-
if (
|
|
656
|
-
return
|
|
683
|
+
if (context.focusSpan.textBefore.length > 0) {
|
|
684
|
+
return context.focusSpan.node.text.slice(
|
|
685
|
+
context.focusSpan.textBefore.length,
|
|
686
|
+
)
|
|
657
687
|
}
|
|
658
688
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
},
|
|
668
|
-
})
|
|
689
|
+
if (context.focusSpan.textAfter.length > 0) {
|
|
690
|
+
return context.focusSpan.node.text.slice(
|
|
691
|
+
0,
|
|
692
|
+
-context.focusSpan.textAfter.length,
|
|
693
|
+
)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return context.focusSpan.node.text
|
|
669
697
|
},
|
|
670
698
|
}),
|
|
671
699
|
'update matches': assign({
|
|
@@ -713,13 +741,6 @@ export const emojiPickerMachine = setup({
|
|
|
713
741
|
return event.index
|
|
714
742
|
},
|
|
715
743
|
}),
|
|
716
|
-
'update emoji insert listener context': sendTo(
|
|
717
|
-
'emoji insert listener',
|
|
718
|
-
({context}) => ({
|
|
719
|
-
type: 'context changed',
|
|
720
|
-
context,
|
|
721
|
-
}),
|
|
722
|
-
),
|
|
723
744
|
'update submit listener context': sendTo(
|
|
724
745
|
'submit listener',
|
|
725
746
|
({context}) => ({
|
|
@@ -727,118 +748,61 @@ export const emojiPickerMachine = setup({
|
|
|
727
748
|
context,
|
|
728
749
|
}),
|
|
729
750
|
),
|
|
730
|
-
'insert selected match': ({context
|
|
751
|
+
'insert selected match': ({context}) => {
|
|
731
752
|
const match = context.matches[context.selectedIndex]
|
|
732
753
|
|
|
733
|
-
if (!match || !context.
|
|
734
|
-
return
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
if (event.type === 'custom.keyword found' && match.type !== 'exact') {
|
|
754
|
+
if (!match || !context.focusSpan) {
|
|
738
755
|
return
|
|
739
756
|
}
|
|
740
757
|
|
|
741
758
|
context.editor.send({
|
|
742
759
|
type: 'custom.insert emoji',
|
|
743
760
|
emoji: match.emoji,
|
|
744
|
-
|
|
745
|
-
focus: context.keywordFocus,
|
|
761
|
+
focusSpan: context.focusSpan,
|
|
746
762
|
})
|
|
747
763
|
},
|
|
748
764
|
'reset': assign({
|
|
749
|
-
|
|
750
|
-
keywordFocus: undefined,
|
|
765
|
+
focusSpan: undefined,
|
|
751
766
|
keyword: '',
|
|
752
767
|
matches: [],
|
|
753
768
|
selectedIndex: 0,
|
|
754
769
|
}),
|
|
755
770
|
},
|
|
756
771
|
guards: {
|
|
772
|
+
'no focus span': ({context}) => {
|
|
773
|
+
return !context.focusSpan
|
|
774
|
+
},
|
|
757
775
|
'has matches': ({context}) => {
|
|
758
776
|
return context.matches.length > 0
|
|
759
777
|
},
|
|
760
778
|
'no matches': not('has matches'),
|
|
761
|
-
'keyword is
|
|
762
|
-
return context.incompleteKeywordRegex.test(context.keyword)
|
|
763
|
-
},
|
|
764
|
-
'keyword is malformed': not('keyword is wel-formed'),
|
|
765
|
-
'selection is before keyword': ({context, event}) => {
|
|
766
|
-
assertEvent(event, 'selection changed')
|
|
767
|
-
|
|
768
|
-
if (!context.keywordAnchor) {
|
|
769
|
-
return true
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
return selectors.isPointAfterSelection(context.keywordAnchor.point)(
|
|
773
|
-
event.snapshot,
|
|
774
|
-
)
|
|
775
|
-
},
|
|
776
|
-
'selection is after keyword': ({context, event}) => {
|
|
777
|
-
assertEvent(event, 'selection changed')
|
|
778
|
-
|
|
779
|
-
if (context.keywordFocus === undefined) {
|
|
780
|
-
return true
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
const keywordFocusPoint = utils.blockOffsetToSpanSelectionPoint({
|
|
784
|
-
context: event.snapshot.context,
|
|
785
|
-
blockOffset: context.keywordFocus,
|
|
786
|
-
direction: 'forward',
|
|
787
|
-
})
|
|
788
|
-
|
|
789
|
-
if (!keywordFocusPoint) {
|
|
790
|
-
return true
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
return selectors.isPointBeforeSelection(keywordFocusPoint)(event.snapshot)
|
|
779
|
+
'keyword is malformed': ({context}) => {
|
|
780
|
+
return !context.incompleteKeywordRegex.test(context.keyword)
|
|
794
781
|
},
|
|
795
|
-
'
|
|
796
|
-
|
|
782
|
+
'keyword is direct match': ({context}) => {
|
|
783
|
+
const fullKeywordRegex = /^:[\S]+:$/
|
|
797
784
|
|
|
798
|
-
|
|
799
|
-
},
|
|
800
|
-
'selection moved unexpectedly': or([
|
|
801
|
-
'selection is before keyword',
|
|
802
|
-
'selection is after keyword',
|
|
803
|
-
'selection is expanded',
|
|
804
|
-
]),
|
|
805
|
-
'unexpected text insertion': ({context, event}) => {
|
|
806
|
-
if (event.type !== 'insert.text') {
|
|
785
|
+
if (!fullKeywordRegex.test(context.keyword)) {
|
|
807
786
|
return false
|
|
808
787
|
}
|
|
809
788
|
|
|
810
|
-
|
|
789
|
+
const match = context.matches.at(context.selectedIndex)
|
|
790
|
+
|
|
791
|
+
if (!match || match.type !== 'exact') {
|
|
811
792
|
return false
|
|
812
793
|
}
|
|
813
794
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const textInsertedBeforeKeyword =
|
|
817
|
-
selectors.isPointBeforeSelection(event.focus)({
|
|
818
|
-
...snapshot,
|
|
819
|
-
context: {
|
|
820
|
-
...snapshot.context,
|
|
821
|
-
selection: {
|
|
822
|
-
anchor: context.keywordAnchor.point,
|
|
823
|
-
focus: context.keywordAnchor.point,
|
|
824
|
-
},
|
|
825
|
-
},
|
|
826
|
-
}) ||
|
|
827
|
-
utils.isEqualSelectionPoints(event.focus, context.keywordAnchor.point)
|
|
828
|
-
|
|
829
|
-
return textInsertedBeforeKeyword
|
|
795
|
+
return true
|
|
830
796
|
},
|
|
831
797
|
},
|
|
832
798
|
}).createMachine({
|
|
833
|
-
/** @xstate-layout N4IgpgJg5mDOIC5RgLYHsBWBLABABywGMBrMAJwDosIAbMAYkLRrQDsctXZyAXSAbQAMAXUSg8aWFh5Y2YkAA9EARmUB2AJwUAzADYNADgCs65YO1q1ygDQgAnojUG1O5c+W7tAJmdfdRgF8A21RMXAIScgpuAEMyQgALTih6Tm4yHgo+BR4hUSQQCSkZOQKlBABaZW0DHSMAFksNQUF6oyNzL1sHBC9vCnrGtS8GtQaNNt0gkPRsfCJSSlj4pNYUiDA6PgoAMzQyAHc4iDz5IulZVnlyr3MKE30LA31DZoNuxD6vAaHdP29vOo1NNwLNwgsostEsl6BstmAKAAjGIkI5kE4iM6SC6lUDlerabT3arDfS3eoaPQfXr9QaWEaNcaTEGhOYRRbRMBxaFrWFYWAofmwU4Fc4lK5lFTqWqDAwUjReeoGbwaNTU1TPCh-QxNNS6XQGHwssHzSJLLkrGHcOiEcU4RIxNYCTGi7Hi66IfxE1oeZX1EbeZXUhUUZSKjxOOV6bTmY1hU0cqGrFLWsC2y72hKOmAnZT5cRuy4ehDVXQUVXDIyWGpmAzveyfLRKwQaVRVwR1ixeONsiHm7nJ+gigvFIuSkvKVuhgwaEwKmMKwbqkaCAaverqVuvKbBUHx9mQi08qAUVhoHAoGI8RJwHCwBJoA4w4eFQu4xSfaoDA3KOmCVSTn06r-i4-hGH0ZiEoI4H1D24JmpyA7JNED5PmsF5XjesD0KwMQAG5YFAV5gDgECPqwL5imOeKIIM9ShsoRh1i2erPB41L6C40GqISUb6n8cEJoeSFrChj7JBh14JHAOH4YRxE4AArnglFvhKNEIGoq4+E0yraPULa6PUHGqhQ3HVDUBL8dogkHv2lqife4noZeUkybhBFEXwOA8Ggqmju+5RGPqFBqP6ujDD4v4tjYDYIMqLjVKo5gdM4TGBLurLwYmR7JmJaFQJJWGpFwvB3psaZ8BARUJP5OLqR+CD1Loq5ymYfyeP4spGOqv70fpv7NBSBKtBlMz7n2iEOSeTkFTVMl1e645eB49wGP+K0UpBbjaMBzQUF4IzBYq7TNa2QS7meGzwAUWVCWQWIBQ15RVJq2ijJoLRtB03jUhUVYUHKtz-gqeoxkYGi2ZN1B0I99XFqobTlh4-5-Ot4H1j0ZirbcENhR4RmKrBmUmnZU3HnDS0aVUjHlh2cqtkqfTBdSSr0RMGieBS7htASUMIUmyFnvNsB3qhySU9RjUVBFdN1ltTPvbowZOGZEWHe9xgTHo-M5SJM3iy5mHSTdI7w+OlnlgY6heJzbgUv4ytxaqtSCBFg1eJFM4XQEQA */
|
|
834
799
|
id: 'emoji picker',
|
|
835
800
|
context: ({input}) => ({
|
|
836
801
|
editor: input.editor,
|
|
837
802
|
keyword: '',
|
|
838
|
-
|
|
839
|
-
keywordFocus: undefined,
|
|
803
|
+
focusSpan: undefined,
|
|
840
804
|
matchEmojis: input.matchEmojis,
|
|
841
|
-
incompleteKeywordRegex:
|
|
805
|
+
incompleteKeywordRegex: /^:[\S]*$/,
|
|
842
806
|
matches: [],
|
|
843
807
|
selectedIndex: 0,
|
|
844
808
|
}),
|
|
@@ -860,17 +824,12 @@ export const emojiPickerMachine = setup({
|
|
|
860
824
|
on: {
|
|
861
825
|
'custom.trigger found': {
|
|
862
826
|
target: 'searching',
|
|
863
|
-
actions: ['set
|
|
864
|
-
},
|
|
865
|
-
'custom.partial keyword found': {
|
|
866
|
-
target: 'searching',
|
|
867
|
-
actions: ['set keyword anchor', 'set keyword focus', 'init keyword'],
|
|
827
|
+
actions: ['set focus span', 'update keyword'],
|
|
868
828
|
},
|
|
869
829
|
'custom.keyword found': {
|
|
870
830
|
actions: [
|
|
871
|
-
'set
|
|
872
|
-
'
|
|
873
|
-
'init keyword',
|
|
831
|
+
'set focus span',
|
|
832
|
+
'update keyword',
|
|
874
833
|
'update matches',
|
|
875
834
|
'insert selected match',
|
|
876
835
|
],
|
|
@@ -894,46 +853,15 @@ export const emojiPickerMachine = setup({
|
|
|
894
853
|
src: 'selection listener',
|
|
895
854
|
input: ({context}) => ({editor: context.editor}),
|
|
896
855
|
},
|
|
897
|
-
{
|
|
898
|
-
src: 'text change listener',
|
|
899
|
-
input: ({context}) => ({editor: context.editor}),
|
|
900
|
-
},
|
|
901
856
|
],
|
|
902
857
|
on: {
|
|
903
|
-
'custom.keyword found': {
|
|
904
|
-
actions: [
|
|
905
|
-
'set keyword anchor',
|
|
906
|
-
'set keyword focus',
|
|
907
|
-
'init keyword',
|
|
908
|
-
'update matches',
|
|
909
|
-
'insert selected match',
|
|
910
|
-
],
|
|
911
|
-
},
|
|
912
|
-
'insert.text': [
|
|
913
|
-
{
|
|
914
|
-
guard: 'unexpected text insertion',
|
|
915
|
-
target: 'idle',
|
|
916
|
-
},
|
|
917
|
-
{
|
|
918
|
-
actions: ['update keyword focus'],
|
|
919
|
-
},
|
|
920
|
-
],
|
|
921
|
-
'delete.forward': {
|
|
922
|
-
actions: ['update keyword focus'],
|
|
923
|
-
},
|
|
924
|
-
'delete.backward': {
|
|
925
|
-
actions: ['update keyword focus'],
|
|
926
|
-
},
|
|
927
858
|
'dismiss': {
|
|
928
859
|
target: 'idle',
|
|
929
860
|
},
|
|
930
861
|
'selection changed': [
|
|
931
|
-
{
|
|
932
|
-
guard: 'selection moved unexpectedly',
|
|
933
|
-
target: 'idle',
|
|
934
|
-
},
|
|
935
862
|
{
|
|
936
863
|
actions: [
|
|
864
|
+
'update focus span',
|
|
937
865
|
'update keyword',
|
|
938
866
|
'update matches',
|
|
939
867
|
'reset selected index',
|
|
@@ -943,10 +871,19 @@ export const emojiPickerMachine = setup({
|
|
|
943
871
|
],
|
|
944
872
|
},
|
|
945
873
|
always: [
|
|
874
|
+
{
|
|
875
|
+
guard: 'no focus span',
|
|
876
|
+
target: 'idle',
|
|
877
|
+
},
|
|
946
878
|
{
|
|
947
879
|
guard: 'keyword is malformed',
|
|
948
880
|
target: 'idle',
|
|
949
881
|
},
|
|
882
|
+
{
|
|
883
|
+
guard: 'keyword is direct match',
|
|
884
|
+
actions: ['insert selected match'],
|
|
885
|
+
target: 'idle',
|
|
886
|
+
},
|
|
950
887
|
],
|
|
951
888
|
initial: 'no matches showing',
|
|
952
889
|
states: {
|