From cfc38100100c759e3fe86de859046eebd2c047e3 Mon Sep 17 00:00:00 2001 From: rdjanuar <rdjanuar008@gmail.com> Date: Thu, 14 Nov 2024 23:54:46 +0700 Subject: [PATCH 1/3] wip --- playground/app/app.vue | 3 ++- playground/app/pages/components/file-upload.vue | 9 +++++++++ src/runtime/components/FileUpload.vue | 9 +++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 playground/app/pages/components/file-upload.vue create mode 100644 src/runtime/components/FileUpload.vue diff --git a/playground/app/app.vue b/playground/app/app.vue index be1678ec02..545f078884 100644 --- a/playground/app/app.vue +++ b/playground/app/app.vue @@ -46,7 +46,8 @@ const components = [ 'table', 'textarea', 'toast', - 'tooltip' + 'tooltip', + 'file-upload' ] const items = components.map(component => ({ label: upperName(component), to: `/components/${component}` })) diff --git a/playground/app/pages/components/file-upload.vue b/playground/app/pages/components/file-upload.vue new file mode 100644 index 0000000000..e7e309d340 --- /dev/null +++ b/playground/app/pages/components/file-upload.vue @@ -0,0 +1,9 @@ +<script setup lang="ts"> +import FileUpload from '#ui/components/FileUpload.vue' +</script> + +<template> + <div> + <FileUpload /> + </div> +</template> diff --git a/src/runtime/components/FileUpload.vue b/src/runtime/components/FileUpload.vue new file mode 100644 index 0000000000..3f680e872f --- /dev/null +++ b/src/runtime/components/FileUpload.vue @@ -0,0 +1,9 @@ +<script lang="ts"> +import { useFileDialog } from '@vueuse/core' +</script> + +<script lang="ts" setup></script> + +<template> + <h1>asdok</h1> +</template> From 062a5025edc8d1a00a3828a10a1342eaa8b2074c Mon Sep 17 00:00:00 2001 From: rdjanuar <rdjanuar008@gmail.com> Date: Sun, 1 Dec 2024 19:58:12 +0700 Subject: [PATCH 2/3] wip --- playground/app/app.vue | 4 +- .../app/pages/components/file-upload.vue | 8 +- src/runtime/components/FileUpload.vue | 120 +++++++++++++++++- src/runtime/components/Input.vue | 2 +- src/runtime/composables/useFileUpload.ts | 109 ++++++++++++++++ src/runtime/types/index.ts | 1 + src/theme/file-upload.ts | 0 7 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 src/runtime/composables/useFileUpload.ts create mode 100644 src/theme/file-upload.ts diff --git a/playground/app/app.vue b/playground/app/app.vue index 545f078884..7fdc173f66 100644 --- a/playground/app/app.vue +++ b/playground/app/app.vue @@ -23,6 +23,7 @@ const components = [ 'dropdown-menu', 'form', 'form-field', + 'file-upload', 'input', 'input-menu', 'kbd', @@ -46,8 +47,7 @@ const components = [ 'table', 'textarea', 'toast', - 'tooltip', - 'file-upload' + 'tooltip' ] const items = components.map(component => ({ label: upperName(component), to: `/components/${component}` })) diff --git a/playground/app/pages/components/file-upload.vue b/playground/app/pages/components/file-upload.vue index e7e309d340..bf542184f6 100644 --- a/playground/app/pages/components/file-upload.vue +++ b/playground/app/pages/components/file-upload.vue @@ -1,9 +1,9 @@ -<script setup lang="ts"> -import FileUpload from '#ui/components/FileUpload.vue' +<script lang="ts" setup> +const files = ref() </script> <template> - <div> - <FileUpload /> + <div class="w-full max-w-96"> + <UFileUpload v-model="files" /> </div> </template> diff --git a/src/runtime/components/FileUpload.vue b/src/runtime/components/FileUpload.vue index 3f680e872f..1c6ed66ace 100644 --- a/src/runtime/components/FileUpload.vue +++ b/src/runtime/components/FileUpload.vue @@ -1,9 +1,123 @@ <script lang="ts"> -import { useFileDialog } from '@vueuse/core' +export interface FileUploadProps { + /** + * @default true + */ + multiple?: boolean + /** + * @default '*' + */ + accept?: string + /** + * Select the input source for the capture file. + * @see [HTMLInputElement Capture](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture) + */ + capture?: 'user' | 'environment' + /** + * Reset when open file dialog. + * @default false + */ + reset?: boolean + /** + * Select directories instead of files. + * @see [HTMLInputElement webkitdirectory](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory) + * @default false + */ + directory?: boolean + /** + * @default 0 + */ + minFileSize?: number + /** + * @default Infinity + */ + maxFileSize?: number + /** + * @default false + */ + disabled?: boolean + /** + * Whether to allow drag and drop in the dropzone element + * @default true + */ + allowDrop?: boolean +} + +export interface FileAcceptDetails extends File { + +} +export interface FileRejectDetails extends File { +} + +export interface FileUplaodEmits { + (e: 'update:modelValue', value: File[]): void + (e: 'change' | 'delete', value: File): void + (e: 'accept', value: FileAcceptDetails): void + (e: 'reject', value: FileRejectDetails): void +} </script> -<script lang="ts" setup></script> +<script lang="ts" setup generic="T extends FileList"> +import { ref, defineModel } from 'vue' + +const emits = defineEmits<FileUplaodEmits>() + +const modelValue = defineModel<FileList>() + +const file = ref<HTMLInputElement>() + +const open = () => { + if (file.value) { + file.value.multiple = true + file.value.click() + } +} + +const onHandleUpload = (event: Event) => { + const result = event.target as HTMLInputElement + const files = [...(modelValue.value ?? []), ...result.files!] + emits('change', result.files?.[0] as File) + emits('update:modelValue', files) +} + +const onHandleDelete = (index: number) => { + const f = Array.from(modelValue.value as FileList).filter((_, i) => i !== index) + emits('update:modelValue', f) +} +</script> <template> - <h1>asdok</h1> + <div class="w-full flex flex-col gap-4" aria-label="root"> + <div + aria-label="dropzone" + tabindex="0" + role="button" + class="min-h-[20rem] w-full border-[var(--ui-border)] border rounded-lg flex flex-col gap-3 justify-center items-center px-6 py-4" + @click="open" + @keyup.prevent.enter="open" + > + <h3 class="font-bold text-lg"> + Drop your files here + </h3> + <UButton class="cursor-pointer" @click.capture.stop="open"> + Open Dialog + </UButton> + </div> + <input ref="file" type="file" class="sr-only" @change="onHandleUpload"> + <ul class="flex flex-col gap-3"> + <li v-for="(asd, index) in modelValue" :key="index" class=""> + <span> + + {{ asd.name }} + </span> + + <UButton + variant="ghost" + color="error" + icon="i-lucide-trash" + @click="onHandleDelete(index)" + /> + </li> + </ul> + </div> </template> diff --git a/src/runtime/components/Input.vue b/src/runtime/components/Input.vue index 103a24d74e..d85c0468ae 100644 --- a/src/runtime/components/Input.vue +++ b/src/runtime/components/Input.vue @@ -136,7 +136,7 @@ function onBlur(event: FocusEvent) { } defineExpose({ - inputRef + inputRef: inputRef }) onMounted(() => { diff --git a/src/runtime/composables/useFileUpload.ts b/src/runtime/composables/useFileUpload.ts new file mode 100644 index 0000000000..fb887fa0e1 --- /dev/null +++ b/src/runtime/composables/useFileUpload.ts @@ -0,0 +1,109 @@ +import { ref, type Ref } from 'vue' +import { createEventHook } from '@vueuse/core' + +export interface UseFileUploadOptions { + /** + * @default true + */ + multiple?: boolean + /** + * @default '*' + */ + accept?: string + /** + * Select the input source for the capture file. + * @see [HTMLInputElement Capture](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture) + */ + capture?: 'user' | 'environment' + /** + * Reset when open file dialog. + * @default false + */ + reset?: boolean + /** + * Select directories instead of files. + * @see [HTMLInputElement webkitdirectory](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory) + * @default false + */ + directory?: boolean + /** + * @default 0 + */ + minFileSize?: number + /** + * @default Infinity + */ + maxFileSize?: number + /** + * @default false + */ + disabled?: boolean + /** + * Whether to allow drag and drop in the dropzone element + * @default true + */ + allowDrop?: boolean +} + +const DEFAULT_OPTIONS: UseFileUploadOptions = { + multiple: true, + accept: '*', + reset: false, + directory: false, + minFileSize: 0, + maxFileSize: Infinity, + disabled: false, + allowDrop: true +} + +export interface UseFileDialogReturn { + files: Ref<FileList | null> + open: (localOptions?: Partial<UseFileUploadOptions>) => void + reset: () => void +} + +export interface FileUploadEmits { + (e: 'change', value: File): void + (e: 'accept', value: any): void + (e: 'reject', value: unknown): void +} + +export function useFileUpload<T>(options = DEFAULT_OPTIONS) { + let input: HTMLInputElement | undefined + + const files = ref<FileList | null | T>(null) + + const { on: onChange, trigger: changeTrigger } = createEventHook<File>() + const { on: onReject, trigger: rejectTrigger } = createEventHook<File>() + const { on: onAccept, trigger: acceptTrigger } = createEventHook<File>() + const { on: onCancel, trigger: cancelTrigger } = createEventHook() + + if (document) { + input = document.createElement('input') + input.type = 'file' + + input.onchange = (event: Event) => { + const result = event.target as HTMLInputElement + files.value = result.files + changeTrigger(result.files?.[0] as File) + } + + input.oncancel = () => { + cancelTrigger() + } + } + + const open = () => { + input.multiple = options.multiple! + input.accept = options.accept! + input.capture = options.capture! + input.click() + } + + return { + open, + files, + onChange, + onCancel + } +} diff --git a/src/runtime/types/index.ts b/src/runtime/types/index.ts index e14b651409..4f02834abe 100644 --- a/src/runtime/types/index.ts +++ b/src/runtime/types/index.ts @@ -41,6 +41,7 @@ export * from '../components/Tabs.vue' export * from '../components/Textarea.vue' export * from '../components/Toast.vue' export * from '../components/Toaster.vue' +export * from '../components/FileUpload.vue' export * from '../components/Tooltip.vue' export * from './form' export * from './locale' diff --git a/src/theme/file-upload.ts b/src/theme/file-upload.ts new file mode 100644 index 0000000000..e69de29bb2 From 643c1e0c0439cfd8e3b8fa70fe9ebbfdca2c6fda Mon Sep 17 00:00:00 2001 From: rdjanuar <rdjanuar008@gmail.com> Date: Sun, 1 Dec 2024 20:04:05 +0700 Subject: [PATCH 3/3] up --- src/runtime/composables/useFileUpload.ts | 109 ----------------------- 1 file changed, 109 deletions(-) delete mode 100644 src/runtime/composables/useFileUpload.ts diff --git a/src/runtime/composables/useFileUpload.ts b/src/runtime/composables/useFileUpload.ts deleted file mode 100644 index fb887fa0e1..0000000000 --- a/src/runtime/composables/useFileUpload.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { ref, type Ref } from 'vue' -import { createEventHook } from '@vueuse/core' - -export interface UseFileUploadOptions { - /** - * @default true - */ - multiple?: boolean - /** - * @default '*' - */ - accept?: string - /** - * Select the input source for the capture file. - * @see [HTMLInputElement Capture](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture) - */ - capture?: 'user' | 'environment' - /** - * Reset when open file dialog. - * @default false - */ - reset?: boolean - /** - * Select directories instead of files. - * @see [HTMLInputElement webkitdirectory](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory) - * @default false - */ - directory?: boolean - /** - * @default 0 - */ - minFileSize?: number - /** - * @default Infinity - */ - maxFileSize?: number - /** - * @default false - */ - disabled?: boolean - /** - * Whether to allow drag and drop in the dropzone element - * @default true - */ - allowDrop?: boolean -} - -const DEFAULT_OPTIONS: UseFileUploadOptions = { - multiple: true, - accept: '*', - reset: false, - directory: false, - minFileSize: 0, - maxFileSize: Infinity, - disabled: false, - allowDrop: true -} - -export interface UseFileDialogReturn { - files: Ref<FileList | null> - open: (localOptions?: Partial<UseFileUploadOptions>) => void - reset: () => void -} - -export interface FileUploadEmits { - (e: 'change', value: File): void - (e: 'accept', value: any): void - (e: 'reject', value: unknown): void -} - -export function useFileUpload<T>(options = DEFAULT_OPTIONS) { - let input: HTMLInputElement | undefined - - const files = ref<FileList | null | T>(null) - - const { on: onChange, trigger: changeTrigger } = createEventHook<File>() - const { on: onReject, trigger: rejectTrigger } = createEventHook<File>() - const { on: onAccept, trigger: acceptTrigger } = createEventHook<File>() - const { on: onCancel, trigger: cancelTrigger } = createEventHook() - - if (document) { - input = document.createElement('input') - input.type = 'file' - - input.onchange = (event: Event) => { - const result = event.target as HTMLInputElement - files.value = result.files - changeTrigger(result.files?.[0] as File) - } - - input.oncancel = () => { - cancelTrigger() - } - } - - const open = () => { - input.multiple = options.multiple! - input.accept = options.accept! - input.capture = options.capture! - input.click() - } - - return { - open, - files, - onChange, - onCancel - } -}