ianstormtaylor / slate

A completely customizable framework for building rich text editors. (Currently in beta.)
http://slatejs.org
MIT License
29.96k stars 3.25k forks source link

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

Open idevelop opened 4 years ago

idevelop commented 4 years ago

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.

codeGun123 commented 4 years ago

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,
 });
bryanph commented 4 years ago

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!

chomamateusz commented 3 years ago

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?

bryanph commented 3 years ago

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

M162 commented 2 years ago

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

FokkeZB commented 2 years ago

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}}"
  }
]
antoniopresto commented 1 year ago

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;
};
HarrisonKeeling commented 5 months ago

We've been having this issue for awhile, but seems to be a different root cause.

We have a reorderable report (similar to Notion) but if a state change is triggered right before the Transform.moveNodes is called, it seems to result in a slate render being initiated, but only completing after the transform is finished. Which, in our case, means it would try to render a child that had already been moved.

We defer state changes until after committing the transforms and it seems to be OK. This also only was happening with a report with greater than two levels of depth.