画像のドラッグ&ドロップをライブラリ使わずに実装する
Reactを使ったフロントエンド開発で画像のドラッグ&ドロップを実装することがあったので記事化してみます。
「React 画像 ドラッグアンドドロップ」で検索すると大体react-dropzone
というライブラリを使った実装方法が多く出てくると思います。これを使っても良いのですが、バンドルサイズを削りたい、せっかくならドラッグイベントについて勉強したい、ということでライブラリを使わずに実装しました。
出来上がりイメージ
以下のgifのような実装を目指します。
画像はいらすとや様より
開発してみた
大まかには以下のものを実装すればOKです。
- ドロップする領域を作成し、そこに以下のドラッグ&ドロップイベントを登録する
-
dragenter
: ドラッグしたものがそこに入ってきたときに何をするか?- 例:ドロップ領域の
opacity
を下げる
- 例:ドロップ領域の
-
dragleave
: ドラッグしたものがそこを出ていったときに何をするか?- 例:ドロップ領域の
opacity
を元に戻す
- 例:ドロップ領域の
-
drop
: ドラッグしているものをそこにドロップしたときに何をするか?- 例:画像をアップロードする
-
- 画像がアップロードされたらその画像を表示する
各イベントの詳細は以下参照
コードを書く
①ドロップする領域を作成
まずはドラッグ&ドロップができる領域として、DropImageZone
というコンポーネントを作ります。
import { DragEvent, FC, ReactNode, useState } from "react";
type Props = {
onDropFile: (file: File) => void;
children: ReactNode;
};
const DropImageZone: FC<Props> = ({ onDropFile, children }) => {
const [isDragActive, setIsDragActive] = useState<boolean>(false);
const onDragEnter = (e: DragEvent<HTMLDivElement>) => {
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
setIsDragActive(true);
}
};
const onDragLeave = (e: DragEvent<HTMLDivElement>) => {
setIsDragActive(false);
};
const onDrop = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragActive(false);
if (e.dataTransfer.files !== null && e.dataTransfer.files.length > 0) {
if (e.dataTransfer.files.length === 1) {
onDropFile(e.dataTransfer.files[0]);
} else {
alert("ファイルは1個まで!");
}
e.dataTransfer.clearData();
}
};
return (
<div
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
onDragOver={(e) => e.preventDefault()}
onDrop={onDrop}
// mergeはclassを合成する自作の関数
// Dragが有効のときopacityを変更する
className={merge(style.drop_zone, isDragActive ? style.opacity : null)}
>
{children}
</div>
);
};
Reactのdiv
要素にonDragEnter
等のイベントを登録することができるので、それらを一つずつやっていくだけです。各イベントの引数の型はReact.DragEvent<HTMLDivElement>
になります。
onDragEnter
はdragenter
イベントに相当します。ここではドラッグ中かどうかを判定するstateであるisDragActive
を変更させます。画像を2個以上同時にアップロードすることは想定してないので、ファイルの数が1個でないときは無効なドラッグということでstateは変更させません。
onDragLeave
はdragleave
イベントに相当します。ここではonDragEnter
で変更したstateを元に戻しています。
onDragOver
はdragover
イベントに相当します。ドラッグ中にするべきことは無いのですが、デフォルトではドラッグしているファイルを開いてしまうのでそれをpreventDefault
で阻止する必要があります。
onDrop
はdrop
イベントに相当します。ここではアップロードされたファイルが1個だけかどうかを検証し、親コンポーネントから受け取ったonDropFile
を実行します。onDropFile
はFile
型の引数を受け取ります。
②画像アップロードしたあとの処理を記述
上記で画像のドロップができるようになったのであとは画像がアップロードされたらそれを表示するだけです。
import { FC, useState } from "react";
import Image from "next/image";
const RegisterCard: FC = () => {
const [image, setImage] = useState<string | null>(null);
const onDropFile = (file: File) => {
if (file.type.substring(0, 5) !== "image") {
alert("画像ファイルでないものはアップロードできません!");
} else {
const fileReader = new FileReader();
fileReader.onload = () => {
const imageSrc: string = fileReader.result as string;
setImage(imageSrc);
};
fileReader.readAsDataURL(file);
}
};
return (
<div className={style.container}>
{image ? (
<Image
src={image}
width={403}
height={598}
alt="画像のプレビュー"
className={style.preview_image}
/>
) : (
<DropImageZone onDropFile={onDropFile}>
<div className={style.content}>
<Image
src={"/file-upload.svg"}
width={24}
height={24}
alt="アップロードアイコン"
/>
</div>
</DropImageZone>
)}
</div>
);
};
画像をstateとしてそれがnull
のとき(画像がまだアップロードされていないとき)はDropImageZone
を表示、画像がアップロードされたらその画像をそのまま表示します。
画像は最初File
型になっているので、FileReader
で読み込んだものをアサーションでそのパスをstring
型で受け取ります。読み込む際にFileのMIME型を見て画像かどうかをチェックしています(MIMEの最初がimage
であれば画像ファイルと判定できる)。
おまけ: ファイル選択での画像アップロードも実装する
多くの場合、ドラッグ&ドロップだけでなくファイル選択でのアップロードもあわせて実装する必要があると思うので、そちらの実装も合わせて紹介します。
こちらの実装はドラッグ&ドロップよりも簡単で、シンプルにinput
要素を使って実装するだけです。私はInputImageSelector
コンポーネントとして切り出してみました。
import { ChangeEvent, FC } from "react";
type Props = {
onDropFile: (file: File) => void;
};
const InputImageSelector: FC<Props> = ({ onDropFile }) => {
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const files = e.target.files;
if (files !== null && files.length > 0) {
if (files.length === 1) {
onDropFile(files[0]);
} else {
alert("ファイルは1個まで!");
}
}
};
return (
<label htmlFor="image" className={style.input_image}>
<input type="file" id="image" accept="image/*" onChange={onChange} />
browse
</label>
);
};
input
要素にonChange
イベントを登録します。また、画像のアップロードのみを想定しているため、accept
プロパティにimage/*
を設定しています。
あとは先程のDropImageZone
と組み合わせるだけです。
...
return (
<div className={style.container}>
{image ? (
<Image
src={image}
width={403}
height={598}
alt="画像のプレビュー"
className={style.preview_image}
/>
) : (
<DropImageZone onDropFile={onDropFile}>
<div className={style.content}>
<Image
src={"/file-upload.svg"}
width={24}
height={24}
alt="アップロードアイコン"
/>
</div>
<div className={style.message}>
Drop files to upload or
<InputImageSelector onDropFile={onDropFile} />
</div>
</DropImageZone>
)}
</div>
);
Discussion