【HTML】ドラッグ＆ドロップについて復習する【標準API】

# HTML ドラッグ＆ドロップ API とは？
>HTML ドラッグ & ドロップインターフェイスにより、アプリケーションはブラウザーでドラッグ＆ドロップ機能を使用することができます。ユーザーはマウスでドラッグ可能な要素を選択し、その要素をドロップ可能な要素へドラッグし、マウスボタンを離すことでドロップすることができます。ドラッグ操作の間、ドラッグ可能な要素の半透明の表示がマウスポインターに続きます。

引用：[HTML ドラッグ＆ドロップ API - Web API _ MDN](https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API)

# 実装してみる
まずはプレーンな`HTML`と`css`のみで実装してみる。
`JavaScript`による実装は何も無いので、この状態ではまだ`要素をドラッグできるだけ`である。
<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="html,result" data-user="naoki-funawatari" data-slug-hash="dyYWKRa" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="lesson1">
  <span>See the Pen <a href="https://codepen.io/naoki-funawatari/pen/dyYWKRa">
  lesson1</a> by naoki-funawatari (<a href="https://codepen.io/naoki-funawatari">@naoki-funawatari</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# ドラッグされるアイテムの実装
まず、ドラッグされるアイテムの実装を行っていく。

* `draggable="true"`が設定された要素がドラッグの対象となる。

## ドラッグの開始
* ドラッグが開始されたことを検知するためには`dragstart`イベントを実装する必要がある。
* ここでは、ドラッグされた要素に対して`dragging`クラスを設定している。
要素が半透明の状態になりドラッグ中であることが分かる。

## ドラッグの終了
* ドラッグが終了したことを検知するためには`dragend`イベントを実装する必要がある。
* ドラッグ終了時に要素から`dragging`クラスを削除することで、半透明の状態が解除される。

### ここまでのまとめ
* 以下が`dragstart`、`dragend`イベントを実装した状態である。
* ドラッグ開始時に要素が半透明になり、ドラッグ終了時に要素の半透明が解除される。
<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="html,result" data-user="naoki-funawatari" data-slug-hash="gOaWKob" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="lesson2">
  <span>See the Pen <a href="https://codepen.io/naoki-funawatari/pen/gOaWKob">
  lesson2</a> by naoki-funawatari (<a href="https://codepen.io/naoki-funawatari">@naoki-funawatari</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# ドロップ先の実装
ここから、ドラッグされたアイテムがドロップされる先の実装を行っていく。

## 要素の重なり
* ドラッグアイテムがドロップ先と重なったことを検知するためには`dragenter`を実装する必要がある。
* ここでは、ドラッグされた要素に対して`over`クラスを設定している。
* 要素の背景色が淡黄色の状態になり要素が重なっていることが分かる。

## 要素の重なりを解除
* ドラッグアイテムとドロップ先が離れたことを検知するためには`dragleave`イベントを実装する必要がある。
* ドロップ先の要素から`over`クラスを削除することで、淡黄色の状態が解除される。

### ここまでのまとめ
* 以下が`dragenter`、`dragleave`イベントを実装した状態である。
* 要素が重なった際に背景色が淡黄色となり、要素が離れた際に背景色が元に戻る。
<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="html,result" data-user="naoki-funawatari" data-slug-hash="dyYWKqE" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="lesson3">
  <span>See the Pen <a href="https://codepen.io/naoki-funawatari/pen/dyYWKqE">
  lesson3</a> by naoki-funawatari (<a href="https://codepen.io/naoki-funawatari">@naoki-funawatari</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# ドロップ処理の実装
ここから、実際にドロップされたときの処理を実装していく。

## ブラウザ既定の処理を変更
* `dragover`、`drop`の各イベント内に`event.preventDefault()`メソッドの記述を追加する。
* そうすることでドラッグしている最中、ドロップ時に発生する`ブラウザ既定のイベント`をキャンセルできる。
* ドロップ先の要素にドロップゾーンとしての役割を持たせるために必須の処理となる。
* これは、例えばブラウザの外からファイルをドロップした際、`JavaScript`でファイルを受け取りたい場合などにも役に立つ。
この処理を実装しなかった場合、単純にブラウザで当該ファイルが展開されてしまう。

## 要素のドロップ
* 要素がドロップされたことを検知するためには`drop`イベントを実装する必要がある。
* ここでは、`ok, dropped!!`とアラートを表示している。
* 同時にドロップ先の要素から`over`クラスを削除することで、淡黄色の状態が解除される。
* ドロップ時には`dragleave`イベントは発生しないため、`drop`イベント内でも同様処理が必要となる。

### ここまでのまとめ
* 以下が`dragover`、`drop`イベントを実装した状態である。
* ブラウザ既定のイベントを解除し、要素がドロップされた際に背景色が元に戻る。
<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="html,result" data-user="naoki-funawatari" data-slug-hash="GRpmBZj" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="lesson4">
  <span>See the Pen <a href="https://codepen.io/naoki-funawatari/pen/GRpmBZj">
  lesson4</a> by naoki-funawatari (<a href="https://codepen.io/naoki-funawatari">@naoki-funawatari</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# 要素の移動を実装
* 要素の移動を実装するには`dragstart`、`dragover`、`drop`イベントをカスタマイズする必要がある。

## ドロップ効果の設定
* `dragstart`イベントの`event.dataTransfer.effectAllowed`プロパティ、
`dragover`イベントの`event.dataTransfer.dropEffect`プロパティに対し、
`copy`、`move`、`link`、`none`、`etc...`などいずれかの設定値を指定する。
* ドラッグ要素、ドロップ先のそれぞれに指定したドロップ効果が一致している場合のみ、ドロップ処理が実行される。
今回は`move`を指定する。

## データの受け渡し
* `dragstart`イベントの`event.dataTransfer.setData()`メソッドで転送するデータを設定する。
* `drop`イベントの`event.dataTransfer.getData()`メソッドでデータを取得する。
* `setData()`、`getData()`メソッドにはそれぞれ`MIMEタイプ`の指定が必要であり、ドロップ効果と同様に一致させておく必要がある。
* 例としては`"text/plain"`、`"application/json"`などがある。
* 今回は`"application/json"`を指定し、`json`形式で要素の`id`を転送する。

## 要素の移動
* 転送されたドラッグ要素の`id`を元に`document.getElementById`メソッドで要素を取得し、
ドロップ先に`appendChild`メソッドで要素を追加することで要素の移動が実装完了となる。
* 移動元から要素を削除する、といった処理は不要。

* ちなみに`appendChild`メソッドによる要素の移動は、本記事の`draggable`とは無関係であり、通常の`JavaScript`の挙動である。
* `draggable`によって実現される機能としては、前述の各イベントや、`setData()`、`getData()`メソッドによるデータの転送がメインとなる。

```javascript:要素の移動自体は単純にJavaScriptの機能
const item1 = document.querySelector("#item1");
const box2 = document.querySelector(".box2");
box2.appendChild(item1);
```

### ここまでのまとめ
* 以下が`dragstart`、`dragover`、`drop`イベントを実装した状態である。
* `dragstart`イベントでドラッグ要素の`id`を転送、
`drop`イベントで`id`を取得し、それを元に要素を取得、及びドロップ先へ要素の追加を行っている
<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="html,result" data-user="naoki-funawatari" data-slug-hash="NWGjLoK" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="lesson5">
  <span>See the Pen <a href="https://codepen.io/naoki-funawatari/pen/NWGjLoK">
  lesson5</a> by naoki-funawatari (<a href="https://codepen.io/naoki-funawatari">@naoki-funawatari</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# 問題点
ここまでの実装でドラッグ＆ドロップに関する実装は一通り完了したが、一つ問題が残っている。
ドロップ先の子要素への移動、つまり`item`から`item`へ移動できてしまう。

## 子要素への移動を制限
* 子要素への移動を制限するには`dragenter`、`dragover`イベントをカスタマイズする必要がある。
* ドラッグした要素が`item`であればドラッグを許可しない、といった形で`return`してしまう。

```javascript:dragenterイベント
// 子要素へのドラッグを制限
if ([...e.target.classList].includes("item")) {
  return;
}
```

```javascript:dragoverイベント
// 子要素へのドラッグを制限
if ([...e.target.classList].includes("item")) {
  // ドラッグ不可のドロップ効果を設定
  e.dataTransfer.dropEffect = "none";
  return;
}
```

## ブラウザ外からのファイルのドロップを制限
ついでにやる。

* ブラウザ外からのファイルのドロップを制限するには`drop`イベントをカスタマイズする必要がある。
* ドロップされたファイル数が`0件`を超えていたら後続の処理を実行しない、といった形で`return`してしまう。
* `event.preventDefault()`メソッドを実行しているためブラウザ上にファイルが展開されるといったことは発生しないが、`event.dataTransfer.getData()`メソッドから取得したデータを元に要素の移動などを実行する処理で例外が発生する。

```javascript:dropイベント
// ブラウザ外からのファイルドロップを制限
if (e.dataTransfer.files.length > 0) {
  return;
}
```

# おそらく完成
これで必要な機能は揃ったと思う。

<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="html,result" data-user="naoki-funawatari" data-slug-hash="bGVWmyq" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="lesson6">
  <span>See the Pen <a href="https://codepen.io/naoki-funawatari/pen/bGVWmyq">
  lesson6</a> by naoki-funawatari (<a href="https://codepen.io/naoki-funawatari">@naoki-funawatari</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# 応用
ここまで要素の移動のみを実装してきた。
実装次第ではそれ以外のことももちろん可能。

## 要素の複製
すぐに思いつくのは要素の複製など。
コントロールキーを押下しながらドロップした場合に要素が複製される、といった形で実装してみる。

* `dragstart`イベント内のドロップ効果を変更する。
移動と複製ができるようにする。

```javascript
event.dataTransfer.effectAllowed = "copyMove";
```

* `dragover`イベント内に分岐を追加し、ドロップ効果を切り替える。

```javascript
// ドロップ効果の設定
if (event.ctrlKey) {
  e.dataTransfer.dropEffect = "copy";
} else {
  e.dataTransfer.dropEffect = "move";
}
```

* `drop`イベント内に分岐を追加し、移動処理と複製処理を切り替える。
* `cloneNode()`メソッドではどうやらイベントリスナーは引き継がれないようなので、個別に設定する。

```javascript
if (event.ctrlKey) {
  // 要素の複製
  const oldItem = document.getElementById(id);
  const newItem = oldItem.cloneNode(true);
  const newId = `item${[...document.querySelectorAll(".item")].length + 1}`;
  newItem.id = newId;
  newItem.classList.remove("dragging");

  // cloneNode() で引き継げない要素
  newItem.addEventListener("dragstart", handleDragStart, false);
  newItem.addEventListener("dragend", handleDragEnd, false);

  // ドロップ先に要素を追加する
  e.target.appendChild(newItem);
} else {
  // 要素の移動
  // ドロップ先に要素を追加する
  e.target.appendChild(document.getElementById(id));
}
```

## 全部入り
<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="html,result" data-user="naoki-funawatari" data-slug-hash="NWGjEoB" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="lesson7">
  <span>See the Pen <a href="https://codepen.io/naoki-funawatari/pen/NWGjEoB">
  lesson7</a> by naoki-funawatari (<a href="https://codepen.io/naoki-funawatari">@naoki-funawatari</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>


精神力がほぼ底を尽きかけたために後半はかなり適当。