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.)
If I represent empty paragraphs in the DOM as <p> </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 ( )
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 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.
Hi, sorry to bump this old topic. I too want the output HTML to look the same as in the editor-view. But I cannot get this to work. Modifying the DOMSerializer would be perfect. That way I could just modify this function provided by the TipTap team:
import { DOMSerializer, Node, Schema } from '@tiptap/pm/model'
import { createHTMLDocument, VHTMLDocument } from 'zeed-dom'
export function getHTMLFromFragment(doc: Node, schema: Schema): string {
const document = DOMSerializer.fromSchema(schema).serializeFragment(doc.content, {
document: createHTMLDocument() as unknown as Document,
}) as unknown as VHTMLDocument
return document.render()
}
But I cannot figure out where/how I should modify it. Thanks for any efforts!