@portabletext/editor 1.16.1 → 1.16.3

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.
@@ -3,13 +3,10 @@ import type {PortableTextBlock} from '@sanity/types'
3
3
  import {isEqual} from 'lodash'
4
4
  import {Editor, Text, Transforms, type Descendant, type Node} from 'slate'
5
5
  import {
6
- and,
7
6
  assertEvent,
8
7
  assign,
9
8
  emit,
10
9
  fromCallback,
11
- not,
12
- or,
13
10
  setup,
14
11
  type AnyEventObject,
15
12
  type CallbackLogicFunction,
@@ -57,6 +54,7 @@ const syncValueCallback: CallbackLogicFunction<
57
54
  schema: EditorSchema
58
55
  }
59
56
  slateEditor: PortableTextSlateEditor
57
+ streamBlocks: boolean
60
58
  value: Array<PortableTextBlock> | undefined
61
59
  }
62
60
  > = ({sendBack, input}) => {
@@ -65,6 +63,7 @@ const syncValueCallback: CallbackLogicFunction<
65
63
  sendBack,
66
64
  slateEditor: input.slateEditor,
67
65
  value: input.value,
66
+ streamBlocks: input.streamBlocks,
68
67
  })
69
68
  }
70
69
 
@@ -85,6 +84,7 @@ const syncValueLogic = fromCallback(syncValueCallback)
85
84
  export const syncMachine = setup({
86
85
  types: {
87
86
  context: {} as {
87
+ initialValueSynced: boolean
88
88
  isProcessingLocalChanges: boolean
89
89
  keyGenerator: () => string
90
90
  schema: EditorSchema
@@ -115,13 +115,18 @@ export const syncMachine = setup({
115
115
  readOnly: boolean
116
116
  }
117
117
  | SyncValueEvent,
118
- emitted: {} as PickFromUnion<
119
- SyncValueEvent,
120
- 'type',
121
- 'done syncing' | 'invalid value' | 'patch' | 'value changed'
122
- >,
118
+ emitted: {} as
119
+ | PickFromUnion<
120
+ SyncValueEvent,
121
+ 'type',
122
+ 'invalid value' | 'patch' | 'value changed'
123
+ >
124
+ | {type: 'done syncing initial value'},
123
125
  },
124
126
  actions: {
127
+ 'assign initial value synced': assign({
128
+ initialValueSynced: true,
129
+ }),
125
130
  'assign readOnly': assign({
126
131
  readOnly: ({event}) => {
127
132
  assertEvent(event, 'update readOnly')
@@ -143,21 +148,19 @@ export const syncMachine = setup({
143
148
  return event.value
144
149
  },
145
150
  }),
146
- 'emit done syncing': emit(({event}) => {
147
- assertEvent(event, 'done syncing')
148
- return event
151
+ 'emit done syncing initial value': emit({
152
+ type: 'done syncing initial value',
149
153
  }),
150
154
  },
151
155
  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
- ]),
156
+ 'initial value synced': ({context}) => context.initialValueSynced,
157
+ 'is busy': ({context}) => {
158
+ return (
159
+ !context.readOnly &&
160
+ (context.isProcessingLocalChanges ||
161
+ (isChangingRemotely(context.slateEditor) ?? false))
162
+ )
163
+ },
161
164
  'value changed while syncing': ({context, event}) => {
162
165
  assertEvent(event, 'done syncing')
163
166
  return context.pendingValue !== event.value
@@ -174,6 +177,7 @@ export const syncMachine = setup({
174
177
  }).createMachine({
175
178
  id: 'sync',
176
179
  context: ({input}) => ({
180
+ initialValueSynced: false,
177
181
  isProcessingLocalChanges: false,
178
182
  keyGenerator: input.keyGenerator,
179
183
  schema: input.schema,
@@ -182,7 +186,6 @@ export const syncMachine = setup({
182
186
  pendingValue: undefined,
183
187
  previousValue: undefined,
184
188
  }),
185
- initial: 'idle',
186
189
  on: {
187
190
  'has pending patches': {
188
191
  actions: assign({
@@ -198,95 +201,117 @@ export const syncMachine = setup({
198
201
  actions: ['assign readOnly'],
199
202
  },
200
203
  },
204
+ type: 'parallel',
201
205
  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'],
206
+ 'setting up': {
207
+ initial: 'syncing initial value',
208
+ states: {
209
+ 'syncing initial value': {
210
+ always: {
211
+ guard: 'initial value synced',
212
+ target: 'done syncing initial value',
213
213
  },
214
- ],
215
- },
216
- },
217
- busy: {
218
- after: {
219
- 1000: {
220
- target: 'syncing',
221
214
  },
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
- ],
215
+ 'done syncing initial value': {
216
+ entry: ['emit done syncing initial value'],
217
+ type: 'final',
218
+ },
235
219
  },
236
220
  },
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,
221
+ 'syncing': {
222
+ initial: 'idle',
223
+ states: {
224
+ idle: {
225
+ on: {
226
+ 'update value': [
227
+ {
228
+ guard: 'is busy',
229
+ target: 'busy',
230
+ actions: ['assign pending value'],
231
+ },
232
+ {
233
+ target: 'syncing',
234
+ actions: ['assign pending value'],
235
+ },
236
+ ],
247
237
  },
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
238
  },
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,
239
+ busy: {
240
+ after: {
241
+ 1000: [
242
+ {
243
+ guard: 'is busy',
244
+ reenter: true,
245
+ },
246
+ {
247
+ target: 'syncing',
248
+ },
249
+ ],
280
250
  },
281
- {
251
+ on: {
252
+ 'update value': [
253
+ {
254
+ actions: ['assign pending value'],
255
+ },
256
+ ],
257
+ },
258
+ },
259
+ syncing: {
260
+ always: {
261
+ guard: 'pending value equals previous value',
282
262
  target: 'idle',
283
- actions: [
284
- 'clear pending value',
285
- 'assign previous value',
286
- 'emit done syncing',
263
+ actions: ['clear pending value', 'assign initial value synced'],
264
+ },
265
+ invoke: {
266
+ src: 'sync value',
267
+ id: 'sync value',
268
+ input: ({context}) => {
269
+ return {
270
+ context: {
271
+ keyGenerator: context.keyGenerator,
272
+ previousValue: context.previousValue,
273
+ readOnly: context.readOnly,
274
+ schema: context.schema,
275
+ },
276
+ slateEditor: context.slateEditor,
277
+ streamBlocks: !context.initialValueSynced,
278
+ value: context.pendingValue,
279
+ }
280
+ },
281
+ },
282
+ on: {
283
+ 'update value': {
284
+ actions: ['assign pending value'],
285
+ },
286
+ 'patch': {
287
+ actions: [emit(({event}) => event)],
288
+ },
289
+ 'invalid value': {
290
+ actions: [emit(({event}) => event)],
291
+ },
292
+ 'value changed': {
293
+ actions: [emit(({event}) => event)],
294
+ },
295
+ 'done syncing': [
296
+ {
297
+ guard: 'value changed while syncing',
298
+ actions: [
299
+ 'assign previous value',
300
+ 'assign initial value synced',
301
+ ],
302
+ reenter: true,
303
+ },
304
+ {
305
+ target: 'idle',
306
+ actions: [
307
+ 'clear pending value',
308
+ 'assign previous value',
309
+ 'assign initial value synced',
310
+ ],
311
+ },
287
312
  ],
288
313
  },
289
- ],
314
+ },
290
315
  },
291
316
  },
292
317
  },
@@ -294,10 +319,11 @@ export const syncMachine = setup({
294
319
 
295
320
  const debug = debugWithName('hook:useSyncValue')
296
321
 
297
- function updateValue({
322
+ async function updateValue({
298
323
  context,
299
324
  sendBack,
300
325
  slateEditor,
326
+ streamBlocks,
301
327
  value,
302
328
  }: {
303
329
  context: {
@@ -308,6 +334,7 @@ function updateValue({
308
334
  }
309
335
  sendBack: (event: SyncValueEvent) => void
310
336
  slateEditor: PortableTextSlateEditor
337
+ streamBlocks: boolean
311
338
  value: PortableTextBlock[] | undefined
312
339
  }) {
313
340
  let isChanged = false
@@ -350,41 +377,45 @@ function updateValue({
350
377
  schemaTypes: context.schema,
351
378
  })
352
379
 
353
- Editor.withoutNormalizing(slateEditor, () => {
354
- withRemoteChanges(slateEditor, () => {
355
- withoutPatching(slateEditor, () => {
356
- const childrenLength = slateEditor.children.length
380
+ await new Promise<void>((resolve) => {
381
+ Editor.withoutNormalizing(slateEditor, () => {
382
+ withRemoteChanges(slateEditor, () => {
383
+ withoutPatching(slateEditor, async () => {
384
+ const childrenLength = slateEditor.children.length
357
385
 
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],
386
+ // Remove blocks that have become superfluous
387
+ if (slateValueFromProps.length < childrenLength) {
388
+ for (
389
+ let i = childrenLength - 1;
390
+ i > slateValueFromProps.length - 1;
391
+ i--
392
+ ) {
393
+ Transforms.removeNodes(slateEditor, {
394
+ at: [i],
395
+ })
396
+ }
397
+ isChanged = true
398
+ }
399
+
400
+ for await (const [currentBlock, currentBlockIndex] of getBlocks({
401
+ slateValue: slateValueFromProps,
402
+ streamBlocks,
403
+ })) {
404
+ // Go through all of the blocks and see if they need to be updated
405
+ const {blockChanged, blockValid} = syncBlock({
406
+ context,
407
+ sendBack,
408
+ block: currentBlock,
409
+ index: currentBlockIndex,
410
+ slateEditor,
411
+ value,
367
412
  })
413
+ isChanged = blockChanged || isChanged
414
+ isValid = isValid && blockValid
368
415
  }
369
- isChanged = true
370
- }
371
416
 
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
- }
417
+ resolve()
418
+ })
388
419
  })
389
420
  })
390
421
  })
@@ -425,6 +456,23 @@ function updateValue({
425
456
  sendBack({type: 'done syncing', value})
426
457
  }
427
458
 
459
+ async function* getBlocks({
460
+ slateValue,
461
+ streamBlocks,
462
+ }: {
463
+ slateValue: Array<Descendant>
464
+ streamBlocks: boolean
465
+ }) {
466
+ let index = 0
467
+ for await (const block of slateValue) {
468
+ if (streamBlocks) {
469
+ await new Promise<void>((resolve) => setTimeout(resolve, 0))
470
+ }
471
+ yield [block, index] as const
472
+ index++
473
+ }
474
+ }
475
+
428
476
  function syncBlock({
429
477
  context,
430
478
  sendBack,
@@ -452,79 +500,91 @@ function syncBlock({
452
500
  const oldBlock = slateEditor.children[currentBlockIndex]
453
501
  const hasChanges = oldBlock && !isEqual(currentBlock, oldBlock)
454
502
 
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
- }
503
+ Editor.withoutNormalizing(slateEditor, () => {
504
+ withRemoteChanges(slateEditor, () => {
505
+ withoutPatching(slateEditor, () => {
506
+ if (hasChanges && blockValid) {
507
+ const validationValue = [value[currentBlockIndex]]
508
+ const validation = validateValue(
509
+ validationValue,
510
+ context.schema,
511
+ context.keyGenerator,
512
+ )
513
+ // Resolve validations that can be resolved automatically, without involving the user (but only if the value was changed)
514
+ if (
515
+ !validation.valid &&
516
+ validation.resolution?.autoResolve &&
517
+ validation.resolution?.patches.length > 0
518
+ ) {
519
+ // Only apply auto resolution if the value has been populated before and is different from the last one.
520
+ if (
521
+ !context.readOnly &&
522
+ context.previousValue &&
523
+ context.previousValue !== value
524
+ ) {
525
+ // Give a console warning about the fact that it did an auto resolution
526
+ console.warn(
527
+ `${validation.resolution.action} for block with _key '${validationValue[0]._key}'. ${validation.resolution?.description}`,
528
+ )
529
+ validation.resolution.patches.forEach((patch) => {
530
+ sendBack({type: 'patch', patch})
531
+ })
532
+ }
533
+ }
534
+ if (validation.valid || validation.resolution?.autoResolve) {
535
+ if (oldBlock._key === currentBlock._key) {
536
+ if (debug.enabled) debug('Updating block', oldBlock, currentBlock)
537
+ _updateBlock(
538
+ slateEditor,
539
+ currentBlock,
540
+ oldBlock,
541
+ currentBlockIndex,
542
+ )
543
+ } else {
544
+ if (debug.enabled)
545
+ debug('Replacing block', oldBlock, currentBlock)
546
+ _replaceBlock(slateEditor, currentBlock, currentBlockIndex)
547
+ }
548
+ blockChanged = true
549
+ } else {
550
+ sendBack({
551
+ type: 'invalid value',
552
+ resolution: validation.resolution,
553
+ value,
554
+ })
555
+ blockValid = false
556
+ }
557
+ }
501
558
 
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,
559
+ if (!oldBlock && blockValid) {
560
+ const validationValue = [value[currentBlockIndex]]
561
+ const validation = validateValue(
562
+ validationValue,
563
+ context.schema,
564
+ context.keyGenerator,
565
+ )
566
+ if (debug.enabled)
567
+ debug(
568
+ 'Validating and inserting new block in the end of the value',
569
+ currentBlock,
570
+ )
571
+ if (validation.valid || validation.resolution?.autoResolve) {
572
+ Transforms.insertNodes(slateEditor, currentBlock, {
573
+ at: [currentBlockIndex],
574
+ })
575
+ } else {
576
+ debug('Invalid', validation)
577
+ sendBack({
578
+ type: 'invalid value',
579
+ resolution: validation.resolution,
580
+ value,
581
+ })
582
+ blockValid = false
583
+ }
584
+ }
524
585
  })
525
- blockValid = false
526
- }
527
- }
586
+ })
587
+ })
528
588
 
529
589
  return {blockChanged, blockValid}
530
590
  }