Skip to content

Commit c0da930

Browse files
committed
main 🧊 rework use drop zone
1 parent cd094fd commit c0da930

File tree

3 files changed

+240
-198
lines changed

3 files changed

+240
-198
lines changed
Lines changed: 74 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react';
1+
import { useEffect, useRef, useState } from 'react';
22
import { getElement, isTarget } from '@/utils/helpers';
33
import { useRefState } from '../useRefState/useRefState';
44
/**
@@ -12,40 +12,40 @@ import { useRefState } from '../useRefState/useRefState';
1212
* @param {DataTypes} [options.dataTypes] The data types
1313
* @param {boolean} [options.multiple] The multiple mode
1414
* @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
15+
* @param {(event: DragEvent) => void} [options.onEnter] The on enter callback function
16+
* @param {(event: DragEvent) => void} [options.onLeave] The on leave callback function
17+
* @param {(event: DragEvent) => void} [options.onOver] The on over callback function
1818
* @returns {[boolean, File[] | null]} The object with drop zone states
1919
*
2020
* @example
21-
* const {isOver, files} = useDropZone(ref, options);
21+
* const { overed, files } = useDropZone(ref, options);
2222
*
2323
* @overload
2424
* @param {Target} target The target element drop zone's
2525
* @param {(files: File[] | null, event: DragEvent) => void} [callback] The callback function to be invoked on drop
2626
* @returns {[boolean, File[] | null]} The object with drop zone states
2727
*
2828
* @example
29-
* const {isOver, files} = useDropZone(ref, () => console.log('callback'));
29+
* const { overed, files } = useDropZone(ref, () => console.log('callback'));
3030
*
3131
* @overload
3232
* @param {DataTypes} [options.dataTypes] The data types
3333
* @param {boolean} [options.multiple] The multiple mode
3434
* @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
35+
* @param {(event: DragEvent) => void} [options.onEnter] The on enter callback function
36+
* @param {(event: DragEvent) => void} [options.onLeave] The on leave callback function
37+
* @param {(event: DragEvent) => void} [options.onOver] The on over callback function
3838
* @returns {[StateRef<Target>, boolean, File[] | null]} The object with drop zone states and ref
3939
*
4040
* @example
41-
* const { ref, isOver, files } = useDropZone(options);
41+
* const { ref, overed, files } = useDropZone(options);
4242
*
4343
* @overload
4444
* @param {(files: File[] | null, event: DragEvent) => void} [callback] The callback function to be invoked on drop
4545
* @returns {[StateRef<Target>, boolean, File[] | null]} The object with drop zone states and ref
4646
*
4747
* @example
48-
* const { ref, isOver, files } = useDropZone(() => console.log('callback'));
48+
* const { ref, overed, files } = useDropZone(() => console.log('callback'));
4949
*/
5050
export const useDropZone = (...params) => {
5151
const target = isTarget(params[0]) ? params[0] : undefined;
@@ -57,72 +57,85 @@ export const useDropZone = (...params) => {
5757
? params[0]
5858
: { onDrop: params[0] };
5959
const internalRef = useRefState();
60+
const counterRef = useRef(0);
6061
const [files, setFiles] = useState(null);
61-
const [isOver, setIsOver] = useState(false);
62+
const [overed, setOvered] = useState(false);
63+
const dataTypes = options.dataTypes;
6264
const getFiles = (event) => {
63-
const list = Array.from(event.dataTransfer?.files ?? []);
64-
return list.length === 0 ? null : options.multiple ? list : [list[0]];
65+
if (!event.dataTransfer) return null;
66+
const list = Array.from(event.dataTransfer.files);
67+
if (options.multiple) return list;
68+
if (!list.length) return null;
69+
return [list[0]];
6570
};
6671
const checkDataTypes = (types) => {
67-
const dataTypes = options.dataTypes;
72+
if (!dataTypes) return true;
6873
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)));
74+
if (!dataTypes.length) return true;
75+
if (!types.length) return false;
76+
return types.every((type) => {
77+
console.log('type', type);
78+
console.log('dataTypes', dataTypes);
79+
return dataTypes.some((dataType) => type.includes(dataType));
80+
});
7281
};
7382
const checkValidity = (items) => {
74-
const types = Array.from(items ?? []).map((item) => item.type);
83+
const types = Array.from(items).map((item) => item.type);
7584
const dataTypesValid = checkDataTypes(types);
7685
const multipleFilesValid = options.multiple || items.length <= 1;
7786
return dataTypesValid && multipleFilesValid;
7887
};
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-
};
10788
useEffect(() => {
10889
if (!target && !internalRef.state) return;
10990
const element = target ? getElement(target) : internalRef.current;
11091
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);
92+
const onEvent = (event, type) => {
93+
if (!event.dataTransfer) return;
94+
const isValid = checkValidity(event.dataTransfer.items);
95+
if (!isValid) {
96+
event.dataTransfer.dropEffect = 'none';
97+
return;
98+
}
99+
event.preventDefault();
100+
event.dataTransfer.dropEffect = 'copy';
101+
const currentFiles = getFiles(event);
102+
if (type === 'drop') {
103+
counterRef.current = 0;
104+
setOvered(false);
105+
setFiles(currentFiles);
106+
options.onDrop?.(currentFiles, event);
107+
return;
108+
}
109+
if (type === 'enter') {
110+
counterRef.current += 1;
111+
setOvered(true);
112+
options.onEnter?.(event);
113+
return;
114+
}
115+
if (type === 'leave') {
116+
counterRef.current -= 1;
117+
if (counterRef.current !== 0) return;
118+
setOvered(false);
119+
options.onLeave?.(event);
120+
return;
121+
}
122+
if (type === 'over') options.onOver?.(event);
123+
};
124+
const onDrop = (event) => onEvent(event, 'drop');
125+
const onDragOver = (event) => onEvent(event, 'over');
126+
const onDragEnter = (event) => onEvent(event, 'enter');
127+
const onDragLeave = (event) => onEvent(event, 'leave');
128+
element.addEventListener('dragenter', onDragEnter);
129+
element.addEventListener('dragover', onDragOver);
130+
element.addEventListener('dragleave', onDragLeave);
131+
element.addEventListener('drop', onDrop);
119132
return () => {
120-
element.removeEventListener('dragenter', handleDragEnter);
121-
element.removeEventListener('dragover', handleDragOver);
122-
element.removeEventListener('dragleave', handleDragLeave);
123-
element.removeEventListener('drop', handleDrop);
133+
element.removeEventListener('dragenter', onDragEnter);
134+
element.removeEventListener('dragover', onDragOver);
135+
element.removeEventListener('dragleave', onDragLeave);
136+
element.removeEventListener('drop', onDrop);
124137
};
125138
}, [target, internalRef.current]);
126-
if (target) return { isOver, files };
127-
return { ref: internalRef, isOver, files };
139+
if (target) return { overed, files };
140+
return { ref: internalRef, overed, files };
128141
};

‎packages/core/src/hooks/useDropZone/useDropZone.demo.tsx

Lines changed: 76 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,98 @@
11
import { useState } from 'react';
2+
23
import { useDropZone } from './useDropZone';
34

45
interface FileMeta {
6+
lastModified: number;
57
name: string;
68
size: number;
79
type: string;
8-
lastModified: number;
910
}
1011

1112
const Demo = () => {
1213
const [files, setFiles] = useState<FileMeta[]>([]);
14+
const [preview, setPreview] = useState<string | null>(null);
1315

14-
const onDrop = (files: File[] | null) => {
15-
setFiles([]);
16-
16+
const callback = (files: File[] | null) => {
1717
if (!files) return;
18+
const file = files[0];
1819

19-
setFiles(
20-
files.map((file) => ({
21-
name: file.name,
22-
size: file.size,
23-
type: file.type,
24-
lastModified: file.lastModified
25-
}))
26-
);
20+
const reader = new FileReader();
21+
reader.onload = (event) => {
22+
const imagePreview = event.target?.result as string;
23+
setPreview(imagePreview);
24+
setFiles([
25+
{
26+
name: file.name,
27+
size: file.size,
28+
type: file.type,
29+
lastModified: file.lastModified
30+
}
31+
]);
32+
};
33+
reader.readAsDataURL(file);
2734
};
2835

29-
const dropZone = useDropZone<HTMLDivElement>(onDrop);
36+
const onRemove = () => {
37+
setPreview(null);
38+
setFiles([]);
39+
};
40+
41+
const dropZone = useDropZone<HTMLDivElement>({
42+
dataTypes: ['image'],
43+
onDrop: callback
44+
});
3045

3146
return (
3247
<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>
48+
<p>Drop images from your computer on to drop zones</p>
49+
<div ref={dropZone.ref} className='mt-6 flex min-h-[300px] w-full flex-col rounded p-5'>
50+
{!preview && (
51+
<div className='m-auto'>
52+
<p>
53+
<span className={dropZone.overed ? 'text-green-300' : 'text-red-300'}>
54+
{dropZone.overed ? 'Drop zone is over' : 'Drop zone is not over'}
55+
</span>
56+
</p>
57+
</div>
58+
)}
59+
60+
{!!preview && (
61+
<div className='flex flex-col gap-3'>
62+
<div className='relative'>
63+
<button
64+
className='absolute right-2 top-1 flex size-8 items-center justify-center'
65+
onClick={onRemove}
66+
>
67+
✕
68+
</button>
69+
<img
70+
alt='Preview'
71+
className='mx-auto max-h-[400px] max-w-full rounded-lg'
72+
src={preview}
73+
/>
74+
</div>
75+
76+
<div className='flex flex-col gap-2'>
77+
{files.map((file, index) => (
78+
<div key={index} className='flex flex-col rounded-lg bg-gray-400/5 p-3'>
79+
<p>
80+
<span className='font-bold'>File name:</span> {file.name}
81+
</p>
82+
<p>
83+
<span className='font-bold'>Size:</span> {file.size}
84+
</p>
85+
<p>
86+
<span className='font-bold'>Type:</span> {file.type}
87+
</p>
88+
<p>
89+
<span className='font-bold'>Last modified:</span> {file.lastModified}
90+
</p>
91+
</div>
92+
))}
93+
</div>
94+
</div>
95+
)}
6696
</div>
6797
</div>
6898
);

0 commit comments

Comments
 (0)