Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/legal-bottles-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@portabletext/editor': patch
---

fix: handle incoming `set` patches targeting text blocks
47 changes: 46 additions & 1 deletion packages/editor/src/internal-utils/applyPatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
parsePatch,
} from '@sanity/diff-match-patch'
import type {Path, PortableTextBlock, PortableTextChild} from '@sanity/types'
import {Element, Node, Text, Transforms, type Descendant} from 'slate'
import {Editor, Element, Node, Text, Transforms, type Descendant} from 'slate'
import type {EditorSchema} from '../editor/editor-schema'
import {KEY_TO_SLATE_ELEMENT} from '../editor/weakMaps'
import type {PortableTextSlateEditor} from '../types/editor'
Expand Down Expand Up @@ -202,6 +202,51 @@ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {

const isTextBlock = editor.isTextBlock(block.node)

if (patch.path.length === 1) {
const updatedBlock = applyAll(block.node, [
{
...patch,
path: patch.path.slice(1),
},
])

if (editor.isTextBlock(block.node) && Element.isElement(updatedBlock)) {
Transforms.setNodes(editor, updatedBlock, {at: [block.index]})

const previousSelection = editor.selection

// Remove the previous children
for (const [_, childPath] of Editor.nodes(editor, {
at: [block.index],
reverse: true,
mode: 'lowest',
})) {
Transforms.removeNodes(editor, {at: childPath})
}

// Insert the new children
Transforms.insertNodes(editor, updatedBlock.children, {
at: [block.index, 0],
})

// Restore the selection
if (previousSelection) {
// Update the selection on the editor object
Transforms.setSelection(editor, previousSelection)
// Actively select the previous selection
Transforms.select(editor, previousSelection)
}

return true
} else {
Transforms.setNodes(editor, updatedBlock as Partial<Node>, {
at: [block.index],
})

return true
}
}

if (isTextBlock && patch.path[1] !== 'children') {
const updatedBlock = applyAll(block.node, [
{
Expand Down
86 changes: 86 additions & 0 deletions packages/editor/tests/event.patches.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1911,4 +1911,90 @@ describe('event.patches', () => {
})
})
})

test('Scenario: `set` block with new markDef', async () => {
const keyGenerator = createTestKeyGenerator()
const blockKey = keyGenerator()
const fooKey = keyGenerator()
const barKey = keyGenerator()
const linkKey = keyGenerator()
const newLinkKey = keyGenerator()

// Given the text foo,bar
// And a link around "foo"
const {editor, locator} = await createTestEditor({
keyGenerator,
initialValue: [
{
_key: blockKey,
_type: 'block',
children: [
{_key: fooKey, _type: 'span', text: 'foo', marks: [linkKey]},
{_key: barKey, _type: 'span', text: 'bar', marks: []},
],
markDefs: [{_key: linkKey, _type: 'link'}],
style: 'normal',
},
],
schemaDefinition: defineSchema({
annotations: [{name: 'link'}],
styles: [{name: 'normal'}, {name: 'h1'}],
}),
})

// When the cursor is put after "foo b"
await userEvent.click(locator)
const midBarSelection = {
anchor: {
path: [{_key: blockKey}, 'children', {_key: barKey}],
offset: 1,
},
focus: {
path: [{_key: blockKey}, 'children', {_key: barKey}],
offset: 1,
},
backward: false,
}
editor.send({
type: 'select',
at: midBarSelection,
})
await vi.waitFor(() => {
expect(editor.getSnapshot().context.selection).toEqual(midBarSelection)
})

// And the block is replaced with a new block with different link _key
const newBlock = {
_key: blockKey,
_type: 'block',
children: [
{_key: fooKey, _type: 'span', text: 'foo', marks: [newLinkKey]},
{_key: barKey, _type: 'span', text: 'bar', marks: []},
],
markDefs: [{_key: newLinkKey, _type: 'link'}],
style: 'normal',
}
editor.send({
type: 'patches',
patches: [
{
origin: 'remote',
type: 'set',
path: [{_key: blockKey}],
value: newBlock,
},
],
snapshot: [newBlock],
})

// Then the block is replaced with the new block
await vi.waitFor(() => {
expect(editor.getSnapshot().context.value).toEqual([newBlock])
})

// And the selection is restored
await vi.waitFor(() => {
expect(editor.getSnapshot().context.selection).toEqual(midBarSelection)
})
})
})