@portabletext/editor 1.22.0 → 1.24.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.
Files changed (53) hide show
  1. package/lib/_chunks-cjs/behavior.core.cjs +65 -2
  2. package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
  3. package/lib/_chunks-cjs/util.slice-blocks.cjs +26 -12
  4. package/lib/_chunks-cjs/util.slice-blocks.cjs.map +1 -1
  5. package/lib/_chunks-es/behavior.core.js +65 -2
  6. package/lib/_chunks-es/behavior.core.js.map +1 -1
  7. package/lib/_chunks-es/util.slice-blocks.js +26 -12
  8. package/lib/_chunks-es/util.slice-blocks.js.map +1 -1
  9. package/lib/behaviors/index.d.cts +1111 -44
  10. package/lib/behaviors/index.d.ts +1111 -44
  11. package/lib/index.cjs +542 -333
  12. package/lib/index.cjs.map +1 -1
  13. package/lib/index.d.cts +446 -1
  14. package/lib/index.d.ts +446 -1
  15. package/lib/index.js +546 -335
  16. package/lib/index.js.map +1 -1
  17. package/lib/selectors/index.d.cts +73 -0
  18. package/lib/selectors/index.d.ts +73 -0
  19. package/package.json +23 -18
  20. package/src/behavior-actions/behavior.action.data-transfer-set.ts +7 -0
  21. package/src/behavior-actions/behavior.action.insert-blocks.ts +61 -0
  22. package/src/behavior-actions/behavior.actions.ts +75 -0
  23. package/src/behaviors/behavior.core.deserialize.ts +46 -0
  24. package/src/behaviors/behavior.core.serialize.ts +44 -0
  25. package/src/behaviors/behavior.core.ts +7 -0
  26. package/src/behaviors/behavior.types.ts +39 -2
  27. package/src/converters/converter.json.ts +53 -0
  28. package/src/converters/converter.portable-text.deserialize.test.ts +686 -0
  29. package/src/converters/converter.portable-text.ts +59 -0
  30. package/src/converters/converter.text-html.deserialize.test.ts +349 -0
  31. package/src/converters/converter.text-html.serialize.test.ts +233 -0
  32. package/src/converters/converter.text-html.ts +61 -0
  33. package/src/converters/converter.text-plain.test.ts +241 -0
  34. package/src/converters/converter.text-plain.ts +91 -0
  35. package/src/converters/converter.ts +65 -0
  36. package/src/converters/converters.ts +11 -0
  37. package/src/editor/Editable.tsx +3 -13
  38. package/src/editor/create-editor.ts +3 -0
  39. package/src/editor/editor-machine.ts +25 -1
  40. package/src/editor/editor-selector.ts +1 -0
  41. package/src/editor/editor-snapshot.ts +5 -0
  42. package/src/editor/plugins/create-with-event-listeners.ts +44 -0
  43. package/src/internal-utils/asserters.ts +9 -0
  44. package/src/internal-utils/mime-type.ts +1 -0
  45. package/src/internal-utils/parse-blocks.ts +136 -0
  46. package/src/internal-utils/test-key-generator.ts +9 -0
  47. package/src/selectors/selector.get-selected-spans.test.ts +1 -0
  48. package/src/selectors/selector.get-selection-text.test.ts +1 -0
  49. package/src/selectors/selector.is-active-decorator.test.ts +1 -0
  50. package/src/utils/util.slice-blocks.test.ts +216 -35
  51. package/src/utils/util.slice-blocks.ts +37 -10
  52. package/src/editor/plugins/__tests__/createWithInsertData.test.tsx +0 -181
  53. package/src/editor/plugins/createWithInsertData.ts +0 -425
@@ -0,0 +1,59 @@
1
+ import {parseBlock} from '../internal-utils/parse-blocks'
2
+ import {sliceBlocks} from '../utils'
3
+ import type {Converter} from './converter'
4
+
5
+ export const converterPortableText: Converter<'application/x-portable-text'> = {
6
+ serialize: ({context, event}) => {
7
+ if (!context.selection) {
8
+ return {
9
+ type: 'serialization.failure',
10
+ mimeType: 'application/x-portable-text',
11
+ originEvent: event.originEvent,
12
+ reason: 'No selection',
13
+ }
14
+ }
15
+
16
+ const blocks = sliceBlocks({
17
+ blocks: context.value,
18
+ selection: context.selection,
19
+ })
20
+
21
+ return {
22
+ type: 'serialization.success',
23
+ data: JSON.stringify(blocks),
24
+ mimeType: 'application/x-portable-text',
25
+ originEvent: event.originEvent,
26
+ }
27
+ },
28
+ deserialize: ({context, event}) => {
29
+ const blocks = JSON.parse(event.data)
30
+
31
+ if (!Array.isArray(blocks)) {
32
+ return {
33
+ type: 'deserialization.failure',
34
+ mimeType: 'application/x-portable-text',
35
+ reason: 'Data is not an array',
36
+ }
37
+ }
38
+
39
+ const parsedBlocks = blocks.flatMap((block) => {
40
+ const parsedBlock = parseBlock({context, block})
41
+ return parsedBlock ? [parsedBlock] : []
42
+ })
43
+
44
+ if (parsedBlocks.length === 0 && blocks.length > 0) {
45
+ return {
46
+ type: 'deserialization.failure',
47
+ mimeType: 'application/x-portable-text',
48
+ reason: 'No blocks were parsed',
49
+ }
50
+ }
51
+
52
+ return {
53
+ type: 'deserialization.success',
54
+ data: parsedBlocks,
55
+ mimeType: 'application/x-portable-text',
56
+ }
57
+ },
58
+ mimeType: 'application/x-portable-text',
59
+ }
@@ -0,0 +1,349 @@
1
+ import {describe, expect, test} from 'vitest'
2
+ import {
3
+ compileSchemaDefinition,
4
+ defineSchema,
5
+ type SchemaDefinition,
6
+ } from '../editor/define-schema'
7
+ import {createTestKeyGenerator} from '../internal-utils/test-key-generator'
8
+ import {converterTextHtml} from './converter.text-html'
9
+ import {coreConverters} from './converters'
10
+
11
+ function createContext(schema: SchemaDefinition) {
12
+ return {
13
+ converters: coreConverters,
14
+ activeDecorators: [],
15
+ keyGenerator: createTestKeyGenerator(),
16
+ schema: compileSchemaDefinition(schema),
17
+ selection: null,
18
+ value: [],
19
+ }
20
+ }
21
+
22
+ const decoratedParagraph =
23
+ '<p><em>foo <code><strong>bar</strong> baz</code></em></p>'
24
+ const image = '<img src="https://exampe.com/image.jpg" alt="Example image">'
25
+ const paragraphWithLink = '<p>fizz <a href="https://example.com">buzz</a></p>'
26
+ const unorderedList = '<ul><li>foo</li><li>bar</li></ul>'
27
+ const orderedList = '<ol><li>foo</li><li>bar</li></ol>'
28
+ const nestedList = '<ol><li>foo<ul><li>bar</li></ul></li></ol>'
29
+
30
+ describe(converterTextHtml.deserialize.name, () => {
31
+ test('paragraph with unknown decorators', () => {
32
+ expect(
33
+ converterTextHtml.deserialize({
34
+ context: createContext(defineSchema({})),
35
+ event: {
36
+ type: 'deserialize',
37
+ data: decoratedParagraph,
38
+ },
39
+ }),
40
+ ).toMatchObject({
41
+ data: [
42
+ {
43
+ _key: 'k0',
44
+ _type: 'block',
45
+ children: [
46
+ {
47
+ _key: 'k1',
48
+ _type: 'span',
49
+ marks: [],
50
+ text: 'foo bar baz',
51
+ },
52
+ ],
53
+ markDefs: [],
54
+ style: 'normal',
55
+ },
56
+ ],
57
+ })
58
+ })
59
+
60
+ test('paragraph with known decorators', () => {
61
+ expect(
62
+ converterTextHtml.deserialize({
63
+ context: createContext(
64
+ defineSchema({
65
+ decorators: [{name: 'strong'}, {name: 'em'}, {name: 'code'}],
66
+ }),
67
+ ),
68
+ event: {
69
+ type: 'deserialize',
70
+ data: decoratedParagraph,
71
+ },
72
+ }),
73
+ ).toMatchObject({
74
+ data: [
75
+ {
76
+ _key: 'k0',
77
+ _type: 'block',
78
+ children: [
79
+ {
80
+ _key: 'k1',
81
+ _type: 'span',
82
+ marks: ['em'],
83
+ text: 'foo ',
84
+ },
85
+ {
86
+ _key: 'k2',
87
+ _type: 'span',
88
+ marks: ['em', 'code', 'strong'],
89
+ text: 'bar',
90
+ },
91
+ {
92
+ _key: 'k3',
93
+ _type: 'span',
94
+ marks: ['em', 'code'],
95
+ text: ' baz',
96
+ },
97
+ ],
98
+ markDefs: [],
99
+ style: 'normal',
100
+ },
101
+ ],
102
+ })
103
+ })
104
+
105
+ test('image', () => {
106
+ expect(
107
+ converterTextHtml.deserialize({
108
+ context: createContext(
109
+ defineSchema({
110
+ blockObjects: [{name: 'image'}],
111
+ }),
112
+ ),
113
+ event: {
114
+ type: 'deserialize',
115
+ data: image,
116
+ },
117
+ }),
118
+ ).toMatchObject({
119
+ data: [],
120
+ })
121
+ })
122
+
123
+ test('paragraph with unknown link', () => {
124
+ expect(
125
+ converterTextHtml.deserialize({
126
+ context: createContext(defineSchema({})),
127
+ event: {
128
+ type: 'deserialize',
129
+ data: paragraphWithLink,
130
+ },
131
+ }),
132
+ ).toMatchObject({
133
+ data: [
134
+ {
135
+ _key: 'k0',
136
+ _type: 'block',
137
+ children: [
138
+ {
139
+ _key: 'k1',
140
+ _type: 'span',
141
+ marks: [],
142
+ text: 'fizz buzz (https://example.com)',
143
+ },
144
+ ],
145
+ markDefs: [],
146
+ style: 'normal',
147
+ },
148
+ ],
149
+ })
150
+ })
151
+
152
+ test('paragraph with known link', () => {
153
+ expect(
154
+ converterTextHtml.deserialize({
155
+ context: createContext(
156
+ defineSchema({
157
+ annotations: [{name: 'link'}],
158
+ }),
159
+ ),
160
+ event: {
161
+ type: 'deserialize',
162
+ data: paragraphWithLink,
163
+ },
164
+ }),
165
+ ).toMatchObject({
166
+ data: [
167
+ {
168
+ _key: 'k1',
169
+ _type: 'block',
170
+ children: [
171
+ {
172
+ _key: 'k2',
173
+ _type: 'span',
174
+ marks: [],
175
+ text: 'fizz ',
176
+ },
177
+ {
178
+ _key: 'k3',
179
+ _type: 'span',
180
+ marks: ['k0'],
181
+ text: 'buzz',
182
+ },
183
+ ],
184
+ markDefs: [
185
+ {
186
+ _key: 'k0',
187
+ _type: 'link',
188
+ href: 'https://example.com',
189
+ },
190
+ ],
191
+ style: 'normal',
192
+ },
193
+ ],
194
+ })
195
+ })
196
+
197
+ test('unordered list', () => {
198
+ expect(
199
+ converterTextHtml.deserialize({
200
+ context: createContext(
201
+ defineSchema({
202
+ lists: [{name: 'bullet'}],
203
+ }),
204
+ ),
205
+ event: {
206
+ type: 'deserialize',
207
+ data: unorderedList,
208
+ },
209
+ }),
210
+ ).toMatchObject({
211
+ data: [
212
+ {
213
+ _key: 'k0',
214
+ _type: 'block',
215
+ children: [
216
+ {
217
+ _key: 'k1',
218
+ _type: 'span',
219
+ marks: [],
220
+ text: 'foo',
221
+ },
222
+ ],
223
+ level: 1,
224
+ listItem: 'bullet',
225
+ markDefs: [],
226
+ style: 'normal',
227
+ },
228
+ {
229
+ _key: 'k2',
230
+ _type: 'block',
231
+ children: [
232
+ {
233
+ _key: 'k3',
234
+ _type: 'span',
235
+ marks: [],
236
+ text: 'bar',
237
+ },
238
+ ],
239
+ level: 1,
240
+ listItem: 'bullet',
241
+ markDefs: [],
242
+ style: 'normal',
243
+ },
244
+ ],
245
+ })
246
+ })
247
+
248
+ test('ordered list', () => {
249
+ expect(
250
+ converterTextHtml.deserialize({
251
+ context: createContext(
252
+ defineSchema({
253
+ lists: [{name: 'number'}],
254
+ }),
255
+ ),
256
+ event: {
257
+ type: 'deserialize',
258
+ data: orderedList,
259
+ },
260
+ }),
261
+ ).toMatchObject({
262
+ data: [
263
+ {
264
+ _key: 'k0',
265
+ _type: 'block',
266
+ children: [
267
+ {
268
+ _key: 'k1',
269
+ _type: 'span',
270
+ marks: [],
271
+ text: 'foo',
272
+ },
273
+ ],
274
+ level: 1,
275
+ listItem: 'number',
276
+ markDefs: [],
277
+ style: 'normal',
278
+ },
279
+ {
280
+ _key: 'k2',
281
+ _type: 'block',
282
+ children: [
283
+ {
284
+ _key: 'k3',
285
+ _type: 'span',
286
+ marks: [],
287
+ text: 'bar',
288
+ },
289
+ ],
290
+ level: 1,
291
+ listItem: 'number',
292
+ markDefs: [],
293
+ style: 'normal',
294
+ },
295
+ ],
296
+ })
297
+ })
298
+
299
+ test('nested list', () => {
300
+ expect(
301
+ converterTextHtml.deserialize({
302
+ context: createContext(
303
+ defineSchema({
304
+ lists: [{name: 'bullet'}, {name: 'number'}],
305
+ }),
306
+ ),
307
+ event: {
308
+ type: 'deserialize',
309
+ data: nestedList,
310
+ },
311
+ }),
312
+ ).toMatchObject({
313
+ data: [
314
+ {
315
+ _key: 'k0',
316
+ _type: 'block',
317
+ children: [
318
+ {
319
+ _key: 'k1',
320
+ _type: 'span',
321
+ marks: [],
322
+ text: 'foo',
323
+ },
324
+ ],
325
+ level: 1,
326
+ listItem: 'number',
327
+ markDefs: [],
328
+ style: 'normal',
329
+ },
330
+ {
331
+ _key: 'k2',
332
+ _type: 'block',
333
+ children: [
334
+ {
335
+ _key: 'k3',
336
+ _type: 'span',
337
+ marks: [],
338
+ text: 'bar',
339
+ },
340
+ ],
341
+ level: 2,
342
+ listItem: 'bullet',
343
+ markDefs: [],
344
+ style: 'normal',
345
+ },
346
+ ],
347
+ })
348
+ })
349
+ })
@@ -0,0 +1,233 @@
1
+ import type {PortableTextBlock, PortableTextTextBlock} from '@sanity/types'
2
+ import {describe, expect, test} from 'vitest'
3
+ import {
4
+ compileSchemaDefinition,
5
+ defineSchema,
6
+ type SchemaDefinition,
7
+ } from '../editor/define-schema'
8
+ import {createTestKeyGenerator} from '../internal-utils/test-key-generator'
9
+ import type {EditorSelection} from '../utils'
10
+ import {converterTextHtml} from './converter.text-html'
11
+ import {coreConverters} from './converters'
12
+
13
+ const decoratedParagraph: PortableTextTextBlock = {
14
+ _key: 'k0',
15
+ _type: 'block',
16
+ children: [
17
+ {
18
+ _key: 'k1',
19
+ _type: 'span',
20
+ marks: ['em'],
21
+ text: 'foo ',
22
+ },
23
+ {
24
+ _key: 'k2',
25
+ _type: 'span',
26
+ marks: ['em', 'code', 'strong'],
27
+ text: 'bar',
28
+ },
29
+ {
30
+ _key: 'k3',
31
+ _type: 'span',
32
+ marks: ['em', 'code'],
33
+ text: ' baz',
34
+ },
35
+ ],
36
+ markDefs: [],
37
+ style: 'normal',
38
+ }
39
+ const image: PortableTextBlock = {
40
+ _type: 'image',
41
+ _key: 'b2',
42
+ src: 'https://example.com/image.jpg',
43
+ alt: 'Example',
44
+ }
45
+ const b2: PortableTextTextBlock = {
46
+ _type: 'block',
47
+ _key: 'b3',
48
+ children: [
49
+ {
50
+ _type: 'span',
51
+ _key: 'b3c1',
52
+ text: 'baz',
53
+ },
54
+ ],
55
+ }
56
+ const paragraphWithInlineBlock: PortableTextTextBlock = {
57
+ _type: 'block',
58
+ _key: 'b4',
59
+ children: [
60
+ {
61
+ _type: 'span',
62
+ _key: 'b4c1',
63
+ text: 'fizz ',
64
+ },
65
+ {
66
+ _type: 'stock-ticker',
67
+ _key: 'b4c2',
68
+ symbol: 'AAPL',
69
+ },
70
+ {
71
+ _type: 'span',
72
+ _key: 'b4c3',
73
+ text: ' buzz',
74
+ },
75
+ ],
76
+ }
77
+
78
+ function createContext(schema: SchemaDefinition, selection: EditorSelection) {
79
+ return {
80
+ converters: coreConverters,
81
+ activeDecorators: [],
82
+ keyGenerator: createTestKeyGenerator(),
83
+ schema: compileSchemaDefinition(schema),
84
+ selection,
85
+ value: [decoratedParagraph, image, b2, paragraphWithInlineBlock],
86
+ }
87
+ }
88
+
89
+ describe(converterTextHtml.serialize.name, () => {
90
+ test('paragraph with decorators', () => {
91
+ expect(
92
+ converterTextHtml.serialize({
93
+ context: createContext(defineSchema({}), {
94
+ anchor: {
95
+ path: [
96
+ {_key: decoratedParagraph._key},
97
+ 'children',
98
+ {_key: decoratedParagraph.children[0]._key},
99
+ ],
100
+ offset: 0,
101
+ },
102
+ focus: {
103
+ path: [
104
+ {_key: decoratedParagraph._key},
105
+ 'children',
106
+ {_key: decoratedParagraph.children[2]._key},
107
+ ],
108
+ offset: 4,
109
+ },
110
+ }),
111
+ event: {
112
+ type: 'serialize',
113
+ originEvent: 'unknown',
114
+ },
115
+ }),
116
+ ).toMatchObject({
117
+ data: '<p><em>foo <code><strong>bar</strong> baz</code></em></p>',
118
+ })
119
+ })
120
+
121
+ test('image', () => {
122
+ expect(
123
+ converterTextHtml.serialize({
124
+ context: createContext(defineSchema({}), {
125
+ anchor: {
126
+ path: [{_key: image._key}],
127
+ offset: 0,
128
+ },
129
+ focus: {
130
+ path: [{_key: image._key}],
131
+ offset: 0,
132
+ },
133
+ }),
134
+ event: {
135
+ type: 'serialize',
136
+ originEvent: 'unknown',
137
+ },
138
+ }),
139
+ ).toMatchObject({
140
+ type: 'serialization.failure',
141
+ })
142
+ })
143
+
144
+ test('inline object', () => {
145
+ expect(
146
+ converterTextHtml.serialize({
147
+ context: createContext(defineSchema({}), {
148
+ anchor: {
149
+ path: [
150
+ {_key: paragraphWithInlineBlock._key},
151
+ 'children',
152
+ {_key: paragraphWithInlineBlock.children[0]._key},
153
+ ],
154
+ offset: 0,
155
+ },
156
+ focus: {
157
+ path: [
158
+ {_key: paragraphWithInlineBlock._key},
159
+ 'children',
160
+ {_key: paragraphWithInlineBlock.children[2]._key},
161
+ ],
162
+ offset: 4,
163
+ },
164
+ }),
165
+ event: {
166
+ type: 'serialize',
167
+ originEvent: 'unknown',
168
+ },
169
+ }),
170
+ ).toMatchObject({
171
+ data: '<p>fizz buzz</p>',
172
+ })
173
+ })
174
+
175
+ test('lists', () => {
176
+ expect(
177
+ converterTextHtml.serialize({
178
+ context: {
179
+ converters: coreConverters,
180
+ activeDecorators: [],
181
+ keyGenerator: createTestKeyGenerator(),
182
+ schema: compileSchemaDefinition(defineSchema({})),
183
+ value: [
184
+ {
185
+ _key: 'k0',
186
+ _type: 'block',
187
+ children: [
188
+ {
189
+ _key: 'k1',
190
+ _type: 'span',
191
+ marks: [],
192
+ text: 'foo',
193
+ },
194
+ ],
195
+ level: 1,
196
+ listItem: 'number',
197
+ },
198
+ {
199
+ _key: 'k2',
200
+ _type: 'block',
201
+ children: [
202
+ {
203
+ _key: 'k3',
204
+ _type: 'span',
205
+ marks: [],
206
+ text: 'bar',
207
+ },
208
+ ],
209
+ level: 2,
210
+ listItem: 'bullet',
211
+ },
212
+ ],
213
+ selection: {
214
+ anchor: {
215
+ path: [{_key: 'k0'}, 'children', {_key: 'k1'}],
216
+ offset: 0,
217
+ },
218
+ focus: {
219
+ path: [{_key: 'k2'}, 'children', {_key: 'k3'}],
220
+ offset: 3,
221
+ },
222
+ },
223
+ },
224
+ event: {
225
+ type: 'serialize',
226
+ originEvent: 'unknown',
227
+ },
228
+ }),
229
+ ).toMatchObject({
230
+ data: '<ol><li>foo<ul><li>bar</li></ul></li></ol>',
231
+ })
232
+ })
233
+ })
@@ -0,0 +1,61 @@
1
+ import {htmlToBlocks} from '@portabletext/block-tools'
2
+ import {toHTML} from '@portabletext/to-html'
3
+ import type {PortableTextBlock} from '@sanity/types'
4
+ import {sliceBlocks} from '../utils'
5
+ import type {Converter} from './converter'
6
+
7
+ export const converterTextHtml: Converter<'text/html'> = {
8
+ serialize: ({context, event}) => {
9
+ if (!context.selection) {
10
+ return {
11
+ type: 'serialization.failure',
12
+ mimeType: 'text/html',
13
+ originEvent: event.originEvent,
14
+ reason: 'No selection',
15
+ }
16
+ }
17
+
18
+ const blocks = sliceBlocks({
19
+ blocks: context.value,
20
+ selection: context.selection,
21
+ })
22
+
23
+ const html = toHTML(blocks, {
24
+ onMissingComponent: false,
25
+ components: {
26
+ unknownType: ({children}) =>
27
+ children !== undefined ? `${children}` : '',
28
+ },
29
+ })
30
+
31
+ if (html === '') {
32
+ return {
33
+ type: 'serialization.failure',
34
+ mimeType: 'text/html',
35
+ originEvent: event.originEvent,
36
+ reason: 'Serialized HTML is empty',
37
+ }
38
+ }
39
+
40
+ return {
41
+ type: 'serialization.success',
42
+ data: html,
43
+ mimeType: 'text/html',
44
+ originEvent: event.originEvent,
45
+ }
46
+ },
47
+ deserialize: ({context, event}) => {
48
+ const blocks = htmlToBlocks(event.data, context.schema.portableText, {
49
+ keyGenerator: context.keyGenerator,
50
+ unstable_whitespaceOnPasteMode:
51
+ context.schema.block.options.unstable_whitespaceOnPasteMode,
52
+ }) as Array<PortableTextBlock>
53
+
54
+ return {
55
+ type: 'deserialization.success',
56
+ data: blocks,
57
+ mimeType: 'text/html',
58
+ }
59
+ },
60
+ mimeType: 'text/html',
61
+ }