slate
slate copied to clipboard
Cannot find a descendant at path when emptying deeply nested editor value
I'm integrating the code examples into our app and I'm hitting an issue that might be a bug, or me not understanding how to correctly reset the editor state.
Here's a quick summary of the React structure of our Composer component:
const EMPTY = [{ children: [{ text: '' }] }];
const [value, setValue] = useState<Node[]>(EMPTY);
<Slate
editor={editor}
value={value}
onChange={(newValue) => setValue(newValue)}
>
<Editable
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
sendMessage(value);
// clear the input
Transforms.select(editor, Editor.start(editor, [])); // move the cursor to the beginning of the input before we clear it
setValue(EMPTY);
}
}}
/>
</Slate>
This works fine when the value in the editor is simple, but when the editor has a value that is deeply nested, like a list with one list item:
[
{
"type": "ul",
"children": [
{
"type": "li",
"children": [
{
"text": "list item"
}
]
}
]
}
]
... I get this error in the console after clearing the editor:
Uncaught Error: Cannot find a descendant at path [0,0,0] in node: {"children":[{"children":[{"text":""}]}],"operations":[{"type":"set_selection","properties":{"anchor":{"path":[0,0,0],"offset":4},"focus":{"path":[0,0,0],"offset":4}},"newProperties":{"anchor":{"path":[0,0,0],"offset":0},"focus":{"path":[0,0,0],"offset":0}}}],"selection":{"anchor":{"path":[0,0,0],"offset":0},"focus":{"path":[0,0,0],"offset":0}},"marks":null}
It seems that, even though the selection range was set to empty, it still holds on to the depth information, the fact that the focus was on a node that was 3 levels deep in the value, so I'm guessing either I have to reset the entire path somehow, or this is a bug.
I managed to fix this by changing the clear transform to:
Transforms.select(editor, {
anchor: { path: [0, 0], offset: 0 },
focus: { path: [0, 0], offset: 0 },
})
I'm curious if there's another, more straightforward way to use the Editor API to get the same result.
Almost similar problems with you are solved in this way. I hope there is a better solution. The following is my way;
Transforms.select(editor, {
path: [p1, p2, p3 + 1, 0],
offset: 2,
});
This error also happens when editor.selection
is null
throughout the lifecycle of the editor object. The workaround mentioned by @idevelop works for me for now though!
Same problem here.
My editor breaks when state is rested from more than one paragraph to empty via value
prop in <Slate>
component from slate-react
.
Is this will be somehow solved in future releases or @idevelop workaround is all we have for now?
@chomamateusz There used to be set_value
operation for this particular use-case but now you might be better off recreating the editor component when this happens so that you don't hold on to previous information. It would be nice if there was a reliable way to reset the editor without having to recreate the component though.
I see this issue is still open. Any update about this issue? Do we have a better way to solve this?
We love Slate at Zapier, but run into this issue as well, when a user undo's pasting a value that results in nodes with an empty leaf text node. This is how we can reproduce:
- Initialize with an empty value, which results in
children
being:
[
{
"children": [
{
"text": ""
}
],
"type": "paragraph"
}
]
- Paste something that we translate to:
[
{
"type": "paragraph",
"children": [
{
"text": "sdfsdfsdfsdf"
},
{
"type": "mapped-field",
"value": "166003157__id",
"children": [
{
"text": ""
}
]
},
{
"text": ""
},
{
"type": "mapped-field",
"value": "166003157__hello",
"children": [
{
"text": ""
}
]
},
{
"text": ""
}
]
}
]
-
Undo e.g. via Cmd+Z on Mac.
-
You'll get:
This happens after the 2nd of these inverseOps
:
[
{
"type": "set_selection",
"properties": {
"anchor": {
"path": [
0,
0
],
"offset": 0
},
"focus": {
"path": [
0,
0
],
"offset": 0
}
},
"newProperties": {
"anchor": {
"path": [
0,
4
],
"offset": 0
},
"focus": {
"path": [
0,
4
],
"offset": 0
}
}
},
{
"type": "insert_node",
"path": [
0,
2
],
"node": {
"text": ""
}
},
{
"type": "set_selection",
"properties": {
"anchor": {
"path": [
0,
5
],
"offset": 0
},
"focus": {
"path": [
0,
5
],
"offset": 0
}
},
"newProperties": {
"anchor": {
"path": [
0,
2
],
"offset": 0
},
"focus": {
"path": [
0,
2
],
"offset": 0
}
}
},
{
"type": "remove_node",
"path": [
0,
5
],
"node": {
"text": ""
}
},
{
"type": "remove_node",
"path": [
0,
4
],
"node": {
"type": "mapped-field",
"value": "166003157__hello",
"children": [
{
"text": ""
}
]
}
},
{
"type": "insert_text",
"path": [
0,
3
],
"offset": 0,
"text": "{{166003157__hello}}"
},
{
"type": "set_selection",
"properties": {
"anchor": {
"path": [
0,
2
],
"offset": 0
},
"focus": {
"path": [
0,
2
],
"offset": 0
}
},
"newProperties": {
"anchor": {
"path": [
0,
3
],
"offset": 20
},
"focus": {
"path": [
0,
3
],
"offset": 20
}
}
},
{
"type": "remove_node",
"path": [
0,
2
],
"node": {
"text": ""
}
},
{
"type": "remove_node",
"path": [
0,
1
],
"node": {
"type": "mapped-field",
"value": "166003157__id",
"children": [
{
"text": ""
}
]
}
},
{
"type": "merge_node",
"path": [
0,
1
],
"position": 12,
"properties": {}
},
{
"type": "insert_text",
"path": [
0,
0
],
"offset": 12,
"text": "{{166003157__id}}"
},
{
"type": "remove_text",
"path": [
0,
0
],
"offset": 0,
"text": "sdfsdfsdfsdf{{166003157__id}}{{166003157__hello}}"
}
]
This plugin worked for me:
import { Editor, Element, Node, Transforms } from 'slate';
function nodeHasNoText(node: Node) {
return Node.string(node) === '';
}
export const withDeleteCheck = (editor: Editor) => {
const { deleteBackward, deleteForward } = editor;
function isEditorEmpty() {
return editor.children.length === 0 || (editor.children.length === 1 && Node.string(editor.children[0]) === '');
}
function ensureNotEmpty() {
if (isEditorEmpty()) {
Transforms.insertNodes(editor, { type: 'paragraph', children: [{ text: '' }] });
}
}
editor.deleteBackward = (unit) => {
if (!isEditorEmpty()) {
deleteBackward(unit);
return;
}
// OPTIONAL:
removeEmptyNodes();
};
editor.deleteForward = (unit) => {
if (!isEditorEmpty()) {
deleteForward(unit);
return;
}
// OPTIONAL:
removeEmptyNodes();
};
// OPTIONAL:
const removeEmptyNodes = () => {
for (const [node, path] of Node.descendants(editor, { reverse: true })) {
if (Element.isElement(node) && nodeHasNoText(node)) {
if (isEditorEmpty()) {
ensureNotEmpty();
return;
}
Transforms.removeNodes(editor, { at: path });
}
}
};
return editor;
};