@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.
Files changed (82) hide show
  1. package/lib/_chunks-cjs/behavior.core.cjs +30 -28
  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 +12 -10
  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 +35 -35
  14. package/lib/behaviors/index.cjs.map +1 -1
  15. package/lib/behaviors/index.d.cts +40 -45
  16. package/lib/behaviors/index.d.ts +40 -45
  17. package/lib/behaviors/index.js +20 -20
  18. package/lib/behaviors/index.js.map +1 -1
  19. package/lib/index.cjs +868 -542
  20. package/lib/index.cjs.map +1 -1
  21. package/lib/index.d.cts +3791 -4503
  22. package/lib/index.d.ts +3791 -4503
  23. package/lib/index.js +865 -541
  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 +62 -17
  28. package/lib/selectors/index.d.ts +62 -17
  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/behavior-actions/behavior.action-utils.insert-block.ts +3 -5
  33. package/src/behavior-actions/behavior.actions.ts +6 -6
  34. package/src/behavior-actions/behavior.guards.ts +2 -6
  35. package/src/behaviors/behavior.code-editor.ts +5 -9
  36. package/src/behaviors/behavior.core.block-objects.ts +14 -20
  37. package/src/behaviors/behavior.core.lists.ts +13 -19
  38. package/src/behaviors/behavior.links.ts +6 -6
  39. package/src/behaviors/behavior.markdown.ts +27 -40
  40. package/src/behaviors/behavior.types.ts +7 -7
  41. package/src/behaviors/index.ts +1 -0
  42. package/src/editor/Editable.tsx +11 -4
  43. package/src/editor/PortableTextEditor.tsx +4 -5
  44. package/src/editor/{hooks/useSyncValue.test.tsx → __tests__/sync-value.test.tsx} +42 -23
  45. package/src/editor/components/Synchronizer.tsx +53 -80
  46. package/src/{utils/getPortableTextMemberSchemaTypes.ts → editor/create-editor-schema.ts} +3 -3
  47. package/src/editor/create-editor.ts +2 -2
  48. package/src/editor/define-schema.ts +8 -3
  49. package/src/editor/editor-machine.ts +136 -104
  50. package/src/editor/editor-provider.tsx +0 -3
  51. package/src/editor/editor-selector.ts +6 -13
  52. package/src/editor/editor-snapshot.ts +5 -6
  53. package/src/editor/get-active-decorators.ts +20 -0
  54. package/src/editor/mutation-machine.ts +100 -0
  55. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +21 -15
  56. package/src/editor/plugins/createWithMaxBlocks.ts +1 -1
  57. package/src/editor/plugins/createWithPatches.ts +0 -4
  58. package/src/editor/plugins/createWithPlaceholderBlock.ts +1 -1
  59. package/src/editor/plugins/createWithPortableTextSelections.ts +4 -1
  60. package/src/editor/plugins/createWithUndoRedo.ts +3 -3
  61. package/src/editor/sync-machine.ts +657 -0
  62. package/src/editor/withSyncRangeDecorations.ts +17 -5
  63. package/src/index.ts +3 -5
  64. package/src/selectors/_exports/index.ts +1 -0
  65. package/src/selectors/index.ts +10 -4
  66. package/src/selectors/selector.get-active-style.ts +37 -0
  67. package/src/selectors/selector.get-selected-spans.ts +136 -0
  68. package/src/selectors/selector.is-active-annotation.ts +49 -0
  69. package/src/selectors/selector.is-active-decorator.ts +21 -0
  70. package/src/selectors/selector.is-active-list-item.ts +13 -0
  71. package/src/selectors/selector.is-active-style.ts +13 -0
  72. package/src/selectors/selector.is-selection-collapsed.ts +12 -0
  73. package/src/selectors/selector.is-selection-expanded.ts +9 -0
  74. package/src/selectors/selectors.ts +0 -11
  75. package/src/utils/__tests__/operationToPatches.test.ts +2 -2
  76. package/src/utils/__tests__/patchToOperations.test.ts +2 -2
  77. package/src/utils/__tests__/values.test.ts +2 -2
  78. package/src/utils/weakMaps.ts +0 -3
  79. package/src/utils/withChanges.ts +1 -8
  80. package/lib/_chunks-cjs/selectors.cjs.map +0 -1
  81. package/lib/_chunks-es/selectors.js.map +0 -1
  82. 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().context.readOnly) {
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().context.readOnly) {
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().context.readOnly) {
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
- slateEditor: BaseEditor & ReactEditor & PortableTextSlateEditor,
8
- syncRangeDecorations: (operation?: Operation) => void,
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
- if (op.type !== 'set_selection') {
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
  }