@portabletext/editor 2.9.2 → 2.10.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 (38) hide show
  1. package/lib/_chunks-cjs/util.merge-text-blocks.cjs +1 -0
  2. package/lib/_chunks-cjs/util.merge-text-blocks.cjs.map +1 -1
  3. package/lib/_chunks-cjs/util.slice-blocks.cjs +6 -1
  4. package/lib/_chunks-cjs/util.slice-blocks.cjs.map +1 -1
  5. package/lib/_chunks-dts/behavior.types.action.d.cts +132 -122
  6. package/lib/_chunks-dts/behavior.types.action.d.ts +69 -59
  7. package/lib/_chunks-es/util.merge-text-blocks.js +1 -0
  8. package/lib/_chunks-es/util.merge-text-blocks.js.map +1 -1
  9. package/lib/_chunks-es/util.slice-blocks.js +6 -1
  10. package/lib/_chunks-es/util.slice-blocks.js.map +1 -1
  11. package/lib/index.cjs +232 -68
  12. package/lib/index.cjs.map +1 -1
  13. package/lib/index.js +234 -70
  14. package/lib/index.js.map +1 -1
  15. package/lib/utils/index.d.ts +2 -2
  16. package/package.json +10 -10
  17. package/src/behaviors/behavior.abstract.insert.ts +109 -24
  18. package/src/behaviors/behavior.abstract.split.ts +1 -0
  19. package/src/behaviors/behavior.perform-event.ts +89 -67
  20. package/src/behaviors/behavior.types.event.ts +9 -1
  21. package/src/converters/converter.portable-text.ts +1 -0
  22. package/src/converters/converter.text-html.ts +1 -0
  23. package/src/converters/converter.text-plain.ts +1 -0
  24. package/src/editor/Editable.tsx +1 -0
  25. package/src/editor/editor-selector.ts +10 -1
  26. package/src/editor/without-normalizing-conditional.ts +13 -0
  27. package/src/internal-utils/parse-blocks.test.ts +14 -14
  28. package/src/internal-utils/parse-blocks.ts +9 -2
  29. package/src/operations/behavior.operation.block.set.ts +4 -3
  30. package/src/operations/behavior.operation.block.unset.ts +8 -2
  31. package/src/operations/behavior.operation.insert.block.ts +4 -1
  32. package/src/operations/behavior.operation.insert.child.ts +95 -0
  33. package/src/operations/behavior.operations.ts +9 -0
  34. package/src/selectors/selector.get-trimmed-selection.test.ts +1 -0
  35. package/src/types/block-with-optional-key.ts +13 -1
  36. package/src/utils/util.merge-text-blocks.ts +1 -1
  37. package/src/utils/util.slice-blocks.ts +3 -3
  38. package/src/utils/util.slice-text-block.test.ts +54 -28
@@ -19,6 +19,7 @@ export function parseBlocks({
19
19
  context: Pick<EditorContext, 'keyGenerator' | 'schema'>
20
20
  blocks: unknown
21
21
  options: {
22
+ removeUnusedMarkDefs: boolean
22
23
  validateFields: boolean
23
24
  }
24
25
  }): Array<PortableTextBlock> {
@@ -41,6 +42,7 @@ export function parseBlock({
41
42
  context: Pick<EditorContext, 'keyGenerator' | 'schema'>
42
43
  block: unknown
43
44
  options: {
45
+ removeUnusedMarkDefs: boolean
44
46
  validateFields: boolean
45
47
  }
46
48
  }): PortableTextBlock | undefined {
@@ -99,7 +101,10 @@ export function parseTextBlock({
99
101
  }: {
100
102
  block: unknown
101
103
  context: Pick<EditorContext, 'keyGenerator' | 'schema'>
102
- options: {validateFields: boolean}
104
+ options: {
105
+ removeUnusedMarkDefs: boolean
106
+ validateFields: boolean
107
+ }
103
108
  }): PortableTextTextBlock | undefined {
104
109
  if (!isTypedObject(block)) {
105
110
  return undefined
@@ -204,7 +209,9 @@ export function parseTextBlock({
204
209
  marks: [],
205
210
  },
206
211
  ],
207
- markDefs: markDefs.filter((markDef) => marks.includes(markDef._key)),
212
+ markDefs: options.removeUnusedMarkDefs
213
+ ? markDefs.filter((markDef) => marks.includes(markDef._key))
214
+ : markDefs,
208
215
  ...customFields,
209
216
  }
210
217
 
@@ -30,15 +30,16 @@ export const blockSetOperationImplementation: BehaviorOperationImplementation<
30
30
  const parsedBlock = parseBlock({
31
31
  context,
32
32
  block: updatedBlock,
33
- options: {validateFields: true},
33
+ options: {
34
+ removeUnusedMarkDefs: false,
35
+ validateFields: true,
36
+ },
34
37
  })
35
38
 
36
39
  if (!parsedBlock) {
37
40
  throw new Error(`Unable to update block at ${JSON.stringify(operation.at)}`)
38
41
  }
39
42
 
40
- parsedBlock.markDefs = updatedBlock.markDefs
41
-
42
43
  const slateBlock = toSlateValue([parsedBlock], {
43
44
  schemaTypes: context.schema,
44
45
  })?.at(0) as SlateElement | undefined
@@ -51,7 +51,10 @@ export const blockUnsetOperationImplementation: BehaviorOperationImplementation<
51
51
  const updatedTextBlock = parseBlock({
52
52
  context,
53
53
  block: omit(parsedBlock, propsToRemove),
54
- options: {validateFields: true},
54
+ options: {
55
+ removeUnusedMarkDefs: true,
56
+ validateFields: true,
57
+ },
55
58
  })
56
59
 
57
60
  if (!updatedTextBlock) {
@@ -81,7 +84,10 @@ export const blockUnsetOperationImplementation: BehaviorOperationImplementation<
81
84
  parsedBlock,
82
85
  operation.props.filter((prop) => prop !== '_type'),
83
86
  ),
84
- options: {validateFields: true},
87
+ options: {
88
+ removeUnusedMarkDefs: true,
89
+ validateFields: true,
90
+ },
85
91
  })
86
92
 
87
93
  if (!updatedBlockObject) {
@@ -24,7 +24,10 @@ export const insertBlockOperationImplementation: BehaviorOperationImplementation
24
24
  const parsedBlock = parseBlock({
25
25
  block: operation.block,
26
26
  context,
27
- options: {validateFields: true},
27
+ options: {
28
+ removeUnusedMarkDefs: true,
29
+ validateFields: true,
30
+ },
28
31
  })
29
32
 
30
33
  if (!parsedBlock) {
@@ -0,0 +1,95 @@
1
+ import {isTextBlock} from '@portabletext/schema'
2
+ import {Transforms} from 'slate'
3
+ import {parseInlineObject, parseSpan} from '../internal-utils/parse-blocks'
4
+ import {getFocusBlock, getFocusSpan} from '../internal-utils/slate-utils'
5
+ import {VOID_CHILD_KEY} from '../internal-utils/values'
6
+ import type {BehaviorOperationImplementation} from './behavior.operations'
7
+
8
+ export const insertChildOperationImplementation: BehaviorOperationImplementation<
9
+ 'insert.child'
10
+ > = ({context, operation}) => {
11
+ const focus = operation.editor.selection?.focus
12
+ const focusBlockIndex = focus?.path.at(0)
13
+ const focusChildIndex = focus?.path.at(1)
14
+
15
+ if (focusBlockIndex === undefined || focusChildIndex === undefined) {
16
+ throw new Error('Unable to insert child without a focus')
17
+ }
18
+
19
+ const [focusBlock, focusBlockPath] = getFocusBlock({editor: operation.editor})
20
+
21
+ if (!focus || !focusBlock || !focusBlockPath) {
22
+ throw new Error('Unable to insert child without a focus block')
23
+ }
24
+
25
+ if (!isTextBlock(context, focusBlock)) {
26
+ throw new Error('Unable to insert child into a non-text block')
27
+ }
28
+
29
+ const markDefs = focusBlock.markDefs ?? []
30
+ const markDefKeyMap = new Map<string, string>()
31
+ for (const markDef of markDefs) {
32
+ markDefKeyMap.set(markDef._key, markDef._key)
33
+ }
34
+
35
+ const span = parseSpan({
36
+ span: operation.child,
37
+ context,
38
+ markDefKeyMap,
39
+ options: {validateFields: true},
40
+ })
41
+
42
+ if (span) {
43
+ const [focusSpan] = getFocusSpan({editor: operation.editor})
44
+
45
+ if (focusSpan) {
46
+ Transforms.insertNodes(operation.editor, span, {
47
+ at: focus,
48
+ select: true,
49
+ })
50
+ } else {
51
+ Transforms.insertNodes(operation.editor, span, {
52
+ at: [focusBlockIndex, focusChildIndex + 1],
53
+ select: true,
54
+ })
55
+ }
56
+
57
+ return
58
+ }
59
+
60
+ const inlineObject = parseInlineObject({
61
+ inlineObject: operation.child,
62
+ context,
63
+ options: {validateFields: true},
64
+ })
65
+
66
+ if (inlineObject) {
67
+ const {_key, _type, ...rest} = inlineObject
68
+
69
+ Transforms.insertNodes(
70
+ operation.editor,
71
+ {
72
+ _key,
73
+ _type,
74
+ children: [
75
+ {
76
+ _key: VOID_CHILD_KEY,
77
+ _type: 'span',
78
+ text: '',
79
+ marks: [],
80
+ },
81
+ ],
82
+ value: rest,
83
+ __inline: true,
84
+ },
85
+ {
86
+ at: [focusBlockIndex, focusChildIndex + 1],
87
+ select: true,
88
+ },
89
+ )
90
+
91
+ return
92
+ }
93
+
94
+ throw new Error('Unable to parse child')
95
+ }
@@ -20,6 +20,7 @@ import {childUnsetOperationImplementation} from './behavior.operation.child.unse
20
20
  import {decoratorAddOperationImplementation} from './behavior.operation.decorator.add'
21
21
  import {deleteOperationImplementation} from './behavior.operation.delete'
22
22
  import {insertBlockOperationImplementation} from './behavior.operation.insert.block'
23
+ import {insertChildOperationImplementation} from './behavior.operation.insert.child'
23
24
  import {insertTextOperationImplementation} from './behavior.operation.insert.text'
24
25
  import {moveBackwardOperationImplementation} from './behavior.operation.move.backward'
25
26
  import {moveBlockOperationImplementation} from './behavior.operation.move.block'
@@ -66,6 +67,7 @@ const behaviorOperationImplementations: BehaviorOperationImplementations = {
66
67
  'history.redo': historyRedoOperationImplementation,
67
68
  'history.undo': historyUndoOperationImplementation,
68
69
  'insert.block': insertBlockOperationImplementation,
70
+ 'insert.child': insertChildOperationImplementation,
69
71
  'insert.text': insertTextOperationImplementation,
70
72
  'move.backward': moveBackwardOperationImplementation,
71
73
  'move.block': moveBlockOperationImplementation,
@@ -167,6 +169,13 @@ export function performOperation({
167
169
  })
168
170
  break
169
171
  }
172
+ case 'insert.child': {
173
+ behaviorOperationImplementations['insert.child']({
174
+ context,
175
+ operation: operation,
176
+ })
177
+ break
178
+ }
170
179
  case 'insert.text': {
171
180
  behaviorOperationImplementations['insert.text']({
172
181
  context,
@@ -33,6 +33,7 @@ function snapshot(
33
33
  },
34
34
  block,
35
35
  options: {
36
+ removeUnusedMarkDefs: true,
36
37
  validateFields: false,
37
38
  },
38
39
  })
@@ -1,4 +1,8 @@
1
- import type {PortableTextObject, PortableTextTextBlock} from '@sanity/types'
1
+ import type {
2
+ PortableTextObject,
3
+ PortableTextSpan,
4
+ PortableTextTextBlock,
5
+ } from '@portabletext/schema'
2
6
 
3
7
  export type TextBlockWithOptionalKey = Omit<PortableTextTextBlock, '_key'> & {
4
8
  _key?: PortableTextTextBlock['_key']
@@ -11,3 +15,11 @@ export type ObjectBlockWithOptionalKey = Omit<PortableTextObject, '_key'> & {
11
15
  export type BlockWithOptionalKey =
12
16
  | TextBlockWithOptionalKey
13
17
  | ObjectBlockWithOptionalKey
18
+
19
+ export type SpanWithOptionalKey = Omit<PortableTextSpan, '_key'> & {
20
+ _key?: PortableTextSpan['_key']
21
+ }
22
+
23
+ export type ChildWithOptionalKey =
24
+ | SpanWithOptionalKey
25
+ | ObjectBlockWithOptionalKey
@@ -18,7 +18,7 @@ export function mergeTextBlocks({
18
18
  const parsedIncomingBlock = parseBlock({
19
19
  context,
20
20
  block: incomingBlock,
21
- options: {validateFields: false},
21
+ options: {removeUnusedMarkDefs: true, validateFields: false},
22
22
  })
23
23
 
24
24
  if (!parsedIncomingBlock || !isTextBlock(context, parsedIncomingBlock)) {
@@ -171,7 +171,7 @@ export function sliceBlocks({
171
171
  keyGenerator: defaultKeyGenerator,
172
172
  },
173
173
  block,
174
- options: {validateFields: false},
174
+ options: {removeUnusedMarkDefs: true, validateFields: false},
175
175
  }) ?? block,
176
176
  )
177
177
  }
@@ -184,7 +184,7 @@ export function sliceBlocks({
184
184
  keyGenerator: defaultKeyGenerator,
185
185
  },
186
186
  block: startBlock,
187
- options: {validateFields: false},
187
+ options: {removeUnusedMarkDefs: true, validateFields: false},
188
188
  })
189
189
  : undefined
190
190
 
@@ -195,7 +195,7 @@ export function sliceBlocks({
195
195
  keyGenerator: defaultKeyGenerator,
196
196
  },
197
197
  block: endBlock,
198
- options: {validateFields: false},
198
+ options: {removeUnusedMarkDefs: true, validateFields: false},
199
199
  })
200
200
  : undefined
201
201
 
@@ -107,7 +107,7 @@ describe(sliceTextBlock.name, () => {
107
107
  })
108
108
  })
109
109
 
110
- test('multiple children', () => {
110
+ describe('multiple children', () => {
111
111
  const fooSpan = createSpan({
112
112
  text: 'foo',
113
113
  marks: ['strong'],
@@ -127,36 +127,62 @@ describe(sliceTextBlock.name, () => {
127
127
  children: [fooSpan, barSpan, stockTicker, bazSpan],
128
128
  })
129
129
 
130
- expect(
131
- sliceTextBlock({
132
- context: {
133
- schema,
134
- selection: {
135
- anchor: {
136
- path: [{_key: block._key}, 'children', {_key: barSpan._key}],
137
- offset: 1,
130
+ test('mid-span', () => {
131
+ expect(
132
+ sliceTextBlock({
133
+ context: {
134
+ schema,
135
+ selection: {
136
+ anchor: {
137
+ path: [{_key: block._key}, 'children', {_key: barSpan._key}],
138
+ offset: 1,
139
+ },
140
+ focus: {
141
+ path: [{_key: block._key}, 'children', {_key: bazSpan._key}],
142
+ offset: 1,
143
+ },
138
144
  },
139
- focus: {
140
- path: [{_key: block._key}, 'children', {_key: bazSpan._key}],
141
- offset: 1,
145
+ },
146
+ block,
147
+ }),
148
+ ).toEqual({
149
+ ...block,
150
+ children: [
151
+ {
152
+ ...barSpan,
153
+ text: 'ar',
154
+ },
155
+ stockTicker,
156
+ {
157
+ ...bazSpan,
158
+ text: 'b',
159
+ },
160
+ ],
161
+ })
162
+ })
163
+
164
+ test('from end of span to end of block', () => {
165
+ expect(
166
+ sliceTextBlock({
167
+ context: {
168
+ schema,
169
+ selection: {
170
+ anchor: {
171
+ path: [{_key: block._key}, 'children', {_key: fooSpan._key}],
172
+ offset: 3,
173
+ },
174
+ focus: {
175
+ path: [{_key: block._key}, 'children', {_key: bazSpan._key}],
176
+ offset: 3,
177
+ },
142
178
  },
143
179
  },
144
- },
145
- block,
146
- }),
147
- ).toEqual({
148
- ...block,
149
- children: [
150
- {
151
- ...barSpan,
152
- text: 'ar',
153
- },
154
- stockTicker,
155
- {
156
- ...bazSpan,
157
- text: 'b',
158
- },
159
- ],
180
+ block,
181
+ }),
182
+ ).toEqual({
183
+ ...block,
184
+ children: [{...fooSpan, text: ''}, barSpan, stockTicker, bazSpan],
185
+ })
160
186
  })
161
187
  })
162
188
  })