@@ -20,14 +20,24 @@ import {useVirtualDrop} from './useVirtualDrop';
20
20
21
21
export interface DropOptions {
22
22
ref : RefObject < HTMLElement > ,
23
+ /**
24
+ * A function returning the drop operation to be performed when items matching the given types are dropped
25
+ * on the drop target.
26
+ */
23
27
getDropOperation ?: ( types : IDragTypes , allowedOperations : DropOperation [ ] ) => DropOperation ,
24
28
getDropOperationForPoint ?: ( types : IDragTypes , allowedOperations : DropOperation [ ] , x : number , y : number ) => DropOperation ,
29
+ /** Handler that is called when a valid drag enters the drop target. */
25
30
onDropEnter ?: ( e : DropEnterEvent ) => void ,
31
+ /** Handler that is called when a valid drag is moved within the drop target. */
26
32
onDropMove ?: ( e : DropMoveEvent ) => void ,
27
- // When the user hovers over the drop target for a period of time.
28
- // typically opens that item. macOS/iOS call this "spring loading".
33
+ /**
34
+ * Handler that is called after a valid drag is held over the drop target for a period of time.
35
+ * This typically opens the item so that the user can drop within it.
36
+ */
29
37
onDropActivate ?: ( e : DropActivateEvent ) => void ,
38
+ /** Handler that is called when a valid drag exits the drop target. */
30
39
onDropExit ?: ( e : DropExitEvent ) => void ,
40
+ /** Handler that is called when a valid drag is dropped on the drop target. */
31
41
onDrop ?: ( e : DropEvent ) => void
32
42
}
33
43
@@ -43,34 +53,86 @@ export function useDrop(options: DropOptions): DropResult {
43
53
let state = useRef ( {
44
54
x : 0 ,
45
55
y : 0 ,
46
- dragEnterCount : 0 ,
56
+ dragOverElements : new Set < Element > ( ) ,
47
57
dropEffect : 'none' as DataTransfer [ 'dropEffect' ] ,
58
+ effectAllowed : 'none' as DataTransfer [ 'effectAllowed' ] ,
48
59
dropActivateTimer : null
49
60
} ) . current ;
50
61
62
+ let fireDropEnter = ( e : DragEvent ) => {
63
+ setDropTarget ( true ) ;
64
+
65
+ if ( typeof options . onDropEnter === 'function' ) {
66
+ let rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( ) ;
67
+ options . onDropEnter ( {
68
+ type : 'dropenter' ,
69
+ x : e . clientX - rect . x ,
70
+ y : e . clientY - rect . y
71
+ } ) ;
72
+ }
73
+ } ;
74
+
75
+ let fireDropExit = ( e : DragEvent ) => {
76
+ setDropTarget ( false ) ;
77
+
78
+ if ( typeof options . onDropExit === 'function' ) {
79
+ let rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( ) ;
80
+ options . onDropExit ( {
81
+ type : 'dropexit' ,
82
+ x : e . clientX - rect . x ,
83
+ y : e . clientY - rect . y
84
+ } ) ;
85
+ }
86
+ } ;
87
+
51
88
let onDragOver = ( e : DragEvent ) => {
52
89
e . preventDefault ( ) ;
53
90
e . stopPropagation ( ) ;
54
91
55
- if ( e . clientX === state . x && e . clientY === state . y ) {
92
+ if ( e . clientX === state . x && e . clientY === state . y && e . dataTransfer . effectAllowed === state . effectAllowed ) {
56
93
e . dataTransfer . dropEffect = state . dropEffect ;
57
94
return ;
58
95
}
59
96
60
97
state . x = e . clientX ;
61
98
state . y = e . clientY ;
62
99
100
+ let prevDropEffect = state . dropEffect ;
101
+
102
+ // Update drop effect if allowed drop operations changed (e.g. user pressed modifier key).
103
+ if ( e . dataTransfer . effectAllowed !== state . effectAllowed ) {
104
+ let allowedOperations = effectAllowedToOperations ( e . dataTransfer . effectAllowed ) ;
105
+ let dropOperation = allowedOperations [ 0 ] ;
106
+ if ( typeof options . getDropOperation === 'function' ) {
107
+ let types = new DragTypes ( e . dataTransfer ) ;
108
+ dropOperation = getDropOperation ( e . dataTransfer . effectAllowed , options . getDropOperation ( types , allowedOperations ) ) ;
109
+ }
110
+
111
+ state . dropEffect = DROP_OPERATION_TO_DROP_EFFECT [ dropOperation ] || 'none' ;
112
+ }
113
+
63
114
if ( typeof options . getDropOperationForPoint === 'function' ) {
64
115
let allowedOperations = effectAllowedToOperations ( e . dataTransfer . effectAllowed ) ;
65
116
let types = new DragTypes ( e . dataTransfer ) ;
66
117
let rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( ) ;
67
- let dropOperation = options . getDropOperationForPoint ( types , allowedOperations , state . x - rect . x , state . y - rect . y ) ;
118
+ let dropOperation = getDropOperation (
119
+ e . dataTransfer . effectAllowed ,
120
+ options . getDropOperationForPoint ( types , allowedOperations , state . x - rect . x , state . y - rect . y )
121
+ ) ;
68
122
state . dropEffect = DROP_OPERATION_TO_DROP_EFFECT [ dropOperation ] || 'none' ;
69
123
}
70
124
125
+ state . effectAllowed = e . dataTransfer . effectAllowed ;
71
126
e . dataTransfer . dropEffect = state . dropEffect ;
72
127
73
- if ( typeof options . onDropMove === 'function' ) {
128
+ // If the drop operation changes, update state and fire events appropriately.
129
+ if ( state . dropEffect === 'none' && prevDropEffect !== 'none' ) {
130
+ fireDropExit ( e ) ;
131
+ } else if ( state . dropEffect !== 'none' && prevDropEffect === 'none' ) {
132
+ fireDropEnter ( e ) ;
133
+ }
134
+
135
+ if ( typeof options . onDropMove === 'function' && state . dropEffect !== 'none' ) {
74
136
let rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( ) ;
75
137
options . onDropMove ( {
76
138
type : 'dropmove' ,
@@ -95,8 +157,8 @@ export function useDrop(options: DropOptions): DropResult {
95
157
96
158
let onDragEnter = ( e : DragEvent ) => {
97
159
e . stopPropagation ( ) ;
98
- state . dragEnterCount ++ ;
99
- if ( state . dragEnterCount > 1 ) {
160
+ state . dragOverElements . add ( e . target as Element ) ;
161
+ if ( state . dragOverElements . size > 1 ) {
100
162
return ;
101
163
}
102
164
@@ -105,52 +167,55 @@ export function useDrop(options: DropOptions): DropResult {
105
167
106
168
if ( typeof options . getDropOperation === 'function' ) {
107
169
let types = new DragTypes ( e . dataTransfer ) ;
108
- dropOperation = options . getDropOperation ( types , allowedOperations ) ;
109
- }
110
-
111
- if ( dropOperation !== 'cancel' ) {
112
- setDropTarget ( true ) ;
170
+ dropOperation = getDropOperation ( e . dataTransfer . effectAllowed , options . getDropOperation ( types , allowedOperations ) ) ;
113
171
}
114
172
115
173
if ( typeof options . getDropOperationForPoint === 'function' ) {
116
174
let types = new DragTypes ( e . dataTransfer ) ;
117
175
let rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( ) ;
118
- dropOperation = options . getDropOperationForPoint ( types , allowedOperations , e . clientX - rect . x , e . clientY - rect . y ) ;
176
+ dropOperation = getDropOperation (
177
+ e . dataTransfer . effectAllowed ,
178
+ options . getDropOperationForPoint ( types , allowedOperations , e . clientX - rect . x , e . clientY - rect . y )
179
+ ) ;
119
180
}
120
181
182
+ state . x = e . clientX ;
183
+ state . y = e . clientY ;
184
+ state . effectAllowed = e . dataTransfer . effectAllowed ;
121
185
state . dropEffect = DROP_OPERATION_TO_DROP_EFFECT [ dropOperation ] || 'none' ;
122
186
e . dataTransfer . dropEffect = state . dropEffect ;
123
187
124
- if ( typeof options . onDropEnter === 'function' && dropOperation !== 'cancel' ) {
125
- let rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( ) ;
126
- options . onDropEnter ( {
127
- type : 'dropenter' ,
128
- x : e . clientX - rect . x ,
129
- y : e . clientY - rect . y
130
- } ) ;
188
+ if ( dropOperation !== 'cancel' ) {
189
+ fireDropEnter ( e ) ;
131
190
}
132
-
133
- state . x = e . clientX ;
134
- state . y = e . clientY ;
135
191
} ;
136
192
137
193
let onDragLeave = ( e : DragEvent ) => {
138
194
e . stopPropagation ( ) ;
139
- state . dragEnterCount -- ;
140
- if ( state . dragEnterCount > 0 ) {
195
+
196
+ // We would use e.relatedTarget to detect if the drag is still inside the drop target,
197
+ // but it is always null in WebKit. https://bugs.webkit.org/show_bug.cgi?id=66547
198
+ // Instead, we track all of the targets of dragenter events in a set, and remove them
199
+ // in dragleave. When the set becomes empty, we've left the drop target completely.
200
+ // We must also remove any elements that are no longer in the DOM, because dragleave
201
+ // events will never be fired for these. This can happen, for example, with drop
202
+ // indicators between items, which disappear when the drop target changes.
203
+
204
+ state . dragOverElements . delete ( e . target as Element ) ;
205
+ for ( let element of state . dragOverElements ) {
206
+ if ( ! e . currentTarget . contains ( element ) ) {
207
+ state . dragOverElements . delete ( element ) ;
208
+ }
209
+ }
210
+
211
+ if ( state . dragOverElements . size > 0 ) {
141
212
return ;
142
213
}
143
214
144
- if ( typeof options . onDropExit === 'function' && state . dropEffect !== 'none' ) {
145
- let rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( ) ;
146
- options . onDropExit ( {
147
- type : 'dropexit' ,
148
- x : e . clientX - rect . x ,
149
- y : e . clientY - rect . y
150
- } ) ;
215
+ if ( state . dropEffect !== 'none' ) {
216
+ fireDropExit ( e ) ;
151
217
}
152
218
153
- setDropTarget ( false ) ;
154
219
clearTimeout ( state . dropActivateTimer ) ;
155
220
} ;
156
221
@@ -180,17 +245,8 @@ export function useDrop(options: DropOptions): DropResult {
180
245
} , 0 ) ;
181
246
}
182
247
183
- if ( typeof options . onDropExit === 'function' ) {
184
- let rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( ) ;
185
- options . onDropExit ( {
186
- type : 'dropexit' ,
187
- x : e . clientX - rect . x ,
188
- y : e . clientY - rect . y
189
- } ) ;
190
- }
191
-
192
- state . dragEnterCount = 0 ;
193
- setDropTarget ( false ) ;
248
+ state . dragOverElements . clear ( ) ;
249
+ fireDropExit ( e ) ;
194
250
clearTimeout ( state . dropActivateTimer ) ;
195
251
} ;
196
252
@@ -255,3 +311,9 @@ function effectAllowedToOperations(effectAllowed: string) {
255
311
256
312
return allowedOperations ;
257
313
}
314
+
315
+ function getDropOperation ( effectAllowed : string , operation : DropOperation ) {
316
+ let allowedOperationsBits = DROP_OPERATION_ALLOWED [ effectAllowed ] ;
317
+ let op = DROP_OPERATION [ operation ] ;
318
+ return allowedOperationsBits & op ? operation : 'cancel' ;
319
+ }
0 commit comments