@portabletext/editor 1.1.1 → 1.1.2

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 (51) hide show
  1. package/README.md +3 -0
  2. package/lib/index.d.mts +1667 -0
  3. package/lib/index.d.ts +1667 -0
  4. package/lib/index.esm.js +305 -153
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +305 -154
  7. package/lib/index.js.map +1 -1
  8. package/lib/index.mjs +305 -153
  9. package/lib/index.mjs.map +1 -1
  10. package/package.json +23 -22
  11. package/src/editor/Editable.tsx +30 -31
  12. package/src/editor/PortableTextEditor.tsx +23 -6
  13. package/src/editor/__tests__/PortableTextEditor.test.tsx +9 -9
  14. package/src/editor/__tests__/PortableTextEditorTester.tsx +2 -5
  15. package/src/editor/__tests__/RangeDecorations.test.tsx +2 -2
  16. package/src/editor/__tests__/handleClick.test.tsx +27 -7
  17. package/src/editor/__tests__/insert-block.test.tsx +4 -4
  18. package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +7 -7
  19. package/src/editor/__tests__/self-solving.test.tsx +176 -0
  20. package/src/editor/components/Leaf.tsx +28 -23
  21. package/src/editor/components/Synchronizer.tsx +60 -32
  22. package/src/editor/editor-machine.ts +195 -0
  23. package/src/editor/hooks/usePortableTextEditorSelection.tsx +11 -13
  24. package/src/editor/hooks/useSyncValue.test.tsx +9 -9
  25. package/src/editor/hooks/useSyncValue.ts +14 -13
  26. package/src/editor/plugins/__tests__/createWithInsertData.test.tsx +1 -1
  27. package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +28 -28
  28. package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +17 -17
  29. package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +8 -8
  30. package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +5 -5
  31. package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +2 -2
  32. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +46 -46
  33. package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +22 -11
  34. package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +9 -9
  35. package/src/editor/plugins/createWithInsertData.ts +4 -8
  36. package/src/editor/plugins/createWithObjectKeys.ts +7 -0
  37. package/src/editor/plugins/createWithPatches.ts +5 -6
  38. package/src/editor/plugins/createWithPortableTextBlockStyle.ts +10 -2
  39. package/src/editor/plugins/createWithPortableTextMarkModel.ts +20 -4
  40. package/src/editor/plugins/createWithPortableTextSelections.ts +4 -5
  41. package/src/editor/plugins/createWithSchemaTypes.ts +9 -0
  42. package/src/editor/plugins/index.ts +18 -8
  43. package/src/index.ts +9 -3
  44. package/src/utils/__tests__/dmpToOperations.test.ts +1 -1
  45. package/src/utils/__tests__/operationToPatches.test.ts +61 -61
  46. package/src/utils/__tests__/patchToOperations.test.ts +39 -39
  47. package/src/utils/__tests__/ranges.test.ts +1 -1
  48. package/src/utils/__tests__/valueNormalization.test.tsx +14 -2
  49. package/src/utils/__tests__/values.test.ts +17 -17
  50. package/src/utils/validateValue.ts +0 -22
  51. package/src/editor/__tests__/utils.ts +0 -44
@@ -0,0 +1,176 @@
1
+ import type {JSONValue, Patch} from '@portabletext/patches'
2
+ import {Schema} from '@sanity/schema'
3
+ import type {PortableTextBlock, PortableTextSpan} from '@sanity/types'
4
+ import {render, waitFor} from '@testing-library/react'
5
+ import {createRef, type ComponentProps, type RefObject} from 'react'
6
+ import {describe, expect, it, vi} from 'vitest'
7
+ import {getTextSelection} from '../../../e2e-tests/__tests__/gherkin-step-helpers'
8
+ import {PortableTextEditable} from '../Editable'
9
+ import {PortableTextEditor} from '../PortableTextEditor'
10
+
11
+ const schema = Schema.compile({
12
+ types: [
13
+ {
14
+ name: 'portable-text',
15
+ type: 'array',
16
+ of: [{type: 'block'}, {type: 'image'}],
17
+ },
18
+ {name: 'image', type: 'object'},
19
+ ],
20
+ }).get('portable-text')
21
+ type OnChange = ComponentProps<typeof PortableTextEditor>['onChange']
22
+
23
+ function block(
24
+ props?: Partial<Omit<PortableTextBlock, '_type'>>,
25
+ ): PortableTextBlock {
26
+ return {
27
+ _type: 'block',
28
+ ...(props ?? {}),
29
+ } as PortableTextBlock
30
+ }
31
+
32
+ function span(
33
+ props?: Partial<Omit<PortableTextSpan, '_type'>>,
34
+ ): PortableTextSpan {
35
+ return {
36
+ _type: 'span',
37
+ ...(props ?? {}),
38
+ } as PortableTextSpan
39
+ }
40
+
41
+ describe('Feature: Self-solving', () => {
42
+ it('Scenario: Missing .markDefs and .marks are added after the editor is made dirty', async () => {
43
+ const editorRef: RefObject<PortableTextEditor> = createRef()
44
+ const onChange = vi.fn<OnChange>()
45
+ const initialValue = [
46
+ block({
47
+ _key: 'b1',
48
+ children: [
49
+ span({
50
+ _key: 's1',
51
+ text: 'foo',
52
+ }),
53
+ ],
54
+ style: 'normal',
55
+ }),
56
+ ]
57
+ const spanPatch: Patch = {
58
+ type: 'set',
59
+ path: [{_key: 'b1'}, 'children', {_key: 's1'}, 'marks'],
60
+ value: [],
61
+ origin: 'local',
62
+ }
63
+ const blockPatch: Patch = {
64
+ type: 'set',
65
+ path: [{_key: 'b1'}],
66
+ value: block({
67
+ _key: 'b1',
68
+ children: [
69
+ span({
70
+ _key: 's1',
71
+ text: 'foo',
72
+ marks: [],
73
+ }),
74
+ ],
75
+ style: 'normal',
76
+ markDefs: [],
77
+ }) as JSONValue,
78
+ origin: 'local',
79
+ }
80
+ const strongPatch: Patch = {
81
+ type: 'set',
82
+ path: [{_key: 'b1'}, 'children', {_key: 's1'}, 'marks'],
83
+ value: ['strong'],
84
+ origin: 'local',
85
+ }
86
+
87
+ render(
88
+ <PortableTextEditor
89
+ ref={editorRef}
90
+ schemaType={schema}
91
+ value={initialValue}
92
+ onChange={onChange}
93
+ >
94
+ <PortableTextEditable />
95
+ </PortableTextEditor>,
96
+ )
97
+
98
+ await waitFor(() => {
99
+ if (editorRef.current) {
100
+ expect(onChange).toHaveBeenNthCalledWith(1, {
101
+ type: 'value',
102
+ value: initialValue,
103
+ })
104
+ expect(onChange).toHaveBeenNthCalledWith(2, {
105
+ type: 'ready',
106
+ })
107
+ }
108
+ })
109
+
110
+ await waitFor(() => {
111
+ if (editorRef.current) {
112
+ PortableTextEditor.select(
113
+ editorRef.current,
114
+ getTextSelection(initialValue, 'foo'),
115
+ )
116
+ PortableTextEditor.toggleMark(editorRef.current, 'strong')
117
+ }
118
+ })
119
+
120
+ await waitFor(() => {
121
+ if (editorRef.current) {
122
+ expect(onChange).toHaveBeenNthCalledWith(3, {
123
+ type: 'selection',
124
+ selection: {
125
+ ...getTextSelection(initialValue, 'foo'),
126
+ backward: false,
127
+ },
128
+ })
129
+ expect(onChange).toHaveBeenNthCalledWith(4, {
130
+ type: 'patch',
131
+ patch: spanPatch,
132
+ })
133
+ expect(onChange).toHaveBeenNthCalledWith(5, {
134
+ type: 'patch',
135
+ patch: blockPatch,
136
+ })
137
+ expect(onChange).toHaveBeenNthCalledWith(6, {
138
+ type: 'patch',
139
+ patch: strongPatch,
140
+ })
141
+ expect(onChange).toHaveBeenNthCalledWith(7, {
142
+ type: 'selection',
143
+ selection: {
144
+ ...getTextSelection(initialValue, 'foo'),
145
+ backward: false,
146
+ },
147
+ })
148
+ expect(onChange).toHaveBeenNthCalledWith(8, {
149
+ type: 'selection',
150
+ selection: {
151
+ ...getTextSelection(initialValue, 'foo'),
152
+ backward: false,
153
+ },
154
+ })
155
+ expect(onChange).toHaveBeenNthCalledWith(9, {
156
+ type: 'mutation',
157
+ patches: [spanPatch, blockPatch, strongPatch],
158
+ snapshot: [
159
+ block({
160
+ _key: 'b1',
161
+ children: [
162
+ span({
163
+ _key: 's1',
164
+ text: 'foo',
165
+ marks: ['strong'],
166
+ }),
167
+ ],
168
+ style: 'normal',
169
+ markDefs: [],
170
+ }),
171
+ ],
172
+ })
173
+ }
174
+ })
175
+ })
176
+ })
@@ -141,28 +141,30 @@ export const Leaf = (props: LeafProps) => {
141
141
  if (!shouldTrackSelectionAndFocus) {
142
142
  return undefined
143
143
  }
144
- const sub = portableTextEditor.change$.subscribe((next) => {
145
- if (next.type === 'blur') {
146
- setFocused(false)
147
- setSelected(false)
148
- return
149
- }
150
- if (next.type === 'focus') {
151
- const sel = PortableTextEditor.getSelection(portableTextEditor)
152
- if (
153
- sel &&
154
- isEqual(sel.focus.path, path) &&
155
- PortableTextEditor.isCollapsedSelection(portableTextEditor)
156
- ) {
157
- setFocused(true)
158
- }
159
- setSelectedFromRange()
160
- return
144
+
145
+ const onBlur = portableTextEditor.editorActor.on('blur', () => {
146
+ setFocused(false)
147
+ setSelected(false)
148
+ })
149
+
150
+ const onFocus = portableTextEditor.editorActor.on('focus', () => {
151
+ const sel = PortableTextEditor.getSelection(portableTextEditor)
152
+ if (
153
+ sel &&
154
+ isEqual(sel.focus.path, path) &&
155
+ PortableTextEditor.isCollapsedSelection(portableTextEditor)
156
+ ) {
157
+ setFocused(true)
161
158
  }
162
- if (next.type === 'selection') {
159
+ setSelectedFromRange()
160
+ })
161
+
162
+ const onSelection = portableTextEditor.editorActor.on(
163
+ 'selection',
164
+ (event) => {
163
165
  if (
164
- next.selection &&
165
- isEqual(next.selection.focus.path, path) &&
166
+ event.selection &&
167
+ isEqual(event.selection.focus.path, path) &&
166
168
  PortableTextEditor.isCollapsedSelection(portableTextEditor)
167
169
  ) {
168
170
  setFocused(true)
@@ -170,10 +172,13 @@ export const Leaf = (props: LeafProps) => {
170
172
  setFocused(false)
171
173
  }
172
174
  setSelectedFromRange()
173
- }
174
- })
175
+ },
176
+ )
177
+
175
178
  return () => {
176
- sub.unsubscribe()
179
+ onBlur.unsubscribe()
180
+ onFocus.unsubscribe()
181
+ onSelection.unsubscribe()
177
182
  }
178
183
  }, [
179
184
  path,
@@ -4,9 +4,10 @@ import {throttle} from 'lodash'
4
4
  import {useCallback, useEffect, useMemo, useRef} from 'react'
5
5
  import {Editor} from 'slate'
6
6
  import {useSlate} from 'slate-react'
7
- import type {EditorChange, EditorChanges} from '../../types/editor'
7
+ import type {EditorChange} from '../../types/editor'
8
8
  import {debugWithName} from '../../utils/debug'
9
9
  import {IS_PROCESSING_LOCAL_CHANGES} from '../../utils/weakMaps'
10
+ import type {EditorActor} from '../editor-machine'
10
11
  import {usePortableTextEditor} from '../hooks/usePortableTextEditor'
11
12
  import {usePortableTextEditorKeyGenerator} from '../hooks/usePortableTextEditorKeyGenerator'
12
13
  import {usePortableTextEditorReadOnlyStatus} from '../hooks/usePortableTextReadOnly'
@@ -23,7 +24,7 @@ const FLUSH_PATCHES_THROTTLED_MS = process.env.NODE_ENV === 'test' ? 500 : 1000
23
24
  * @internal
24
25
  */
25
26
  export interface SynchronizerProps {
26
- change$: EditorChanges
27
+ editorActor: EditorActor
27
28
  getValue: () => Array<PortableTextBlock> | undefined
28
29
  onChange: (change: EditorChange) => void
29
30
  value: Array<PortableTextBlock> | undefined
@@ -37,12 +38,12 @@ export function Synchronizer(props: SynchronizerProps) {
37
38
  const portableTextEditor = usePortableTextEditor()
38
39
  const keyGenerator = usePortableTextEditorKeyGenerator()
39
40
  const readOnly = usePortableTextEditorReadOnlyStatus()
40
- const {change$, getValue, onChange, value} = props
41
+ const {editorActor, getValue, onChange, value} = props
41
42
  const pendingPatches = useRef<Patch[]>([])
42
43
 
43
44
  const syncValue = useSyncValue({
45
+ editorActor,
44
46
  keyGenerator,
45
- onChange,
46
47
  portableTextEditor,
47
48
  readOnly,
48
49
  })
@@ -60,7 +61,7 @@ export function Synchronizer(props: SynchronizerProps) {
60
61
  debug(`Patches:\n${JSON.stringify(pendingPatches.current, null, 2)}`)
61
62
  }
62
63
  const snapshot = getValue()
63
- change$.next({
64
+ editorActor.send({
64
65
  type: 'mutation',
65
66
  patches: pendingPatches.current,
66
67
  snapshot,
@@ -68,7 +69,7 @@ export function Synchronizer(props: SynchronizerProps) {
68
69
  pendingPatches.current = []
69
70
  }
70
71
  IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, false)
71
- }, [slateEditor, getValue, change$])
72
+ }, [editorActor, slateEditor, getValue])
72
73
 
73
74
  const onFlushPendingPatchesThrottled = useMemo(() => {
74
75
  return throttle(
@@ -99,50 +100,78 @@ export function Synchronizer(props: SynchronizerProps) {
99
100
 
100
101
  // Subscribe to, and handle changes from the editor
101
102
  useEffect(() => {
102
- debug('Subscribing to editor changes$')
103
- const sub = change$.subscribe((next: EditorChange): void => {
104
- switch (next.type) {
103
+ debug('Subscribing to editor changes')
104
+ const sub = editorActor.on('*', (event) => {
105
+ switch (event.type) {
105
106
  case 'patch':
106
107
  IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, true)
107
- pendingPatches.current.push(next.patch)
108
+ pendingPatches.current.push(event.patch)
108
109
  onFlushPendingPatchesThrottled()
109
- onChange(next)
110
+ onChange(event)
110
111
  break
112
+ case 'loading': {
113
+ onChange({type: 'loading', isLoading: true})
114
+ break
115
+ }
116
+ case 'done loading': {
117
+ onChange({type: 'loading', isLoading: false})
118
+ break
119
+ }
120
+ case 'offline': {
121
+ onChange({type: 'connection', value: 'offline'})
122
+ break
123
+ }
124
+ case 'online': {
125
+ onChange({type: 'connection', value: 'online'})
126
+ break
127
+ }
128
+ case 'value changed': {
129
+ onChange({type: 'value', value: event.value})
130
+ break
131
+ }
132
+ case 'invalid value': {
133
+ onChange({
134
+ type: 'invalidValue',
135
+ resolution: event.resolution,
136
+ value: event.value,
137
+ })
138
+ break
139
+ }
140
+ case 'error': {
141
+ onChange({
142
+ ...event,
143
+ level: 'warning',
144
+ })
145
+ break
146
+ }
111
147
  default:
112
- onChange(next)
148
+ onChange(event)
113
149
  }
114
150
  })
115
151
  return () => {
116
- debug('Unsubscribing to changes$')
152
+ debug('Unsubscribing to changes')
117
153
  sub.unsubscribe()
118
154
  }
119
- }, [change$, onChange, onFlushPendingPatchesThrottled, slateEditor])
155
+ }, [editorActor, onFlushPendingPatchesThrottled, slateEditor])
120
156
 
121
157
  // Sync the value when going online
122
158
  const handleOnline = useCallback(() => {
123
159
  debug('Editor is online, syncing from props.value')
124
- change$.next({type: 'connection', value: 'online'})
125
160
  syncValue(value)
126
- }, [change$, syncValue, value])
127
-
128
- const handleOffline = useCallback(() => {
129
- debug('Editor is offline')
130
- change$.next({type: 'connection', value: 'offline'})
131
- }, [change$])
161
+ }, [syncValue, value])
132
162
 
133
163
  // Notify about window online and offline status changes
134
164
  useEffect(() => {
135
- if (portableTextEditor.props.patches$) {
136
- window.addEventListener('online', handleOnline)
137
- window.addEventListener('offline', handleOffline)
138
- }
139
- return () => {
165
+ const subscription = editorActor.on('online', () => {
140
166
  if (portableTextEditor.props.patches$) {
141
- window.removeEventListener('online', handleOnline)
142
- window.removeEventListener('offline', handleOffline)
167
+ handleOnline()
143
168
  }
169
+ })
170
+
171
+ return () => {
172
+ subscription.unsubscribe()
144
173
  }
145
- })
174
+ }, [editorActor])
146
175
 
147
176
  // This hook must be set up after setting up the subscription above, or it will not pick up validation errors from the useSyncValue hook.
148
177
  // This will cause the editor to not be able to signal a validation error and offer invalid value resolution of the initial value.
@@ -152,11 +181,10 @@ export function Synchronizer(props: SynchronizerProps) {
152
181
  syncValue(value)
153
182
  // Signal that we have our first value, and are ready to roll.
154
183
  if (isInitialValueFromProps.current) {
155
- change$.next({type: 'loading', isLoading: false})
156
- change$.next({type: 'ready'})
184
+ editorActor.send({type: 'ready'})
157
185
  isInitialValueFromProps.current = false
158
186
  }
159
- }, [change$, syncValue, value])
187
+ }, [editorActor, syncValue, value])
160
188
 
161
189
  return null
162
190
  }
@@ -0,0 +1,195 @@
1
+ import type {Patch} from '@portabletext/patches'
2
+ import type {PortableTextBlock} from '@sanity/types'
3
+ import type {FocusEvent} from 'react'
4
+ import {
5
+ assertEvent,
6
+ assign,
7
+ emit,
8
+ enqueueActions,
9
+ fromCallback,
10
+ setup,
11
+ type ActorRefFrom,
12
+ } from 'xstate'
13
+ import type {EditorSelection, InvalidValueResolution} from '../types/editor'
14
+
15
+ /**
16
+ * @internal
17
+ */
18
+ export type EditorActor = ActorRefFrom<typeof editorMachine>
19
+
20
+ const networkLogic = fromCallback(({sendBack}) => {
21
+ const onlineHandler = () => {
22
+ sendBack({type: 'online'})
23
+ }
24
+ const offlineHandler = () => {
25
+ sendBack({type: 'offline'})
26
+ }
27
+
28
+ window.addEventListener('online', onlineHandler)
29
+ window.addEventListener('offline', offlineHandler)
30
+
31
+ return () => {
32
+ window.removeEventListener('online', onlineHandler)
33
+ window.removeEventListener('offline', offlineHandler)
34
+ }
35
+ })
36
+
37
+ /**
38
+ * @internal
39
+ */
40
+ export type PatchEvent = {type: 'patch'; patch: Patch}
41
+
42
+ /**
43
+ * @internal
44
+ */
45
+ export type MutationEvent = {
46
+ type: 'mutation'
47
+ patches: Array<Patch>
48
+ snapshot: Array<PortableTextBlock> | undefined
49
+ }
50
+
51
+ type EditorEvent =
52
+ | {type: 'normalizing'}
53
+ | {type: 'done normalizing'}
54
+ | EditorEmittedEvent
55
+
56
+ type EditorEmittedEvent =
57
+ | {type: 'ready'}
58
+ | PatchEvent
59
+ | MutationEvent
60
+ | {
61
+ type: 'unset'
62
+ previousValue: Array<PortableTextBlock>
63
+ }
64
+ | {
65
+ type: 'value changed'
66
+ value: Array<PortableTextBlock> | undefined
67
+ }
68
+ | {
69
+ type: 'invalid value'
70
+ resolution: InvalidValueResolution | null
71
+ value: Array<PortableTextBlock> | undefined
72
+ }
73
+ | {
74
+ type: 'error'
75
+ name: string
76
+ description: string
77
+ data: unknown
78
+ }
79
+ | {type: 'selection'; selection: EditorSelection}
80
+ | {type: 'blur'; event: FocusEvent<HTMLDivElement, Element>}
81
+ | {type: 'focus'; event: FocusEvent<HTMLDivElement, Element>}
82
+ | {type: 'online'}
83
+ | {type: 'offline'}
84
+ | {type: 'loading'}
85
+ | {type: 'done loading'}
86
+
87
+ /**
88
+ * @internal
89
+ */
90
+ export const editorMachine = setup({
91
+ types: {
92
+ context: {} as {
93
+ pendingEvents: Array<PatchEvent | MutationEvent>
94
+ },
95
+ events: {} as EditorEvent,
96
+ emitted: {} as EditorEmittedEvent,
97
+ },
98
+ actions: {
99
+ 'emit patch event': emit(({event}) => {
100
+ assertEvent(event, 'patch')
101
+ return event
102
+ }),
103
+ 'emit mutation event': emit(({event}) => {
104
+ assertEvent(event, 'mutation')
105
+ return event
106
+ }),
107
+ 'defer event': assign({
108
+ pendingEvents: ({context, event}) => {
109
+ assertEvent(event, ['patch', 'mutation'])
110
+ return [...context.pendingEvents, event]
111
+ },
112
+ }),
113
+ 'emit pending events': enqueueActions(({context, enqueue}) => {
114
+ for (const event of context.pendingEvents) {
115
+ enqueue(emit(event))
116
+ }
117
+ }),
118
+ 'clear pending events': assign({
119
+ pendingEvents: [],
120
+ }),
121
+ },
122
+ actors: {
123
+ networkLogic,
124
+ },
125
+ }).createMachine({
126
+ id: 'editor',
127
+ context: {
128
+ pendingEvents: [],
129
+ },
130
+ invoke: {
131
+ id: 'networkLogic',
132
+ src: 'networkLogic',
133
+ },
134
+ on: {
135
+ 'ready': {actions: emit(({event}) => event)},
136
+ 'unset': {actions: emit(({event}) => event)},
137
+ 'value changed': {actions: emit(({event}) => event)},
138
+ 'invalid value': {actions: emit(({event}) => event)},
139
+ 'error': {actions: emit(({event}) => event)},
140
+ 'selection': {actions: emit(({event}) => event)},
141
+ 'blur': {actions: emit(({event}) => event)},
142
+ 'focus': {actions: emit(({event}) => event)},
143
+ 'online': {actions: emit({type: 'online'})},
144
+ 'offline': {actions: emit({type: 'offline'})},
145
+ 'loading': {actions: emit({type: 'loading'})},
146
+ 'done loading': {actions: emit({type: 'done loading'})},
147
+ },
148
+ initial: 'pristine',
149
+ states: {
150
+ pristine: {
151
+ initial: 'idle',
152
+ states: {
153
+ idle: {
154
+ on: {
155
+ normalizing: {
156
+ target: 'normalizing',
157
+ },
158
+ patch: {
159
+ actions: 'defer event',
160
+ target: '#editor.dirty',
161
+ },
162
+ mutation: {
163
+ actions: 'defer event',
164
+ target: '#editor.dirty',
165
+ },
166
+ },
167
+ },
168
+ normalizing: {
169
+ on: {
170
+ 'done normalizing': {
171
+ target: 'idle',
172
+ },
173
+ 'patch': {
174
+ actions: 'defer event',
175
+ },
176
+ 'mutation': {
177
+ actions: 'defer event',
178
+ },
179
+ },
180
+ },
181
+ },
182
+ },
183
+ dirty: {
184
+ entry: ['emit pending events', 'clear pending events'],
185
+ on: {
186
+ patch: {
187
+ actions: 'emit patch event',
188
+ },
189
+ mutation: {
190
+ actions: 'emit mutation event',
191
+ },
192
+ },
193
+ },
194
+ },
195
+ })
@@ -7,6 +7,7 @@ import {
7
7
  } from 'react'
8
8
  import type {EditorChanges, EditorSelection} from '../../types/editor'
9
9
  import {debugWithName} from '../../utils/debug'
10
+ import type {EditorActor} from '../editor-machine'
10
11
 
11
12
  /**
12
13
  * A React context for sharing the editor selection.
@@ -36,30 +37,27 @@ const debugVerbose = debug.enabled && false
36
37
  */
37
38
  export function PortableTextEditorSelectionProvider(
38
39
  props: React.PropsWithChildren<{
39
- change$: EditorChanges
40
+ editorActor: EditorActor
40
41
  }>,
41
42
  ) {
42
- const {change$} = props
43
43
  const [selection, setSelection] = useState<EditorSelection>(null)
44
44
 
45
45
  // Subscribe to, and handle changes from the editor
46
46
  useEffect(() => {
47
- debug('Subscribing to selection changes$')
48
- const subscription = change$.subscribe((next): void => {
49
- if (next.type === 'selection') {
50
- // Set the selection state in a transition, we don't need the state immediately.
51
- startTransition(() => {
52
- if (debugVerbose) debug('Setting selection')
53
- setSelection(next.selection)
54
- })
55
- }
47
+ debug('Subscribing to selection changes')
48
+ const subscription = props.editorActor.on('selection', (event) => {
49
+ // Set the selection state in a transition, we don't need the state immediately.
50
+ startTransition(() => {
51
+ if (debugVerbose) debug('Setting selection')
52
+ setSelection(event.selection)
53
+ })
56
54
  })
57
55
 
58
56
  return () => {
59
- debug('Unsubscribing to selection changes$')
57
+ debug('Unsubscribing to selection changes')
60
58
  subscription.unsubscribe()
61
59
  }
62
- }, [change$])
60
+ }, [props.editorActor])
63
61
 
64
62
  return (
65
63
  <PortableTextEditorSelectionContext.Provider value={selection}>