本日はブラウザにファイルをドラッグ・アンド・ドロップする機能を実装する。
例えば、あまり馴染みがないかもしれないがGoogleの「画像で検索」にドラッグ・アンド・ドロップ機能がついている。
※画像検索の画面から検索窓にあるカメラマークをクリックするとこの画面が出る。かなり便利。
この機能はその説明通り「画像をウィンドウの外からドラッグ・アンド・ドロップ」するだけで文字ではなく画像を元に検索がかかる。
このドラッグアンドドロップの部分はどうやってVue.js
で実装すればよいのだろうか。
よくある実装ではCSS
を使ってドロップするための枠を確保しているサイトが多い。
今回はVuetify
を使ってそもそもCSS
を1文字も書かなくてもOKにする。
長いため最下部にvue
ファイル全体のコードを貼り付けておく。
面倒な場合は最下部に掲載したコードをまるごとコピペして各自いじってみると良い。
今回の記事を完成させると次のように動作する
CSS
を利用しないのでインプットUI
のデザイン設計も必要なく最低限のコードでスマートに実装できる。
これは説明するまでもないが、Vue
プロジェクトの新規作成。
vue create プロジェクト名
その後プロジェクトのディレクトリ内で次を入力してVuetify
を追加する。
vue add vuetify
軽く説明するとVue
に簡単にマテリアルデザインを導入する私がいつも利用しているプラグイン。
プラグインなのでnpm install
ではなくVue
の機能add
から追加している。
インストール後はこのドキュメントにあるようにタグを入れていくだけでよくCSSは基本使わなくて良くなる。
Vue
にはそもそもFile inputs
というファイルを入力するUI
の部分が実装されている。
あくまでUI
なので裏側の処理やドラッグアンドドロップに関するコードは入っていない。
File input component — Vuetify.js
上記ページの最下部にある「複雑な選択スロット」というサンプルを貼り付ける。
まずHTML
部分
<template>
<v-file-input
v-model="files"
color="deep-purple accent-4"
counter
label="File input"
multiple
placeholder="Select your files"
prepend-icon="mdi-paperclip"
outlined
:show-size="1000"
>
<template v-slot:selection="{ index, text }">
<v-chip
v-if="index < 2"
color="deep-purple accent-4"
dark
label
small
>
{{ text }}
</v-chip>
<span
v-else-if="index === 2"
class="overline grey--text text--darken-3 mx-2"
>
+{{ files.length - 2 }} File(s)
</span>
</template>
</v-file-input>
</template>
<script>
export default {
data: () => ({
files: [],
}),
}
</script>
data
部にfiles
だけ追加しておく
data: () => ({
files: [],
}),
なんとたったこれだけで既に「複数のファイルを同時に登録する」というUIが完成した。
コレ自体はドラッグアンドドロップはできないがクリックするとファイルオープンのウィンドウが開く。
試しにファイルを4つ選択してみた画像。非常にわかりやすい。
つまりこれにあとはドラッグしてドロップできるコードを追加できればOKだ。
今回は非常に参考になるYoutube
動画を発見した。
こちらはドロップ部分の外観にCSS
を使っているので逆にCSS
を使う方法が気になる場合は参考にする。
先程ファイルを登録するVuetify
のUI
はv-file-input
というタグで表示できた。
ドラッグアンドドロップイベントをハンドリングするのだが、どうやらこのv-file-input
の中に書いてしまうとハンドルできない。
重要:そこで外側にdiv
やv-row
で囲ってそちらの@
でイベントをハンドルしていく。
<v-row //この中でイベントをハンドリングする!
@dragover.prevent
@dragenter="onDragEnter"
@dragleave="onDragLeave"
@drop="onDrop"
>
<v-file-input //ここには書かない!
v-model="files"
color="deep-purple accent-4"
//続く
@dragover.prevent
:ブラウザに画像をドロップした場合デフォルトで画像がブラウザで開かれる等の処理が行われる。これはソレを防止する。@dragenter="onDragEnter"
:領域にドラッグが入った時に実行される関数@dragleave="onDragLeave"
:領域からドラッグが出た時に実行される関数@drop="onDrop"
:領域でドラッグしているものをドロップしたら実行される関数methods: {
onDrop(e) {
e.preventDefault();
e.stopPropagation();
this.isDragging = false;
const _files = e.dataTransfer.files;
for (const file in _files) {
if (!isNaN(file)) {
//filesはファイル以外のデータが入っており、ファイルの場合のみキー名が数字になるため
this.files.push(_files[file]);
}
}
},
onDragEnter(e) {
e.preventDefault();
this.isDragging = true;
this.dragCount++;
},
onDragLeave(e) {
e.preventDefault();
this.dragCount--;
if (this.dragCount <= 0) {
this.isDragging = false;
}
}
}
いずれのメソッドもカッコ内e
でイベントを受け取ることができる。
preventDefault()
でファイルがブラウザで開かれるのを防止
stopPropagation()
さらに親側にイベントが行くのでソレも防止(これがないとエラーがでる)
this.dragCount
境界線付近ではenterとleaveが何度も発生するので帳尻を合わせるコード。
ドラッグオーバーした時に背景色が変化するようになっているが、これがないと一瞬変化した色がすぐにもとに戻ってしまう。 詳しく説明すると長くなりそうなので色々実験してみてほしい。個人的にこのコードはあまり美しく無いような気がするが...
※実際にやってみないと意味不明かと思うので基本はここは無視でもよい。気になったら構造を確認するテストをやってみてほしい。
コードではe.dataTransfer.files
というコードを使ってブラウザ側からファイルデータを受け取っている。
このデータ構造とv-file-input
に使うデータ構造はほとんど一致しているのだが一部違うため変換が必要。
それが上記onDrop(e)
のコード。
onDrop(e) {
e.preventDefault();
e.stopPropagation();
this.isDragging = false;
const _files = e.dataTransfer.files;
for (const file in _files) {
if (!isNaN(file)) {
//filesはファイル以外のデータが入っており、ファイルの場合のみキー名が数字になるため
this.files.push(_files[file]);
}
}
},
まずdataTransfer.files
にはファイル以外の余計なデータが入っている。ファイルは必ず
数字キー:{ データ }
という構造になっているのでその部分だけをisNaN
で選別する(ファイル以外には文字列のキー名が入っている)
またthis.files.push(_files[file]);
このコード。this.files
はv-file-input
が扱うデータだが、これが配列[]
になっている。for
で取得できるのはファイルのオブジェクトなのでそれをpush
することにより配列に入れる必要がある。
さて、データは実際にドラッグドロップで登録できたがこれを送信するコードは書いていない。
v-file-input
で登録できるデータの扱いと一緒であるので各自実装すること。
Vue
とDOM
今回はドラッグアンドドロップに関して次のイベントをハンドリングしている。
@dragover.prevent
@dragenter="onDragEnter"
@dragleave="onDragLeave"
@drop="onDrop"
これらのイベントは@
(v-onの省略が@
)によって取得している。
これはvue
の機能であるが@
以降のdrag~
のイベントはDOM
によるイベントである。
私もかなり詳しいことを聞かれたら答えられないのだが、公式に次の解説がなされている。
v-on
ディレクティブを使うことで、DOM イベントの購読、イベント発火時の JavaScript の実行が可能になります。
DOM
のイベントを受け取っているのだ。
ではDOM
のイベントはどこに書いてあるのだろうか。Vue.js
のページにはもちろん記述はない。
MDNを参考にすると良い。
今回使用したコードの全部をここに貼り付ける。
Vuetify
以外入れていないので、そのままHelloWorld.vue
を書き換えてコピペで動くはず。
<template>
<v-container>
<v-row
class="text-center"
@dragover.prevent
@dragenter="onDragEnter"
@dragleave="onDragLeave"
@drop="onDrop"
>
<v-file-input
v-model="files"
color="deep-purple accent-4"
counter
label="File input"
multiple
placeholder="Select your files"
prepend-icon="mdi-paperclip"
outlined
:show-size="1000"
:background-color="isDragging ? 'blue' : 'null'"
>
<template v-slot:selection="{ index, text }">
<v-chip v-if="index < 2" color="deep-purple accent-4" dark label small>{{ text }}</v-chip>
<span
v-else-if="index === 2"
class="overline grey--text text--darken-3 mx-2"
>+{{ files.length - 2 }} File(s)</span>
</template>
</v-file-input>
</v-row>
</v-container>
</template>
<script>
export default {
name: "DragAndDrop",
data: () => ({
files: [],
isDragging: false,
dragCount: 0
}),
methods: {
onDrop(e) {
e.preventDefault();
e.stopPropagation();
this.isDragging = false;
const _files = e.dataTransfer.files;
for (const file in _files) {
if (!isNaN(file)) {
//filesはファイル以外のデータが入っており、ファイルの場合のみキー名が数字になるため
this.files.push(_files[file]);
}
}
},
onDragEnter(e) {
e.preventDefault();
this.isDragging = true;
this.dragCount++;
},
onDragLeave(e) {
e.preventDefault();
this.dragCount--;
if (this.dragCount <= 0) {
this.isDragging = false;
}
}
}
};
</script>