@portabletext/editor 3.2.5 → 3.3.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.
@@ -1,5 +1,4 @@
1
1
  import type {PortableTextBlock} from '@sanity/types'
2
- import {createDraft, finishDraft, type WritableDraft} from 'immer'
3
2
  import {Element, Path, type Node, type Operation} from 'slate'
4
3
  import type {EditorSchema} from '../editor/editor-schema'
5
4
  import type {EditorContext} from '../editor/editor-snapshot'
@@ -14,34 +13,37 @@ import {
14
13
  isPartialSpanNode,
15
14
  isSpanNode,
16
15
  isTextBlockNode,
17
- type PortableTextNode,
16
+ type EditorNode,
17
+ type ObjectNode,
18
18
  type SpanNode,
19
19
  type TextBlockNode,
20
20
  } from './portable-text-node'
21
21
 
22
22
  export function applyOperationToPortableText(
23
- context: Pick<EditorContext, 'keyGenerator' | 'schema'>,
23
+ context: Pick<EditorContext, 'schema'>,
24
24
  value: Array<PortableTextBlock>,
25
25
  operation: OmitFromUnion<Operation, 'type', 'set_selection'>,
26
- ) {
27
- const draft = createDraft({children: value})
26
+ ): Array<PortableTextBlock> {
27
+ const root = {children: value} as EditorNode<EditorSchema>
28
28
 
29
29
  try {
30
- applyOperationToPortableTextDraft(context, draft, operation)
30
+ const newRoot = applyOperationToPortableTextImmutable(
31
+ context,
32
+ root,
33
+ operation,
34
+ )
35
+ return newRoot.children as Array<PortableTextBlock>
31
36
  } catch (e) {
32
37
  console.error(e)
38
+ return value
33
39
  }
34
-
35
- return finishDraft(draft).children
36
40
  }
37
41
 
38
- function applyOperationToPortableTextDraft(
39
- context: Pick<EditorContext, 'keyGenerator' | 'schema'>,
40
- root: WritableDraft<{
41
- children: Array<PortableTextBlock>
42
- }>,
42
+ function applyOperationToPortableTextImmutable(
43
+ context: Pick<EditorContext, 'schema'>,
44
+ root: EditorNode<EditorSchema>,
43
45
  operation: OmitFromUnion<Operation, 'type', 'set_selection'>,
44
- ) {
46
+ ): EditorNode<EditorSchema> {
45
47
  switch (operation.type) {
46
48
  case 'insert_node': {
47
49
  const {path, node: insertedNode} = operation
@@ -49,11 +51,11 @@ function applyOperationToPortableTextDraft(
49
51
  const index = path[path.length - 1]
50
52
 
51
53
  if (!parent) {
52
- break
54
+ return root
53
55
  }
54
56
 
55
57
  if (index > parent.children.length) {
56
- break
58
+ return root
57
59
  }
58
60
 
59
61
  if (path.length === 1) {
@@ -61,8 +63,7 @@ function applyOperationToPortableTextDraft(
61
63
 
62
64
  if (isTextBlockNode(context, insertedNode)) {
63
65
  // Text blocks can be inserted as is
64
-
65
- parent.children.splice(index, 0, {
66
+ const newBlock = {
66
67
  ...insertedNode,
67
68
  children: insertedNode.children.map((child) => {
68
69
  if ('__inline' in child) {
@@ -79,73 +80,88 @@ function applyOperationToPortableTextDraft(
79
80
 
80
81
  return child
81
82
  }),
82
- })
83
+ }
83
84
 
84
- break
85
+ return {
86
+ ...root,
87
+ children: insertChildren(root.children, index, newBlock),
88
+ }
85
89
  }
86
90
 
87
91
  if (Element.isElement(insertedNode) && !('__inline' in insertedNode)) {
88
92
  // Void blocks have to have their `value` spread onto the block
89
-
90
- parent.children.splice(index, 0, {
93
+ const newBlock = {
91
94
  _key: insertedNode._key,
92
95
  _type: insertedNode._type,
93
96
  ...('value' in insertedNode &&
94
97
  typeof insertedNode.value === 'object'
95
98
  ? insertedNode.value
96
99
  : {}),
97
- })
98
- break
100
+ }
101
+
102
+ return {
103
+ ...root,
104
+ children: insertChildren(root.children, index, newBlock),
105
+ }
99
106
  }
100
107
  }
101
108
 
102
109
  if (path.length === 2) {
103
110
  // Inserting children into blocks
111
+ const blockIndex = path[0]
104
112
 
105
113
  if (!isTextBlockNode(context, parent)) {
106
114
  // Only text blocks can have children
107
- break
115
+ return root
108
116
  }
109
117
 
118
+ let newChild: SpanNode<EditorSchema> | ObjectNode | undefined
119
+
110
120
  if (isPartialSpanNode(insertedNode)) {
111
121
  // Text nodes can be inserted as is
112
-
113
- parent.children.splice(index, 0, insertedNode)
114
- break
115
- }
116
-
117
- if ('__inline' in insertedNode) {
122
+ newChild = insertedNode
123
+ } else if ('__inline' in insertedNode) {
118
124
  // Void children have to have their `value` spread onto the block
119
-
120
- parent.children.splice(index, 0, {
125
+ newChild = {
121
126
  _key: insertedNode._key,
122
127
  _type: insertedNode._type,
123
128
  ...('value' in insertedNode &&
124
129
  typeof insertedNode.value === 'object'
125
130
  ? insertedNode.value
126
131
  : {}),
127
- })
128
- break
132
+ }
133
+ } else {
134
+ return root
129
135
  }
136
+
137
+ return updateTextBlockAtIndex(context, root, blockIndex, (block) => ({
138
+ ...block,
139
+ children: insertChildren(block.children, index, newChild),
140
+ }))
130
141
  }
131
142
 
132
- break
143
+ return root
133
144
  }
134
145
 
135
146
  case 'insert_text': {
136
147
  const {path, offset, text} = operation
137
- if (text.length === 0) break
138
- const span = getSpan(context, root, path)
148
+ if (text.length === 0) return root
139
149
 
150
+ const span = getSpan(context, root, path)
140
151
  if (!span) {
141
- break
152
+ return root
142
153
  }
143
154
 
155
+ const blockIndex = path[0]
156
+ const childIndex = path[1]
144
157
  const before = span.text.slice(0, offset)
145
158
  const after = span.text.slice(offset)
146
- span.text = before + text + after
159
+ const newSpan = {...span, text: before + text + after}
147
160
 
148
- break
161
+ return updateTextBlockAtIndex(context, root, blockIndex, (block) => ({
162
+ ...block,
163
+ children: replaceChild(block.children, childIndex, newSpan),
164
+ }))
149
165
  }
150
166
 
151
167
  case 'merge_node': {
@@ -156,32 +172,50 @@ function applyOperationToPortableTextDraft(
156
172
  const parent = getParent(context, root, path)
157
173
 
158
174
  if (!node || !prev || !parent) {
159
- break
175
+ return root
160
176
  }
161
177
 
162
178
  const index = path[path.length - 1]
163
179
 
164
180
  if (isPartialSpanNode(node) && isPartialSpanNode(prev)) {
165
- prev.text += node.text
166
- } else if (
167
- isTextBlockNode(context, node) &&
168
- isTextBlockNode(context, prev)
169
- ) {
170
- prev.children.push(...node.children)
171
- } else {
172
- break
181
+ // Merging spans
182
+ const blockIndex = path[0]
183
+ const newPrev = {...prev, text: prev.text + node.text}
184
+
185
+ return updateTextBlockAtIndex(context, root, blockIndex, (block) => {
186
+ const newChildren = replaceChild(
187
+ block.children,
188
+ index - 1,
189
+ newPrev as never,
190
+ )
191
+ return {
192
+ ...block,
193
+ children: removeChildren(newChildren, index),
194
+ }
195
+ })
173
196
  }
174
197
 
175
- parent.children.splice(index, 1)
198
+ if (isTextBlockNode(context, node) && isTextBlockNode(context, prev)) {
199
+ // Merging blocks
200
+ const newPrev = {
201
+ ...prev,
202
+ children: [...prev.children, ...node.children],
203
+ }
204
+ const newChildren = replaceChild(root.children, index - 1, newPrev)
205
+ return {
206
+ ...root,
207
+ children: removeChildren(newChildren, index),
208
+ }
209
+ }
176
210
 
177
- break
211
+ return root
178
212
  }
179
213
 
180
214
  case 'move_node': {
181
215
  const {path, newPath} = operation
182
216
 
183
217
  if (Path.isAncestor(path, newPath)) {
184
- break
218
+ return root
185
219
  }
186
220
 
187
221
  const node = getNode(context, root, path)
@@ -189,7 +223,32 @@ function applyOperationToPortableTextDraft(
189
223
  const index = path[path.length - 1]
190
224
 
191
225
  if (!node || !parent) {
192
- break
226
+ return root
227
+ }
228
+
229
+ // First, remove the node from its current position
230
+ let newRoot: EditorNode<EditorSchema>
231
+
232
+ if (path.length === 1) {
233
+ // Removing block from root
234
+ newRoot = {
235
+ ...root,
236
+ children: removeChildren(root.children, index),
237
+ }
238
+ } else if (path.length === 2) {
239
+ // Removing child from block
240
+ const blockIndex = path[0]
241
+ newRoot = updateTextBlockAtIndex(
242
+ context,
243
+ root,
244
+ blockIndex,
245
+ (block) => ({
246
+ ...block,
247
+ children: removeChildren(block.children, index),
248
+ }),
249
+ )
250
+ } else {
251
+ return root
193
252
  }
194
253
 
195
254
  // This is tricky, but since the `path` and `newPath` both refer to
@@ -198,55 +257,92 @@ function applyOperationToPortableTextDraft(
198
257
  // of date. So instead of using the `op.newPath` directly, we
199
258
  // transform `op.path` to ascertain what the `newPath` would be after
200
259
  // the operation was applied.
201
- parent.children.splice(index, 1)
202
260
  const truePath = Path.transform(path, operation)!
203
- const newParent = getNode(context, root, Path.parent(truePath))
204
261
  const newIndex = truePath[truePath.length - 1]
205
262
 
206
- if (!newParent) {
207
- break
263
+ if (truePath.length === 1) {
264
+ // Inserting block at root
265
+ return {
266
+ ...newRoot,
267
+ children: insertChildren(newRoot.children, newIndex, node as never),
268
+ }
208
269
  }
209
270
 
210
- if (!('children' in newParent)) {
211
- break
212
- }
271
+ if (truePath.length === 2) {
272
+ // Inserting child into block
273
+ const newBlockIndex = truePath[0]
274
+ const newParent = newRoot.children[newBlockIndex]
213
275
 
214
- if (!Array.isArray(newParent.children)) {
215
- break
216
- }
276
+ if (!newParent || !isTextBlockNode(context, newParent)) {
277
+ return root
278
+ }
217
279
 
218
- newParent.children.splice(newIndex, 0, node)
280
+ return updateTextBlockAtIndex(
281
+ context,
282
+ newRoot,
283
+ newBlockIndex,
284
+ (block) => ({
285
+ ...block,
286
+ children: insertChildren(block.children, newIndex, node as never),
287
+ }),
288
+ )
289
+ }
219
290
 
220
- break
291
+ return root
221
292
  }
222
293
 
223
294
  case 'remove_node': {
224
295
  const {path} = operation
225
296
  const index = path[path.length - 1]
226
297
  const parent = getParent(context, root, path)
227
- parent?.children.splice(index, 1)
228
298
 
229
- break
299
+ if (!parent) {
300
+ return root
301
+ }
302
+
303
+ if (path.length === 1) {
304
+ // Removing block from root
305
+ return {
306
+ ...root,
307
+ children: removeChildren(root.children, index),
308
+ }
309
+ }
310
+
311
+ if (path.length === 2) {
312
+ // Removing child from block
313
+ const blockIndex = path[0]
314
+ return updateTextBlockAtIndex(context, root, blockIndex, (block) => ({
315
+ ...block,
316
+ children: removeChildren(block.children, index),
317
+ }))
318
+ }
319
+
320
+ return root
230
321
  }
231
322
 
232
323
  case 'remove_text': {
233
324
  const {path, offset, text} = operation
234
325
 
235
326
  if (text.length === 0) {
236
- break
327
+ return root
237
328
  }
238
329
 
239
330
  const span = getSpan(context, root, path)
240
331
 
241
332
  if (!span) {
242
- break
333
+ return root
243
334
  }
244
335
 
336
+ const blockIndex = path[0]
337
+ const childIndex = path[1]
245
338
  const before = span.text.slice(0, offset)
246
339
  const after = span.text.slice(offset + text.length)
247
- span.text = before + after
340
+ const newSpan = {...span, text: before + after}
248
341
 
249
- break
342
+ return updateTextBlockAtIndex(context, root, blockIndex, (block) => ({
343
+ ...block,
344
+ children: replaceChild(block.children, childIndex, newSpan as never),
345
+ }))
250
346
  }
251
347
 
252
348
  case 'set_node': {
@@ -255,11 +351,11 @@ function applyOperationToPortableTextDraft(
255
351
  const node = getNode(context, root, path)
256
352
 
257
353
  if (!node) {
258
- break
354
+ return root
259
355
  }
260
356
 
261
357
  if (isEditorNode(node)) {
262
- break
358
+ return root
263
359
  }
264
360
 
265
361
  if (isObjectNode(context, node)) {
@@ -274,6 +370,8 @@ function applyOperationToPortableTextDraft(
274
370
  : {}
275
371
  ) as Partial<Node>
276
372
 
373
+ const newNode = {...node}
374
+
277
375
  for (const key in newProperties) {
278
376
  if (key === 'value') {
279
377
  continue
@@ -282,9 +380,9 @@ function applyOperationToPortableTextDraft(
282
380
  const value = newProperties[key as keyof Partial<Node>]
283
381
 
284
382
  if (value == null) {
285
- delete node[<keyof PortableTextNode<EditorSchema>>key]
383
+ delete newNode[key]
286
384
  } else {
287
- node[<keyof PortableTextNode<EditorSchema>>key] = value
385
+ newNode[key] = value
288
386
  }
289
387
  }
290
388
 
@@ -294,7 +392,7 @@ function applyOperationToPortableTextDraft(
294
392
  }
295
393
 
296
394
  if (!newProperties.hasOwnProperty(key)) {
297
- delete node[<keyof PortableTextNode<EditorSchema>>key]
395
+ delete newNode[key]
298
396
  }
299
397
  }
300
398
 
@@ -302,134 +400,207 @@ function applyOperationToPortableTextDraft(
302
400
  const value = valueAfter[key as keyof Partial<Node>]
303
401
 
304
402
  if (value == null) {
305
- delete node[<keyof PortableTextNode<EditorSchema>>key]
403
+ delete newNode[key]
306
404
  } else {
307
- node[<keyof PortableTextNode<EditorSchema>>key] = value
405
+ newNode[key] = value
308
406
  }
309
407
  }
310
408
 
311
409
  for (const key in valueBefore) {
312
410
  if (!valueAfter.hasOwnProperty(key)) {
313
- delete node[<keyof PortableTextNode<EditorSchema>>key]
411
+ delete newNode[key]
412
+ }
413
+ }
414
+
415
+ if (path.length === 1) {
416
+ return {
417
+ ...root,
418
+ children: replaceChild(root.children, path[0], newNode),
314
419
  }
315
420
  }
316
421
 
317
- break
422
+ if (path.length === 2) {
423
+ return updateTextBlockAtIndex(context, root, path[0], (block) => ({
424
+ ...block,
425
+ children: replaceChild(block.children, path[1], newNode),
426
+ }))
427
+ }
428
+
429
+ return root
318
430
  }
319
431
 
320
432
  if (isTextBlockNode(context, node)) {
433
+ const newNode = {...node}
434
+
321
435
  for (const key in newProperties) {
322
436
  if (key === 'children' || key === 'text') {
323
- break
437
+ continue
324
438
  }
325
439
 
326
440
  const value = newProperties[key as keyof Partial<Node>]
327
441
 
328
442
  if (value == null) {
329
- delete node[<keyof Partial<Node>>key]
443
+ delete newNode[key]
330
444
  } else {
331
- node[<keyof Partial<Node>>key] = value
445
+ newNode[key] = value
332
446
  }
333
447
  }
334
448
 
335
449
  // properties that were previously defined, but are now missing, must be deleted
336
450
  for (const key in properties) {
337
451
  if (!newProperties.hasOwnProperty(key)) {
338
- delete node[<keyof Partial<Node>>key]
452
+ delete newNode[key]
339
453
  }
340
454
  }
341
455
 
342
- break
456
+ return {
457
+ ...root,
458
+ children: replaceChild(root.children, path[0], newNode),
459
+ }
343
460
  }
344
461
 
345
462
  if (isPartialSpanNode(node)) {
463
+ const newNode = {...node}
464
+
346
465
  for (const key in newProperties) {
347
466
  if (key === 'text') {
348
- break
467
+ continue
349
468
  }
350
469
 
351
470
  const value = newProperties[key as keyof Partial<Node>]
352
471
 
353
472
  if (value == null) {
354
- delete node[<keyof PortableTextNode<EditorSchema>>key]
473
+ delete newNode[key]
355
474
  } else {
356
- node[<keyof PortableTextNode<EditorSchema>>key] = value
475
+ newNode[key] = value
357
476
  }
358
477
  }
359
478
 
360
479
  // properties that were previously defined, but are now missing, must be deleted
361
480
  for (const key in properties) {
362
481
  if (!newProperties.hasOwnProperty(key)) {
363
- delete node[<keyof PortableTextNode<EditorSchema>>key]
482
+ delete newNode[key]
364
483
  }
365
484
  }
366
485
 
367
- break
486
+ return updateTextBlockAtIndex(context, root, path[0], (block) => ({
487
+ ...block,
488
+ children: replaceChild(block.children, path[1], newNode),
489
+ }))
368
490
  }
369
491
 
370
- break
492
+ return root
371
493
  }
372
494
 
373
495
  case 'split_node': {
374
496
  const {path, position, properties} = operation
375
497
 
376
498
  if (path.length === 0) {
377
- break
499
+ return root
378
500
  }
379
501
 
380
502
  const parent = getParent(context, root, path)
381
503
  const index = path[path.length - 1]
382
504
 
383
505
  if (!parent) {
384
- break
506
+ return root
385
507
  }
386
508
 
387
509
  if (isEditorNode(parent)) {
388
510
  const block = getBlock(root, path)
389
511
 
390
512
  if (!block || !isTextBlockNode(context, block)) {
391
- break
513
+ return root
392
514
  }
393
515
 
394
516
  const before = block.children.slice(0, position)
395
517
  const after = block.children.slice(position)
396
- block.children = before
518
+ const updatedTextBlockNode = {...block, children: before}
397
519
 
398
520
  // _key is deliberately left out
399
521
  const newTextBlockNode = {
400
522
  ...properties,
401
523
  children: after,
402
524
  _type: context.schema.block.name,
403
- } as unknown as TextBlockNode<EditorSchema>
404
-
405
- parent.children.splice(index + 1, 0, newTextBlockNode)
525
+ }
406
526
 
407
- break
527
+ return {
528
+ ...root,
529
+ children: insertChildren(
530
+ replaceChild(root.children, index, updatedTextBlockNode),
531
+ index + 1,
532
+ newTextBlockNode,
533
+ ),
534
+ }
408
535
  }
409
536
 
410
537
  if (isTextBlockNode(context, parent)) {
411
538
  const node = getNode(context, root, path)
412
539
 
413
540
  if (!node || !isSpanNode(context, node)) {
414
- break
541
+ return root
415
542
  }
416
543
 
544
+ const blockIndex = path[0]
417
545
  const before = node.text.slice(0, position)
418
546
  const after = node.text.slice(position)
419
- node.text = before
547
+ const updatedSpanNode = {...node, text: before}
420
548
 
421
549
  // _key is deliberately left out
422
550
  const newSpanNode = {
423
551
  ...properties,
424
552
  text: after,
425
- } as unknown as SpanNode<EditorSchema>
553
+ }
426
554
 
427
- parent.children.splice(index + 1, 0, newSpanNode)
555
+ return updateTextBlockAtIndex(context, root, blockIndex, (block) => {
556
+ return {
557
+ ...block,
558
+ children: insertChildren(
559
+ replaceChild(block.children, index, updatedSpanNode),
560
+ index + 1,
561
+ newSpanNode,
562
+ ),
563
+ }
564
+ })
428
565
  }
429
566
 
430
- break
567
+ return root
431
568
  }
432
569
  }
570
+ }
571
+
572
+ function insertChildren<T>(children: T[], index: number, ...nodes: T[]): T[] {
573
+ return [...children.slice(0, index), ...nodes, ...children.slice(index)]
574
+ }
575
+
576
+ function removeChildren<T>(children: T[], index: number, count = 1): T[] {
577
+ return [...children.slice(0, index), ...children.slice(index + count)]
578
+ }
579
+
580
+ function replaceChild<T>(children: T[], index: number, newChild: T): T[] {
581
+ return [...children.slice(0, index), newChild, ...children.slice(index + 1)]
582
+ }
583
+
584
+ function updateTextBlockAtIndex(
585
+ context: Pick<EditorContext, 'schema'>,
586
+ root: EditorNode<EditorSchema>,
587
+ blockIndex: number,
588
+ updater: (block: TextBlockNode<EditorSchema>) => TextBlockNode<EditorSchema>,
589
+ ): EditorNode<EditorSchema> {
590
+ const block = root.children.at(blockIndex)
591
+
592
+ if (!block) {
593
+ return root
594
+ }
433
595
 
434
- return root
596
+ if (!isTextBlockNode(context, block)) {
597
+ return root
598
+ }
599
+
600
+ const newBlock = updater(block)
601
+
602
+ return {
603
+ ...root,
604
+ children: replaceChild(root.children, blockIndex, newBlock),
605
+ }
435
606
  }
@@ -31,7 +31,7 @@ export function isEditorNode<TEditorSchema extends EditorSchema>(
31
31
  //////////
32
32
 
33
33
  export type TextBlockNode<TEditorSchema extends EditorSchema> = {
34
- _key: string
34
+ _key?: string
35
35
  _type: TEditorSchema['block']['name']
36
36
  children: Array<SpanNode<TEditorSchema> | ObjectNode>
37
37
  [other: string]: unknown
@@ -47,7 +47,7 @@ export function isTextBlockNode<TEditorSchema extends EditorSchema>(
47
47
  //////////
48
48
 
49
49
  export type SpanNode<TEditorSchema extends EditorSchema> = {
50
- _key: string
50
+ _key?: string
51
51
  _type?: TEditorSchema['span']['name']
52
52
  text: string
53
53
  [other: string]: unknown