@portabletext/editor 1.44.13 → 1.44.14

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.44.13",
3
+ "version": "1.44.14",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -0,0 +1,133 @@
1
+ import type {Path} from '@sanity/types'
2
+ import {Editor, Node, Range, Text, Transforms} from 'slate'
3
+ import type {BehaviorActionImplementation} from './behavior.actions'
4
+
5
+ /**
6
+ * @public
7
+ */
8
+ export type AddedAnnotationPaths = {
9
+ /**
10
+ * @deprecated An annotation may be applied to multiple blocks, resulting
11
+ * in multiple `markDef`'s being created. Use `markDefPaths` instead.
12
+ */
13
+ markDefPath: Path
14
+ markDefPaths: Array<Path>
15
+ /**
16
+ * @deprecated Does not return anything meaningful since an annotation
17
+ * can span multiple blocks and spans. If references the span closest
18
+ * to the focus point of the selection.
19
+ */
20
+ spanPath: Path
21
+ }
22
+
23
+ export const addAnnotationActionImplementation: BehaviorActionImplementation<
24
+ 'annotation.add',
25
+ AddedAnnotationPaths | undefined
26
+ > = ({context, action}) => {
27
+ const editor = action.editor
28
+
29
+ if (!editor.selection || Range.isCollapsed(editor.selection)) {
30
+ return
31
+ }
32
+
33
+ let paths: AddedAnnotationPaths | undefined = undefined
34
+ let spanPath: Path | undefined
35
+ let markDefPath: Path | undefined
36
+ const markDefPaths: Path[] = []
37
+
38
+ const selectedBlocks = Editor.nodes(editor, {
39
+ at: editor.selection,
40
+ match: (node) => editor.isTextBlock(node),
41
+ reverse: Range.isBackward(editor.selection),
42
+ })
43
+
44
+ for (const [block, blockPath] of selectedBlocks) {
45
+ if (block.children.length === 0) {
46
+ continue
47
+ }
48
+
49
+ if (block.children.length === 1 && block.children[0].text === '') {
50
+ continue
51
+ }
52
+
53
+ const annotationKey = context.keyGenerator()
54
+ const markDefs = block.markDefs ?? []
55
+ const existingMarkDef = markDefs.find(
56
+ (markDef) =>
57
+ markDef._type === action.annotation.name &&
58
+ markDef._key === annotationKey,
59
+ )
60
+
61
+ if (existingMarkDef === undefined) {
62
+ Transforms.setNodes(
63
+ editor,
64
+ {
65
+ markDefs: [
66
+ ...markDefs,
67
+ {
68
+ _type: action.annotation.name,
69
+ _key: annotationKey,
70
+ ...action.annotation.value,
71
+ },
72
+ ],
73
+ },
74
+ {at: blockPath},
75
+ )
76
+
77
+ markDefPath = [{_key: block._key}, 'markDefs', {_key: annotationKey}]
78
+
79
+ if (Range.isBackward(editor.selection)) {
80
+ markDefPaths.unshift(markDefPath)
81
+ } else {
82
+ markDefPaths.push(markDefPath)
83
+ }
84
+ }
85
+
86
+ Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
87
+
88
+ const children = Node.children(editor, blockPath)
89
+
90
+ for (const [span, path] of children) {
91
+ if (!editor.isTextSpan(span)) {
92
+ continue
93
+ }
94
+
95
+ if (!Range.includes(editor.selection, path)) {
96
+ continue
97
+ }
98
+
99
+ const marks = span.marks ?? []
100
+ const existingSameTypeAnnotations = marks.filter((mark) =>
101
+ markDefs.some(
102
+ (markDef) =>
103
+ markDef._key === mark && markDef._type === action.annotation.name,
104
+ ),
105
+ )
106
+
107
+ Transforms.setNodes(
108
+ editor,
109
+ {
110
+ marks: [
111
+ ...marks.filter(
112
+ (mark) => !existingSameTypeAnnotations.includes(mark),
113
+ ),
114
+ annotationKey,
115
+ ],
116
+ },
117
+ {at: path},
118
+ )
119
+
120
+ spanPath = [{_key: block._key}, 'children', {_key: span._key}]
121
+ }
122
+ }
123
+
124
+ if (markDefPath && spanPath) {
125
+ paths = {
126
+ markDefPath,
127
+ markDefPaths,
128
+ spanPath,
129
+ }
130
+ }
131
+
132
+ return paths
133
+ }
@@ -0,0 +1,150 @@
1
+ import type {PortableTextSpan} from '@sanity/types'
2
+ import {Editor, Node, Path, Range, Transforms} from 'slate'
3
+ import type {BehaviorActionImplementation} from './behavior.actions'
4
+
5
+ export const removeAnnotationActionImplementation: BehaviorActionImplementation<
6
+ 'annotation.remove'
7
+ > = ({action}) => {
8
+ const editor = action.editor
9
+
10
+ if (!editor.selection) {
11
+ return
12
+ }
13
+
14
+ if (Range.isCollapsed(editor.selection)) {
15
+ const [block, blockPath] = Editor.node(editor, editor.selection, {
16
+ depth: 1,
17
+ })
18
+
19
+ if (!editor.isTextBlock(block)) {
20
+ return
21
+ }
22
+
23
+ const markDefs = block.markDefs ?? []
24
+ const potentialAnnotations = markDefs.filter(
25
+ (markDef) => markDef._type === action.annotation.name,
26
+ )
27
+
28
+ const [selectedChild, selectedChildPath] = Editor.node(
29
+ editor,
30
+ editor.selection,
31
+ {
32
+ depth: 2,
33
+ },
34
+ )
35
+
36
+ if (!editor.isTextSpan(selectedChild)) {
37
+ return
38
+ }
39
+
40
+ const annotationToRemove = selectedChild.marks?.find((mark) =>
41
+ potentialAnnotations.some((markDef) => markDef._key === mark),
42
+ )
43
+
44
+ if (!annotationToRemove) {
45
+ return
46
+ }
47
+
48
+ const previousSpansWithSameAnnotation: Array<
49
+ [span: PortableTextSpan, path: Path]
50
+ > = []
51
+
52
+ for (const [child, childPath] of Node.children(editor, blockPath, {
53
+ reverse: true,
54
+ })) {
55
+ if (!editor.isTextSpan(child)) {
56
+ continue
57
+ }
58
+
59
+ if (!Path.isBefore(childPath, selectedChildPath)) {
60
+ continue
61
+ }
62
+
63
+ if (child.marks?.includes(annotationToRemove)) {
64
+ previousSpansWithSameAnnotation.push([child, childPath])
65
+ } else {
66
+ break
67
+ }
68
+ }
69
+
70
+ const nextSpansWithSameAnnotation: Array<
71
+ [span: PortableTextSpan, path: Path]
72
+ > = []
73
+
74
+ for (const [child, childPath] of Node.children(editor, blockPath)) {
75
+ if (!editor.isTextSpan(child)) {
76
+ continue
77
+ }
78
+
79
+ if (!Path.isAfter(childPath, selectedChildPath)) {
80
+ continue
81
+ }
82
+
83
+ if (child.marks?.includes(annotationToRemove)) {
84
+ nextSpansWithSameAnnotation.push([child, childPath])
85
+ } else {
86
+ break
87
+ }
88
+ }
89
+
90
+ for (const [child, childPath] of [
91
+ ...previousSpansWithSameAnnotation,
92
+ [selectedChild, selectedChildPath] as const,
93
+ ...nextSpansWithSameAnnotation,
94
+ ]) {
95
+ Transforms.setNodes(
96
+ editor,
97
+ {
98
+ marks: child.marks?.filter((mark) => mark !== annotationToRemove),
99
+ },
100
+ {at: childPath},
101
+ )
102
+ }
103
+ } else {
104
+ Transforms.setNodes(
105
+ editor,
106
+ {},
107
+ {
108
+ match: (node) => editor.isTextSpan(node),
109
+ split: true,
110
+ hanging: true,
111
+ },
112
+ )
113
+
114
+ const blocks = Editor.nodes(editor, {
115
+ at: editor.selection,
116
+ match: (node) => editor.isTextBlock(node),
117
+ })
118
+
119
+ for (const [block, blockPath] of blocks) {
120
+ const children = Node.children(editor, blockPath)
121
+
122
+ for (const [child, childPath] of children) {
123
+ if (!editor.isTextSpan(child)) {
124
+ continue
125
+ }
126
+
127
+ if (!Range.includes(editor.selection, childPath)) {
128
+ continue
129
+ }
130
+
131
+ const markDefs = block.markDefs ?? []
132
+ const marks = child.marks ?? []
133
+ const marksWithoutAnnotation = marks.filter((mark) => {
134
+ const markDef = markDefs.find((markDef) => markDef._key === mark)
135
+ return markDef?._type !== action.annotation.name
136
+ })
137
+
138
+ if (marksWithoutAnnotation.length !== marks.length) {
139
+ Transforms.setNodes(
140
+ editor,
141
+ {
142
+ marks: marksWithoutAnnotation,
143
+ },
144
+ {at: childPath},
145
+ )
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
@@ -1,10 +1,6 @@
1
1
  import {omit} from 'lodash'
2
2
  import type {InternalBehaviorAction} from '../behaviors/behavior.types.action'
3
3
  import type {EditorContext} from '../editor/editor-snapshot'
4
- import {
5
- addAnnotationActionImplementation,
6
- removeAnnotationActionImplementation,
7
- } from '../editor/plugins/createWithEditableAPI'
8
4
  import {removeDecoratorActionImplementation} from '../editor/plugins/createWithPortableTextMarkModel'
9
5
  import {
10
6
  historyRedoActionImplementation,
@@ -12,6 +8,8 @@ import {
12
8
  } from '../editor/plugins/createWithUndoRedo'
13
9
  import {debugWithName} from '../internal-utils/debug'
14
10
  import type {PickFromUnion} from '../type-utils'
11
+ import {addAnnotationActionImplementation} from './behavior.action.annotation.add'
12
+ import {removeAnnotationActionImplementation} from './behavior.action.annotation.remove'
15
13
  import {blockSetBehaviorActionImplementation} from './behavior.action.block.set'
16
14
  import {blockUnsetBehaviorActionImplementation} from './behavior.action.block.unset'
17
15
  import {blurActionImplementation} from './behavior.action.blur'
@@ -1,6 +1,7 @@
1
1
  import {htmlToBlocks} from '@portabletext/block-tools'
2
2
  import {toHTML} from '@portabletext/to-html'
3
3
  import type {PortableTextBlock} from '@sanity/types'
4
+ import {parseBlock} from '../internal-utils/parse-blocks'
4
5
  import {sliceBlocks} from '../utils'
5
6
  import {defineConverter} from './converter.types'
6
7
 
@@ -59,7 +60,18 @@ export const converterTextHtml = defineConverter({
59
60
  },
60
61
  ) as Array<PortableTextBlock>
61
62
 
62
- if (blocks.length === 0) {
63
+ const parsedBlocks = blocks.flatMap((block) => {
64
+ const parsedBlock = parseBlock({
65
+ context: snapshot.context,
66
+ block,
67
+ options: {
68
+ refreshKeys: false,
69
+ },
70
+ })
71
+ return parsedBlock ? [parsedBlock] : []
72
+ })
73
+
74
+ if (parsedBlocks.length === 0) {
63
75
  return {
64
76
  type: 'deserialization.failure',
65
77
  mimeType: 'text/html',
@@ -69,7 +81,7 @@ export const converterTextHtml = defineConverter({
69
81
 
70
82
  return {
71
83
  type: 'deserialization.success',
72
- data: blocks,
84
+ data: parsedBlocks,
73
85
  mimeType: 'text/html',
74
86
  }
75
87
  },
@@ -1,5 +1,6 @@
1
1
  import {htmlToBlocks} from '@portabletext/block-tools'
2
2
  import {isPortableTextTextBlock, type PortableTextBlock} from '@sanity/types'
3
+ import {parseBlock} from '../internal-utils/parse-blocks'
3
4
  import {sliceBlocks} from '../utils'
4
5
  import {defineConverter} from './converter.types'
5
6
 
@@ -80,7 +81,18 @@ export const converterTextPlain = defineConverter({
80
81
  },
81
82
  ) as Array<PortableTextBlock>
82
83
 
83
- if (blocks.length === 0) {
84
+ const parsedBlocks = blocks.flatMap((block) => {
85
+ const parsedBlock = parseBlock({
86
+ context: snapshot.context,
87
+ block,
88
+ options: {
89
+ refreshKeys: false,
90
+ },
91
+ })
92
+ return parsedBlock ? [parsedBlock] : []
93
+ })
94
+
95
+ if (parsedBlocks.length === 0) {
84
96
  return {
85
97
  type: 'deserialization.failure',
86
98
  mimeType: 'text/plain',
@@ -90,7 +102,7 @@ export const converterTextPlain = defineConverter({
90
102
 
91
103
  return {
92
104
  type: 'deserialization.success',
93
- data: blocks,
105
+ data: parsedBlocks,
94
106
  mimeType: 'text/plain',
95
107
  }
96
108
  },
@@ -15,6 +15,7 @@ import {
15
15
  import {Subject} from 'rxjs'
16
16
  import {Slate} from 'slate-react'
17
17
  import {useEffectEvent} from 'use-effect-event'
18
+ import type {AddedAnnotationPaths} from '../behavior-actions/behavior.action.annotation.add'
18
19
  import {debugWithName} from '../internal-utils/debug'
19
20
  import {compileType} from '../internal-utils/schema'
20
21
  import type {
@@ -34,7 +35,6 @@ import type {EditorActor} from './editor-machine'
34
35
  import {PortableTextEditorContext} from './hooks/usePortableTextEditor'
35
36
  import {PortableTextEditorSelectionProvider} from './hooks/usePortableTextEditorSelection'
36
37
  import {defaultKeyGenerator} from './key-generator'
37
- import type {AddedAnnotationPaths} from './plugins/createWithEditableAPI'
38
38
 
39
39
  const debug = debugWithName('component:PortableTextEditor')
40
40
 
@@ -4,7 +4,6 @@ import {
4
4
  type PortableTextBlock,
5
5
  type PortableTextChild,
6
6
  type PortableTextObject,
7
- type PortableTextSpan,
8
7
  type PortableTextTextBlock,
9
8
  } from '@sanity/types'
10
9
  import {
@@ -12,13 +11,12 @@ import {
12
11
  Node,
13
12
  Range,
14
13
  Element as SlateElement,
15
- Path as SlatePath,
16
14
  Text,
17
15
  Transforms,
18
16
  } from 'slate'
19
17
  import type {DOMNode} from 'slate-dom'
20
18
  import {ReactEditor} from 'slate-react'
21
- import type {BehaviorActionImplementation} from '../../behavior-actions/behavior.actions'
19
+ import {addAnnotationActionImplementation} from '../../behavior-actions/behavior.action.annotation.add'
22
20
  import {debugWithName} from '../../internal-utils/debug'
23
21
  import {toSlateRange} from '../../internal-utils/ranges'
24
22
  import {
@@ -608,282 +606,3 @@ function isAnnotationActive({
608
606
  return false
609
607
  }
610
608
  }
611
-
612
- /**
613
- * @public
614
- */
615
- export type AddedAnnotationPaths = {
616
- /**
617
- * @deprecated An annotation may be applied to multiple blocks, resulting
618
- * in multiple `markDef`'s being created. Use `markDefPaths` instead.
619
- */
620
- markDefPath: Path
621
- markDefPaths: Array<Path>
622
- /**
623
- * @deprecated Does not return anything meaningful since an annotation
624
- * can span multiple blocks and spans. If references the span closest
625
- * to the focus point of the selection.
626
- */
627
- spanPath: Path
628
- }
629
-
630
- export const addAnnotationActionImplementation: BehaviorActionImplementation<
631
- 'annotation.add',
632
- AddedAnnotationPaths | undefined
633
- > = ({context, action}) => {
634
- const editor = action.editor
635
-
636
- if (!editor.selection || Range.isCollapsed(editor.selection)) {
637
- return
638
- }
639
-
640
- let paths: AddedAnnotationPaths | undefined = undefined
641
- let spanPath: Path | undefined
642
- let markDefPath: Path | undefined
643
- const markDefPaths: Path[] = []
644
-
645
- const selectedBlocks = Editor.nodes(editor, {
646
- at: editor.selection,
647
- match: (node) => editor.isTextBlock(node),
648
- reverse: Range.isBackward(editor.selection),
649
- })
650
-
651
- for (const [block, blockPath] of selectedBlocks) {
652
- if (block.children.length === 0) {
653
- continue
654
- }
655
-
656
- if (block.children.length === 1 && block.children[0].text === '') {
657
- continue
658
- }
659
-
660
- const annotationKey = context.keyGenerator()
661
- const markDefs = block.markDefs ?? []
662
- const existingMarkDef = markDefs.find(
663
- (markDef) =>
664
- markDef._type === action.annotation.name &&
665
- markDef._key === annotationKey,
666
- )
667
-
668
- if (existingMarkDef === undefined) {
669
- Transforms.setNodes(
670
- editor,
671
- {
672
- markDefs: [
673
- ...markDefs,
674
- {
675
- _type: action.annotation.name,
676
- _key: annotationKey,
677
- ...action.annotation.value,
678
- },
679
- ],
680
- },
681
- {at: blockPath},
682
- )
683
-
684
- markDefPath = [{_key: block._key}, 'markDefs', {_key: annotationKey}]
685
-
686
- if (Range.isBackward(editor.selection)) {
687
- markDefPaths.unshift(markDefPath)
688
- } else {
689
- markDefPaths.push(markDefPath)
690
- }
691
- }
692
-
693
- Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
694
-
695
- const children = Node.children(editor, blockPath)
696
-
697
- for (const [span, path] of children) {
698
- if (!editor.isTextSpan(span)) {
699
- continue
700
- }
701
-
702
- if (!Range.includes(editor.selection, path)) {
703
- continue
704
- }
705
-
706
- const marks = span.marks ?? []
707
- const existingSameTypeAnnotations = marks.filter((mark) =>
708
- markDefs.some(
709
- (markDef) =>
710
- markDef._key === mark && markDef._type === action.annotation.name,
711
- ),
712
- )
713
-
714
- Transforms.setNodes(
715
- editor,
716
- {
717
- marks: [
718
- ...marks.filter(
719
- (mark) => !existingSameTypeAnnotations.includes(mark),
720
- ),
721
- annotationKey,
722
- ],
723
- },
724
- {at: path},
725
- )
726
-
727
- spanPath = [{_key: block._key}, 'children', {_key: span._key}]
728
- }
729
- }
730
-
731
- if (markDefPath && spanPath) {
732
- paths = {
733
- markDefPath,
734
- markDefPaths,
735
- spanPath,
736
- }
737
- }
738
-
739
- return paths
740
- }
741
-
742
- export const removeAnnotationActionImplementation: BehaviorActionImplementation<
743
- 'annotation.remove'
744
- > = ({action}) => {
745
- const editor = action.editor
746
-
747
- debug('Removing annotation', action.annotation.name)
748
-
749
- if (!editor.selection) {
750
- return
751
- }
752
-
753
- if (Range.isCollapsed(editor.selection)) {
754
- const [block, blockPath] = Editor.node(editor, editor.selection, {
755
- depth: 1,
756
- })
757
-
758
- if (!editor.isTextBlock(block)) {
759
- return
760
- }
761
-
762
- const markDefs = block.markDefs ?? []
763
- const potentialAnnotations = markDefs.filter(
764
- (markDef) => markDef._type === action.annotation.name,
765
- )
766
-
767
- const [selectedChild, selectedChildPath] = Editor.node(
768
- editor,
769
- editor.selection,
770
- {
771
- depth: 2,
772
- },
773
- )
774
-
775
- if (!editor.isTextSpan(selectedChild)) {
776
- return
777
- }
778
-
779
- const annotationToRemove = selectedChild.marks?.find((mark) =>
780
- potentialAnnotations.some((markDef) => markDef._key === mark),
781
- )
782
-
783
- if (!annotationToRemove) {
784
- return
785
- }
786
-
787
- const previousSpansWithSameAnnotation: Array<
788
- [span: PortableTextSpan, path: SlatePath]
789
- > = []
790
-
791
- for (const [child, childPath] of Node.children(editor, blockPath, {
792
- reverse: true,
793
- })) {
794
- if (!editor.isTextSpan(child)) {
795
- continue
796
- }
797
-
798
- if (!SlatePath.isBefore(childPath, selectedChildPath)) {
799
- continue
800
- }
801
-
802
- if (child.marks?.includes(annotationToRemove)) {
803
- previousSpansWithSameAnnotation.push([child, childPath])
804
- } else {
805
- break
806
- }
807
- }
808
-
809
- const nextSpansWithSameAnnotation: Array<
810
- [span: PortableTextSpan, path: SlatePath]
811
- > = []
812
-
813
- for (const [child, childPath] of Node.children(editor, blockPath)) {
814
- if (!editor.isTextSpan(child)) {
815
- continue
816
- }
817
-
818
- if (!SlatePath.isAfter(childPath, selectedChildPath)) {
819
- continue
820
- }
821
-
822
- if (child.marks?.includes(annotationToRemove)) {
823
- nextSpansWithSameAnnotation.push([child, childPath])
824
- } else {
825
- break
826
- }
827
- }
828
-
829
- for (const [child, childPath] of [
830
- ...previousSpansWithSameAnnotation,
831
- [selectedChild, selectedChildPath] as const,
832
- ...nextSpansWithSameAnnotation,
833
- ]) {
834
- Transforms.setNodes(
835
- editor,
836
- {
837
- marks: child.marks?.filter((mark) => mark !== annotationToRemove),
838
- },
839
- {at: childPath},
840
- )
841
- }
842
- } else {
843
- Transforms.setNodes(
844
- editor,
845
- {},
846
- {
847
- match: (node) => editor.isTextSpan(node),
848
- split: true,
849
- hanging: true,
850
- },
851
- )
852
-
853
- const blocks = Editor.nodes(editor, {
854
- at: editor.selection,
855
- match: (node) => editor.isTextBlock(node),
856
- })
857
-
858
- for (const [block, blockPath] of blocks) {
859
- const children = Node.children(editor, blockPath)
860
-
861
- for (const [child, childPath] of children) {
862
- if (!editor.isTextSpan(child)) {
863
- continue
864
- }
865
-
866
- if (!Range.includes(editor.selection, childPath)) {
867
- continue
868
- }
869
-
870
- const markDefs = block.markDefs ?? []
871
- const marks = child.marks ?? []
872
- const marksWithoutAnnotation = marks.filter((mark) => {
873
- const markDef = markDefs.find((markDef) => markDef._key === mark)
874
- return markDef?._type !== action.annotation.name
875
- })
876
-
877
- if (marksWithoutAnnotation.length !== marks.length) {
878
- Transforms.setNodes(
879
- editor,
880
- {
881
- marks: marksWithoutAnnotation,
882
- },
883
- {at: childPath},
884
- )
885
- }
886
- }
887
- }
888
- }
889
- }