@portabletext/editor 1.27.0 → 1.30.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 (79) hide show
  1. package/README.md +5 -5
  2. package/lib/_chunks-cjs/behavior.core.cjs +40 -37
  3. package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
  4. package/lib/_chunks-cjs/parse-blocks.cjs +79 -0
  5. package/lib/_chunks-cjs/parse-blocks.cjs.map +1 -0
  6. package/lib/_chunks-cjs/plugin.event-listener.cjs +357 -140
  7. package/lib/_chunks-cjs/plugin.event-listener.cjs.map +1 -1
  8. package/lib/_chunks-cjs/selector.get-selection-start-point.cjs +15 -0
  9. package/lib/_chunks-cjs/selector.get-selection-start-point.cjs.map +1 -0
  10. package/lib/_chunks-cjs/selector.is-at-the-start-of-block.cjs +88 -88
  11. package/lib/_chunks-cjs/selector.is-at-the-start-of-block.cjs.map +1 -1
  12. package/lib/_chunks-es/behavior.core.js +40 -37
  13. package/lib/_chunks-es/behavior.core.js.map +1 -1
  14. package/lib/_chunks-es/parse-blocks.js +80 -0
  15. package/lib/_chunks-es/parse-blocks.js.map +1 -0
  16. package/lib/_chunks-es/plugin.event-listener.js +359 -141
  17. package/lib/_chunks-es/plugin.event-listener.js.map +1 -1
  18. package/lib/_chunks-es/selector.get-selection-start-point.js +16 -0
  19. package/lib/_chunks-es/selector.get-selection-start-point.js.map +1 -0
  20. package/lib/_chunks-es/selector.is-at-the-start-of-block.js +88 -88
  21. package/lib/_chunks-es/selector.is-at-the-start-of-block.js.map +1 -1
  22. package/lib/behaviors/index.d.cts +196 -124
  23. package/lib/behaviors/index.d.ts +196 -124
  24. package/lib/index.cjs +22 -21
  25. package/lib/index.cjs.map +1 -1
  26. package/lib/index.d.cts +505 -0
  27. package/lib/index.d.ts +505 -0
  28. package/lib/index.js +22 -21
  29. package/lib/index.js.map +1 -1
  30. package/lib/plugins/index.cjs +249 -1
  31. package/lib/plugins/index.cjs.map +1 -1
  32. package/lib/plugins/index.d.cts +246 -1
  33. package/lib/plugins/index.d.ts +246 -1
  34. package/lib/plugins/index.js +257 -3
  35. package/lib/plugins/index.js.map +1 -1
  36. package/lib/selectors/index.cjs +42 -3
  37. package/lib/selectors/index.cjs.map +1 -1
  38. package/lib/selectors/index.d.cts +39 -0
  39. package/lib/selectors/index.d.ts +39 -0
  40. package/lib/selectors/index.js +45 -4
  41. package/lib/selectors/index.js.map +1 -1
  42. package/lib/utils/index.cjs +70 -1
  43. package/lib/utils/index.cjs.map +1 -1
  44. package/lib/utils/index.d.cts +168 -2
  45. package/lib/utils/index.d.ts +168 -2
  46. package/lib/utils/index.js +71 -1
  47. package/lib/utils/index.js.map +1 -1
  48. package/package.json +4 -4
  49. package/src/behavior-actions/behavior.action.delete.ts +18 -0
  50. package/src/behavior-actions/behavior.action.insert-break.ts +96 -91
  51. package/src/behavior-actions/behavior.actions.ts +9 -0
  52. package/src/behaviors/_exports/index.ts +1 -0
  53. package/src/behaviors/behavior.core.deserialize.ts +52 -38
  54. package/src/behaviors/behavior.core.ts +4 -11
  55. package/src/behaviors/behavior.types.ts +4 -0
  56. package/src/editor/PortableTextEditor.tsx +308 -1
  57. package/src/editor/components/DefaultObject.tsx +21 -0
  58. package/src/editor/components/Element.tsx +5 -5
  59. package/src/editor/components/Leaf.tsx +1 -6
  60. package/src/internal-utils/__tests__/patchToOperations.test.ts +19 -21
  61. package/src/internal-utils/applyPatch.ts +11 -3
  62. package/src/plugins/index.ts +2 -0
  63. package/src/plugins/plugin.behavior.tsx +22 -0
  64. package/src/plugins/plugin.one-line.tsx +225 -0
  65. package/src/selectors/index.ts +7 -2
  66. package/src/selectors/selector.get-active-annotations.test.ts +122 -0
  67. package/src/selectors/selector.get-active-annotations.ts +30 -0
  68. package/src/selectors/selector.get-selection-end-point.ts +17 -0
  69. package/src/selectors/selector.get-selection-start-point.ts +17 -0
  70. package/src/selectors/selector.get-selection.ts +8 -0
  71. package/src/selectors/selector.get-value.ts +11 -0
  72. package/src/selectors/selector.is-overlapping-selection.ts +46 -0
  73. package/src/utils/index.ts +4 -0
  74. package/src/utils/util.is-span.ts +12 -0
  75. package/src/utils/util.is-text-block.ts +12 -0
  76. package/src/utils/util.merge-text-blocks.ts +36 -0
  77. package/src/utils/util.split-text-block.ts +55 -0
  78. package/src/editor/nodes/DefaultAnnotation.tsx +0 -20
  79. package/src/editor/nodes/DefaultObject.tsx +0 -18
@@ -0,0 +1,225 @@
1
+ import {defineBehavior, raise} from '../behaviors'
2
+ import * as selectors from '../selectors'
3
+ import * as utils from '../utils'
4
+ import {BehaviorPlugin} from './plugin.behavior'
5
+
6
+ const oneLineBehaviors = [
7
+ /**
8
+ * Hitting Enter on an expanded selection should just delete that selection
9
+ * without causing a line break.
10
+ */
11
+ defineBehavior({
12
+ on: 'insert.break',
13
+ guard: ({context}) =>
14
+ context.selection && selectors.isSelectionExpanded({context})
15
+ ? {selection: context.selection}
16
+ : false,
17
+ actions: [(_, {selection}) => [{type: 'delete', selection}]],
18
+ }),
19
+ /**
20
+ * All other cases of `insert.break` should be aborted.
21
+ */
22
+ defineBehavior({
23
+ on: 'insert.break',
24
+ actions: [() => [{type: 'noop'}]],
25
+ }),
26
+ /**
27
+ * `insert.block` `before` or `after` is not allowed in a one-line editor.
28
+ */
29
+ defineBehavior({
30
+ on: 'insert.block',
31
+ guard: ({event}) =>
32
+ event.placement === 'before' || event.placement === 'after',
33
+ actions: [() => [{type: 'noop'}]],
34
+ }),
35
+ /**
36
+ * Other cases of `insert.block` are allowed.
37
+ *
38
+ * If a text block is inserted and the focus block is fully selected, then
39
+ * the focus block can be replaced with the inserted block.
40
+ */
41
+ defineBehavior({
42
+ on: 'insert.block',
43
+ guard: ({context, event}) => {
44
+ const focusTextBlock = selectors.getFocusTextBlock({context})
45
+ const selectionStartPoint = selectors.getSelectionStartPoint({context})
46
+ const selectionEndPoint = selectors.getSelectionEndPoint({context})
47
+
48
+ if (
49
+ !focusTextBlock ||
50
+ !utils.isTextBlock(context, event.block) ||
51
+ !selectionStartPoint ||
52
+ !selectionEndPoint
53
+ ) {
54
+ return false
55
+ }
56
+
57
+ const blockStartPoint = utils.getBlockStartPoint(focusTextBlock)
58
+ const blockEndPoint = utils.getBlockEndPoint(focusTextBlock)
59
+ const newFocus = utils.getBlockEndPoint({
60
+ node: event.block,
61
+ path: [{_key: event.block._key}],
62
+ })
63
+
64
+ if (
65
+ utils.isEqualSelectionPoints(blockStartPoint, selectionStartPoint) &&
66
+ utils.isEqualSelectionPoints(blockEndPoint, selectionEndPoint)
67
+ ) {
68
+ return {focusTextBlock, newFocus}
69
+ }
70
+
71
+ return false
72
+ },
73
+ actions: [
74
+ ({event}, {focusTextBlock, newFocus}) => [
75
+ {type: 'delete.block', blockPath: focusTextBlock.path},
76
+ {type: 'insert.block', block: event.block, placement: 'auto'},
77
+ {
78
+ type: 'select',
79
+ selection: {
80
+ anchor: newFocus,
81
+ focus: newFocus,
82
+ },
83
+ },
84
+ ],
85
+ ],
86
+ }),
87
+ /**
88
+ * An ordinary `insert.block` is acceptable if it's a text block. In that
89
+ * case it will get merged into the existing text block.
90
+ */
91
+ defineBehavior({
92
+ on: 'insert.block',
93
+ guard: ({context, event}) => {
94
+ const focusTextBlock = selectors.getFocusTextBlock({context})
95
+ const selectionStartPoint = selectors.getSelectionStartPoint({context})
96
+ const selectionEndPoint = selectors.getSelectionEndPoint({context})
97
+
98
+ if (
99
+ !focusTextBlock ||
100
+ !utils.isTextBlock(context, event.block) ||
101
+ !selectionStartPoint ||
102
+ !selectionEndPoint
103
+ ) {
104
+ return false
105
+ }
106
+
107
+ const blockBeforeStartPoint = utils.splitTextBlock({
108
+ context,
109
+ block: focusTextBlock.node,
110
+ point: selectionStartPoint,
111
+ })?.before
112
+ const blockAfterEndPoint = utils.splitTextBlock({
113
+ context,
114
+ block: focusTextBlock.node,
115
+ point: selectionEndPoint,
116
+ })?.after
117
+
118
+ if (!blockBeforeStartPoint || !blockAfterEndPoint) {
119
+ return false
120
+ }
121
+
122
+ const targetBlock = utils.mergeTextBlocks({
123
+ context,
124
+ targetBlock: blockBeforeStartPoint,
125
+ incomingBlock: event.block,
126
+ })
127
+
128
+ const newFocus = utils.getBlockEndPoint({
129
+ node: targetBlock,
130
+ path: [{_key: targetBlock._key}],
131
+ })
132
+
133
+ const mergedBlock = utils.mergeTextBlocks({
134
+ context,
135
+ targetBlock,
136
+ incomingBlock: blockAfterEndPoint,
137
+ })
138
+
139
+ return {focusTextBlock, mergedBlock, newFocus}
140
+ },
141
+ actions: [
142
+ (_, {focusTextBlock, mergedBlock, newFocus}) => [
143
+ {type: 'delete.block', blockPath: focusTextBlock.path},
144
+ {type: 'insert.block', block: mergedBlock, placement: 'auto'},
145
+ {
146
+ type: 'select',
147
+ selection: {
148
+ anchor: newFocus,
149
+ focus: newFocus,
150
+ },
151
+ },
152
+ ],
153
+ ],
154
+ }),
155
+ /**
156
+ * Fallback Behavior to avoid `insert.block` in case the Behaviors above all
157
+ * end up with a falsy guard.
158
+ */
159
+ defineBehavior({
160
+ on: 'insert.block',
161
+ actions: [() => [{type: 'noop'}]],
162
+ }),
163
+ /**
164
+ * If multiple blocks are inserted, then the non-text blocks are filtered out
165
+ * and the text blocks are merged into one block
166
+ */
167
+ defineBehavior({
168
+ on: 'insert.blocks',
169
+ guard: ({context, event}) => {
170
+ return event.blocks
171
+ .filter((block) => utils.isTextBlock(context, block))
172
+ .reduce((targetBlock, incomingBlock) => {
173
+ return utils.mergeTextBlocks({
174
+ context,
175
+ targetBlock,
176
+ incomingBlock,
177
+ })
178
+ })
179
+ },
180
+ actions: [
181
+ // `insert.block` is raised so the Behavior above can handle the
182
+ // insertion
183
+ (_, block) => [raise({type: 'insert.block', block, placement: 'auto'})],
184
+ ],
185
+ }),
186
+ /**
187
+ * Block objects do not fit in a one-line editor
188
+ */
189
+ defineBehavior({
190
+ on: 'insert.block object',
191
+ actions: [() => [{type: 'noop'}]],
192
+ }),
193
+ /**
194
+ * `insert.text block` is raised as an `insert.block` so it can be handled
195
+ * by the Behaviors above.
196
+ */
197
+ defineBehavior({
198
+ on: 'insert.text block',
199
+ actions: [
200
+ ({context, event}) => [
201
+ raise({
202
+ type: 'insert.block',
203
+ block: {
204
+ _key: context.keyGenerator(),
205
+ _type: context.schema.block.name,
206
+ children: event.textBlock?.children ?? [],
207
+ },
208
+ placement: event.placement,
209
+ }),
210
+ ],
211
+ ],
212
+ }),
213
+ ]
214
+
215
+ /**
216
+ * @beta
217
+ * Restrict the editor to one line. The plugin takes care of blocking
218
+ * `insert.break` events and smart handling of other `insert.*` events.
219
+ *
220
+ * Place it with as high priority as possible to make sure other plugins don't
221
+ * overwrite `insert.*` events before this plugin gets a chance to do so.
222
+ */
223
+ export function OneLinePlugin() {
224
+ return <BehaviorPlugin behaviors={oneLineBehaviors} />
225
+ }
@@ -1,3 +1,4 @@
1
+ export type {EditorSchema} from '../editor/define-schema'
1
2
  export type {EditorSelector} from '../editor/editor-selector'
2
3
  export type {EditorContext, EditorSnapshot} from '../editor/editor-snapshot'
3
4
  export type {
@@ -5,20 +6,24 @@ export type {
5
6
  EditorSelectionPoint,
6
7
  PortableTextMemberSchemaTypes,
7
8
  } from '../types/editor'
8
-
9
- export type {EditorSchema} from '../editor/define-schema'
9
+ export {getActiveAnnotations} from './selector.get-active-annotations'
10
10
  export {getActiveListItem} from './selector.get-active-list-item'
11
11
  export {getActiveStyle} from './selector.get-active-style'
12
12
  export {getSelectedSlice} from './selector.get-selected-slice'
13
13
  export {getSelectedSpans} from './selector.get-selected-spans'
14
+ export {getSelection} from './selector.get-selection'
15
+ export {getSelectionEndPoint} from './selector.get-selection-end-point'
16
+ export {getSelectionStartPoint} from './selector.get-selection-start-point'
14
17
  export {getSelectionText} from './selector.get-selection-text'
15
18
  export {getBlockTextBefore} from './selector.get-text-before'
19
+ export {getValue} from './selector.get-value'
16
20
  export {isActiveAnnotation} from './selector.is-active-annotation'
17
21
  export {isActiveDecorator} from './selector.is-active-decorator'
18
22
  export {isActiveListItem} from './selector.is-active-list-item'
19
23
  export {isActiveStyle} from './selector.is-active-style'
20
24
  export {isAtTheEndOfBlock} from './selector.is-at-the-end-of-block'
21
25
  export {isAtTheStartOfBlock} from './selector.is-at-the-start-of-block'
26
+ export {isOverlappingSelection} from './selector.is-overlapping-selection'
22
27
  export {isPointAfterSelection} from './selector.is-point-after-selection'
23
28
  export {isPointBeforeSelection} from './selector.is-point-before-selection'
24
29
  export {isSelectionCollapsed} from './selector.is-selection-collapsed'
@@ -0,0 +1,122 @@
1
+ import type {PortableTextBlock} from '@sanity/types'
2
+ import {expect, test} from 'vitest'
3
+ import type {EditorSchema} from '../editor/define-schema'
4
+ import type {EditorSnapshot} from '../editor/editor-snapshot'
5
+ import {createTestKeyGenerator} from '../internal-utils/test-key-generator'
6
+ import type {EditorSelection} from '../utils'
7
+ import {getActiveAnnotations} from './selector.get-active-annotations'
8
+
9
+ function snapshot(
10
+ value: Array<PortableTextBlock>,
11
+ selection: EditorSelection,
12
+ ): EditorSnapshot {
13
+ return {
14
+ context: {
15
+ converters: [],
16
+ schema: {} as EditorSchema,
17
+ keyGenerator: createTestKeyGenerator(),
18
+ activeDecorators: [],
19
+ value,
20
+ selection,
21
+ },
22
+ }
23
+ }
24
+
25
+ const link = {
26
+ _key: 'k4',
27
+ _type: 'link',
28
+ href: 'https://example.com',
29
+ }
30
+
31
+ const comment = {
32
+ _key: 'k5',
33
+ _type: 'comment',
34
+ comment: 'Consider rewriting this',
35
+ }
36
+
37
+ const block = {
38
+ _type: 'block',
39
+ _key: 'k0',
40
+ children: [
41
+ {
42
+ _key: 'k1',
43
+ _type: 'span',
44
+ text: 'foo',
45
+ marks: ['strong'],
46
+ },
47
+ {
48
+ _type: 'span',
49
+ _key: 'k2',
50
+ text: 'bar',
51
+ marks: [link._key, comment._key, 'strong'],
52
+ },
53
+ {
54
+ _key: 'k3',
55
+ _type: 'span',
56
+ text: 'baz',
57
+ marks: [comment._key, 'strong'],
58
+ },
59
+ ],
60
+ markDefs: [link, comment],
61
+ }
62
+
63
+ test(getActiveAnnotations.name, () => {
64
+ expect(getActiveAnnotations(snapshot([], null))).toEqual([])
65
+ expect(getActiveAnnotations(snapshot([block], null))).toEqual([])
66
+ expect(
67
+ getActiveAnnotations(
68
+ snapshot([block], {
69
+ anchor: {
70
+ path: [{_key: 'k0'}, 'children', {_key: 'k1'}],
71
+ offset: 0,
72
+ },
73
+ focus: {
74
+ path: [{_key: 'k0'}, 'children', {_key: 'k1'}],
75
+ offset: 3,
76
+ },
77
+ }),
78
+ ),
79
+ ).toEqual([])
80
+ expect(
81
+ getActiveAnnotations(
82
+ snapshot([block], {
83
+ anchor: {
84
+ path: [{_key: 'k0'}, 'children', {_key: 'k1'}],
85
+ offset: 0,
86
+ },
87
+ focus: {
88
+ path: [{_key: 'k0'}, 'children', {_key: 'k2'}],
89
+ offset: 3,
90
+ },
91
+ }),
92
+ ),
93
+ ).toEqual([link, comment])
94
+ expect(
95
+ getActiveAnnotations(
96
+ snapshot([block], {
97
+ anchor: {
98
+ path: [{_key: 'k0'}, 'children', {_key: 'k1'}],
99
+ offset: 0,
100
+ },
101
+ focus: {
102
+ path: [{_key: 'k0'}, 'children', {_key: 'k3'}],
103
+ offset: 3,
104
+ },
105
+ }),
106
+ ),
107
+ ).toEqual([link, comment])
108
+ expect(
109
+ getActiveAnnotations(
110
+ snapshot([block], {
111
+ anchor: {
112
+ path: [{_key: 'k0'}, 'children', {_key: 'k3'}],
113
+ offset: 0,
114
+ },
115
+ focus: {
116
+ path: [{_key: 'k0'}, 'children', {_key: 'k3'}],
117
+ offset: 3,
118
+ },
119
+ }),
120
+ ),
121
+ ).toEqual([comment])
122
+ })
@@ -0,0 +1,30 @@
1
+ import {isPortableTextTextBlock, type PortableTextObject} from '@sanity/types'
2
+ import type {EditorSelector} from '../editor/editor-selector'
3
+ import {getSelectedSpans} from './selector.get-selected-spans'
4
+ import {getSelectedBlocks} from './selectors'
5
+
6
+ /**
7
+ * @public
8
+ */
9
+ export const getActiveAnnotations: EditorSelector<Array<PortableTextObject>> = (
10
+ snapshot,
11
+ ) => {
12
+ if (!snapshot.context.selection) {
13
+ return []
14
+ }
15
+
16
+ const selectedBlocks = getSelectedBlocks(snapshot)
17
+ const selectedSpans = getSelectedSpans(snapshot)
18
+
19
+ if (selectedSpans.length === 0) {
20
+ return []
21
+ }
22
+
23
+ const selectionMarkDefs = selectedBlocks.flatMap((block) =>
24
+ isPortableTextTextBlock(block.node) ? (block.node.markDefs ?? []) : [],
25
+ )
26
+
27
+ return selectionMarkDefs.filter((markDef) =>
28
+ selectedSpans.some((span) => span.node.marks?.includes(markDef._key)),
29
+ )
30
+ }
@@ -0,0 +1,17 @@
1
+ import type {EditorSelector} from '../editor/editor-selector'
2
+ import type {EditorSelectionPoint} from '../utils'
3
+
4
+ /**
5
+ * @public
6
+ */
7
+ export const getSelectionEndPoint: EditorSelector<
8
+ EditorSelectionPoint | undefined
9
+ > = ({context}) => {
10
+ if (!context.selection) {
11
+ return undefined
12
+ }
13
+
14
+ return context.selection.backward
15
+ ? context.selection.anchor
16
+ : context.selection.focus
17
+ }
@@ -0,0 +1,17 @@
1
+ import type {EditorSelector} from '../editor/editor-selector'
2
+ import type {EditorSelectionPoint} from '../utils'
3
+
4
+ /**
5
+ * @public
6
+ */
7
+ export const getSelectionStartPoint: EditorSelector<
8
+ EditorSelectionPoint | undefined
9
+ > = ({context}) => {
10
+ if (!context.selection) {
11
+ return undefined
12
+ }
13
+
14
+ return context.selection.backward
15
+ ? context.selection.focus
16
+ : context.selection.anchor
17
+ }
@@ -0,0 +1,8 @@
1
+ import type {EditorSelection, EditorSelector} from './_exports'
2
+
3
+ /**
4
+ * @public
5
+ */
6
+ export const getSelection: EditorSelector<EditorSelection> = ({context}) => {
7
+ return context.selection
8
+ }
@@ -0,0 +1,11 @@
1
+ import type {PortableTextBlock} from '@sanity/types'
2
+ import type {EditorSelector} from './_exports'
3
+
4
+ /**
5
+ * @public
6
+ */
7
+ export const getValue: EditorSelector<Array<PortableTextBlock>> = ({
8
+ context,
9
+ }) => {
10
+ return context.value
11
+ }
@@ -0,0 +1,46 @@
1
+ import type {EditorSelection} from '../types/editor'
2
+ import type {EditorSelector} from './../editor/editor-selector'
3
+ import {getSelectionEndPoint} from './selector.get-selection-end-point'
4
+ import {getSelectionStartPoint} from './selector.get-selection-start-point'
5
+ import {isPointAfterSelection} from './selector.is-point-after-selection'
6
+ import {isPointBeforeSelection} from './selector.is-point-before-selection'
7
+
8
+ /**
9
+ * @public
10
+ */
11
+ export function isOverlappingSelection(
12
+ selection: EditorSelection,
13
+ ): EditorSelector<boolean> {
14
+ return ({context}) => {
15
+ if (!selection || !context.selection) {
16
+ return false
17
+ }
18
+
19
+ const selectionStartPoint = getSelectionStartPoint({
20
+ context: {
21
+ ...context,
22
+ selection,
23
+ },
24
+ })
25
+ const selectionEndPoint = getSelectionEndPoint({
26
+ context: {
27
+ ...context,
28
+ selection,
29
+ },
30
+ })
31
+
32
+ if (!selectionStartPoint || !selectionEndPoint) {
33
+ return false
34
+ }
35
+
36
+ if (!isPointAfterSelection(selectionStartPoint)({context})) {
37
+ return false
38
+ }
39
+
40
+ if (!isPointBeforeSelection(selectionEndPoint)({context})) {
41
+ return false
42
+ }
43
+
44
+ return true
45
+ }
46
+ }
@@ -10,5 +10,9 @@ export {getTextBlockText} from './util.get-text-block-text'
10
10
  export {isEmptyTextBlock} from './util.is-empty-text-block'
11
11
  export {isEqualSelectionPoints} from './util.is-equal-selection-points'
12
12
  export {isKeyedSegment} from './util.is-keyed-segment'
13
+ export {isSpan} from './util.is-span'
14
+ export {isTextBlock} from './util.is-text-block'
15
+ export {mergeTextBlocks} from './util.merge-text-blocks'
13
16
  export {reverseSelection} from './util.reverse-selection'
14
17
  export {sliceBlocks} from './util.slice-blocks'
18
+ export {splitTextBlock} from './util.split-text-block'
@@ -0,0 +1,12 @@
1
+ import type {PortableTextChild, PortableTextSpan} from '@sanity/types'
2
+ import type {EditorContext} from '../selectors'
3
+
4
+ /**
5
+ * @public
6
+ */
7
+ export function isSpan(
8
+ context: Pick<EditorContext, 'schema'>,
9
+ child: PortableTextChild,
10
+ ): child is PortableTextSpan {
11
+ return child._type === context.schema.span.name
12
+ }
@@ -0,0 +1,12 @@
1
+ import type {PortableTextBlock, PortableTextTextBlock} from '@sanity/types'
2
+ import type {EditorContext} from '../selectors'
3
+
4
+ /**
5
+ * @public
6
+ */
7
+ export function isTextBlock(
8
+ context: Pick<EditorContext, 'schema'>,
9
+ block: PortableTextBlock,
10
+ ): block is PortableTextTextBlock {
11
+ return block._type === context.schema.block.name
12
+ }
@@ -0,0 +1,36 @@
1
+ import type {PortableTextTextBlock} from '@sanity/types'
2
+ import {parseBlock} from '../internal-utils/parse-blocks'
3
+ import type {EditorContext} from '../selectors'
4
+ import {isTextBlock} from './util.is-text-block'
5
+
6
+ /**
7
+ * @beta
8
+ */
9
+ export function mergeTextBlocks({
10
+ context,
11
+ targetBlock,
12
+ incomingBlock,
13
+ }: {
14
+ context: Pick<EditorContext, 'keyGenerator' | 'schema'>
15
+ targetBlock: PortableTextTextBlock
16
+ incomingBlock: PortableTextTextBlock
17
+ }) {
18
+ const parsedIncomingBlock = parseBlock({
19
+ context,
20
+ block: incomingBlock,
21
+ options: {refreshKeys: true},
22
+ })
23
+
24
+ if (!parsedIncomingBlock || !isTextBlock(context, parsedIncomingBlock)) {
25
+ return targetBlock
26
+ }
27
+
28
+ return {
29
+ ...targetBlock,
30
+ children: [...targetBlock.children, ...parsedIncomingBlock.children],
31
+ markDefs: [
32
+ ...(targetBlock.markDefs ?? []),
33
+ ...(parsedIncomingBlock.markDefs ?? []),
34
+ ],
35
+ }
36
+ }
@@ -0,0 +1,55 @@
1
+ import type {PortableTextTextBlock} from '@sanity/types'
2
+ import {isTextBlock, sliceBlocks, type EditorSelectionPoint} from '.'
3
+ import type {EditorContext} from '../selectors'
4
+ import {isSpan} from './util.is-span'
5
+
6
+ /**
7
+ * @beta
8
+ */
9
+ export function splitTextBlock({
10
+ context,
11
+ block,
12
+ point,
13
+ }: {
14
+ context: Pick<EditorContext, 'schema'>
15
+ block: PortableTextTextBlock
16
+ point: EditorSelectionPoint
17
+ }): {before: PortableTextTextBlock; after: PortableTextTextBlock} | undefined {
18
+ const firstChild = block.children.at(0)
19
+ const lastChild = block.children.at(block.children.length - 1)
20
+
21
+ if (!firstChild || !lastChild) {
22
+ return undefined
23
+ }
24
+
25
+ const before = sliceBlocks({
26
+ blocks: [block],
27
+ selection: {
28
+ anchor: {
29
+ path: [{_key: block._key}, 'children', {_key: firstChild._key}],
30
+ offset: 0,
31
+ },
32
+ focus: point,
33
+ },
34
+ }).at(0)
35
+ const after = sliceBlocks({
36
+ blocks: [block],
37
+ selection: {
38
+ anchor: point,
39
+ focus: {
40
+ path: [{_key: block._key}, 'children', {_key: lastChild._key}],
41
+ offset: isSpan(context, lastChild) ? lastChild.text.length : 0,
42
+ },
43
+ },
44
+ }).at(0)
45
+
46
+ if (!before || !after) {
47
+ return undefined
48
+ }
49
+
50
+ if (!isTextBlock(context, before) || !isTextBlock(context, after)) {
51
+ return undefined
52
+ }
53
+
54
+ return {before, after}
55
+ }
@@ -1,20 +0,0 @@
1
- import type {PortableTextObject} from '@sanity/types'
2
- import {useCallback, type ReactNode} from 'react'
3
-
4
- type Props = {
5
- annotation: PortableTextObject
6
- children: ReactNode
7
- }
8
- export function DefaultAnnotation(props: Props) {
9
- const handleClick = useCallback(
10
- () => alert(JSON.stringify(props.annotation)),
11
- [props.annotation],
12
- )
13
- return (
14
- <span style={{color: 'blue'}} onClick={handleClick}>
15
- {props.children}
16
- </span>
17
- )
18
- }
19
-
20
- DefaultAnnotation.displayName = 'DefaultAnnotation'
@@ -1,18 +0,0 @@
1
- import type {PortableTextBlock, PortableTextChild} from '@sanity/types'
2
- import type {JSX} from 'react'
3
-
4
- type Props = {
5
- value: PortableTextBlock | PortableTextChild
6
- }
7
-
8
- const DefaultObject = (props: Props): JSX.Element => {
9
- return (
10
- <div style={{userSelect: 'none'}}>
11
- [{props.value._type}: {props.value._key}]
12
- </div>
13
- )
14
- }
15
-
16
- DefaultObject.displayName = 'DefaultObject'
17
-
18
- export default DefaultObject