Correct way to apply marks to inline nodes


I am working on a custom node that is similar to mention node. The node has to show a popup every time the cursor is inside of it, popup allows to search and select a text key to be inserted into the node, alternatively user just enters it manually.

All instances of the node are replaced with the values corresponding to the keys they contain in the post-processing. That can be called templating, I guess. The challenge lies in ensuring that the node can be marked so that the text remains formatted as the user desires after replacement.

Currently, I’m using the appendTransaction method to iterate through transactions and their steps. During this process, I look for AddMarkStep and RemoveMarkStep steps, check whether my custom node is within the step’s range, and then add or remove the mark using setNodeMarkup . While this approach somewhat works, it seems to be quite selective.

Am I doing it completely wrong, or should I just keep pushing in this direction?

Won’t the mark steps automatically add/remove marks to the node already?

Hello, Marijn! Thank you for a rapid response, I swear I didn’t want to bother you, but I ran out of options.

If you say so… I believe something is wrong with my setup or I am missing some prerequisites. A bit more details of my implementation that may or may not affect that behavior:

  1. I am using TipTap, my extension is mix of TipTap Node (node definition) and ProseMirror plugin (basically simplified TipTap’s suggestion plugin). The node schema is as follows, I experimented quite a lot with the last 3, but wasn’t able to find cause of the issue:
group: 'inline',
content: 'text*',
inline: true,
marks: '',
selectable: true,
isolating: true,
  1. The toDOM is a ["span", attrs, 0]. The node view is custom (built with TipTap’s wrapper for Vue), it has both equivalents for dom and contentDOM.

  2. Just to clarify on what I’m trying to achieve. The node must have some text in it that is name of the variable it will be replaced with. While the node should be markable, it should not allow the text inside to be directly marked, or at least the underlaying text should be treated as a whole, partial marks do not make sense for my use-case. Better if I could just wrap the whole node in marks to make replacement process as straightforward as possible. And I kinda did it with my approach, but toggleMark only appear to enable a mark and do nothing when I intent to disable it. I did some debugging and transaction I am getting when trying to disable the mark does not contain any changes at all.

Do you have any suggestions on what may be causing it? I am open to any ideas, including getting rid of TipTap wrappers if that would make any sense for you. Or is it just I am trying to do something close to impossible?

AddMarkStep will add the given mark to inline nodes in its range, and this is an inline node. Your description of the problem sounds like it’s not doing that? Or am I misunderstanding?

It appears that it doesn’t somehow. Let’s say I add some text, my node, then some text, and select everything like this:

Screenshot 2023-11-07 at 21.37.16

Then I toggle bold via command and get the following result and transaction with steps:

Screenshot 2023-11-07 at 21.41.12

If I count correctly, my node is within the first and the second steps’ ranges, but there’s no sign (visually and in the result of getHTML) of bold being applied to it compared to when I apply it as described in the original post.

Oh, I see, your node has content. Yeah, in that case it’ll add the mark to the content, not the node itself. Inline nodes with content are generally messy—they don’t fit the model of the document being a tree on the block level and flat on the inline level very well, and browsers can make it hard to control whether the cursor is inside or outside of them. If you can model this as an atomic node or a mark, that’ll probably be easier.

That is a rather obvious advice, to get rid of editable content and make the node atomic, but that is exactly what I needed to hear. I spent a day reworking everything and it just started to make sense and just work as I expect it to. THANKS A LOT.

The only thing that still confuses me is node decoration on copy-pasted nodes. I am using a decoraton to draw a popup near the node user interacts with (pretty much same as in mention, only difference is that I am using Decoration.node instead of Decoration.inline.

It works just fine with nodes created by an inputRule or via insertContent, but completely fails if I copy and paste the whole node.

Here’s how it looks normally when user clicks on the node, decorator wraps it:

  <span data-decoration-id="id_1428080181" class="expression-active">
    <span data-variable="a" data-type="expression" class="expression" contenteditable="false">{a}</span>
  <br class="ProseMirror-trailingBreak">

Then, if I copy and paste this node (regardless if it’s decorated at that moment), the second node gets added to the tree, but interaction with it has no effect, wrapper does not appear. Any interaction with the first node or any new node created with normal means works as expected.

Are there some quirks with copied nodes?

UPD: Nevermind, I suck at understanding indexing. Thank you again!