In response to several questions and discussions about embedding other editing components in a ProseMirror view in order to edit certain types of nodes, as well as the issue of it being unreasonably hard to show placeholders for empty textblock nodes, I finally tried to set up such a thing myself. My main test case was having a CodeMirror instance inside a ProseMirror instance, for editing code blocks.
Turns out it wasn’t quite as easy as I expected, so I’ve been working on a design to make this easier. There are several problems:
-
Applying changes made in the custom node representation to ProseMirror’s state.
-
Fighting between ProseMirror’s selection handling and the selection in nested controls
-
Cleverly updating the nested control instead of redrawing it every time it changes (i.e. #378)
You can kind of hack around these with the current API, but the code will be awful, inefficient, and fragile.
My proposal for a solution to this is a feature called “node views”, which are little views in the same spirit as the editor view, i.e. objects that manage a piece of the DOM, keeping as little internal state as possible, and simply displaying values that they are given.
In the node-view branch of the prosemirror-view repository, there’s a commit for which I’ve pasted the added doc comments below:
EditorProps.nodeViews
: ?Object<(node: Node, sendAction: (state: EditorState, pos: number) → ?Action) → NodeView>
Use the given node view implementation to manage the DOM representation of certain node types. When a node view is created, it is passed an action-sending function that it can use to find out its current position in the document and the current editor state, and optionally produce an action to send to the editor’s onAction
callback.
NodeView
: interface
A node view is an object that acts as an intermediary between an editor view and the DOM representation of a certain node. An instance is created when the node is drawn, and it can handle certain aspects of the DOM behavior for the node.
-
dom
: dom.Node
The DOM node that should be used as the node’s representation. -
contentDOM
: ?dom.Node
The DOM node that holds the node’s child nodes. Defaults to the maindom
property. Only relevant for non-leaf node types. -
parseRule
: ?() → ParseRule
Can be used by the view to control the parsing of the DOM. -
update
: ?(newNode: Node) → bool
An optional method that the redraw algorithm will use to update the view when the node or its content changes. When present, it will be called when a DOM update finds a changed node of the same type in the place of the view’s node. When it returns true, it should have updated the DOM structure to show the changed node. It may return false to indicate that it can’t update, and a redraw should happen. -
select
: ?()
When present, this will be called when the node is selected as a node selection. It replaces the default selected-node styling. -
deselect
: ?()
When present, andselect
is also present, this will be called when the node stops being selected. It should remove any styling thatselect
added.
EditorView.nodeViewAtPos
: (number) → ?NodeView
Find the node view for the node at the given position, if any.
I’ve put my implementation of CodeMirror-for-code-blocks in a gist. It’s not super polished yet, but before I spend more time on it, I wanted to ask for feedback. If you think this wouldn’t quite work for your use case, let me know, and we can try to improve it. If you think this is a terrible idea for some reason, speak up too, since I’m not yet committed to this.
One thing that this doesn’t really solve yet is that it can be awkward to express all the state of such a nested control with node attributes. Some really transient things you can just keep as local state in the node view, but there are situations (such as when an unhandled DOM input event occurs) where ProseMirror will redraw a node just to be sure its DOM is still coherent, and you could lose that state in such a situation. But since these views are transient things rendered for document nodes, the fact that their state is determined by the node might be unavoidable.
You also get things like the need to make something, like the code block in the demo, a leaf node that would be more ‘naturally’ modeled as a textblock node. The main reason for this is that you want to manage its content yourself, and ProseMirror assumes that it gets to draw and manage actual child nodes. We could create an exception to that, but that doesn’t fix the issue that we’d now have document positions that point at things that aren’t actually managed by ProseMirror, so it couldn’t put the cursor there or find their coordinates, even though they are valid positions in the document’s range. My conclusion was that forcing a somewhat unnatural modeling is a lesser evil than having to work around such cases.
(I’m even wondering whether all nodes should be treated like this, and the editor should keep a data structure that describes the whole rendered DOM, rather than attaching properties to the DOM itself. The upside would be that a node having a view would no longer be a special case, and it would make some DOM inspection and update mechanisms faster and easier. It does, however, involve defining and maintaining yet another data structure.)