ProseMirror is a well-documented, but fairly nuanced, "toolkit for building rich-text editors." These are Bill's notes on how all the ProseMirror pieces fit together, with embedded examples where possible.
NodeView
dispatch
generally: send off a transaction to EditorView to generate a new state, which replaces view.state
. You can see this in this test, where we dispatch a transaction through view.dispatch (that happens in buildApplyDateSuggestion), and subsequently, the same view
object has apparently mutated with a new state
object
NodeView can be used to control how either a node or a plugin is rendered into the html DOM. Plugins also have views, which control how the state from the plugin is output.
update
called when PM determines that something in this node or plugins view has changed, so the view is updating itself.
"This node
passed to update
may be anything that the editor's update algorithm might try to draw here, so you must verify that this is a node that this node view can handle [before acting on it in the update
method]."
Document Position
One widespread and kind of weird thing about position in ProseMirror is that, while they refer to indexes within the ProseMirror DOM, when you're providing the index for a node, you usually give the index one before the node's actual index. This is necessary because nodeAt
and nodeAfter
will only return the node that you intend to refer to if they're given an index prior to that node.
tl; dr Often hard to get without mapping from starting document state through every transaction. Three options are using forEach()
, nodesBetween()
, or descendants()
.
Resolving the node before a given position: apparently $pos.doc.resolve($pos.before(1))
Resolving node after an index is the default doc.nodeAt(pos)
, used often
Side exercise: Examining nodeAt
Dollar sign $ it would seem always/only used to indicate a ProseMirror ResolvedPos. Examples: $start, $end, $head, $anchor, $pos. It is unclear to Bill why this convention was chosen.
Plugins
Use of a transaction meta key to indicate to a plugin when it should take action (emoji popper reading meta example as triggered here, file plugin reading meta example as set here from this originating caller that wants to store info about the current local/remote UUID of the file the file-plugin
is trying to display)
Why? Because it provides a signal that can be easily detected in the plugin's state.apply
method, and used to change the state of the plugin's state variables
Plugins can (always do, in my experience) have a view that is used to change how the document looks
Control how document looks through marks (saved changes to nodes in the doc) or Decorations (ephemeral modifications), discussed below
View and main plugin both will often use class variables to store state. In date suggester, the plugin instance uses a local variable pluginView
to keep handy the instance of its view (stored in the createDateSuggestionPlugin()
closure)
In view, these class instance vars are used:
The plugin's state.apply
function as well as its appendTransactions
function runs every time any transaction is dispatched, which more often (in general, a lot more often) than every time the editor visibly updates. The view's update()
method is, according to Jordan, called only once per render.
It can be ambiguous where to store state between the plugin, the view, and a potential React component the view mounts. In general, the less systems have to know about the variable, the better.
Legacy knowledge
State
Example: Getting state variable (setup as such in the plugin) by key
Alternate syntax, from the key codePluginKey.getState(state)
Decorations
Have a type (inline, node, widget); they control aspects of how a node or view is shown without participating in the prosemirror dom
Transactions
From the docs: "When the user types, or otherwise interacts with the view, it generates ‘state transactions’. Every state update has to go through updateState, and every normal editing update will happen by dispatching a transaction."
setMeta used to communicate over transactions between plugins/view/whatever else
Here's a piece of the method that updates local file UUIDs to remote file UUIDs, using setMeta to tell the history plugin not to mind the transaction it's going to generate
Why does doc.nodesBetween()
and doc.forEach
return pos
indexes that are one before where the corresponding node
appears to be within the ProseMirror document structure? It's tempting to think it would be so that doc.resolve
can be called on the position that's returned, and still select the desired node
? I.e., because doc.resolve
chooses the next node after the index given, perhaps these iteration functions return an index one prior to the element they're iterating to account for the behavior of doc.resolve?