How to preserve <br> tags in empty paragraphs

Hi.

In the app I’m working on, the user can click on various HTML elements (currently P and H1-H6) to edit their contents. When the user clicks on element E, I parse the contents of E into a ProseMirror document, set up a state and a view, then render the view into E. When the user blurs the editor, I serialize the ProseMirror document and replace the contents of E with the resulting fragment.

This works as expected, with one glitch: if I append empty paragraphs (<p></p>) to the end of the edited content (by pressing Enter a number of times at the end of the text), ProseMirror renders these empty paragraphs as <p><br></p> which looks alright while the editor is active, but as soon as I blur the editor, these turn into actual empty paragraphs (<p></p>) which are not displayed by the browser.

Blurring (unmounting) the editor and then focusing (mounting) it again thus makes the empty paragraphs disappear then reappear which is not too pretty.

Do you have any pointers on how this problem could be solved? (I’d prefer not to apply any modifications to the CSS of the page.)

I managed to solve my problem.

If I represent empty paragraphs in the DOM as <p>&nbsp;</p>, these show up in the browser as expected (they do not collapse). When I parse this DOM into a ProseMirror document, I use a paragraph rule like this:

const EMPTY_PARAGRAPH_CONTENT = '\u00A0'; // non-breaking space (&nbsp;)

paragraph: {
	content: 'inline*',
	group: 'block',
	parseDOM: [
		// rule to identify empty paragraphs
		{
			tag: 'p',
			priority: 51, // try to match this first
			getAttrs(value) {
				let el = value;
				// empty paragraphs are stored with a non-visible marker
				// inside them to prevent collapse by Firefox and Chrome
				if (el.textContent !== EMPTY_PARAGRAPH_CONTENT) {
					return false; // skip this rule
				}
				// match
				return null;
			},
			getContent() {
				// remove marker while editing
				//
				// ProseMirror will add <br> tags inside <p></p> to ensure
				// that empty paragraphs show up inside the contenteditable
				//
				// we will re-add the marker when the editor is closed
				return Model.Fragment.empty;
			},
		},
		// rule to identify generic paragraphs
		{
			tag: 'p',
		},
	],
	toDOM: (node) => ['p', 0]
},

When the user leaves the contenteditable, I serialize the ProseMirror document and do an extra conversion step on the resulting DocumentFragment:

function fixEmptyParagraphs(fragment) {
	// add a non-visible marker to all empty paragraphs to prevent
	// visual collapse of the paragraphs in Firefox and Chrome
	function fix(node) {
		if (node.hasChildNodes()) {
			node.childNodes.forEach(fix);
		} else if (node instanceof HTMLElement && node.tagName === 'P') {
			node.appendChild(document.createTextNode(EMPTY_PARAGRAPH_CONTENT));
		}
	}

	fix(fragment);
}

This way the &nbsp; is only present in the saved data, ProseMirror does not see it and can employ its BR hacks to ensure empty paragraphs are shown as intended internally.

The <br> nodes in empty textblocks are a standard kludge in editable content to make sure empty blocks show up and avoid weird cursor issues. ProseMirror doesn’t add them to the actual document content, but just renders them in the editable view. Making the paragraphs show up when rendered outside the editor is a separate problem, which you could fix with, for example, a css rule using the :empty pseudoselector.