@portabletext/editor 1.0.11 → 1.0.13

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.
@@ -79,15 +79,21 @@ describe('plugin:withPortableTextMarksModel', () => {
79
79
  ]
80
80
 
81
81
  const onChange = jest.fn()
82
+
83
+ render(
84
+ <PortableTextEditorTester
85
+ onChange={onChange}
86
+ ref={editorRef}
87
+ schemaType={schemaType}
88
+ value={initialValue}
89
+ />,
90
+ )
91
+
82
92
  await waitFor(() => {
83
- render(
84
- <PortableTextEditorTester
85
- onChange={onChange}
86
- ref={editorRef}
87
- schemaType={schemaType}
88
- value={initialValue}
89
- />,
90
- )
93
+ if (editorRef.current) {
94
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
95
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
96
+ }
91
97
  })
92
98
 
93
99
  await waitFor(() => {
@@ -382,6 +388,7 @@ Array [
382
388
  }
383
389
  })
384
390
  })
391
+
385
392
  it('toggles marks on children with annotation marks correctly', async () => {
386
393
  const editorRef: RefObject<PortableTextEditor> = createRef()
387
394
  const initialValue = [
@@ -413,61 +420,65 @@ Array [
413
420
  },
414
421
  ]
415
422
  const onChange = jest.fn()
423
+
424
+ render(
425
+ <PortableTextEditorTester
426
+ onChange={onChange}
427
+ ref={editorRef}
428
+ schemaType={schemaType}
429
+ value={initialValue}
430
+ />,
431
+ )
432
+
416
433
  await waitFor(() => {
417
- render(
418
- <PortableTextEditorTester
419
- onChange={onChange}
420
- ref={editorRef}
421
- schemaType={schemaType}
422
- value={initialValue}
423
- />,
424
- )
434
+ if (editorRef.current) {
435
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
436
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
437
+ }
425
438
  })
426
- const editor = editorRef.current!
427
- expect(editor).toBeDefined()
428
439
 
429
440
  await waitFor(() => {
430
- PortableTextEditor.focus(editor)
431
- PortableTextEditor.select(editor, {
432
- focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0},
433
- anchor: {path: [{_key: 'a'}, 'children', {_key: 'b1'}], offset: 12},
434
- })
435
- PortableTextEditor.toggleMark(editor, 'strong')
436
- expect(PortableTextEditor.getValue(editor)).toMatchInlineSnapshot(`
437
- Array [
438
- Object {
439
- "_key": "a",
440
- "_type": "myTestBlockType",
441
- "children": Array [
442
- Object {
443
- "_key": "a1",
444
- "_type": "span",
445
- "marks": Array [
446
- "abc",
447
- "strong",
448
- ],
449
- "text": "A link",
450
- },
451
- Object {
452
- "_key": "a2",
453
- "_type": "span",
454
- "marks": Array [
455
- "strong",
456
- ],
457
- "text": ", not a link",
458
- },
459
- ],
460
- "markDefs": Array [
461
- Object {
462
- "_key": "abc",
463
- "_type": "link",
464
- "href": "http://www.link.com",
465
- },
466
- ],
467
- "style": "normal",
468
- },
469
- ]
470
- `)
441
+ if (editorRef.current) {
442
+ PortableTextEditor.focus(editorRef.current)
443
+ PortableTextEditor.select(editorRef.current, {
444
+ focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0},
445
+ anchor: {path: [{_key: 'a'}, 'children', {_key: 'a2'}], offset: 12},
446
+ })
447
+ PortableTextEditor.toggleMark(editorRef.current, 'strong')
448
+ }
449
+ })
450
+
451
+ await waitFor(() => {
452
+ if (editorRef.current) {
453
+ expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
454
+ {
455
+ _key: 'a',
456
+ _type: 'myTestBlockType',
457
+ children: [
458
+ {
459
+ _key: 'a1',
460
+ _type: 'span',
461
+ marks: ['abc', 'strong'],
462
+ text: 'A link',
463
+ },
464
+ {
465
+ _key: 'a2',
466
+ _type: 'span',
467
+ marks: ['strong'],
468
+ text: ', not a link',
469
+ },
470
+ ],
471
+ markDefs: [
472
+ {
473
+ _type: 'link',
474
+ _key: 'abc',
475
+ href: 'http://www.link.com',
476
+ },
477
+ ],
478
+ style: 'normal',
479
+ },
480
+ ])
481
+ }
471
482
  })
472
483
  })
473
484
 
@@ -605,11 +616,11 @@ Array [
605
616
  const editorRef: RefObject<PortableTextEditor> = createRef()
606
617
  const initialValue = [
607
618
  {
608
- _key: '1987f99da4a2',
619
+ _key: 'ba',
609
620
  _type: 'myTestBlockType',
610
621
  children: [
611
622
  {
612
- _key: '3693e789451c',
623
+ _key: 'sa',
613
624
  _type: 'span',
614
625
  marks: [],
615
626
  text: '1',
@@ -619,19 +630,19 @@ Array [
619
630
  style: 'normal',
620
631
  },
621
632
  {
622
- _key: '2f55670a03bb',
633
+ _key: 'bb',
623
634
  _type: 'myTestBlockType',
624
635
  children: [
625
636
  {
626
- _key: '9f5ed7dee7ab',
637
+ _key: 'sb',
627
638
  _type: 'span',
628
- marks: ['bab319ad3a9d'],
639
+ marks: ['aa'],
629
640
  text: '2',
630
641
  },
631
642
  ],
632
643
  markDefs: [
633
644
  {
634
- _key: 'bab319ad3a9d',
645
+ _key: 'aa',
635
646
  _type: 'link',
636
647
  href: 'http://www.123.com',
637
648
  },
@@ -640,82 +651,87 @@ Array [
640
651
  },
641
652
  ]
642
653
  const sel: EditorSelection = {
643
- focus: {path: [{_key: '2f55670a03bb'}, 'children', {_key: '9f5ed7dee7ab'}], offset: 0},
644
- anchor: {path: [{_key: '2f55670a03bb'}, 'children', {_key: '9f5ed7dee7ab'}], offset: 0},
654
+ focus: {path: [{_key: 'bb'}, 'children', {_key: 'sb'}], offset: 0},
655
+ anchor: {path: [{_key: 'bb'}, 'children', {_key: 'sb'}], offset: 0},
645
656
  }
646
657
  const onChange = jest.fn()
658
+
659
+ render(
660
+ <PortableTextEditorTester
661
+ onChange={onChange}
662
+ ref={editorRef}
663
+ schemaType={schemaType}
664
+ value={initialValue}
665
+ />,
666
+ )
667
+
647
668
  await waitFor(() => {
648
- render(
649
- <PortableTextEditorTester
650
- onChange={onChange}
651
- ref={editorRef}
652
- schemaType={schemaType}
653
- value={initialValue}
654
- />,
655
- )
669
+ if (editorRef.current) {
670
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
671
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
672
+ }
656
673
  })
657
674
 
658
- const editor = editorRef.current!
659
- expect(editor).toBeDefined()
675
+ await waitFor(() => {
676
+ if (editorRef.current) {
677
+ PortableTextEditor.select(editorRef.current, sel)
678
+ PortableTextEditor.insertBreak(editorRef.current)
679
+ }
680
+ })
660
681
 
661
682
  await waitFor(() => {
662
- PortableTextEditor.select(editor, sel)
663
- PortableTextEditor.focus(editor)
664
- PortableTextEditor.insertBreak(editor)
665
- expect(PortableTextEditor.getValue(editor)).toMatchInlineSnapshot(`
666
- Array [
667
- Object {
668
- "_key": "1987f99da4a2",
669
- "_type": "myTestBlockType",
670
- "children": Array [
671
- Object {
672
- "_key": "3693e789451c",
673
- "_type": "span",
674
- "marks": Array [],
675
- "text": "1",
676
- },
677
- ],
678
- "markDefs": Array [],
679
- "style": "normal",
680
- },
681
- Object {
682
- "_key": "3",
683
- "_type": "myTestBlockType",
684
- "children": Array [
685
- Object {
686
- "_key": "2",
687
- "_type": "span",
688
- "marks": Array [],
689
- "text": "",
690
- },
691
- ],
692
- "markDefs": Array [],
693
- "style": "normal",
694
- },
695
- Object {
696
- "_key": "2f55670a03bb",
697
- "_type": "myTestBlockType",
698
- "children": Array [
699
- Object {
700
- "_key": "9f5ed7dee7ab",
701
- "_type": "span",
702
- "marks": Array [
703
- "bab319ad3a9d",
704
- ],
705
- "text": "2",
706
- },
707
- ],
708
- "markDefs": Array [
709
- Object {
710
- "_key": "bab319ad3a9d",
711
- "_type": "link",
712
- "href": "http://www.123.com",
713
- },
714
- ],
715
- "style": "normal",
716
- },
717
- ]
718
- `)
683
+ if (editorRef.current) {
684
+ expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
685
+ {
686
+ _key: 'ba',
687
+ _type: 'myTestBlockType',
688
+ children: [
689
+ {
690
+ _key: 'sa',
691
+ _type: 'span',
692
+ marks: [],
693
+ text: '1',
694
+ },
695
+ ],
696
+ markDefs: [],
697
+ style: 'normal',
698
+ },
699
+ {
700
+ _key: '3',
701
+ _type: 'myTestBlockType',
702
+ children: [
703
+ {
704
+ _key: '2',
705
+ _type: 'span',
706
+ marks: [],
707
+ text: '',
708
+ },
709
+ ],
710
+ markDefs: [],
711
+ style: 'normal',
712
+ },
713
+ {
714
+ _key: 'bb',
715
+ _type: 'myTestBlockType',
716
+ children: [
717
+ {
718
+ _key: 'sb',
719
+ _type: 'span',
720
+ marks: ['aa'],
721
+ text: '2',
722
+ },
723
+ ],
724
+ markDefs: [
725
+ {
726
+ _key: 'aa',
727
+ _type: 'link',
728
+ href: 'http://www.123.com',
729
+ },
730
+ ],
731
+ style: 'normal',
732
+ },
733
+ ])
734
+ }
719
735
  })
720
736
  })
721
737
  })
@@ -724,11 +740,11 @@ Array [
724
740
  const editorRef: RefObject<PortableTextEditor> = createRef()
725
741
  const initialValue = [
726
742
  {
727
- _key: '1987f99da4a2',
743
+ _key: 'ba',
728
744
  _type: 'myTestBlockType',
729
745
  children: [
730
746
  {
731
- _key: '3693e789451c',
747
+ _key: 'sa',
732
748
  _type: 'span',
733
749
  marks: [],
734
750
  text: '',
@@ -740,32 +756,38 @@ Array [
740
756
  ]
741
757
  const onChange = jest.fn()
742
758
 
759
+ render(
760
+ <PortableTextEditorTester
761
+ onChange={onChange}
762
+ ref={editorRef}
763
+ schemaType={schemaType}
764
+ value={initialValue}
765
+ />,
766
+ )
767
+
743
768
  await waitFor(() => {
744
- render(
745
- <PortableTextEditorTester
746
- onChange={onChange}
747
- ref={editorRef}
748
- schemaType={schemaType}
749
- value={initialValue}
750
- />,
751
- )
769
+ if (editorRef.current) {
770
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
771
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
772
+ }
752
773
  })
753
774
 
754
- const editor = editorRef.current!
755
- expect(editor).toBeDefined()
756
-
757
775
  await waitFor(() => {
758
- PortableTextEditor.focus(editor)
776
+ if (editorRef.current) {
777
+ PortableTextEditor.focus(editorRef.current)
778
+ }
759
779
  })
760
- const currentSelectionObject = PortableTextEditor.getSelection(editor)
761
780
 
762
781
  await waitFor(() => {
763
- PortableTextEditor.toggleMark(editor, 'strong')
782
+ if (editorRef.current) {
783
+ const currentSelectionObject = PortableTextEditor.getSelection(editorRef.current)
784
+ PortableTextEditor.toggleMark(editorRef.current, 'strong')
785
+ const nextSelectionObject = PortableTextEditor.getSelection(editorRef.current)
786
+ expect(currentSelectionObject).toEqual(nextSelectionObject)
787
+ expect(currentSelectionObject === nextSelectionObject).toBe(false)
788
+ expect(onChange).toHaveBeenCalledWith({type: 'selection', selection: nextSelectionObject})
789
+ }
764
790
  })
765
- const nextSelectionObject = PortableTextEditor.getSelection(editor)
766
- expect(currentSelectionObject).toEqual(nextSelectionObject)
767
- expect(currentSelectionObject === nextSelectionObject).toBe(false)
768
- expect(onChange).toHaveBeenCalledWith({type: 'selection', selection: nextSelectionObject})
769
791
  })
770
792
 
771
793
  it('should return active marks that cover the whole selection', async () => {
@@ -53,6 +53,14 @@ describe('plugin:withUndoRedo', () => {
53
53
  value={initialValue}
54
54
  />,
55
55
  )
56
+
57
+ await waitFor(() => {
58
+ if (editorRef.current) {
59
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
60
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
61
+ }
62
+ })
63
+
56
64
  await waitFor(() => {
57
65
  if (editorRef.current) {
58
66
  PortableTextEditor.focus(editorRef.current)
@@ -88,6 +96,7 @@ describe('plugin:withUndoRedo', () => {
88
96
  it('preserves the keys when redoing ', async () => {
89
97
  const editorRef: RefObject<PortableTextEditor> = createRef()
90
98
  const onChange = jest.fn()
99
+
91
100
  render(
92
101
  <PortableTextEditorTester
93
102
  onChange={onChange}
@@ -96,6 +105,14 @@ describe('plugin:withUndoRedo', () => {
96
105
  value={initialValue}
97
106
  />,
98
107
  )
108
+
109
+ await waitFor(() => {
110
+ if (editorRef.current) {
111
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
112
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
113
+ }
114
+ })
115
+
99
116
  await waitFor(() => {
100
117
  if (editorRef.current) {
101
118
  PortableTextEditor.focus(editorRef.current)
@@ -23,23 +23,38 @@ export function createWithObjectKeys(
23
23
  editor.apply = (operation) => {
24
24
  if (operation.type === 'split_node') {
25
25
  const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.properties)
26
- operation.properties = {
27
- ...operation.properties,
28
- ...(withNewKey ? {_key: keyGenerator()} : {}),
29
- }
26
+
27
+ apply({
28
+ ...operation,
29
+ properties: {
30
+ ...operation.properties,
31
+ ...(withNewKey ? {_key: keyGenerator()} : {}),
32
+ },
33
+ })
34
+
35
+ return
30
36
  }
37
+
31
38
  if (operation.type === 'insert_node') {
32
39
  // Must be given a new key or adding/removing marks while typing gets in trouble (duped keys)!
33
40
  const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.node)
41
+
34
42
  if (!Editor.isEditor(operation.node)) {
35
- operation.node = {
36
- ...operation.node,
37
- ...(withNewKey ? {_key: keyGenerator()} : {}),
38
- }
43
+ apply({
44
+ ...operation,
45
+ node: {
46
+ ...operation.node,
47
+ ...(withNewKey ? {_key: keyGenerator()} : {}),
48
+ },
49
+ })
50
+
51
+ return
39
52
  }
40
53
  }
54
+
41
55
  apply(operation)
42
56
  }
57
+
43
58
  editor.normalizeNode = (entry) => {
44
59
  const [node, path] = entry
45
60
  if (Element.isElement(node) && node._type === schemaTypes.block.name) {
@@ -6,6 +6,7 @@
6
6
  *
7
7
  */
8
8
 
9
+ import {isPortableTextBlock, isPortableTextSpan} from '@portabletext/toolkit'
9
10
  import {isEqual, uniq} from 'lodash'
10
11
  import {type Subject} from 'rxjs'
11
12
  import {type Descendant, Editor, Element, Path, Range, Text, Transforms} from 'slate'
@@ -18,12 +19,14 @@ import {
18
19
  import {debugWithName} from '../../utils/debug'
19
20
  import {toPortableTextRange} from '../../utils/ranges'
20
21
  import {EMPTY_MARKS} from '../../utils/values'
22
+ import {withoutPreserveKeys} from '../../utils/withPreserveKeys'
21
23
 
22
24
  const debug = debugWithName('plugin:withPortableTextMarkModel')
23
25
 
24
26
  export function createWithPortableTextMarkModel(
25
27
  types: PortableTextMemberSchemaTypes,
26
28
  change$: Subject<EditorChange>,
29
+ keyGenerator: () => string,
27
30
  ): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
28
31
  return function withPortableTextMarkModel(editor: PortableTextSlateEditor) {
29
32
  const {apply, normalizeNode} = editor
@@ -230,8 +233,8 @@ export function createWithPortableTextMarkModel(
230
233
  }
231
234
  }
232
235
 
233
- // Special hook before inserting text at the end of an annotation.
234
236
  editor.apply = (op) => {
237
+ // Special hook before inserting text at the end of an annotation.
235
238
  if (op.type === 'insert_text') {
236
239
  const {selection} = editor
237
240
  if (
@@ -253,26 +256,70 @@ export function createWithPortableTextMarkModel(
253
256
  Array.isArray(node.marks) &&
254
257
  node.marks.length > 0
255
258
  ) {
256
- apply(op)
257
- Transforms.splitNodes(editor, {
258
- match: Text.isText,
259
- at: {...selection.focus, offset: selection.focus.offset},
260
- })
261
259
  const marksWithoutAnnotationMarks: string[] = (
262
260
  {
263
261
  ...(Editor.marks(editor) || {}),
264
262
  }.marks || []
265
263
  ).filter((mark) => decorators.includes(mark))
266
- Transforms.setNodes(
267
- editor,
268
- {marks: marksWithoutAnnotationMarks},
269
- {at: Path.next(selection.focus.path)},
270
- )
264
+ Transforms.insertNodes(editor, {
265
+ _type: 'span',
266
+ _key: keyGenerator(),
267
+ text: op.text,
268
+ marks: marksWithoutAnnotationMarks,
269
+ })
271
270
  debug('Inserting text at end of annotation')
272
271
  return
273
272
  }
274
273
  }
275
274
  }
275
+
276
+ if (op.type === 'remove_text') {
277
+ const nodeEntry = Array.from(
278
+ Editor.nodes(editor, {
279
+ mode: 'lowest',
280
+ at: {path: op.path, offset: op.offset},
281
+ match: (n) => n._type === types.span.name,
282
+ voids: false,
283
+ }),
284
+ )[0]
285
+ const node = nodeEntry[0]
286
+ const blockEntry = Editor.node(editor, Path.parent(op.path))
287
+ const block = blockEntry[0]
288
+
289
+ if (node && isPortableTextSpan(node) && block && isPortableTextBlock(block)) {
290
+ const markDefs = block.markDefs ?? []
291
+ const nodeHasAnnotations = (node.marks ?? []).some((mark) =>
292
+ markDefs.find((markDef) => markDef._key === mark),
293
+ )
294
+ const deletingPartOfTheNode = op.offset !== 0
295
+ const deletingFromTheEnd = op.offset + op.text.length === node.text.length
296
+
297
+ if (nodeHasAnnotations && deletingPartOfTheNode && deletingFromTheEnd) {
298
+ /**
299
+ * If all of these conditions match then override the ordinary
300
+ * `remove_text` operation and turn it into `split_nodes` followed
301
+ * by `remove_nodes`. This is so if the operation can be properly
302
+ * undone. Undoing a `remove_text` results in an `insert_text` and
303
+ * we want to bail out of that in this exact scenario to make sure
304
+ * the inserted text is annotated. (See custom logic regarding
305
+ * `insert_text`)
306
+ */
307
+ Editor.withoutNormalizing(editor, () => {
308
+ withoutPreserveKeys(editor, () => {
309
+ Transforms.splitNodes(editor, {
310
+ match: Text.isText,
311
+ at: {path: op.path, offset: op.offset},
312
+ })
313
+ })
314
+ Transforms.removeNodes(editor, {at: Path.next(op.path)})
315
+ })
316
+
317
+ editor.onChange()
318
+ return
319
+ }
320
+ }
321
+ }
322
+
276
323
  apply(op)
277
324
  }
278
325
 
@@ -419,17 +466,24 @@ export function createWithPortableTextMarkModel(
419
466
  */
420
467
  function mergeSpans(editor: PortableTextSlateEditor) {
421
468
  const {selection} = editor
469
+
422
470
  if (selection) {
423
- for (const [node, path] of Array.from(
471
+ const textNodesInSelection = Array.from(
424
472
  Editor.nodes(editor, {
425
473
  at: Editor.range(editor, [selection.anchor.path[0]], [selection.focus.path[0]]),
474
+ match: Text.isText,
475
+ reverse: true,
426
476
  }),
427
- ).reverse()) {
477
+ )
478
+
479
+ for (const [node, path] of textNodesInSelection) {
428
480
  const [parent] = path.length > 1 ? Editor.node(editor, Path.parent(path)) : [undefined]
429
481
  const nextPath = [path[0], path[1] + 1]
482
+
430
483
  if (editor.isTextBlock(parent)) {
431
484
  const nextNode = parent.children[nextPath[1]]
432
- if (Text.isText(node) && Text.isText(nextNode) && isEqual(nextNode.marks, node.marks)) {
485
+
486
+ if (Text.isText(nextNode) && isEqual(nextNode.marks, node.marks)) {
433
487
  debug('Merging spans')
434
488
  Transforms.mergeNodes(editor, {at: nextPath, voids: true})
435
489
  editor.onChange()
@@ -146,17 +146,15 @@ export function createWithUndoRedo(
146
146
  ),
147
147
  )
148
148
  })
149
+ const reversedOperations = transformedOperations.map(Operation.inverse).reverse()
150
+
149
151
  try {
150
152
  Editor.withoutNormalizing(editor, () => {
151
153
  withPreserveKeys(editor, () => {
152
154
  withoutSaving(editor, () => {
153
- transformedOperations
154
- .map(Operation.inverse)
155
- .reverse()
156
- // eslint-disable-next-line max-nested-callbacks
157
- .forEach((op) => {
158
- editor.apply(op)
159
- })
155
+ reversedOperations.forEach((op) => {
156
+ editor.apply(op)
157
+ })
160
158
  })
161
159
  })
162
160
  })