Skip to content

Commit cd094fd

Browse files
authored
Merge pull request #344 from siberiacancode/#317
#317 [feat] useDropZone
2 parents e3a6ee8 + 88c5400 commit cd094fd

File tree

5 files changed

+424
-0
lines changed

5 files changed

+424
-0
lines changed

packages/core/src/bundle/hooks/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export * from './useDocumentEvent/useDocumentEvent';
2626
export * from './useDocumentTitle/useDocumentTitle';
2727
export * from './useDocumentVisibility/useDocumentVisibility';
2828
export * from './useDoubleClick/useDoubleClick';
29+
export * from './useDropZone/useDropZone';
2930
export * from './useElementSize/useElementSize';
3031
export * from './useEvent/useEvent';
3132
export * from './useEventListener/useEventListener';
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { useEffect, useState } from 'react';
2+
import { getElement, isTarget } from '@/utils/helpers';
3+
import { useRefState } from '../useRefState/useRefState';
4+
/**
5+
* @name useDropZone
6+
* @description - Hook that provides drop zone functionality
7+
* @category Elements
8+
*
9+
* @overload
10+
* @template Target The target element
11+
* @param {Target} target The target element drop zone's
12+
* @param {DataTypes} [options.dataTypes] The data types
13+
* @param {boolean} [options.multiple] The multiple mode
14+
* @param {(files: File[] | null, event: DragEvent) => void} [options.onDrop] The on drop callback function
15+
* @param {(files: File[] | null, event: DragEvent) => void} [options.onEnter] The on enter callback function
16+
* @param {(files: File[] | null, event: DragEvent) => void} [options.onLeave] The on leave callback function
17+
* @param {(files: File[] | null, event: DragEvent) => void} [options.onOver] The on over callback function
18+
* @returns {[boolean, File[] | null]} The object with drop zone states
19+
*
20+
* @example
21+
* const {isOver, files} = useDropZone(ref, options);
22+
*
23+
* @overload
24+
* @param {Target} target The target element drop zone's
25+
* @param {(files: File[] | null, event: DragEvent) => void} [callback] The callback function to be invoked on drop
26+
* @returns {[boolean, File[] | null]} The object with drop zone states
27+
*
28+
* @example
29+
* const {isOver, files} = useDropZone(ref, () => console.log('callback'));
30+
*
31+
* @overload
32+
* @param {DataTypes} [options.dataTypes] The data types
33+
* @param {boolean} [options.multiple] The multiple mode
34+
* @param {(files: File[] | null, event: DragEvent) => void} [options.onDrop] The on drop callback function
35+
* @param {(files: File[] | null, event: DragEvent) => void} [options.onEnter] The on enter callback function
36+
* @param {(files: File[] | null, event: DragEvent) => void} [options.onLeave] The on leave callback function
37+
* @param {(files: File[] | null, event: DragEvent) => void} [options.onOver] The on over callback function
38+
* @returns {[StateRef<Target>, boolean, File[] | null]} The object with drop zone states and ref
39+
*
40+
* @example
41+
* const { ref, isOver, files } = useDropZone(options);
42+
*
43+
* @overload
44+
* @param {(files: File[] | null, event: DragEvent) => void} [callback] The callback function to be invoked on drop
45+
* @returns {[StateRef<Target>, boolean, File[] | null]} The object with drop zone states and ref
46+
*
47+
* @example
48+
* const { ref, isOver, files } = useDropZone(() => console.log('callback'));
49+
*/
50+
export const useDropZone = (...params) => {
51+
const target = isTarget(params[0]) ? params[0] : undefined;
52+
const options = target
53+
? typeof params[1] === 'object'
54+
? params[1]
55+
: { onDrop: params[1] }
56+
: typeof params[0] === 'object'
57+
? params[0]
58+
: { onDrop: params[0] };
59+
const internalRef = useRefState();
60+
const [files, setFiles] = useState(null);
61+
const [isOver, setIsOver] = useState(false);
62+
const getFiles = (event) => {
63+
const list = Array.from(event.dataTransfer?.files ?? []);
64+
return list.length === 0 ? null : options.multiple ? list : [list[0]];
65+
};
66+
const checkDataTypes = (types) => {
67+
const dataTypes = options.dataTypes;
68+
if (typeof dataTypes === 'function') return dataTypes(types);
69+
if (!dataTypes?.length) return true;
70+
if (types.length === 0) return false;
71+
return types.every((type) => dataTypes?.some((dataType) => type.includes(dataType)));
72+
};
73+
const checkValidity = (items) => {
74+
const types = Array.from(items ?? []).map((item) => item.type);
75+
const dataTypesValid = checkDataTypes(types);
76+
const multipleFilesValid = options.multiple || items.length <= 1;
77+
return dataTypesValid && multipleFilesValid;
78+
};
79+
const handleDragEvent = (event, eventType) => {
80+
const dataTransferItemList = event.dataTransfer?.items;
81+
const isValid = (dataTransferItemList && checkValidity(dataTransferItemList)) ?? false;
82+
if (!isValid) {
83+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'none';
84+
return;
85+
}
86+
event.preventDefault();
87+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
88+
const currentFiles = getFiles(event);
89+
if (eventType === 'drop') {
90+
setIsOver(false);
91+
setFiles(currentFiles);
92+
options.onDrop?.(currentFiles, event);
93+
return;
94+
}
95+
if (eventType === 'enter') {
96+
setIsOver(true);
97+
options.onEnter?.(null, event);
98+
return;
99+
}
100+
if (eventType === 'leave') {
101+
setIsOver(false);
102+
options.onLeave?.(null, event);
103+
return;
104+
}
105+
if (eventType === 'over') options.onOver?.(null, event);
106+
};
107+
useEffect(() => {
108+
if (!target && !internalRef.state) return;
109+
const element = target ? getElement(target) : internalRef.current;
110+
if (!element) return;
111+
const handleDrop = (event) => handleDragEvent(event, 'drop');
112+
const handleDragOver = (event) => handleDragEvent(event, 'over');
113+
const handleDragEnter = (event) => handleDragEvent(event, 'enter');
114+
const handleDragLeave = (event) => handleDragEvent(event, 'leave');
115+
element.addEventListener('dragenter', handleDragEnter);
116+
element.addEventListener('dragover', handleDragOver);
117+
element.addEventListener('dragleave', handleDragLeave);
118+
element.addEventListener('drop', handleDrop);
119+
return () => {
120+
element.removeEventListener('dragenter', handleDragEnter);
121+
element.removeEventListener('dragover', handleDragOver);
122+
element.removeEventListener('dragleave', handleDragLeave);
123+
element.removeEventListener('drop', handleDrop);
124+
};
125+
}, [target, internalRef.current]);
126+
if (target) return { isOver, files };
127+
return { ref: internalRef, isOver, files };
128+
};

packages/core/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export * from './useDocumentEvent/useDocumentEvent';
2626
export * from './useDocumentTitle/useDocumentTitle';
2727
export * from './useDocumentVisibility/useDocumentVisibility';
2828
export * from './useDoubleClick/useDoubleClick';
29+
export * from './useDropZone/useDropZone';
2930
export * from './useElementSize/useElementSize';
3031
export * from './useEvent/useEvent';
3132
export * from './useEventListener/useEventListener';
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { useState } from 'react';
2+
import { useDropZone } from './useDropZone';
3+
4+
interface FileMeta {
5+
name: string;
6+
size: number;
7+
type: string;
8+
lastModified: number;
9+
}
10+
11+
const Demo = () => {
12+
const [files, setFiles] = useState<FileMeta[]>([]);
13+
14+
const onDrop = (files: File[] | null) => {
15+
setFiles([]);
16+
17+
if (!files) return;
18+
19+
setFiles(
20+
files.map((file) => ({
21+
name: file.name,
22+
size: file.size,
23+
type: file.type,
24+
lastModified: file.lastModified
25+
}))
26+
);
27+
};
28+
29+
const dropZone = useDropZone<HTMLDivElement>(onDrop);
30+
31+
return (
32+
<div>
33+
<p>Drop files from your computer on to drop zones</p>
34+
<div
35+
ref={dropZone.ref}
36+
className='flex flex-col p-5 w-full min-h-[300px] bg-gray-400/10 mt-6 rounded'
37+
>
38+
<div className='m-auto'>
39+
<p className='text-xl font-bold'>Drop Zone</p>
40+
<p>
41+
isOver:{' '}
42+
<span className={dropZone.isOver ? 'text-green-500' : 'text-red-500'}>
43+
{String(dropZone.isOver)}
44+
</span>
45+
</p>
46+
</div>
47+
<div className='flex flex-col gap-3'>
48+
{!!files.length &&
49+
files.map((file, index) => (
50+
<div key={index} className='flex p-5 bg-gray-400/5 flex-col rounded'>
51+
<p>
52+
<span className='font-bold'>File name:</span> {file.name}
53+
</p>
54+
<p>
55+
<span className='font-bold'>Size:</span> {file.size}
56+
</p>
57+
<p>
58+
<span className='font-bold'>Type:</span> {file.type}
59+
</p>
60+
<p>
61+
<span className='font-bold'>Last modified:</span> {file.lastModified}
62+
</p>
63+
</div>
64+
))}
65+
</div>
66+
</div>
67+
</div>
68+
);
69+
};
70+
71+
export default Demo;

0 commit comments

Comments
 (0)