Draggable and NodeViews

Is it possible to make draggable work with nodes that are rendered with a NodeView ?

What goes wrong? In principle, it should work already, and it does seem to work in the footnote example.

I have a paragraph with text, a task and an empty paragraph:

I try to drag the task and drop it in the paragrah below it and this is what happens:

This is my nodeView and schema

    buildNodeView(){
        return (node, view, getPos) => {
            const componentRef = this.buildComponent(node.attrs); // builds Angular component.
            return {
                dom: componentRef.location.nativeElement,
                setSelection: (anchor, head) => {},
                stopEvent: () => true,
                update: newNode => false,
                ignoreMutation: mutation => true,
                destroy: () => componentRef.destroy()
            }
        }
    }
    task:  {
        group: "component",
        selectable: true,
        isolating: true,
        draggable: true,
        attrs: { 
            ...
        }
    },

You’ll have to define a toDOM and parseDOM on your task node if you want it to be able to go through the clipboard or be dragged – clipboard content is represented as HTML.

hmm, I see…
So, toDOM gets the node and returns the DOM and parseDOM gets the DOM and returns the node, correct ?
I think the toDOM should be easy as this is pretty much what the nodeView is doing using the this.buildComponent(node.attrs); but the parseDOM will be much more complicated as some of my nodeViews render complex Angular components and parsing their DOM to get the node attributes will be tricky. Or am I missing something ?

You’ll want your toDOM and parseDOM to describe a more or less semantic representation of the node, not some big Angular component – nobody wants angular components on their clipboard. So unless your node has lots of attributes or content, it shouldn’t be too hard to come up with a simple HTML representation.

Ok, I’ll try making my Angular component render a simple and invisible html element with all necessary information which will be used by the schema node’s parseDOM to create the node with the needed attributes. Thanks!

That sounds wrong. You’re rendering the Angular component in a node view, right? So in your editor, your toDOM method is overridden by a node view, and not used for the editable display. So toDOM doesn’t have to involve itself with the Angular stuff at all, it can just output a single recognizeable node (say <div data-type="mynode" data-someattribute="myvalue">), and doesn’t need to hide anything.

Ok, thanks for the help Marijn, I think I am almost there.

This is the html my noveView generates:

This is my schema task node:

    task:  {
        group: "component",
        selectable: true,
        isolating: true,
        draggable: true,
        attrs: { 
            id: {},
            description: {default: null},
           ...
        },
        toDOM: function(node){
            return ["ruum-task", { 
               /** style for testing without nodeView. */
                style: 'width: 200px; height: 20px; background: red; display: block',
                'data-id': node.attrs.id, 
                'data-description': node.attrs.description 
            }];
        },
        parseDOM: [{
            tag: "ruum-task", 
            getAttrs: function getAttrs(dom) {
                return {
                    id: dom.getAttribute("data-id"), 
                    description: dom.getAttribute("data-description")
                }
            }
        }]
    },

With this, when I am not using a nodeView for task node it works perfectly. When I use the nodeView I am able to drag and drop a task but instead of moving it the task is duplicated.

This is the transaction that is dispatched when I drop the task

JSON.stringify(transaction.steps[0].toJSON())
{"stepType":"replace","from":8,"to":8,"slice":{"content":[{"type":"task","attrs":{"id":"task_i18t6y9v","assigneeId":null,"assignees":[],"description":"gggg","isDone":false,"createdBy":null,"createdAt":null,"dueDate":null,"startDate":null,"completedBy":null,"completedAt":null,"status":null,"beingCreatedFromCanvas":false}},{"type":"paragraph","content":[{"type":"hard_break"}]}]}}

Is your node selectable? Drag-and-drop within the editor relies on the selection to remember which part it should delete when the dragged content is dropped. I guess something is going wrong there. Can you inspect state.selection right before the drop happens? What kind of selection is it?

It is selectable and the selection is on the task node.

stateBefore.selection.toJSON()
{type: "node", anchor: 9}

stateBefore.doc.nodeAt(9).type
NodeType {name: "task", schema: Schema, spec: {…}, groups: Array(1), attrs: {…}, …}

It is weird, if I remove the nodeView it works.

Interesting. You can try to log what tr.selection and dragging.move are when inputHandlers.drop calls deleteSelection. Or you can create a minimal example script that sets up the situation in which this fails, and I’ll take a look.

I’ve pushed a few patches that should help with this. But you’ll still have to make sure you let drag-related events through in stopEvent, or the editor won’t get a chance to handle dragging correctly (something like return !/drag/.test(e.type), or explicitly list the events you want to handle yourself)

I setup the stopEvent method to work as you suggested. The NodeView is now being moved properly, however the original place still exists.

I will continue to try and figure out why this is the case.

Is the node selectable? (I.e. did you explicitly set selectable: false on it?)

Edit: err, I think your video shows that it is. Then I’m out of ideas.

Okay, it seems that the problem is that in order for drag and drop to work correctly it needs to let mousedown events pass.

I need to block mousedown so I can click on the form when editing an image without the selection being set to the entire image node.

The solution then is to make stopEvent let through mousedown events that occur on the image, but not on the form.

For example:

stopEvent(e: Event) {
  return (
    (e.type === "mousedown" && e.target.tagName !== "IMG") || 
    !/drag/.test(e.type)
  )
}

That’s odd – it worked for me when blocking mousedown. But that was with a simple node view, maybe there’s something different about yours.

Not blocking mousedown and dragging events also worked for me, here is a minimal example(well, not so minimal as it uses angular but it should be easy to make it run): https://github.com/FelipeTaiarol/ProseMirror-DragNDrop-Problem

Another interesting thing is that if I am using a nodeView I can have a schema node that does not implements parseDOM and with a toDOM that returns an empty array and it works normally.

Related to this, I have an inline node with atom: true and draggable: true that should a) change position when dragged and b) open a form for editing when clicked.

I can get both of these to happen by returning event.type !== 'mousedown' && !event.type.startsWith('drag') from the node view’s stopEvent, but can’t find a way to stop the selectNode handler firing when dragging the node around. Might it be possible to pass the event into selectNode so the handler can decide whether to handle it?