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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<USelect v-model="size" :items="sizes" />
|
<USelect v-model="size" :items="sizes" />
|
||||||
</div>
|
</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">
|
<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/*">
|
<UFileUpload v-slot="{ open, reset, urls }" v-model="state.avatar" accept="image/*">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
@@ -108,7 +108,10 @@ async function onSubmit(event: FormSubmitEvent<schema>) {
|
|||||||
label="Drop your image here"
|
label="Drop your image here"
|
||||||
description="SVG, PNG, JPG or GIF (max. 2MB)"
|
description="SVG, PNG, JPG or GIF (max. 2MB)"
|
||||||
multiple
|
multiple
|
||||||
|
class="w-full"
|
||||||
:size="size"
|
:size="size"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UFileUpload multiple />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ export interface FileUploadProps<M extends boolean = false> {
|
|||||||
* @defaultValue true
|
* @defaultValue true
|
||||||
*/
|
*/
|
||||||
dropzone?: boolean
|
dropzone?: boolean
|
||||||
defaultValue?: File[]
|
|
||||||
class?: any
|
class?: any
|
||||||
ui?: FileUpload['slots']
|
ui?: FileUpload['slots']
|
||||||
}
|
}
|
||||||
@@ -66,22 +65,21 @@ export interface FileUploadSlots {
|
|||||||
default(props: {
|
default(props: {
|
||||||
open: UseFileDialogReturn['open']
|
open: UseFileDialogReturn['open']
|
||||||
reset: UseFileDialogReturn['reset']
|
reset: UseFileDialogReturn['reset']
|
||||||
urls: string[]
|
|
||||||
}): any
|
}): any
|
||||||
leading(props?: {}): any
|
leading(props?: {}): any
|
||||||
label(props?: {}): any
|
label(props?: {}): any
|
||||||
description(props?: {}): any
|
description(props?: {}): any
|
||||||
actions(props?: {}): any
|
actions(props?: {}): any
|
||||||
files(props: { files: FileList }): any
|
files(props?: {}): any
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="M extends boolean = false">
|
<script setup lang="ts" generic="M extends boolean = false">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { Primitive } from 'reka-ui'
|
import { Primitive } from 'reka-ui'
|
||||||
import { useFileDialog, useDropZone } from '@vueuse/core'
|
|
||||||
import { useAppConfig } from '#imports'
|
import { useAppConfig } from '#imports'
|
||||||
import { useFormField } from '../composables/useFormField'
|
import { useFormField } from '../composables/useFormField'
|
||||||
|
import { useFileUpload } from '../composables/useFileUpload'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
import UButton from './Button.vue'
|
import UButton from './Button.vue'
|
||||||
|
|
||||||
@@ -89,6 +87,7 @@ defineOptions({ inheritAttrs: false })
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<FileUploadProps<M>>(), {
|
const props = withDefaults(defineProps<FileUploadProps<M>>(), {
|
||||||
accept: '*',
|
accept: '*',
|
||||||
|
multiple: false as never,
|
||||||
dropzone: true
|
dropzone: true
|
||||||
})
|
})
|
||||||
defineEmits<FileUploadEmits<M>>()
|
defineEmits<FileUploadEmits<M>>()
|
||||||
@@ -99,51 +98,51 @@ const modelValue = defineModel<(M extends true ? File[] : File) | null>()
|
|||||||
const appConfig = useAppConfig() as FileUpload['AppConfig']
|
const appConfig = useAppConfig() as FileUpload['AppConfig']
|
||||||
|
|
||||||
const inputRef = ref<HTMLInputElement>()
|
const inputRef = ref<HTMLInputElement>()
|
||||||
const dropZoneRef = ref<HTMLDivElement>()
|
const dropzoneRef = ref<HTMLDivElement>()
|
||||||
|
|
||||||
const { files, open, reset, onChange } = useFileDialog({
|
const { isDragging, open, reset } = useFileUpload({
|
||||||
multiple: props.multiple,
|
|
||||||
accept: props.accept,
|
accept: props.accept,
|
||||||
reset: props.reset,
|
multiple: props.multiple,
|
||||||
input: inputRef.value,
|
dropzone: props.dropzone,
|
||||||
initialFiles: props.defaultValue
|
dropzoneRef,
|
||||||
})
|
onUpdate
|
||||||
const { isOverDropZone } = useDropZone(dropZoneRef, {
|
|
||||||
onDrop,
|
|
||||||
// dataTypes: props.accept.split(','),
|
|
||||||
multiple: props.multiple
|
|
||||||
})
|
})
|
||||||
const { emitFormInput, id, name, disabled, ariaAttrs } = useFormField<FileUploadProps>(props, { deferInputValidation: true })
|
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 || {}) })({
|
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.fileUpload || {}) })({
|
||||||
dropzone: props.dropzone,
|
dropzone: props.dropzone,
|
||||||
multiple: props.multiple,
|
|
||||||
color: props.color,
|
color: props.color,
|
||||||
size: props.size,
|
size: props.size,
|
||||||
highlight: props.highlight
|
highlight: props.highlight
|
||||||
}))
|
}))
|
||||||
|
|
||||||
onChange((files) => {
|
function createObjectUrl(file: File): string {
|
||||||
modelValue.value = (props.multiple ? files : files?.[0]) as (M extends true ? File[] : File) | null
|
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()
|
emitFormInput()
|
||||||
})
|
|
||||||
|
|
||||||
function onDrop(files: File[] | null) {
|
|
||||||
modelValue.value = (props.multiple ? files : files?.[0]) as (M extends true ? File[] : File) | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFile(index: number) {
|
function removeFile(index: number) {
|
||||||
const file = files.value?.[index]
|
if (!props.multiple || !modelValue.value) {
|
||||||
|
return
|
||||||
if (file) {
|
|
||||||
URL.revokeObjectURL(URL.createObjectURL(file))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileListArr = Array.from(files.value!)
|
const fileList = Array.from(modelValue.value as File[])
|
||||||
fileListArr.splice(index, 1)
|
|
||||||
|
fileList.splice(index, 1)
|
||||||
|
|
||||||
|
modelValue.value = fileList as (M extends true ? File[] : File) | null
|
||||||
|
|
||||||
|
emitFormInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -153,11 +152,11 @@ defineExpose({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
<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
|
<div
|
||||||
ref="dropZoneRef"
|
ref="dropzoneRef"
|
||||||
role="button"
|
role="button"
|
||||||
:data-dragging="isOverDropZone"
|
:data-dragging="isDragging"
|
||||||
:class="ui.base({ class: props.ui?.base })"
|
:class="ui.base({ class: props.ui?.base })"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="open()"
|
@click="open()"
|
||||||
@@ -188,12 +187,19 @@ defineExpose({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="files && files.length > 0" :class="ui.files({ class: props.ui?.files })">
|
<div v-if="modelValue && (modelValue as File[]).length > 0" :class="ui.files({ class: props.ui?.files })">
|
||||||
<slot name="files" :files="files">
|
<slot name="files">
|
||||||
<div v-for="(file, index) in files" :key="file.name" class="flex items-center gap-2 border border-default rounded-md p-2">
|
<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="urls[index]" :icon="appConfig.ui.icons.file" />
|
<UAvatar :src="createObjectUrl(file)" :icon="appConfig.ui.icons.file" />
|
||||||
<span class="text-sm">{{ file.name }}</span>
|
<span class="text-sm truncate">{{ file.name }}</span>
|
||||||
<UButton size="xs" color="neutral" variant="link" :trailing-icon="appConfig.ui.icons.close" @click="removeFile(index)" />
|
<UButton
|
||||||
|
size="xs"
|
||||||
|
color="neutral"
|
||||||
|
variant="link"
|
||||||
|
:trailing-icon="appConfig.ui.icons.close"
|
||||||
|
class="ms-auto"
|
||||||
|
@click="removeFile(index)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,7 +211,7 @@ defineExpose({
|
|||||||
type="file"
|
type="file"
|
||||||
:name="name"
|
:name="name"
|
||||||
:accept="accept"
|
:accept="accept"
|
||||||
:multiple="multiple"
|
:multiple="(multiple as boolean)"
|
||||||
:required="required"
|
:required="required"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
v-bind="{ ...$attrs, ...ariaAttrs }"
|
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