本日はブラウザにファイルをドラッグ・アンド・ドロップする機能を実装する。
例えば、あまり馴染みがないかもしれないが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>