@portabletext/editor 1.16.4 → 1.17.1
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 +1 -1
- package/lib/_chunks-cjs/behavior.core.cjs +5 -12
- package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
- package/lib/_chunks-cjs/selector.get-text-before.cjs +3 -10
- package/lib/_chunks-cjs/selector.get-text-before.cjs.map +1 -1
- package/lib/_chunks-cjs/selector.is-selection-collapsed.cjs +1 -4
- package/lib/_chunks-cjs/selector.is-selection-collapsed.cjs.map +1 -1
- package/lib/_chunks-es/behavior.core.js +5 -12
- package/lib/_chunks-es/behavior.core.js.map +1 -1
- package/lib/_chunks-es/selector.get-text-before.js +3 -10
- package/lib/_chunks-es/selector.get-text-before.js.map +1 -1
- package/lib/_chunks-es/selector.is-selection-collapsed.js +1 -4
- package/lib/_chunks-es/selector.is-selection-collapsed.js.map +1 -1
- package/lib/behaviors/index.cjs +318 -37
- package/lib/behaviors/index.cjs.map +1 -1
- package/lib/behaviors/index.d.cts +46 -0
- package/lib/behaviors/index.d.ts +46 -0
- package/lib/behaviors/index.js +320 -38
- package/lib/behaviors/index.js.map +1 -1
- package/lib/index.cjs +745 -1153
- package/lib/index.cjs.map +1 -1
- package/lib/index.js +745 -1153
- package/lib/index.js.map +1 -1
- package/lib/selectors/index.cjs +7 -19
- package/lib/selectors/index.cjs.map +1 -1
- package/lib/selectors/index.js +7 -19
- package/lib/selectors/index.js.map +1 -1
- package/package.json +11 -11
- package/src/behaviors/behavior.emoji-picker.ts +416 -0
- package/src/behaviors/index.ts +4 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import {assertEvent, assign, createActor, setup} from 'xstate'
|
|
2
|
+
import * as selectors from '../selectors'
|
|
3
|
+
import {isHotkey} from '../utils/is-hotkey'
|
|
4
|
+
import {defineBehavior} from './behavior.types'
|
|
5
|
+
|
|
6
|
+
const emojiCharRegEx = /^[a-zA-Z-_0-9]{1}$/
|
|
7
|
+
const incompleteEmojiRegEx = /:([a-zA-Z-_0-9]+)$/
|
|
8
|
+
const emojiRegEx = /:([a-zA-Z-_0-9]+):$/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @alpha
|
|
12
|
+
*/
|
|
13
|
+
export type EmojiPickerBehaviorsConfig<TEmojiMatch> = {
|
|
14
|
+
/**
|
|
15
|
+
* Match emojis by keyword.
|
|
16
|
+
*/
|
|
17
|
+
matchEmojis: ({keyword}: {keyword: string}) => Array<TEmojiMatch>
|
|
18
|
+
onMatchesChanged: ({matches}: {matches: Array<TEmojiMatch>}) => void
|
|
19
|
+
onSelectedIndexChanged: ({selectedIndex}: {selectedIndex: number}) => void
|
|
20
|
+
/**
|
|
21
|
+
* Parse an emoji match to a string that will be inserted into the editor.
|
|
22
|
+
*/
|
|
23
|
+
parseMatch: ({match}: {match: TEmojiMatch}) => string | undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @alpha
|
|
28
|
+
*/
|
|
29
|
+
export function createEmojiPickerBehaviors<TEmojiMatch>(
|
|
30
|
+
config: EmojiPickerBehaviorsConfig<TEmojiMatch>,
|
|
31
|
+
) {
|
|
32
|
+
const emojiPickerActor = createActor(createEmojiPickerMachine<TEmojiMatch>())
|
|
33
|
+
emojiPickerActor.start()
|
|
34
|
+
emojiPickerActor.subscribe((state) => {
|
|
35
|
+
config.onMatchesChanged({matches: state.context.matches})
|
|
36
|
+
config.onSelectedIndexChanged({selectedIndex: state.context.selectedIndex})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
defineBehavior({
|
|
41
|
+
on: 'insert.text',
|
|
42
|
+
guard: ({context, event}) => {
|
|
43
|
+
const isEmojiChar = emojiCharRegEx.test(event.text)
|
|
44
|
+
|
|
45
|
+
if (!isEmojiChar) {
|
|
46
|
+
return {emojis: []}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const focusBlock = selectors.getFocusTextBlock({context})
|
|
50
|
+
const textBefore = selectors.getBlockTextBefore({context})
|
|
51
|
+
const emojiKeyword = `${textBefore}${event.text}`.match(
|
|
52
|
+
incompleteEmojiRegEx,
|
|
53
|
+
)?.[1]
|
|
54
|
+
|
|
55
|
+
if (!focusBlock || emojiKeyword === undefined) {
|
|
56
|
+
return {emojis: []}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const emojis = config.matchEmojis({keyword: emojiKeyword})
|
|
60
|
+
|
|
61
|
+
return {emojis}
|
|
62
|
+
},
|
|
63
|
+
actions: [
|
|
64
|
+
(_, params) => [
|
|
65
|
+
{
|
|
66
|
+
type: 'effect',
|
|
67
|
+
effect: () => {
|
|
68
|
+
emojiPickerActor.send({
|
|
69
|
+
type: 'emojis found',
|
|
70
|
+
matches: params.emojis,
|
|
71
|
+
})
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
],
|
|
76
|
+
}),
|
|
77
|
+
defineBehavior({
|
|
78
|
+
on: 'insert.text',
|
|
79
|
+
guard: ({context, event}) => {
|
|
80
|
+
const isColon = event.text === ':'
|
|
81
|
+
|
|
82
|
+
if (!isColon) {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const matches = emojiPickerActor.getSnapshot().context.matches
|
|
87
|
+
const selectedIndex =
|
|
88
|
+
emojiPickerActor.getSnapshot().context.selectedIndex
|
|
89
|
+
const emoji = matches[selectedIndex]
|
|
90
|
+
? config.parseMatch({match: matches[selectedIndex]})
|
|
91
|
+
: undefined
|
|
92
|
+
|
|
93
|
+
const focusBlock = selectors.getFocusTextBlock({context})
|
|
94
|
+
const textBefore = selectors.getBlockTextBefore({context})
|
|
95
|
+
const emojiKeyword = `${textBefore}:`.match(emojiRegEx)?.[1]
|
|
96
|
+
|
|
97
|
+
if (!focusBlock || emojiKeyword === undefined) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const emojiStringLength = emojiKeyword.length + 2
|
|
102
|
+
|
|
103
|
+
if (emoji) {
|
|
104
|
+
return {
|
|
105
|
+
focusBlock,
|
|
106
|
+
emoji,
|
|
107
|
+
emojiStringLength,
|
|
108
|
+
textBeforeLength: textBefore.length + 1,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return false
|
|
113
|
+
},
|
|
114
|
+
actions: [
|
|
115
|
+
() => [
|
|
116
|
+
{
|
|
117
|
+
type: 'insert.text',
|
|
118
|
+
text: ':',
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
(_, params) => [
|
|
122
|
+
{
|
|
123
|
+
type: 'effect',
|
|
124
|
+
effect: () => {
|
|
125
|
+
emojiPickerActor.send({type: 'select'})
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
type: 'delete.text',
|
|
130
|
+
anchor: {
|
|
131
|
+
path: params.focusBlock.path,
|
|
132
|
+
offset: params.textBeforeLength - params.emojiStringLength,
|
|
133
|
+
},
|
|
134
|
+
focus: {
|
|
135
|
+
path: params.focusBlock.path,
|
|
136
|
+
offset: params.textBeforeLength,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'insert.text',
|
|
141
|
+
text: params.emoji,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
],
|
|
145
|
+
}),
|
|
146
|
+
defineBehavior({
|
|
147
|
+
on: 'key.down',
|
|
148
|
+
guard: ({context, event}) => {
|
|
149
|
+
const isShift = isHotkey('Shift', event.keyboardEvent)
|
|
150
|
+
const isColon = event.keyboardEvent.key === ':'
|
|
151
|
+
const isEmojiChar = emojiCharRegEx.test(event.keyboardEvent.key)
|
|
152
|
+
|
|
153
|
+
if (isShift || isColon || isEmojiChar) {
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const isArrowDown = isHotkey('ArrowDown', event.keyboardEvent)
|
|
158
|
+
const isArrowUp = isHotkey('ArrowUp', event.keyboardEvent)
|
|
159
|
+
const isEnter = isHotkey('Enter', event.keyboardEvent)
|
|
160
|
+
const isTab = isHotkey('Tab', event.keyboardEvent)
|
|
161
|
+
const matches = emojiPickerActor.getSnapshot().context.matches
|
|
162
|
+
|
|
163
|
+
if (isEnter || isTab) {
|
|
164
|
+
const selectedIndex =
|
|
165
|
+
emojiPickerActor.getSnapshot().context.selectedIndex
|
|
166
|
+
|
|
167
|
+
const emoji = matches[selectedIndex]
|
|
168
|
+
? config.parseMatch({match: matches[selectedIndex]})
|
|
169
|
+
: undefined
|
|
170
|
+
|
|
171
|
+
if (!emoji) {
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const focusBlock = selectors.getFocusTextBlock({context})
|
|
176
|
+
const textBefore = selectors.getBlockTextBefore({context})
|
|
177
|
+
const emojiKeyword = textBefore.match(incompleteEmojiRegEx)?.[1]
|
|
178
|
+
|
|
179
|
+
if (!focusBlock || emojiKeyword === undefined) {
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const emojiStringLength = emojiKeyword.length + 1
|
|
184
|
+
|
|
185
|
+
if (emoji) {
|
|
186
|
+
return {
|
|
187
|
+
action: 'select' as const,
|
|
188
|
+
focusBlock,
|
|
189
|
+
emoji,
|
|
190
|
+
emojiStringLength,
|
|
191
|
+
textBeforeLength: textBefore.length,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (isArrowDown && matches.length > 0) {
|
|
199
|
+
return {action: 'navigate down' as const}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (isArrowUp && matches.length > 0) {
|
|
203
|
+
return {action: 'navigate up' as const}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {action: 'reset' as const}
|
|
207
|
+
},
|
|
208
|
+
actions: [
|
|
209
|
+
(_, params) => {
|
|
210
|
+
if (params.action === 'select') {
|
|
211
|
+
return [
|
|
212
|
+
{
|
|
213
|
+
type: 'effect',
|
|
214
|
+
effect: () => {
|
|
215
|
+
emojiPickerActor.send({type: 'select'})
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: 'delete.text',
|
|
220
|
+
anchor: {
|
|
221
|
+
path: params.focusBlock.path,
|
|
222
|
+
offset: params.textBeforeLength - params.emojiStringLength,
|
|
223
|
+
},
|
|
224
|
+
focus: {
|
|
225
|
+
path: params.focusBlock.path,
|
|
226
|
+
offset: params.textBeforeLength,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
type: 'insert.text',
|
|
231
|
+
text: params.emoji,
|
|
232
|
+
},
|
|
233
|
+
]
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (params.action === 'navigate up') {
|
|
237
|
+
return [
|
|
238
|
+
// If we are navigating then we want to hijack the key event and
|
|
239
|
+
// turn it into a noop.
|
|
240
|
+
{
|
|
241
|
+
type: 'noop',
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
type: 'effect',
|
|
245
|
+
effect: () => {
|
|
246
|
+
emojiPickerActor.send({type: 'navigate up'})
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (params.action === 'navigate down') {
|
|
253
|
+
return [
|
|
254
|
+
// If we are navigating then we want to hijack the key event and
|
|
255
|
+
// turn it into a noop.
|
|
256
|
+
{
|
|
257
|
+
type: 'noop',
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
type: 'effect',
|
|
261
|
+
effect: () => {
|
|
262
|
+
emojiPickerActor.send({type: 'navigate down'})
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return [
|
|
269
|
+
{
|
|
270
|
+
type: 'effect',
|
|
271
|
+
effect: () => {
|
|
272
|
+
emojiPickerActor.send({type: 'reset'})
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
]
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
}),
|
|
279
|
+
defineBehavior({
|
|
280
|
+
on: 'delete.backward',
|
|
281
|
+
guard: ({context, event}) => {
|
|
282
|
+
if (event.unit !== 'character') {
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const matches = emojiPickerActor.getSnapshot().context.matches
|
|
287
|
+
|
|
288
|
+
if (matches.length === 0) {
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const focusBlock = selectors.getFocusTextBlock({context})
|
|
293
|
+
const textBefore = selectors.getBlockTextBefore({context})
|
|
294
|
+
const emojiKeyword = textBefore
|
|
295
|
+
.slice(0, textBefore.length - 1)
|
|
296
|
+
.match(incompleteEmojiRegEx)?.[1]
|
|
297
|
+
|
|
298
|
+
if (!focusBlock || emojiKeyword === undefined) {
|
|
299
|
+
return {emojis: []}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const emojis = config.matchEmojis({keyword: emojiKeyword})
|
|
303
|
+
|
|
304
|
+
return {emojis}
|
|
305
|
+
},
|
|
306
|
+
actions: [
|
|
307
|
+
(_, params) => [
|
|
308
|
+
{
|
|
309
|
+
type: 'effect',
|
|
310
|
+
effect: () => {
|
|
311
|
+
emojiPickerActor.send({
|
|
312
|
+
type: 'emojis found',
|
|
313
|
+
matches: params.emojis,
|
|
314
|
+
})
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
],
|
|
319
|
+
}),
|
|
320
|
+
]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function createEmojiPickerMachine<TEmojiSearchResult>() {
|
|
324
|
+
return setup({
|
|
325
|
+
types: {
|
|
326
|
+
context: {} as {
|
|
327
|
+
matches: Array<TEmojiSearchResult>
|
|
328
|
+
selectedIndex: number
|
|
329
|
+
},
|
|
330
|
+
events: {} as
|
|
331
|
+
| {
|
|
332
|
+
type: 'emojis found'
|
|
333
|
+
matches: Array<TEmojiSearchResult>
|
|
334
|
+
}
|
|
335
|
+
| {
|
|
336
|
+
type: 'navigate down' | 'navigate up' | 'select' | 'reset'
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
actions: {
|
|
340
|
+
'assign matches': assign({
|
|
341
|
+
matches: ({event}) => {
|
|
342
|
+
assertEvent(event, 'emojis found')
|
|
343
|
+
return event.matches
|
|
344
|
+
},
|
|
345
|
+
}),
|
|
346
|
+
'reset matches': assign({
|
|
347
|
+
matches: [],
|
|
348
|
+
}),
|
|
349
|
+
'reset selected index': assign({
|
|
350
|
+
selectedIndex: 0,
|
|
351
|
+
}),
|
|
352
|
+
'increment selected index': assign({
|
|
353
|
+
selectedIndex: ({context}) => {
|
|
354
|
+
if (context.selectedIndex === context.matches.length - 1) {
|
|
355
|
+
return 0
|
|
356
|
+
}
|
|
357
|
+
return context.selectedIndex + 1
|
|
358
|
+
},
|
|
359
|
+
}),
|
|
360
|
+
'decrement selected index': assign({
|
|
361
|
+
selectedIndex: ({context}) => {
|
|
362
|
+
if (context.selectedIndex === 0) {
|
|
363
|
+
return context.matches.length - 1
|
|
364
|
+
}
|
|
365
|
+
return context.selectedIndex - 1
|
|
366
|
+
},
|
|
367
|
+
}),
|
|
368
|
+
},
|
|
369
|
+
guards: {
|
|
370
|
+
'no matches': ({context}) => context.matches.length === 0,
|
|
371
|
+
},
|
|
372
|
+
}).createMachine({
|
|
373
|
+
id: 'emoji picker',
|
|
374
|
+
context: {
|
|
375
|
+
matches: [],
|
|
376
|
+
selectedIndex: 0,
|
|
377
|
+
},
|
|
378
|
+
initial: 'idle',
|
|
379
|
+
states: {
|
|
380
|
+
'idle': {
|
|
381
|
+
on: {
|
|
382
|
+
'emojis found': {
|
|
383
|
+
actions: 'assign matches',
|
|
384
|
+
target: 'showing matches',
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
'showing matches': {
|
|
389
|
+
always: {
|
|
390
|
+
guard: 'no matches',
|
|
391
|
+
target: 'idle',
|
|
392
|
+
},
|
|
393
|
+
exit: ['reset selected index'],
|
|
394
|
+
on: {
|
|
395
|
+
'emojis found': {
|
|
396
|
+
actions: 'assign matches',
|
|
397
|
+
},
|
|
398
|
+
'navigate down': {
|
|
399
|
+
actions: 'increment selected index',
|
|
400
|
+
},
|
|
401
|
+
'navigate up': {
|
|
402
|
+
actions: 'decrement selected index',
|
|
403
|
+
},
|
|
404
|
+
'reset': {
|
|
405
|
+
target: 'idle',
|
|
406
|
+
actions: ['reset selected index', 'reset matches'],
|
|
407
|
+
},
|
|
408
|
+
'select': {
|
|
409
|
+
target: 'idle',
|
|
410
|
+
actions: ['reset selected index', 'reset matches'],
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
})
|
|
416
|
+
}
|
package/src/behaviors/index.ts
CHANGED
|
@@ -11,6 +11,10 @@ export {
|
|
|
11
11
|
type CodeEditorBehaviorsConfig,
|
|
12
12
|
} from './behavior.code-editor'
|
|
13
13
|
export {coreBehavior, coreBehaviors} from './behavior.core'
|
|
14
|
+
export {
|
|
15
|
+
type EmojiPickerBehaviorsConfig,
|
|
16
|
+
createEmojiPickerBehaviors,
|
|
17
|
+
} from './behavior.emoji-picker'
|
|
14
18
|
export {createLinkBehaviors, type LinkBehaviorsConfig} from './behavior.links'
|
|
15
19
|
export {
|
|
16
20
|
createMarkdownBehaviors,
|