react-juce icon indicating copy to clipboard operation
react-juce copied to clipboard

Add support for Drag and Drop component/View

Open JoshMarler opened this issue 5 years ago • 5 comments

It would be great to add support for drag and drop views/components. We could have an outer View/Component type acting as a juce::DragAndDropContainer and apply a "draggable" property to the base View type which would apply to child components of the outer drag/drop container element.

See:

https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API w3schools.com/html/html5_draganddrop.asp

https://docs.juce.com/master/classDragAndDropContainer.html https://docs.juce.com/master/classDragAndDropTarget.html

JoshMarler avatar Jun 10 '20 18:06 JoshMarler

@nick-thompson this is a thing I'm going to need fairly soon so I might start to play around with a few things. If you've thought about this at all in the past I'd love to hear any ideas!

JoshMarler avatar Jun 10 '20 18:06 JoshMarler

Ahh cool, good idea. Yea React Native launched without a drag and drop option and definitely it became a much-wanted feature.

So you're thinking something like

<DropTarget onDragOver={this._handleDragOver} onDrop={this._handleDrop} />

and a property on View that marks them draggable?

<View draggable><Text>Drag me!</Text></View>

What would you envision coming in those drag event callbacks on the js side?

nick-thompson avatar Jun 10 '20 18:06 nick-thompson

@nick-thompson , another one that probably doesn't need to be beta. Will remove and correct if you think it should be.

JoshMarler avatar Nov 23 '20 12:11 JoshMarler

After recent discussions we think we have a basic approach in mind for Drag and Drop support which should leverage juce::DragAndDropComponent and aim to interop well with the HTML drag and drop API:

https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API

Rough plan for the implementation is as follows:

  • ReactApplicationRoot inherits from juce::DragAndDropContainer

  • View gains the following properties, draggable, onDragStart, onDragEnd

  • During View::onMouseDrag, if props.hasProperty("draggable") && props.hasProperty("onDragStart") we findParentDragContainer (which will be the ReactApplicationRoot instance) and call juce::DragAndDropContainer::startDragging

  • We always use an Image during drag, and letting JUCE use the default component snapshot should work well. We con potentially add a dragImage prop which allows users to provide an image file URL or similar so specify the drag image.

  • In View::onMouseDragwe call props[onDragStart] once and then set a flag to not call it again for subsequent drag events. We'll clear this flag in View::onMouseEnd or equivalent.

  • In BluepritnBackend we'll catch the call to onDragStart and wrap it in a SyntheticDragEvent, just as we do for mouse and key events. Our SyntheticDragEvent will have the dataTransfer prop described by the HTML drag event on which the drag callback can assign setData. This stuff is just regular JS.

  • We'll have a <DropTarget> component or a dropTarget prop on view along with a onDrop callback prop which receives a SyntheticDragEvent instance. The dropTarget callback can then inspect the SyntheticDragEvent's dataTransfer prop with user defined data to perform operations (i.e. drag sample file to target and load onDrop).

Below is a rough suggestion from Nick on what a SyntheticDragEvent may come to look like:

export class SyntheticDragEvent extends SyntheticEvent {
  constructor(props: any) {
    super(props);

    this.__data = null;
    this.__mimeType = null;
    this.dataTransfer = {
      setData: (mimeType, data) => {
        this.__data = data;
        this.__mimeType = mimeType;
        __BlueprintNative__.setDragData(mimeType, data);
      }
    };
  }
}

Following the workflow here: https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/setData

The assumption is that users could write data to the dataTransfer object using the setData call in onDragStarted. This data could then be read in onDrop using dataTransfer.getData(). Persisting the user defined data across event callbacks might take a bit of thought.

@nick-thompson, would you mind a quick scan of this based on our earlier discussions and let me know if anything needs correcting? I should be getting onto this one asap.

JoshMarler avatar Jan 06 '21 21:01 JoshMarler

Yep, this looks good. I want to slightly clarify a few things:

  • I generally think we should break this effort up into two passes. In the first pass, we'll implement the notion of dragging, and in the second pass, we'll implement the notion of dropping.
  • The first pass, dragging, covers most of the bullet points above. The part that I want to flesh out a bit here is this: the idea behind __BlueprintNative__.setDragData is that during the drag operation, we will want to attach any notion of the drag data (i.e. dataTransfer.setData) to ReactApplicationRoot, who is the real arbiter of the drag operation. The reason for this is that the user will potentially want to drag a component from within the ReactApplicationRoot to somewhere outside the ReactApplicationRoot (i.e. another part of the application written in regular JUCE, or even outside the window like onto the desktop). ReactApplicationRoot will need to know this data to implement shouldDropFilesWhenDraggedExternally: https://docs.juce.com/master/classDragAndDropContainer.html#a69b389d34bc9748eb5f82a8995aae28d
  • I think that's a good stopping point for a first PR: implement dragging + dataTransfer + dropping text/files externally.
  • In the second PR we'll address how to equip blueprint::View components for dropping. Initially, I thought I'd prefer to just equip any View with the ability to be a drop target, as we see in Web. But I feel we can simplify that API with the introduction of a DropTarget view. So...
  • The DropTarget will be a new native View that simply inherits blueprint::View and juce::DragAndDropTarget and implements isInterestedInDragSource to return true all the time (we can get into the specifics of ignoring drop events based on their data type later). Besides the DragAndDropTarget interface, our DropTarget should function like a totally normal <View/>
  • We can raise the onDragEnter, onDragOver, and onDrop events into js land with synthetic drag events.
  • Ideally, at the beginning of the drag operation, ReactApplicationRoot can enumerate this SourceDetails struct with the data it received from javascript via the setDragData callback in the syntheticDragEvent callback. That way, when we receive the itemDropped callback in our blueprint::DropTarget, we can push that same var into a new SyntheticDragEvent so that the receiver in javascript-land can access the drag data as expected

Let me know if I missed anything!

nick-thompson avatar Jan 07 '21 14:01 nick-thompson