@portabletext/editor 1.15.2 → 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.
- package/lib/_chunks-cjs/behavior.core.cjs +30 -28
- package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
- package/lib/_chunks-cjs/selector.get-text-before.cjs +14 -14
- package/lib/_chunks-cjs/selector.get-text-before.cjs.map +1 -1
- package/lib/_chunks-cjs/{selectors.cjs → selector.is-selection-collapsed.cjs} +8 -8
- package/lib/_chunks-cjs/selector.is-selection-collapsed.cjs.map +1 -0
- package/lib/_chunks-es/behavior.core.js +12 -10
- package/lib/_chunks-es/behavior.core.js.map +1 -1
- package/lib/_chunks-es/selector.get-text-before.js +14 -14
- package/lib/_chunks-es/selector.get-text-before.js.map +1 -1
- package/lib/_chunks-es/{selectors.js → selector.is-selection-collapsed.js} +8 -8
- package/lib/_chunks-es/selector.is-selection-collapsed.js.map +1 -0
- package/lib/behaviors/index.cjs +35 -35
- package/lib/behaviors/index.cjs.map +1 -1
- package/lib/behaviors/index.d.cts +40 -45
- package/lib/behaviors/index.d.ts +40 -45
- package/lib/behaviors/index.js +20 -20
- package/lib/behaviors/index.js.map +1 -1
- package/lib/index.cjs +868 -542
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +3791 -4503
- package/lib/index.d.ts +3791 -4503
- package/lib/index.js +865 -541
- package/lib/index.js.map +1 -1
- package/lib/selectors/index.cjs +166 -16
- package/lib/selectors/index.cjs.map +1 -1
- package/lib/selectors/index.d.cts +62 -17
- package/lib/selectors/index.d.ts +62 -17
- package/lib/selectors/index.js +154 -3
- package/lib/selectors/index.js.map +1 -1
- package/package.json +11 -11
- package/src/behavior-actions/behavior.action-utils.insert-block.ts +3 -5
- package/src/behavior-actions/behavior.actions.ts +6 -6
- package/src/behavior-actions/behavior.guards.ts +2 -6
- package/src/behaviors/behavior.code-editor.ts +5 -9
- package/src/behaviors/behavior.core.block-objects.ts +14 -20
- package/src/behaviors/behavior.core.lists.ts +13 -19
- package/src/behaviors/behavior.links.ts +6 -6
- package/src/behaviors/behavior.markdown.ts +27 -40
- package/src/behaviors/behavior.types.ts +7 -7
- package/src/behaviors/index.ts +1 -0
- package/src/editor/Editable.tsx +11 -4
- package/src/editor/PortableTextEditor.tsx +4 -5
- package/src/editor/{hooks/useSyncValue.test.tsx → __tests__/sync-value.test.tsx} +42 -23
- package/src/editor/components/Synchronizer.tsx +53 -80
- package/src/{utils/getPortableTextMemberSchemaTypes.ts → editor/create-editor-schema.ts} +3 -3
- package/src/editor/create-editor.ts +2 -2
- package/src/editor/define-schema.ts +8 -3
- package/src/editor/editor-machine.ts +136 -104
- package/src/editor/editor-provider.tsx +0 -3
- package/src/editor/editor-selector.ts +6 -13
- package/src/editor/editor-snapshot.ts +5 -6
- package/src/editor/get-active-decorators.ts +20 -0
- package/src/editor/mutation-machine.ts +100 -0
- package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +21 -15
- package/src/editor/plugins/createWithMaxBlocks.ts +1 -1
- package/src/editor/plugins/createWithPatches.ts +0 -4
- package/src/editor/plugins/createWithPlaceholderBlock.ts +1 -1
- package/src/editor/plugins/createWithPortableTextSelections.ts +4 -1
- package/src/editor/plugins/createWithUndoRedo.ts +3 -3
- package/src/editor/sync-machine.ts +657 -0
- package/src/editor/withSyncRangeDecorations.ts +17 -5
- package/src/index.ts +3 -5
- package/src/selectors/_exports/index.ts +1 -0
- package/src/selectors/index.ts +10 -4
- package/src/selectors/selector.get-active-style.ts +37 -0
- package/src/selectors/selector.get-selected-spans.ts +136 -0
- package/src/selectors/selector.is-active-annotation.ts +49 -0
- package/src/selectors/selector.is-active-decorator.ts +21 -0
- package/src/selectors/selector.is-active-list-item.ts +13 -0
- package/src/selectors/selector.is-active-style.ts +13 -0
- package/src/selectors/selector.is-selection-collapsed.ts +12 -0
- package/src/selectors/selector.is-selection-expanded.ts +9 -0
- package/src/selectors/selectors.ts +0 -11
- package/src/utils/__tests__/operationToPatches.test.ts +2 -2
- package/src/utils/__tests__/patchToOperations.test.ts +2 -2
- package/src/utils/__tests__/values.test.ts +2 -2
- package/src/utils/weakMaps.ts +0 -3
- package/src/utils/withChanges.ts +1 -8
- package/lib/_chunks-cjs/selectors.cjs.map +0 -1
- package/lib/_chunks-es/selectors.js.map +0 -1
- package/src/editor/hooks/useSyncValue.ts +0 -426
|
@@ -114,7 +114,7 @@ export function createWithUndoRedo(
|
|
|
114
114
|
editor.history = {undos: [], redos: []}
|
|
115
115
|
const {apply} = editor
|
|
116
116
|
editor.apply = (op: Operation) => {
|
|
117
|
-
if (editorActor.getSnapshot().
|
|
117
|
+
if (editorActor.getSnapshot().matches({'edit mode': 'read only'})) {
|
|
118
118
|
apply(op)
|
|
119
119
|
return
|
|
120
120
|
}
|
|
@@ -181,7 +181,7 @@ export function createWithUndoRedo(
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
editor.undo = () => {
|
|
184
|
-
if (editorActor.getSnapshot().
|
|
184
|
+
if (editorActor.getSnapshot().matches({'edit mode': 'read only'})) {
|
|
185
185
|
return
|
|
186
186
|
}
|
|
187
187
|
const {undos} = editor.history
|
|
@@ -239,7 +239,7 @@ export function createWithUndoRedo(
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
editor.redo = () => {
|
|
242
|
-
if (editorActor.getSnapshot().
|
|
242
|
+
if (editorActor.getSnapshot().matches({'edit mode': 'read only'})) {
|
|
243
243
|
return
|
|
244
244
|
}
|
|
245
245
|
const {redos} = editor.history
|
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import type {Patch} from '@portabletext/patches'
|
|
2
|
+
import type {PortableTextBlock} from '@sanity/types'
|
|
3
|
+
import {isEqual} from 'lodash'
|
|
4
|
+
import {Editor, Text, Transforms, type Descendant, type Node} from 'slate'
|
|
5
|
+
import {
|
|
6
|
+
and,
|
|
7
|
+
assertEvent,
|
|
8
|
+
assign,
|
|
9
|
+
emit,
|
|
10
|
+
fromCallback,
|
|
11
|
+
not,
|
|
12
|
+
or,
|
|
13
|
+
setup,
|
|
14
|
+
type AnyEventObject,
|
|
15
|
+
type CallbackLogicFunction,
|
|
16
|
+
} from 'xstate'
|
|
17
|
+
import type {PickFromUnion} from '../type-utils'
|
|
18
|
+
import type {
|
|
19
|
+
InvalidValueResolution,
|
|
20
|
+
PortableTextSlateEditor,
|
|
21
|
+
} from '../types/editor'
|
|
22
|
+
import {debugWithName} from '../utils/debug'
|
|
23
|
+
import {validateValue} from '../utils/validateValue'
|
|
24
|
+
import {toSlateValue, VOID_CHILD_KEY} from '../utils/values'
|
|
25
|
+
import {isChangingRemotely, withRemoteChanges} from '../utils/withChanges'
|
|
26
|
+
import {withoutPatching} from '../utils/withoutPatching'
|
|
27
|
+
import type {EditorSchema} from './define-schema'
|
|
28
|
+
import {withoutSaving} from './plugins/createWithUndoRedo'
|
|
29
|
+
|
|
30
|
+
type SyncValueEvent =
|
|
31
|
+
| {
|
|
32
|
+
type: 'patch'
|
|
33
|
+
patch: Patch
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
type: 'invalid value'
|
|
37
|
+
resolution: InvalidValueResolution | null
|
|
38
|
+
value: Array<PortableTextBlock> | undefined
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
type: 'value changed'
|
|
42
|
+
value: Array<PortableTextBlock> | undefined
|
|
43
|
+
}
|
|
44
|
+
| {
|
|
45
|
+
type: 'done syncing'
|
|
46
|
+
value: Array<PortableTextBlock> | undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const syncValueCallback: CallbackLogicFunction<
|
|
50
|
+
AnyEventObject,
|
|
51
|
+
SyncValueEvent,
|
|
52
|
+
{
|
|
53
|
+
context: {
|
|
54
|
+
keyGenerator: () => string
|
|
55
|
+
previousValue: Array<PortableTextBlock> | undefined
|
|
56
|
+
readOnly: boolean
|
|
57
|
+
schema: EditorSchema
|
|
58
|
+
}
|
|
59
|
+
slateEditor: PortableTextSlateEditor
|
|
60
|
+
value: Array<PortableTextBlock> | undefined
|
|
61
|
+
}
|
|
62
|
+
> = ({sendBack, input}) => {
|
|
63
|
+
updateValue({
|
|
64
|
+
context: input.context,
|
|
65
|
+
sendBack,
|
|
66
|
+
slateEditor: input.slateEditor,
|
|
67
|
+
value: input.value,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const syncValueLogic = fromCallback(syncValueCallback)
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Sync value with the editor state
|
|
75
|
+
*
|
|
76
|
+
* Normally nothing here should apply, and the editor and the real world are perfectly aligned.
|
|
77
|
+
*
|
|
78
|
+
* Inconsistencies could happen though, so we need to check the editor state when the value changes.
|
|
79
|
+
*
|
|
80
|
+
* For performance reasons, it makes sense to also do the content validation here, as we already
|
|
81
|
+
* iterate over the value and can validate only the new content that is actually changed.
|
|
82
|
+
*
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
85
|
+
export const syncMachine = setup({
|
|
86
|
+
types: {
|
|
87
|
+
context: {} as {
|
|
88
|
+
isProcessingLocalChanges: boolean
|
|
89
|
+
keyGenerator: () => string
|
|
90
|
+
schema: EditorSchema
|
|
91
|
+
readOnly: boolean
|
|
92
|
+
slateEditor: PortableTextSlateEditor
|
|
93
|
+
pendingValue: Array<PortableTextBlock> | undefined
|
|
94
|
+
previousValue: Array<PortableTextBlock> | undefined
|
|
95
|
+
},
|
|
96
|
+
input: {} as {
|
|
97
|
+
keyGenerator: () => string
|
|
98
|
+
schema: EditorSchema
|
|
99
|
+
readOnly: boolean
|
|
100
|
+
slateEditor: PortableTextSlateEditor
|
|
101
|
+
},
|
|
102
|
+
events: {} as
|
|
103
|
+
| {
|
|
104
|
+
type: 'has pending patches'
|
|
105
|
+
}
|
|
106
|
+
| {
|
|
107
|
+
type: 'mutation'
|
|
108
|
+
}
|
|
109
|
+
| {
|
|
110
|
+
type: 'update value'
|
|
111
|
+
value: Array<PortableTextBlock> | undefined
|
|
112
|
+
}
|
|
113
|
+
| {
|
|
114
|
+
type: 'toggle readOnly'
|
|
115
|
+
}
|
|
116
|
+
| SyncValueEvent,
|
|
117
|
+
emitted: {} as PickFromUnion<
|
|
118
|
+
SyncValueEvent,
|
|
119
|
+
'type',
|
|
120
|
+
'done syncing' | 'invalid value' | 'patch' | 'value changed'
|
|
121
|
+
>,
|
|
122
|
+
},
|
|
123
|
+
actions: {
|
|
124
|
+
'assign readOnly': assign({
|
|
125
|
+
readOnly: ({context}) => !context.readOnly,
|
|
126
|
+
}),
|
|
127
|
+
'assign pending value': assign({
|
|
128
|
+
pendingValue: ({event}) => {
|
|
129
|
+
assertEvent(event, 'update value')
|
|
130
|
+
return event.value
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
'clear pending value': assign({
|
|
134
|
+
pendingValue: undefined,
|
|
135
|
+
}),
|
|
136
|
+
'assign previous value': assign({
|
|
137
|
+
previousValue: ({event}) => {
|
|
138
|
+
assertEvent(event, 'done syncing')
|
|
139
|
+
return event.value
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
'emit done syncing': emit(({event}) => {
|
|
143
|
+
assertEvent(event, 'done syncing')
|
|
144
|
+
return event
|
|
145
|
+
}),
|
|
146
|
+
},
|
|
147
|
+
guards: {
|
|
148
|
+
'is readOnly': ({context}) => context.readOnly,
|
|
149
|
+
'is processing local changes': ({context}) =>
|
|
150
|
+
context.isProcessingLocalChanges,
|
|
151
|
+
'is processing remote changes': ({context}) =>
|
|
152
|
+
isChangingRemotely(context.slateEditor) ?? false,
|
|
153
|
+
'is busy': and([
|
|
154
|
+
not('is readOnly'),
|
|
155
|
+
or(['is processing local changes', 'is processing remote changes']),
|
|
156
|
+
]),
|
|
157
|
+
'value changed while syncing': ({context, event}) => {
|
|
158
|
+
assertEvent(event, 'done syncing')
|
|
159
|
+
return context.pendingValue !== event.value
|
|
160
|
+
},
|
|
161
|
+
'pending value equals previous value': ({context}) =>
|
|
162
|
+
!(
|
|
163
|
+
context.previousValue === undefined &&
|
|
164
|
+
context.pendingValue === undefined
|
|
165
|
+
) && isEqual(context.pendingValue, context.previousValue),
|
|
166
|
+
},
|
|
167
|
+
actors: {
|
|
168
|
+
'sync value': syncValueLogic,
|
|
169
|
+
},
|
|
170
|
+
}).createMachine({
|
|
171
|
+
id: 'sync',
|
|
172
|
+
context: ({input}) => ({
|
|
173
|
+
isProcessingLocalChanges: false,
|
|
174
|
+
keyGenerator: input.keyGenerator,
|
|
175
|
+
schema: input.schema,
|
|
176
|
+
readOnly: input.readOnly,
|
|
177
|
+
slateEditor: input.slateEditor,
|
|
178
|
+
pendingValue: undefined,
|
|
179
|
+
previousValue: undefined,
|
|
180
|
+
}),
|
|
181
|
+
initial: 'idle',
|
|
182
|
+
on: {
|
|
183
|
+
'has pending patches': {
|
|
184
|
+
actions: assign({
|
|
185
|
+
isProcessingLocalChanges: true,
|
|
186
|
+
}),
|
|
187
|
+
},
|
|
188
|
+
'mutation': {
|
|
189
|
+
actions: assign({
|
|
190
|
+
isProcessingLocalChanges: false,
|
|
191
|
+
}),
|
|
192
|
+
},
|
|
193
|
+
'toggle readOnly': {
|
|
194
|
+
actions: ['assign readOnly'],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
states: {
|
|
198
|
+
idle: {
|
|
199
|
+
on: {
|
|
200
|
+
'update value': [
|
|
201
|
+
{
|
|
202
|
+
guard: 'is busy',
|
|
203
|
+
target: 'busy',
|
|
204
|
+
actions: ['assign pending value'],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
target: 'syncing',
|
|
208
|
+
actions: ['assign pending value'],
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
busy: {
|
|
214
|
+
after: {
|
|
215
|
+
1000: {
|
|
216
|
+
target: 'syncing',
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
on: {
|
|
220
|
+
'update value': [
|
|
221
|
+
{
|
|
222
|
+
guard: 'is busy',
|
|
223
|
+
actions: ['assign pending value'],
|
|
224
|
+
reenter: true,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
target: 'syncing',
|
|
228
|
+
actions: ['assign pending value'],
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
syncing: {
|
|
234
|
+
invoke: {
|
|
235
|
+
src: 'sync value',
|
|
236
|
+
id: 'sync value',
|
|
237
|
+
input: ({context}) => ({
|
|
238
|
+
context: {
|
|
239
|
+
keyGenerator: context.keyGenerator,
|
|
240
|
+
previousValue: context.previousValue,
|
|
241
|
+
readOnly: context.readOnly,
|
|
242
|
+
schema: context.schema,
|
|
243
|
+
},
|
|
244
|
+
slateEditor: context.slateEditor,
|
|
245
|
+
value: context.pendingValue ?? undefined,
|
|
246
|
+
}),
|
|
247
|
+
},
|
|
248
|
+
always: {
|
|
249
|
+
guard: 'pending value equals previous value',
|
|
250
|
+
actions: [
|
|
251
|
+
emit(({context}) => ({
|
|
252
|
+
type: 'done syncing',
|
|
253
|
+
value: context.previousValue,
|
|
254
|
+
})),
|
|
255
|
+
],
|
|
256
|
+
target: 'idle',
|
|
257
|
+
},
|
|
258
|
+
on: {
|
|
259
|
+
'update value': {
|
|
260
|
+
actions: ['assign pending value'],
|
|
261
|
+
},
|
|
262
|
+
'patch': {
|
|
263
|
+
actions: [emit(({event}) => event)],
|
|
264
|
+
},
|
|
265
|
+
'invalid value': {
|
|
266
|
+
actions: [emit(({event}) => event)],
|
|
267
|
+
},
|
|
268
|
+
'value changed': {
|
|
269
|
+
actions: [emit(({event}) => event)],
|
|
270
|
+
},
|
|
271
|
+
'done syncing': [
|
|
272
|
+
{
|
|
273
|
+
guard: 'value changed while syncing',
|
|
274
|
+
actions: ['assign previous value', 'emit done syncing'],
|
|
275
|
+
reenter: true,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
target: 'idle',
|
|
279
|
+
actions: [
|
|
280
|
+
'clear pending value',
|
|
281
|
+
'assign previous value',
|
|
282
|
+
'emit done syncing',
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const debug = debugWithName('hook:useSyncValue')
|
|
292
|
+
|
|
293
|
+
function updateValue({
|
|
294
|
+
context,
|
|
295
|
+
sendBack,
|
|
296
|
+
slateEditor,
|
|
297
|
+
value,
|
|
298
|
+
}: {
|
|
299
|
+
context: {
|
|
300
|
+
keyGenerator: () => string
|
|
301
|
+
previousValue: Array<PortableTextBlock> | undefined
|
|
302
|
+
readOnly: boolean
|
|
303
|
+
schema: EditorSchema
|
|
304
|
+
}
|
|
305
|
+
sendBack: (event: SyncValueEvent) => void
|
|
306
|
+
slateEditor: PortableTextSlateEditor
|
|
307
|
+
value: PortableTextBlock[] | undefined
|
|
308
|
+
}) {
|
|
309
|
+
let isChanged = false
|
|
310
|
+
let isValid = true
|
|
311
|
+
|
|
312
|
+
const hadSelection = !!slateEditor.selection
|
|
313
|
+
|
|
314
|
+
// If empty value, remove everything in the editor and insert a placeholder block
|
|
315
|
+
if (!value || value.length === 0) {
|
|
316
|
+
debug('Value is empty')
|
|
317
|
+
Editor.withoutNormalizing(slateEditor, () => {
|
|
318
|
+
withoutSaving(slateEditor, () => {
|
|
319
|
+
withoutPatching(slateEditor, () => {
|
|
320
|
+
if (hadSelection) {
|
|
321
|
+
Transforms.deselect(slateEditor)
|
|
322
|
+
}
|
|
323
|
+
const childrenLength = slateEditor.children.length
|
|
324
|
+
slateEditor.children.forEach((_, index) => {
|
|
325
|
+
Transforms.removeNodes(slateEditor, {
|
|
326
|
+
at: [childrenLength - 1 - index],
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
Transforms.insertNodes(
|
|
330
|
+
slateEditor,
|
|
331
|
+
slateEditor.pteCreateTextBlock({decorators: []}),
|
|
332
|
+
{at: [0]},
|
|
333
|
+
)
|
|
334
|
+
// Add a new selection in the top of the document
|
|
335
|
+
if (hadSelection) {
|
|
336
|
+
Transforms.select(slateEditor, [0, 0])
|
|
337
|
+
}
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
isChanged = true
|
|
342
|
+
}
|
|
343
|
+
// Remove, replace or add nodes according to what is changed.
|
|
344
|
+
if (value && value.length > 0) {
|
|
345
|
+
const slateValueFromProps = toSlateValue(value, {
|
|
346
|
+
schemaTypes: context.schema,
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
Editor.withoutNormalizing(slateEditor, () => {
|
|
350
|
+
withRemoteChanges(slateEditor, () => {
|
|
351
|
+
withoutPatching(slateEditor, () => {
|
|
352
|
+
const childrenLength = slateEditor.children.length
|
|
353
|
+
|
|
354
|
+
// Remove blocks that have become superfluous
|
|
355
|
+
if (slateValueFromProps.length < childrenLength) {
|
|
356
|
+
for (
|
|
357
|
+
let i = childrenLength - 1;
|
|
358
|
+
i > slateValueFromProps.length - 1;
|
|
359
|
+
i--
|
|
360
|
+
) {
|
|
361
|
+
Transforms.removeNodes(slateEditor, {
|
|
362
|
+
at: [i],
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
isChanged = true
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const [
|
|
369
|
+
currentBlockIndex,
|
|
370
|
+
currentBlock,
|
|
371
|
+
] of slateValueFromProps.entries()) {
|
|
372
|
+
// Go through all of the blocks and see if they need to be updated
|
|
373
|
+
const {blockChanged, blockValid} = syncBlock({
|
|
374
|
+
context,
|
|
375
|
+
sendBack,
|
|
376
|
+
block: currentBlock,
|
|
377
|
+
index: currentBlockIndex,
|
|
378
|
+
slateEditor,
|
|
379
|
+
value,
|
|
380
|
+
})
|
|
381
|
+
isChanged = blockChanged || isChanged
|
|
382
|
+
isValid = isValid && blockValid
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!isValid) {
|
|
390
|
+
debug('Invalid value, returning')
|
|
391
|
+
sendBack({type: 'done syncing', value})
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (isChanged) {
|
|
396
|
+
debug('Server value changed, syncing editor')
|
|
397
|
+
try {
|
|
398
|
+
slateEditor.onChange()
|
|
399
|
+
} catch (err) {
|
|
400
|
+
console.error(err)
|
|
401
|
+
sendBack({
|
|
402
|
+
type: 'invalid value',
|
|
403
|
+
resolution: null,
|
|
404
|
+
value,
|
|
405
|
+
})
|
|
406
|
+
sendBack({type: 'done syncing', value})
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
if (hadSelection && !slateEditor.selection) {
|
|
410
|
+
Transforms.select(slateEditor, {
|
|
411
|
+
anchor: {path: [0, 0], offset: 0},
|
|
412
|
+
focus: {path: [0, 0], offset: 0},
|
|
413
|
+
})
|
|
414
|
+
slateEditor.onChange()
|
|
415
|
+
}
|
|
416
|
+
sendBack({type: 'value changed', value})
|
|
417
|
+
} else {
|
|
418
|
+
debug('Server value and editor value is equal, no need to sync.')
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
sendBack({type: 'done syncing', value})
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function syncBlock({
|
|
425
|
+
context,
|
|
426
|
+
sendBack,
|
|
427
|
+
block,
|
|
428
|
+
index,
|
|
429
|
+
slateEditor,
|
|
430
|
+
value,
|
|
431
|
+
}: {
|
|
432
|
+
context: {
|
|
433
|
+
keyGenerator: () => string
|
|
434
|
+
previousValue: Array<PortableTextBlock> | undefined
|
|
435
|
+
readOnly: boolean
|
|
436
|
+
schema: EditorSchema
|
|
437
|
+
}
|
|
438
|
+
sendBack: (event: SyncValueEvent) => void
|
|
439
|
+
block: Descendant
|
|
440
|
+
index: number
|
|
441
|
+
slateEditor: PortableTextSlateEditor
|
|
442
|
+
value: Array<PortableTextBlock>
|
|
443
|
+
}) {
|
|
444
|
+
let blockChanged = false
|
|
445
|
+
let blockValid = true
|
|
446
|
+
const currentBlock = block
|
|
447
|
+
const currentBlockIndex = index
|
|
448
|
+
const oldBlock = slateEditor.children[currentBlockIndex]
|
|
449
|
+
const hasChanges = oldBlock && !isEqual(currentBlock, oldBlock)
|
|
450
|
+
|
|
451
|
+
if (hasChanges && blockValid) {
|
|
452
|
+
const validationValue = [value[currentBlockIndex]]
|
|
453
|
+
const validation = validateValue(
|
|
454
|
+
validationValue,
|
|
455
|
+
context.schema,
|
|
456
|
+
context.keyGenerator,
|
|
457
|
+
)
|
|
458
|
+
// Resolve validations that can be resolved automatically, without involving the user (but only if the value was changed)
|
|
459
|
+
if (
|
|
460
|
+
!validation.valid &&
|
|
461
|
+
validation.resolution?.autoResolve &&
|
|
462
|
+
validation.resolution?.patches.length > 0
|
|
463
|
+
) {
|
|
464
|
+
// Only apply auto resolution if the value has been populated before and is different from the last one.
|
|
465
|
+
if (
|
|
466
|
+
!context.readOnly &&
|
|
467
|
+
context.previousValue &&
|
|
468
|
+
context.previousValue !== value
|
|
469
|
+
) {
|
|
470
|
+
// Give a console warning about the fact that it did an auto resolution
|
|
471
|
+
console.warn(
|
|
472
|
+
`${validation.resolution.action} for block with _key '${validationValue[0]._key}'. ${validation.resolution?.description}`,
|
|
473
|
+
)
|
|
474
|
+
validation.resolution.patches.forEach((patch) => {
|
|
475
|
+
sendBack({type: 'patch', patch})
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (validation.valid || validation.resolution?.autoResolve) {
|
|
480
|
+
if (oldBlock._key === currentBlock._key) {
|
|
481
|
+
if (debug.enabled) debug('Updating block', oldBlock, currentBlock)
|
|
482
|
+
_updateBlock(slateEditor, currentBlock, oldBlock, currentBlockIndex)
|
|
483
|
+
} else {
|
|
484
|
+
if (debug.enabled) debug('Replacing block', oldBlock, currentBlock)
|
|
485
|
+
_replaceBlock(slateEditor, currentBlock, currentBlockIndex)
|
|
486
|
+
}
|
|
487
|
+
blockChanged = true
|
|
488
|
+
} else {
|
|
489
|
+
sendBack({
|
|
490
|
+
type: 'invalid value',
|
|
491
|
+
resolution: validation.resolution,
|
|
492
|
+
value,
|
|
493
|
+
})
|
|
494
|
+
blockValid = false
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!oldBlock && blockValid) {
|
|
499
|
+
const validationValue = [value[currentBlockIndex]]
|
|
500
|
+
const validation = validateValue(
|
|
501
|
+
validationValue,
|
|
502
|
+
context.schema,
|
|
503
|
+
context.keyGenerator,
|
|
504
|
+
)
|
|
505
|
+
if (debug.enabled)
|
|
506
|
+
debug(
|
|
507
|
+
'Validating and inserting new block in the end of the value',
|
|
508
|
+
currentBlock,
|
|
509
|
+
)
|
|
510
|
+
if (validation.valid || validation.resolution?.autoResolve) {
|
|
511
|
+
Transforms.insertNodes(slateEditor, currentBlock, {
|
|
512
|
+
at: [currentBlockIndex],
|
|
513
|
+
})
|
|
514
|
+
} else {
|
|
515
|
+
debug('Invalid', validation)
|
|
516
|
+
sendBack({
|
|
517
|
+
type: 'invalid value',
|
|
518
|
+
resolution: validation.resolution,
|
|
519
|
+
value,
|
|
520
|
+
})
|
|
521
|
+
blockValid = false
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {blockChanged, blockValid}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* This code is moved out of the above algorithm to keep complexity down.
|
|
530
|
+
* @internal
|
|
531
|
+
*/
|
|
532
|
+
function _replaceBlock(
|
|
533
|
+
slateEditor: PortableTextSlateEditor,
|
|
534
|
+
currentBlock: Descendant,
|
|
535
|
+
currentBlockIndex: number,
|
|
536
|
+
) {
|
|
537
|
+
// While replacing the block and the current selection focus is on the replaced block,
|
|
538
|
+
// temporarily deselect the editor then optimistically try to restore the selection afterwards.
|
|
539
|
+
const currentSelection = slateEditor.selection
|
|
540
|
+
const selectionFocusOnBlock =
|
|
541
|
+
currentSelection && currentSelection.focus.path[0] === currentBlockIndex
|
|
542
|
+
if (selectionFocusOnBlock) {
|
|
543
|
+
Transforms.deselect(slateEditor)
|
|
544
|
+
}
|
|
545
|
+
Transforms.removeNodes(slateEditor, {at: [currentBlockIndex]})
|
|
546
|
+
Transforms.insertNodes(slateEditor, currentBlock, {at: [currentBlockIndex]})
|
|
547
|
+
slateEditor.onChange()
|
|
548
|
+
if (selectionFocusOnBlock) {
|
|
549
|
+
Transforms.select(slateEditor, currentSelection)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* This code is moved out of the above algorithm to keep complexity down.
|
|
555
|
+
* @internal
|
|
556
|
+
*/
|
|
557
|
+
function _updateBlock(
|
|
558
|
+
slateEditor: PortableTextSlateEditor,
|
|
559
|
+
currentBlock: Descendant,
|
|
560
|
+
oldBlock: Descendant,
|
|
561
|
+
currentBlockIndex: number,
|
|
562
|
+
) {
|
|
563
|
+
// Update the root props on the block
|
|
564
|
+
Transforms.setNodes(slateEditor, currentBlock as Partial<Node>, {
|
|
565
|
+
at: [currentBlockIndex],
|
|
566
|
+
})
|
|
567
|
+
// Text block's need to have their children updated as well (setNode does not target a node's children)
|
|
568
|
+
if (
|
|
569
|
+
slateEditor.isTextBlock(currentBlock) &&
|
|
570
|
+
slateEditor.isTextBlock(oldBlock)
|
|
571
|
+
) {
|
|
572
|
+
const oldBlockChildrenLength = oldBlock.children.length
|
|
573
|
+
if (currentBlock.children.length < oldBlockChildrenLength) {
|
|
574
|
+
// Remove any children that have become superfluous
|
|
575
|
+
Array.from(
|
|
576
|
+
Array(oldBlockChildrenLength - currentBlock.children.length),
|
|
577
|
+
).forEach((_, index) => {
|
|
578
|
+
const childIndex = oldBlockChildrenLength - 1 - index
|
|
579
|
+
if (childIndex > 0) {
|
|
580
|
+
debug('Removing child')
|
|
581
|
+
Transforms.removeNodes(slateEditor, {
|
|
582
|
+
at: [currentBlockIndex, childIndex],
|
|
583
|
+
})
|
|
584
|
+
}
|
|
585
|
+
})
|
|
586
|
+
}
|
|
587
|
+
currentBlock.children.forEach(
|
|
588
|
+
(currentBlockChild, currentBlockChildIndex) => {
|
|
589
|
+
const oldBlockChild = oldBlock.children[currentBlockChildIndex]
|
|
590
|
+
const isChildChanged = !isEqual(currentBlockChild, oldBlockChild)
|
|
591
|
+
const isTextChanged = !isEqual(
|
|
592
|
+
currentBlockChild.text,
|
|
593
|
+
oldBlockChild?.text,
|
|
594
|
+
)
|
|
595
|
+
const path = [currentBlockIndex, currentBlockChildIndex]
|
|
596
|
+
if (isChildChanged) {
|
|
597
|
+
// Update if this is the same child
|
|
598
|
+
if (currentBlockChild._key === oldBlockChild?._key) {
|
|
599
|
+
debug('Updating changed child', currentBlockChild, oldBlockChild)
|
|
600
|
+
Transforms.setNodes(
|
|
601
|
+
slateEditor,
|
|
602
|
+
currentBlockChild as Partial<Node>,
|
|
603
|
+
{
|
|
604
|
+
at: path,
|
|
605
|
+
},
|
|
606
|
+
)
|
|
607
|
+
const isSpanNode =
|
|
608
|
+
Text.isText(currentBlockChild) &&
|
|
609
|
+
currentBlockChild._type === 'span' &&
|
|
610
|
+
Text.isText(oldBlockChild) &&
|
|
611
|
+
oldBlockChild._type === 'span'
|
|
612
|
+
if (isSpanNode && isTextChanged) {
|
|
613
|
+
Transforms.delete(slateEditor, {
|
|
614
|
+
at: {
|
|
615
|
+
focus: {path, offset: 0},
|
|
616
|
+
anchor: {path, offset: oldBlockChild.text.length},
|
|
617
|
+
},
|
|
618
|
+
})
|
|
619
|
+
Transforms.insertText(slateEditor, currentBlockChild.text, {
|
|
620
|
+
at: path,
|
|
621
|
+
})
|
|
622
|
+
slateEditor.onChange()
|
|
623
|
+
} else if (!isSpanNode) {
|
|
624
|
+
// If it's a inline block, also update the void text node key
|
|
625
|
+
debug('Updating changed inline object child', currentBlockChild)
|
|
626
|
+
Transforms.setNodes(
|
|
627
|
+
slateEditor,
|
|
628
|
+
{_key: VOID_CHILD_KEY},
|
|
629
|
+
{
|
|
630
|
+
at: [...path, 0],
|
|
631
|
+
voids: true,
|
|
632
|
+
},
|
|
633
|
+
)
|
|
634
|
+
}
|
|
635
|
+
// Replace the child if _key's are different
|
|
636
|
+
} else if (oldBlockChild) {
|
|
637
|
+
debug('Replacing child', currentBlockChild)
|
|
638
|
+
Transforms.removeNodes(slateEditor, {
|
|
639
|
+
at: [currentBlockIndex, currentBlockChildIndex],
|
|
640
|
+
})
|
|
641
|
+
Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
|
|
642
|
+
at: [currentBlockIndex, currentBlockChildIndex],
|
|
643
|
+
})
|
|
644
|
+
slateEditor.onChange()
|
|
645
|
+
// Insert it if it didn't exist before
|
|
646
|
+
} else if (!oldBlockChild) {
|
|
647
|
+
debug('Inserting new child', currentBlockChild)
|
|
648
|
+
Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
|
|
649
|
+
at: [currentBlockIndex, currentBlockChildIndex],
|
|
650
|
+
})
|
|
651
|
+
slateEditor.onChange()
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
}
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import type {BaseEditor, Operation} from 'slate'
|
|
2
2
|
import type {ReactEditor} from 'slate-react'
|
|
3
3
|
import type {PortableTextSlateEditor} from '../types/editor'
|
|
4
|
+
import type {EditorActor} from './editor-machine'
|
|
4
5
|
|
|
5
6
|
// React Compiler considers `slateEditor` as immutable, and opts-out if we do this inline in a useEffect, doing it in a function moves it out of the scope, and opts-in again for the rest of the component.
|
|
6
|
-
export function withSyncRangeDecorations(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
export function withSyncRangeDecorations({
|
|
8
|
+
editorActor,
|
|
9
|
+
slateEditor,
|
|
10
|
+
syncRangeDecorations,
|
|
11
|
+
}: {
|
|
12
|
+
editorActor: EditorActor
|
|
13
|
+
slateEditor: BaseEditor & ReactEditor & PortableTextSlateEditor
|
|
14
|
+
syncRangeDecorations: (operation?: Operation) => void
|
|
15
|
+
}) {
|
|
10
16
|
const originalApply = slateEditor.apply
|
|
17
|
+
|
|
11
18
|
slateEditor.apply = (op: Operation) => {
|
|
12
19
|
originalApply(op)
|
|
13
|
-
|
|
20
|
+
|
|
21
|
+
if (
|
|
22
|
+
!editorActor.getSnapshot().matches({'edit mode': 'read only'}) &&
|
|
23
|
+
op.type !== 'set_selection'
|
|
24
|
+
) {
|
|
14
25
|
syncRangeDecorations(op)
|
|
15
26
|
}
|
|
16
27
|
}
|
|
28
|
+
|
|
17
29
|
return () => {
|
|
18
30
|
slateEditor.apply = originalApply
|
|
19
31
|
}
|