@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.
- package/README.md +3 -0
- package/lib/index.d.mts +1667 -0
- package/lib/index.d.ts +1667 -0
- package/lib/index.esm.js +305 -153
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +305 -154
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +305 -153
- package/lib/index.mjs.map +1 -1
- package/package.json +23 -22
- package/src/editor/Editable.tsx +30 -31
- package/src/editor/PortableTextEditor.tsx +23 -6
- package/src/editor/__tests__/PortableTextEditor.test.tsx +9 -9
- package/src/editor/__tests__/PortableTextEditorTester.tsx +2 -5
- package/src/editor/__tests__/RangeDecorations.test.tsx +2 -2
- package/src/editor/__tests__/handleClick.test.tsx +27 -7
- package/src/editor/__tests__/insert-block.test.tsx +4 -4
- package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +7 -7
- package/src/editor/__tests__/self-solving.test.tsx +176 -0
- package/src/editor/components/Leaf.tsx +28 -23
- package/src/editor/components/Synchronizer.tsx +60 -32
- package/src/editor/editor-machine.ts +195 -0
- package/src/editor/hooks/usePortableTextEditorSelection.tsx +11 -13
- package/src/editor/hooks/useSyncValue.test.tsx +9 -9
- package/src/editor/hooks/useSyncValue.ts +14 -13
- package/src/editor/plugins/__tests__/createWithInsertData.test.tsx +1 -1
- package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +28 -28
- package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +17 -17
- package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +8 -8
- package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +5 -5
- package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +2 -2
- package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +46 -46
- package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +22 -11
- package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +9 -9
- package/src/editor/plugins/createWithInsertData.ts +4 -8
- package/src/editor/plugins/createWithObjectKeys.ts +7 -0
- package/src/editor/plugins/createWithPatches.ts +5 -6
- package/src/editor/plugins/createWithPortableTextBlockStyle.ts +10 -2
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +20 -4
- package/src/editor/plugins/createWithPortableTextSelections.ts +4 -5
- package/src/editor/plugins/createWithSchemaTypes.ts +9 -0
- package/src/editor/plugins/index.ts +18 -8
- package/src/index.ts +9 -3
- package/src/utils/__tests__/dmpToOperations.test.ts +1 -1
- package/src/utils/__tests__/operationToPatches.test.ts +61 -61
- package/src/utils/__tests__/patchToOperations.test.ts +39 -39
- package/src/utils/__tests__/ranges.test.ts +1 -1
- package/src/utils/__tests__/valueNormalization.test.tsx +14 -2
- package/src/utils/__tests__/values.test.ts +17 -17
- package/src/utils/validateValue.ts +0 -22
- 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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
159
|
+
setSelectedFromRange()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const onSelection = portableTextEditor.editorActor.on(
|
|
163
|
+
'selection',
|
|
164
|
+
(event) => {
|
|
163
165
|
if (
|
|
164
|
-
|
|
165
|
-
isEqual(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 =
|
|
104
|
-
switch (
|
|
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(
|
|
108
|
+
pendingPatches.current.push(event.patch)
|
|
108
109
|
onFlushPendingPatchesThrottled()
|
|
109
|
-
onChange(
|
|
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(
|
|
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
|
-
}, [
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
change$.next({type: 'ready'})
|
|
184
|
+
editorActor.send({type: 'ready'})
|
|
157
185
|
isInitialValueFromProps.current = false
|
|
158
186
|
}
|
|
159
|
-
}, [
|
|
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
|
-
|
|
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 =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
}, [
|
|
60
|
+
}, [props.editorActor])
|
|
63
61
|
|
|
64
62
|
return (
|
|
65
63
|
<PortableTextEditorSelectionContext.Provider value={selection}>
|