How to control content inserting?

Hi everyone,

We’re trying to figure out how best to insert content into a document. When the user is editing a node, we would like to provide an option for them to insert a new node into the parent of the node they are editing.

For example, consider this simplified schema:

const mySchema = new Schema({
  nodes: {
    doc: { content: "html" },
    html: { content: "title body?", toDOM: () => ["html", {}, 0] },
    title: { content: "text*", toDOM: () => ["title", {}, 0] },
    body: { content: "(ul|p)*", toDOM: () => ["body", {}, 0] },
    ul: { content: "li*", toDOM: () => ["ul", {}, 0] },
    li: { content: "p*", toDOM: () => ["li", {}, 0] },
    p: { content: "text*", toDOM: () => ["p", {}, 0] },
    text: { inline: true },
  },
});

Let’s say the cursor is deep inside a <p> (paragraph) within a <li> inside a <ul>, we would like that when the user presses Enter to be able to insert a new <p> tag after the </ul>, at the parent level, e.g. inside the <body> tag.

<html>
<title>Example of image mixed with block nodes</title>
<body>
  <p>Text in the body node</p>
  <ul>
    <li>
      <p>Text in the li node</p> <!-- NOTE: The cursor is placed within this <p> tag -->
    </li>
  </ul>
</body>
</html>

We’re trying to implement this via a custom keymap:

const enterKeyPlugin = keymap({
  Enter: (state, dispatch) => {
    const { $from } = state.selection;
    const { schema } = state;

    if (!dispatch) return true;

    const paragraph = schema.nodes.p.createAndFill();

    if (!paragraph) return false;

    const tr = state.tr.insert($from.after(), paragraph);

    const resolvedPos = tr.doc.resolve($from.after() + 1);
    const selection = TextSelection.near(resolvedPos);

    dispatch(tr.setSelection(selection).scrollIntoView());

    return true; // prevent default
  },
});

When we try to insert a <p> tag after the nested paragraph in the <ul> (list), we find that it is being joined as a child to the <li> tag, and unfortunately we can’t seem to escape that and place the new <p> in the <body>

Our question:

  • How do we correctly find the position in the parent node (e.g. body) to insert new content when we are in a deeply nested selection?

  • What’s the best way to “climb up” from the current position to find the correct ancestor and insert after it?

This comes up for us in several scenarios — like exiting a list and inserting a paragraph after it, or inserting sibling nodes at higher structural levels.

Thanks in advance for any advice!

Sample code example https://codesandbox.io/p/devbox/confident-hooks-forked-5pm7q7?workspaceId=ws_GkHAzkZJBA2J7JZx2YhuSS

You can walk up the depth of a ResolvedPos like selection.$from to look at each of its parent nodes, and then use the object’s methods to find a position in that node.

But if your schema allows list items to have multiple paragraphs, it seems very surprising, to the user, when Enter doesn’t actually split the paragraph they are in.

1 Like