slate icon indicating copy to clipboard operation
slate copied to clipboard

Cannot find a descendant at path when emptying deeply nested editor value

Open idevelop opened this issue 4 years ago • 8 comments

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.

idevelop avatar Sep 04 '20 12:09 idevelop

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,
 });

codeGun123 avatar Sep 18 '20 07:09 codeGun123

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!

bryanph avatar Oct 25 '20 13:10 bryanph

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 avatar Jan 14 '21 07:01 chomamateusz

@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.

bryanph avatar Jan 29 '21 16:01 bryanph

I see this issue is still open. Any update about this issue? Do we have a better way to solve this?

M162 avatar Apr 27 '22 12:04 M162

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:

  1. Initialize with an empty value, which results in children being:
[
  {
    "children": [
      {
        "text": ""
      }
    ],
    "type": "paragraph"
  }
]
  1. 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": ""
      }
    ]
  }
]
  1. Undo e.g. via Cmd+Z on Mac.

  2. 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}}"
  }
]

FokkeZB avatar Aug 29 '22 09:08 FokkeZB

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;
};

antoniopresto avatar Sep 21 '23 01:09 antoniopresto