From 28c8f1dbd6d98adea16cd51b59cf0d0962c2b54f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 21 Apr 2023 16:03:44 -0500 Subject: [PATCH] Add useDraggedFiles hook --- app/soapbox/hooks/index.ts | 1 + app/soapbox/hooks/useDraggedFiles.ts | 70 ++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 app/soapbox/hooks/useDraggedFiles.ts diff --git a/app/soapbox/hooks/index.ts b/app/soapbox/hooks/index.ts index 6f52e0c8f..0bd63eb21 100644 --- a/app/soapbox/hooks/index.ts +++ b/app/soapbox/hooks/index.ts @@ -6,6 +6,7 @@ export { useBackend } from './useBackend'; export { useClickOutside } from './useClickOutside'; export { useCompose } from './useCompose'; export { useDebounce } from './useDebounce'; +export { useDraggedFiles } from './useDraggedFiles'; export { useGetState } from './useGetState'; export { useGroupsPath } from './useGroupsPath'; export { useDimensions } from './useDimensions'; diff --git a/app/soapbox/hooks/useDraggedFiles.ts b/app/soapbox/hooks/useDraggedFiles.ts new file mode 100644 index 000000000..a35303ef6 --- /dev/null +++ b/app/soapbox/hooks/useDraggedFiles.ts @@ -0,0 +1,70 @@ +import React, { useCallback, useEffect, useState } from 'react'; + +/** Controls the state of files being dragged over a node. */ +function useDraggedFiles(node: React.RefObject, onDrop?: (files: FileList) => void) { + const [isDragging, setIsDragging] = useState(false); + + const handleDocumentDragEnter = useCallback((e: DragEvent) => { + if (isDraggingFiles(e)) { + setIsDragging(true); + } + }, [setIsDragging]); + + const handleDocumentDragLeave = useCallback((e: DragEvent) => { + if (isOffscreen(e)) { + setIsDragging(false); + } + }, [setIsDragging]); + + const handleDocumentDrop = useCallback((e: DragEvent) => { + setIsDragging(false); + }, [setIsDragging]); + + const handleDrop = useCallback((e: DragEvent) => { + if (isDraggingFiles(e) && onDrop) { + onDrop(e.dataTransfer.files); + } + setIsDragging(false); + e.preventDefault(); + }, [onDrop]); + + useEffect(() => { + document.addEventListener('dragenter', handleDocumentDragEnter); + document.addEventListener('dragleave', handleDocumentDragLeave); + document.addEventListener('drop', handleDocumentDrop); + return () => { + document.removeEventListener('dragenter', handleDocumentDragEnter); + document.removeEventListener('dragleave', handleDocumentDragLeave); + document.removeEventListener('drop', handleDocumentDrop); + }; + }, []); + + useEffect(() => { + node.current?.addEventListener('drop', handleDrop); + return () => { + node.current?.removeEventListener('drop', handleDrop); + }; + }, [node.current]); + + return { + /** Whether the document is being dragged over. */ + isDragging, + }; +} + +/** Ensure only files are being dragged, and not eg highlighted text. */ +function isDraggingFiles(e: DragEvent): e is DragEvent & { dataTransfer: DataTransfer } { + if (e.dataTransfer) { + const { types } = e.dataTransfer; + return types.length === 1 && types[0] === 'Files'; + } else { + return false; + } +} + +/** Check whether the cursor is in the screen. Mostly useful for dragleave events. */ +function isOffscreen(e: DragEvent): boolean { + return e.screenX === 0 && e.screenY === 0; +} + +export { useDraggedFiles }; \ No newline at end of file