Pragmatic ProseMirror guide
This repo used to be a simple cookbook but I decided to convert this repo into a more pragmatic guide that the official one. In part because I wil most likely forget all the info written here, and in part because I'd like to help you, dear reader, in your struggle to use this library which is not for the faint of heart.
This is a work in progress. If you have any suggestion, please do not hesitate to create an issue or a PR.
Also, check the ProseMirror Utils repo by Atlassian. Not only it is useful per se, but the source code offers a lot of information on how to do certain things.
Basics
Triggering changes from the keyboard
By default, a barebones editor will trigger text changes on a contentEditable
element and no more. That is, move the cursor forwards and backwards, insert text, delete, etc. It won't create new paragraphs when pressing enter since ProseMirror is totally agnostic and this behavior is something that needs to be configured.
The prosemirror-keymap
module allows us to hookup on keyboard events triggered by the contentEditable
elements. This plugin by itself will not trigger changes either.
For example, we can configure a keymap()
to trigger the undo
and redo
functions from the prosemirror-history
module which would in turn interact with the editor and trigger those changes.
Common keyboard commands (eg: creating a new paragraph when pressing enter, etc) are configured in the baseKeymap
config of the prosemirror-commands
module.
import {undo, redo, history} from "prosemirror-history";
import {keymap} from "prosemirror-keymap";
import {baseKeymap} from "prosemirror-commands";
let state = EditorState.create({
schema,
plugins: [
history(),
keymap({"Mod-z": undo, "Mod-y": redo}),
keymap(baseKeymap)
]
});
These packages only exist for our convenience and are not part of the core. We can replace them with our own later on.
There are other commands in prosemirror-commands
which can be used to trigger changes. Here are some common ones:
toggleMark
which allows you to toggle inline marks on/off on the selected text (eg: bold, italics, etc).setBlockType
which allows you to change the type of the current block (eg: paragraph, H1, image, etc).
All the available commands are documented here: https://prosemirror.net/docs/ref/#commands
Triggering changes from your code
To dispatch changes we need to get the current state and the use the dispatch
function from our editor view and call methods on state.tr
which is an instance of the Transaction class.
For example, if we wanted to delete the current selction, we could do something like this:
function deleteSelection (state, dispatch) {
if (state.selection.empty) return false;
if (dispatch) dispatch(state.tr.deleteSelection())
return true;
}
deleteSelection(state, dispatch);
It's a convention in ProseMirror that most commands can be tested without making any changes to know if it can be executed. To do that we just call the function without passing dispatch
. In this case we'd return true
without deleting anything.
Again, you could call this deleteSelection
command from a keyboard shortcut using the prosemirror-keymap
as explained earlier, or from your own logic. Commands are explained in more detail here: https://prosemirror.net/docs/guide/#commands.
Check the custom menu example for more info on triggering commands from your UI.
chainCommands
About Modularity and agnosticism are taken to the extreme in ProseMirror which is one of the reasons it's so flexible. Once you start digging into how comands work you'll see every little behavior needs its own command. In consequence, you will need the same keyboard shortcut to trigger a number of commands that can cancel each other out. This is done using the chainCommands
function.
If you take a look at the prosemirror-commands
package you will see this example for the Enter
key;
"Enter": chainCommands(newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock),
How this works is that chainCommands
will trigger the commands in order until one command returns true and stop there.
For example, the first command newlineInCode
would test if it can be triggered (eg: if it's in a code block) and return true after it has disptached a transaction. If not, it would return false
or undefined
and chainCommands()
would go on to createParagraphNear
and so on.
If no command returns true
then chainCommands
will return false and ProseMirror will continue looking for matches for the triggered key in other keymap
plugin instances.
Finally, if no configured shortcut returns true
, ProseMirror will allow the keyboard event to execute in the contentEditable
element.
I found chainCommands
to be very confusing at first, so I created my own version to show the name of the function that was actually executed:
export function chainCommands(...commands) {
return function(state, dispatch, view) {
console.log('chainCommands');
for (let i = 0; i < commands.length; i++) {
const result = commands[i](state, dispatch, view);
// If the command has executed `result` will be true
if (result) {
// On creation, some commands create a closure and return
// an anonymous function so we can't really know their name...
console.log(commands[i].name || 'anonymous function');
return true;
}
}
console.log('no command matched!');
return false
}
}
How to move the cursor
At some point you will need to start making your own commands. A very common need is to move the cursor somewhere.
This is how you would do it from inside a command (with access to a dispatch
function and state
object) once you have figured out the position where you want your cursor to go:
dispatch(state.tr.setSelection(TextSelection.create(state.doc, 345)))
The idea here is that we're defining an empty TextSelection
with a single absolute position in the document. It's called an empty selection because both the start and end cursor of the selection would be the same. In the ProseMirror docs these two cursors are referred to as anchor
and head
.
Get an HTML string from an editor state
import {DOMSerializer} from 'prosemirror-model';
function getHTMLStringFromState (state) {
const fragment = DOMSerializer.fromSchema(state.schema).serializeFragment(state.doc.content);
const div = document.createElement("div");
div.appendChild(fragment);
return div.innerHTML;
}
Editor
Get notified of updates and changes
Here's a simple plugin to know when something has changed in the editor (cursor position, selection, etc) and hook up on the updates:
import {EditorState, Plugin} from "prosemirror-state";
const onUpdatePlugin = new Plugin({
view () {
return {
update (updatedEditorView) {
// For example, let's print the cursor position:
const $cursor = updatedEditorView.state.selection.$cursor;
console.log('cursor position:', $cursor.pos);
}
}
}
});
const editorState = EditorState.create({
schema,
plugins: [onUpdatePlugin]
});
You can also hookup on the transactions:
const editorView = new EditorView(editorElement, {
state: editorState,
dispatchTransaction (transaction) {
const newState = editorView.state.apply(transaction);
editorView.updateState(newState);
// do something here such as eventBus.dispatch('NEW-TRANSACTIION', transaction)
}
});
Utils
Get the current active marks
This is a helper function that returns an array with the names of the active marks in the current selection or cursor position. This is helpful when we need to highlight the buttons of the marks that are applied (bold, italic, etc):
export function getActiveMarkCodes (view) {
const isEmpty = view.state.selection.empty;
const state = view.state;
if (isEmpty) {
const $from = view.state.selection.$from;
const storedMarks = state.storedMarks;
// Return either the stored marks, or the marks at the cursor position.
// Stored marks are the marks that are going to be applied to the next input
// if you dispatched a mark toggle with an empty cursor.
if (storedMarks) {
return storedMarks.map((mark) => mark.type.name);
} else {
return $from.marks().map((mark) => mark.type.name);
}
} else {
const $head = view.state.selection.$head;
const $anchor = view.state.selection.$anchor;
// We're using a Set to not get duplicate values
const activeMarks = new Set();
// Here we're getting the marks at the head and anchor of the selection
$head.marks().forEach((mark) => activeMarks.add(mark.type.name));
$anchor.marks().forEach((mark) => activeMarks.add(mark.type.name));
return Array.from(activeMarks);
}
}
Get the available marks
Here we're iterating all the marks in the schema and test whether they can be applied with toggleMark
.
export function getAvailableMarkCodes (editorView) {
const markTypes = editorView.state.schema.marks;
return Object.keys(markTypes).filter((key) => {
const mark = markTypes[key];
return toggleMark(mark)(editorView.state, null, editorView);
});
}
Check the current available node types
This is a helper function that returns an array with the node types that can be applied in the current selection or cursor position. This is helpful, for example, if we want to enable/disable/highlight buttons to switch from heading to paragraph.
import {setBlockType} from 'prosemirror-commands';
function getAvailableBlockTypes (editorView, schema) {
// get all the available nodeTypes in the schema
const nodeTypes = schema.nodes;
// iterate all the nodeTypes and check which ones can be applied
return Object.keys(nodeTypes).filter((key) => {
const nodeType = nodeTypes[key];
// setBlockType() returns a function which returns false when a node can't be applied
return setBlockType(nodeType)(this.editorView.state, null, this.editorView);
});
}
Working with nodes
Creating a simple node
Internally, ProseMirror uses the Node class to represent its state, but the best way of creating nodes is to use the create()
method from the NodeType
class:
const node = state.schema.nodes.paragraph.create(null, null, null);
How to create text nodes
// create a text node
const textNode = state.schema.text("hello");
// create a new node with the text node as a child
const paragraphNode = state.schema.nodes.paragraph.create(null, textNode, null);
Decorations
Apply a decoration where the cursor is
Here's a plugin that automatically applies a decoration to the node where the cursor is by adding current-element
CSS class to the element:
new Plugin({
props: {
decorations(state) {
const selection = state.selection;
const resolved = state.doc.resolve(selection.from);
const decoration = Decoration.node(resolved.before(), resolved.after(), {class: 'current-element'});
// This is equivalent to:
// const decoration = Decoration.node(resolved.start() - 1, resolved.end() + 1, {class: 'current-element'});
return DecorationSet.create(state.doc, [decoration]);
}
}
})
Apply a decoration to the selected node(s)
Here's a plugin that automatically applies a decoration to all the nodes that are touched by a selection. In this case we're adding the selected
CSS class:
new Plugin({
props: {
decorations(state) {
const selection = state.selection;
const decorations = [];
state.doc.nodesBetween(selection.from, selection.to, (node, position) => {
if (node.isBlock) {
decorations.push(Decoration.node(position, position + node.nodeSize, {class: 'selected'}));
}
});
return DecorationSet.create(state.doc, decorations);
}
}
})
Miscellaneous
How to keep ProseMirror focused when clicking in your menu
By default, the browser will remove the current selection and blur focused elements when a click happens. To prevent this, you need to use event.preventDefault()
on the mousedown
event of your clickable elements.