@portabletext/editor 1.25.0 → 1.26.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 (81) hide show
  1. package/lib/_chunks-cjs/behavior.core.cjs +131 -36
  2. package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
  3. package/lib/_chunks-cjs/selector.get-text-before.cjs +8 -8
  4. package/lib/_chunks-cjs/selector.get-text-before.cjs.map +1 -1
  5. package/lib/_chunks-cjs/{selector.is-active-style.cjs → selector.is-at-the-start-of-block.cjs} +29 -3
  6. package/lib/_chunks-cjs/selector.is-at-the-start-of-block.cjs.map +1 -0
  7. package/lib/_chunks-cjs/util.is-empty-text-block.cjs +2 -2
  8. package/lib/_chunks-cjs/util.is-empty-text-block.cjs.map +1 -1
  9. package/lib/_chunks-cjs/util.is-equal-selection-points.cjs +46 -0
  10. package/lib/_chunks-cjs/util.is-equal-selection-points.cjs.map +1 -0
  11. package/lib/_chunks-cjs/util.reverse-selection.cjs +0 -16
  12. package/lib/_chunks-cjs/util.reverse-selection.cjs.map +1 -1
  13. package/lib/_chunks-es/behavior.core.js +99 -4
  14. package/lib/_chunks-es/behavior.core.js.map +1 -1
  15. package/lib/_chunks-es/selector.get-text-before.js +2 -2
  16. package/lib/_chunks-es/{selector.is-active-style.js → selector.is-at-the-start-of-block.js} +29 -2
  17. package/lib/_chunks-es/selector.is-at-the-start-of-block.js.map +1 -0
  18. package/lib/_chunks-es/util.is-empty-text-block.js +1 -1
  19. package/lib/_chunks-es/util.is-equal-selection-points.js +47 -0
  20. package/lib/_chunks-es/util.is-equal-selection-points.js.map +1 -0
  21. package/lib/_chunks-es/util.reverse-selection.js +0 -16
  22. package/lib/_chunks-es/util.reverse-selection.js.map +1 -1
  23. package/lib/behaviors/index.cjs +27 -27
  24. package/lib/behaviors/index.cjs.map +1 -1
  25. package/lib/behaviors/index.d.cts +413 -0
  26. package/lib/behaviors/index.d.ts +413 -0
  27. package/lib/behaviors/index.js +1 -1
  28. package/lib/index.cjs +147 -106
  29. package/lib/index.cjs.map +1 -1
  30. package/lib/index.d.cts +1653 -222
  31. package/lib/index.d.ts +1653 -222
  32. package/lib/index.js +143 -102
  33. package/lib/index.js.map +1 -1
  34. package/lib/selectors/index.cjs +25 -23
  35. package/lib/selectors/index.cjs.map +1 -1
  36. package/lib/selectors/index.d.cts +16 -0
  37. package/lib/selectors/index.d.ts +16 -0
  38. package/lib/selectors/index.js +3 -1
  39. package/lib/utils/index.cjs +5 -3
  40. package/lib/utils/index.cjs.map +1 -1
  41. package/lib/utils/index.d.cts +19 -0
  42. package/lib/utils/index.d.ts +19 -0
  43. package/lib/utils/index.js +4 -2
  44. package/package.json +1 -1
  45. package/src/behavior-actions/behavior.action-utils.insert-block.ts +3 -3
  46. package/src/behavior-actions/behavior.action.block.set.ts +23 -0
  47. package/src/behavior-actions/behavior.action.block.unset.ts +21 -0
  48. package/src/behavior-actions/behavior.action.insert-break.ts +2 -69
  49. package/src/behavior-actions/behavior.action.insert.block.ts +29 -0
  50. package/src/behavior-actions/behavior.actions.ts +28 -9
  51. package/src/behaviors/behavior.core.insert-break.ts +122 -0
  52. package/src/behaviors/behavior.core.ts +6 -2
  53. package/src/behaviors/behavior.types.ts +16 -1
  54. package/src/converters/converter.json.ts +4 -4
  55. package/src/converters/converter.portable-text.deserialize.test.ts +1 -1
  56. package/src/converters/converter.portable-text.ts +4 -4
  57. package/src/converters/converter.text-html.deserialize.test.ts +1 -1
  58. package/src/converters/converter.text-html.serialize.test.ts +1 -1
  59. package/src/converters/converter.text-html.ts +4 -4
  60. package/src/converters/converter.text-plain.test.ts +1 -1
  61. package/src/converters/converter.text-plain.ts +3 -3
  62. package/src/converters/{converter.ts → converter.types.ts} +6 -0
  63. package/src/editor/create-editor.ts +4 -1
  64. package/src/editor/editor-machine.ts +8 -2
  65. package/src/editor/editor-snapshot.ts +1 -1
  66. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +2 -2
  67. package/src/editor/plugins/create-with-event-listeners.ts +3 -0
  68. package/src/selectors/index.ts +2 -0
  69. package/src/selectors/selector.is-at-the-end-of-block.ts +22 -0
  70. package/src/selectors/selector.is-at-the-start-of-block.ts +25 -0
  71. package/src/selectors/selector.is-selection-collapsed.ts +6 -2
  72. package/src/utils/index.ts +2 -0
  73. package/src/utils/util.get-block-end-point.ts +34 -0
  74. package/src/utils/util.is-equal-selection-points.ts +13 -0
  75. package/lib/_chunks-cjs/selector.is-active-style.cjs.map +0 -1
  76. package/lib/_chunks-cjs/util.is-keyed-segment.cjs +0 -6
  77. package/lib/_chunks-cjs/util.is-keyed-segment.cjs.map +0 -1
  78. package/lib/_chunks-es/selector.is-active-style.js.map +0 -1
  79. package/lib/_chunks-es/util.is-keyed-segment.js +0 -7
  80. package/lib/_chunks-es/util.is-keyed-segment.js.map +0 -1
  81. /package/src/converters/{converters.ts → converters.core.ts} +0 -0
@@ -0,0 +1,122 @@
1
+ import * as selectors from '../selectors'
2
+ import {defineBehavior, raise} from './behavior.types'
3
+
4
+ const atTheEndOfTextBlock = defineBehavior({
5
+ on: 'insert.break',
6
+ guard: ({context}) => {
7
+ const focusTextBlock = selectors.getFocusTextBlock({context})
8
+ const selectionCollapsed = selectors.isSelectionCollapsed({context})
9
+
10
+ if (!context.selection || !focusTextBlock || !selectionCollapsed) {
11
+ return false
12
+ }
13
+
14
+ const atTheEndOfBlock = selectors.isAtTheEndOfBlock(focusTextBlock)({
15
+ context,
16
+ })
17
+
18
+ const focusListItem = focusTextBlock.node.listItem
19
+ const focusLevel = focusTextBlock.node.level
20
+
21
+ if (atTheEndOfBlock) {
22
+ return {focusListItem, focusLevel}
23
+ }
24
+
25
+ return false
26
+ },
27
+ actions: [
28
+ ({context}, {focusListItem, focusLevel}) => [
29
+ raise({
30
+ type: 'insert.block',
31
+ block: {
32
+ _type: context.schema.block.name,
33
+ _key: context.keyGenerator(),
34
+ children: [
35
+ {
36
+ _key: context.keyGenerator(),
37
+ _type: context.schema.span.name,
38
+ text: '',
39
+ marks: [],
40
+ },
41
+ ],
42
+ markDefs: [],
43
+ listItem: focusListItem,
44
+ level: focusLevel,
45
+ style: context.schema.styles[0]?.value,
46
+ },
47
+ placement: 'after',
48
+ }),
49
+ ],
50
+ ],
51
+ })
52
+
53
+ const atTheStartOfTextBlock = defineBehavior({
54
+ on: 'insert.break',
55
+ guard: ({context}) => {
56
+ const focusTextBlock = selectors.getFocusTextBlock({context})
57
+ const selectionCollapsed = selectors.isSelectionCollapsed({context})
58
+
59
+ if (!context.selection || !focusTextBlock || !selectionCollapsed) {
60
+ return false
61
+ }
62
+
63
+ const focusSpan = selectors.getFocusSpan({context})
64
+
65
+ const focusDecorators = focusSpan?.node.marks?.filter(
66
+ (mark) =>
67
+ context.schema.decorators.some(
68
+ (decorator) => decorator.value === mark,
69
+ ) ?? [],
70
+ )
71
+ const focusAnnotations =
72
+ focusSpan?.node.marks?.filter(
73
+ (mark) =>
74
+ !context.schema.decorators.some(
75
+ (decorator) => decorator.value === mark,
76
+ ),
77
+ ) ?? []
78
+ const focusListItem = focusTextBlock.node.listItem
79
+ const focusLevel = focusTextBlock.node.level
80
+
81
+ const atTheStartOfBlock = selectors.isAtTheStartOfBlock(focusTextBlock)({
82
+ context,
83
+ })
84
+
85
+ if (atTheStartOfBlock) {
86
+ return {focusAnnotations, focusDecorators, focusListItem, focusLevel}
87
+ }
88
+
89
+ return false
90
+ },
91
+ actions: [
92
+ (
93
+ {context},
94
+ {focusAnnotations, focusDecorators, focusListItem, focusLevel},
95
+ ) => [
96
+ raise({
97
+ type: 'insert.block',
98
+ block: {
99
+ _key: context.keyGenerator(),
100
+ _type: context.schema.block.name,
101
+ children: [
102
+ {
103
+ _key: context.keyGenerator(),
104
+ _type: context.schema.span.name,
105
+ marks: focusAnnotations.length === 0 ? focusDecorators : [],
106
+ text: '',
107
+ },
108
+ ],
109
+ listItem: focusListItem,
110
+ level: focusLevel,
111
+ style: context.schema.styles[0]?.value,
112
+ },
113
+ placement: 'before',
114
+ }),
115
+ ],
116
+ ],
117
+ })
118
+
119
+ export const coreInsertBreakBehaviors = {
120
+ atTheEndOfTextBlock,
121
+ atTheStartOfTextBlock,
122
+ }
@@ -2,14 +2,15 @@ import {coreAnnotationBehaviors} from './behavior.core.annotations'
2
2
  import {coreBlockObjectBehaviors} from './behavior.core.block-objects'
3
3
  import {coreDecoratorBehaviors} from './behavior.core.decorators'
4
4
  import {coreDeserializeBehavior} from './behavior.core.deserialize'
5
+ import {coreInsertBreakBehaviors} from './behavior.core.insert-break'
5
6
  import {coreListBehaviors} from './behavior.core.lists'
6
7
  import {coreSerializeBehaviors} from './behavior.core.serialize'
7
8
  import {coreStyleBehaviors} from './behavior.core.style'
8
- import {defineBehavior} from './behavior.types'
9
+ import {defineBehavior, raise} from './behavior.types'
9
10
 
10
11
  const softReturn = defineBehavior({
11
12
  on: 'insert.soft break',
12
- actions: [() => [{type: 'insert.text', text: '\n'}]],
13
+ actions: [() => [raise({type: 'insert.text', text: '\n'})]],
13
14
  })
14
15
 
15
16
  /**
@@ -38,6 +39,8 @@ export const coreBehaviors = [
38
39
  coreListBehaviors.clearListOnEnter,
39
40
  coreListBehaviors.indentListOnTab,
40
41
  coreListBehaviors.unindentListOnShiftTab,
42
+ coreInsertBreakBehaviors.atTheEndOfTextBlock,
43
+ coreInsertBreakBehaviors.atTheStartOfTextBlock,
41
44
  coreSerializeBehaviors.serialize,
42
45
  coreSerializeBehaviors['serialization.success'],
43
46
  coreStyleBehaviors.toggleStyleOff,
@@ -53,6 +56,7 @@ export const coreBehavior = {
53
56
  decorators: coreDecoratorBehaviors,
54
57
  deserialize: coreDeserializeBehavior,
55
58
  blockObjects: coreBlockObjectBehaviors,
59
+ insertBreak: coreInsertBreakBehaviors,
56
60
  lists: coreListBehaviors,
57
61
  ...coreSerializeBehaviors,
58
62
  style: coreSerializeBehaviors,
@@ -5,7 +5,7 @@ import type {
5
5
  } from '@sanity/types'
6
6
  import type {TextUnit} from 'slate'
7
7
  import type {TextInsertTextOptions} from 'slate/dist/interfaces/transforms/text'
8
- import type {ConverterEvent} from '../converters/converter'
8
+ import type {ConverterEvent} from '../converters/converter.types'
9
9
  import type {EditorContext} from '../editor/editor-snapshot'
10
10
  import type {MIMEType} from '../internal-utils/mime-type'
11
11
  import type {OmitFromUnion, PickFromUnion} from '../type-utils'
@@ -35,6 +35,16 @@ export type SyntheticBehaviorEvent =
35
35
  value: {[prop: string]: unknown}
36
36
  }
37
37
  }
38
+ | {
39
+ type: 'block.set'
40
+ at: [KeyedSegment]
41
+ [props: string]: unknown
42
+ }
43
+ | {
44
+ type: 'block.unset'
45
+ at: [KeyedSegment]
46
+ props: Array<string>
47
+ }
38
48
  | {
39
49
  type: 'blur'
40
50
  }
@@ -101,6 +111,11 @@ export type SyntheticBehaviorEvent =
101
111
  | {
102
112
  type: 'insert.soft break'
103
113
  }
114
+ | {
115
+ type: 'insert.block'
116
+ block: PortableTextBlock
117
+ placement: 'auto' | 'after' | 'before'
118
+ }
104
119
  | {
105
120
  type: 'insert.span'
106
121
  text: string
@@ -1,6 +1,7 @@
1
- import type {Converter} from './converter'
1
+ import {defineConverter} from './converter.types'
2
2
 
3
- export const converterJson: Converter<'application/json'> = {
3
+ export const converterJson = defineConverter({
4
+ mimeType: 'application/json',
4
5
  serialize: ({context, event}) => {
5
6
  const portableTextConverter = context.converters.find(
6
7
  (converter) => converter.mimeType === 'application/x-portable-text',
@@ -49,5 +50,4 @@ export const converterJson: Converter<'application/json'> = {
49
50
  mimeType: 'application/json',
50
51
  }
51
52
  },
52
- mimeType: 'application/json',
53
- }
53
+ })
@@ -6,7 +6,7 @@ import {
6
6
  } from '../editor/define-schema'
7
7
  import {createTestKeyGenerator} from '../internal-utils/test-key-generator'
8
8
  import {converterPortableText} from './converter.portable-text'
9
- import {coreConverters} from './converters'
9
+ import {coreConverters} from './converters.core'
10
10
 
11
11
  function createContext(schema: SchemaDefinition) {
12
12
  return {
@@ -1,8 +1,9 @@
1
1
  import {parseBlock} from '../internal-utils/parse-blocks'
2
2
  import {sliceBlocks} from '../utils'
3
- import type {Converter} from './converter'
3
+ import {defineConverter} from './converter.types'
4
4
 
5
- export const converterPortableText: Converter<'application/x-portable-text'> = {
5
+ export const converterPortableText = defineConverter({
6
+ mimeType: 'application/x-portable-text',
6
7
  serialize: ({context, event}) => {
7
8
  if (!context.selection) {
8
9
  return {
@@ -55,5 +56,4 @@ export const converterPortableText: Converter<'application/x-portable-text'> = {
55
56
  mimeType: 'application/x-portable-text',
56
57
  }
57
58
  },
58
- mimeType: 'application/x-portable-text',
59
- }
59
+ })
@@ -6,7 +6,7 @@ import {
6
6
  } from '../editor/define-schema'
7
7
  import {createTestKeyGenerator} from '../internal-utils/test-key-generator'
8
8
  import {converterTextHtml} from './converter.text-html'
9
- import {coreConverters} from './converters'
9
+ import {coreConverters} from './converters.core'
10
10
 
11
11
  function createContext(schema: SchemaDefinition) {
12
12
  return {
@@ -8,7 +8,7 @@ import {
8
8
  import {createTestKeyGenerator} from '../internal-utils/test-key-generator'
9
9
  import type {EditorSelection} from '../utils'
10
10
  import {converterTextHtml} from './converter.text-html'
11
- import {coreConverters} from './converters'
11
+ import {coreConverters} from './converters.core'
12
12
 
13
13
  const decoratedParagraph: PortableTextTextBlock = {
14
14
  _key: 'k0',
@@ -2,9 +2,10 @@ import {htmlToBlocks} from '@portabletext/block-tools'
2
2
  import {toHTML} from '@portabletext/to-html'
3
3
  import type {PortableTextBlock} from '@sanity/types'
4
4
  import {sliceBlocks} from '../utils'
5
- import type {Converter} from './converter'
5
+ import {defineConverter} from './converter.types'
6
6
 
7
- export const converterTextHtml: Converter<'text/html'> = {
7
+ export const converterTextHtml = defineConverter({
8
+ mimeType: 'text/html',
8
9
  serialize: ({context, event}) => {
9
10
  if (!context.selection) {
10
11
  return {
@@ -57,5 +58,4 @@ export const converterTextHtml: Converter<'text/html'> = {
57
58
  mimeType: 'text/html',
58
59
  }
59
60
  },
60
- mimeType: 'text/html',
61
- }
61
+ })
@@ -8,7 +8,7 @@ import {
8
8
  import type {EditorContext} from '../editor/editor-snapshot'
9
9
  import type {EditorSelection} from '../utils'
10
10
  import {converterTextPlain} from './converter.text-plain'
11
- import {coreConverters} from './converters'
11
+ import {coreConverters} from './converters.core'
12
12
 
13
13
  const b1: PortableTextTextBlock = {
14
14
  _type: 'block',
@@ -1,9 +1,9 @@
1
1
  import {htmlToBlocks} from '@portabletext/block-tools'
2
2
  import {isPortableTextTextBlock, type PortableTextBlock} from '@sanity/types'
3
3
  import {sliceBlocks} from '../utils'
4
- import type {Converter} from './converter'
4
+ import {defineConverter} from './converter.types'
5
5
 
6
- export const converterTextPlain: Converter<'text/plain'> = {
6
+ export const converterTextPlain = defineConverter({
7
7
  mimeType: 'text/plain',
8
8
  serialize: ({context, event}) => {
9
9
  if (!context.selection) {
@@ -73,7 +73,7 @@ export const converterTextPlain: Converter<'text/plain'> = {
73
73
  mimeType: 'text/plain',
74
74
  }
75
75
  },
76
- }
76
+ })
77
77
 
78
78
  const entityMap: Record<string, string> = {
79
79
  '&': '&amp;',
@@ -9,6 +9,12 @@ export type Converter<TMIMEType extends MIMEType = MIMEType> = {
9
9
  deserialize: Deserializer<TMIMEType>
10
10
  }
11
11
 
12
+ export function defineConverter<TMIMEType extends MIMEType>(
13
+ converter: Converter<TMIMEType>,
14
+ ): Converter<TMIMEType> {
15
+ return converter
16
+ }
17
+
12
18
  export type ConverterEvent<TMIMEType extends MIMEType = MIMEType> =
13
19
  | {
14
20
  type: 'serialize'
@@ -12,7 +12,7 @@ import {
12
12
  type Snapshot,
13
13
  } from 'xstate'
14
14
  import type {Behavior, CustomBehaviorEvent} from '../behaviors/behavior.types'
15
- import {coreConverters} from '../converters/converters'
15
+ import {coreConverters} from '../converters/converters.core'
16
16
  import {compileType} from '../internal-utils/schema'
17
17
  import type {PickFromUnion} from '../type-utils'
18
18
  import type {EditableAPI} from '../types/editor'
@@ -66,6 +66,8 @@ export type EditorEvent =
66
66
  | 'annotation.add'
67
67
  | 'annotation.remove'
68
68
  | 'annotation.toggle'
69
+ | 'block.set'
70
+ | 'block.unset'
69
71
  | 'blur'
70
72
  | 'data transfer.set'
71
73
  | 'decorator.add'
@@ -76,6 +78,7 @@ export type EditorEvent =
76
78
  | 'deserialization.failure'
77
79
  | 'deserialization.success'
78
80
  | 'focus'
81
+ | 'insert.block'
79
82
  | 'insert.block object'
80
83
  | 'insert.inline object'
81
84
  | 'insert.span'
@@ -20,7 +20,7 @@ import {
20
20
  type NativeBehaviorEvent,
21
21
  type SyntheticBehaviorEvent,
22
22
  } from '../behaviors/behavior.types'
23
- import type {Converter} from '../converters/converter'
23
+ import type {Converter} from '../converters/converter.types'
24
24
  import type {OmitFromUnion, PickFromUnion} from '../type-utils'
25
25
  import type {
26
26
  EditorSelection,
@@ -172,7 +172,6 @@ export type InternalEditorEmittedEvent =
172
172
  description: string
173
173
  data: unknown
174
174
  }
175
- | {type: 'select'; selection: EditorSelection}
176
175
  | {type: 'selection'; selection: EditorSelection}
177
176
  | {type: 'blurred'; event: FocusEvent<HTMLDivElement, Element>}
178
177
  | {type: 'focused'; event: FocusEvent<HTMLDivElement, Element>}
@@ -186,6 +185,8 @@ export type InternalEditorEmittedEvent =
186
185
  | 'annotation.add'
187
186
  | 'annotation.remove'
188
187
  | 'annotation.toggle'
188
+ | 'block.set'
189
+ | 'block.unset'
189
190
  | 'blur'
190
191
  | 'data transfer.set'
191
192
  | 'decorator.add'
@@ -198,6 +199,7 @@ export type InternalEditorEmittedEvent =
198
199
  | 'deserialization.failure'
199
200
  | 'deserialization.success'
200
201
  | 'focus'
202
+ | 'insert.block'
201
203
  | 'insert.block object'
202
204
  | 'insert.inline object'
203
205
  | 'insert.span'
@@ -208,6 +210,7 @@ export type InternalEditorEmittedEvent =
208
210
  | 'move.block'
209
211
  | 'move.block down'
210
212
  | 'move.block up'
213
+ | 'select'
211
214
  | 'select.next block'
212
215
  | 'select.previous block'
213
216
  | 'serialization.failure'
@@ -596,6 +599,9 @@ export const editorMachine = setup({
596
599
  'annotation.*': {
597
600
  actions: emit(({event}) => event),
598
601
  },
602
+ 'block.*': {
603
+ actions: emit(({event}) => event),
604
+ },
599
605
  'blur': {
600
606
  actions: emit(({event}) => event),
601
607
  },
@@ -1,5 +1,5 @@
1
1
  import type {PortableTextBlock} from '@sanity/types'
2
- import type {Converter} from '../converters/converter'
2
+ import type {Converter} from '../converters/converter.types'
3
3
  import {toPortableTextRange} from '../internal-utils/ranges'
4
4
  import {fromSlateValue} from '../internal-utils/values'
5
5
  import {KEY_TO_VALUE_ELEMENT} from '../internal-utils/weakMaps'
@@ -461,11 +461,11 @@ describe('plugin:withPortableTextMarksModel', () => {
461
461
  style: 'normal',
462
462
  },
463
463
  {
464
- _key: '3',
464
+ _key: '5',
465
465
  _type: 'myTestBlockType',
466
466
  children: [
467
467
  {
468
- _key: '2',
468
+ _key: '3',
469
469
  _type: 'span',
470
470
  marks: [],
471
471
  text: '',
@@ -28,6 +28,8 @@ export function createWithEventListeners(
28
28
  case 'annotation.add':
29
29
  case 'annotation.remove':
30
30
  case 'annotation.toggle':
31
+ case 'block.set':
32
+ case 'block.unset':
31
33
  case 'blur':
32
34
  case 'data transfer.set':
33
35
  case 'decorator.add':
@@ -40,6 +42,7 @@ export function createWithEventListeners(
40
42
  case 'deserialization.failure':
41
43
  case 'deserialization.success':
42
44
  case 'focus':
45
+ case 'insert.block':
43
46
  case 'insert.block object':
44
47
  case 'insert.inline object':
45
48
  case 'insert.span':
@@ -17,6 +17,8 @@ export {isActiveAnnotation} from './selector.is-active-annotation'
17
17
  export {isActiveDecorator} from './selector.is-active-decorator'
18
18
  export {isActiveListItem} from './selector.is-active-list-item'
19
19
  export {isActiveStyle} from './selector.is-active-style'
20
+ export {isAtTheEndOfBlock} from './selector.is-at-the-end-of-block'
21
+ export {isAtTheStartOfBlock} from './selector.is-at-the-start-of-block'
20
22
  export {isPointAfterSelection} from './selector.is-point-after-selection'
21
23
  export {isPointBeforeSelection} from './selector.is-point-before-selection'
22
24
  export {isSelectionCollapsed} from './selector.is-selection-collapsed'
@@ -0,0 +1,22 @@
1
+ import type {KeyedSegment, PortableTextBlock} from '@sanity/types'
2
+ import type {EditorSelector} from '../editor/editor-selector'
3
+ import * as utils from '../utils'
4
+ import {isSelectionCollapsed} from './selector.is-selection-collapsed'
5
+
6
+ /**
7
+ * @public
8
+ */
9
+ export function isAtTheEndOfBlock(block: {
10
+ node: PortableTextBlock
11
+ path: [KeyedSegment]
12
+ }): EditorSelector<boolean> {
13
+ return ({context}) => {
14
+ if (!context.selection || !isSelectionCollapsed({context})) {
15
+ return false
16
+ }
17
+
18
+ const blockEndPoint = utils.getBlockEndPoint(block)
19
+
20
+ return utils.isEqualSelectionPoints(context.selection.focus, blockEndPoint)
21
+ }
22
+ }
@@ -0,0 +1,25 @@
1
+ import type {KeyedSegment, PortableTextBlock} from '@sanity/types'
2
+ import type {EditorSelector} from '../editor/editor-selector'
3
+ import * as utils from '../utils'
4
+ import {isSelectionCollapsed} from './selector.is-selection-collapsed'
5
+
6
+ /**
7
+ * @public
8
+ */
9
+ export function isAtTheStartOfBlock(block: {
10
+ node: PortableTextBlock
11
+ path: [KeyedSegment]
12
+ }): EditorSelector<boolean> {
13
+ return ({context}) => {
14
+ if (!context.selection || !isSelectionCollapsed({context})) {
15
+ return false
16
+ }
17
+
18
+ const blockStartPoint = utils.getBlockStartPoint(block)
19
+
20
+ return utils.isEqualSelectionPoints(
21
+ context.selection.focus,
22
+ blockStartPoint,
23
+ )
24
+ }
25
+ }
@@ -4,9 +4,13 @@ import type {EditorSelector} from '../editor/editor-selector'
4
4
  * @public
5
5
  */
6
6
  export const isSelectionCollapsed: EditorSelector<boolean> = ({context}) => {
7
+ if (!context.selection) {
8
+ return false
9
+ }
10
+
7
11
  return (
8
- JSON.stringify(context.selection?.anchor.path) ===
9
- JSON.stringify(context.selection?.focus.path) &&
12
+ JSON.stringify(context.selection.anchor.path) ===
13
+ JSON.stringify(context.selection.focus.path) &&
10
14
  context.selection?.anchor.offset === context.selection?.focus.offset
11
15
  )
12
16
  }
@@ -4,9 +4,11 @@ export {
4
4
  blockOffsetToSpanSelectionPoint,
5
5
  spanSelectionPointToBlockOffset,
6
6
  } from './util.block-offset'
7
+ export {getBlockEndPoint} from './util.get-block-end-point'
7
8
  export {getBlockStartPoint} from './util.get-block-start-point'
8
9
  export {getTextBlockText} from './util.get-text-block-text'
9
10
  export {isEmptyTextBlock} from './util.is-empty-text-block'
11
+ export {isEqualSelectionPoints} from './util.is-equal-selection-points'
10
12
  export {isKeyedSegment} from './util.is-keyed-segment'
11
13
  export {reverseSelection} from './util.reverse-selection'
12
14
  export {sliceBlocks} from './util.slice-blocks'
@@ -0,0 +1,34 @@
1
+ import {
2
+ isPortableTextSpan,
3
+ isPortableTextTextBlock,
4
+ type KeyedSegment,
5
+ type PortableTextBlock,
6
+ } from '@sanity/types'
7
+ import type {EditorSelectionPoint} from '../types/editor'
8
+
9
+ /**
10
+ * @public
11
+ */
12
+ export function getBlockEndPoint({
13
+ node,
14
+ path,
15
+ }: {
16
+ node: PortableTextBlock
17
+ path: [KeyedSegment]
18
+ }): EditorSelectionPoint {
19
+ if (isPortableTextTextBlock(node)) {
20
+ const lastChild = node.children[node.children.length - 1]
21
+
22
+ if (lastChild) {
23
+ return {
24
+ path: [...path, 'children', {_key: lastChild._key}],
25
+ offset: isPortableTextSpan(lastChild) ? lastChild.text.length : 0,
26
+ }
27
+ }
28
+ }
29
+
30
+ return {
31
+ path,
32
+ offset: 0,
33
+ }
34
+ }
@@ -0,0 +1,13 @@
1
+ import type {EditorSelectionPoint} from '../types/editor'
2
+
3
+ /**
4
+ * @public
5
+ */
6
+ export function isEqualSelectionPoints(
7
+ a: EditorSelectionPoint,
8
+ b: EditorSelectionPoint,
9
+ ) {
10
+ return (
11
+ a.offset === b.offset && JSON.stringify(a.path) === JSON.stringify(b.path)
12
+ )
13
+ }