@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "3.2.5",
3
+ "version": "3.3.0",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -65,7 +65,6 @@
65
65
  "@portabletext/to-html": "^4.0.1",
66
66
  "@xstate/react": "^6.0.0",
67
67
  "debug": "^4.4.3",
68
- "immer": "^11.0.1",
69
68
  "lodash": "^4.17.21",
70
69
  "lodash.startcase": "^4.4.0",
71
70
  "react-compiler-runtime": "1.0.0",
@@ -75,6 +74,7 @@
75
74
  "xstate": "^5.24.0",
76
75
  "@portabletext/block-tools": "^4.1.7",
77
76
  "@portabletext/keyboard-shortcuts": "^2.1.0",
77
+ "@portabletext/markdown": "^1.0.3",
78
78
  "@portabletext/patches": "^2.0.0",
79
79
  "@portabletext/schema": "^2.0.0"
80
80
  },
@@ -107,8 +107,8 @@
107
107
  "vitest": "^4.0.14",
108
108
  "vitest-browser-react": "^2.0.2",
109
109
  "@portabletext/sanity-bridge": "1.2.10",
110
- "racejar": "2.0.0",
111
- "@portabletext/test": "^1.0.0"
110
+ "@portabletext/test": "^1.0.0",
111
+ "racejar": "2.0.0"
112
112
  },
113
113
  "peerDependencies": {
114
114
  "@portabletext/sanity-bridge": "^1.2.10",
@@ -1,4 +1,5 @@
1
1
  import {isTextBlock} from '@portabletext/schema'
2
+ import type {MIMEType} from '../internal-utils/mime-type'
2
3
  import {getActiveAnnotations} from '../selectors/selector.get-active-annotations'
3
4
  import {getActiveDecorators} from '../selectors/selector.get-active-decorators'
4
5
  import {getFocusTextBlock} from '../selectors/selector.get-focus-text-block'
@@ -6,60 +7,67 @@ import {getTextBlockText} from '../utils/util.get-text-block-text'
6
7
  import {raise} from './behavior.types.action'
7
8
  import {defineBehavior} from './behavior.types.behavior'
8
9
 
10
+ const mimeTypePriority: Array<MIMEType> = [
11
+ 'application/x-portable-text',
12
+ 'application/json',
13
+ 'text/markdown',
14
+ 'text/html',
15
+ 'text/plain',
16
+ ]
17
+
18
+ /**
19
+ * Finds the first available data from the dataTransfer based on priority.
20
+ * Optionally starts from a specific mime type in the priority list.
21
+ */
22
+ function getFirstAvailableData({
23
+ dataTransfer,
24
+ startAfter,
25
+ }: {
26
+ dataTransfer: DataTransfer
27
+ startAfter?: MIMEType
28
+ }): {mimeType: MIMEType; data: string} | undefined {
29
+ const startIndex = startAfter ? mimeTypePriority.indexOf(startAfter) + 1 : 0
30
+
31
+ for (let index = startIndex; index < mimeTypePriority.length; index++) {
32
+ const mimeType = mimeTypePriority.at(index)
33
+
34
+ if (!mimeType) {
35
+ continue
36
+ }
37
+
38
+ const data = dataTransfer.getData(mimeType)
39
+
40
+ if (!data) {
41
+ continue
42
+ }
43
+
44
+ return {
45
+ mimeType,
46
+ data,
47
+ }
48
+ }
49
+
50
+ return undefined
51
+ }
52
+
9
53
  export const abstractDeserializeBehaviors = [
10
54
  defineBehavior({
11
55
  on: 'deserialize',
12
56
  guard: ({event}) => {
13
- const portableText = event.originEvent.originEvent.dataTransfer.getData(
14
- 'application/x-portable-text',
15
- )
16
-
17
- if (portableText) {
18
- return {
19
- type: 'deserialize.data',
20
- mimeType: 'application/x-portable-text',
21
- data: portableText,
22
- originEvent: event.originEvent,
23
- } as const
24
- }
25
-
26
- const json =
27
- event.originEvent.originEvent.dataTransfer.getData('application/json')
28
-
29
- if (json) {
30
- return {
31
- type: 'deserialize.data',
32
- mimeType: 'application/json',
33
- data: json,
34
- originEvent: event.originEvent,
35
- } as const
36
- }
37
-
38
- const html =
39
- event.originEvent.originEvent.dataTransfer.getData('text/html')
57
+ const availableData = getFirstAvailableData({
58
+ dataTransfer: event.originEvent.originEvent.dataTransfer,
59
+ })
40
60
 
41
- if (html) {
42
- return {
43
- type: 'deserialize.data',
44
- mimeType: 'text/html',
45
- data: html,
46
- originEvent: event.originEvent,
47
- } as const
61
+ if (!availableData) {
62
+ return false
48
63
  }
49
64
 
50
- const text =
51
- event.originEvent.originEvent.dataTransfer.getData('text/plain')
52
-
53
- if (text) {
54
- return {
55
- type: 'deserialize.data',
56
- mimeType: 'text/plain',
57
- data: text,
58
- originEvent: event.originEvent,
59
- } as const
65
+ return {
66
+ type: 'deserialize.data' as const,
67
+ mimeType: availableData.mimeType,
68
+ data: availableData.data,
69
+ originEvent: event.originEvent,
60
70
  }
61
-
62
- return false
63
71
  },
64
72
  actions: [(_, deserializeEvent) => [raise(deserializeEvent)]],
65
73
  }),
@@ -184,49 +192,25 @@ export const abstractDeserializeBehaviors = [
184
192
  defineBehavior({
185
193
  on: 'deserialization.failure',
186
194
  guard: ({event}) => {
187
- if (event.mimeType === 'application/x-portable-text') {
188
- const json =
189
- event.originEvent.originEvent.dataTransfer.getData('application/json')
190
-
191
- if (json) {
192
- return {
193
- type: 'deserialize.data',
194
- mimeType: 'application/json',
195
- data: json,
196
- originEvent: event.originEvent,
197
- } as const
198
- }
195
+ if (event.mimeType === '*/*') {
196
+ return false
199
197
  }
200
198
 
201
- if (event.mimeType === 'application/json') {
202
- const html =
203
- event.originEvent.originEvent.dataTransfer.getData('text/html')
204
-
205
- if (html) {
206
- return {
207
- type: 'deserialize.data',
208
- mimeType: 'text/html',
209
- data: html,
210
- originEvent: event.originEvent,
211
- } as const
212
- }
213
- }
199
+ const availableData = getFirstAvailableData({
200
+ dataTransfer: event.originEvent.originEvent.dataTransfer,
201
+ startAfter: event.mimeType,
202
+ })
214
203
 
215
- if (event.mimeType === 'text/html') {
216
- const text =
217
- event.originEvent.originEvent.dataTransfer.getData('text/plain')
218
-
219
- if (text) {
220
- return {
221
- type: 'deserialize.data',
222
- mimeType: 'text/plain',
223
- data: text,
224
- originEvent: event.originEvent,
225
- } as const
226
- }
204
+ if (!availableData) {
205
+ return false
227
206
  }
228
207
 
229
- return false
208
+ return {
209
+ type: 'deserialize.data' as const,
210
+ mimeType: availableData.mimeType,
211
+ data: availableData.data,
212
+ originEvent: event.originEvent,
213
+ }
230
214
  },
231
215
  actions: [(_, deserializeDataEvent) => [raise(deserializeDataEvent)]],
232
216
  }),
@@ -0,0 +1,67 @@
1
+ import {
2
+ markdownToPortableText,
3
+ portableTextToMarkdown,
4
+ } from '@portabletext/markdown'
5
+ import {getSelectedValue} from '../selectors/selector.get-selected-value'
6
+ import {parseBlock} from '../utils/parse-blocks'
7
+ import {defineConverter} from './converter.types'
8
+
9
+ export const converterTextMarkdown = defineConverter({
10
+ mimeType: 'text/markdown',
11
+ serialize: ({snapshot, event}) => {
12
+ const selection = snapshot.context.selection
13
+
14
+ if (!selection) {
15
+ return {
16
+ type: 'serialization.failure',
17
+ mimeType: 'text/markdown',
18
+ reason: 'No selection',
19
+ originEvent: event.originEvent,
20
+ }
21
+ }
22
+
23
+ const blocks = getSelectedValue(snapshot)
24
+
25
+ const markdown = portableTextToMarkdown(blocks)
26
+
27
+ return {
28
+ type: 'serialization.success',
29
+ data: markdown,
30
+ mimeType: 'text/markdown',
31
+ originEvent: event.originEvent,
32
+ }
33
+ },
34
+ deserialize: ({snapshot, event}) => {
35
+ const blocks = markdownToPortableText(event.data, {
36
+ keyGenerator: snapshot.context.keyGenerator,
37
+ schema: snapshot.context.schema,
38
+ })
39
+
40
+ const parsedBlocks = blocks.flatMap((block) => {
41
+ const parsedBlock = parseBlock({
42
+ context: snapshot.context,
43
+ block,
44
+ options: {
45
+ normalize: false,
46
+ removeUnusedMarkDefs: true,
47
+ validateFields: false,
48
+ },
49
+ })
50
+ return parsedBlock ? [parsedBlock] : []
51
+ })
52
+
53
+ if (parsedBlocks.length === 0) {
54
+ return {
55
+ type: 'deserialization.failure',
56
+ mimeType: 'text/markdown',
57
+ reason: 'No blocks deserialized',
58
+ }
59
+ }
60
+
61
+ return {
62
+ type: 'deserialization.success',
63
+ data: parsedBlocks,
64
+ mimeType: 'text/markdown',
65
+ }
66
+ },
67
+ })
@@ -2,6 +2,7 @@ import type {PortableTextMemberSchemaTypes} from '../types/editor'
2
2
  import {converterJson} from './converter.json'
3
3
  import {converterPortableText} from './converter.portable-text'
4
4
  import {createConverterTextHtml} from './converter.text-html'
5
+ import {converterTextMarkdown} from './converter.text-markdown'
5
6
  import {createConverterTextPlain} from './converter.text-plain'
6
7
 
7
8
  export function createCoreConverters(
@@ -10,6 +11,7 @@ export function createCoreConverters(
10
11
  return [
11
12
  converterJson,
12
13
  converterPortableText,
14
+ converterTextMarkdown,
13
15
  createConverterTextHtml(legacySchema),
14
16
  createConverterTextPlain(legacySchema),
15
17
  ]