@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.
- package/lib/_chunks-cjs/behavior.core.cjs +12 -4
- package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
- package/lib/_chunks-cjs/editor-provider.cjs +131 -109
- package/lib/_chunks-cjs/editor-provider.cjs.map +1 -1
- package/lib/_chunks-cjs/{parse-blocks.cjs → util.selection-point-to-block-offset.cjs} +74 -4
- package/lib/_chunks-cjs/util.selection-point-to-block-offset.cjs.map +1 -0
- package/lib/_chunks-cjs/util.slice-blocks.cjs +2 -2
- package/lib/_chunks-cjs/util.slice-blocks.cjs.map +1 -1
- package/lib/_chunks-cjs/util.split-text-block.cjs +68 -0
- package/lib/_chunks-cjs/util.split-text-block.cjs.map +1 -0
- package/lib/_chunks-es/behavior.core.js +12 -4
- package/lib/_chunks-es/behavior.core.js.map +1 -1
- package/lib/_chunks-es/editor-provider.js +125 -103
- package/lib/_chunks-es/editor-provider.js.map +1 -1
- package/lib/_chunks-es/{parse-blocks.js → util.selection-point-to-block-offset.js} +76 -5
- package/lib/_chunks-es/util.selection-point-to-block-offset.js.map +1 -0
- package/lib/_chunks-es/util.slice-blocks.js +2 -2
- package/lib/_chunks-es/util.slice-blocks.js.map +1 -1
- package/lib/_chunks-es/util.split-text-block.js +70 -0
- package/lib/_chunks-es/util.split-text-block.js.map +1 -0
- package/lib/behaviors/index.d.cts +383 -111
- package/lib/behaviors/index.d.ts +383 -111
- package/lib/index.cjs +198 -195
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +345 -90
- package/lib/index.d.ts +345 -90
- package/lib/index.js +205 -202
- package/lib/index.js.map +1 -1
- package/lib/plugins/index.cjs +11 -11
- package/lib/plugins/index.cjs.map +1 -1
- package/lib/plugins/index.d.cts +335 -93
- package/lib/plugins/index.d.ts +335 -93
- package/lib/plugins/index.js +2 -2
- package/lib/selectors/index.d.cts +333 -81
- package/lib/selectors/index.d.ts +333 -81
- package/lib/utils/index.cjs +15 -87
- package/lib/utils/index.cjs.map +1 -1
- package/lib/utils/index.d.cts +386 -84
- package/lib/utils/index.d.ts +386 -84
- package/lib/utils/index.js +13 -86
- package/lib/utils/index.js.map +1 -1
- package/package.json +6 -6
- package/src/behavior-actions/behavior.action.decorator.add.ts +13 -2
- package/src/behaviors/behavior.core.block-objects.ts +32 -2
- package/src/behaviors/behavior.default.ts +38 -14
- package/src/behaviors/behavior.types.ts +5 -4
- package/src/converters/converter.portable-text.ts +9 -0
- package/src/converters/converter.text-plain.test.ts +5 -5
- package/src/converters/converter.text-plain.ts +12 -19
- package/src/editor/Editable.tsx +122 -68
- package/src/editor/PortableTextEditor.tsx +8 -8
- package/src/editor/__tests__/self-solving.test.tsx +1 -1
- package/src/editor/components/Element.tsx +2 -9
- package/src/editor/create-editor.ts +13 -5
- package/src/editor/editor-machine.ts +6 -2
- package/src/editor/editor-provider.tsx +11 -7
- package/src/editor/editor-selector.ts +4 -3
- package/src/editor/editor-snapshot.ts +2 -2
- package/src/editor/plugins/create-with-event-listeners.ts +2 -5
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +1 -2
- package/src/internal-utils/block-keys.ts +9 -0
- package/src/internal-utils/collapse-selection.ts +36 -0
- package/src/internal-utils/compound-client-rect.ts +28 -0
- package/src/internal-utils/drag-selection.test.ts +507 -0
- package/src/internal-utils/drag-selection.ts +66 -0
- package/src/internal-utils/editor-selection.test.ts +40 -0
- package/src/internal-utils/editor-selection.ts +60 -0
- package/src/internal-utils/event-position.ts +55 -80
- package/src/internal-utils/inline-object-selection.ts +115 -0
- package/src/internal-utils/selection-block-keys.ts +20 -0
- package/src/internal-utils/selection-elements.ts +61 -0
- package/src/internal-utils/selection-focus-text.ts +38 -0
- package/src/internal-utils/selection-text.test.ts +23 -0
- package/src/internal-utils/selection-text.ts +90 -0
- package/src/internal-utils/split-string.ts +12 -0
- package/src/internal-utils/string-overlap.test.ts +14 -0
- package/src/internal-utils/string-overlap.ts +28 -0
- package/src/internal-utils/string-utils.ts +7 -0
- package/src/internal-utils/terse-pt.test.ts +60 -0
- package/src/internal-utils/terse-pt.ts +36 -0
- package/src/internal-utils/text-block-key.test.ts +30 -0
- package/src/internal-utils/text-block-key.ts +30 -0
- package/src/internal-utils/text-marks.test.ts +33 -0
- package/src/internal-utils/text-marks.ts +26 -0
- package/src/internal-utils/text-selection.test.ts +175 -0
- package/src/internal-utils/text-selection.ts +122 -0
- package/src/internal-utils/value-annotations.ts +31 -0
- package/src/internal-utils/values.ts +16 -5
- package/src/utils/index.ts +5 -0
- package/src/utils/util.block-offset-to-block-selection-point.ts +28 -0
- package/src/utils/util.block-offset-to-selection-point.ts +33 -0
- package/src/utils/util.block-offsets-to-selection.ts +3 -3
- package/src/utils/util.is-equal-selections.ts +20 -0
- package/src/utils/util.is-selection-collapsed.ts +15 -0
- package/src/utils/util.reverse-selection.ts +9 -5
- package/src/utils/util.selection-point-to-block-offset.ts +31 -0
- package/lib/_chunks-cjs/parse-blocks.cjs.map +0 -1
- package/lib/_chunks-es/parse-blocks.js.map +0 -1
- 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:
|
|
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:
|
|
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,
|
package/src/utils/index.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
18
|
+
const anchor = blockOffsetToSelectionPoint({
|
|
19
19
|
value,
|
|
20
20
|
blockOffset: offsets.anchor,
|
|
21
21
|
direction: backward ? 'backward' : 'forward',
|
|
22
22
|
})
|
|
23
|
-
const focus =
|
|
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
|
+
}
|