@portabletext/editor 1.15.3 → 1.16.1

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