Pasting HTML that matches schema doesn't fill the editor

I’ve run into an issue which I can’t seem to find the answer to.

Say I have the schema

{
		nodes: {
			doc: {
				content: 'headline lead block+',
			},
			text: {},
			paragraph: {
				group: 'block',
				content: 'text*',
				toDOM() {
					return ['p', 0];
				},
				parseDOM: [{ tag: 'p:not(:first-of-type)' }],
			},
			headline: {
				content: 'text*',
				marks: '',
				toDOM() {
					return ['h1', 0];
				},
				parseDOM: [{ tag: 'h1' }],
			},
			lead: {
				content: 'text*',
				toDOM() {
					return ['p', { class: 'lead' }, 0];
				},
				parseDOM: [{ tag: 'p:first-of-type' }],
			},
		},
	}

Which I expect to mean that my document will contain a headline followed by a lead followed by any number of nodes with the block group.

The editor presents the elements correctly

<h1><br class="ProseMirror-trailingBreak"></h1>
<p class="lead"><br class="ProseMirror-trailingBreak"></p>
<p><br class="ProseMirror-trailingBreak"></p>

However, if i put my cursor into the h1 and paste

<h1>An eiusdem modi?</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. An potest, inquit ille, quicquam esse suavius quam nihil dolere?</p>
<p>Tu enim ista lenius, hic Stoicorum more nos vexat. Et certamen honestum et disputatio splendida! omnis est enim de virtutis dignitate contentio.</p>

the editor refuses to paste. I assume this is because it doesn’t match the schema some how. But if I select the editors entire content and paste, it works and the content is replaced as expected.

Things I have tried:

  1. Creating the following transformPastedHTML function and updating the schema:
schema = {
  ...
  lead: {
    ...
    parseDOM: [{ tag: 'p.lead' }],
  }
}

transformPastedHTML(html, view) {
  const div = document.createElement('div');
  div.innerHTML = html;
  
  const firstParagraph = div.querySelector('h1 + p:first-of-type');
  if (firstParagraph) {
    firstParagraph.classList.add('lead');
  }
  return div.innerHTML;
}
  1. Changing the group for lead to be block, which works and displays correctly however the editors initial lead and paragraph nodes remain at the end of the document instead of being replaced. This also has the adverse effect that the lead node can be split.

I considered replacing lead with paragraph however I feel that makes it harder for me to prevent the display of a floating menu that should only be present when inside a block that can be replaced, by a list for example.

I have set up a CodePen to demonstrate this behaviour: https://codepen.io/bytespider/pen/abgYexK

All help is appreciated.

I think what’s happening is that the content already has a lead node, so though it is parsed correctly, it cannot be inserted as it is, because that would violate the schema constraints by creating a document with two leads. This type of restrictive top level content is probably gong to be a pain to work with in general.

Thank you for your reply Marijn.

Since this is likely only going to arise when a new document is created and a user is pasting content from somewhere else, I decided to check if the document is empty before updating the selection to the whole document.

handlePaste(view, event, slice) {
    const singleNode =
      slice.openStart == 0 &&
      slice.openEnd == 0 &&
      slice.content.childCount == 1
        ? slice.content.firstChild
        : null;
    
    const emptyDoc = isNodeEmpty(view.state.doc);
    let tr = singleNode
      ? view.state.tr.replaceSelectionWith(singleNode, false)
      : (emptyDoc
         ? view.state.tr.setSelection(new AllSelection(view.state.doc)).replaceSelection(slice)
         : view.state.tr.replaceSelection(slice)
        );
    view.dispatch(
      tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")
    );
    return true;
  }

The implementation of isNodeEmpty comes from tiptap/packages/core/src/helpers/isNodeEmpty.ts at develop · ueberdosis/tiptap · GitHub

Only time will tell if this is a good idea or not, or if as you suggest, this schema will be a pain.