@portabletext/editor 1.23.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 +23 -9
  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 +23 -9
  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 +535 -333
  12. package/lib/index.cjs.map +1 -1
  13. package/lib/index.d.cts +158 -1
  14. package/lib/index.d.ts +158 -1
  15. package/lib/index.js +539 -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 +11 -10
  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 +2 -0
  39. package/src/editor/editor-machine.ts +18 -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 +87 -0
  51. package/src/utils/util.slice-blocks.ts +27 -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: [],
@@ -134,6 +134,24 @@ describe(sliceBlocks.name, () => {
134
134
  ).toEqual([b2])
135
135
  })
136
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
+
137
155
  test('ending selection on a block object', () => {
138
156
  expect(
139
157
  sliceBlocks({
@@ -254,4 +272,73 @@ describe(sliceBlocks.name, () => {
254
272
  },
255
273
  ])
256
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
+ },
342
+ ])
343
+ })
257
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,17 +78,17 @@ export function sliceBlocks({
66
78
  },
67
79
  ],
68
80
  }
69
- continue
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
- }
77
-
78
- if (startChildKey === endChildKey) {
79
- break
91
+ continue
80
92
  }
81
93
 
82
94
  if (startBlock && isPortableTextTextBlock(startBlock)) {
@@ -107,7 +119,12 @@ export function sliceBlocks({
107
119
  }
108
120
 
109
121
  if (block._key === endBlockKey) {
110
- if (isPortableTextTextBlock(block) && endBlockKey) {
122
+ if (!isPortableTextTextBlock(block)) {
123
+ endBlock = block
124
+ break
125
+ }
126
+
127
+ if (endChildKey) {
111
128
  endBlock = {
112
129
  ...block,
113
130
  children: [],
@@ -1,181 +0,0 @@
1
- import {isPortableTextSpan, isPortableTextTextBlock} from '@sanity/types'
2
- import type {Descendant} from 'slate'
3
- import {describe, expect, it} from 'vitest'
4
- import {exportedForTesting} from '../createWithInsertData'
5
-
6
- const initialValue = [
7
- {
8
- _key: 'a',
9
- _type: 'myTestBlockType',
10
- children: [
11
- {
12
- _key: 'a1',
13
- _type: 'span',
14
- marks: ['link1'],
15
- text: 'Block A',
16
- },
17
- {
18
- _key: 'a2',
19
- _type: 'span',
20
- marks: ['colour1'],
21
- text: 'Block B',
22
- },
23
- ],
24
- markDefs: [
25
- {
26
- _key: 'link1',
27
- _type: 'link',
28
- href: 'google.com',
29
- newTab: false,
30
- },
31
- {
32
- _key: 'colour1',
33
- _type: 'color',
34
- color: 'red',
35
- },
36
- ],
37
- style: 'normal',
38
- },
39
- ] satisfies Array<Descendant>
40
-
41
- describe('plugin: createWithInsertData _regenerateKeys', () => {
42
- it('has MarkDefs that are allowed annotations', async () => {
43
- const {_regenerateKeys} = exportedForTesting
44
- let keyCursor = 0
45
-
46
- const generatedValue = _regenerateKeys(
47
- {
48
- isTextBlock: isPortableTextTextBlock,
49
- isTextSpan: isPortableTextSpan,
50
- },
51
- initialValue,
52
- () => {
53
- keyCursor++
54
- return `k${keyCursor}`
55
- },
56
- 'span',
57
- {
58
- annotations: [
59
- {
60
- name: 'color',
61
- jsonType: 'object',
62
- fields: [],
63
- __experimental_search: [],
64
- },
65
- {
66
- name: 'link',
67
- jsonType: 'object',
68
- fields: [],
69
- __experimental_search: [],
70
- },
71
- ],
72
- },
73
- )
74
-
75
- // the keys are not important here as it's not what we are testing here
76
- expect(generatedValue).toStrictEqual([
77
- {
78
- _key: 'k3',
79
- _type: 'myTestBlockType',
80
- children: [
81
- {_key: 'k4', _type: 'span', marks: ['k1'], text: 'Block A'},
82
- {
83
- _key: 'k5',
84
- _type: 'span',
85
- marks: ['k2'],
86
- text: 'Block B',
87
- },
88
- ],
89
- markDefs: [
90
- {_key: 'k1', _type: 'link', href: 'google.com', newTab: false},
91
- {_key: 'k2', _type: 'color', color: 'red'},
92
- ],
93
- style: 'normal',
94
- },
95
- ])
96
- })
97
-
98
- it('removes MarkDefs when no annotations are allowed', async () => {
99
- const {_regenerateKeys} = exportedForTesting
100
- let keyCursor = 0
101
-
102
- const generatedValue = _regenerateKeys(
103
- {
104
- isTextBlock: isPortableTextTextBlock,
105
- isTextSpan: isPortableTextSpan,
106
- },
107
- initialValue,
108
- () => {
109
- keyCursor++
110
- return `k${keyCursor}`
111
- },
112
- 'span',
113
- {annotations: []},
114
- )
115
-
116
- // orphaned children marks are removed later in the normalize function
117
- expect(generatedValue).toStrictEqual([
118
- {
119
- _key: 'k1',
120
- _type: 'myTestBlockType',
121
- children: [
122
- {_key: 'a1', _type: 'span', marks: ['link1'], text: 'Block A'},
123
- {
124
- _key: 'a2',
125
- _type: 'span',
126
- marks: ['colour1'],
127
- text: 'Block B',
128
- },
129
- ],
130
- style: 'normal',
131
- },
132
- ])
133
- })
134
-
135
- it('updates MarkDefs when one annotations is allowed but one is not allowed', async () => {
136
- const {_regenerateKeys} = exportedForTesting
137
- let keyCursor = 0
138
-
139
- const generatedValue = _regenerateKeys(
140
- {
141
- isTextBlock: isPortableTextTextBlock,
142
- isTextSpan: isPortableTextSpan,
143
- },
144
- initialValue,
145
- () => {
146
- keyCursor++
147
- return `k${keyCursor}`
148
- },
149
- 'span',
150
- {
151
- annotations: [
152
- {
153
- name: 'color',
154
- jsonType: 'object',
155
- fields: [],
156
- __experimental_search: [],
157
- },
158
- ],
159
- },
160
- )
161
-
162
- // orphaned children marks are removed later in the normalize function
163
- expect(generatedValue).toStrictEqual([
164
- {
165
- _key: 'k1',
166
- _type: 'myTestBlockType',
167
- children: [
168
- {_key: 'a1', _type: 'span', marks: ['link1'], text: 'Block A'},
169
- {
170
- _key: 'a2',
171
- _type: 'span',
172
- marks: ['colour1'],
173
- text: 'Block B',
174
- },
175
- ],
176
- markDefs: [{_key: 'colour1', _type: 'color', color: 'red'}],
177
- style: 'normal',
178
- },
179
- ])
180
- })
181
- })