@portabletext/editor 2.7.2 → 2.8.1

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 (72) 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/selector.is-selection-expanded.cjs +10 -4
  4. package/lib/_chunks-cjs/selector.is-selection-expanded.cjs.map +1 -1
  5. package/lib/_chunks-cjs/util.merge-text-blocks.cjs +0 -1
  6. package/lib/_chunks-cjs/util.merge-text-blocks.cjs.map +1 -1
  7. package/lib/_chunks-cjs/util.slice-blocks.cjs +60 -9
  8. package/lib/_chunks-cjs/util.slice-blocks.cjs.map +1 -1
  9. package/lib/_chunks-dts/behavior.types.action.d.cts +149 -149
  10. package/lib/_chunks-dts/behavior.types.action.d.ts +86 -86
  11. package/lib/_chunks-es/selector.is-selecting-entire-blocks.js +4 -2
  12. package/lib/_chunks-es/selector.is-selecting-entire-blocks.js.map +1 -1
  13. package/lib/_chunks-es/selector.is-selection-expanded.js +10 -4
  14. package/lib/_chunks-es/selector.is-selection-expanded.js.map +1 -1
  15. package/lib/_chunks-es/util.merge-text-blocks.js +0 -1
  16. package/lib/_chunks-es/util.merge-text-blocks.js.map +1 -1
  17. package/lib/_chunks-es/util.slice-blocks.js +56 -8
  18. package/lib/_chunks-es/util.slice-blocks.js.map +1 -1
  19. package/lib/index.cjs +94 -131
  20. package/lib/index.cjs.map +1 -1
  21. package/lib/index.js +90 -128
  22. package/lib/index.js.map +1 -1
  23. package/lib/plugins/index.d.cts +3 -3
  24. package/lib/selectors/index.d.cts +13 -3
  25. package/lib/selectors/index.d.ts +13 -3
  26. package/package.json +13 -14
  27. package/src/behaviors/behavior.abstract.insert.ts +58 -1
  28. package/src/behaviors/behavior.abstract.split.ts +0 -1
  29. package/src/behaviors/behavior.core.annotations.ts +24 -2
  30. package/src/behaviors/behavior.core.ts +1 -1
  31. package/src/behaviors/behavior.types.event.ts +18 -18
  32. package/src/converters/converter.portable-text.ts +0 -1
  33. package/src/converters/converter.text-html.serialize.test.ts +27 -17
  34. package/src/converters/converter.text-html.ts +0 -1
  35. package/src/converters/converter.text-plain.test.ts +1 -1
  36. package/src/converters/converter.text-plain.ts +0 -1
  37. package/src/editor/Editable.tsx +0 -1
  38. package/src/editor/plugins/createWithEditableAPI.ts +16 -0
  39. package/src/internal-utils/parse-blocks.test.ts +23 -23
  40. package/src/internal-utils/parse-blocks.ts +13 -24
  41. package/src/internal-utils/test-editor.tsx +15 -21
  42. package/src/operations/behavior.operation.annotation.add.ts +2 -13
  43. package/src/operations/behavior.operation.block.set.ts +1 -1
  44. package/src/operations/behavior.operation.block.unset.ts +2 -2
  45. package/src/operations/behavior.operation.insert.block.ts +1 -1
  46. package/src/operations/behavior.operations.ts +0 -18
  47. package/src/plugins/plugin.internal.auto-close-brackets.test.tsx +25 -71
  48. package/src/plugins/plugin.markdown.test.tsx +12 -30
  49. package/src/selectors/selector.get-selected-value.test.ts +748 -0
  50. package/src/selectors/selector.get-selected-value.ts +28 -7
  51. package/src/selectors/selector.get-trimmed-selection.test.ts +0 -1
  52. package/src/selectors/selector.is-active-annotation.test.ts +320 -0
  53. package/src/selectors/selector.is-active-annotation.ts +24 -0
  54. package/src/utils/util.merge-text-blocks.ts +1 -1
  55. package/src/utils/util.slice-blocks.ts +36 -3
  56. package/src/editor/__tests__/PortableTextEditor.test.tsx +0 -430
  57. package/src/editor/__tests__/PortableTextEditorTester.tsx +0 -58
  58. package/src/editor/__tests__/RangeDecorations.test.tsx +0 -213
  59. package/src/editor/__tests__/insert-block.test.tsx +0 -224
  60. package/src/editor/__tests__/self-solving.test.tsx +0 -183
  61. package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +0 -298
  62. package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +0 -177
  63. package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +0 -538
  64. package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +0 -162
  65. package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +0 -65
  66. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +0 -612
  67. package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +0 -103
  68. package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +0 -147
  69. package/src/internal-utils/__tests__/valueNormalization.test.tsx +0 -79
  70. package/src/operations/behavior.operation.insert-inline-object.ts +0 -59
  71. package/src/operations/behavior.operation.insert-span.ts +0 -48
  72. package/src/utils/util.slice-blocks.test.ts +0 -465
@@ -35,13 +35,34 @@ export const getSelectedValue: EditorSelector<Array<PortableTextBlock>> = (
35
35
  return []
36
36
  }
37
37
 
38
- const slicedValue = snapshot.context.value.slice(
39
- startBlockIndex,
40
- endBlockIndex + 1,
38
+ const startBlock = snapshot.context.value.at(startBlockIndex)
39
+ const slicedStartBlock = startBlock
40
+ ? sliceBlocks({
41
+ context: snapshot.context,
42
+ blocks: [startBlock],
43
+ }).at(0)
44
+ : undefined
45
+
46
+ if (startBlockIndex === endBlockIndex) {
47
+ return slicedStartBlock ? [slicedStartBlock] : []
48
+ }
49
+
50
+ const endBlock = snapshot.context.value.at(endBlockIndex)
51
+ const slicedEndBlock = endBlock
52
+ ? sliceBlocks({
53
+ context: snapshot.context,
54
+ blocks: [endBlock],
55
+ }).at(0)
56
+ : undefined
57
+
58
+ const middleBlocks = snapshot.context.value.slice(
59
+ startBlockIndex + 1,
60
+ endBlockIndex,
41
61
  )
42
62
 
43
- return sliceBlocks({
44
- context: snapshot.context,
45
- blocks: slicedValue,
46
- })
63
+ return [
64
+ ...(slicedStartBlock ? [slicedStartBlock] : []),
65
+ ...middleBlocks,
66
+ ...(slicedEndBlock ? [slicedEndBlock] : []),
67
+ ]
47
68
  }
@@ -33,7 +33,6 @@ function snapshot(
33
33
  },
34
34
  block,
35
35
  options: {
36
- refreshKeys: false,
37
36
  validateFields: false,
38
37
  },
39
38
  })
@@ -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)
@@ -18,7 +18,7 @@ export function mergeTextBlocks({
18
18
  const parsedIncomingBlock = parseBlock({
19
19
  context,
20
20
  block: incomingBlock,
21
- options: {refreshKeys: false, validateFields: false},
21
+ options: {validateFields: false},
22
22
  })
23
23
 
24
24
  if (!parsedIncomingBlock || !isTextBlock(context, parsedIncomingBlock)) {
@@ -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: {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: {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: {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
  }