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
-  }
-}