@portabletext/editor 1.15.3 → 1.16.0

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.
Files changed (70) hide show
  1. package/lib/_chunks-cjs/behavior.core.cjs +25 -25
  2. package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
  3. package/lib/_chunks-cjs/selector.get-text-before.cjs +14 -14
  4. package/lib/_chunks-cjs/selector.get-text-before.cjs.map +1 -1
  5. package/lib/_chunks-cjs/{selectors.cjs → selector.is-selection-collapsed.cjs} +8 -8
  6. package/lib/_chunks-cjs/selector.is-selection-collapsed.cjs.map +1 -0
  7. package/lib/_chunks-es/behavior.core.js +7 -7
  8. package/lib/_chunks-es/behavior.core.js.map +1 -1
  9. package/lib/_chunks-es/selector.get-text-before.js +14 -14
  10. package/lib/_chunks-es/selector.get-text-before.js.map +1 -1
  11. package/lib/_chunks-es/{selectors.js → selector.is-selection-collapsed.js} +8 -8
  12. package/lib/_chunks-es/selector.is-selection-collapsed.js.map +1 -0
  13. package/lib/behaviors/index.cjs +23 -23
  14. package/lib/behaviors/index.cjs.map +1 -1
  15. package/lib/behaviors/index.d.cts +1 -0
  16. package/lib/behaviors/index.d.ts +1 -0
  17. package/lib/behaviors/index.js +8 -8
  18. package/lib/behaviors/index.js.map +1 -1
  19. package/lib/index.cjs +855 -515
  20. package/lib/index.cjs.map +1 -1
  21. package/lib/index.d.cts +4126 -4834
  22. package/lib/index.d.ts +4126 -4834
  23. package/lib/index.js +852 -514
  24. package/lib/index.js.map +1 -1
  25. package/lib/selectors/index.cjs +166 -16
  26. package/lib/selectors/index.cjs.map +1 -1
  27. package/lib/selectors/index.d.cts +54 -5
  28. package/lib/selectors/index.d.ts +54 -5
  29. package/lib/selectors/index.js +154 -3
  30. package/lib/selectors/index.js.map +1 -1
  31. package/package.json +11 -11
  32. package/src/behaviors/behavior.code-editor.ts +5 -9
  33. package/src/behaviors/behavior.core.block-objects.ts +13 -19
  34. package/src/behaviors/behavior.core.lists.ts +11 -17
  35. package/src/behaviors/behavior.links.ts +4 -4
  36. package/src/behaviors/behavior.markdown.ts +16 -21
  37. package/src/editor/Editable.tsx +11 -4
  38. package/src/editor/PortableTextEditor.tsx +2 -3
  39. package/src/editor/{hooks/useSyncValue.test.tsx → __tests__/sync-value.test.tsx} +42 -23
  40. package/src/editor/components/Synchronizer.tsx +53 -80
  41. package/src/editor/editor-machine.ts +129 -80
  42. package/src/editor/editor-provider.tsx +0 -3
  43. package/src/editor/editor-selector.ts +5 -0
  44. package/src/editor/editor-snapshot.ts +1 -0
  45. package/src/editor/get-active-decorators.ts +20 -0
  46. package/src/editor/mutation-machine.ts +100 -0
  47. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +21 -15
  48. package/src/editor/plugins/createWithMaxBlocks.ts +1 -1
  49. package/src/editor/plugins/createWithPatches.ts +0 -4
  50. package/src/editor/plugins/createWithPlaceholderBlock.ts +1 -1
  51. package/src/editor/plugins/createWithPortableTextSelections.ts +4 -1
  52. package/src/editor/plugins/createWithUndoRedo.ts +3 -3
  53. package/src/editor/sync-machine.ts +657 -0
  54. package/src/editor/withSyncRangeDecorations.ts +17 -5
  55. package/src/selectors/_exports/index.ts +1 -0
  56. package/src/selectors/index.ts +9 -1
  57. package/src/selectors/selector.get-active-style.ts +37 -0
  58. package/src/selectors/selector.get-selected-spans.ts +136 -0
  59. package/src/selectors/selector.is-active-annotation.ts +49 -0
  60. package/src/selectors/selector.is-active-decorator.ts +21 -0
  61. package/src/selectors/selector.is-active-list-item.ts +13 -0
  62. package/src/selectors/selector.is-active-style.ts +13 -0
  63. package/src/selectors/selector.is-selection-collapsed.ts +12 -0
  64. package/src/selectors/selector.is-selection-expanded.ts +9 -0
  65. package/src/selectors/selectors.ts +0 -11
  66. package/src/utils/weakMaps.ts +0 -3
  67. package/src/utils/withChanges.ts +1 -8
  68. package/lib/_chunks-cjs/selectors.cjs.map +0 -1
  69. package/lib/_chunks-es/selectors.js.map +0 -1
  70. package/src/editor/hooks/useSyncValue.ts +0 -426
@@ -1,30 +1,18 @@
1
- import type {Patch} from '@portabletext/patches'
2
- import type {PortableTextBlock} from '@sanity/types'
3
- import {useSelector} from '@xstate/react'
4
- import {throttle} from 'lodash'
5
- import {useCallback, useEffect, useRef} from 'react'
6
- import {Editor} from 'slate'
1
+ import {useActorRef, useSelector} from '@xstate/react'
2
+ import {useEffect} from 'react'
7
3
  import type {PortableTextSlateEditor} from '../../types/editor'
8
4
  import {debugWithName} from '../../utils/debug'
9
- import {IS_PROCESSING_LOCAL_CHANGES} from '../../utils/weakMaps'
10
5
  import type {EditorActor} from '../editor-machine'
11
- import {useSyncValue} from '../hooks/useSyncValue'
12
- import type {PortableTextEditor} from '../PortableTextEditor'
6
+ import {mutationMachine} from '../mutation-machine'
7
+ import {syncMachine} from '../sync-machine'
13
8
 
14
9
  const debug = debugWithName('component:PortableTextEditor:Synchronizer')
15
- const debugVerbose = debug.enabled && false
16
-
17
- // The editor will commit changes in a throttled fashion in order
18
- // not to overload the network and degrade performance while typing.
19
- const FLUSH_PATCHES_THROTTLED_MS = process.env.NODE_ENV === 'test' ? 500 : 1000
20
10
 
21
11
  /**
22
12
  * @internal
23
13
  */
24
14
  export interface SynchronizerProps {
25
15
  editorActor: EditorActor
26
- getValue: () => Array<PortableTextBlock> | undefined
27
- portableTextEditor: PortableTextEditor
28
16
  slateEditor: PortableTextSlateEditor
29
17
  }
30
18
 
@@ -33,90 +21,75 @@ export interface SynchronizerProps {
33
21
  * @internal
34
22
  */
35
23
  export function Synchronizer(props: SynchronizerProps) {
36
- const readOnly = useSelector(props.editorActor, (s) => s.context.readOnly)
37
- const value = useSelector(props.editorActor, (s) => s.context.value)
38
- const {editorActor, getValue, portableTextEditor, slateEditor} = props
39
- const pendingPatches = useRef<Patch[]>([])
24
+ const {editorActor, slateEditor} = props
40
25
 
41
- const syncValue = useSyncValue({
42
- editorActor,
43
- portableTextEditor,
44
- readOnly,
45
- slateEditor,
26
+ const value = useSelector(props.editorActor, (s) => s.context.value)
27
+ const readOnly = useSelector(props.editorActor, (s) =>
28
+ s.matches({'edit mode': 'read only'}),
29
+ )
30
+ const syncActorRef = useActorRef(syncMachine, {
31
+ input: {
32
+ keyGenerator: props.editorActor.getSnapshot().context.keyGenerator,
33
+ readOnly: props.editorActor
34
+ .getSnapshot()
35
+ .matches({'edit mode': 'read only'}),
36
+ schema: props.editorActor.getSnapshot().context.schema,
37
+ slateEditor,
38
+ },
39
+ })
40
+ const mutationActorRef = useActorRef(mutationMachine, {
41
+ input: {
42
+ schema: editorActor.getSnapshot().context.schema,
43
+ slateEditor,
44
+ },
46
45
  })
47
46
 
48
47
  useEffect(() => {
49
- IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, false)
50
- }, [slateEditor])
51
-
52
- const onFlushPendingPatches = useCallback(() => {
53
- if (pendingPatches.current.length > 0) {
54
- debug('Flushing pending patches')
55
- if (debugVerbose) {
56
- debug(`Patches:\n${JSON.stringify(pendingPatches.current, null, 2)}`)
48
+ const subscription = mutationActorRef.on('*', (event) => {
49
+ if (event.type === 'has pending patches') {
50
+ syncActorRef.send({type: 'has pending patches'})
51
+ }
52
+ if (event.type === 'mutation') {
53
+ syncActorRef.send({type: 'mutation'})
54
+ editorActor.send(event)
57
55
  }
58
- const snapshot = getValue()
59
- editorActor.send({
60
- type: 'mutation',
61
- patches: pendingPatches.current,
62
- snapshot,
63
- })
64
- pendingPatches.current = []
56
+ })
57
+
58
+ return () => {
59
+ subscription.unsubscribe()
65
60
  }
66
- IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, false)
67
- }, [editorActor, slateEditor, getValue])
61
+ }, [mutationActorRef, syncActorRef, editorActor])
68
62
 
69
- // Flush pending patches immediately on unmount
70
63
  useEffect(() => {
64
+ const subscription = syncActorRef.on('*', (event) => {
65
+ props.editorActor.send(event)
66
+ })
67
+
71
68
  return () => {
72
- onFlushPendingPatches()
69
+ subscription.unsubscribe()
73
70
  }
74
- }, [onFlushPendingPatches])
71
+ }, [props.editorActor, syncActorRef])
75
72
 
76
- // Subscribe to, and handle changes from the editor
77
73
  useEffect(() => {
78
- const onFlushPendingPatchesThrottled = throttle(
79
- () => {
80
- // If the editor is normalizing (each operation) it means that it's not in the middle of a bigger transform,
81
- // and we can flush these changes immediately.
82
- if (Editor.isNormalizing(slateEditor)) {
83
- onFlushPendingPatches()
84
- return
85
- }
86
- // If it's in the middle of something, try again.
87
- onFlushPendingPatchesThrottled()
88
- },
89
- FLUSH_PATCHES_THROTTLED_MS,
90
- {
91
- leading: false,
92
- trailing: true,
93
- },
94
- )
74
+ syncActorRef.send({type: 'toggle readOnly'})
75
+ }, [syncActorRef, readOnly])
95
76
 
77
+ useEffect(() => {
78
+ debug('Value from props changed, syncing new value')
79
+ syncActorRef.send({type: 'update value', value})
80
+ }, [syncActorRef, value])
81
+
82
+ // Subscribe to, and handle changes from the editor
83
+ useEffect(() => {
96
84
  debug('Subscribing to patch events')
97
85
  const sub = editorActor.on('patch', (event) => {
98
- IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, true)
99
- pendingPatches.current.push(event.patch)
100
- onFlushPendingPatchesThrottled()
86
+ mutationActorRef.send(event)
101
87
  })
102
88
  return () => {
103
89
  debug('Unsubscribing to patch events')
104
90
  sub.unsubscribe()
105
91
  }
106
- }, [editorActor, onFlushPendingPatches, slateEditor])
107
-
108
- // This hook must be set up after setting up the subscription above, or it will not pick up validation errors from the useSyncValue hook.
109
- // This will cause the editor to not be able to signal a validation error and offer invalid value resolution of the initial value.
110
- const isInitialValueFromProps = useRef(true)
111
- useEffect(() => {
112
- debug('Value from props changed, syncing new value')
113
- syncValue(value)
114
- // Signal that we have our first value, and are ready to roll.
115
- if (isInitialValueFromProps.current) {
116
- editorActor.send({type: 'ready'})
117
- isInitialValueFromProps.current = false
118
- }
119
- }, [editorActor, syncValue, value])
92
+ }, [editorActor, mutationActorRef, slateEditor])
120
93
 
121
94
  return null
122
95
  }
@@ -31,6 +31,7 @@ import {fromSlateValue} from '../utils/values'
31
31
  import {KEY_TO_VALUE_ELEMENT} from '../utils/weakMaps'
32
32
  import type {EditorSchema} from './define-schema'
33
33
  import type {EditorContext} from './editor-snapshot'
34
+ import {getActiveDecorators} from './get-active-decorators'
34
35
 
35
36
  export * from 'xstate/guards'
36
37
 
@@ -68,6 +69,7 @@ export type MutationEvent = {
68
69
  export type InternalEditorEvent =
69
70
  | {type: 'normalizing'}
70
71
  | {type: 'done normalizing'}
72
+ | {type: 'done syncing'}
71
73
  | {
72
74
  type: 'behavior event'
73
75
  behaviorEvent: SyntheticBehaviorEvent | NativeBehaviorEvent
@@ -98,7 +100,7 @@ export type InternalEditorEvent =
98
100
  type: 'update maxBlocks'
99
101
  maxBlocks: number | undefined
100
102
  }
101
- | OmitFromUnion<InternalEditorEmittedEvent, 'type', 'readOnly toggled'>
103
+ | OmitFromUnion<InternalEditorEmittedEvent, 'type', 'read only' | 'editable'>
102
104
 
103
105
  /**
104
106
  * @alpha
@@ -108,13 +110,14 @@ export type EditorEmittedEvent = PickFromUnion<
108
110
  'type',
109
111
  | 'blurred'
110
112
  | 'done loading'
113
+ | 'editable'
111
114
  | 'error'
112
115
  | 'focused'
113
116
  | 'invalid value'
114
117
  | 'loading'
115
118
  | 'mutation'
116
119
  | 'patch'
117
- | 'readOnly toggled'
120
+ | 'read only'
118
121
  | 'ready'
119
122
  | 'selection'
120
123
  | 'value changed'
@@ -152,7 +155,8 @@ export type InternalEditorEmittedEvent =
152
155
  | {type: 'focused'; event: FocusEvent<HTMLDivElement, Element>}
153
156
  | {type: 'loading'}
154
157
  | {type: 'done loading'}
155
- | {type: 'readOnly toggled'; readOnly: boolean}
158
+ | {type: 'read only'}
159
+ | {type: 'editable'}
156
160
  | PickFromUnion<
157
161
  SyntheticBehaviorEvent,
158
162
  'type',
@@ -180,7 +184,7 @@ export const editorMachine = setup({
180
184
  keyGenerator: () => string
181
185
  pendingEvents: Array<PatchEvent | MutationEvent>
182
186
  schema: EditorSchema
183
- readOnly: boolean
187
+ initialReadOnly: boolean
184
188
  maxBlocks: number | undefined
185
189
  selection: EditorSelection
186
190
  value: Array<PortableTextBlock> | undefined
@@ -217,6 +221,8 @@ export const editorMachine = setup({
217
221
  assertEvent(event, 'mutation')
218
222
  return event
219
223
  }),
224
+ 'emit read only': emit({type: 'read only'}),
225
+ 'emit editable': emit({type: 'editable'}),
220
226
  'defer event': assign({
221
227
  pendingEvents: ({context, event}) => {
222
228
  assertEvent(event, ['patch', 'mutation'])
@@ -228,6 +234,7 @@ export const editorMachine = setup({
228
234
  enqueue(emit(event))
229
235
  }
230
236
  }),
237
+ 'emit ready': emit({type: 'ready'}),
231
238
  'clear pending events': assign({
232
239
  pendingEvents: [],
233
240
  }),
@@ -276,6 +283,10 @@ export const editorMachine = setup({
276
283
  )
277
284
 
278
285
  const editorContext = {
286
+ activeDecorators: getActiveDecorators({
287
+ schema: context.schema,
288
+ slateEditorInstance: event.editor,
289
+ }),
279
290
  keyGenerator: context.keyGenerator,
280
291
  schema: context.schema,
281
292
  selection,
@@ -345,48 +356,11 @@ export const editorMachine = setup({
345
356
  pendingEvents: [],
346
357
  schema: input.schema,
347
358
  selection: null,
348
- readOnly: input.readOnly ?? false,
359
+ initialReadOnly: input.readOnly ?? false,
349
360
  maxBlocks: input.maxBlocks,
350
361
  value: input.value,
351
362
  }),
352
363
  on: {
353
- 'annotation.add': {
354
- actions: emit(({event}) => event),
355
- guard: ({context}) => !context.readOnly,
356
- },
357
- 'annotation.remove': {
358
- actions: emit(({event}) => event),
359
- guard: ({context}) => !context.readOnly,
360
- },
361
- 'annotation.toggle': {
362
- actions: emit(({event}) => event),
363
- guard: ({context}) => !context.readOnly,
364
- },
365
- 'blur': {
366
- actions: emit(({event}) => event),
367
- guard: ({context}) => !context.readOnly,
368
- },
369
- 'decorator.*': {
370
- actions: emit(({event}) => event),
371
- guard: ({context}) => !context.readOnly,
372
- },
373
- 'focus': {
374
- actions: emit(({event}) => event),
375
- guard: ({context}) => !context.readOnly,
376
- },
377
- 'insert.*': {
378
- actions: emit(({event}) => event),
379
- guard: ({context}) => !context.readOnly,
380
- },
381
- 'list item.*': {
382
- actions: emit(({event}) => event),
383
- guard: ({context}) => !context.readOnly,
384
- },
385
- 'style.*': {
386
- actions: emit(({event}) => event),
387
- guard: ({context}) => !context.readOnly,
388
- },
389
- 'ready': {actions: emit(({event}) => event)},
390
364
  'unset': {actions: emit(({event}) => event)},
391
365
  'value changed': {actions: emit(({event}) => event)},
392
366
  'invalid value': {actions: emit(({event}) => event)},
@@ -405,22 +379,9 @@ export const editorMachine = setup({
405
379
  'update behaviors': {actions: 'assign behaviors'},
406
380
  'update schema': {actions: 'assign schema'},
407
381
  'update value': {actions: assign({value: ({event}) => event.value})},
408
- 'toggle readOnly': {
409
- actions: [
410
- assign({readOnly: ({context}) => !context.readOnly}),
411
- emit(({context}) => ({
412
- type: 'readOnly toggled',
413
- readOnly: context.readOnly,
414
- })),
415
- ],
416
- },
417
382
  'update maxBlocks': {
418
383
  actions: assign({maxBlocks: ({event}) => event.maxBlocks}),
419
384
  },
420
- 'behavior event': {
421
- actions: 'handle behavior event',
422
- guard: ({context}) => !context.readOnly,
423
- },
424
385
  'behavior action intends': {
425
386
  actions: [
426
387
  ({context, event}) => {
@@ -455,49 +416,137 @@ export const editorMachine = setup({
455
416
  ],
456
417
  },
457
418
  },
458
- initial: 'pristine',
419
+ type: 'parallel',
459
420
  states: {
460
- pristine: {
461
- initial: 'idle',
421
+ 'edit mode': {
422
+ initial: 'read only',
462
423
  states: {
463
- idle: {
424
+ 'read only': {
425
+ initial: 'determine initial edit mode',
426
+ states: {
427
+ 'determine initial edit mode': {
428
+ on: {
429
+ 'done syncing': [
430
+ {
431
+ target: '#editor.edit mode.read only.read only',
432
+ guard: ({context}) => context.initialReadOnly,
433
+ },
434
+ {
435
+ target: '#editor.edit mode.editable',
436
+ },
437
+ ],
438
+ },
439
+ },
440
+ 'read only': {
441
+ on: {
442
+ 'toggle readOnly': {
443
+ target: '#editor.edit mode.editable',
444
+ actions: ['emit editable'],
445
+ },
446
+ },
447
+ },
448
+ },
449
+ },
450
+ 'editable': {
464
451
  on: {
465
- normalizing: {
466
- target: 'normalizing',
452
+ 'toggle readOnly': {
453
+ target: '#editor.edit mode.read only.read only',
454
+ actions: ['emit read only'],
467
455
  },
468
- patch: {
469
- actions: 'defer event',
470
- target: '#editor.dirty',
456
+ 'behavior event': {
457
+ actions: 'handle behavior event',
471
458
  },
472
- mutation: {
473
- actions: 'defer event',
474
- target: '#editor.dirty',
459
+ 'annotation.add': {
460
+ actions: emit(({event}) => event),
461
+ },
462
+ 'annotation.remove': {
463
+ actions: emit(({event}) => event),
464
+ },
465
+ 'annotation.toggle': {
466
+ actions: emit(({event}) => event),
467
+ },
468
+ 'blur': {
469
+ actions: emit(({event}) => event),
470
+ },
471
+ 'decorator.*': {
472
+ actions: emit(({event}) => event),
473
+ },
474
+ 'focus': {
475
+ actions: emit(({event}) => event),
476
+ },
477
+ 'insert.*': {
478
+ actions: emit(({event}) => event),
479
+ },
480
+ 'list item.*': {
481
+ actions: emit(({event}) => event),
482
+ },
483
+ 'style.*': {
484
+ actions: emit(({event}) => event),
475
485
  },
476
486
  },
477
487
  },
478
- normalizing: {
488
+ },
489
+ },
490
+ 'setup': {
491
+ initial: 'setting up',
492
+ states: {
493
+ 'setting up': {
494
+ exit: ['emit ready'],
479
495
  on: {
480
- 'done normalizing': {
481
- target: 'idle',
482
- },
483
496
  'patch': {
484
497
  actions: 'defer event',
485
498
  },
486
499
  'mutation': {
487
500
  actions: 'defer event',
488
501
  },
502
+ 'done syncing': {
503
+ target: 'pristine',
504
+ },
489
505
  },
490
506
  },
491
- },
492
- },
493
- dirty: {
494
- entry: ['emit pending events', 'clear pending events'],
495
- on: {
496
- patch: {
497
- actions: 'emit patch event',
507
+ 'pristine': {
508
+ initial: 'idle',
509
+ states: {
510
+ idle: {
511
+ on: {
512
+ normalizing: {
513
+ target: 'normalizing',
514
+ },
515
+ patch: {
516
+ actions: 'defer event',
517
+ target: '#editor.setup.dirty',
518
+ },
519
+ mutation: {
520
+ actions: 'defer event',
521
+ target: '#editor.setup.dirty',
522
+ },
523
+ },
524
+ },
525
+ normalizing: {
526
+ on: {
527
+ 'done normalizing': {
528
+ target: 'idle',
529
+ },
530
+ 'patch': {
531
+ actions: 'defer event',
532
+ },
533
+ 'mutation': {
534
+ actions: 'defer event',
535
+ },
536
+ },
537
+ },
538
+ },
498
539
  },
499
- mutation: {
500
- actions: 'emit mutation event',
540
+ 'dirty': {
541
+ entry: ['emit pending events', 'clear pending events'],
542
+ on: {
543
+ patch: {
544
+ actions: 'emit patch event',
545
+ },
546
+ mutation: {
547
+ actions: 'emit mutation event',
548
+ },
549
+ },
501
550
  },
502
551
  },
503
552
  },
@@ -28,7 +28,6 @@ export function EditorProvider(props: EditorProviderProps) {
28
28
  const editor = useCreateEditor(props.initialConfig)
29
29
  const editorActor = editor._internal.editorActor
30
30
  const slateEditor = editor._internal.slateEditor
31
- const editable = editor._internal.editable
32
31
  const portableTextEditor = useMemo(
33
32
  () =>
34
33
  new PortableTextEditor({
@@ -47,8 +46,6 @@ export function EditorProvider(props: EditorProviderProps) {
47
46
  />
48
47
  <Synchronizer
49
48
  editorActor={editorActor}
50
- getValue={editable.getValue}
51
- portableTextEditor={portableTextEditor}
52
49
  slateEditor={slateEditor.instance}
53
50
  />
54
51
  <EditorActorContext.Provider value={editorActor}>
@@ -1,6 +1,7 @@
1
1
  import {useSelector} from '@xstate/react'
2
2
  import type {Editor} from './create-editor'
3
3
  import type {EditorSnapshot} from './editor-snapshot'
4
+ import {getActiveDecorators} from './get-active-decorators'
4
5
  import {getValue} from './get-value'
5
6
 
6
7
  function defaultCompare<T>(a: T, b: T) {
@@ -24,6 +25,10 @@ export function useEditorSelector<TSelected>(
24
25
  editor._internal.editorActor,
25
26
  (snapshot) => {
26
27
  const context = {
28
+ activeDecorators: getActiveDecorators({
29
+ schema: snapshot.context.schema,
30
+ slateEditorInstance: editor._internal.slateEditor.instance,
31
+ }),
27
32
  keyGenerator: snapshot.context.keyGenerator,
28
33
  schema: snapshot.context.schema,
29
34
  selection: snapshot.context.selection,
@@ -6,6 +6,7 @@ import type {EditorSchema} from './define-schema'
6
6
  * @alpha
7
7
  */
8
8
  export type EditorContext = {
9
+ activeDecorators: Array<string>
9
10
  keyGenerator: () => string
10
11
  schema: EditorSchema
11
12
  selection: EditorSelection
@@ -0,0 +1,20 @@
1
+ import {Editor} from 'slate'
2
+ import type {PortableTextSlateEditor} from '../types/editor'
3
+ import type {EditorSchema} from './define-schema'
4
+
5
+ export function getActiveDecorators({
6
+ schema,
7
+ slateEditorInstance,
8
+ }: {
9
+ schema: EditorSchema
10
+ slateEditorInstance: PortableTextSlateEditor
11
+ }) {
12
+ const decorators = schema.decorators.map((decorator) => decorator.value)
13
+
14
+ const marks =
15
+ {
16
+ ...(Editor.marks(slateEditorInstance) ?? {}),
17
+ }.marks ?? []
18
+
19
+ return marks.filter((mark) => decorators.includes(mark))
20
+ }
@@ -0,0 +1,100 @@
1
+ import type {Patch} from '@portabletext/patches'
2
+ import type {PortableTextBlock} from '@sanity/types'
3
+ import {Editor} from 'slate'
4
+ import {assign, emit, setup} from 'xstate'
5
+ import type {PortableTextSlateEditor} from '../types/editor'
6
+ import {fromSlateValue} from '../utils/values'
7
+ import {KEY_TO_VALUE_ELEMENT} from '../utils/weakMaps'
8
+ import type {EditorSchema} from './define-schema'
9
+
10
+ const FLUSH_PATCHES_THROTTLED_MS = process.env.NODE_ENV === 'test' ? 500 : 1000
11
+
12
+ /**
13
+ * Makes sure editor mutation events are debounced
14
+ */
15
+ export const mutationMachine = setup({
16
+ types: {
17
+ context: {} as {
18
+ pendingPatches: Array<Patch>
19
+ schema: EditorSchema
20
+ slateEditor: PortableTextSlateEditor
21
+ },
22
+ events: {} as {type: 'patch'; patch: Patch},
23
+ input: {} as {
24
+ schema: EditorSchema
25
+ slateEditor: PortableTextSlateEditor
26
+ },
27
+ emitted: {} as
28
+ | {
29
+ type: 'has pending patches'
30
+ }
31
+ | {
32
+ type: 'mutation'
33
+ patches: Array<Patch>
34
+ snapshot: Array<PortableTextBlock> | undefined
35
+ },
36
+ },
37
+ actions: {
38
+ 'emit has pending patches': emit({type: 'has pending patches'}),
39
+ 'emit mutation': emit(({context}) => ({
40
+ type: 'mutation' as const,
41
+ patches: context.pendingPatches,
42
+ snapshot: fromSlateValue(
43
+ context.slateEditor.children,
44
+ context.schema.block.name,
45
+ KEY_TO_VALUE_ELEMENT.get(context.slateEditor),
46
+ ),
47
+ })),
48
+ 'clear pending patches': assign({
49
+ pendingPatches: [],
50
+ }),
51
+ 'defer patch': assign({
52
+ pendingPatches: ({context, event}) => [
53
+ ...context.pendingPatches,
54
+ event.patch,
55
+ ],
56
+ }),
57
+ },
58
+ guards: {
59
+ 'slate is normalizing': ({context}) =>
60
+ Editor.isNormalizing(context.slateEditor),
61
+ },
62
+ }).createMachine({
63
+ id: 'mutation',
64
+ context: ({input}) => ({
65
+ pendingPatches: [],
66
+ schema: input.schema,
67
+ slateEditor: input.slateEditor,
68
+ }),
69
+ initial: 'idle',
70
+ states: {
71
+ 'idle': {
72
+ on: {
73
+ patch: {
74
+ actions: ['defer patch', 'emit has pending patches'],
75
+ target: 'has pending patches',
76
+ },
77
+ },
78
+ },
79
+ 'has pending patches': {
80
+ after: {
81
+ [FLUSH_PATCHES_THROTTLED_MS]: [
82
+ {
83
+ guard: 'slate is normalizing',
84
+ target: 'idle',
85
+ actions: ['emit mutation', 'clear pending patches'],
86
+ },
87
+ {
88
+ reenter: true,
89
+ },
90
+ ],
91
+ },
92
+ on: {
93
+ patch: {
94
+ actions: ['defer patch'],
95
+ reenter: true,
96
+ },
97
+ },
98
+ },
99
+ },
100
+ })