Root Custom Node as wrapper around blocks

Hey there, I’m fairly new with Prosemirror. We have this requirement in our application that whatever user writes within the editor should be divided into pages. So in order to achieve this, I’ve created a custom Page node with the following schema:

  const pageDOM: DOMOutputSpec = ['div', { class: 'prosemirror-page' }, 0]
  const page: NodeSpec = {
    content: 'block*',
    group: 'block',
    defining: true,
    draggable: false,
    parseDOM: [{ tag: 'div.prosemirror-page' }],
    toDOM () {
        return pageDOM
    }
}

And also my top-level doc looks like this, which is intended and every other block or inline content should be within one or more Page blocks.

const doc: NodeSpec = {
  content: 'page+'
}

While when the editor mounts, It already loads with a Page node as the root node, I want to achieve certain behaviours, and I’m not sure what would be the right approach. It’d be great If I could have some help on some of these and preferably examples that can help me get started because I looked it up and I couldn’t find similar questions.

I want to enable users to:

  1. Be able to add new Pages by pressing Enter twice (It means that once they hit enter it goes to the next line and if the next line is empty or is a hard break) it will add a new empty Page for them with selection starting in the newly created Page block
  2. Similar to 1, be able to break Pages into two, by doing the Double Enter thingy but this time, assuming that the line isn’t empty on second Enter

From what I’ve seen on the examples, especially NoteGroup and Notes example I assume that I should be defining a custom keymap behaviour on top of the default one for Enter key and use Lift/Wrap functionality to move content to a new Page by lifting it first and then wrapping it. But I’m not sure if that would be right, or if there are better approaches.

Yes, binding a custom enter handler that checks for the circumstances (in empty block at top level of page, I suppose) that trigger this behavior, and then creating a node-splitting transaction, sounds like the right direction.

Possibly relevant: A4 pages conceptual guide

Thanks for the reply, yes, after struggling for a bit yesterday with lift and wrap I finally got it to work with split. While it works perfectly fine with split, with lift and wrap however, instead of dividing them into two separate Page nodes (or wrappers), it’d keep on wrapping the empty paragraph in nested Page nodes as if it was never lifting them out of the existing wrapper Page node, which is quite weird. Maybe I missed something. Something like this:

|- page
|-- page
|--- page
|---- paragraph
|----- hard break

Anyway, I’ll leave what worked for me here in case someone else needs something similar:

export const splitPage: Command<ProsemirrorSchema> = ({ selection, schema, tr: transaction }, dispatch) => {
  const { empty: selectionIsEmpty, $from: cursorStart } = selection

  if (selectionIsEmpty && dispatch) {
    // Evaluates if the cursor position is at the start of a new line where there are no nodes before or after cursor
    const EMPTY_LINE = cursorStart.nodeBefore === null && cursorStart.nodeAfter === null

    // Evaluates if the cursor position is at the start of a new line but there is content after the cursor
    const BREAK_LINE = cursorStart.nodeBefore === null && cursorStart.nodeAfter !== null

    if (EMPTY_LINE || BREAK_LINE) {
      const pageNodePosition = cursorStart.pos - 1 // Let's walk back one pos to be in the Page Node
      const pageNodeDepth = 1 // Since Page nodes are always at the highest level in our schema

      dispatch(transaction.split(pageNodePosition, pageNodeDepth, [{ type: schema.nodes.page }]))

      return true
    }
  }
  return false
}

I’d appreciate it if you could add your thoughts on why a sequence of lifting and wrapping transactions wouldn’t work than split because I thought maybe they’re similar but lift and wrap provide more flexibility?

The problem may have been that your schema doesn’t allow paragraphs at the top level, so lifting just didn’t work (the document has to conform to the schema at every point, even in multi-step transactions).

1 Like

Interesting, thanks for the explanation @marjin, it now makes sense. So in case, we only want to allow pages, what would be the best approach? Perhaps, adding paragraphs to the doc content property but only allowing page insertion?

I think you should be able to do everything you need with splitting. If not, consider directly creating ReplaceAroundSteps directly, which is a bit more involved, but should be powerful enough to express almost any structural transformation.

1 Like

Awesome, got it thanks for the help and amazing work! @marjin