ドラッグアンドドロップ完全に理解した
背景
- Web開発において,DnD(ドラッグアンドドロップ)を実装したいときってあるよね
- いろいろな理由があってライブラリを使わずに実装しなければならないときってあるよね
- 「「onDragイベント使えばできる」けどめんどくさい」ということは知っている
drag関連のイベント多すぎ問題😇
😇😇😇😇😇😇😇😇😇
はじめに,こいつらを平たく説明します.
JavaScriptのeventの説明は割愛します.一言でいうと〇〇が起きたとき(発火時)に関数が実行されるやつ.
また,その前に説明を理解しやすくするためにそれぞれのイベントが,
- 持つ要素(Dragする対象)に記述されるイベントなのか
- 落とされる要素(Dropされる対象)に記述されるイベントなのか
を分けて考えましょう.
ここではDrag itemとDrop zoneという用語で統一したいと思います.
ondrag
Drag itemを持っているときに数ミリ秒ごとに発火するってだけです
console.logするとconsoleを汚染してくるので注意
要素を持っているときに数ミリ秒単位で監視したいものでもない限りは使用しなくてよさそう
というわけで実はこのイベントは最小限のDnD実装に不要😇
ondragstart
Drag itemを持ち上げたときに一度だけ発火
Drag中かどうかを判別するフラグを操作する処理を実行させると良さそうです
Drag itemの残像のCSSを変更したりするために使用しますね
例:
let isDragging = false;
const onDragStart = () => {
isDragging = true;
};
dragend
Drag itemを放したときに一度だけ発火
ondragstartの対称となる存在ですね
例:
let isDragging = false;
const onDragEnd = () => {
isDragging = false;
};
ondragenter
Drag itemがDrop zoneに入った(乗った)ときDrop zoneに発火
Drag itemがDrop zoneの上にあるとき実行したい処理を書くと良さそうです
しかし,乗っただけでこのイベントは発火するための,どの要素の上に乗ったのかを判別する条件文きが必要になってきます
例:
let isDroppable
const onDragEnter = (e) => {
if (e.currentTarget.id === 'drop-zone') {
isDroppable = true;
}
};
dragleave
Drop zoneからDrag itemが出たときDrop zoneに発火
当然一度ondragenterが発火していないと発火し得ない
ondragenterと対称となるイベント
例:
let isDroppable
const onDragLeave = (e) => {
if (e.currentTarget.id === 'drop-zone') {
isDroppable = false;
}
};
ondragover
Drag itemがなにかの上にあると数ミリ秒ごとに発火し続ける様子
ondropを発火させるために必要
e.prevent.default()
しとけばondropが発火する
例:
const onDragOver = (e) => {
e.prevent.default();
};
ondrop
ondragoverの条件を満たしているときに,Drag itemをDrop zoneの上で放すと発火
該当のDrop itemを該当のDrop Zoneに落としたときに実行したい処理を書くと良さそう
つまり実装したかった大半の処理はここになるのかと
例:
const onDrop = () => {
dosomething()
};
要素をDragさせるには
実はdraggable
属性をtrue
にする必要がある
<div draggable>drag item</div>
これで動かせるいえーい
実際では左の持つ前の要素の透明度を下げるとそれっぽいUIになる.
最後にReactで実装したコードを貼っておきます
CSSはtailwindCSSですみません.(雰囲気だけでもということで)
import clsx from 'clsx';
import { useState } from 'react';
const DnDTest = () => {
const [isDragging, setIsDragging] = useState(false);
const [isDroppable, setIsDroppable] = useState(false);
/**
* drag item に関するイベント
*/
/* drag itemを持ったとき */
const onDragStart = (e: React.DragEvent<HTMLDivElement>) => {
console.log('non drag start', e);
setIsDragging(true);
// e.dataTransfer.setData('text/plain', e.currentTarget.id);
// e.dataTransfer.dropEffect = 'move';
};
/* drag itemを放したとき */
const onDragEnd = (e: React.DragEvent<HTMLDivElement>) => {
console.log('on drag end', e);
setIsDragging(false);
};
/**
* drop zone に関するイベント
*/
/* Itemがdrag zoneに入ったとき */
const onDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
console.log('on drag enter', e);
if (e.currentTarget.id === 'drop-zone') {
setIsDroppable(true);
}
};
/* Itemがdrag zoneから出たとき */
const onDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
// 放しても実行される
console.log('on drag leave', e);
if (e.currentTarget.id === 'drop-zone') {
setIsDroppable(false);
}
};
const onDrop = (e: React.DragEvent<HTMLDivElement>) => {
console.log('on drop', e);
if (e.currentTarget.id === 'drop-zone') {
setIsDroppable(false);
}
};
return (
<div>
{/* Drag Item */}
<div
className={clsx(
'h-32 w-32 cursor-pointer rounded bg-teal-300',
isDragging && 'opacity-50', // Drag中は元の要素は透明に
)}
draggable // ドラッグを可能にする
onDragStart={onDragStart}
onDragEnd={onDragEnd}
// onDrag={(e) => console.log('on drag', e)} // ドラッグ中に発火するだけで未使用でもOK
>
drag item
</div>
{/* Drop Zone */}
<div
id='drop-zone'
className={clsx(
'z-10 h-40 w-40 rounded border-4 bg-slate-400',
isDroppable
? 'border-dashed border-slate-800'
: 'border-solid border-slate-400 ',
)}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
onDragOver={(e) => {
e.preventDefault(); // これがないとdropイベントが発火しない
}}
onDrop={onDrop}
>
drop zone
</div>
</div>
);
};
Discussion