@portabletext/editor 1.16.0 → 1.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.16.0",
3
+ "version": "1.16.2",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -173,7 +173,8 @@ export class PortableTextEditor extends Component<
173
173
  if (!this.props.editor && !prevProps.editor) {
174
174
  if (this.props.readOnly !== prevProps.readOnly) {
175
175
  this.editor._internal.editorActor.send({
176
- type: 'toggle readOnly',
176
+ type: 'update readOnly',
177
+ readOnly: this.props.readOnly ?? false,
177
178
  })
178
179
  }
179
180
 
@@ -34,18 +34,33 @@ describe('RangeDecorations', () => {
34
34
  },
35
35
  },
36
36
  ]
37
- const {rerender} = render(
38
- <PortableTextEditorTester
39
- onChange={onChange}
40
- rangeDecorations={rangeDecorations}
41
- ref={editorRef}
42
- schemaType={schemaType}
43
- value={value}
44
- />,
37
+
38
+ const {rerender} = await waitFor(() =>
39
+ render(
40
+ <PortableTextEditorTester
41
+ onChange={onChange}
42
+ rangeDecorations={rangeDecorations}
43
+ ref={editorRef}
44
+ schemaType={schemaType}
45
+ value={value}
46
+ />,
47
+ ),
45
48
  )
49
+
50
+ await waitFor(() => {
51
+ if (editorRef.current) {
52
+ expect(onChange).toHaveBeenCalledWith({
53
+ type: 'value',
54
+ value,
55
+ })
56
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
57
+ }
58
+ })
59
+
46
60
  await waitFor(() => {
47
61
  expect([rangeDecorationIteration, 'initial']).toEqual([1, 'initial'])
48
62
  })
63
+
49
64
  // Re-render with the same range decorations
50
65
  rerender(
51
66
  <PortableTextEditorTester
@@ -48,7 +48,7 @@ describe('when PTE would display warnings, instead it self solves', () => {
48
48
  _type: 'myTestBlockType',
49
49
  children: [
50
50
  {
51
- _key: '4',
51
+ _key: '2',
52
52
  _type: 'span',
53
53
  text: 'Hello with a new key',
54
54
  marks: [],
@@ -162,7 +162,7 @@ describe('when PTE would display warnings, instead it self solves', () => {
162
162
  _type: 'myTestBlockType',
163
163
  children: [
164
164
  {
165
- _key: '5',
165
+ _key: '2',
166
166
  _type: 'span',
167
167
  text: '',
168
168
  marks: [],
@@ -176,7 +176,7 @@ describe('when PTE would display warnings, instead it self solves', () => {
176
176
  _type: 'myTestBlockType',
177
177
  children: [
178
178
  {
179
- _key: '6',
179
+ _key: '4',
180
180
  _type: 'span',
181
181
  text: '',
182
182
  marks: [],
@@ -71,7 +71,7 @@ export function Synchronizer(props: SynchronizerProps) {
71
71
  }, [props.editorActor, syncActorRef])
72
72
 
73
73
  useEffect(() => {
74
- syncActorRef.send({type: 'toggle readOnly'})
74
+ syncActorRef.send({type: 'update readOnly', readOnly})
75
75
  }, [syncActorRef, readOnly])
76
76
 
77
77
  useEffect(() => {
@@ -33,6 +33,9 @@ import {createEditableAPI} from './plugins/createWithEditableAPI'
33
33
  export type EditorConfig = {
34
34
  behaviors?: Array<Behavior>
35
35
  keyGenerator?: () => string
36
+ /**
37
+ * @deprecated Will be removed in the next major version
38
+ */
36
39
  maxBlocks?: number
37
40
  readOnly?: boolean
38
41
  initialValue?: Array<PortableTextBlock>
@@ -64,8 +67,8 @@ export type EditorEvent = PickFromUnion<
64
67
  | 'list item.toggle'
65
68
  | 'style.toggle'
66
69
  | 'patches'
67
- | 'toggle readOnly'
68
70
  | 'update behaviors'
71
+ | 'update readOnly'
69
72
  | 'update value'
70
73
  >
71
74
 
@@ -81,6 +81,10 @@ export type InternalEditorEvent =
81
81
  editor: PortableTextSlateEditor
82
82
  actionIntends: Array<BehaviorActionIntend>
83
83
  }
84
+ | {
85
+ type: 'update readOnly'
86
+ readOnly: boolean
87
+ }
84
88
  | {
85
89
  type: 'update schema'
86
90
  schema: EditorSchema
@@ -93,9 +97,6 @@ export type InternalEditorEvent =
93
97
  type: 'update value'
94
98
  value: Array<PortableTextBlock> | undefined
95
99
  }
96
- | {
97
- type: 'toggle readOnly'
98
- }
99
100
  | {
100
101
  type: 'update maxBlocks'
101
102
  maxBlocks: number | undefined
@@ -439,7 +440,8 @@ export const editorMachine = setup({
439
440
  },
440
441
  'read only': {
441
442
  on: {
442
- 'toggle readOnly': {
443
+ 'update readOnly': {
444
+ guard: ({event}) => !event.readOnly,
443
445
  target: '#editor.edit mode.editable',
444
446
  actions: ['emit editable'],
445
447
  },
@@ -449,7 +451,8 @@ export const editorMachine = setup({
449
451
  },
450
452
  'editable': {
451
453
  on: {
452
- 'toggle readOnly': {
454
+ 'update readOnly': {
455
+ guard: ({event}) => event.readOnly,
453
456
  target: '#editor.edit mode.read only.read only',
454
457
  actions: ['emit read only'],
455
458
  },
@@ -57,6 +57,7 @@ const syncValueCallback: CallbackLogicFunction<
57
57
  schema: EditorSchema
58
58
  }
59
59
  slateEditor: PortableTextSlateEditor
60
+ streamBlocks: boolean
60
61
  value: Array<PortableTextBlock> | undefined
61
62
  }
62
63
  > = ({sendBack, input}) => {
@@ -65,6 +66,7 @@ const syncValueCallback: CallbackLogicFunction<
65
66
  sendBack,
66
67
  slateEditor: input.slateEditor,
67
68
  value: input.value,
69
+ streamBlocks: input.streamBlocks,
68
70
  })
69
71
  }
70
72
 
@@ -85,6 +87,7 @@ const syncValueLogic = fromCallback(syncValueCallback)
85
87
  export const syncMachine = setup({
86
88
  types: {
87
89
  context: {} as {
90
+ initialValueSynced: boolean
88
91
  isProcessingLocalChanges: boolean
89
92
  keyGenerator: () => string
90
93
  schema: EditorSchema
@@ -111,7 +114,8 @@ export const syncMachine = setup({
111
114
  value: Array<PortableTextBlock> | undefined
112
115
  }
113
116
  | {
114
- type: 'toggle readOnly'
117
+ type: 'update readOnly'
118
+ readOnly: boolean
115
119
  }
116
120
  | SyncValueEvent,
117
121
  emitted: {} as PickFromUnion<
@@ -121,8 +125,14 @@ export const syncMachine = setup({
121
125
  >,
122
126
  },
123
127
  actions: {
128
+ 'assign initial value synced': assign({
129
+ initialValueSynced: true,
130
+ }),
124
131
  'assign readOnly': assign({
125
- readOnly: ({context}) => !context.readOnly,
132
+ readOnly: ({event}) => {
133
+ assertEvent(event, 'update readOnly')
134
+ return event.readOnly
135
+ },
126
136
  }),
127
137
  'assign pending value': assign({
128
138
  pendingValue: ({event}) => {
@@ -170,6 +180,7 @@ export const syncMachine = setup({
170
180
  }).createMachine({
171
181
  id: 'sync',
172
182
  context: ({input}) => ({
183
+ initialValueSynced: false,
173
184
  isProcessingLocalChanges: false,
174
185
  keyGenerator: input.keyGenerator,
175
186
  schema: input.schema,
@@ -190,7 +201,7 @@ export const syncMachine = setup({
190
201
  isProcessingLocalChanges: false,
191
202
  }),
192
203
  },
193
- 'toggle readOnly': {
204
+ 'update readOnly': {
194
205
  actions: ['assign readOnly'],
195
206
  },
196
207
  },
@@ -234,16 +245,19 @@ export const syncMachine = setup({
234
245
  invoke: {
235
246
  src: 'sync value',
236
247
  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
- }),
248
+ input: ({context}) => {
249
+ return {
250
+ context: {
251
+ keyGenerator: context.keyGenerator,
252
+ previousValue: context.previousValue,
253
+ readOnly: context.readOnly,
254
+ schema: context.schema,
255
+ },
256
+ slateEditor: context.slateEditor,
257
+ streamBlocks: !context.initialValueSynced,
258
+ value: context.pendingValue ?? undefined,
259
+ }
260
+ },
247
261
  },
248
262
  always: {
249
263
  guard: 'pending value equals previous value',
@@ -271,7 +285,11 @@ export const syncMachine = setup({
271
285
  'done syncing': [
272
286
  {
273
287
  guard: 'value changed while syncing',
274
- actions: ['assign previous value', 'emit done syncing'],
288
+ actions: [
289
+ 'assign previous value',
290
+ 'emit done syncing',
291
+ 'assign initial value synced',
292
+ ],
275
293
  reenter: true,
276
294
  },
277
295
  {
@@ -280,6 +298,7 @@ export const syncMachine = setup({
280
298
  'clear pending value',
281
299
  'assign previous value',
282
300
  'emit done syncing',
301
+ 'assign initial value synced',
283
302
  ],
284
303
  },
285
304
  ],
@@ -290,10 +309,11 @@ export const syncMachine = setup({
290
309
 
291
310
  const debug = debugWithName('hook:useSyncValue')
292
311
 
293
- function updateValue({
312
+ async function updateValue({
294
313
  context,
295
314
  sendBack,
296
315
  slateEditor,
316
+ streamBlocks,
297
317
  value,
298
318
  }: {
299
319
  context: {
@@ -304,6 +324,7 @@ function updateValue({
304
324
  }
305
325
  sendBack: (event: SyncValueEvent) => void
306
326
  slateEditor: PortableTextSlateEditor
327
+ streamBlocks: boolean
307
328
  value: PortableTextBlock[] | undefined
308
329
  }) {
309
330
  let isChanged = false
@@ -346,41 +367,45 @@ function updateValue({
346
367
  schemaTypes: context.schema,
347
368
  })
348
369
 
349
- Editor.withoutNormalizing(slateEditor, () => {
350
- withRemoteChanges(slateEditor, () => {
351
- withoutPatching(slateEditor, () => {
352
- const childrenLength = slateEditor.children.length
370
+ await new Promise<void>((resolve) => {
371
+ Editor.withoutNormalizing(slateEditor, () => {
372
+ withRemoteChanges(slateEditor, () => {
373
+ withoutPatching(slateEditor, async () => {
374
+ const childrenLength = slateEditor.children.length
353
375
 
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],
376
+ // Remove blocks that have become superfluous
377
+ if (slateValueFromProps.length < childrenLength) {
378
+ for (
379
+ let i = childrenLength - 1;
380
+ i > slateValueFromProps.length - 1;
381
+ i--
382
+ ) {
383
+ Transforms.removeNodes(slateEditor, {
384
+ at: [i],
385
+ })
386
+ }
387
+ isChanged = true
388
+ }
389
+
390
+ for await (const [currentBlock, currentBlockIndex] of getBlocks({
391
+ slateValue: slateValueFromProps,
392
+ streamBlocks,
393
+ })) {
394
+ // Go through all of the blocks and see if they need to be updated
395
+ const {blockChanged, blockValid} = syncBlock({
396
+ context,
397
+ sendBack,
398
+ block: currentBlock,
399
+ index: currentBlockIndex,
400
+ slateEditor,
401
+ value,
363
402
  })
403
+ isChanged = blockChanged || isChanged
404
+ isValid = isValid && blockValid
364
405
  }
365
- isChanged = true
366
- }
367
406
 
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
- }
407
+ resolve()
408
+ })
384
409
  })
385
410
  })
386
411
  })
@@ -421,6 +446,23 @@ function updateValue({
421
446
  sendBack({type: 'done syncing', value})
422
447
  }
423
448
 
449
+ async function* getBlocks({
450
+ slateValue,
451
+ streamBlocks,
452
+ }: {
453
+ slateValue: Array<Descendant>
454
+ streamBlocks: boolean
455
+ }) {
456
+ let index = 0
457
+ for await (const block of slateValue) {
458
+ if (streamBlocks) {
459
+ await new Promise<void>((resolve) => setTimeout(resolve, 0))
460
+ }
461
+ yield [block, index] as const
462
+ index++
463
+ }
464
+ }
465
+
424
466
  function syncBlock({
425
467
  context,
426
468
  sendBack,
@@ -448,79 +490,91 @@ function syncBlock({
448
490
  const oldBlock = slateEditor.children[currentBlockIndex]
449
491
  const hasChanges = oldBlock && !isEqual(currentBlock, oldBlock)
450
492
 
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
- }
493
+ Editor.withoutNormalizing(slateEditor, () => {
494
+ withRemoteChanges(slateEditor, () => {
495
+ withoutPatching(slateEditor, () => {
496
+ if (hasChanges && blockValid) {
497
+ const validationValue = [value[currentBlockIndex]]
498
+ const validation = validateValue(
499
+ validationValue,
500
+ context.schema,
501
+ context.keyGenerator,
502
+ )
503
+ // Resolve validations that can be resolved automatically, without involving the user (but only if the value was changed)
504
+ if (
505
+ !validation.valid &&
506
+ validation.resolution?.autoResolve &&
507
+ validation.resolution?.patches.length > 0
508
+ ) {
509
+ // Only apply auto resolution if the value has been populated before and is different from the last one.
510
+ if (
511
+ !context.readOnly &&
512
+ context.previousValue &&
513
+ context.previousValue !== value
514
+ ) {
515
+ // Give a console warning about the fact that it did an auto resolution
516
+ console.warn(
517
+ `${validation.resolution.action} for block with _key '${validationValue[0]._key}'. ${validation.resolution?.description}`,
518
+ )
519
+ validation.resolution.patches.forEach((patch) => {
520
+ sendBack({type: 'patch', patch})
521
+ })
522
+ }
523
+ }
524
+ if (validation.valid || validation.resolution?.autoResolve) {
525
+ if (oldBlock._key === currentBlock._key) {
526
+ if (debug.enabled) debug('Updating block', oldBlock, currentBlock)
527
+ _updateBlock(
528
+ slateEditor,
529
+ currentBlock,
530
+ oldBlock,
531
+ currentBlockIndex,
532
+ )
533
+ } else {
534
+ if (debug.enabled)
535
+ debug('Replacing block', oldBlock, currentBlock)
536
+ _replaceBlock(slateEditor, currentBlock, currentBlockIndex)
537
+ }
538
+ blockChanged = true
539
+ } else {
540
+ sendBack({
541
+ type: 'invalid value',
542
+ resolution: validation.resolution,
543
+ value,
544
+ })
545
+ blockValid = false
546
+ }
547
+ }
497
548
 
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,
549
+ if (!oldBlock && blockValid) {
550
+ const validationValue = [value[currentBlockIndex]]
551
+ const validation = validateValue(
552
+ validationValue,
553
+ context.schema,
554
+ context.keyGenerator,
555
+ )
556
+ if (debug.enabled)
557
+ debug(
558
+ 'Validating and inserting new block in the end of the value',
559
+ currentBlock,
560
+ )
561
+ if (validation.valid || validation.resolution?.autoResolve) {
562
+ Transforms.insertNodes(slateEditor, currentBlock, {
563
+ at: [currentBlockIndex],
564
+ })
565
+ } else {
566
+ debug('Invalid', validation)
567
+ sendBack({
568
+ type: 'invalid value',
569
+ resolution: validation.resolution,
570
+ value,
571
+ })
572
+ blockValid = false
573
+ }
574
+ }
520
575
  })
521
- blockValid = false
522
- }
523
- }
576
+ })
577
+ })
524
578
 
525
579
  return {blockChanged, blockValid}
526
580
  }