Creating a wrapper for all blocks

Hi! I’m trying to create a draggable handle next to all blocks in my document. To do this, I want to wrap all blocks in a parent div laid out like this:

<div class="wrapper">
  <div class="handle>...</div>
  <div class="content">Block content goes here</div>
</div>

There are three ways I can think of handling this, but I’ve run into problems with each of them.

The first thing I tried was using decorations. If I apply a Decoration.node to all blocks and pass a nodeName in its spec, then I can create a wrapping node. However, this appears to only let me create a single wrapping node, not the hierarchy described above.

The second option I tried was to create an outerBlock type which renders the hierarchy shown above and whose schema contains exactly one block, then change my document schema to contain one or more outerBlocks. This mostly works, but breaks things like Enter inserting a line break, since it tries to break the parent node (a block) when it now needs to break the grandparent node (outerBlock).

The third option I considered was to change all my block node types to render the hierarchy shown above. This feels inelegant since I would need to manually add this to every block node (which in my case is quite a few — I’m using TipTap, so ideally I’d like to do this in a way that won’t require adding code to every extension I use).

Is there a more elegant solution to this problem? If not, which of these options do you think would be the best direction to go?

Thanks for your time!

1 Like

@TheLastBanana You should check https://prosemirror.net/docs/ref/#view.NodeView

I think widget decorations might be more suited for your use case—you could insert a dragging handle widget at the start of each block.

@Priestch That’s what I used for the second and third options I suggested, yeah. Useful, but the problem is ultimately that in order to draw the handle, I either need to add NodeView settings to every possible block node or wrap all block nodes in a wrapper node with a NodeView, both of which come with drawbacks.

@marijn Can widgets contain document nodes? The draggable handle acts more like a border around the entire node rather than just an element next to the node. I suppose I could use JS to position the widget such that it looks like a border despite being a sibling element, though.

No, they can’t—I was imagining some kind of handles to the side. But indeed, making them look like wrappers would require some CSS hacks.

I’ve run into another problem with the widget approach, which is that the paragraph node isn’t marked as draggable in its spec. As a result, the widget can’t actually be used to drag the paragraph. Is there any way to change this other than to make my own custom paragraph type with draggable set to true?

Nodes containing text should not, as a rule, be marked draggable, since that’ll interfere with text selection. Is it workable to register your own dragstart handler for the handles, which puts the node’s content onto the clipboard?

Did anyone get anywhere with this? I need a wrapper node view for all top level blocks (i.e, direct descendents of the doc), in order to insert drag handles and also a button that changes the type of the block, but in a way that doesn’t break enter behavior. I’d gotten some of the way there with a widget decoration for the button, using display: flex on prosemirror to get everything inline, and then using another widget decoration at the end of each node to force a wrap. But I’m not sure how I could make drag handles work, when they’re not actually inside the node that I want them to drag for. It would be easier if I could actually wrap every block with a node view.

@marijn The current idea is to use the plug-in to traverse the child nodes, and then wrap the traversed Node nodes in the child node traversal. but I have created a DOM div, but I don’t know how to insert decoration.widget.could you write a demo please.

You should do something like that

view.dom.addEveventListener("mouseover",function cc(evt){
  let ob = view.postAtCoords({"left":evt.clientX,"top":evt.clientY})
  if (ob){
  let pos = ob.pos
  let dom  = view.domAtPos(pos)
  let node = view.state.resolve(pos)
  if (something about the node and the dom){
    let handle = crel("div")
    let box = dom.getBoundClientRect()
    handle.style["top"] = box["top"]+"px"
    handles_wrapper.append(handle)
    dom.addEventListener("mouseleave",function(){
      handle.remove()
    })
  }
})

with handles_wrapper being outside of the editor

and if you want to have all nodes having handles shown at all time, you can just subscribe to prosemirror new transactions, and redraw all the handles for the nodes at each transaction

1 Like