Skip to content

Commit 1d41af7

Browse files
authoredFeb 28, 2024
feat(portable-text-editor): implement isSelectionOverlapping method (#5870)
* feat(portable-text-editor): implement `isSelectionsOverlapping` method * test(portable-text-editor): add `isSelectionsOverlapping` test
1 parent f83e8e4 commit 1d41af7

File tree

4 files changed

+183
-0
lines changed

4 files changed

+183
-0
lines changed
 

‎packages/@sanity/portable-text-editor/src/editor/PortableTextEditor.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -292,4 +292,11 @@ export class PortableTextEditor extends Component<PortableTextEditorProps> {
292292
debug('Host redoing')
293293
editor.editable?.redo()
294294
}
295+
static isSelectionsOverlapping = (
296+
editor: PortableTextEditor,
297+
selectionA: EditorSelection,
298+
selectionB: EditorSelection,
299+
) => {
300+
return editor.editable?.isSelectionsOverlapping(selectionA, selectionB)
301+
}
295302
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import {describe, expect, it, jest} from '@jest/globals'
2+
import {type PortableTextBlock} from '@sanity/types'
3+
import {render, waitFor} from '@testing-library/react'
4+
import {createRef, type RefObject} from 'react'
5+
6+
import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester'
7+
import {PortableTextEditor} from '../../PortableTextEditor'
8+
9+
const INITIAL_VALUE: PortableTextBlock[] = [
10+
{
11+
_key: 'a',
12+
_type: 'block',
13+
children: [
14+
{
15+
_key: 'a1',
16+
_type: 'span',
17+
marks: [],
18+
text: 'This is some text in the block',
19+
},
20+
],
21+
markDefs: [],
22+
style: 'normal',
23+
},
24+
]
25+
26+
describe('plugin:withEditableAPI: .isSelectionsOverlapping', () => {
27+
it('returns true if the selections are partially overlapping', async () => {
28+
const editorRef: RefObject<PortableTextEditor> = createRef()
29+
const onChange = jest.fn()
30+
render(
31+
<PortableTextEditorTester
32+
onChange={onChange}
33+
ref={editorRef}
34+
schemaType={schemaType}
35+
value={INITIAL_VALUE}
36+
/>,
37+
)
38+
const selectionA = {
39+
focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 4},
40+
anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 8},
41+
}
42+
43+
const selectionB = {
44+
focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 2},
45+
anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 6},
46+
}
47+
48+
await waitFor(() => {
49+
if (editorRef.current) {
50+
const isOverlapping = PortableTextEditor.isSelectionsOverlapping(
51+
editorRef.current,
52+
selectionA,
53+
selectionB,
54+
)
55+
56+
expect(isOverlapping).toBe(true)
57+
}
58+
})
59+
})
60+
61+
it('returns true if the selections are fully overlapping', async () => {
62+
const editorRef: RefObject<PortableTextEditor> = createRef()
63+
const onChange = jest.fn()
64+
render(
65+
<PortableTextEditorTester
66+
onChange={onChange}
67+
ref={editorRef}
68+
schemaType={schemaType}
69+
value={INITIAL_VALUE}
70+
/>,
71+
)
72+
const selectionA = {
73+
focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 4},
74+
anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 8},
75+
}
76+
77+
const selectionB = {
78+
focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 4},
79+
anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 8},
80+
}
81+
82+
await waitFor(() => {
83+
if (editorRef.current) {
84+
const isOverlapping = PortableTextEditor.isSelectionsOverlapping(
85+
editorRef.current,
86+
selectionA,
87+
selectionB,
88+
)
89+
90+
expect(isOverlapping).toBe(true)
91+
}
92+
})
93+
})
94+
95+
it('return true if selection is fully inside another selection', async () => {
96+
const editorRef: RefObject<PortableTextEditor> = createRef()
97+
const onChange = jest.fn()
98+
render(
99+
<PortableTextEditorTester
100+
onChange={onChange}
101+
ref={editorRef}
102+
schemaType={schemaType}
103+
value={INITIAL_VALUE}
104+
/>,
105+
)
106+
const selectionA = {
107+
focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 2},
108+
anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 10},
109+
}
110+
111+
const selectionB = {
112+
focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 4},
113+
anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 6},
114+
}
115+
116+
await waitFor(() => {
117+
if (editorRef.current) {
118+
const isOverlapping = PortableTextEditor.isSelectionsOverlapping(
119+
editorRef.current,
120+
selectionA,
121+
selectionB,
122+
)
123+
124+
expect(isOverlapping).toBe(true)
125+
}
126+
})
127+
})
128+
129+
it('returns false if the selections are not overlapping', async () => {
130+
const editorRef: RefObject<PortableTextEditor> = createRef()
131+
const onChange = jest.fn()
132+
render(
133+
<PortableTextEditorTester
134+
onChange={onChange}
135+
ref={editorRef}
136+
schemaType={schemaType}
137+
value={INITIAL_VALUE}
138+
/>,
139+
)
140+
const selectionA = {
141+
focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 4},
142+
anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 8},
143+
}
144+
145+
const selectionB = {
146+
focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 10},
147+
anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 12},
148+
}
149+
150+
await waitFor(() => {
151+
if (editorRef.current) {
152+
const isOverlapping = PortableTextEditor.isSelectionsOverlapping(
153+
editorRef.current,
154+
selectionA,
155+
selectionB,
156+
)
157+
158+
expect(isOverlapping).toBe(false)
159+
}
160+
})
161+
})
162+
})

‎packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts

+13
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,19 @@ export function createWithEditableAPI(
515515
getFragment: () => {
516516
return fromSlateValue(editor.getFragment(), types.block.name)
517517
},
518+
isSelectionsOverlapping: (selectionA: EditorSelection, selectionB: EditorSelection) => {
519+
// Convert the selections to Slate ranges
520+
const rangeA = toSlateRange(selectionA, editor)
521+
const rangeB = toSlateRange(selectionB, editor)
522+
523+
// Make sure the ranges are valid
524+
const isValidRanges = Range.isRange(rangeA) && Range.isRange(rangeB)
525+
526+
// Check if the ranges are overlapping
527+
const isOverlapping = isValidRanges && Range.includes(rangeA, rangeB)
528+
529+
return isOverlapping
530+
},
518531
})
519532
return editor
520533
}

‎packages/@sanity/portable-text-editor/src/types/editor.ts

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface EditableAPI {
5959
isCollapsedSelection: () => boolean
6060
isExpandedSelection: () => boolean
6161
isMarkActive: (mark: string) => boolean
62+
isSelectionsOverlapping: (selectionA: EditorSelection, selectionB: EditorSelection) => boolean
6263
isVoid: (element: PortableTextBlock | PortableTextChild) => boolean
6364
marks: () => string[]
6465
redo: () => void

0 commit comments

Comments
 (0)
Please sign in to comment.