It took me a few hours of digging to figure out how to register a new node type that would simply be a paragraph that adds a css class name. I found the dino example to be a bit confusing, especially as it only handles an inline type without children rather than a block type. I thought I’d put my example code here just to document how to do this, and would welcome suggestions for improving/simplifying. Might be useful to add something like this to the docs.
import {ProseMirror} from "prosemirror";
import {Paragraph, Schema, defaultSchema} from "prosemirror/dist/model";
/**
* Create a new node type that is a paragraph with a class name.
* @param {String} name - The human-readable name of the paragraph type
* @param {String} classname - The CSS class name to apply to the paragraph
* @param {Number} menuRank - Rank order for menu placement.
* @return {Object} the new node type
*/
function classyParagraphType(name, classname, menuRank) {
// The base type just extends from Paragraph.
class ClassyParagraph extends Paragraph {};
// parser: identify an element for this type if it is a "p" with our class name.
ClassyParagraph.register("parseDOM", "p", {
// DOMParseSpec implementation: http://prosemirror.net/ref.html#DOMParseSpec
rank: 10, // Make sure this is lower than the paragraph type default of 50.
parse: function(dom, state) {
let classnames = dom.getAttribute("class");
if (!classnames || !classnames.split(" ").includes(classname)) {
return false;
}
// wrap our block type around child nodes.
state.wrapIn(dom, this);
}
});
// serializer: render this node with the class.
ClassyParagraph.prototype.serializeDOM = function(node, s) {
return s.renderAs(node, "p", {"class": classname});
};
// menu: add this type to the menu.
ClassyParagraph.register("command", "make", {
derive: true,
label: `Change to ${name} paragraph`,
menu: {
group: "textblock",
rank: menuRank,
display: {type: "label", label: `${name} paragraph`},
activeDisplay: `${name} paragraph`,
"class": `menu-${classname}`
}
})
return ClassyParagraph
}
// Create a new editor schema that adds a couple of paragraph-with-classname types.
const editorSchema = new Schema(defaultSchema.spec.update({
// Register a "drop cap" paragraph type
dropcap: classyParagraphType("Drop Cap", "dropcap", 7),
// Register a "pull quote" paragraph type
pullquote: classyParagraphType("Pull Quote", "pullquote", 8),
}));
// Instantiate an editor with our new schema.
let editor = new ProseMirror({
place: document.getElementById("editor"),
menuBar: true,
schema: editorSchema
});
The key parts that were confusing/absent from the dino example which took me a bunch of time to figure out:
- The attrs definition in the class is confusing. I’m still not sure whether this could be more elegantly accomplished using attribute definitions rather than overriding the serializer/parser; but it didn’t seem that specifying an attribute definition (e.g.
get attrs() { return {"class": new Attribute({default: classname})} }
) worked for rendering or parsing the class attribute without also overriding"parseDOM"
andserializeDOM
. - The parser for a block type needs to use
state.wrapIn
rather thanstate.insert
to render its child components. - Dino’s serializeDOM implementation makes use of the not very documented
elt
, which was giving me trouble. It took me a while to figure out thatserializeDOM
offers the second “serializer” argument withrenderAs
, which I needed to get children to render properly. - The
derive
key in registering a command isn’t super clear. Dino adds all kinds of stuff pertaining to choosing the “type” of dino to show. It took a bunch of spelunking in the source and confusing error messages to figure out I just neededderive: true
.
Hope this example helps someone else out.