useDropZone() composable 能把一个元素(或组件)变成从桌面拖入文件的投放目标:当拖拽悬停在该区域上时,它会通过响应式的 isOverDropZone 布尔值告诉你;当文件被放下时,它会通过响应式数组 acceptedDropZoneFiles 以及 onDrop 钩子把 File 对象交给你。
被放下的文件会经过与 QFile 和 QUploader 完全相同的校验(accept、maxFileSize、maxTotalSize、maxFiles 和 filter),未通过校验的文件也会以与这两个组件触发 @rejected 相同的方式被上报。
当你希望用自己的卡片、面板或整个页面来接收拖入的文件,而 QFile 的表单字段外观或 QUploader 的上传队列反而碍手碍脚时,就可以用它。如果你只需要拿到文件并给元素加上一个悬停样式类,那么 v-drop-zone 指令在模板里就能搞定。可以再搭配区域内某个按钮上的 useFilePicker composable,让键盘和触屏用户也能提供文件。
在 SSR 或 SSG 模式的服务端,什么都无法被放下:在客户端接管之前,isOverDropZone 始终保持为 false,acceptedDropZoneFiles 也始终为空。
该 composable 也可以在 setup() 之外调用:在 boot 文件、store 或普通模块中。此时没有可回退的组件根节点,也没有需要等待的挂载时机,所以你必须提供一个 target(一个元素,或指向元素的 ref / getter);此时该区域会立即开始监听,且不会自行释放:完成后请调用 stopDropZone()(或把响应式的 target 指向 null)。
语法
import { useTemplateRef } from 'vue'
import { useDropZone } from 'quasar'
setup () {
const target = useTemplateRef('target') // an Element or a component
const {
isOverDropZone,
acceptedDropZoneFiles,
rejectedDropZoneFiles,
resetDropZone,
stopDropZone
} = useDropZone({
// all optional:
target, // omit it to use the component's own root element
disabled: false, // stop accepting drops
multiple: false, // accept more than one file per drop
accept: 'image/*,.pdf', // same format as the native "accept" attribute
maxFileSize: 1048576, // bytes
maxTotalSize: 10485760, // bytes
maxFiles: 5,
filter (files) { // keep only the files you return
return files.filter(file => file.name.endsWith('.jpg'))
},
onDrop (files, evt) {}, // the accepted files of a drop, plus the Event
onRejected (rejected) {}, // [{ failedPropValidation, file }, ...]
onEnter (evt) {}, // a drag entered the zone
onLeave (evt) {} // it left the zone or got dropped
})
// ...
}function useDropZone(
options?: MaybeRefOrGetter<{
target?: MaybeRefOrGetter<
Element | ComponentPublicInstance | null | undefined
>
disabled?: boolean
multiple?: boolean
accept?: string
maxFileSize?: string | number
maxTotalSize?: string | number
maxFiles?: string | number
filter?: (files: readonly File[]) => readonly File[]
onDrop?: (files: File[], evt: DragEvent) => void
onRejected?: (rejected: QRejectedEntry[]) => void
onEnter?: (evt: DragEvent) => void
onLeave?: (evt: DragEvent) => void
}>
): {
isOverDropZone: Ref<boolean>
acceptedDropZoneFiles: ShallowRef<File[]>
rejectedDropZoneFiles: ShallowRef<QRejectedEntry[]>
resetDropZone: () => void
stopDropZone: () => void
}
// the same type as the entries of the QFile/QUploader "rejected" event
interface QRejectedEntry {
failedPropValidation:
| 'accept'
| 'max-file-size'
| 'max-total-size'
| 'filter'
| 'max-files'
| 'duplicate'
file: File
}不提供 target 时,投放区域就是调用该 composable 的那个组件的根元素,以组件被挂载的那一刻为准。渲染为片段(fragment,即多个根节点)的组件没有可供监听的根元素,因此这种情况下必须提供一个 target。
当有东西被拖拽悬停在该区域上(包括其子元素)时,isOverDropZone 会变为 true;当拖拽离开该区域或被放下时,它会回到 false;在这两次状态转换时会分别以拖拽事件为参数调用 onEnter 和 onLeave。你可以用它来高亮该区域。如果在拖拽进行到一半时释放了区域(通过 disabled、切换 target 或调用 stopDropZone()),isOverDropZone 会回到 false,但不会调用 onLeave,因为此时没有可上报的拖拽事件。嵌套在另一个区域内部的区域会独占这些拖拽事件,因此外层区域感知不到发生在内层区域上的投放(并且会保持高亮,直到下一次拖拽离开它为止)。
不设置 multiple 时,只有第一个被放下的文件会被保留(其余文件不会作为被拒绝项上报),这与 QFile 的行为一致。被放下的文件夹不会被展开:它们会表现为没有类型的文件,从而被 accept 过滤掉。
acceptedDropZoneFiles 保存最近一次投放中通过校验的文件。如果某次投放的所有文件都被拒绝,acceptedDropZoneFiles 会保持不变;而 rejectedDropZoneFiles 始终反映最近一次投放的结果。resetDropZone() 会同时清空这两者。
每一次投放都会调用 onDrop,参数为通过校验的文件(如果没有任何文件通过,或者拖拽根本没有携带文件,则为空数组)以及投放事件,因此拖拽的其他载荷(例如 evt.dataTransfer.getData('text/plain'))依然触手可及。只有当至少有一个文件被拒绝时才会调用 onRejected。
被拒绝项的 failedPropValidation 是 accept、max-file-size、max-total-size、max-files 或 filter 之一,指明该文件未能通过的那个选项(duplicate 也属于同一个 QRejectedEntry 类型,但只有 QFile 和 QUploader 在向列表追加文件时才会上报它)。
stopDropZone() 会释放该区域:元素不再接受投放(浏览器的默认处理行为重新生效),isOverDropZone 回到 false。该 composable 仍会持续遵循选项,因此把 target 指向另一个元素会重新激活它,而更改其他任何选项则不会。当你没有指定 target 时它尤其有用,因为组件自身的根元素无法被换掉(如果使用响应式的 target,把它设为 null 同样能释放该区域)。你无需在组件销毁时调用它,因为该 composable 会自行释放区域。
文件被放到页面上任何其他位置时,会让浏览器导航到该文件(或下载它),从而离开你的应用。是否要防范这种情况由你决定:在 window 上监听 dragover 和 drop,并在这两个事件里都调用 preventDefault(),即可在任何地方取消这种导航。
运行时更改选项
选项可以是一个普通对象、一个 Ref 或一个 getter 函数。校验相关的选项和各个钩子会在每次投放时读取。使用 Ref 或 getter 时,该 composable 还会追踪 target 和 disabled 所读取的任何响应式状态,并在该状态变化时重新应用它们,因此你无需调用任何东西去「更新」它:
- 切换
disabled会释放该区域(isOverDropZone回到false,浏览器对投放的默认处理重新生效),随后再重新激活 - 把
target指向另一个元素(或让模板 ref 通过v-if发生变化)会跟随它
import { ref } from 'vue'
import { useDropZone } from 'quasar'
setup () {
const uploading = ref(false)
const allowVideos = ref(false)
const { isOverDropZone, acceptedDropZoneFiles } = useDropZone(() => ({
multiple: true,
disabled: uploading.value,
accept: allowVideos.value ? 'image/*,video/*' : 'image/*'
}))
// ...
}示例
下面这个虚线框会接受拖入的图片,并在拖拽悬停其上时高亮。框内的按钮通过 useFilePicker 打开文件对话框,并使用相同的校验选项,两者共同填充同一个列表;被拒绝的文件会通过通知(notification)上报:
<template>
<div class="q-pa-md q-gutter-md">
<div
ref="zone"
class="drop-zone column flex-center q-pa-lg rounded-borders text-center"
:class="isOverDropZone ? 'drop-zone--over' : ''"
>
<q-icon name="cloud_upload" size="48px" />
<div class="q-mt-sm">Drop images here (up to 5MB each)</div>
<q-btn
push
color="primary"
label="Or pick them"
class="q-mt-sm"
@click="openFilePicker"
/>
</div>
<q-list
v-if="files.length !== 0"
bordered
separator
class="rounded-borders"
>
<q-item v-for="(file, index) in files" :key="index">
<q-item-section>{{ file.name }}</q-item-section>
<q-item-section side>{{
format.humanStorageSize(file.size)
}}</q-item-section>
</q-item>
</q-list>
</div>
</template>
<script setup>
import { shallowRef, useTemplateRef } from 'vue'
import { format, useDropZone, useFilePicker, useQuasar } from 'quasar'
const $q = useQuasar()
const zone = useTemplateRef('zone')
const files = shallowRef([])
const options = {
multiple: true,
accept: 'image/*',
maxFileSize: 5 * 1024 * 1024,
onRejected(rejected) {
$q.notify({
type: 'negative',
message: `${rejected.length} file(s) did not pass the validation`
})
}
}
function addFiles(accepted) {
files.value = [...files.value, ...accepted]
}
const { isOverDropZone } = useDropZone({
...options,
target: zone,
onDrop: addFiles
})
const { openFilePicker } = useFilePicker({
...options,
onChange: addFiles
})
</script>
<style lang="sass" scoped>
.drop-zone
border: 2px dashed $grey-5
transition: background-color .2s, border-color .2s
&--over
border-color: $primary
background-color: rgba($primary, .08)
</style>要预览拖入的图片(或者把任意拖入的文件交给 <img>、<video> 或下载链接),可以搭配 useObjectUrl composable,它会为 File 创建对象 URL,并在不再需要时将其释放。