Skip to content

Commit 3b94de4

Browse files
feat(useDropZone): add multiple prop to control multi-file drop (#4227)
Co-authored-by: Dan T. Ngossinga <[email protected]>
1 parent 991793a commit 3b94de4

File tree

2 files changed

+86
-41
lines changed

2 files changed

+86
-41
lines changed

packages/core/useDropZone/index.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ function onDrop(files: File[] | null) {
2121
const { isOverDropZone } = useDropZone(dropZoneRef, {
2222
onDrop,
2323
// specify the types of data to be received.
24-
dataTypes: ['image/jpeg']
24+
dataTypes: ['image/jpeg'],
25+
// control multi-file drop
26+
multiple: true,
27+
// whether to prevent default behavior for unhandled events
28+
preventDefaultForUnhandled: false,
2529
})
2630
</script>
2731

packages/core/useDropZone/index.ts

Lines changed: 81 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Ref } from 'vue-demi'
33
// eslint-disable-next-line no-restricted-imports
44
import { ref, shallowRef, unref } from 'vue-demi'
55
import type { MaybeRef, MaybeRefOrGetter } from '@vueuse/shared'
6-
import { isClient, notNullish } from '@vueuse/shared'
6+
import { isClient } from '@vueuse/shared'
77

88
import { useEventListener } from '../useEventListener'
99

@@ -22,6 +22,14 @@ export interface UseDropZoneOptions {
2222
onEnter?: (files: File[] | null, event: DragEvent) => void
2323
onLeave?: (files: File[] | null, event: DragEvent) => void
2424
onOver?: (files: File[] | null, event: DragEvent) => void
25+
/**
26+
* Allow multiple files to be dropped. Defaults to true.
27+
*/
28+
multiple?: boolean
29+
/**
30+
* Prevent default behavior for unhandled events. Defaults to false.
31+
*/
32+
preventDefaultForUnhandled?: boolean
2533
}
2634

2735
export function useDropZone(
@@ -31,59 +39,92 @@ export function useDropZone(
3139
const isOverDropZone = ref(false)
3240
const files = shallowRef<File[] | null>(null)
3341
let counter = 0
34-
let isDataTypeIncluded = true
42+
let isValid = true
43+
3544
if (isClient) {
3645
const _options = typeof options === 'function' ? { onDrop: options } : options
46+
const multiple = _options.multiple ?? true
47+
const preventDefaultForUnhandled = _options.preventDefaultForUnhandled ?? false
48+
3749
const getFiles = (event: DragEvent) => {
3850
const list = Array.from(event.dataTransfer?.files ?? [])
39-
return (files.value = list.length === 0 ? null : list)
51+
return list.length === 0 ? null : (multiple ? list : [list[0]])
4052
}
4153

42-
useEventListener<DragEvent>(target, 'dragenter', (event) => {
43-
const types = Array.from(event?.dataTransfer?.items || [])
44-
.map(i => i.kind === 'file' ? i.type : null)
45-
.filter(notNullish)
46-
47-
if (_options.dataTypes && event.dataTransfer) {
54+
const checkDataTypes = (types: string[]) => {
55+
if (_options.dataTypes) {
4856
const dataTypes = unref(_options.dataTypes)
49-
isDataTypeIncluded = typeof dataTypes === 'function'
57+
return typeof dataTypes === 'function'
5058
? dataTypes(types)
5159
: dataTypes
5260
? dataTypes.some(item => types.includes(item))
5361
: true
54-
if (!isDataTypeIncluded)
55-
return
5662
}
57-
event.preventDefault()
58-
counter += 1
59-
isOverDropZone.value = true
60-
const files = getFiles(event)
61-
_options.onEnter?.(files, event)
62-
})
63-
useEventListener<DragEvent>(target, 'dragover', (event) => {
64-
if (!isDataTypeIncluded)
65-
return
66-
event.preventDefault()
67-
const files = getFiles(event)
68-
_options.onOver?.(files, event)
69-
})
70-
useEventListener<DragEvent>(target, 'dragleave', (event) => {
71-
if (!isDataTypeIncluded)
63+
return true
64+
}
65+
66+
const checkValidity = (event: DragEvent) => {
67+
const items = Array.from(event.dataTransfer?.items ?? [])
68+
const types = items
69+
.filter(item => item.kind === 'file')
70+
.map(item => item.type)
71+
72+
const dataTypesValid = checkDataTypes(types)
73+
const multipleFilesValid = multiple || items.filter(item => item.kind === 'file').length <= 1
74+
75+
return dataTypesValid && multipleFilesValid
76+
}
77+
78+
const handleDragEvent = (event: DragEvent, eventType: 'enter' | 'over' | 'leave' | 'drop') => {
79+
isValid = checkValidity(event)
80+
81+
if (!isValid) {
82+
if (preventDefaultForUnhandled) {
83+
event.preventDefault()
84+
}
85+
if (event.dataTransfer) {
86+
event.dataTransfer.dropEffect = 'none'
87+
}
7288
return
89+
}
90+
7391
event.preventDefault()
74-
counter -= 1
75-
if (counter === 0)
76-
isOverDropZone.value = false
77-
const files = getFiles(event)
78-
_options.onLeave?.(files, event)
79-
})
80-
useEventListener<DragEvent>(target, 'drop', (event) => {
81-
event.preventDefault()
82-
counter = 0
83-
isOverDropZone.value = false
84-
const files = getFiles(event)
85-
_options.onDrop?.(files, event)
86-
})
92+
if (event.dataTransfer) {
93+
event.dataTransfer.dropEffect = 'copy'
94+
}
95+
96+
const currentFiles = getFiles(event)
97+
98+
switch (eventType) {
99+
case 'enter':
100+
counter += 1
101+
isOverDropZone.value = true
102+
_options.onEnter?.(null, event)
103+
break
104+
case 'over':
105+
_options.onOver?.(null, event)
106+
break
107+
case 'leave':
108+
counter -= 1
109+
if (counter === 0)
110+
isOverDropZone.value = false
111+
_options.onLeave?.(null, event)
112+
break
113+
case 'drop':
114+
counter = 0
115+
isOverDropZone.value = false
116+
if (isValid) {
117+
files.value = currentFiles
118+
_options.onDrop?.(currentFiles, event)
119+
}
120+
break
121+
}
122+
}
123+
124+
useEventListener<DragEvent>(target, 'dragenter', event => handleDragEvent(event, 'enter'))
125+
useEventListener<DragEvent>(target, 'dragover', event => handleDragEvent(event, 'over'))
126+
useEventListener<DragEvent>(target, 'dragleave', event => handleDragEvent(event, 'leave'))
127+
useEventListener<DragEvent>(target, 'drop', event => handleDragEvent(event, 'drop'))
87128
}
88129

89130
return {

0 commit comments

Comments
 (0)