@wordpress/compose
Version:
WordPress higher-order components (HOCs).
203 lines (169 loc) • 5.94 kB
text/typescript
/**
* Internal dependencies
*/
import useRefEffect from '../use-ref-effect';
import useEvent from '../use-event';
import type { UseDropZoneProps } from './types';
/**
* A hook to facilitate drag and drop handling.
*
* @param {UseDropZoneProps} props Hook options
* @return Ref callback to be passed to the drop zone element.
*/
export default function useDropZone( {
dropZoneElement,
isDisabled,
onDrop: _onDrop,
onDragStart: _onDragStart,
onDragEnter: _onDragEnter,
onDragLeave: _onDragLeave,
onDragEnd: _onDragEnd,
onDragOver: _onDragOver,
}: UseDropZoneProps ): React.RefCallback< HTMLElement > {
const onDropEvent = useEvent( _onDrop );
const onDragStartEvent = useEvent( _onDragStart );
const onDragEnterEvent = useEvent( _onDragEnter );
const onDragLeaveEvent = useEvent( _onDragLeave );
const onDragEndEvent = useEvent( _onDragEnd );
const onDragOverEvent = useEvent( _onDragOver );
return useRefEffect(
( elem: HTMLElement ) => {
if ( isDisabled ) {
return;
}
// If a custom dropZoneRef is passed, use that instead of the element.
// This allows the dropzone to cover an expanded area, rather than
// be restricted to the area of the ref returned by this hook.
const element = dropZoneElement ?? elem;
let isDragging = false;
const { ownerDocument } = element;
/**
* Checks if an element is in the drop zone.
*
* @param {EventTarget|null} targetToCheck
*
* @return {boolean} True if in drop zone, false if not.
*/
function isElementInZone(
targetToCheck: EventTarget | null
): boolean {
const { defaultView } = ownerDocument;
if (
! targetToCheck ||
! defaultView ||
! ( targetToCheck instanceof defaultView.HTMLElement ) ||
! element.contains( targetToCheck )
) {
return false;
}
let elementToCheck: HTMLElement | null = targetToCheck;
do {
if ( elementToCheck.dataset.isDropZone ) {
return elementToCheck === element;
}
} while ( ( elementToCheck = elementToCheck.parentElement ) );
return false;
}
function maybeDragStart( event: DragEvent ): void {
if ( isDragging ) {
return;
}
isDragging = true;
// Note that `dragend` doesn't fire consistently for file and
// HTML drag events where the drag origin is outside the browser
// window. In Firefox it may also not fire if the originating
// node is removed.
ownerDocument.addEventListener( 'dragend', maybeDragEnd );
ownerDocument.addEventListener( 'mousemove', maybeDragEnd );
if ( _onDragStart ) {
onDragStartEvent( event );
}
}
function onDragEnter( event: DragEvent ): void {
event.preventDefault();
// The `dragenter` event will also fire when entering child
// elements, but we only want to call `onDragEnter` when
// entering the drop zone, which means the `relatedTarget`
// (element that has been left) should be outside the drop zone.
if ( element.contains( event.relatedTarget as Node ) ) {
return;
}
if ( _onDragEnter ) {
onDragEnterEvent( event );
}
}
function onDragOver( event: DragEvent ): void {
if ( ! event.defaultPrevented && _onDragOver ) {
onDragOverEvent( event );
}
event.preventDefault();
}
function onDragLeave( event: DragEvent ): void {
// The `dragleave` event will also fire when leaving child
// elements, but we only want to call `onDragLeave` when
// leaving the drop zone, which means the `relatedTarget`
// (element that has been entered) should be outside the drop
// zone.
// Note: This is not entirely reliable in Safari due to this bug
// https://bugs.webkit.org/show_bug.cgi?id=66547
if ( isElementInZone( event.relatedTarget ) ) {
return;
}
if ( _onDragLeave ) {
onDragLeaveEvent( event );
}
}
function onDrop( event: DragEvent ): void {
// Don't handle drop if an inner drop zone already handled it.
if ( event.defaultPrevented ) {
return;
}
// Prevent the browser default while also signalling to parent
// drop zones that `onDrop` is already handled.
event.preventDefault();
// This seemingly useless line has been shown to resolve a
// Safari issue where files dragged directly from the dock are
// not recognized.
// eslint-disable-next-line no-unused-expressions
event.dataTransfer && event.dataTransfer.files.length;
if ( _onDrop ) {
onDropEvent( event );
}
maybeDragEnd( event );
}
function maybeDragEnd( event: Event ): void {
if ( ! isDragging ) {
return;
}
isDragging = false;
ownerDocument.removeEventListener( 'dragend', maybeDragEnd );
ownerDocument.removeEventListener( 'mousemove', maybeDragEnd );
if ( _onDragEnd ) {
onDragEndEvent( event as MouseEvent );
}
}
element.setAttribute( 'data-is-drop-zone', 'true' );
element.addEventListener( 'drop', onDrop );
element.addEventListener( 'dragenter', onDragEnter );
element.addEventListener( 'dragover', onDragOver );
element.addEventListener( 'dragleave', onDragLeave );
// The `dragstart` event doesn't fire if the drag started outside
// the document.
ownerDocument.addEventListener( 'dragenter', maybeDragStart );
return () => {
element.removeAttribute( 'data-is-drop-zone' );
element.removeEventListener( 'drop', onDrop );
element.removeEventListener( 'dragenter', onDragEnter );
element.removeEventListener( 'dragover', onDragOver );
element.removeEventListener( 'dragleave', onDragLeave );
ownerDocument.removeEventListener( 'dragend', maybeDragEnd );
ownerDocument.removeEventListener( 'mousemove', maybeDragEnd );
ownerDocument.removeEventListener(
'dragenter',
maybeDragStart
);
};
},
[ isDisabled, dropZoneElement ] // Refresh when the passed in dropZoneElement changes.
);
}