Add CSS class to current node or selected nodes


#1

I’d like to add a visual cue to my users about the current node (where the cursor is) or the selected nodes so that they have a better notion of where the node(s) start and end.

I was hoping ProseMirror would add a css class to the selected node(s), or current node, but it only adds a ProseMirror-focused class to the container.

So what would be the best approach for adding a temporary CSS class these nodes? Of course whenever the selection changes or the cursor moves the CSS class should be removed.

I’ve read about decorators but it seems those are tied to particular node types / marks, which I’m not sure is the best approach for handling this.

Regards


#2

I’ve read about decorators but it seems those are tied to particular node types

They aren’t. Decorations are what you want here—a plugin can draw decorations for the current selection (or just a single inline decorations for the selected range, if you don’t need the class on block elements).


#3

So I ended up doing this:

new Plugin({
	props: {
		decorations(state) {
			const resolved = state.doc.resolve(state.selection.from);
			var element = document.createElement("div");
			element.className = 'selected';
			const decoration = Decoration.widget(resolved.start(), element);
			return DecorationSet.create(state.doc, [decoration]);
		}
	}
})

Which produces:

<h1>
     <div class="selected ProseMirror-widget" contenteditable="false"></div>Lorem ipsum
</h1>

Is it possible to apply a css class to the <h1> instead of adding something new to the DOM?


#4

Node decorations can add classes to nodes, but you’ll have to iterate over the selected nodes (possibly using nodesBetween) to create a decoration for each node that you want to add the class to.


#5

In my first attempt I tried using different variations of Decoration.node() but I could never apply the class to <h1>.

For example:

const decoration = Decoration.node(resolved.start(), resolved.end(), {class: 'selected'});

I also tried Decoration.inline() and as you can imagine didn’t work either.

What am I missing?

Edit: just for reference the previous code results in this:

<h1>
    <span class="selected">Lorem ipsum</span>
</h1>

#6

You want resolved..before(), resolved.after() here.


#7

Ah that was it! Thanks for your help @marijn.

For reference this is the complete plugin:

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: 'selected'});
			// equivalent to
			// const decoration = Decoration.node(resolved.start() - 1, resolved.end() + 1, {class: 'selected'});
			return DecorationSet.create(state.doc, [decoration]);
		}
	}
})

So, if I’m understanding this correctly, the problem was that my indexes didn’t include the whole heading node so PM concluded that it had to apply the decoration to the text node (hence creating a <span>) at the specified positions. Is this it?

Edit:

For reference here is the plugin that applies the decoration to all the nodes that fall into the selection, not only the cursor position:

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);
		}
	}
})