@@ -20,14 +20,24 @@ import {useVirtualDrop} from './useVirtualDrop';
2020
2121export interface DropOptions {
2222 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+ */
2327 getDropOperation ?: ( types : IDragTypes , allowedOperations : DropOperation [ ] ) => DropOperation ,
2428 getDropOperationForPoint ?: ( types : IDragTypes , allowedOperations : DropOperation [ ] , x : number , y : number ) => DropOperation ,
29+ /** Handler that is called when a valid drag enters the drop target. */
2530 onDropEnter ?: ( e : DropEnterEvent ) => void ,
31+ /** Handler that is called when a valid drag is moved within the drop target. */
2632 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+ */
2937 onDropActivate ?: ( e : DropActivateEvent ) => void ,
38+ /** Handler that is called when a valid drag exits the drop target. */
3039 onDropExit ?: ( e : DropExitEvent ) => void ,
40+ /** Handler that is called when a valid drag is dropped on the drop target. */
3141 onDrop ?: ( e : DropEvent ) => void
3242}
3343
@@ -43,34 +53,86 @@ export function useDrop(options: DropOptions): DropResult {
4353 let state = useRef ( {
4454 x : 0 ,
4555 y : 0 ,
46- dragEnterCount : 0 ,
56+ dragOverElements : new Set < Element > ( ) ,
4757 dropEffect : 'none' as DataTransfer [ 'dropEffect' ] ,
58+ effectAllowed : 'none' as DataTransfer [ 'effectAllowed' ] ,
4859 dropActivateTimer : null
4960 } ) . current ;
5061
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+
5188 let onDragOver = ( e : DragEvent ) => {
5289 e . preventDefault ( ) ;
5390 e . stopPropagation ( ) ;
5491
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 ) {
5693 e . dataTransfer . dropEffect = state . dropEffect ;
5794 return ;
5895 }
5996
6097 state . x = e . clientX ;
6198 state . y = e . clientY ;
6299
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+
63114 if ( typeof options . getDropOperationForPoint === 'function' ) {
64115 let allowedOperations = effectAllowedToOperations ( e . dataTransfer . effectAllowed ) ;
65116 let types = new DragTypes ( e . dataTransfer ) ;
66117 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+ ) ;
68122 state . dropEffect = DROP_OPERATION_TO_DROP_EFFECT [ dropOperation ] || 'none' ;
69123 }
70124
125+ state . effectAllowed = e . dataTransfer . effectAllowed ;
71126 e . dataTransfer . dropEffect = state . dropEffect ;
72127
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' ) {
74136 let rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( ) ;
75137 options . onDropMove ( {
76138 type : 'dropmove' ,
@@ -95,8 +157,8 @@ export function useDrop(options: DropOptions): DropResult {
95157
96158 let onDragEnter = ( e : DragEvent ) => {
97159 e . stopPropagation ( ) ;
98- state . dragEnterCount ++ ;
99- if ( state . dragEnterCount > 1 ) {
160+ state . dragOverElements . add ( e . target as Element ) ;
161+ if ( state . dragOverElements . size > 1 ) {
100162 return ;
101163 }
102164
@@ -105,52 +167,55 @@ export function useDrop(options: DropOptions): DropResult {
105167
106168 if ( typeof options . getDropOperation === 'function' ) {
107169 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 ) ) ;
113171 }
114172
115173 if ( typeof options . getDropOperationForPoint === 'function' ) {
116174 let types = new DragTypes ( e . dataTransfer ) ;
117175 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+ ) ;
119180 }
120181
182+ state . x = e . clientX ;
183+ state . y = e . clientY ;
184+ state . effectAllowed = e . dataTransfer . effectAllowed ;
121185 state . dropEffect = DROP_OPERATION_TO_DROP_EFFECT [ dropOperation ] || 'none' ;
122186 e . dataTransfer . dropEffect = state . dropEffect ;
123187
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 ) ;
131190 }
132-
133- state . x = e . clientX ;
134- state . y = e . clientY ;
135191 } ;
136192
137193 let onDragLeave = ( e : DragEvent ) => {
138194 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 ) {
141212 return ;
142213 }
143214
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 ) ;
151217 }
152218
153- setDropTarget ( false ) ;
154219 clearTimeout ( state . dropActivateTimer ) ;
155220 } ;
156221
@@ -180,17 +245,8 @@ export function useDrop(options: DropOptions): DropResult {
180245 } , 0 ) ;
181246 }
182247
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 ) ;
194250 clearTimeout ( state . dropActivateTimer ) ;
195251 } ;
196252
@@ -255,3 +311,9 @@ function effectAllowedToOperations(effectAllowed: string) {
255311
256312 return allowedOperations ;
257313}
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