react-juce
react-juce copied to clipboard
Add support for Drag and Drop component/View
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
@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!
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 , another one that probably doesn't need to be beta. Will remove and correct if you think it should be.
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 -
Viewgains the following properties,draggable,onDragStart,onDragEnd -
During View::onMouseDrag, if props.hasProperty("draggable") && props.hasProperty("onDragStart") we findParentDragContainer (which will be the
ReactApplicationRootinstance) and calljuce::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
dragImageprop which allows users to provide an image file URL or similar so specify the drag image. -
In
View::onMouseDragwe callprops[onDragStart]once and then set a flag to not call it again for subsequent drag events. We'll clear this flag inView::onMouseEndor equivalent. -
In
BluepritnBackendwe'll catch the call toonDragStartand wrap it in aSyntheticDragEvent, just as we do for mouse and key events. OurSyntheticDragEventwill have thedataTransferprop described by the HTML drag event on which the drag callback can assignsetData. This stuff is just regular JS. -
We'll have a
<DropTarget>component or adropTargetprop on view along with aonDropcallback prop which receives aSyntheticDragEventinstance. ThedropTargetcallback can then inspect theSyntheticDragEvent'sdataTransferprop 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.
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__.setDragDatais that during the drag operation, we will want to attach any notion of the drag data (i.e.dataTransfer.setData) toReactApplicationRoot, 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).ReactApplicationRootwill need to know this data to implementshouldDropFilesWhenDraggedExternally: 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::Viewcomponents for dropping. Initially, I thought I'd prefer to just equip anyViewwith the ability to be a drop target, as we see in Web. But I feel we can simplify that API with the introduction of aDropTargetview. So... - The
DropTargetwill be a new native View that simply inheritsblueprint::Viewandjuce::DragAndDropTargetand implementsisInterestedInDragSourceto return true all the time (we can get into the specifics of ignoring drop events based on their data type later). Besides theDragAndDropTargetinterface, ourDropTargetshould function like a totally normal<View/> - We can raise the
onDragEnter,onDragOver, andonDropevents into js land with synthetic drag events. - Ideally, at the beginning of the drag operation, ReactApplicationRoot can enumerate this
SourceDetailsstruct with the data it received from javascript via thesetDragDatacallback in the syntheticDragEvent callback. That way, when we receive theitemDroppedcallback in ourblueprint::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!