Skip to content

Commit e7ba699

Browse files
committed
* More stable handling of nested elements. Instead of keeping track of
dragOverElements in a list, the related target of the event is used to determine if it is an event that happened on the main element or bubbled up from child elements. * Fix a bug in drag emulation that occurred when a nested HTML element was dragged. Getting the element under the drag image was wrong.
1 parent 208eede commit e7ba699

File tree

7 files changed

+139
-99
lines changed

7 files changed

+139
-99
lines changed

lib/html5_dnd.dart

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,6 @@ Element currentDraggable;
2828
/// The [DraggableGroup] the [currentDraggable] belongs to.
2929
DraggableGroup currentDraggableGroup;
3030

31-
/// Keep track of [EventTarget]s where dragEnter or dragLeave has been fired on.
32-
/// This is necessary as a dragEnter or dragLeave event is not only fired
33-
/// on the [dropzoneElement] but also on its children. Now, whenever the
34-
/// [dragOverElements] is empty we know the dragEnter or dragLeave event
35-
/// was fired on the real [dropzoneElement] and not on its children.
36-
Set<EventTarget> currentDragOverElements = new Set<EventTarget>();
37-
3831
/**
3932
* Abstract superclass for all groups containing drag and drop elements.
4033
*/

lib/html5_sortable.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'dart:html';
88
import 'dart:async';
99
import 'package:meta/meta.dart';
1010
import 'package:logging/logging.dart';
11+
1112
import 'package:html5_dnd/html5_dnd.dart';
1213

1314
import 'package:html5_dnd/src/css_utils.dart' as css;

lib/src/dnd/draggable.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ class DraggableGroup extends Group {
146146
* Adds the CSS classes and fires dragStart event.
147147
*/
148148
void _handleDragStart(Element element, Point mousePagePosition, Point mouseClientPosition) {
149+
_logger.finest('handleDragStart');
150+
149151
currentDraggable = element;
150152
currentDraggableGroup = this;
151153

@@ -180,6 +182,8 @@ class DraggableGroup extends Group {
180182
*/
181183
void _handleDragEnd(Element element, Point mousePagePosition,
182184
Point mouseClientPosition) {
185+
_logger.finest('handleDragEnd');
186+
183187
// Remove CSS classes.
184188
if (draggingClass != null) {
185189
element.classes.remove(draggingClass);
@@ -196,7 +200,6 @@ class DraggableGroup extends Group {
196200
// Reset variables.
197201
currentDraggable = null;
198202
currentDraggableGroup = null;
199-
currentDragOverElements.clear();
200203
}
201204
}
202205

@@ -233,6 +236,7 @@ List<StreamSubscription> _installDraggable(Element element, DraggableGroup group
233236
return;
234237
}
235238
_logger.finest('dragStart');
239+
236240
// In Firefox it is possible to start selection outside of a draggable,
237241
// then drag entire selection. This leads to strange behavior, so we
238242
// deactivate selection here.
@@ -275,12 +279,14 @@ List<StreamSubscription> _installDraggable(Element element, DraggableGroup group
275279
subs.add(element.onDragEnd.listen((MouseEvent mouseEvent) {
276280
// Do nothing if no element of this dnd is dragged.
277281
if (currentDraggable == null) return;
282+
278283
_logger.finest('dragEnd');
279284

280285
group._handleDragEnd(element, mouseEvent.page, mouseEvent.client);
281286

282287
// Reset variables.
283288
isHandle = false;
289+
_lastDragEnterTarget = null;
284290
}));
285291

286292
return subs;

lib/src/dnd/draggable_emulated.dart

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ List<StreamSubscription> _installEmulatedDraggable(Element element, DraggableGro
7777
// -------------------
7878
// Fire Dropzone events (DragEnter, DragOver, DragLeave)
7979
// -------------------
80-
_fireEventsForDropzone(moveEvent);
80+
_fireEventsForDropzone(element, moveEvent);
8181
}
8282
});
8383

@@ -144,20 +144,22 @@ void _emulateDragEnd(Element element, DraggableGroup group) {
144144
}
145145
}
146146

147+
// Restore cursor.
148+
_restoreCursor();
149+
147150
// Reset variables.
148-
_emulDragImage = null;
149151
_emulDragStarted = false;
150-
151-
// Reset cursor.
152-
_restoreCursor();
152+
_emulDragImage = null;
153+
_emulPrevMouseTarget = null;
153154
};
154155

155156
subMouseUp = document.onMouseUp.listen((MouseEvent upEvent) {
156157
// Fire the drop event.
157158
EventTarget target = upEvent.target;
158-
if (_emulDragImage != null && target == _emulDragImage.element) {
159+
if (_emulDragImage != null &&
160+
(_emulDragImage.element == target || _emulDragImage.element.contains(target))) {
159161
// Forward event on the drag image to element underneath.
160-
target = _getElementUnder(upEvent);
162+
target = _getElementUnder(_emulDragImage.element, upEvent);
161163
}
162164
target.dispatchEvent(_createEmulatedMouseEvent(upEvent, EMULATED_DROP));
163165

@@ -178,11 +180,11 @@ void _emulateDragEnd(Element element, DraggableGroup group) {
178180
* If an event occurs on the [dragImageElement] it is forwarded to the element
179181
* underneath.
180182
*/
181-
void _fireEventsForDropzone(MouseEvent mouseEvent) {
183+
void _fireEventsForDropzone(Element element, MouseEvent mouseEvent) {
182184
EventTarget target = mouseEvent.target;
183-
if (target == _emulDragImage.element) {
185+
if (_emulDragImage.element == target || _emulDragImage.element.contains(target)) {
184186
// Forward events on the drag image to element underneath.
185-
target = _getElementUnder(mouseEvent);
187+
target = _getElementUnder(_emulDragImage.element, mouseEvent);
186188
}
187189

188190
if (_emulPrevMouseTarget == target) {
@@ -257,21 +259,14 @@ void _restoreCursor() {
257259
}
258260

259261
/**
260-
* Returns the element that is one layer under the element where the mouse
261-
* is currently over.
262+
* Returns the element where the mouse is currently over. If the mouse is over
263+
* [element], the element below [element] is returned.
262264
*/
263-
EventTarget _getElementUnder(MouseEvent event) {
264-
var target = event.target;
265-
if (target is Element) {
266-
target.style.visibility = 'hidden';
267-
Element elementUnder = document.elementFromPoint(event.client.x, event.client.y);
268-
target.style.visibility = 'visible';
269-
if (elementUnder != null) {
270-
return elementUnder;
271-
}
272-
}
273-
// Could not get element under --> return original target.
274-
return target;
265+
EventTarget _getElementUnder(Element element, MouseEvent event) {
266+
element.style.visibility = 'hidden';
267+
Element elementUnder = document.elementFromPoint(event.client.x, event.client.y);
268+
element.style.visibility = 'visible';
269+
return elementUnder;
275270
}
276271

277272
/**

lib/src/dnd/dropzone.dart

Lines changed: 98 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -91,31 +91,26 @@ class DropzoneGroup extends Group {
9191

9292
void _handleDragEnter(Element element, Point mousePagePosition,
9393
Point mouseClientPosition, EventTarget target) {
94-
_logger.finest('handleDragEnter {dragOverElements.length: ${currentDragOverElements.length}}');
94+
_logger.finest('handleDragEnter');
9595

96-
// Only handle dropzone element itself and not any of its children.
97-
if (currentDragOverElements.isEmpty) {
98-
if (currentDraggableGroup.overClass != null) {
99-
String overClass = currentDraggableGroup.overClass;
100-
element.classes.add(overClass);
101-
102-
// Make sure overClass is removed when drag ended. Is necessary
103-
// because if drag is aborted (e.g. with esc-key), no dragLeave or
104-
// drop event is fired on the dropzone.
105-
StreamSubscription dragEndSub;
106-
dragEndSub = currentDraggableGroup.onDragEnd.listen((_) {
107-
element.classes.remove(overClass);
108-
dragEndSub.cancel();
109-
});
110-
}
96+
if (currentDraggableGroup.overClass != null) {
97+
String overClass = currentDraggableGroup.overClass;
98+
element.classes.add(overClass);
11199

112-
if (_onDragEnter != null) {
113-
_onDragEnter.add(new DropzoneEvent(currentDraggable,
114-
element, mousePagePosition, mouseClientPosition));
115-
}
100+
// Make sure overClass is removed when drag ended. Is necessary
101+
// because if drag is aborted (e.g. with esc-key), no dragLeave or
102+
// drop event is fired on the dropzone.
103+
StreamSubscription dragEndSub;
104+
dragEndSub = currentDraggableGroup.onDragEnd.listen((_) {
105+
element.classes.remove(overClass);
106+
dragEndSub.cancel();
107+
});
116108
}
117109

118-
currentDragOverElements.add(target);
110+
if (_onDragEnter != null) {
111+
_onDragEnter.add(new DropzoneEvent(currentDraggable,
112+
element, mousePagePosition, mouseClientPosition));
113+
}
119114
}
120115

121116
void _handleDragOver(Element element, Point mousePagePosition, Point mouseClientPosition) {
@@ -128,26 +123,21 @@ class DropzoneGroup extends Group {
128123
void _handleDragLeave(Element element, Point mousePagePosition,
129124
Point mouseClientPosition, EventTarget target,
130125
EventTarget relatedTarget) {
131-
// Firefox fires too many onDragLeave events. This condition fixes it.
132-
if (target != relatedTarget) {
133-
currentDragOverElements.remove(target);
126+
_logger.finest('handleDragLeave');
127+
128+
if (currentDraggableGroup.overClass != null) {
129+
element.classes.remove(currentDraggableGroup.overClass);
134130
}
135-
_logger.finest('handleDragLeave {dragOverElements.length: ${currentDragOverElements.length}}');
136131

137-
// Only handle event if dropzone element is left and not on any of its children.
138-
if (currentDragOverElements.isEmpty) {
139-
if (currentDraggableGroup.overClass != null) {
140-
element.classes.remove(currentDraggableGroup.overClass);
141-
}
142-
143-
if (_onDragLeave != null) {
144-
_onDragLeave.add(new DropzoneEvent(currentDraggable, element,
145-
mousePagePosition, mouseClientPosition));
146-
}
132+
if (_onDragLeave != null) {
133+
_onDragLeave.add(new DropzoneEvent(currentDraggable, element,
134+
mousePagePosition, mouseClientPosition));
147135
}
148136
}
149137

150138
void _handleDrop(Element element, Point mousePagePosition, Point mouseClientPosition) {
139+
_logger.finest('handleDrop');
140+
151141
if (_onDrop != null) {
152142
_onDrop.add(new DropzoneEvent(currentDraggable, element,
153143
mousePagePosition, mouseClientPosition));
@@ -188,22 +178,37 @@ List<StreamSubscription> _installDropzone(Element element, DropzoneGroup group)
188178
// Do nothing if no element of this dnd is dragged.
189179
if (currentDraggable == null) return;
190180

181+
// Firefox fires too many dragEnter events. This condition fixes it.
182+
// Actually, according to W3C spec, a dragEnter event should not have a
183+
// related target at all
184+
if (mouseEvent.target == mouseEvent.relatedTarget) return;
185+
191186
// Necessary for IE?
192187
mouseEvent.preventDefault();
193188

194-
// Test if this dropzone accepts the current draggable.
195-
draggableAccepted = group._draggableAccepted();
196-
if (draggableAccepted) {
197-
mouseEvent.dataTransfer.dropEffect = currentDraggableGroup.dropEffect;
198-
} else {
199-
mouseEvent.dataTransfer.dropEffect = 'none';
200-
return; // Return here as drop is not accepted.
201-
}
202-
203189
_logger.finest('dragEnter');
204-
205-
group._handleDragEnter(element, mouseEvent.page, mouseEvent.client,
206-
mouseEvent.target);
190+
191+
// Related target (the element dragged from) is the last entered element.
192+
var relatedTarget = _lastDragEnterTarget;
193+
194+
// Save current target.
195+
_lastDragEnterTarget = mouseEvent.target;
196+
197+
// Only continue if the event is a real event generated for the main
198+
// element and not bubbled up by any of its children.
199+
if (_isMainEvent(element, relatedTarget)) {
200+
// Test if this dropzone accepts the current draggable.
201+
draggableAccepted = group._draggableAccepted();
202+
if (draggableAccepted) {
203+
mouseEvent.dataTransfer.dropEffect = currentDraggableGroup.dropEffect;
204+
} else {
205+
mouseEvent.dataTransfer.dropEffect = 'none';
206+
return; // Return here as drop is not accepted.
207+
}
208+
209+
group._handleDragEnter(element, mouseEvent.page, mouseEvent.client,
210+
mouseEvent.target);
211+
}
207212
}));
208213

209214
// -------------------
@@ -233,10 +238,33 @@ List<StreamSubscription> _installDropzone(Element element, DropzoneGroup group)
233238
// Do nothing if no element of this dnd is dragged.
234239
if (currentDraggable == null || !draggableAccepted) return;
235240

241+
// Firefox fires too many dragLeave events. This condition fixes it.
242+
// Actually, according to W3C spec, a dragLeave event should not have a
243+
// related target at all
244+
if (mouseEvent.target == mouseEvent.relatedTarget) return;
245+
236246
_logger.finest('dragLeave');
237247

238-
group._handleDragLeave(element, mouseEvent.page, mouseEvent.client,
239-
mouseEvent.target, mouseEvent.relatedTarget);
248+
// Related target (the element dragged to) is the last entered element.
249+
var relatedTarget;
250+
if (mouseEvent.target != _lastDragEnterTarget) {
251+
relatedTarget = _lastDragEnterTarget;
252+
} else {
253+
// The mouse left the main element. When this happens, there is no
254+
// dragEnter event before the dragLeave because the entered element would
255+
// be null or an element outside of the main element.
256+
relatedTarget = null;
257+
}
258+
259+
// Only continue if the event is a real event generated for the main
260+
// element and not bubbled up by any of its children.
261+
if (_isMainEvent(element, relatedTarget)) {
262+
// Main element was left so reset the targets.
263+
_lastDragEnterTarget = null;
264+
265+
group._handleDragLeave(element, mouseEvent.page, mouseEvent.client,
266+
mouseEvent.target, mouseEvent.relatedTarget);
267+
}
240268
}));
241269

242270
// -------------------
@@ -245,6 +273,7 @@ List<StreamSubscription> _installDropzone(Element element, DropzoneGroup group)
245273
subs.add(element.onDrop.listen((MouseEvent mouseEvent) {
246274
// Do nothing if no element of this dnd is dragged.
247275
if (currentDraggable == null || !draggableAccepted) return;
276+
248277
_logger.finest('drop');
249278

250279
// Stops browsers from redirecting.
@@ -256,6 +285,26 @@ List<StreamSubscription> _installDropzone(Element element, DropzoneGroup group)
256285
return subs;
257286
}
258287

288+
289+
// As dragEnter and dragLeave events do not have the relatedTarget property set,
290+
// we keep track of the last target the user entered and use this as related target.
291+
EventTarget _lastDragEnterTarget;
292+
293+
/**
294+
* Returns true if the mouse event with [relatedTarget] is actually a real event
295+
* generated for [mainElement] and not bubbled up by any of its children.
296+
*
297+
* This is used to filter dragEnter, dragLeave, mouseOver, mouseOut events:
298+
* * For dragEnter and mouseOver the [relatedTarget] must be the element the
299+
* mouse entered from.
300+
* * For dragLeave and mouseOut the [relatedTarget] must be the element the
301+
* mouse entered to.
302+
*/
303+
bool _isMainEvent(Element mainElement, EventTarget relatedTarget) {
304+
return (relatedTarget == null
305+
|| (relatedTarget != mainElement && !mainElement.contains(relatedTarget)));
306+
}
307+
259308
/**
260309
* Event for dropzone elements.
261310
*/

lib/src/dnd/dropzone_emulated.dart

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,13 @@ List<StreamSubscription> _installEmulatedDropzone(Element element, DropzoneGroup
2121

2222
_logger.finest('emulated dragEnter');
2323

24-
if (!element.contains(mouseEvent.relatedTarget)) {
25-
// Mouse was moved from outside and might have skipped some elements.
26-
// Must clear the drag over elements.
27-
_logger.finest('relatedTarget is not a child of element having the listener, clearing dragOverElements.');
28-
currentDragOverElements.clear();
24+
// Only continue if the event is a real event generated for the main
25+
// element and not bubbled up by any of its children.
26+
if (_isMainEvent(element, mouseEvent.relatedTarget)) {
27+
// screenX and screenY were abused as pageX and pageY! TODO: Wait for #11452 to be fixed.
28+
Point mousePagePosition = mouseEvent.screen;
29+
group._handleDragEnter(element, mousePagePosition, mouseEvent.client, mouseEvent.target);
2930
}
30-
31-
// screenX and screenY were abused as pageX and pageY! TODO: Wait for #11452 to be fixed.
32-
Point mousePagePosition = mouseEvent.screen;
33-
group._handleDragEnter(element, mousePagePosition, mouseEvent.client, mouseEvent.target);
3431
}));
3532

3633
// -------------------
@@ -72,10 +69,14 @@ List<StreamSubscription> _installEmulatedDropzone(Element element, DropzoneGroup
7269

7370
_logger.finest('emulated dragLeave');
7471

75-
// screenX and screenY were abused as pageX and pageY! TODO: Wait for #11452 to be fixed.
76-
Point mousePagePosition = mouseEvent.screen;
77-
group._handleDragLeave(element, mousePagePosition, mouseEvent.client,
78-
mouseEvent.target, mouseEvent.relatedTarget);
72+
// Only continue if the event is a real event generated for the main
73+
// element and not bubbled up by any of its children.
74+
if (_isMainEvent(element, mouseEvent.relatedTarget)) {
75+
// screenX and screenY were abused as pageX and pageY! TODO: Wait for #11452 to be fixed.
76+
Point mousePagePosition = mouseEvent.screen;
77+
group._handleDragLeave(element, mousePagePosition, mouseEvent.client,
78+
mouseEvent.target, mouseEvent.relatedTarget);
79+
}
7980
}));
8081

8182
// -------------------

0 commit comments

Comments
 (0)