mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
up
This commit is contained in:
@@ -72,12 +72,12 @@ async function onSubmit(event: FormSubmitEvent<schema>) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center gap-8">
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<USelect v-model="size" :items="sizes" />
|
||||
</div>
|
||||
|
||||
<UForm :schema="schema" :state="state" class="space-y-4 w-80 flex flex-col items-center" @submit="onSubmit">
|
||||
<UForm :schema="schema" :state="state" class="space-y-4 w-80" @submit="onSubmit">
|
||||
<UFormField name="avatar" label="Avatar" description="JPG, GIF or PNG. 1MB Max." :size="size">
|
||||
<UFileUpload v-slot="{ open, reset, urls }" v-model="state.avatar" accept="image/*">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
@@ -108,7 +108,10 @@ async function onSubmit(event: FormSubmitEvent<schema>) {
|
||||
label="Drop your image here"
|
||||
description="SVG, PNG, JPG or GIF (max. 2MB)"
|
||||
multiple
|
||||
class="w-full"
|
||||
:size="size"
|
||||
/>
|
||||
|
||||
<UFileUpload multiple />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -53,7 +53,6 @@ export interface FileUploadProps<M extends boolean = false> {
|
||||
* @defaultValue true
|
||||
*/
|
||||
dropzone?: boolean
|
||||
defaultValue?: File[]
|
||||
class?: any
|
||||
ui?: FileUpload['slots']
|
||||
}
|
||||
@@ -66,22 +65,21 @@ export interface FileUploadSlots {
|
||||
default(props: {
|
||||
open: UseFileDialogReturn['open']
|
||||
reset: UseFileDialogReturn['reset']
|
||||
urls: string[]
|
||||
}): any
|
||||
leading(props?: {}): any
|
||||
label(props?: {}): any
|
||||
description(props?: {}): any
|
||||
actions(props?: {}): any
|
||||
files(props: { files: FileList }): any
|
||||
files(props?: {}): any
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="M extends boolean = false">
|
||||
import { ref, computed } from 'vue'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { useFileDialog, useDropZone } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { useFormField } from '../composables/useFormField'
|
||||
import { useFileUpload } from '../composables/useFileUpload'
|
||||
import { tv } from '../utils/tv'
|
||||
import UButton from './Button.vue'
|
||||
|
||||
@@ -89,6 +87,7 @@ defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<FileUploadProps<M>>(), {
|
||||
accept: '*',
|
||||
multiple: false as never,
|
||||
dropzone: true
|
||||
})
|
||||
defineEmits<FileUploadEmits<M>>()
|
||||
@@ -99,51 +98,51 @@ const modelValue = defineModel<(M extends true ? File[] : File) | null>()
|
||||
const appConfig = useAppConfig() as FileUpload['AppConfig']
|
||||
|
||||
const inputRef = ref<HTMLInputElement>()
|
||||
const dropZoneRef = ref<HTMLDivElement>()
|
||||
const dropzoneRef = ref<HTMLDivElement>()
|
||||
|
||||
const { files, open, reset, onChange } = useFileDialog({
|
||||
multiple: props.multiple,
|
||||
const { isDragging, open, reset } = useFileUpload({
|
||||
accept: props.accept,
|
||||
reset: props.reset,
|
||||
input: inputRef.value,
|
||||
initialFiles: props.defaultValue
|
||||
})
|
||||
const { isOverDropZone } = useDropZone(dropZoneRef, {
|
||||
onDrop,
|
||||
// dataTypes: props.accept.split(','),
|
||||
multiple: props.multiple
|
||||
multiple: props.multiple,
|
||||
dropzone: props.dropzone,
|
||||
dropzoneRef,
|
||||
onUpdate
|
||||
})
|
||||
const { emitFormInput, id, name, disabled, ariaAttrs } = useFormField<FileUploadProps>(props, { deferInputValidation: true })
|
||||
|
||||
const urls = computed(() => Array.from(files.value || []).map(file => URL.createObjectURL(file)))
|
||||
|
||||
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.fileUpload || {}) })({
|
||||
dropzone: props.dropzone,
|
||||
multiple: props.multiple,
|
||||
color: props.color,
|
||||
size: props.size,
|
||||
highlight: props.highlight
|
||||
}))
|
||||
|
||||
onChange((files) => {
|
||||
modelValue.value = (props.multiple ? files : files?.[0]) as (M extends true ? File[] : File) | null
|
||||
function createObjectUrl(file: File): string {
|
||||
return URL.createObjectURL(file)
|
||||
}
|
||||
|
||||
function onUpdate(files: File[]) {
|
||||
if (props.multiple) {
|
||||
const existingFiles = (modelValue.value as File[]) || []
|
||||
modelValue.value = [...existingFiles, ...(files || [])] as (M extends true ? File[] : File) | null
|
||||
} else {
|
||||
modelValue.value = files?.[0] as (M extends true ? File[] : File) | null
|
||||
}
|
||||
|
||||
emitFormInput()
|
||||
})
|
||||
|
||||
function onDrop(files: File[] | null) {
|
||||
modelValue.value = (props.multiple ? files : files?.[0]) as (M extends true ? File[] : File) | null
|
||||
}
|
||||
|
||||
function removeFile(index: number) {
|
||||
const file = files.value?.[index]
|
||||
|
||||
if (file) {
|
||||
URL.revokeObjectURL(URL.createObjectURL(file))
|
||||
if (!props.multiple || !modelValue.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const fileListArr = Array.from(files.value!)
|
||||
fileListArr.splice(index, 1)
|
||||
const fileList = Array.from(modelValue.value as File[])
|
||||
|
||||
fileList.splice(index, 1)
|
||||
|
||||
modelValue.value = fileList as (M extends true ? File[] : File) | null
|
||||
|
||||
emitFormInput()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -153,11 +152,11 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<slot :open="open" :reset="reset" :urls="urls">
|
||||
<slot :open="open" :reset="reset">
|
||||
<div
|
||||
ref="dropZoneRef"
|
||||
ref="dropzoneRef"
|
||||
role="button"
|
||||
:data-dragging="isOverDropZone"
|
||||
:data-dragging="isDragging"
|
||||
:class="ui.base({ class: props.ui?.base })"
|
||||
tabindex="0"
|
||||
@click="open()"
|
||||
@@ -188,12 +187,19 @@ defineExpose({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="files && files.length > 0" :class="ui.files({ class: props.ui?.files })">
|
||||
<slot name="files" :files="files">
|
||||
<div v-for="(file, index) in files" :key="file.name" class="flex items-center gap-2 border border-default rounded-md p-2">
|
||||
<UAvatar :src="urls[index]" :icon="appConfig.ui.icons.file" />
|
||||
<span class="text-sm">{{ file.name }}</span>
|
||||
<UButton size="xs" color="neutral" variant="link" :trailing-icon="appConfig.ui.icons.close" @click="removeFile(index)" />
|
||||
<div v-if="modelValue && (modelValue as File[]).length > 0" :class="ui.files({ class: props.ui?.files })">
|
||||
<slot name="files">
|
||||
<div v-for="(file, index) in Array.isArray(modelValue) ? modelValue : [modelValue]" :key="file.name" class="min-w-0 flex items-center gap-2 border border-default rounded-md p-2">
|
||||
<UAvatar :src="createObjectUrl(file)" :icon="appConfig.ui.icons.file" />
|
||||
<span class="text-sm truncate">{{ file.name }}</span>
|
||||
<UButton
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
:trailing-icon="appConfig.ui.icons.close"
|
||||
class="ms-auto"
|
||||
@click="removeFile(index)"
|
||||
/>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
@@ -205,7 +211,7 @@ defineExpose({
|
||||
type="file"
|
||||
:name="name"
|
||||
:accept="accept"
|
||||
:multiple="multiple"
|
||||
:multiple="(multiple as boolean)"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
v-bind="{ ...$attrs, ...ariaAttrs }"
|
||||
|
||||
69
src/runtime/composables/useFileUpload.ts
Normal file
69
src/runtime/composables/useFileUpload.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { computed, unref } from 'vue'
|
||||
import { useFileDialog, useDropZone } from '@vueuse/core'
|
||||
import type { MaybeRef, MaybeRefOrGetter } from '@vueuse/core'
|
||||
|
||||
export interface UseFileUploadOptions {
|
||||
/**
|
||||
* Specifies the allowed file types. Provide a comma-separated list of MIME types or file extensions.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept
|
||||
* @defaultValue '*'
|
||||
*/
|
||||
accept?: MaybeRef<string>
|
||||
dropzone?: boolean
|
||||
dropzoneRef: MaybeRefOrGetter<HTMLElement | null | undefined>
|
||||
multiple?: boolean
|
||||
onUpdate: (files: File[]) => void
|
||||
}
|
||||
|
||||
function parseAcceptToDataTypes(accept: string): string[] | undefined {
|
||||
if (!accept || accept === '*') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const types = accept
|
||||
.split(',')
|
||||
.map(type => type.trim())
|
||||
.filter((type) => {
|
||||
return !type.startsWith('.')
|
||||
})
|
||||
|
||||
return types.length > 0 ? types : undefined
|
||||
}
|
||||
|
||||
export function useFileUpload(options: UseFileUploadOptions) {
|
||||
const { dropzone = true, dropzoneRef, multiple = false, accept = '*', onUpdate } = options
|
||||
|
||||
const dataTypes = computed(() => {
|
||||
const acceptValue = unref(accept)
|
||||
return parseAcceptToDataTypes(acceptValue)
|
||||
})
|
||||
|
||||
const onDrop = (files: FileList | File[] | null) => {
|
||||
if (!files || files.length === 0) {
|
||||
return
|
||||
}
|
||||
if (files instanceof FileList) {
|
||||
files = Array.from(files)
|
||||
}
|
||||
if (files.length > 1 && !multiple) {
|
||||
files = [files[0]!]
|
||||
}
|
||||
onUpdate(files)
|
||||
}
|
||||
|
||||
const { isOverDropZone: isDragging } = dropzone
|
||||
? useDropZone(dropzoneRef, { dataTypes: dataTypes.value, onDrop })
|
||||
: { isOverDropZone: false }
|
||||
const { onChange, open, reset } = useFileDialog({
|
||||
accept: unref(accept),
|
||||
multiple
|
||||
})
|
||||
|
||||
onChange(fileList => onDrop(fileList))
|
||||
|
||||
return {
|
||||
isDragging,
|
||||
open,
|
||||
reset
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user