@portabletext/editor 1.39.1 → 1.40.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 (99) hide show
  1. package/lib/_chunks-cjs/behavior.core.cjs +12 -4
  2. package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
  3. package/lib/_chunks-cjs/editor-provider.cjs +131 -109
  4. package/lib/_chunks-cjs/editor-provider.cjs.map +1 -1
  5. package/lib/_chunks-cjs/{parse-blocks.cjs → util.selection-point-to-block-offset.cjs} +74 -4
  6. package/lib/_chunks-cjs/util.selection-point-to-block-offset.cjs.map +1 -0
  7. package/lib/_chunks-cjs/util.slice-blocks.cjs +2 -2
  8. package/lib/_chunks-cjs/util.slice-blocks.cjs.map +1 -1
  9. package/lib/_chunks-cjs/util.split-text-block.cjs +68 -0
  10. package/lib/_chunks-cjs/util.split-text-block.cjs.map +1 -0
  11. package/lib/_chunks-es/behavior.core.js +12 -4
  12. package/lib/_chunks-es/behavior.core.js.map +1 -1
  13. package/lib/_chunks-es/editor-provider.js +125 -103
  14. package/lib/_chunks-es/editor-provider.js.map +1 -1
  15. package/lib/_chunks-es/{parse-blocks.js → util.selection-point-to-block-offset.js} +76 -5
  16. package/lib/_chunks-es/util.selection-point-to-block-offset.js.map +1 -0
  17. package/lib/_chunks-es/util.slice-blocks.js +2 -2
  18. package/lib/_chunks-es/util.slice-blocks.js.map +1 -1
  19. package/lib/_chunks-es/util.split-text-block.js +70 -0
  20. package/lib/_chunks-es/util.split-text-block.js.map +1 -0
  21. package/lib/behaviors/index.d.cts +383 -111
  22. package/lib/behaviors/index.d.ts +383 -111
  23. package/lib/index.cjs +198 -195
  24. package/lib/index.cjs.map +1 -1
  25. package/lib/index.d.cts +345 -90
  26. package/lib/index.d.ts +345 -90
  27. package/lib/index.js +205 -202
  28. package/lib/index.js.map +1 -1
  29. package/lib/plugins/index.cjs +11 -11
  30. package/lib/plugins/index.cjs.map +1 -1
  31. package/lib/plugins/index.d.cts +335 -93
  32. package/lib/plugins/index.d.ts +335 -93
  33. package/lib/plugins/index.js +2 -2
  34. package/lib/selectors/index.d.cts +333 -81
  35. package/lib/selectors/index.d.ts +333 -81
  36. package/lib/utils/index.cjs +15 -87
  37. package/lib/utils/index.cjs.map +1 -1
  38. package/lib/utils/index.d.cts +386 -84
  39. package/lib/utils/index.d.ts +386 -84
  40. package/lib/utils/index.js +13 -86
  41. package/lib/utils/index.js.map +1 -1
  42. package/package.json +6 -6
  43. package/src/behavior-actions/behavior.action.decorator.add.ts +13 -2
  44. package/src/behaviors/behavior.core.block-objects.ts +32 -2
  45. package/src/behaviors/behavior.default.ts +38 -14
  46. package/src/behaviors/behavior.types.ts +5 -4
  47. package/src/converters/converter.portable-text.ts +9 -0
  48. package/src/converters/converter.text-plain.test.ts +5 -5
  49. package/src/converters/converter.text-plain.ts +12 -19
  50. package/src/editor/Editable.tsx +122 -68
  51. package/src/editor/PortableTextEditor.tsx +8 -8
  52. package/src/editor/__tests__/self-solving.test.tsx +1 -1
  53. package/src/editor/components/Element.tsx +2 -9
  54. package/src/editor/create-editor.ts +13 -5
  55. package/src/editor/editor-machine.ts +6 -2
  56. package/src/editor/editor-provider.tsx +11 -7
  57. package/src/editor/editor-selector.ts +4 -3
  58. package/src/editor/editor-snapshot.ts +2 -2
  59. package/src/editor/plugins/create-with-event-listeners.ts +2 -5
  60. package/src/editor/plugins/createWithPortableTextMarkModel.ts +1 -2
  61. package/src/internal-utils/block-keys.ts +9 -0
  62. package/src/internal-utils/collapse-selection.ts +36 -0
  63. package/src/internal-utils/compound-client-rect.ts +28 -0
  64. package/src/internal-utils/drag-selection.test.ts +507 -0
  65. package/src/internal-utils/drag-selection.ts +66 -0
  66. package/src/internal-utils/editor-selection.test.ts +40 -0
  67. package/src/internal-utils/editor-selection.ts +60 -0
  68. package/src/internal-utils/event-position.ts +55 -80
  69. package/src/internal-utils/inline-object-selection.ts +115 -0
  70. package/src/internal-utils/selection-block-keys.ts +20 -0
  71. package/src/internal-utils/selection-elements.ts +61 -0
  72. package/src/internal-utils/selection-focus-text.ts +38 -0
  73. package/src/internal-utils/selection-text.test.ts +23 -0
  74. package/src/internal-utils/selection-text.ts +90 -0
  75. package/src/internal-utils/split-string.ts +12 -0
  76. package/src/internal-utils/string-overlap.test.ts +14 -0
  77. package/src/internal-utils/string-overlap.ts +28 -0
  78. package/src/internal-utils/string-utils.ts +7 -0
  79. package/src/internal-utils/terse-pt.test.ts +60 -0
  80. package/src/internal-utils/terse-pt.ts +36 -0
  81. package/src/internal-utils/text-block-key.test.ts +30 -0
  82. package/src/internal-utils/text-block-key.ts +30 -0
  83. package/src/internal-utils/text-marks.test.ts +33 -0
  84. package/src/internal-utils/text-marks.ts +26 -0
  85. package/src/internal-utils/text-selection.test.ts +175 -0
  86. package/src/internal-utils/text-selection.ts +122 -0
  87. package/src/internal-utils/value-annotations.ts +31 -0
  88. package/src/internal-utils/values.ts +16 -5
  89. package/src/utils/index.ts +5 -0
  90. package/src/utils/util.block-offset-to-block-selection-point.ts +28 -0
  91. package/src/utils/util.block-offset-to-selection-point.ts +33 -0
  92. package/src/utils/util.block-offsets-to-selection.ts +3 -3
  93. package/src/utils/util.is-equal-selections.ts +20 -0
  94. package/src/utils/util.is-selection-collapsed.ts +15 -0
  95. package/src/utils/util.reverse-selection.ts +9 -5
  96. package/src/utils/util.selection-point-to-block-offset.ts +31 -0
  97. package/lib/_chunks-cjs/parse-blocks.cjs.map +0 -1
  98. package/lib/_chunks-es/parse-blocks.js.map +0 -1
  99. package/src/editor/components/use-draggable.ts +0 -123
@@ -0,0 +1,36 @@
1
+ import {isPortableTextBlock, isPortableTextSpan} from '@portabletext/toolkit'
2
+ import type {PortableTextBlock} from '@sanity/types'
3
+
4
+ export function getTersePt(value: Array<PortableTextBlock> | undefined) {
5
+ if (!value) {
6
+ return undefined
7
+ }
8
+
9
+ const blocks: Array<string> = []
10
+
11
+ for (const block of value) {
12
+ if (blocks.length > 0) {
13
+ blocks.push('|')
14
+ }
15
+ if (isPortableTextBlock(block)) {
16
+ for (const child of block.children) {
17
+ if (isPortableTextSpan(child)) {
18
+ blocks.push(child.text)
19
+ } else {
20
+ blocks.push(`[${child._type}]`)
21
+ }
22
+ }
23
+ } else {
24
+ blocks.push(`[${block._type}]`)
25
+ }
26
+ }
27
+
28
+ return blocks
29
+ }
30
+
31
+ export function parseTersePt(text: string) {
32
+ return text
33
+ .replace(/\|/g, ',|,')
34
+ .split(',')
35
+ .map((span) => span.replace(/\\n/g, '\n'))
36
+ }
@@ -0,0 +1,30 @@
1
+ import {expect, test} from 'vitest'
2
+ import {getTextBlockKey} from './text-block-key'
3
+
4
+ test(getTextBlockKey.name, () => {
5
+ const emptyBlock = {
6
+ _key: 'b1',
7
+ _type: 'block',
8
+ children: [{_key: 's1', _type: 'span', text: ''}],
9
+ }
10
+ const fooBlock = {
11
+ _key: 'b2',
12
+ _type: 'block',
13
+ children: [{_key: 's2', _type: 'span', text: 'foo'}],
14
+ }
15
+ const softReturnBlock = {
16
+ _key: 'b3',
17
+ _type: 'block',
18
+ children: [{_key: 's3', _type: 'span', text: 'foo\nbar'}],
19
+ }
20
+
21
+ expect(getTextBlockKey([emptyBlock, fooBlock, softReturnBlock], '')).toBe(
22
+ 'b1',
23
+ )
24
+ expect(getTextBlockKey([emptyBlock, fooBlock, softReturnBlock], 'foo')).toBe(
25
+ 'b2',
26
+ )
27
+ expect(
28
+ getTextBlockKey([emptyBlock, fooBlock, softReturnBlock], 'foo\nbar'),
29
+ ).toBe('b3')
30
+ })
@@ -0,0 +1,30 @@
1
+ import {isPortableTextBlock} from '@portabletext/toolkit'
2
+ import {isPortableTextSpan, type PortableTextBlock} from '@sanity/types'
3
+
4
+ export function getTextBlockKey(
5
+ value: Array<PortableTextBlock> | undefined,
6
+ text: string,
7
+ ) {
8
+ if (!value) {
9
+ throw new Error(`Unable to find block key for text "${text}"`)
10
+ }
11
+
12
+ let blockKey: string | undefined
13
+
14
+ for (const block of value) {
15
+ if (isPortableTextBlock(block)) {
16
+ for (const child of block.children) {
17
+ if (isPortableTextSpan(child) && child.text === text) {
18
+ blockKey = block._key
19
+ break
20
+ }
21
+ }
22
+ }
23
+ }
24
+
25
+ if (!blockKey) {
26
+ throw new Error(`Unable to find block key for text "${text}"`)
27
+ }
28
+
29
+ return blockKey
30
+ }
@@ -0,0 +1,33 @@
1
+ import {expect, test} from 'vitest'
2
+ import {getTextMarks} from './text-marks'
3
+
4
+ test(getTextMarks.name, () => {
5
+ const fooBlock = {
6
+ _key: 'b1',
7
+ _type: 'block',
8
+ children: [{_key: 's1', _type: 'span', text: 'foo'}],
9
+ }
10
+ const splitBarBlock = {
11
+ _key: 'b1',
12
+ _type: 'block',
13
+ children: [
14
+ {_key: 's1', _type: 'span', text: 'ba', marks: ['strong']},
15
+ {_key: 's2', _type: 'span', text: 'r'},
16
+ ],
17
+ }
18
+ const splitFooBarBazBlock = {
19
+ _key: 'b1',
20
+ _type: 'block',
21
+ children: [
22
+ {_key: 's1', _type: 'span', text: 'foo '},
23
+ {_key: 's2', _type: 'span', text: 'bar', marks: ['strong']},
24
+ {_key: 's3', _type: 'span', text: ' '},
25
+ {_key: 's4', _type: 'span', text: 'baz', marks: ['l1']},
26
+ ],
27
+ }
28
+
29
+ expect(getTextMarks([fooBlock, splitBarBlock], 'ba')).toEqual(['strong'])
30
+ expect(getTextMarks([splitFooBarBazBlock], 'bar')).toEqual(['strong'])
31
+ expect(getTextMarks([splitFooBarBazBlock], ' ')).toEqual([])
32
+ expect(getTextMarks([splitFooBarBazBlock], 'baz')).toEqual(['l1'])
33
+ })
@@ -0,0 +1,26 @@
1
+ import {isPortableTextBlock, isPortableTextSpan} from '@portabletext/toolkit'
2
+ import type {PortableTextBlock} from '@sanity/types'
3
+
4
+ export function getTextMarks(
5
+ value: Array<PortableTextBlock> | undefined,
6
+ text: string,
7
+ ) {
8
+ if (!value) {
9
+ return undefined
10
+ }
11
+
12
+ let marks: Array<string> | undefined = undefined
13
+
14
+ for (const block of value) {
15
+ if (isPortableTextBlock(block)) {
16
+ for (const child of block.children) {
17
+ if (isPortableTextSpan(child) && child.text === text) {
18
+ marks = child.marks ?? []
19
+ break
20
+ }
21
+ }
22
+ }
23
+ }
24
+
25
+ return marks
26
+ }
@@ -0,0 +1,175 @@
1
+ import {expect, test} from 'vitest'
2
+ import {
3
+ getSelectionAfterText,
4
+ getSelectionBeforeText,
5
+ getTextSelection,
6
+ } from './text-selection'
7
+
8
+ test(getTextSelection.name, () => {
9
+ const joinedBlock = {
10
+ _key: 'b1',
11
+ _type: 'block',
12
+ children: [{_key: 's1', _type: 'span', text: 'foo bar baz'}],
13
+ }
14
+
15
+ expect(getTextSelection([joinedBlock], 'foo ')).toEqual({
16
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 0},
17
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 4},
18
+ })
19
+ expect(getTextSelection([joinedBlock], 'o')).toEqual({
20
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 1},
21
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 2},
22
+ })
23
+ expect(getTextSelection([joinedBlock], 'bar')).toEqual({
24
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 4},
25
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 7},
26
+ })
27
+ expect(getTextSelection([joinedBlock], ' baz')).toEqual({
28
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 7},
29
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 11},
30
+ })
31
+
32
+ const noSpaceBlock = {
33
+ _key: 'b1',
34
+ _type: 'block',
35
+ children: [
36
+ {_key: 's1', _type: 'span', text: 'foo'},
37
+ {_key: 's2', _type: 'span', text: 'bar'},
38
+ ],
39
+ }
40
+
41
+ expect(getTextSelection([noSpaceBlock], 'obar')).toEqual({
42
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 2},
43
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's2'}], offset: 3},
44
+ })
45
+
46
+ const emptyLineBlock = {
47
+ _key: 'b1',
48
+ _type: 'block',
49
+ children: [
50
+ {_key: 's1', _type: 'span', text: 'foo'},
51
+ {_key: 's2', _type: 'span', text: ''},
52
+ {_key: 's3', _type: 'span', text: 'bar'},
53
+ ],
54
+ }
55
+
56
+ expect(getTextSelection([emptyLineBlock], 'foobar')).toEqual({
57
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 0},
58
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 3},
59
+ })
60
+
61
+ const splitBlock = {
62
+ _key: 'b1',
63
+ _type: 'block',
64
+ children: [
65
+ {_key: 's1', _type: 'span', text: 'foo '},
66
+ {_key: 's2', _type: 'span', text: 'bar'},
67
+ {_key: 's3', _type: 'span', text: ' baz'},
68
+ ],
69
+ }
70
+
71
+ expect(getTextSelection([splitBlock], 'foo')).toEqual({
72
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 0},
73
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 3},
74
+ })
75
+ expect(getTextSelection([splitBlock], 'bar')).toEqual({
76
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's2'}], offset: 0},
77
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's2'}], offset: 3},
78
+ })
79
+ expect(getTextSelection([splitBlock], 'baz')).toEqual({
80
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 1},
81
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 4},
82
+ })
83
+ expect(getTextSelection([splitBlock], 'foo bar baz')).toEqual({
84
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 0},
85
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 4},
86
+ })
87
+ expect(getTextSelection([splitBlock], 'o bar b')).toEqual({
88
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 2},
89
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 2},
90
+ })
91
+
92
+ const twoBlocks = [
93
+ {
94
+ _key: 'b1',
95
+ _type: 'block',
96
+ children: [{_key: 's1', _type: 'span', text: 'foo'}],
97
+ },
98
+ {
99
+ _key: 'b2',
100
+ _type: 'block',
101
+ children: [{_key: 's2', _type: 'span', text: 'bar'}],
102
+ },
103
+ ]
104
+
105
+ expect(getTextSelection(twoBlocks, 'ooba')).toEqual({
106
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 1},
107
+ focus: {path: [{_key: 'b2'}, 'children', {_key: 's2'}], offset: 2},
108
+ })
109
+ })
110
+
111
+ test(getSelectionBeforeText.name, () => {
112
+ const splitBlock = {
113
+ _type: 'block',
114
+ _key: 'b1',
115
+ children: [
116
+ {_type: 'span', _key: 's1', text: 'foo '},
117
+ {_type: 'span', _key: 's2', text: 'bar'},
118
+ {_type: 'span', _key: 's3', text: ' baz'},
119
+ ],
120
+ }
121
+
122
+ expect(getSelectionBeforeText([splitBlock], 'foo ')).toEqual({
123
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 0},
124
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 0},
125
+ backward: false,
126
+ })
127
+ expect(getSelectionBeforeText([splitBlock], 'f')).toEqual({
128
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 0},
129
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 0},
130
+ backward: false,
131
+ })
132
+ expect(getSelectionBeforeText([splitBlock], 'o')).toEqual({
133
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 1},
134
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 1},
135
+ backward: false,
136
+ })
137
+ expect(getSelectionBeforeText([splitBlock], 'bar')).toEqual({
138
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's2'}], offset: 0},
139
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's2'}], offset: 0},
140
+ backward: false,
141
+ })
142
+ expect(getSelectionBeforeText([splitBlock], ' baz')).toEqual({
143
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 0},
144
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 0},
145
+ backward: false,
146
+ })
147
+ })
148
+
149
+ test(getSelectionAfterText.name, () => {
150
+ const splitBlock = {
151
+ _type: 'block',
152
+ _key: 'b1',
153
+ children: [
154
+ {_type: 'span', _key: 's1', text: 'foo '},
155
+ {_type: 'span', _key: 's2', text: 'bar'},
156
+ {_type: 'span', _key: 's3', text: ' baz'},
157
+ ],
158
+ }
159
+
160
+ expect(getSelectionAfterText([splitBlock], 'foo ')).toEqual({
161
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 4},
162
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 4},
163
+ backward: false,
164
+ })
165
+ expect(getSelectionAfterText([splitBlock], 'bar')).toEqual({
166
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's2'}], offset: 3},
167
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's2'}], offset: 3},
168
+ backward: false,
169
+ })
170
+ expect(getSelectionAfterText([splitBlock], ' baz')).toEqual({
171
+ anchor: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 4},
172
+ focus: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 4},
173
+ backward: false,
174
+ })
175
+ })
@@ -0,0 +1,122 @@
1
+ import {isPortableTextBlock, isPortableTextSpan} from '@portabletext/toolkit'
2
+ import type {PortableTextBlock} from '@sanity/types'
3
+ import type {EditorSelection, EditorSelectionPoint} from '../types/editor'
4
+ import {collapseSelection} from './collapse-selection'
5
+ import {splitString} from './split-string'
6
+ import {stringOverlap} from './string-overlap'
7
+
8
+ export function getTextSelection(
9
+ value: Array<PortableTextBlock> | undefined,
10
+ text: string,
11
+ ): EditorSelection {
12
+ if (!value) {
13
+ throw new Error(`Unable to find selection for value ${value}`)
14
+ }
15
+
16
+ let anchor: EditorSelectionPoint | undefined
17
+ let focus: EditorSelectionPoint | undefined
18
+
19
+ for (const block of value) {
20
+ if (isPortableTextBlock(block)) {
21
+ for (const child of block.children) {
22
+ if (isPortableTextSpan(child)) {
23
+ if (child.text === text) {
24
+ anchor = {
25
+ path: [{_key: block._key}, 'children', {_key: child._key}],
26
+ offset: 0,
27
+ }
28
+ focus = {
29
+ path: [{_key: block._key}, 'children', {_key: child._key}],
30
+ offset: text.length,
31
+ }
32
+ break
33
+ }
34
+
35
+ const splitChildText = splitString(child.text, text)
36
+
37
+ if (splitChildText[0] === '' && splitChildText[1] !== '') {
38
+ anchor = {
39
+ path: [{_key: block._key}, 'children', {_key: child._key}],
40
+ offset: 0,
41
+ }
42
+ focus = {
43
+ path: [{_key: block._key}, 'children', {_key: child._key}],
44
+ offset: text.length,
45
+ }
46
+ break
47
+ }
48
+
49
+ if (
50
+ splitChildText[0] !== '' &&
51
+ splitChildText[1] === '' &&
52
+ child.text.indexOf(text) !== -1
53
+ ) {
54
+ anchor = {
55
+ path: [{_key: block._key}, 'children', {_key: child._key}],
56
+ offset: child.text.length - text.length,
57
+ }
58
+ focus = {
59
+ path: [{_key: block._key}, 'children', {_key: child._key}],
60
+ offset: child.text.length,
61
+ }
62
+ break
63
+ }
64
+
65
+ if (splitChildText[0] !== '' && splitChildText[1] !== '') {
66
+ anchor = {
67
+ path: [{_key: block._key}, 'children', {_key: child._key}],
68
+ offset: splitChildText[0].length,
69
+ }
70
+ focus = {
71
+ path: [{_key: block._key}, 'children', {_key: child._key}],
72
+ offset: splitChildText[0].length + text.length,
73
+ }
74
+ break
75
+ }
76
+
77
+ const overlap = stringOverlap(child.text, text)
78
+
79
+ if (overlap !== '') {
80
+ if (child.text.lastIndexOf(overlap) >= 0 && !anchor) {
81
+ anchor = {
82
+ path: [{_key: block._key}, 'children', {_key: child._key}],
83
+ offset: child.text.lastIndexOf(overlap),
84
+ }
85
+ continue
86
+ }
87
+
88
+ if (child.text.indexOf(overlap) === 0) {
89
+ focus = {
90
+ path: [{_key: block._key}, 'children', {_key: child._key}],
91
+ offset: overlap.length,
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ if (!anchor || !focus) {
101
+ throw new Error(`Unable to find selection for text "${text}"`)
102
+ }
103
+
104
+ return {
105
+ anchor,
106
+ focus,
107
+ }
108
+ }
109
+
110
+ export function getSelectionBeforeText(
111
+ value: Array<PortableTextBlock> | undefined,
112
+ text: string,
113
+ ): EditorSelection {
114
+ return collapseSelection(getTextSelection(value, text), 'start')
115
+ }
116
+
117
+ export function getSelectionAfterText(
118
+ value: Array<PortableTextBlock> | undefined,
119
+ text: string,
120
+ ): EditorSelection {
121
+ return collapseSelection(getTextSelection(value, text), 'end')
122
+ }
@@ -0,0 +1,31 @@
1
+ import {isPortableTextBlock, isPortableTextSpan} from '@portabletext/toolkit'
2
+ import type {PortableTextBlock} from '@sanity/types'
3
+
4
+ export function getValueAnnotations(
5
+ value: Array<PortableTextBlock> | undefined,
6
+ ): Array<string> {
7
+ if (!value) {
8
+ return []
9
+ }
10
+
11
+ const annotations: Array<string> = []
12
+
13
+ for (const block of value) {
14
+ if (isPortableTextBlock(block)) {
15
+ for (const child of block.children) {
16
+ if (isPortableTextSpan(child) && child.marks) {
17
+ for (const mark of child.marks) {
18
+ if (
19
+ block.markDefs?.some((markDef) => markDef._key === mark) &&
20
+ !annotations.includes(mark)
21
+ ) {
22
+ annotations.push(mark)
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
29
+
30
+ return annotations
31
+ }
@@ -37,9 +37,6 @@ export function toSlateValue(
37
37
  if (value && Array.isArray(value)) {
38
38
  return value.map((block) => {
39
39
  const {_type, _key, ...rest} = block
40
- const voidChildren = [
41
- {_key: VOID_CHILD_KEY, _type: 'span', text: '', marks: []},
42
- ]
43
40
  const isPortableText = block && block._type === schemaTypes.block.name
44
41
  if (isPortableText) {
45
42
  const textBlock = block as PortableTextTextBlock
@@ -61,7 +58,14 @@ export function toSlateValue(
61
58
  {
62
59
  _type: cType,
63
60
  _key: cKey,
64
- children: voidChildren,
61
+ children: [
62
+ {
63
+ _key: VOID_CHILD_KEY,
64
+ _type: 'span',
65
+ text: '',
66
+ marks: [],
67
+ },
68
+ ],
65
69
  value: cRest,
66
70
  __inline: true,
67
71
  },
@@ -92,7 +96,14 @@ export function toSlateValue(
92
96
  {
93
97
  _type,
94
98
  _key,
95
- children: voidChildren,
99
+ children: [
100
+ {
101
+ _key: VOID_CHILD_KEY,
102
+ _type: 'span',
103
+ text: '',
104
+ marks: [],
105
+ },
106
+ ],
96
107
  value: rest,
97
108
  },
98
109
  keyMap,
@@ -2,6 +2,8 @@ export {
2
2
  blockOffsetToSpanSelectionPoint,
3
3
  spanSelectionPointToBlockOffset,
4
4
  } from './util.block-offset'
5
+ export {blockOffsetToBlockSelectionPoint} from './util.block-offset-to-block-selection-point'
6
+ export {blockOffsetToSelectionPoint} from './util.block-offset-to-selection-point'
5
7
  export {blockOffsetsToSelection} from './util.block-offsets-to-selection'
6
8
  export {childSelectionPointToBlockOffset} from './util.child-selection-point-to-block-offset'
7
9
  export {getBlockEndPoint} from './util.get-block-end-point'
@@ -9,10 +11,13 @@ export {getBlockStartPoint} from './util.get-block-start-point'
9
11
  export {getTextBlockText} from './util.get-text-block-text'
10
12
  export {isEmptyTextBlock} from './util.is-empty-text-block'
11
13
  export {isEqualSelectionPoints} from './util.is-equal-selection-points'
14
+ export {isEqualSelections} from './util.is-equal-selections'
12
15
  export {isKeyedSegment} from './util.is-keyed-segment'
16
+ export {isSelectionCollapsed} from './util.is-selection-collapsed'
13
17
  export {isSpan} from './util.is-span'
14
18
  export {isTextBlock} from './util.is-text-block'
15
19
  export {mergeTextBlocks} from './util.merge-text-blocks'
16
20
  export {reverseSelection} from './util.reverse-selection'
21
+ export {selectionPointToBlockOffset} from './util.selection-point-to-block-offset'
17
22
  export {sliceBlocks} from './util.slice-blocks'
18
23
  export {splitTextBlock} from './util.split-text-block'
@@ -0,0 +1,28 @@
1
+ import type {PortableTextBlock} from '@sanity/types'
2
+ import type {BlockOffset} from '../types/block-offset'
3
+ import type {EditorSelectionPoint} from '../types/editor'
4
+
5
+ /**
6
+ * @public
7
+ */
8
+ export function blockOffsetToBlockSelectionPoint({
9
+ value,
10
+ blockOffset,
11
+ }: {
12
+ value: Array<PortableTextBlock>
13
+ blockOffset: BlockOffset
14
+ }): EditorSelectionPoint | undefined {
15
+ let selectionPoint: EditorSelectionPoint | undefined
16
+
17
+ for (const block of value) {
18
+ if (block._key === blockOffset.path[0]._key) {
19
+ selectionPoint = {
20
+ path: [{_key: block._key}],
21
+ offset: blockOffset.offset,
22
+ }
23
+ break
24
+ }
25
+ }
26
+
27
+ return selectionPoint
28
+ }
@@ -0,0 +1,33 @@
1
+ import type {PortableTextBlock} from '@sanity/types'
2
+ import type {BlockOffset} from '../types/block-offset'
3
+ import type {EditorSelectionPoint} from '../types/editor'
4
+ import {blockOffsetToSpanSelectionPoint} from './util.block-offset'
5
+ import {blockOffsetToBlockSelectionPoint} from './util.block-offset-to-block-selection-point'
6
+
7
+ /**
8
+ * @public
9
+ */
10
+ export function blockOffsetToSelectionPoint({
11
+ value,
12
+ blockOffset,
13
+ direction,
14
+ }: {
15
+ value: Array<PortableTextBlock>
16
+ blockOffset: BlockOffset
17
+ direction: 'forward' | 'backward'
18
+ }): EditorSelectionPoint | undefined {
19
+ const spanSelectionPoint = blockOffsetToSpanSelectionPoint({
20
+ value,
21
+ blockOffset,
22
+ direction,
23
+ })
24
+
25
+ if (!spanSelectionPoint) {
26
+ return blockOffsetToBlockSelectionPoint({
27
+ value,
28
+ blockOffset,
29
+ })
30
+ }
31
+
32
+ return spanSelectionPoint
33
+ }
@@ -1,7 +1,7 @@
1
1
  import type {PortableTextBlock} from '@sanity/types'
2
2
  import type {EditorSelection} from '..'
3
3
  import type {BlockOffset} from '../types/block-offset'
4
- import {blockOffsetToSpanSelectionPoint} from './util.block-offset'
4
+ import {blockOffsetToSelectionPoint} from './util.block-offset-to-selection-point'
5
5
 
6
6
  /**
7
7
  * @public
@@ -15,12 +15,12 @@ export function blockOffsetsToSelection({
15
15
  offsets: {anchor: BlockOffset; focus: BlockOffset}
16
16
  backward?: boolean
17
17
  }): EditorSelection {
18
- const anchor = blockOffsetToSpanSelectionPoint({
18
+ const anchor = blockOffsetToSelectionPoint({
19
19
  value,
20
20
  blockOffset: offsets.anchor,
21
21
  direction: backward ? 'backward' : 'forward',
22
22
  })
23
- const focus = blockOffsetToSpanSelectionPoint({
23
+ const focus = blockOffsetToSelectionPoint({
24
24
  value,
25
25
  blockOffset: offsets.focus,
26
26
  direction: backward ? 'forward' : 'backward',
@@ -0,0 +1,20 @@
1
+ import type {EditorSelection} from '../types/editor'
2
+ import {isEqualSelectionPoints} from './util.is-equal-selection-points'
3
+
4
+ /**
5
+ * @public
6
+ */
7
+ export function isEqualSelections(a: EditorSelection, b: EditorSelection) {
8
+ if (!a && !b) {
9
+ return true
10
+ }
11
+
12
+ if (!a || !b) {
13
+ return false
14
+ }
15
+
16
+ return (
17
+ isEqualSelectionPoints(a.anchor, b.anchor) &&
18
+ isEqualSelectionPoints(a.focus, b.focus)
19
+ )
20
+ }
@@ -0,0 +1,15 @@
1
+ import type {EditorSelection} from '../types/editor'
2
+
3
+ /**
4
+ * @public
5
+ */
6
+ export function isSelectionCollapsed(selection: EditorSelection) {
7
+ if (!selection) {
8
+ return false
9
+ }
10
+
11
+ return (
12
+ selection.anchor.path.join() === selection.focus.path.join() &&
13
+ selection.anchor.offset === selection.focus.offset
14
+ )
15
+ }