2020-06-19

【Vue.js】CSSなしでファイルをドラッグアンドドロップで追加する!【Vuetify】

ブラウザにファイルをドラッグアンドドロップして登録する機能を作る。CSSは1行も書かない!

Article Image

ドラッグアンドドロップ

本日はブラウザにファイルをドラッグ・アンド・ドロップする機能を実装する。

例えば、あまり馴染みがないかもしれないがGoogleの「画像で検索」にドラッグ・アンド・ドロップ機能がついている。

image-20200617132953998

※画像検索の画面から検索窓にあるカメラマークをクリックするとこの画面が出る。かなり便利。

image-20200617133040286

この機能はその説明通り「画像をウィンドウの外からドラッグ・アンド・ドロップ」するだけで文字ではなく画像を元に検索がかかる。

このドラッグアンドドロップの部分はどうやってVue.jsで実装すればよいのだろうか。

CSSを使わないコード!

よくある実装ではCSSを使ってドロップするための枠を確保しているサイトが多い。

今回はVuetifyを使ってそもそもCSSを1文字も書かなくてもOKにする。

完成コードについて

長いため最下部にvueファイル全体のコードを貼り付けておく。

面倒な場合は最下部に掲載したコードをまるごとコピペして各自いじってみると良い。

完成品

今回の記事を完成させると次のように動作する

CSSを利用しないのでインプットUIのデザイン設計も必要なく最低限のコードでスマートに実装できる。

新規作成

これは説明するまでもないが、Vueプロジェクトの新規作成。

vue create プロジェクト名

その後プロジェクトのディレクトリ内で次を入力してVuetifyを追加する。

vue add vuetify

Vuetify

軽く説明するとVueに簡単にマテリアルデザインを導入する私がいつも利用しているプラグイン。

プラグインなのでnpm installではなくVueの機能addから追加している。

クイックスタート — Vuetify.js

インストール後はこのドキュメントにあるようにタグを入れていくだけでよく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が完成した。

image-20200617134548596

コレ自体はドラッグアンドドロップはできないがクリックするとファイルオープンのウィンドウが開く。

試しにファイルを4つ選択してみた画像。非常にわかりやすい。

ドラッグアンドドロップを実装

つまりこれにあとはドラッグしてドロップできるコードを追加できればOKだ。

今回は非常に参考になるYoutube動画を発見した。

こちらはドロップ部分の外観にCSSを使っているので逆にCSSを使う方法が気になる場合は参考にする。

ドラッグイベントをハンドリング

先程ファイルを登録するVuetifyUIv-file-inputというタグで表示できた。

ドラッグアンドドロップイベントをハンドリングするのだが、どうやらこのv-file-inputの中に書いてしまうとハンドルできない。

重要:そこで外側にdivv-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部分

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.filesv-file-inputが扱うデータだが、これが配列[]になっている。forで取得できるのはファイルのオブジェクトなのでそれをpushすることにより配列に入れる必要がある。

今回やっていないこと

さて、データは実際にドラッグドロップで登録できたがこれを送信するコードは書いていない。

v-file-inputで登録できるデータの扱いと一緒であるので各自実装すること。

参考:理解しておくVueDOM

今回はドラッグアンドドロップに関して次のイベントをハンドリングしている。

  @dragover.prevent
  @dragenter="onDragEnter"
  @dragleave="onDragLeave"
  @drop="onDrop"

これらのイベントは@(v-onの省略が@)によって取得している。

これはvueの機能であるが@以降のdrag~のイベントはDOMによるイベントである。

私もかなり詳しいことを聞かれたら答えられないのだが、公式に次の解説がなされている。

イベントハンドリング — Vue.js

v-on ディレクティブを使うことで、DOM イベントの購読、イベント発火時の JavaScript の実行が可能になります。

DOMのイベントを受け取っているのだ。

DragEvent

ではDOMのイベントはどこに書いてあるのだろうか。Vue.jsのページにはもちろん記述はない。

MDNを参考にすると良い。

DragEvent - Web API | 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>


この記事のタグ

この記事をシェア


謎の技術研究部 (謎技研)