Paragraphs inside blocks: how to create a new block?

Short version: If I have a schema that contains lines (<p>) inside “blocks” (<div>), what command/code can I use to create a new “block”, or make it so that hitting Enter on a blank line creates one?

Long version: I’m trying to use ProseMirror to replace what is currently just a textarea. In the textarea, we’ve been following the convention that a blank line (two consecutive line breaks) separates paragraphs (blocks) of text. (Line breaks are significant because the editor is meant for proofreading a scanned page.)

After my first attempt where I tried to model the document/page as “paragraphs” (<p>) containing inline hard breaks (<br>), I decided I prefer instead to have nodes in the tree for individual lines (so <p>), with the “paragraphs”/“blocks” (now <div>s enclosing the <p> lines) containing them, with a schema like:

const schema = new Schema({
  nodes: {
    // The document (page) is a sequence of blocks.
    doc: { content: 'block*' },
    // A block is a sequence of lines. Represented in the DOM as a `<div>`.
    block: {
      content: "line+",
      toDOM() { return ['div', 0] as DOMOutputSpec; },
    },
    // A line contains text. Represented in the DOM as a `<p>` element.
    line: {
      content: 'text*',
      parseDOM: [{ tag: 'p' }],
      toDOM() { return ['p', 0] as DOMOutputSpec; },
    },
    text: { inline: true },
  },
});

But that’s as far as I’ve got, because now I’m not sure how to work with the “paragraphs”/“blocks” (the <div>s), specifically how to create a new block. So I’m looking for an existing function, or some hints about how to write code that does (I imagine) something like:

  • Find the enclosing block (<div>) around the current cursor / selection,
  • Create and append a new sibling block,
  • Move selection to inside the new block.

And then I suppose I could also bind Enter to look at the current line and call the above createNewBlock when the current line is empty, so that hitting Enter on a blank line will start a new block.

By looking at the source code of splitBlock at prosemirror-commands/commands.ts at da45eaa53968bd373727b85adfc18637397b9792 · ProseMirror/prosemirror-commands · GitHub and removing everything I didn’t understand or didn’t seem necessary for my use-case for now, I came up with this which seems to work (the depth of “2” is the difference):

export const newBlock: Command = (state, dispatch) => {
  if (dispatch) {
    let tr = state.tr
    tr.split(state.selection.$head.pos, 2)
    dispatch(tr)
  }
  return true
}

and used as (in plugins when creating the EditorState):

      keymap({ 'Mod-Enter': newBlock }),

For making this happen automatically when Enter is typed when at the start of the line, I’m not sure what’s the best way to check for being at the start of the line, but this seems to work:

export const newBlockIfAtStartOfLine: Command = (state, dispatch) => {
  let $head = state.selection.$head
  // If at start of the line, start a new block.
  if ($head.pos == $head.posAtIndex(0)) {
    return newBlock(state, dispatch)
  }
  return false
}

used as

      keymap({ 'Enter': newBlockIfAtStartOfLine }),