@portabletext/editor 1.0.11 → 1.0.12

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,6 +19,7 @@ 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
 
@@ -230,8 +232,8 @@ export function createWithPortableTextMarkModel(
230
232
  }
231
233
  }
232
234
 
233
- // Special hook before inserting text at the end of an annotation.
234
235
  editor.apply = (op) => {
236
+ // Special hook before inserting text at the end of an annotation.
235
237
  if (op.type === 'insert_text') {
236
238
  const {selection} = editor
237
239
  if (
@@ -273,6 +275,54 @@ export function createWithPortableTextMarkModel(
273
275
  }
274
276
  }
275
277
  }
278
+
279
+ if (op.type === 'remove_text') {
280
+ const nodeEntry = Array.from(
281
+ Editor.nodes(editor, {
282
+ mode: 'lowest',
283
+ at: {path: op.path, offset: op.offset},
284
+ match: (n) => n._type === types.span.name,
285
+ voids: false,
286
+ }),
287
+ )[0]
288
+ const node = nodeEntry[0]
289
+ const blockEntry = Editor.node(editor, Path.parent(op.path))
290
+ const block = blockEntry[0]
291
+
292
+ if (node && isPortableTextSpan(node) && block && isPortableTextBlock(block)) {
293
+ const markDefs = block.markDefs ?? []
294
+ const nodeHasAnnotations = (node.marks ?? []).some((mark) =>
295
+ markDefs.find((markDef) => markDef._key === mark),
296
+ )
297
+ const deletingPartOfTheNode = op.offset !== 0
298
+ const deletingFromTheEnd = op.offset + op.text.length === node.text.length
299
+
300
+ if (nodeHasAnnotations && deletingPartOfTheNode && deletingFromTheEnd) {
301
+ /**
302
+ * If all of these conditions match then override the ordinary
303
+ * `remove_text` operation and turn it into `split_nodes` followed
304
+ * by `remove_nodes`. This is so if the operation can be properly
305
+ * undone. Undoing a `remove_text` results in an `insert_text` and
306
+ * we want to bail out of that in this exact scenario to make sure
307
+ * the inserted text is annotated. (See custom logic regarding
308
+ * `insert_text`)
309
+ */
310
+ Editor.withoutNormalizing(editor, () => {
311
+ withoutPreserveKeys(editor, () => {
312
+ Transforms.splitNodes(editor, {
313
+ match: Text.isText,
314
+ at: {path: op.path, offset: op.offset},
315
+ })
316
+ })
317
+ Transforms.removeNodes(editor, {at: Path.next(op.path)})
318
+ })
319
+
320
+ editor.onChange()
321
+ return
322
+ }
323
+ }
324
+ }
325
+
276
326
  apply(op)
277
327
  }
278
328
 
@@ -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
  })
@@ -101,6 +101,7 @@ Array [
101
101
  ],
102
102
  {schemaTypes},
103
103
  )
104
+
104
105
  expect(result).toMatchInlineSnapshot(`
105
106
  Array [
106
107
  Object {
@@ -9,6 +9,13 @@ export function withPreserveKeys(editor: Editor, fn: () => void): void {
9
9
  PRESERVE_KEYS.set(editor, prev)
10
10
  }
11
11
 
12
+ export function withoutPreserveKeys(editor: Editor, fn: () => void): void {
13
+ const prev = isPreservingKeys(editor)
14
+ PRESERVE_KEYS.set(editor, false)
15
+ fn()
16
+ PRESERVE_KEYS.set(editor, prev)
17
+ }
18
+
12
19
  export function isPreservingKeys(editor: Editor): boolean | undefined {
13
20
  return PRESERVE_KEYS.get(editor)
14
21
  }