@portabletext/editor 2.9.1 → 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 (53) hide show
  1. package/lib/_chunks-cjs/selector.is-selecting-entire-blocks.cjs +9 -1
  2. package/lib/_chunks-cjs/selector.is-selecting-entire-blocks.cjs.map +1 -1
  3. package/lib/_chunks-cjs/util.merge-text-blocks.cjs +1 -0
  4. package/lib/_chunks-cjs/util.merge-text-blocks.cjs.map +1 -1
  5. package/lib/_chunks-cjs/util.slice-blocks.cjs +6 -1
  6. package/lib/_chunks-cjs/util.slice-blocks.cjs.map +1 -1
  7. package/lib/_chunks-dts/behavior.types.action.d.cts +141 -131
  8. package/lib/_chunks-dts/behavior.types.action.d.ts +71 -61
  9. package/lib/_chunks-es/selector.is-selecting-entire-blocks.js +9 -1
  10. package/lib/_chunks-es/selector.is-selecting-entire-blocks.js.map +1 -1
  11. package/lib/_chunks-es/util.merge-text-blocks.js +1 -0
  12. package/lib/_chunks-es/util.merge-text-blocks.js.map +1 -1
  13. package/lib/_chunks-es/util.slice-blocks.js +6 -1
  14. package/lib/_chunks-es/util.slice-blocks.js.map +1 -1
  15. package/lib/index.cjs +419 -309
  16. package/lib/index.cjs.map +1 -1
  17. package/lib/index.js +422 -312
  18. package/lib/index.js.map +1 -1
  19. package/lib/plugins/index.d.cts +3 -3
  20. package/lib/plugins/index.d.ts +3 -3
  21. package/lib/utils/index.d.ts +2 -2
  22. package/package.json +8 -8
  23. package/src/behaviors/behavior.abstract.insert.ts +109 -24
  24. package/src/behaviors/behavior.abstract.split.ts +1 -0
  25. package/src/behaviors/behavior.perform-event.ts +84 -118
  26. package/src/behaviors/behavior.types.event.ts +9 -1
  27. package/src/converters/converter.portable-text.ts +1 -0
  28. package/src/converters/converter.text-html.ts +1 -0
  29. package/src/converters/converter.text-plain.ts +1 -0
  30. package/src/editor/Editable.tsx +1 -0
  31. package/src/editor/editor-selector.ts +10 -1
  32. package/src/editor/plugins/create-with-event-listeners.ts +13 -14
  33. package/src/editor/sync-machine.ts +9 -0
  34. package/src/editor/with-performing-behavior-operation.ts +21 -0
  35. package/src/editor/without-normalizing-conditional.ts +13 -0
  36. package/src/internal-utils/parse-blocks.test.ts +14 -14
  37. package/src/internal-utils/parse-blocks.ts +9 -2
  38. package/src/internal-utils/slate-utils.test.tsx +119 -0
  39. package/src/internal-utils/slate-utils.ts +14 -1
  40. package/src/internal-utils/text-marks.ts +1 -1
  41. package/src/internal-utils/values.ts +1 -55
  42. package/src/operations/behavior.operation.block.set.ts +18 -36
  43. package/src/operations/behavior.operation.block.unset.ts +8 -2
  44. package/src/operations/behavior.operation.insert.block.ts +4 -1
  45. package/src/operations/behavior.operation.insert.child.ts +95 -0
  46. package/src/operations/behavior.operations.ts +140 -128
  47. package/src/selectors/selector.get-mark-state.ts +19 -5
  48. package/src/selectors/selector.get-trimmed-selection.test.ts +1 -0
  49. package/src/types/block-with-optional-key.ts +13 -1
  50. package/src/utils/util.merge-text-blocks.ts +1 -1
  51. package/src/utils/util.slice-blocks.ts +3 -3
  52. package/src/utils/util.slice-text-block.test.ts +54 -28
  53. package/src/editor/with-applying-behavior-operations.ts +0 -18
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ import {Editor} from 'slate'
1
2
  import type {
2
3
  AbstractBehaviorEventType,
3
4
  SyntheticBehaviorEvent,
@@ -19,6 +20,7 @@ import {childUnsetOperationImplementation} from './behavior.operation.child.unse
19
20
  import {decoratorAddOperationImplementation} from './behavior.operation.decorator.add'
20
21
  import {deleteOperationImplementation} from './behavior.operation.delete'
21
22
  import {insertBlockOperationImplementation} from './behavior.operation.insert.block'
23
+ import {insertChildOperationImplementation} from './behavior.operation.insert.child'
22
24
  import {insertTextOperationImplementation} from './behavior.operation.insert.text'
23
25
  import {moveBackwardOperationImplementation} from './behavior.operation.move.backward'
24
26
  import {moveBlockOperationImplementation} from './behavior.operation.move.block'
@@ -65,6 +67,7 @@ const behaviorOperationImplementations: BehaviorOperationImplementations = {
65
67
  'history.redo': historyRedoOperationImplementation,
66
68
  'history.undo': historyUndoOperationImplementation,
67
69
  'insert.block': insertBlockOperationImplementation,
70
+ 'insert.child': insertChildOperationImplementation,
68
71
  'insert.text': insertTextOperationImplementation,
69
72
  'move.backward': moveBackwardOperationImplementation,
70
73
  'move.block': moveBlockOperationImplementation,
@@ -79,133 +82,142 @@ export function performOperation({
79
82
  context: BehaviorOperationImplementationContext
80
83
  operation: BehaviorOperation
81
84
  }) {
82
- try {
83
- switch (operation.type) {
84
- case 'annotation.add': {
85
- behaviorOperationImplementations['annotation.add']({
86
- context,
87
- operation: operation,
88
- })
89
- break
90
- }
91
- case 'annotation.remove': {
92
- behaviorOperationImplementations['annotation.remove']({
93
- context,
94
- operation: operation,
95
- })
96
- break
97
- }
98
- case 'block.set': {
99
- behaviorOperationImplementations['block.set']({
100
- context,
101
- operation: operation,
102
- })
103
- break
104
- }
105
- case 'block.unset': {
106
- behaviorOperationImplementations['block.unset']({
107
- context,
108
- operation: operation,
109
- })
110
- break
111
- }
112
- case 'child.set': {
113
- behaviorOperationImplementations['child.set']({
114
- context,
115
- operation: operation,
116
- })
117
- break
118
- }
119
- case 'child.unset': {
120
- behaviorOperationImplementations['child.unset']({
121
- context,
122
- operation: operation,
123
- })
124
- break
125
- }
126
- case 'decorator.add': {
127
- behaviorOperationImplementations['decorator.add']({
128
- context,
129
- operation: operation,
130
- })
131
- break
132
- }
133
- case 'decorator.remove': {
134
- behaviorOperationImplementations['decorator.remove']({
135
- context,
136
- operation: operation,
137
- })
138
- break
139
- }
140
- case 'delete': {
141
- behaviorOperationImplementations.delete({
142
- context,
143
- operation: operation,
144
- })
145
- break
146
- }
147
- case 'history.redo': {
148
- behaviorOperationImplementations['history.redo']({
149
- context,
150
- operation: operation,
151
- })
152
- break
153
- }
154
- case 'history.undo': {
155
- behaviorOperationImplementations['history.undo']({
156
- context,
157
- operation: operation,
158
- })
159
- break
160
- }
161
- case 'insert.block': {
162
- behaviorOperationImplementations['insert.block']({
163
- context,
164
- operation: operation,
165
- })
166
- break
167
- }
168
- case 'insert.text': {
169
- behaviorOperationImplementations['insert.text']({
170
- context,
171
- operation: operation,
172
- })
173
- break
174
- }
175
- case 'move.backward': {
176
- behaviorOperationImplementations['move.backward']({
177
- context,
178
- operation: operation,
179
- })
180
- break
181
- }
182
- case 'move.block': {
183
- behaviorOperationImplementations['move.block']({
184
- context,
185
- operation: operation,
186
- })
187
- break
188
- }
189
- case 'move.forward': {
190
- behaviorOperationImplementations['move.forward']({
191
- context,
192
- operation: operation,
193
- })
194
- break
195
- }
196
- default: {
197
- behaviorOperationImplementations.select({
198
- context,
199
- operation: operation,
200
- })
201
- break
202
- }
85
+ Editor.withoutNormalizing(operation.editor, () => {
86
+ try {
87
+ switch (operation.type) {
88
+ case 'annotation.add': {
89
+ behaviorOperationImplementations['annotation.add']({
90
+ context,
91
+ operation: operation,
92
+ })
93
+ break
94
+ }
95
+ case 'annotation.remove': {
96
+ behaviorOperationImplementations['annotation.remove']({
97
+ context,
98
+ operation: operation,
99
+ })
100
+ break
101
+ }
102
+ case 'block.set': {
103
+ behaviorOperationImplementations['block.set']({
104
+ context,
105
+ operation: operation,
106
+ })
107
+ break
108
+ }
109
+ case 'block.unset': {
110
+ behaviorOperationImplementations['block.unset']({
111
+ context,
112
+ operation: operation,
113
+ })
114
+ break
115
+ }
116
+ case 'child.set': {
117
+ behaviorOperationImplementations['child.set']({
118
+ context,
119
+ operation: operation,
120
+ })
121
+ break
122
+ }
123
+ case 'child.unset': {
124
+ behaviorOperationImplementations['child.unset']({
125
+ context,
126
+ operation: operation,
127
+ })
128
+ break
129
+ }
130
+ case 'decorator.add': {
131
+ behaviorOperationImplementations['decorator.add']({
132
+ context,
133
+ operation: operation,
134
+ })
135
+ break
136
+ }
137
+ case 'decorator.remove': {
138
+ behaviorOperationImplementations['decorator.remove']({
139
+ context,
140
+ operation: operation,
141
+ })
142
+ break
143
+ }
144
+ case 'delete': {
145
+ behaviorOperationImplementations.delete({
146
+ context,
147
+ operation: operation,
148
+ })
149
+ break
150
+ }
151
+ case 'history.redo': {
152
+ behaviorOperationImplementations['history.redo']({
153
+ context,
154
+ operation: operation,
155
+ })
156
+ break
157
+ }
158
+ case 'history.undo': {
159
+ behaviorOperationImplementations['history.undo']({
160
+ context,
161
+ operation: operation,
162
+ })
163
+ break
164
+ }
165
+ case 'insert.block': {
166
+ behaviorOperationImplementations['insert.block']({
167
+ context,
168
+ operation: operation,
169
+ })
170
+ break
171
+ }
172
+ case 'insert.child': {
173
+ behaviorOperationImplementations['insert.child']({
174
+ context,
175
+ operation: operation,
176
+ })
177
+ break
178
+ }
179
+ case 'insert.text': {
180
+ behaviorOperationImplementations['insert.text']({
181
+ context,
182
+ operation: operation,
183
+ })
184
+ break
185
+ }
186
+ case 'move.backward': {
187
+ behaviorOperationImplementations['move.backward']({
188
+ context,
189
+ operation: operation,
190
+ })
191
+ break
192
+ }
193
+ case 'move.block': {
194
+ behaviorOperationImplementations['move.block']({
195
+ context,
196
+ operation: operation,
197
+ })
198
+ break
199
+ }
200
+ case 'move.forward': {
201
+ behaviorOperationImplementations['move.forward']({
202
+ context,
203
+ operation: operation,
204
+ })
205
+ break
206
+ }
207
+ default: {
208
+ behaviorOperationImplementations.select({
209
+ context,
210
+ operation: operation,
211
+ })
212
+ break
213
+ }
214
+ }
215
+ } catch (error) {
216
+ console.error(
217
+ new Error(
218
+ `Executing "${operation.type}" failed due to: ${error.message}`,
219
+ ),
220
+ )
203
221
  }
204
- } catch (error) {
205
- console.error(
206
- new Error(
207
- `Executing "${operation.type}" failed due to: ${error.message}`,
208
- ),
209
- )
210
- }
222
+ })
211
223
  }
@@ -6,10 +6,16 @@ import {getNextSpan} from './selector.get-next-span'
6
6
  import {getPreviousSpan} from './selector.get-previous-span'
7
7
  import {getSelectedSpans} from './selector.get-selected-spans'
8
8
 
9
- export type MarkState = {
10
- state: 'changed' | 'unchanged'
11
- marks: Array<string>
12
- }
9
+ export type MarkState =
10
+ | {
11
+ state: 'unchanged'
12
+ marks: Array<string>
13
+ }
14
+ | {
15
+ state: 'changed'
16
+ marks: Array<string>
17
+ previousMarks: Array<string>
18
+ }
13
19
 
14
20
  /**
15
21
  * Given that text is inserted at the current position, what marks should
@@ -106,21 +112,25 @@ export const getMarkState: EditorSelector<MarkState | undefined> = (
106
112
  if (previousSpanHasSameMarks) {
107
113
  return {
108
114
  state: 'changed',
115
+ previousMarks: marks,
109
116
  marks: previousSpan?.node.marks ?? [],
110
117
  }
111
118
  } else if (previousSpanHasSameAnnotations) {
112
119
  return {
113
120
  state: 'changed',
121
+ previousMarks: marks,
114
122
  marks: previousSpan?.node.marks ?? [],
115
123
  }
116
124
  } else if (previousSpanHasSameAnnotation) {
117
125
  return {
118
126
  state: 'unchanged',
127
+ previousMarks: marks,
119
128
  marks: focusSpan.node.marks ?? [],
120
129
  }
121
130
  } else if (!previousSpan) {
122
131
  return {
123
132
  state: 'changed',
133
+ previousMarks: marks,
124
134
  marks: [],
125
135
  }
126
136
  }
@@ -135,6 +145,7 @@ export const getMarkState: EditorSelector<MarkState | undefined> = (
135
145
  ) {
136
146
  return {
137
147
  state: 'changed',
148
+ previousMarks: marks,
138
149
  marks: nextSpan?.node.marks ?? [],
139
150
  }
140
151
  }
@@ -142,6 +153,7 @@ export const getMarkState: EditorSelector<MarkState | undefined> = (
142
153
  if (!nextSpan) {
143
154
  return {
144
155
  state: 'changed',
156
+ previousMarks: marks,
145
157
  marks: [],
146
158
  }
147
159
  }
@@ -152,11 +164,13 @@ export const getMarkState: EditorSelector<MarkState | undefined> = (
152
164
  if (previousSpanHasAnnotations) {
153
165
  return {
154
166
  state: 'changed',
167
+ previousMarks: marks,
155
168
  marks: [],
156
169
  }
157
170
  } else {
158
171
  return {
159
172
  state: 'changed',
173
+ previousMarks: marks,
160
174
  marks: (previousSpan?.node.marks ?? []).filter((mark) =>
161
175
  decorators.includes(mark),
162
176
  ),
@@ -166,6 +180,6 @@ export const getMarkState: EditorSelector<MarkState | undefined> = (
166
180
 
167
181
  return {
168
182
  state: 'unchanged',
169
- marks: focusSpan.node.marks ?? [],
183
+ marks,
170
184
  }
171
185
  }
@@ -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
  })
@@ -1,18 +0,0 @@
1
- import {Editor} from 'slate'
2
- import {defaultKeyGenerator} from './key-generator'
3
-
4
- const CURRENT_OPERATION_ID: WeakMap<Editor, string | undefined> = new WeakMap()
5
-
6
- export function withApplyingBehaviorOperations(editor: Editor, fn: () => void) {
7
- CURRENT_OPERATION_ID.set(editor, defaultKeyGenerator())
8
- Editor.withoutNormalizing(editor, fn)
9
- CURRENT_OPERATION_ID.set(editor, undefined)
10
- }
11
-
12
- export function getCurrentOperationId(editor: Editor) {
13
- return CURRENT_OPERATION_ID.get(editor)
14
- }
15
-
16
- export function isApplyingBehaviorOperations(editor: Editor) {
17
- return getCurrentOperationId(editor) !== undefined
18
- }