【HTML】要素を置き場から置き場にドラッグ＆ドロップする (PC+スマートフォン両対応)

Web ページで要素を置き場から置き場にドラッグ＆ドロップしたいことがあると思いますが、ドラッグイベントを利用するとスマートフォンではイベント発火せず悲しいと思います。なのでスマートフォンではタッチイベントを利用して同様に動作するようにしました。

目新しいことはしていないし、タッチイベントでドラッグ＆ドロップするという記事は Web 上にいくつかあると思いますが、コードが断片的でコピペで動かなかったり、パソコンとスマートフォンの両方に対応していなかったりしたので記事にしたいと思います。

### 要約

PCでもスマートフォンでも以下のページのカード1とカード2が別の置き場にドラッグできます (PCではマウスで、スマートフォンでは指でドラッグ)[^1]。
[https://cookiebox26.github.io/games/misc/card_0.html](https://cookiebox26.github.io/games/misc/card_0.html)
<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177688/6dfb38c1-8ffa-cfdb-aad8-5adef3cbc0c1.png"/>

[^1]: PC+スマートフォン対応といっていますが、非タッチパネルPCとiPhoneでしか動作確認していないので、その他のデバイスで意図通りに動作しない可能性があります。

ソースは上のページのソースを表示すればみられますが以下です。
[https://github.com/CookieBox26/games/blob/main/misc/card_0.html](https://github.com/CookieBox26/games/blob/main/misc/card_0.html)

- `// ----- パソコン・スマートフォン共通 -----` の箇所のコードは両デバイス共通です。
- `// ----- パソコンでのドラッグドロップ用 -----` の箇所はパソコン用です。
- `// ----- スマートフォンでのドラッグドロップ用 -----` の箇所はスマートフォン用です。

### 参考文献
パソコン用コードについては大部分を以下の記事に倣っています。

https://qiita.com/naoki-funawatari/items/c78630b85f5f7fe21d19

<details><summary>今回書いたコードの上の記事との主な差分についてはここを展開してください。</summary>

- 上の記事の趣旨と違って、私の場合はボックス内にアイテム (カード) を1枚までしか置きたくなかったので、「先客のカードがいたらだめ」という判定があります。
- dragenter/dragover/dragleave/drop イベントリスナーをドロップ先のボックスたちでなくページ自体 (document) に設定しています。そのため「イベント発生元はカードを置けるボックスか」という判定が入っています。この変更をした理由は以下です (後者が主)。
  - コードが一元的になるのと、ボックスを動的に生成しても伝搬されると思います。
  - 画面外からファイルをドラッグされたとき画面内どこでも禁止カーソルにできます。
- ドラッグ中の要素の id を DataTransfer.setData/getData で設定/取得するのではなく、ドラッグ中の要素をグローバル変数 draggedCard に保存するようにしています。DataTransfer.setData/getData が本来運搬用メソッドなのでこちらをつかうのが行儀がよいと思いますが、どうせスマートフォン用にグローバル変数を立ててしまっているのと、運搬中の要素を割と参照するのでこの方がやりやすかったためです。 
</details>

### ソースの大まかな説明

#### パソコン用

パソコン用には以下のドラッグイベントでカード移動を実現しています ([ここ](https://github.com/CookieBox26/games/blob/6d316cc9a3f8c9746b998cd727c20ef515331287/misc/card_0.html#L71-L123))。

- カードをドラッグ開始したときのイベント handleDragEnter
- カードをドラッグ終了したときのイベント handleDragOver
- 要素の上に何かがドラッグされてきたときのイベント handleDragEnter
- 要素の上に何かがドラッグされているときのイベント handleDragOver
- 要素の上にドラッグされていた何かが去っていったときのイベント handleDragLeave
- 要素の上に何かが手放されたときのイベント handleDrop

詳細はコード参照ですが、
- handleDragEnter ではカードがドラッグされ始めたときにそのカードを半透明にして (ドラッグされ出したカードが半透明/透明にならずに鎮座し続けていると違和感があるので) かつそのカードをグローバル変数に保存し、
- handleDragOver では要素の上にドラッグされてきたのがカードであってその要素がカードを置けるボックスならここに置けると示すために強調表示するようにし、
- 最終的には handleDrop でカードを置けるボックスにカードがドロップされたときグローバル変数に保存してあったカードを appendChild してカード移動を実現しています。

#### スマートフォン用

タッチ動作が優先されるスマートフォンでは「この要素がドラッグされ出した」「この要素の上に何かがドラッグ/ドロップされてきた」というドラッグイベントは発火しないので、「指がこの座標を触った/通過した/放れた」というタッチイベントから同じようなことをすることになります ([ここ](https://github.com/CookieBox26/games/blob/6d316cc9a3f8c9746b998cd727c20ef515331287/misc/card_0.html#L125-L171))。

- 画面にタッチしたときのイベント handleTouchStart
- 画面にタッチしている指を動かしたときのイベント handleTouchMove
- 画面にタッチしている指を放したときのイベント handleTouchEnd

詳細はコード参照ですが、
- handleTouchStart でタッチ座標にカードがあれば半透明にしてグローバル変数に保存し、
- handleTouchMove でタッチ座標がカードを置けるボックスを通過したら強調表示し、
- 最終的には handleTouchEnd でタッチ終了座標がカードを置けるボックスならグローバル変数に保存してあったカードを appendChild してカード移動を実現しています。

#### スマートフォン用のドラッグプレビューについて

スマートフォン用では dragPreview というグローバル変数を立てています。これはパソコンでいう何かをドラッグしているときにマウスに追従してくる画像 (ドラッグプレビュー) を実現するためのものです。

パソコン用サイトでは何かをドラッグすれば自動的にドラッグプレビューが表示されますが[^2]、そもそもスマートフォンのようなタッチデバイスではドラッグ動作が想定されないのでタッチイベントでカード移動しているわけで、自分で実装しなければドラッグプレビューは出ません。

なので、指を画面に付けて動かしている間、指が触っている座標を中心に半透明のカードを表示させています (スタイルシートの `.drag-preview` の箇所がそれで、`transform: translate(-50%, -50%);` としています)。なお、`display: none;` と初期値で非表示にしているのは、カードが動き始めてからプレビュー表示しないとカードをタッチした瞬間に変な場所にプレビューが出るためです (タッチした瞬間に座標を調整してもよいですが)。

[^2]: [DataTransfer.setDragImage() - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/DataTransfer/setDragImage) 「半透明の画像が生成され、ドラッグ中にマウスポインターに沿って移動します。この画像は自動的に作成されるので、自分で作成する必要はありません。ただし、カスタム画像が必要な場合は、DataTransfer.setDragImage() メソッドを使用して、使用するカスタム画像を設定することができます。」

### 備考

パソコン用サイトで自動的に生成されるドラッグプレビューの不透明度はたぶん 0.75 だと思います。もっと透明がよいとかもっと不透明がよいとか別の画像にしたいとかの場合は [^2] を参照すれば変更できると思います。私は移動中のカードが幽霊のようになってしまうのは変だと思うので完全に不透明にしたいと一度は思いましたが、カードの下がやや透けて置く場所がみえやすいのはそれはそれで親切かと思い直し変更していません。