Hi @Derrick56007 thanks for putting this library together. I tried a few for Flutter web and this one seems to work the best for us on various browsers. We ran into a few issues with bugs around multiple drop targets (need a single instance of the drag target), the mouse leaving the window, etc. So I copied your code and refactored it some.
Happy to issue a PR at some point but a bit swamped at the moment.
There are some pieces that are coupled to our codebase in here but thought it'd be better to share the code here to start a discussion.
A few high level points
- We have a single instance that handles the drag/drop events, stops the propagation, and relays them to widgets that need it. This is because if you do run
e.stopPropagation()
for every widget it won't work for multiple widgets (event gets swallowed).
- We handle dragEnter/dragLeave because dragOver can't detect if your mouse leaves the window
- Centralized the logic to handle
_dragInBounds
updates to better deal with edge cases
- Reduced the number of null operators necessary with early returns
Let me know what you think
//@dart=2.12
// adapted from https://github.com/Derrick56007/dropzone
import 'dart:html';
import 'package:flutter/widgets.dart';
import 'package:turtle_app/file_manager/file_picker/html.dart';
import '../file_drop_target.dart';
import '../local_file.dart';
import 'package:turtle_app/util/event_handler.dart';
FileDropTarget createFileDropTarget({
Key? key,
required Widget child,
Function()? onEnter,
Function()? onExit,
Function(Iterable<LocalFile>)? onDrop,
}) =>
HtmlFileDropTarget(
key: key,
child: child,
onEnter: onEnter,
onExit: onExit,
onDrop: onDrop,
);
class HtmlFileDropTarget extends StatefulWidget implements FileDropTarget {
const HtmlFileDropTarget({
Key? key,
required this.child,
this.onEnter,
this.onExit,
this.onDrop,
}) : super(key: key);
final Widget child;
final Function()? onEnter;
final Function()? onExit;
final Function(Iterable<LocalFile>)? onDrop;
@override
State<StatefulWidget> createState() => _HtmlFileDropTargetState();
}
class _HtmlFileDropTargetState extends State<HtmlFileDropTarget> {
bool _dragInBounds = false;
final _dispose = <void Function()>[];
@override
void dispose() {
super.dispose();
for (final cb in _dispose) cb();
}
@override
void initState() {
super.initState();
_dispose.add(_controller.onDragEnter.addListener(_onDragEnter));
_dispose.add(_controller.onDragOver.addListener(_onDragOver));
_dispose.add(_controller.onDragLeave.addListener(_onDragLeave));
_dispose.add(_controller.onDragDrop.addListener(_onDragDrop));
}
@override
Widget build(BuildContext c) => widget.child;
void _onDragEnter(MouseEvent e) {
// can treat this like drag-over
_onDragOver(e);
}
void _onDragLeave(MouseEvent e) {
// mouse has left the window or drag region so consider this as onExit
_setDragInBounds(false);
}
void _onDragOver(MouseEvent e) {
final renderObject = context.findRenderObject();
if (renderObject == null) {
_setDragInBounds(false);
return;
}
final bounds = _getGlobalPaintBounds(renderObject);
_setDragInBounds(_isCursorWithinBounds(e, bounds));
}
void _onDragDrop(MouseEvent e) {
_setDragInBounds(false);
final htmlFiles = e.dataTransfer.files ?? [];
final localFiles = htmlFiles.map(castToFile);
widget.onDrop?.call(localFiles);
}
void _setDragInBounds(bool value) {
if (_dragInBounds == value) return;
_dragInBounds = value;
if (_dragInBounds) {
widget.onEnter?.call();
} else {
widget.onExit?.call();
}
}
}
Rect _getGlobalPaintBounds(RenderObject renderObject) {
final translation = renderObject.getTransformTo(null).getTranslation();
return renderObject.paintBounds.shift(Offset(translation.x, translation.y));
}
bool _isCursorWithinBounds(MouseEvent e, Rect bounds) {
return e.layer.x >= bounds.left &&
e.layer.x <= bounds.right &&
e.layer.y >= bounds.top &&
e.layer.y <= bounds.bottom;
}
class _HtmlFileDropTargetController {
_HtmlFileDropTargetController() {
final body = document.body!;
body.onDragEnter.listen(_onDragEnter);
body.onDragOver.listen(_onDragOver);
body.onDragLeave.listen(_onDragLeave);
body.onDrop.listen(_onDrop);
}
final onDragEnter = Event<MouseEvent>();
final onDragOver = Event<MouseEvent>();
final onDragLeave = Event<MouseEvent>();
final onDragDrop = Event<MouseEvent>();
void _onDragEnter(MouseEvent e) {
_stopEvent(e);
onDragEnter.notifyListeners(e);
}
void _onDragOver(MouseEvent e) {
_stopEvent(e);
onDragOver.notifyListeners(e);
}
void _onDragLeave(MouseEvent e) {
_stopEvent(e);
onDragLeave.notifyListeners(e);
}
void _onDrop(MouseEvent e) {
_stopEvent(e);
onDragDrop.notifyListeners(e);
}
}
final _controller = _HtmlFileDropTargetController();
void _stopEvent(MouseEvent e) {
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
}
typedef EventHandler<T> = void Function(T event);
class Event<T> {
final _listeners = <EventHandler<T>>{};
void Function() addListener(EventHandler<T> listener) {
_listeners.add(listener);
return () => removeListener(listener);
}
void removeListener(EventHandler<T> listener) => _listeners.remove(listener);
void notifyListeners(T event) {
for (final listener in _listeners) listener(event);
}
void dispose() {
_listeners.clear();
}
}
Let me know if you have any questions on the changes in the code. Cheers!
enhancement