🙆

ドラッグアンドドロップ完全に理解した

2022/07/29に公開

背景

  • Web開発において,DnD(ドラッグアンドドロップ)を実装したいときってあるよね
  • いろいろな理由があってライブラリを使わずに実装しなければならないときってあるよね
  • 「「onDragイベント使えばできる」けどめんどくさい」ということは知っている

drag関連のイベント多すぎ問題😇

😇😇😇😇😇😇😇😇😇

はじめに,こいつらを平たく説明します.

JavaScriptのeventの説明は割愛します.一言でいうと〇〇が起きたとき(発火時)に関数が実行されるやつ.

また,その前に説明を理解しやすくするためにそれぞれのイベントが,

  • 持つ要素(Dragする対象)に記述されるイベントなのか
  • 落とされる要素(Dropされる対象)に記述されるイベントなのか

を分けて考えましょう.

ここではDrag itemDrop 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 itemDrop zoneに入った(乗った)ときDrop zoneに発火

Drag itemDrop 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 itemDrop 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