@portabletext/editor 2.7.2 → 2.8.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 (50) hide show
  1. package/lib/_chunks-cjs/selector.is-selecting-entire-blocks.cjs +3 -1
  2. package/lib/_chunks-cjs/selector.is-selecting-entire-blocks.cjs.map +1 -1
  3. package/lib/_chunks-cjs/util.slice-blocks.cjs +60 -6
  4. package/lib/_chunks-cjs/util.slice-blocks.cjs.map +1 -1
  5. package/lib/_chunks-dts/behavior.types.action.d.cts +95 -95
  6. package/lib/_chunks-dts/behavior.types.action.d.ts +95 -95
  7. package/lib/_chunks-es/selector.is-selecting-entire-blocks.js +4 -2
  8. package/lib/_chunks-es/selector.is-selecting-entire-blocks.js.map +1 -1
  9. package/lib/_chunks-es/util.slice-blocks.js +56 -5
  10. package/lib/_chunks-es/util.slice-blocks.js.map +1 -1
  11. package/lib/index.cjs +94 -121
  12. package/lib/index.cjs.map +1 -1
  13. package/lib/index.js +90 -118
  14. package/lib/index.js.map +1 -1
  15. package/lib/plugins/index.d.cts +3 -3
  16. package/lib/plugins/index.d.ts +3 -3
  17. package/lib/selectors/index.d.cts +13 -3
  18. package/lib/selectors/index.d.ts +13 -3
  19. package/lib/utils/index.d.ts +2 -2
  20. package/package.json +13 -14
  21. package/src/behaviors/behavior.abstract.insert.ts +58 -1
  22. package/src/behaviors/behavior.core.annotations.ts +24 -2
  23. package/src/behaviors/behavior.core.ts +1 -1
  24. package/src/behaviors/behavior.types.event.ts +18 -18
  25. package/src/converters/converter.text-html.serialize.test.ts +27 -17
  26. package/src/converters/converter.text-plain.test.ts +1 -1
  27. package/src/editor/plugins/createWithEditableAPI.ts +16 -0
  28. package/src/internal-utils/parse-blocks.ts +2 -1
  29. package/src/operations/behavior.operation.annotation.add.ts +1 -12
  30. package/src/operations/behavior.operations.ts +0 -18
  31. package/src/selectors/selector.is-active-annotation.test.ts +320 -0
  32. package/src/selectors/selector.is-active-annotation.ts +24 -0
  33. package/src/utils/util.slice-blocks.test.ts +39 -5
  34. package/src/utils/util.slice-blocks.ts +36 -3
  35. package/src/editor/__tests__/PortableTextEditor.test.tsx +0 -430
  36. package/src/editor/__tests__/PortableTextEditorTester.tsx +0 -58
  37. package/src/editor/__tests__/RangeDecorations.test.tsx +0 -213
  38. package/src/editor/__tests__/insert-block.test.tsx +0 -224
  39. package/src/editor/__tests__/self-solving.test.tsx +0 -183
  40. package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +0 -298
  41. package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +0 -177
  42. package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +0 -538
  43. package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +0 -162
  44. package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +0 -65
  45. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +0 -612
  46. package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +0 -103
  47. package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +0 -147
  48. package/src/internal-utils/__tests__/valueNormalization.test.tsx +0 -79
  49. package/src/operations/behavior.operation.insert-inline-object.ts +0 -59
  50. package/src/operations/behavior.operation.insert-span.ts +0 -48
@@ -0,0 +1,320 @@
1
+ import {compileSchema, defineSchema} from '@portabletext/schema'
2
+ import {createTestKeyGenerator} from '@portabletext/test'
3
+ import {describe, expect, test} from 'vitest'
4
+ import {createTestSnapshot} from '../internal-utils/create-test-snapshot'
5
+ import {isActiveAnnotation} from './selector.is-active-annotation'
6
+
7
+ describe(isActiveAnnotation.name, () => {
8
+ const keyGenerator = createTestKeyGenerator()
9
+ const annotationKey = keyGenerator()
10
+ const blockKey = keyGenerator()
11
+ const fooKey = keyGenerator()
12
+ const barKey = keyGenerator()
13
+ const bazKey = keyGenerator()
14
+ const value = [
15
+ {
16
+ _type: 'block',
17
+ _key: blockKey,
18
+ children: [
19
+ {
20
+ _type: 'span',
21
+ _key: fooKey,
22
+ text: 'foo ',
23
+ marks: [],
24
+ },
25
+ {
26
+ _type: 'span',
27
+ _key: barKey,
28
+ text: 'bar',
29
+ marks: [annotationKey],
30
+ },
31
+ {
32
+ _type: 'span',
33
+ _key: bazKey,
34
+ text: ' baz',
35
+ marks: [],
36
+ },
37
+ ],
38
+ markDefs: [
39
+ {_key: annotationKey, _type: 'link', href: 'https://example.com'},
40
+ ],
41
+ },
42
+ ]
43
+ const schema = compileSchema(
44
+ defineSchema({
45
+ annotations: [{name: 'link', fields: [{name: 'href', type: 'string'}]}],
46
+ }),
47
+ )
48
+
49
+ describe('collapsed selection', () => {
50
+ test('selection before the annotation', () => {
51
+ const snapshot = createTestSnapshot({
52
+ context: {
53
+ schema,
54
+ value,
55
+ selection: {
56
+ anchor: {
57
+ path: [{_key: blockKey}, 'children', {_key: fooKey}],
58
+ offset: 4,
59
+ },
60
+ focus: {
61
+ path: [{_key: blockKey}, 'children', {_key: fooKey}],
62
+ offset: 4,
63
+ },
64
+ },
65
+ },
66
+ })
67
+
68
+ expect(isActiveAnnotation('link')(snapshot)).toBe(false)
69
+ })
70
+
71
+ test('selection at the start of the annotation', () => {
72
+ const snapshot = createTestSnapshot({
73
+ context: {
74
+ schema,
75
+ value,
76
+ selection: {
77
+ anchor: {
78
+ path: [{_key: blockKey}, 'children', {_key: barKey}],
79
+ offset: 0,
80
+ },
81
+ focus: {
82
+ path: [{_key: blockKey}, 'children', {_key: barKey}],
83
+ offset: 0,
84
+ },
85
+ },
86
+ },
87
+ })
88
+
89
+ expect(isActiveAnnotation('link')(snapshot)).toBe(false)
90
+ })
91
+
92
+ test('selection after the annotation', () => {
93
+ const snapshot = createTestSnapshot({
94
+ context: {
95
+ schema,
96
+ value,
97
+ selection: {
98
+ anchor: {
99
+ path: [{_key: blockKey}, 'children', {_key: bazKey}],
100
+ offset: 0,
101
+ },
102
+ focus: {
103
+ path: [{_key: blockKey}, 'children', {_key: bazKey}],
104
+ offset: 0,
105
+ },
106
+ },
107
+ },
108
+ })
109
+
110
+ expect(isActiveAnnotation('link')(snapshot)).toBe(false)
111
+ })
112
+
113
+ test('selection at the end of the annotation', () => {
114
+ const snapshot = createTestSnapshot({
115
+ context: {
116
+ schema,
117
+ value,
118
+ selection: {
119
+ anchor: {
120
+ path: [{_key: blockKey}, 'children', {_key: barKey}],
121
+ offset: 3,
122
+ },
123
+ focus: {
124
+ path: [{_key: blockKey}, 'children', {_key: barKey}],
125
+ offset: 3,
126
+ },
127
+ },
128
+ },
129
+ })
130
+
131
+ expect(isActiveAnnotation('link')(snapshot)).toBe(false)
132
+ })
133
+
134
+ test('selection in the annotation', () => {
135
+ const snapshot = createTestSnapshot({
136
+ context: {
137
+ schema,
138
+ value,
139
+ selection: {
140
+ anchor: {
141
+ path: [{_key: blockKey}, 'children', {_key: barKey}],
142
+ offset: 2,
143
+ },
144
+ focus: {
145
+ path: [{_key: blockKey}, 'children', {_key: barKey}],
146
+ offset: 2,
147
+ },
148
+ },
149
+ },
150
+ })
151
+
152
+ expect(isActiveAnnotation('link')(snapshot)).toBe(true)
153
+ })
154
+ })
155
+
156
+ describe('expanded selection', () => {
157
+ test('selection on the annotation', () => {
158
+ const snapshot = createTestSnapshot({
159
+ context: {
160
+ schema,
161
+ value,
162
+ selection: {
163
+ anchor: {
164
+ path: [{_key: blockKey}, 'children', {_key: barKey}],
165
+ offset: 0,
166
+ },
167
+ focus: {
168
+ path: [{_key: blockKey}, 'children', {_key: barKey}],
169
+ offset: 3,
170
+ },
171
+ },
172
+ },
173
+ })
174
+
175
+ expect(isActiveAnnotation('link')(snapshot)).toBe(true)
176
+ })
177
+
178
+ test('selection in the annotation', () => {
179
+ const snapshot = createTestSnapshot({
180
+ context: {
181
+ schema,
182
+ value,
183
+ selection: {
184
+ anchor: {
185
+ path: [{_key: blockKey}, 'children', {_key: barKey}],
186
+ offset: 1,
187
+ },
188
+ focus: {
189
+ path: [{_key: blockKey}, 'children', {_key: barKey}],
190
+ offset: 2,
191
+ },
192
+ },
193
+ },
194
+ })
195
+
196
+ expect(isActiveAnnotation('link')(snapshot)).toBe(true)
197
+ })
198
+
199
+ describe('selection including the annotation', () => {
200
+ const snapshot = createTestSnapshot({
201
+ context: {
202
+ schema,
203
+ value,
204
+ selection: {
205
+ anchor: {
206
+ path: [{_key: blockKey}, 'children', {_key: fooKey}],
207
+ offset: 2,
208
+ },
209
+ focus: {
210
+ path: [{_key: blockKey}, 'children', {_key: bazKey}],
211
+ offset: 2,
212
+ },
213
+ },
214
+ },
215
+ })
216
+
217
+ test('mode: full', () => {
218
+ expect(isActiveAnnotation('link')(snapshot)).toBe(false)
219
+ })
220
+
221
+ test('mode: partial', () => {
222
+ expect(isActiveAnnotation('link', {mode: 'partial'})(snapshot)).toBe(
223
+ true,
224
+ )
225
+ })
226
+ })
227
+
228
+ describe('selection before the annotation', () => {
229
+ const snapshot = createTestSnapshot({
230
+ context: {
231
+ schema,
232
+ value,
233
+ selection: {
234
+ anchor: {
235
+ path: [{_key: blockKey}, 'children', {_key: fooKey}],
236
+ offset: 0,
237
+ },
238
+ focus: {
239
+ path: [{_key: blockKey}, 'children', {_key: fooKey}],
240
+ offset: 4,
241
+ },
242
+ },
243
+ },
244
+ })
245
+
246
+ test('mode: full', () => {
247
+ expect(isActiveAnnotation('link')(snapshot)).toBe(false)
248
+ })
249
+
250
+ test('mode: partial', () => {
251
+ expect(isActiveAnnotation('link', {mode: 'partial'})(snapshot)).toBe(
252
+ false,
253
+ )
254
+ })
255
+ })
256
+
257
+ test('selection overlapping from the start', () => {
258
+ const snapshot = createTestSnapshot({
259
+ context: {
260
+ schema,
261
+ value,
262
+ selection: {
263
+ anchor: {
264
+ path: [{_key: blockKey}, 'children', {_key: fooKey}],
265
+ offset: 0,
266
+ },
267
+ focus: {
268
+ path: [{_key: blockKey}, 'children', {_key: barKey}],
269
+ offset: 2,
270
+ },
271
+ },
272
+ },
273
+ })
274
+
275
+ expect(isActiveAnnotation('link')(snapshot)).toBe(false)
276
+ })
277
+
278
+ test('selection after the annotation', () => {
279
+ const snapshot = createTestSnapshot({
280
+ context: {
281
+ schema,
282
+ value,
283
+ selection: {
284
+ anchor: {
285
+ path: [{_key: blockKey}, 'children', {_key: bazKey}],
286
+ offset: 0,
287
+ },
288
+ focus: {
289
+ path: [{_key: blockKey}, 'children', {_key: bazKey}],
290
+ offset: 4,
291
+ },
292
+ },
293
+ },
294
+ })
295
+
296
+ expect(isActiveAnnotation('link')(snapshot)).toBe(false)
297
+ })
298
+
299
+ test('selection overlapping from the end', () => {
300
+ const snapshot = createTestSnapshot({
301
+ context: {
302
+ schema,
303
+ value,
304
+ selection: {
305
+ anchor: {
306
+ path: [{_key: blockKey}, 'children', {_key: barKey}],
307
+ offset: 2,
308
+ },
309
+ focus: {
310
+ path: [{_key: blockKey}, 'children', {_key: bazKey}],
311
+ offset: 4,
312
+ },
313
+ },
314
+ },
315
+ })
316
+
317
+ expect(isActiveAnnotation('link')(snapshot)).toBe(false)
318
+ })
319
+ })
320
+ })
@@ -2,14 +2,38 @@ import {isTextBlock} from '@portabletext/schema'
2
2
  import type {EditorSelector} from '../editor/editor-selector'
3
3
  import {getActiveAnnotationsMarks} from './selector.get-active-annotation-marks'
4
4
  import {getSelectedBlocks} from './selector.get-selected-blocks'
5
+ import {getSelectedValue} from './selector.get-selected-value'
5
6
 
6
7
  /**
8
+ * Check whether an annotation is active in the given `snapshot`.
9
+ *
7
10
  * @public
8
11
  */
9
12
  export function isActiveAnnotation(
10
13
  annotation: string,
14
+ options?: {
15
+ /**
16
+ * Choose whether the annotation has to take up the entire selection in the
17
+ * `snapshot` or if the annotation can be partially selected.
18
+ *
19
+ * Defaults to 'full'
20
+ */
21
+ mode?: 'partial' | 'full'
22
+ },
11
23
  ): EditorSelector<boolean> {
12
24
  return (snapshot) => {
25
+ const mode = options?.mode ?? 'full'
26
+
27
+ if (mode === 'partial') {
28
+ const selectedValue = getSelectedValue(snapshot)
29
+
30
+ const selectionMarkDefs = selectedValue.flatMap((block) =>
31
+ isTextBlock(snapshot.context, block) ? (block.markDefs ?? []) : [],
32
+ )
33
+
34
+ return selectionMarkDefs.some((markDef) => markDef._type === annotation)
35
+ }
36
+
13
37
  const selectedBlocks = getSelectedBlocks(snapshot)
14
38
  const selectionMarkDefs = selectedBlocks.flatMap((block) =>
15
39
  isTextBlock(snapshot.context, block.node)
@@ -11,13 +11,17 @@ const b1: PortableTextTextBlock = {
11
11
  _type: 'span',
12
12
  _key: 'b1c1',
13
13
  text: 'foo',
14
+ marks: [],
14
15
  },
15
16
  {
16
17
  _type: 'span',
17
18
  _key: 'b1c2',
18
19
  text: 'bar',
20
+ marks: [],
19
21
  },
20
22
  ],
23
+ markDefs: [],
24
+ style: 'normal',
21
25
  }
22
26
  const b2: PortableTextBlock = {
23
27
  _type: 'image',
@@ -33,8 +37,11 @@ const b3: PortableTextTextBlock = {
33
37
  _type: 'span',
34
38
  _key: 'b3c1',
35
39
  text: 'baz',
40
+ marks: [],
36
41
  },
37
42
  ],
43
+ markDefs: [],
44
+ style: 'normal',
38
45
  }
39
46
  const b4: PortableTextTextBlock = {
40
47
  _type: 'block',
@@ -44,6 +51,7 @@ const b4: PortableTextTextBlock = {
44
51
  _type: 'span',
45
52
  _key: 'b4c1',
46
53
  text: 'fizz',
54
+ marks: [],
47
55
  },
48
56
  {
49
57
  _type: 'stock-ticker',
@@ -54,11 +62,19 @@ const b4: PortableTextTextBlock = {
54
62
  _type: 'span',
55
63
  _key: 'b4c3',
56
64
  text: 'buzz',
65
+ marks: [],
57
66
  },
58
67
  ],
68
+ markDefs: [],
69
+ style: 'normal',
59
70
  }
60
71
 
61
- const schema = compileSchema(defineSchema({}))
72
+ const schema = compileSchema(
73
+ defineSchema({
74
+ blockObjects: [{name: 'image'}],
75
+ inlineObjects: [{name: 'stock-ticker'}],
76
+ }),
77
+ )
62
78
  const blocks: Array<PortableTextBlock> = [b1, b2, b3, b4]
63
79
 
64
80
  describe(sliceBlocks.name, () => {
@@ -413,7 +429,11 @@ describe(sliceBlocks.name, () => {
413
429
  {
414
430
  _key: 'b0',
415
431
  _type: 'block',
416
- children: [{_key: 's0', _type: 'span', text: 'Hello, world!'}],
432
+ children: [
433
+ {_key: 's0', _type: 'span', text: 'Hello, world!', marks: []},
434
+ ],
435
+ markDefs: [],
436
+ style: 'normal',
417
437
  _map: {},
418
438
  },
419
439
  ],
@@ -422,7 +442,9 @@ describe(sliceBlocks.name, () => {
422
442
  {
423
443
  _key: 'b0',
424
444
  _type: 'block',
425
- children: [{_key: 's0', _type: 'span', text: 'world'}],
445
+ children: [{_key: 's0', _type: 'span', text: 'world', marks: []}],
446
+ markDefs: [],
447
+ style: 'normal',
426
448
  _map: {},
427
449
  },
428
450
  ])
@@ -449,8 +471,16 @@ describe(sliceBlocks.name, () => {
449
471
  _key: 'b0',
450
472
  _type: 'block',
451
473
  children: [
452
- {_key: 's0', _type: 'span', text: 'Hello, world!', _map: {}},
474
+ {
475
+ _key: 's0',
476
+ _type: 'span',
477
+ text: 'Hello, world!',
478
+ _map: {},
479
+ marks: [],
480
+ },
453
481
  ],
482
+ markDefs: [],
483
+ style: 'normal',
454
484
  },
455
485
  ],
456
486
  }),
@@ -458,7 +488,11 @@ describe(sliceBlocks.name, () => {
458
488
  {
459
489
  _key: 'b0',
460
490
  _type: 'block',
461
- children: [{_key: 's0', _type: 'span', text: 'world', _map: {}}],
491
+ children: [
492
+ {_key: 's0', _type: 'span', text: 'world', _map: {}, marks: []},
493
+ ],
494
+ markDefs: [],
495
+ style: 'normal',
462
496
  },
463
497
  ])
464
498
  })
@@ -1,6 +1,8 @@
1
1
  import {isSpan, isTextBlock} from '@portabletext/schema'
2
2
  import type {PortableTextBlock} from '@sanity/types'
3
3
  import type {EditorContext} from '../editor/editor-snapshot'
4
+ import {defaultKeyGenerator} from '../editor/key-generator'
5
+ import {parseBlock} from '../internal-utils/parse-blocks'
4
6
  import {
5
7
  getBlockKeyFromSelectionPoint,
6
8
  getChildKeyFromSelectionPoint,
@@ -162,13 +164,44 @@ export function sliceBlocks({
162
164
  }
163
165
 
164
166
  if (startBlock) {
165
- middleBlocks.push(block)
167
+ middleBlocks.push(
168
+ parseBlock({
169
+ context: {
170
+ ...context,
171
+ keyGenerator: defaultKeyGenerator,
172
+ },
173
+ block,
174
+ options: {refreshKeys: false, validateFields: false},
175
+ }) ?? block,
176
+ )
166
177
  }
167
178
  }
168
179
 
180
+ const parsedStartBlock = startBlock
181
+ ? parseBlock({
182
+ context: {
183
+ ...context,
184
+ keyGenerator: defaultKeyGenerator,
185
+ },
186
+ block: startBlock,
187
+ options: {refreshKeys: false, validateFields: false},
188
+ })
189
+ : undefined
190
+
191
+ const parsedEndBlock = endBlock
192
+ ? parseBlock({
193
+ context: {
194
+ ...context,
195
+ keyGenerator: defaultKeyGenerator,
196
+ },
197
+ block: endBlock,
198
+ options: {refreshKeys: false, validateFields: false},
199
+ })
200
+ : undefined
201
+
169
202
  return [
170
- ...(startBlock ? [startBlock] : []),
203
+ ...(parsedStartBlock ? [parsedStartBlock] : []),
171
204
  ...middleBlocks,
172
- ...(endBlock ? [endBlock] : []),
205
+ ...(parsedEndBlock ? [parsedEndBlock] : []),
173
206
  ]
174
207
  }