[Vuetify3/Nuxt3] ファイルアップロードボタンの実装

# はじめに
画面のデザイン的に、Vuetify3のデフォルトのアップロードボタンでなく、いかにもボタンボタンしてる アップロードボタンが作りたくなりました。Vuetify2での例はありましたが Vuetify3での例がなかったのでそのままコピペできず、ちょっと調べることになりました。

## 開発環境
- Nuxt 3.0.0-rc.1
- Vuetify 3.0.0-beta.1

## ほしいイメージ
##### これじゃなくて
<img width="auto" height="20px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254984/25c16616-7f73-c1e9-b8e0-905b47497efb.png">

##### これがほしい
<img width="auto" height="20px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254984/87f2da4f-ddb3-2f17-f891-323359e7e5b4.png">

## 結果
過程をいっぱい書いても 興味ない方も居ると思うので出来上がったものをまず置いときます

### 1: ボタンがある
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254984/c9d10ec6-38b3-42d1-0e8f-0d2f6dc18eb3.png)
### 2: クリックでクルクルアニメーション+選択ダイアログ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254984/e6f0d2d7-02a6-cecd-c517-992677c54c14.png)
### 3: 選択後ファイル名が出る + emitされる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254984/8d37c955-681e-3b93-a4fa-8e5d73e77686.png)
### -: ドラッグアンドドロップもできる
![messageImage_1653136247393.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254984/72cc856b-7d34-d6fb-8ad1-48791d7170b6.jpeg)

```FileSelectButton.vue
<template>
  <v-btn
      @dragover.prevent="isDragged = true"
      @dragleave.prevent="isDragged = false"
      @drop.prevent="onFileDropped"
      width="100%"
      height="100%"
      color="info"
      class="text-none"
      :variant="!isDragged ? 'contained' : 'outlined'"
      depressed
      @click="openFileSelect"
  >
      <v-progress-circular
          v-if="isSelecting"
          class="mx-12 my-6"
          indeterminate
          color="white"
      />
      <v-row v-if="!isSelecting" class="text-center" justify="center">
          <v-col class="mt-2 mb-n2" cols="10">
              <v-icon size="40">
                  {{ props.buttonIcon }}
              </v-icon>
          </v-col>
          <v-col class="text-truncate mb-2 mt-n2" cols="10">
              {{ displayText }}
          </v-col>
      </v-row>
  </v-btn>
  <input
      ref="uploader"
      class="d-none"
      type="file"
      :accept="props.accept"
      @change="onFileSelectChange"
  >
</template>

<script setup lang="ts">
import {
  mdiCloudUpload
} from '@mdi/js'

interface Props {
  buttonTitle?: string,
  buttonIcon?: string,
  accept?: string
}
const props = withDefaults(defineProps<Props>(), {
  buttonTitle: "ファイルを選択",
  buttonIcon: mdiCloudUpload,
  accept: "image/*"
})

interface Emits {
  (e: "update:File", value: File): void;
}
const emits = defineEmits<Emits>()

const isSelecting = ref<boolean>(false)
const isDragged = ref<boolean>(false)
const selectedFile = ref<File | null>(null)

const displayText = computed(() => {
  switch (true) {
    case isDragged.value:
      return "離すとアップロード"
    case selectedFile.value != null:
      return selectedFile.value!.name
    default:
      return props.buttonTitle
  }
})


// ボタンクリックでファイル選択を開く
const uploader = ref<HTMLInputElement>()
const openFileSelect= () => {
  isSelecting.value = true
  window.addEventListener('focus', () => {
    isSelecting.value = false
  }, { once: true })
  uploader.value?.click()
}

// ファイル選択確定時にEmit
const onFileSelectChange = (e: Event) => {
    const target = e.target as HTMLInputElement
    const files = target.files
    const file = files![0]
    selectedFile.value = file
    emits("update:File", file)
}

// D&D時にEmit
const onFileDropped = (e: DragEvent) => {
    isDragged.value = false
    if (!e) {
      return
    }

    if (!e.dataTransfer) {
      return
    }

    if (e.dataTransfer.files.length === 0) {
      return
    }
    const file = e.dataTransfer.files[0]
    selectedFile.value = file
    emits("update:File", file)
}
</script>
```

# 作ってみる
過程に興味ある人向け (様々な資料を参考にさせていただきました)

## 1: 既存のファイルアップロードボタンを見てみる
Googleで `Vuetify file upload button` と検索すると Vuetify2の例が出てくる

https://ourcodeworld.com/articles/read/1424/how-to-use-a-button-as-a-file-uploader-with-vuetify-in-vuejs

https://codepen.io/blachocolat/pen/BgMKRQ

どうやらv-btnが押されたら refsを経由して input type="file" の要素をクリックするという
処理をするということがわかった(そんなことできるのか...)

## 2: Vue3 の refsの仕様を調べる
Vue2でrefsは使った覚えがあったが Vue3のrefsの使い方がよくわからなかったのでCompositionAPIの記事を参考にさせていただいた

https://qiita.com/jay-es/items/6d0279737fb510b0aa6c

https://codelikes.com/vue-refs/#toc4

そして型についてはなんやかんや困ったが
どうやら`HTMLInputElement`型を取ってくればいいということがわかった
Eventに関しては 正しい使い方がちょっとわからなかったが as で捻じ曲げて e.targetを HTMLInputElement として扱って とりあえずtypescript文法上は動くようにした

```ts
// ファイル選択確定時にEmit
const onFileSelectChange = (e: Event) => {
    const target = e.target as HTMLInputElement
    const files = target.files
    const file = files![0]
    selectedFile.value = file
    emits("update:File", file)
}
```

https://zenn.dev/kata_n/scraps/7269527f995239

https://github.com/microsoft/TypeScript/issues/31816


## 3: Vue3.2 Setupでの Props/Emitを調べる
Nuxt3 RCすげ～！って始めたので 実はVue3.2自体知らない、よってこれも調べた。とても参考にさせていただいた。

https://zenn.dev/azukiazusa/articles/676d88675e4e74#props-%E3%81%A8-emit


## 4: ドラッグアンドドロップに対応させる
できればドラッグアンドドロップに対応させたいよねってことでこちらも参考にさせて頂いた。こちらはほぼそのままで動かすことができた。

https://qiita.com/punkshiraishi/items/49b91767b5143bcb1fcc

## 5: ドラッグアンドドロップ時にボタンをアウトラインにする
Vuetify2の v-btn は `outlined` をつけるとアウトラインになったが Vuetify3 では `variant=outlined` を指定するようになっていた。これに気づかず若干ハマった。

https://vuetifyjs.com/ja/components/buttons/#section-4f7f304465b9

https://next.vuetifyjs.com/en/api/v-btn/#props-variant

## 6: サイズ調整
そのままだと絶妙に残念なサイズをしていたので、親要素にサイズを任せることにして
`width="100%"` と `height="100%"`をつけたら いい感じになった