@portabletext/editor 1.28.0 → 1.30.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 (61) hide show
  1. package/lib/_chunks-cjs/behavior.core.cjs +40 -37
  2. package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
  3. package/lib/_chunks-cjs/parse-blocks.cjs +79 -0
  4. package/lib/_chunks-cjs/parse-blocks.cjs.map +1 -0
  5. package/lib/_chunks-cjs/plugin.event-listener.cjs +55 -97
  6. package/lib/_chunks-cjs/plugin.event-listener.cjs.map +1 -1
  7. package/lib/_chunks-cjs/selector.get-selection-start-point.cjs +15 -0
  8. package/lib/_chunks-cjs/selector.get-selection-start-point.cjs.map +1 -0
  9. package/lib/_chunks-es/behavior.core.js +40 -37
  10. package/lib/_chunks-es/behavior.core.js.map +1 -1
  11. package/lib/_chunks-es/parse-blocks.js +80 -0
  12. package/lib/_chunks-es/parse-blocks.js.map +1 -0
  13. package/lib/_chunks-es/plugin.event-listener.js +57 -98
  14. package/lib/_chunks-es/plugin.event-listener.js.map +1 -1
  15. package/lib/_chunks-es/selector.get-selection-start-point.js +16 -0
  16. package/lib/_chunks-es/selector.get-selection-start-point.js.map +1 -0
  17. package/lib/behaviors/index.d.cts +196 -124
  18. package/lib/behaviors/index.d.ts +196 -124
  19. package/lib/index.d.cts +248 -0
  20. package/lib/index.d.ts +248 -0
  21. package/lib/plugins/index.cjs +249 -1
  22. package/lib/plugins/index.cjs.map +1 -1
  23. package/lib/plugins/index.d.cts +246 -1
  24. package/lib/plugins/index.d.ts +246 -1
  25. package/lib/plugins/index.js +257 -3
  26. package/lib/plugins/index.js.map +1 -1
  27. package/lib/selectors/index.cjs +28 -1
  28. package/lib/selectors/index.cjs.map +1 -1
  29. package/lib/selectors/index.d.cts +21 -0
  30. package/lib/selectors/index.d.ts +21 -0
  31. package/lib/selectors/index.js +28 -0
  32. package/lib/selectors/index.js.map +1 -1
  33. package/lib/utils/index.cjs +70 -1
  34. package/lib/utils/index.cjs.map +1 -1
  35. package/lib/utils/index.d.cts +168 -2
  36. package/lib/utils/index.d.ts +168 -2
  37. package/lib/utils/index.js +71 -1
  38. package/lib/utils/index.js.map +1 -1
  39. package/package.json +5 -5
  40. package/src/behavior-actions/behavior.action.delete.ts +18 -0
  41. package/src/behavior-actions/behavior.action.insert-break.ts +3 -8
  42. package/src/behavior-actions/behavior.actions.ts +9 -0
  43. package/src/behaviors/_exports/index.ts +1 -0
  44. package/src/behaviors/behavior.core.deserialize.ts +52 -38
  45. package/src/behaviors/behavior.core.ts +4 -11
  46. package/src/behaviors/behavior.types.ts +4 -0
  47. package/src/editor/PortableTextEditor.tsx +20 -0
  48. package/src/internal-utils/__tests__/patchToOperations.test.ts +19 -21
  49. package/src/internal-utils/applyPatch.ts +11 -3
  50. package/src/plugins/index.ts +2 -0
  51. package/src/plugins/plugin.behavior.tsx +22 -0
  52. package/src/plugins/plugin.one-line.tsx +225 -0
  53. package/src/selectors/index.ts +3 -0
  54. package/src/selectors/selector.get-selection-end-point.ts +17 -0
  55. package/src/selectors/selector.get-selection-start-point.ts +17 -0
  56. package/src/selectors/selector.is-overlapping-selection.ts +46 -0
  57. package/src/utils/index.ts +4 -0
  58. package/src/utils/util.is-span.ts +12 -0
  59. package/src/utils/util.is-text-block.ts +12 -0
  60. package/src/utils/util.merge-text-blocks.ts +36 -0
  61. package/src/utils/util.split-text-block.ts +55 -0
@@ -31,6 +31,7 @@ import {insertBlock} from './behavior.action-utils.insert-block'
31
31
  import {blockSetBehaviorActionImplementation} from './behavior.action.block.set'
32
32
  import {blockUnsetBehaviorActionImplementation} from './behavior.action.block.unset'
33
33
  import {dataTransferSetActionImplementation} from './behavior.action.data-transfer-set'
34
+ import {deleteActionImplementation} from './behavior.action.delete'
34
35
  import {insertBlockObjectActionImplementation} from './behavior.action.insert-block-object'
35
36
  import {insertBlocksActionImplementation} from './behavior.action.insert-blocks'
36
37
  import {
@@ -89,6 +90,7 @@ const behaviorActionImplementations: BehaviorActionImplementations = {
89
90
  'focus': ({action}) => {
90
91
  ReactEditor.focus(action.editor)
91
92
  },
93
+ 'delete': deleteActionImplementation,
92
94
  'delete.backward': ({action}) => {
93
95
  deleteBackward(action.editor, action.unit)
94
96
  },
@@ -412,6 +414,13 @@ function performDefaultAction({
412
414
  })
413
415
  break
414
416
  }
417
+ case 'delete': {
418
+ behaviorActionImplementations.delete({
419
+ context,
420
+ action,
421
+ })
422
+ break
423
+ }
415
424
  case 'delete.backward': {
416
425
  behaviorActionImplementations['delete.backward']({
417
426
  context,
@@ -0,0 +1 @@
1
+ export * from '../index'
@@ -1,46 +1,60 @@
1
1
  import {defineBehavior, raise} from './behavior.types'
2
2
 
3
- export const coreDeserializeBehavior = defineBehavior({
4
- on: 'deserialize',
5
- guard: ({context, event}) => {
6
- const deserializeEvents = context.converters.flatMap((converter) => {
7
- const data = event.dataTransfer.getData(converter.mimeType)
3
+ export const coreDeserializeBehaviors = {
4
+ 'deserialize': defineBehavior({
5
+ on: 'deserialize',
6
+ guard: ({context, event}) => {
7
+ const deserializeEvents = context.converters.flatMap((converter) => {
8
+ const data = event.dataTransfer.getData(converter.mimeType)
8
9
 
9
- if (!data) {
10
- return []
11
- }
10
+ if (!data) {
11
+ return []
12
+ }
12
13
 
13
- return [
14
- converter.deserialize({context, event: {type: 'deserialize', data}}),
15
- ]
16
- })
14
+ return [
15
+ converter.deserialize({context, event: {type: 'deserialize', data}}),
16
+ ]
17
+ })
17
18
 
18
- const firstSuccess = deserializeEvents.find(
19
- (deserializeEvent) => deserializeEvent.type === 'deserialization.success',
20
- )
19
+ const firstSuccess = deserializeEvents.find(
20
+ (deserializeEvent) =>
21
+ deserializeEvent.type === 'deserialization.success',
22
+ )
21
23
 
22
- if (!firstSuccess) {
23
- return {
24
- type: 'deserialization.failure',
25
- mimeType: '*/*',
26
- reason: deserializeEvents
27
- .map((deserializeEvent) =>
28
- deserializeEvent.type === 'deserialization.failure'
29
- ? deserializeEvent.reason
30
- : '',
31
- )
32
- .join(', '),
33
- } as const
34
- }
24
+ if (!firstSuccess) {
25
+ return {
26
+ type: 'deserialization.failure',
27
+ mimeType: '*/*',
28
+ reason: deserializeEvents
29
+ .map((deserializeEvent) =>
30
+ deserializeEvent.type === 'deserialization.failure'
31
+ ? deserializeEvent.reason
32
+ : '',
33
+ )
34
+ .join(', '),
35
+ } as const
36
+ }
35
37
 
36
- return firstSuccess
37
- },
38
- actions: [
39
- ({event}, deserializeEvent) => [
40
- raise({
41
- ...deserializeEvent,
42
- dataTransfer: event.dataTransfer,
43
- }),
38
+ return firstSuccess
39
+ },
40
+ actions: [
41
+ ({event}, deserializeEvent) => [
42
+ raise({
43
+ ...deserializeEvent,
44
+ dataTransfer: event.dataTransfer,
45
+ }),
46
+ ],
47
+ ],
48
+ }),
49
+ 'deserialization.success': defineBehavior({
50
+ on: 'deserialization.success',
51
+ actions: [
52
+ ({event}) => [
53
+ raise({
54
+ type: 'insert.blocks',
55
+ blocks: event.data,
56
+ }),
57
+ ],
44
58
  ],
45
- ],
46
- })
59
+ }),
60
+ }
@@ -1,23 +1,16 @@
1
1
  import {coreAnnotationBehaviors} from './behavior.core.annotations'
2
2
  import {coreBlockObjectBehaviors} from './behavior.core.block-objects'
3
3
  import {coreDecoratorBehaviors} from './behavior.core.decorators'
4
- import {coreDeserializeBehavior} from './behavior.core.deserialize'
4
+ import {coreDeserializeBehaviors} from './behavior.core.deserialize'
5
5
  import {coreInsertBreakBehaviors} from './behavior.core.insert-break'
6
6
  import {coreListBehaviors} from './behavior.core.lists'
7
7
  import {coreSerializeBehaviors} from './behavior.core.serialize'
8
8
  import {coreStyleBehaviors} from './behavior.core.style'
9
- import {defineBehavior, raise} from './behavior.types'
10
-
11
- const softReturn = defineBehavior({
12
- on: 'insert.soft break',
13
- actions: [() => [raise({type: 'insert.text', text: '\n'})]],
14
- })
15
9
 
16
10
  /**
17
11
  * @beta
18
12
  */
19
13
  export const coreBehaviors = [
20
- softReturn,
21
14
  coreAnnotationBehaviors.toggleAnnotationOff,
22
15
  coreAnnotationBehaviors.toggleAnnotationOn,
23
16
  coreDecoratorBehaviors.toggleDecoratorOff,
@@ -26,7 +19,8 @@ export const coreBehaviors = [
26
19
  coreDecoratorBehaviors.emShortcut,
27
20
  coreDecoratorBehaviors.underlineShortcut,
28
21
  coreDecoratorBehaviors.codeShortcut,
29
- coreDeserializeBehavior,
22
+ coreDeserializeBehaviors.deserialize,
23
+ coreDeserializeBehaviors['deserialization.success'],
30
24
  coreBlockObjectBehaviors.arrowDownOnLonelyBlockObject,
31
25
  coreBlockObjectBehaviors.arrowUpOnLonelyBlockObject,
32
26
  coreBlockObjectBehaviors.breakingBlockObject,
@@ -51,10 +45,9 @@ export const coreBehaviors = [
51
45
  * @beta
52
46
  */
53
47
  export const coreBehavior = {
54
- softReturn,
55
48
  annotation: coreAnnotationBehaviors,
56
49
  decorators: coreDecoratorBehaviors,
57
- deserialize: coreDeserializeBehavior,
50
+ deserialize: coreDeserializeBehaviors,
58
51
  blockObjects: coreBlockObjectBehaviors,
59
52
  insertBreak: coreInsertBreakBehaviors,
60
53
  lists: coreListBehaviors,
@@ -66,6 +66,10 @@ export type SyntheticBehaviorEvent =
66
66
  type: 'decorator.toggle'
67
67
  decorator: string
68
68
  }
69
+ | {
70
+ type: 'delete'
71
+ selection: NonNullable<EditorSelection>
72
+ }
69
73
  | {
70
74
  type: 'delete.backward'
71
75
  unit: TextUnit
@@ -671,6 +671,16 @@ export class PortableTextEditor extends Component<
671
671
  editor.editable?.toggleMark(mark)
672
672
  }
673
673
 
674
+ /**
675
+ * @deprecated
676
+ * Use built-in selectors or write your own: https://www.portabletext.org/reference/selectors/
677
+ *
678
+ * ```
679
+ * import * as selectors from '@portabletext/editor/selectors'
680
+ * const editor = useEditor()
681
+ * const selectedSlice = useEditorSelector(editor, selectors.getSelectedSlice)
682
+ * ```
683
+ */
674
684
  static getFragment = (
675
685
  editor: PortableTextEditor,
676
686
  ): PortableTextBlock[] | undefined => {
@@ -688,6 +698,16 @@ export class PortableTextEditor extends Component<
688
698
  editor.editable?.redo()
689
699
  }
690
700
 
701
+ /**
702
+ * @deprecated
703
+ * Use built-in selectors or write your own: https://www.portabletext.org/reference/selectors/
704
+ *
705
+ * ```
706
+ * import * as selectors from '@portabletext/editor/selectors'
707
+ * const editor = useEditor()
708
+ * const isOverlapping = useEditorSelector(editor, selectors.isOverlappingSelection(selectionB))
709
+ * ```
710
+ */
691
711
  static isSelectionsOverlapping = (
692
712
  editor: PortableTextEditor,
693
713
  selectionA: EditorSelection,
@@ -75,29 +75,27 @@ describe('operationToPatches', () => {
75
75
  patches.forEach((p) => {
76
76
  patchToOperations(editor, p)
77
77
  })
78
- expect(editor.children).toMatchInlineSnapshot(`
79
- [
80
- {
81
- "__inline": false,
82
- "_key": "c01739b0d03b",
83
- "_type": "image",
84
- "children": [
85
- {
86
- "_key": "${VOID_CHILD_KEY}",
87
- "_type": "span",
88
- "marks": [],
89
- "text": "",
90
- },
91
- ],
92
- "value": {
93
- "asset": {
94
- "_ref": "image-f52f71bc1df46e080dabe43a8effe8ccfb5f21de-4032x3024-png",
95
- "_type": "reference",
96
- },
78
+ expect(editor.children).toEqual([
79
+ {
80
+ __inline: false,
81
+ _key: 'c01739b0d03b',
82
+ _type: 'image',
83
+ children: [
84
+ {
85
+ _key: VOID_CHILD_KEY,
86
+ _type: 'span',
87
+ marks: [],
88
+ text: '',
89
+ },
90
+ ],
91
+ value: {
92
+ asset: {
93
+ _ref: 'image-b5681d9d0b2b6c922238e7c694500dd7c1349b19-256x256-jpg',
94
+ _type: 'reference',
97
95
  },
98
96
  },
99
- ]
100
- `)
97
+ },
98
+ ])
101
99
  })
102
100
  it('will not create operations for insertion inside blocks', () => {
103
101
  editor.children = [
@@ -298,9 +298,17 @@ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {
298
298
  })
299
299
  }
300
300
  } else if (block && 'value' in block) {
301
- const newVal = applyAll([block.value], [patch])[0]
302
- Transforms.setNodes(editor, {...block, value: newVal}, {at: blockPath})
303
- return true
301
+ if (patch.path.length > 1 && patch.path[1] !== 'children') {
302
+ const newVal = applyAll(block.value, [
303
+ {
304
+ ...patch,
305
+ path: patch.path.slice(1),
306
+ },
307
+ ])
308
+ Transforms.setNodes(editor, {...block, value: newVal}, {at: blockPath})
309
+ } else {
310
+ return false
311
+ }
304
312
  }
305
313
  debugState(editor, 'after')
306
314
  return true
@@ -1,3 +1,5 @@
1
+ export {BehaviorPlugin} from './plugin.behavior'
1
2
  export {EventListenerPlugin} from './plugin.event-listener'
2
3
  export {EditorRefPlugin} from './plugin.editor-ref'
3
4
  export {MarkdownPlugin, type MarkdownPluginConfig} from './plugin.markdown'
5
+ export {OneLinePlugin} from './plugin.one-line'
@@ -0,0 +1,22 @@
1
+ import {useEffect} from 'react'
2
+ import type {Behavior} from '../behaviors'
3
+ import {useEditor} from '../editor/editor-provider'
4
+
5
+ /**
6
+ * @beta
7
+ */
8
+ export function BehaviorPlugin(props: {behaviors: Array<Behavior>}) {
9
+ const editor = useEditor()
10
+
11
+ useEffect(() => {
12
+ const unregisterBehaviors = props.behaviors.map((behavior) =>
13
+ editor.registerBehavior({behavior}),
14
+ )
15
+
16
+ return () => {
17
+ unregisterBehaviors.forEach((unregister) => unregister())
18
+ }
19
+ }, [editor, props.behaviors])
20
+
21
+ return null
22
+ }
@@ -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
+ }
@@ -12,6 +12,8 @@ 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
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'
15
17
  export {getSelectionText} from './selector.get-selection-text'
16
18
  export {getBlockTextBefore} from './selector.get-text-before'
17
19
  export {getValue} from './selector.get-value'
@@ -21,6 +23,7 @@ export {isActiveListItem} from './selector.is-active-list-item'
21
23
  export {isActiveStyle} from './selector.is-active-style'
22
24
  export {isAtTheEndOfBlock} from './selector.is-at-the-end-of-block'
23
25
  export {isAtTheStartOfBlock} from './selector.is-at-the-start-of-block'
26
+ export {isOverlappingSelection} from './selector.is-overlapping-selection'
24
27
  export {isPointAfterSelection} from './selector.is-point-after-selection'
25
28
  export {isPointBeforeSelection} from './selector.is-point-before-selection'
26
29
  export {isSelectionCollapsed} from './selector.is-selection-collapsed'
@@ -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,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
+ }