I have created a drag and drop interface that will eventually be turned into a form builder. I've created a fiddle of the form builder http://jsfiddle.net/szASZ/1/. Everything seems to be working properly when you drag one of the top items into the gray drop zones beneath it.
However, where I am getting the issue is when you refresh the page/fiddle and try to sort the items within the drop zone. If you grab the "Radio Input" at the top of the first drop zone and move it within or outside of that drop zone, everything works properly.
Now, try dragging the last item "Checkbox Input" within its drop zone and everything should work correctly as well. Finally, refresh the page/fiddle and move that same, last "Checkbox Input" to another drop zone. The placeholder will show and then never go away. What I am finding is that "onDragEnd" event is never called during this instance of the drag and drop cycle, but it is called every other time. This event is normally relayed to the top level of the form builder where it takes the item that is being dragged and sets it in the new array of data.
To make things all the more fun, if I add an empty object onto the arrays that contain the dropped items (http://jsfiddle.net/szASZ/ - Line 30, 34), everything works as expected so it would seem that the last item in the array of "drop zone" items does not properly fire the "onDragEnd" event.
componentWillMount: function () {
var newInput = [
{ "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Text Input" },
{ "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Checkbox Input" },
{ "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Radio Input" }
];
var newZones = [
[
{ "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Radio Input" },
{ "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Text Input" },
{ "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Checkbox Input" }
,{}
],
[
{ "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Text Input" },
{ "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Checkbox Input" }
,{}
]
];
this.setState({ inputs: newInput });
this.setState({ zones: newZones });
}
Basically, this is the main problem that I am having and I have no idea why every other item gets sorted, dropped and the events trigger properly but the last one? Thanks!
This is directly related to/caused by a React bug I filed a couple months ago:
#1355: touchmove doesn't fire on removed element
React takes advantage of event bubbling and binds all events at the document root, which works in most situations but falls down in this particular case. Unlike mouse events, touch and drag events are always sent to the event which received the touchstart
or dragstart
event (rather than the element that the pointer is currently over). If the element that received the dragstart
event is removed from the DOM (as it is in this particular case since it disappears from the list it started in) then that (detached) element will still receive the dragend
event but it won't bubble up to the document root, so React never sees the event.
You only see this when dragging the last element because you're using the key
prop improperly. Because you're using the index as the key currently, when you have a list A B C D and start dragging B (causing the list to become A C D), React mutates the second element (previously B) to have text "C", mutates the third element (previously C) to have text "D", and deletes the fourth element (previously D). If instead of the index you use a unique per-item ID as the key, React will always remove the element that you're dragging, which will be easier to reason about. (Because of the bug, you'd then see that this behavior happens always instead of only when dragging the last element.)
As a workaround, you can bind the dragend
/dragenter
/dragleave
handlers manually in the dragstart handler and clean them up on dragend
. This will sidestep React's event system completely for those events:
// DRAGGING
dragStart: function (e) {
this.getDOMNode().addEventListener('dragend', this.dragEnd, false);
this.getDOMNode().addEventListener('dragenter', this.dragEnter, false);
this.getDOMNode().addEventListener('dragleave', this.dragLeave, false);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', e.target.innerHTML);
this.props.dragStart(this.props.item, { sorting: true });
},
dragEnd: function (e) {
this.getDOMNode().removeEventListener('dragend', this.dragEnd, false);
this.getDOMNode().removeEventListener('dragenter', this.dragEnter, false);
this.getDOMNode().removeEventListener('dragleave', this.dragLeave, false);
var opts = e.dataTransfer.dropEffect === 'none' ? { success: false } : { success: true };
this.props.dragEnd(opts);
},
With that setup, you'd only specify onDragStart
in render
.
(I'll see if I can get this issue fixed – as far as I know, you're the first person to hit it besides me so it hasn't been a priority for us.)
I had a similar bug with onDragEnd
not firing for a node which was re-rendered.
@Ben Alpert, thanks for your workaround, it's great! I would suggest one little modification:
dragEnd: function(e) {
// component can be umounted and getDOMNode will throw in that case
if (this.isMounted()) {
this.getDOMNode().removeEventListener('dragend', this.dragEnd, false);
// ...
}
// ... invoke the callback
},
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With