@portabletext/editor 1.44.13 → 1.44.15

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.15",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -91,7 +91,7 @@
91
91
  "@testing-library/jest-dom": "^6.6.3",
92
92
  "@testing-library/react": "^16.3.0",
93
93
  "@types/debug": "^4.1.12",
94
- "@types/lodash": "^4.17.13",
94
+ "@types/lodash": "^4.17.16",
95
95
  "@types/lodash.startcase": "^4.4.9",
96
96
  "@types/react": "^19.1.0",
97
97
  "@types/react-dom": "^19.1.1",
@@ -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,5 +1,7 @@
1
+ import type {ConverterEvent} from '../converters/converter.types'
1
2
  import {isTextBlock} from '../internal-utils/parse-blocks'
2
3
  import * as selectors from '../selectors'
4
+ import type {PickFromUnion} from '../type-utils'
3
5
  import {getTextBlockText} from '../utils'
4
6
  import {abstractAnnotationBehaviors} from './behavior.abstract.annotation'
5
7
  import {abstractDecoratorBehaviors} from './behavior.abstract.decorator'
@@ -15,44 +17,44 @@ import {defineBehavior} from './behavior.types.behavior'
15
17
  const raiseDeserializationSuccessOrFailure = defineBehavior({
16
18
  on: 'deserialize',
17
19
  guard: ({snapshot, event}) => {
18
- const deserializeEvents = snapshot.context.converters.flatMap(
19
- (converter) => {
20
- const data = event.originEvent.originEvent.dataTransfer.getData(
21
- converter.mimeType,
22
- )
20
+ let success:
21
+ | PickFromUnion<ConverterEvent, 'type', 'deserialization.success'>
22
+ | undefined
23
+ const failures: Array<
24
+ PickFromUnion<ConverterEvent, 'type', 'deserialization.failure'>
25
+ > = []
23
26
 
24
- if (!data) {
25
- return []
26
- }
27
+ for (const converter of snapshot.context.converters) {
28
+ const data = event.originEvent.originEvent.dataTransfer.getData(
29
+ converter.mimeType,
30
+ )
27
31
 
28
- return [
29
- converter.deserialize({
30
- snapshot,
31
- event: {type: 'deserialize', data},
32
- }),
33
- ]
34
- },
35
- )
32
+ if (!data) {
33
+ continue
34
+ }
36
35
 
37
- const firstSuccess = deserializeEvents.find(
38
- (deserializeEvent) => deserializeEvent.type === 'deserialization.success',
39
- )
36
+ const deserializeEvent = converter.deserialize({
37
+ snapshot,
38
+ event: {type: 'deserialize', data},
39
+ })
40
+
41
+ if (deserializeEvent.type === 'deserialization.success') {
42
+ success = deserializeEvent
43
+ break
44
+ } else {
45
+ failures.push(deserializeEvent)
46
+ }
47
+ }
40
48
 
41
- if (!firstSuccess) {
49
+ if (!success) {
42
50
  return {
43
51
  type: 'deserialization.failure',
44
52
  mimeType: '*/*',
45
- reason: deserializeEvents
46
- .map((deserializeEvent) =>
47
- deserializeEvent.type === 'deserialization.failure'
48
- ? deserializeEvent.reason
49
- : '',
50
- )
51
- .join(', '),
53
+ reason: failures.map((failure) => failure.reason).join(', '),
52
54
  } as const
53
55
  }
54
56
 
55
- return firstSuccess
57
+ return success
56
58
  },
57
59
  actions: [
58
60
  ({event}, deserializeEvent) => [
@@ -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