@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,136 @@
1
+ import {
2
+ isPortableTextSpan,
3
+ isPortableTextTextBlock,
4
+ type PortableTextBlock,
5
+ } from '@sanity/types'
6
+ import type {EditorContext} from '../editor/editor-snapshot'
7
+ import {isTypedObject} from './asserters'
8
+
9
+ export function parseBlock({
10
+ context,
11
+ block,
12
+ }: {
13
+ context: Pick<EditorContext, 'keyGenerator' | 'schema'>
14
+ block: unknown
15
+ }): PortableTextBlock | undefined {
16
+ if (!isTypedObject(block)) {
17
+ return undefined
18
+ }
19
+
20
+ if (
21
+ block._type !== context.schema.block.name &&
22
+ !context.schema.blockObjects.some(
23
+ (blockObject) => blockObject.name === block._type,
24
+ )
25
+ ) {
26
+ return undefined
27
+ }
28
+
29
+ if (!isPortableTextTextBlock(block)) {
30
+ return {
31
+ ...block,
32
+ _key: context.keyGenerator(),
33
+ }
34
+ }
35
+
36
+ const markDefKeyMap = new Map<string, string>()
37
+ const markDefs = (block.markDefs ?? []).flatMap((markDef) => {
38
+ if (
39
+ context.schema.annotations.some(
40
+ (annotation) => annotation.name === markDef._type,
41
+ )
42
+ ) {
43
+ const _key = context.keyGenerator()
44
+ markDefKeyMap.set(markDef._key, _key)
45
+
46
+ return [
47
+ {
48
+ ...markDef,
49
+ _key,
50
+ },
51
+ ]
52
+ }
53
+
54
+ return []
55
+ })
56
+
57
+ const children = block.children.flatMap((child) => {
58
+ if (!isTypedObject(child)) {
59
+ return []
60
+ }
61
+
62
+ if (
63
+ child._type !== context.schema.span.name &&
64
+ !context.schema.inlineObjects.some(
65
+ (inlineObject) => inlineObject.name === child._type,
66
+ )
67
+ ) {
68
+ return []
69
+ }
70
+
71
+ if (!isPortableTextSpan(child)) {
72
+ return [
73
+ {
74
+ ...child,
75
+ _key: context.keyGenerator(),
76
+ },
77
+ ]
78
+ }
79
+
80
+ const marks = (child.marks ?? []).flatMap((mark) => {
81
+ if (markDefKeyMap.has(mark)) {
82
+ return [markDefKeyMap.get(mark)]
83
+ }
84
+
85
+ if (
86
+ context.schema.decorators.some((decorator) => decorator.value === mark)
87
+ ) {
88
+ return [mark]
89
+ }
90
+
91
+ return []
92
+ })
93
+
94
+ return [
95
+ {
96
+ ...child,
97
+ _key: context.keyGenerator(),
98
+ marks,
99
+ },
100
+ ]
101
+ })
102
+
103
+ const parsedBlock = {
104
+ ...block,
105
+ _key: context.keyGenerator(),
106
+ children:
107
+ children.length > 0
108
+ ? children
109
+ : [
110
+ {
111
+ _key: context.keyGenerator(),
112
+ _type: context.schema.span.name,
113
+ text: '',
114
+ marks: [],
115
+ },
116
+ ],
117
+ markDefs,
118
+ }
119
+
120
+ if (!context.schema.styles.find((style) => style.value === block.style)) {
121
+ const defaultStyle = context.schema.styles[0].value
122
+
123
+ if (defaultStyle !== undefined) {
124
+ parsedBlock.style = defaultStyle
125
+ } else {
126
+ delete parsedBlock.style
127
+ }
128
+ }
129
+
130
+ if (!context.schema.lists.find((list) => list.value === block.listItem)) {
131
+ delete parsedBlock.listItem
132
+ delete parsedBlock.level
133
+ }
134
+
135
+ return parsedBlock
136
+ }
@@ -0,0 +1,9 @@
1
+ export function createTestKeyGenerator() {
2
+ let index = 0
3
+
4
+ return function keyGenerator() {
5
+ const key = `k${index}`
6
+ index++
7
+ return key
8
+ }
9
+ }
@@ -6,6 +6,7 @@ test(getSelectedSpans.name, () => {
6
6
  function snapshot(selection: EditorSelection): EditorSnapshot {
7
7
  return {
8
8
  context: {
9
+ converters: [],
9
10
  schema: {} as EditorSchema,
10
11
  keyGenerator: () => '',
11
12
  activeDecorators: [],
@@ -6,6 +6,7 @@ test(getSelectionText.name, () => {
6
6
  function snapshot(selection: EditorSelection): EditorSnapshot {
7
7
  return {
8
8
  context: {
9
+ converters: [],
9
10
  schema: {} as EditorSchema,
10
11
  keyGenerator: () => '',
11
12
  activeDecorators: [],
@@ -7,6 +7,7 @@ test(isActiveDecorator.name, () => {
7
7
  function snapshot(selection: EditorSelection): EditorSnapshot {
8
8
  return {
9
9
  context: {
10
+ converters: [],
10
11
  schema: {} as EditorSchema,
11
12
  keyGenerator: () => '',
12
13
  activeDecorators: [],
@@ -2,43 +2,62 @@ import type {PortableTextBlock, PortableTextTextBlock} from '@sanity/types'
2
2
  import {describe, expect, test} from 'vitest'
3
3
  import {sliceBlocks} from './util.slice-blocks'
4
4
 
5
- const textBlock1: PortableTextTextBlock = {
5
+ const b1: PortableTextTextBlock = {
6
6
  _type: 'block',
7
7
  _key: 'b1',
8
8
  children: [
9
9
  {
10
10
  _type: 'span',
11
- _key: 's1',
11
+ _key: 'b1c1',
12
12
  text: 'foo',
13
- marks: ['strong'],
14
13
  },
15
14
  {
16
15
  _type: 'span',
17
- _key: 's2',
16
+ _key: 'b1c2',
18
17
  text: 'bar',
19
18
  },
20
19
  ],
21
20
  }
22
- const textBlock2: PortableTextTextBlock = {
21
+ const b2: PortableTextBlock = {
22
+ _type: 'image',
23
+ _key: 'b2',
24
+ src: 'https://example.com/image.jpg',
25
+ alt: 'Example',
26
+ }
27
+ const b3: PortableTextTextBlock = {
23
28
  _type: 'block',
24
29
  _key: 'b3',
25
30
  children: [
26
31
  {
27
32
  _type: 'span',
28
- _key: 's3',
33
+ _key: 'b3c1',
29
34
  text: 'baz',
30
35
  },
31
36
  ],
32
37
  }
38
+ const b4: PortableTextTextBlock = {
39
+ _type: 'block',
40
+ _key: 'b4',
41
+ children: [
42
+ {
43
+ _type: 'span',
44
+ _key: 'b4c1',
45
+ text: 'fizz',
46
+ },
47
+ {
48
+ _type: 'stock-ticker',
49
+ _key: 'b4c2',
50
+ symbol: 'AAPL',
51
+ },
52
+ {
53
+ _type: 'span',
54
+ _key: 'b4c3',
55
+ text: 'buzz',
56
+ },
57
+ ],
58
+ }
33
59
 
34
- const blocks: Array<PortableTextBlock> = [
35
- textBlock1,
36
- {
37
- _type: 'image',
38
- _key: 'b2',
39
- },
40
- textBlock2,
41
- ]
60
+ const blocks: Array<PortableTextBlock> = [b1, b2, b3, b4]
42
61
 
43
62
  describe(sliceBlocks.name, () => {
44
63
  test('sensible defaults', () => {
@@ -52,19 +71,19 @@ describe(sliceBlocks.name, () => {
52
71
  blocks,
53
72
  selection: {
54
73
  anchor: {
55
- path: [{_key: 'b1'}, 'children', {_key: 's1'}],
74
+ path: [{_key: b1._key}, 'children', {_key: b1.children[0]._key}],
56
75
  offset: 0,
57
76
  },
58
77
  focus: {
59
- path: [{_key: 'b1'}, 'children', {_key: 's1'}],
78
+ path: [{_key: b1._key}, 'children', {_key: b1.children[0]._key}],
60
79
  offset: 3,
61
80
  },
62
81
  },
63
82
  }),
64
83
  ).toEqual([
65
84
  {
66
- ...textBlock1,
67
- children: [textBlock1.children[0]],
85
+ ...b1,
86
+ children: [b1.children[0]],
68
87
  },
69
88
  ])
70
89
  })
@@ -75,21 +94,21 @@ describe(sliceBlocks.name, () => {
75
94
  blocks,
76
95
  selection: {
77
96
  anchor: {
78
- path: [{_key: 'b1'}, 'children', {_key: 's1'}],
97
+ path: [{_key: b1._key}, 'children', {_key: b1.children[0]._key}],
79
98
  offset: 1,
80
99
  },
81
100
  focus: {
82
- path: [{_key: 'b1'}, 'children', {_key: 's1'}],
101
+ path: [{_key: b1._key}, 'children', {_key: b1.children[0]._key}],
83
102
  offset: 2,
84
103
  },
85
104
  },
86
105
  }),
87
106
  ).toEqual([
88
107
  {
89
- ...textBlock1,
108
+ ...b1,
90
109
  children: [
91
110
  {
92
- ...textBlock1.children[0],
111
+ ...b1.children[0],
93
112
  text: 'o',
94
113
  },
95
114
  ],
@@ -97,67 +116,229 @@ describe(sliceBlocks.name, () => {
97
116
  ])
98
117
  })
99
118
 
119
+ test('starting and ending selection on a block object', () => {
120
+ expect(
121
+ sliceBlocks({
122
+ blocks,
123
+ selection: {
124
+ anchor: {
125
+ path: [{_key: b2._key}],
126
+ offset: 0,
127
+ },
128
+ focus: {
129
+ path: [{_key: b2._key}],
130
+ offset: 0,
131
+ },
132
+ },
133
+ }),
134
+ ).toEqual([b2])
135
+ })
136
+
137
+ test('starting selection on a block object', () => {
138
+ expect(
139
+ sliceBlocks({
140
+ blocks,
141
+ selection: {
142
+ anchor: {
143
+ path: [{_key: b2._key}],
144
+ offset: 0,
145
+ },
146
+ focus: {
147
+ path: [{_key: b3._key}, 'children', {_key: b3.children[0]._key}],
148
+ offset: 3,
149
+ },
150
+ },
151
+ }),
152
+ ).toEqual([b2, b3])
153
+ })
154
+
100
155
  test('ending selection on a block object', () => {
101
156
  expect(
102
157
  sliceBlocks({
103
158
  blocks,
104
159
  selection: {
105
160
  anchor: {
106
- path: [{_key: 'b1'}, 'children', {_key: 's1'}],
161
+ path: [{_key: b1._key}, 'children', {_key: b1.children[0]._key}],
107
162
  offset: 3,
108
163
  },
109
164
  focus: {
110
- path: [{_key: 'b2'}],
165
+ path: [{_key: b2._key}],
111
166
  offset: 0,
112
167
  },
113
168
  },
114
169
  }),
115
170
  ).toEqual([
116
171
  {
117
- ...textBlock1,
172
+ ...b1,
118
173
  children: [
119
174
  {
120
- ...textBlock1.children[0],
175
+ ...b1.children[0],
121
176
  text: '',
122
177
  },
178
+ ...b1.children.slice(1),
123
179
  ],
124
180
  },
125
181
  blocks[1],
126
182
  ])
127
183
  })
128
184
 
185
+ test('slicing across block object', () => {
186
+ expect(
187
+ sliceBlocks({
188
+ blocks,
189
+ selection: {
190
+ anchor: {
191
+ path: [{_key: b1._key}, 'children', {_key: b1.children[0]._key}],
192
+ offset: 0,
193
+ },
194
+ focus: {
195
+ path: [{_key: b3._key}, 'children', {_key: b3.children[0]._key}],
196
+ offset: 3,
197
+ },
198
+ },
199
+ }),
200
+ ).toEqual([b1, b2, b3])
201
+ })
202
+
129
203
  test('starting and ending mid-span', () => {
130
204
  expect(
131
205
  sliceBlocks({
132
206
  blocks,
133
207
  selection: {
134
208
  anchor: {
135
- path: [{_key: 'b1'}, 'children', {_key: 's1'}],
209
+ path: [{_key: b3._key}, 'children', {_key: b3.children[0]._key}],
136
210
  offset: 2,
137
211
  },
138
- focus: {path: [{_key: 'b3'}, 'children', {_key: 's3'}], offset: 1},
212
+ focus: {
213
+ path: [{_key: b4._key}, 'children', {_key: b4.children[0]._key}],
214
+ offset: 1,
215
+ },
139
216
  },
140
217
  }),
141
218
  ).toEqual([
142
219
  {
143
- ...textBlock1,
220
+ ...b3,
144
221
  children: [
145
222
  {
146
- ...textBlock1.children[0],
147
- text: 'o',
223
+ ...b3.children[0],
224
+ text: 'z',
148
225
  },
149
226
  ],
150
227
  },
151
- blocks[1],
152
228
  {
153
- ...textBlock2,
229
+ ...b4,
230
+ children: [
231
+ {
232
+ ...b4.children[0],
233
+ text: 'f',
234
+ },
235
+ ],
236
+ },
237
+ ])
238
+ })
239
+
240
+ test('starting mid-span and ending end-span', () => {
241
+ expect(
242
+ sliceBlocks({
243
+ blocks,
244
+ selection: {
245
+ anchor: {
246
+ path: [{_key: b3._key}, 'children', {_key: b3.children[0]._key}],
247
+ offset: 2,
248
+ },
249
+ focus: {
250
+ path: [{_key: b4._key}, 'children', {_key: b4.children[0]._key}],
251
+ offset: 4,
252
+ },
253
+ },
254
+ }),
255
+ ).toEqual([
256
+ {
257
+ ...b3,
154
258
  children: [
155
259
  {
156
- ...textBlock2.children[0],
157
- text: 'az',
260
+ ...b3.children[0],
261
+ text: 'z',
158
262
  },
159
263
  ],
160
264
  },
265
+ {
266
+ ...b4,
267
+ children: [
268
+ {
269
+ ...b4.children[0],
270
+ },
271
+ ],
272
+ },
273
+ ])
274
+ })
275
+
276
+ test('starting on inline object', () => {
277
+ expect(
278
+ sliceBlocks({
279
+ blocks,
280
+ selection: {
281
+ anchor: {
282
+ path: [{_key: b4._key}, 'children', {_key: b4.children[1]._key}],
283
+ offset: 0,
284
+ },
285
+ focus: {
286
+ path: [{_key: b4._key}, 'children', {_key: b4.children[2]._key}],
287
+ offset: 4,
288
+ },
289
+ },
290
+ }),
291
+ ).toEqual([
292
+ {
293
+ ...b4,
294
+ children: [b4.children[1], b4.children[2]],
295
+ },
296
+ ])
297
+ })
298
+
299
+ test('ending on inline object', () => {
300
+ expect(
301
+ sliceBlocks({
302
+ blocks,
303
+ selection: {
304
+ anchor: {
305
+ path: [{_key: b4._key}, 'children', {_key: b4.children[0]._key}],
306
+ offset: 0,
307
+ },
308
+ focus: {
309
+ path: [{_key: b4._key}, 'children', {_key: b4.children[1]._key}],
310
+ offset: 0,
311
+ },
312
+ },
313
+ }),
314
+ ).toEqual([
315
+ {
316
+ ...b4,
317
+ children: [b4.children[0], b4.children[1]],
318
+ },
319
+ ])
320
+ })
321
+
322
+ test('starting and ending on inline object', () => {
323
+ expect(
324
+ sliceBlocks({
325
+ blocks,
326
+ selection: {
327
+ anchor: {
328
+ path: [{_key: b4._key}, 'children', {_key: b4.children[1]._key}],
329
+ offset: 0,
330
+ },
331
+ focus: {
332
+ path: [{_key: b4._key}, 'children', {_key: b4.children[1]._key}],
333
+ offset: 0,
334
+ },
335
+ },
336
+ }),
337
+ ).toEqual([
338
+ {
339
+ ...b4,
340
+ children: [b4.children[1]],
341
+ },
161
342
  ])
162
343
  })
163
344
  })
@@ -47,8 +47,20 @@ export function sliceBlocks({
47
47
  }
48
48
 
49
49
  for (const block of blocks) {
50
+ if (!isPortableTextTextBlock(block)) {
51
+ if (block._key === startBlockKey && block._key === endBlockKey) {
52
+ startBlock = block
53
+ break
54
+ }
55
+ }
56
+
50
57
  if (block._key === startBlockKey) {
51
- if (isPortableTextTextBlock(block) && startChildKey) {
58
+ if (!isPortableTextTextBlock(block)) {
59
+ startBlock = block
60
+ continue
61
+ }
62
+
63
+ if (startChildKey) {
52
64
  for (const child of block.children) {
53
65
  if (child._key === startChildKey) {
54
66
  if (isPortableTextSpan(child)) {
@@ -66,14 +78,17 @@ export function sliceBlocks({
66
78
  },
67
79
  ],
68
80
  }
69
- break
81
+ } else {
82
+ startBlock = {
83
+ ...block,
84
+ children: [child],
85
+ }
70
86
  }
71
87
 
72
- startBlock = {
73
- ...block,
74
- children: [child],
88
+ if (startChildKey === endChildKey) {
89
+ break
75
90
  }
76
- break
91
+ continue
77
92
  }
78
93
 
79
94
  if (startBlock && isPortableTextTextBlock(startBlock)) {
@@ -97,10 +112,19 @@ export function sliceBlocks({
97
112
  }
98
113
 
99
114
  startBlock = block
115
+
116
+ if (startBlockKey === endBlockKey) {
117
+ break
118
+ }
100
119
  }
101
120
 
102
121
  if (block._key === endBlockKey) {
103
- if (isPortableTextTextBlock(block) && endBlockKey) {
122
+ if (!isPortableTextTextBlock(block)) {
123
+ endBlock = block
124
+ break
125
+ }
126
+
127
+ if (endChildKey) {
104
128
  endBlock = {
105
129
  ...block,
106
130
  children: [],
@@ -111,8 +135,9 @@ export function sliceBlocks({
111
135
  if (child._key === endChildKey && isPortableTextSpan(child)) {
112
136
  endBlock.children.push({
113
137
  ...child,
114
- text: child.text.slice(endPoint.offset),
138
+ text: child.text.slice(0, endPoint.offset),
115
139
  })
140
+
116
141
  break
117
142
  }
118
143
 
@@ -124,7 +149,7 @@ export function sliceBlocks({
124
149
  }
125
150
  }
126
151
 
127
- continue
152
+ break
128
153
  }
129
154
 
130
155
  endBlock = block
@@ -132,7 +157,9 @@ export function sliceBlocks({
132
157
  break
133
158
  }
134
159
 
135
- middleBlocks.push(block)
160
+ if (startBlock) {
161
+ middleBlocks.push(block)
162
+ }
136
163
  }
137
164
 
138
165
  return [