mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-22 06:53:25 +02:00
Fix NODE_TO_KEY correction for split_node and merge_node (#4901)
* Fix NODE_TO_KEY correction for split_node and merge_node * fix lint * add changeset * Add NODE_TO_KEY tests for number of mounts for split_node and merge_node
This commit is contained in:
5
.changeset/funny-humans-whisper.md
Normal file
5
.changeset/funny-humans-whisper.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'slate-react': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixes a bug where nodes remounted on split_node and merge_node
|
@@ -60,42 +60,38 @@ export const withReact = <T extends Editor>(editor: T) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This attempts to reset the NODE_TO_KEY entry to the correct value
|
||||||
|
// as apply() changes the object reference and hence invalidates the NODE_TO_KEY entry
|
||||||
e.apply = (op: Operation) => {
|
e.apply = (op: Operation) => {
|
||||||
const matches: [Path, Key][] = []
|
const matches: [Path, Key][] = []
|
||||||
|
|
||||||
switch (op.type) {
|
switch (op.type) {
|
||||||
case 'insert_text':
|
case 'insert_text':
|
||||||
case 'remove_text':
|
case 'remove_text':
|
||||||
case 'set_node': {
|
case 'set_node':
|
||||||
for (const [node, path] of Editor.levels(e, { at: op.path })) {
|
case 'split_node': {
|
||||||
const key = ReactEditor.findKey(e, node)
|
matches.push(...getMatches(e, op.path))
|
||||||
matches.push([path, key])
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'insert_node':
|
case 'insert_node':
|
||||||
case 'remove_node':
|
case 'remove_node': {
|
||||||
case 'merge_node':
|
matches.push(...getMatches(e, Path.parent(op.path)))
|
||||||
case 'split_node': {
|
break
|
||||||
for (const [node, path] of Editor.levels(e, {
|
|
||||||
at: Path.parent(op.path),
|
|
||||||
})) {
|
|
||||||
const key = ReactEditor.findKey(e, node)
|
|
||||||
matches.push([path, key])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'merge_node': {
|
||||||
|
const prevPath = Path.previous(op.path)
|
||||||
|
matches.push(...getMatches(e, prevPath))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'move_node': {
|
case 'move_node': {
|
||||||
for (const [node, path] of Editor.levels(e, {
|
const commonPath = Path.common(
|
||||||
at: Path.common(Path.parent(op.path), Path.parent(op.newPath)),
|
Path.parent(op.path),
|
||||||
})) {
|
Path.parent(op.newPath)
|
||||||
const key = ReactEditor.findKey(e, node)
|
)
|
||||||
matches.push([path, key])
|
matches.push(...getMatches(e, commonPath))
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,3 +251,12 @@ export const withReact = <T extends Editor>(editor: T) => {
|
|||||||
|
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getMatches = (e: Editor, path: Path) => {
|
||||||
|
const matches: [Path, Key][] = []
|
||||||
|
for (const [n, p] of Editor.levels(e, { at: path })) {
|
||||||
|
const key = ReactEditor.findKey(e, n)
|
||||||
|
matches.push([p, key])
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { createEditor, NodeEntry, Range } from 'slate'
|
import {
|
||||||
|
createEditor,
|
||||||
|
NodeEntry,
|
||||||
|
Node,
|
||||||
|
Range,
|
||||||
|
Element,
|
||||||
|
Transforms,
|
||||||
|
} from 'slate'
|
||||||
import { create, act, ReactTestRenderer } from 'react-test-renderer'
|
import { create, act, ReactTestRenderer } from 'react-test-renderer'
|
||||||
import {
|
import {
|
||||||
Slate,
|
Slate,
|
||||||
@@ -11,14 +18,14 @@ import {
|
|||||||
DefaultLeaf,
|
DefaultLeaf,
|
||||||
} from '../src'
|
} from '../src'
|
||||||
|
|
||||||
describe('slate-react', () => {
|
|
||||||
describe('Editable', () => {
|
|
||||||
describe('decorate', () => {
|
|
||||||
const createNodeMock = () => ({
|
const createNodeMock = () => ({
|
||||||
ownerDocument: global.document,
|
ownerDocument: global.document,
|
||||||
getRootNode: () => global.document,
|
getRootNode: () => global.document,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('slate-react', () => {
|
||||||
|
describe('Editable', () => {
|
||||||
|
describe('decorate', () => {
|
||||||
it('should be called on all nodes in document', () => {
|
it('should be called on all nodes in document', () => {
|
||||||
const editor = withReact(createEditor())
|
const editor = withReact(createEditor())
|
||||||
const value = [{ type: 'block', children: [{ text: '' }] }]
|
const value = [{ type: 'block', children: [{ text: '' }] }]
|
||||||
@@ -172,5 +179,72 @@ describe('slate-react', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('NODE_TO_KEY logic', () => {
|
||||||
|
it('should not unmount the node that gets split on a split_node operation', async () => {
|
||||||
|
const editor = withReact(createEditor())
|
||||||
|
const value = [{ type: 'block', children: [{ text: 'test' }] }]
|
||||||
|
const mounts = jest.fn<void, [Element]>()
|
||||||
|
|
||||||
|
let el: ReactTestRenderer
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
el = create(
|
||||||
|
<Slate editor={editor} value={value} onChange={() => {}}>
|
||||||
|
<DefaultEditable
|
||||||
|
renderElement={({ element, children }) => {
|
||||||
|
React.useEffect(() => mounts(element), [])
|
||||||
|
|
||||||
|
return children
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Slate>,
|
||||||
|
{ createNodeMock }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// slate updates at next tick, so we need this to be async
|
||||||
|
await act(async () =>
|
||||||
|
Transforms.splitNodes(editor, { at: { path: [0, 0], offset: 2 } })
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2 renders, one for the main element and one for the split element
|
||||||
|
expect(mounts).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not unmount the node that gets merged into on a merge_node operation', async () => {
|
||||||
|
const editor = withReact(createEditor())
|
||||||
|
const value = [
|
||||||
|
{ type: 'block', children: [{ text: 'te' }] },
|
||||||
|
{ type: 'block', children: [{ text: 'st' }] },
|
||||||
|
]
|
||||||
|
const mounts = jest.fn<void, [Element]>()
|
||||||
|
|
||||||
|
let el: ReactTestRenderer
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
el = create(
|
||||||
|
<Slate editor={editor} value={value} onChange={() => {}}>
|
||||||
|
<DefaultEditable
|
||||||
|
renderElement={({ element, children }) => {
|
||||||
|
React.useEffect(() => mounts(element), [])
|
||||||
|
|
||||||
|
return children
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Slate>,
|
||||||
|
{ createNodeMock }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// slate updates at next tick, so we need this to be async
|
||||||
|
await act(async () =>
|
||||||
|
Transforms.mergeNodes(editor, { at: { path: [0, 0], offset: 0 } })
|
||||||
|
)
|
||||||
|
|
||||||
|
// only 2 renders for the initial render
|
||||||
|
expect(mounts).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user