Skip to content

Commit 095df8d

Browse files
committed
fix: handle incoming set patches targeting text blocks
1 parent 51759b8 commit 095df8d

3 files changed

Lines changed: 133 additions & 1 deletion

File tree

.changeset/legal-bottles-love.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@portabletext/editor': patch
3+
---
4+
5+
fix: handle incoming `set` patches targeting text blocks

packages/editor/src/internal-utils/applyPatch.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
parsePatch,
1717
} from '@sanity/diff-match-patch'
1818
import type {Path, PortableTextBlock, PortableTextChild} from '@sanity/types'
19-
import {Element, Node, Text, Transforms, type Descendant} from 'slate'
19+
import {Editor, Element, Node, Text, Transforms, type Descendant} from 'slate'
2020
import type {EditorSchema} from '../editor/editor-schema'
2121
import {KEY_TO_SLATE_ELEMENT} from '../editor/weakMaps'
2222
import type {PortableTextSlateEditor} from '../types/editor'
@@ -202,6 +202,47 @@ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {
202202

203203
const isTextBlock = editor.isTextBlock(block.node)
204204

205+
if (patch.path.length === 1) {
206+
const updatedBlock = applyAll(block.node, [
207+
{
208+
...patch,
209+
path: patch.path.slice(1),
210+
},
211+
])
212+
213+
if (editor.isTextBlock(block.node) && Element.isElement(updatedBlock)) {
214+
Transforms.setNodes(editor, updatedBlock, {at: [block.index]})
215+
216+
const previousSelection = editor.selection
217+
218+
// Remove the previous children
219+
for (const [_, childPath] of Editor.nodes(editor, {
220+
at: [block.index],
221+
reverse: true,
222+
mode: 'lowest',
223+
})) {
224+
Transforms.removeNodes(editor, {at: childPath})
225+
}
226+
227+
// Insert the new children
228+
Transforms.insertNodes(editor, updatedBlock.children, {
229+
at: [block.index, 0],
230+
})
231+
232+
if (previousSelection) {
233+
Transforms.select(editor, previousSelection)
234+
}
235+
236+
return true
237+
} else {
238+
Transforms.setNodes(editor, updatedBlock as Partial<Node>, {
239+
at: [block.index],
240+
})
241+
242+
return true
243+
}
244+
}
245+
205246
if (isTextBlock && patch.path[1] !== 'children') {
206247
const updatedBlock = applyAll(block.node, [
207248
{

packages/editor/tests/event.patches.test.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1911,4 +1911,90 @@ describe('event.patches', () => {
19111911
})
19121912
})
19131913
})
1914+
1915+
test('Scenario: `set` block with new markDef', async () => {
1916+
const keyGenerator = createTestKeyGenerator()
1917+
const blockKey = keyGenerator()
1918+
const fooKey = keyGenerator()
1919+
const barKey = keyGenerator()
1920+
const linkKey = keyGenerator()
1921+
const newLinkKey = keyGenerator()
1922+
1923+
// Given the text foo,bar
1924+
// And a link around "foo"
1925+
const {editor, locator} = await createTestEditor({
1926+
keyGenerator,
1927+
initialValue: [
1928+
{
1929+
_key: blockKey,
1930+
_type: 'block',
1931+
children: [
1932+
{_key: fooKey, _type: 'span', text: 'foo', marks: [linkKey]},
1933+
{_key: barKey, _type: 'span', text: 'bar', marks: []},
1934+
],
1935+
markDefs: [{_key: linkKey, _type: 'link'}],
1936+
style: 'normal',
1937+
},
1938+
],
1939+
schemaDefinition: defineSchema({
1940+
annotations: [{name: 'link'}],
1941+
styles: [{name: 'normal'}, {name: 'h1'}],
1942+
}),
1943+
})
1944+
1945+
// When the cursor is put after "foo b"
1946+
await userEvent.click(locator)
1947+
const midBarSelection = {
1948+
anchor: {
1949+
path: [{_key: blockKey}, 'children', {_key: barKey}],
1950+
offset: 1,
1951+
},
1952+
focus: {
1953+
path: [{_key: blockKey}, 'children', {_key: barKey}],
1954+
offset: 1,
1955+
},
1956+
backward: false,
1957+
}
1958+
editor.send({
1959+
type: 'select',
1960+
at: midBarSelection,
1961+
})
1962+
await vi.waitFor(() => {
1963+
expect(editor.getSnapshot().context.selection).toEqual(midBarSelection)
1964+
})
1965+
1966+
// And the block is replaced with a new block with different link _key
1967+
const newBlock = {
1968+
_key: blockKey,
1969+
_type: 'block',
1970+
children: [
1971+
{_key: fooKey, _type: 'span', text: 'foo', marks: [newLinkKey]},
1972+
{_key: barKey, _type: 'span', text: 'bar', marks: []},
1973+
],
1974+
markDefs: [{_key: newLinkKey, _type: 'link'}],
1975+
style: 'normal',
1976+
}
1977+
editor.send({
1978+
type: 'patches',
1979+
patches: [
1980+
{
1981+
origin: 'remote',
1982+
type: 'set',
1983+
path: [{_key: blockKey}],
1984+
value: newBlock,
1985+
},
1986+
],
1987+
snapshot: [newBlock],
1988+
})
1989+
1990+
// Then the block is replaced with the new block
1991+
await vi.waitFor(() => {
1992+
expect(editor.getSnapshot().context.value).toEqual([newBlock])
1993+
})
1994+
1995+
// And the selection is restored
1996+
await vi.waitFor(() => {
1997+
expect(editor.getSnapshot().context.selection).toEqual(midBarSelection)
1998+
})
1999+
})
19142000
})

0 commit comments

Comments
 (0)