HTML ドラッグ&ドロップ API とは?
HTML ドラッグ & ドロップインターフェイスにより、アプリケーションはブラウザーでドラッグ&ドロップ機能を使用することができます。ユーザーはマウスでドラッグ可能な要素を選択し、その要素をドロップ可能な要素へドラッグし、マウスボタンを離すことでドロップすることができます。ドラッグ操作の間、ドラッグ可能な要素の半透明の表示がマウスポインターに続きます。
引用:HTML ドラッグ&ドロップ API - Web API _ MDN
実装してみる
まずはプレーンなHTML
とcss
のみで実装してみる。
JavaScript
による実装は何も無いので、この状態ではまだ要素をドラッグできるだけ
である。
See the Pen lesson1 by naoki-funawatari (@naoki-funawatari) on CodePen.
ドラッグされるアイテムの実装
まず、ドラッグされるアイテムの実装を行っていく。
-
draggable="true"
が設定された要素がドラッグの対象となる。
ドラッグの開始
- ドラッグが開始されたことを検知するためには
dragstart
イベントを実装する必要がある。 - ここでは、ドラッグされた要素に対して
dragging
クラスを設定している。
要素が半透明の状態になりドラッグ中であることが分かる。
ドラッグの終了
- ドラッグが終了したことを検知するためには
dragend
イベントを実装する必要がある。 - ドラッグ終了時に要素から
dragging
クラスを削除することで、半透明の状態が解除される。
ここまでのまとめ
- 以下が
dragstart
、dragend
イベントを実装した状態である。 - ドラッグ開始時に要素が半透明になり、ドラッグ終了時に要素の半透明が解除される。
See the Pen lesson2 by naoki-funawatari (@naoki-funawatari) on CodePen.
ドロップ先の実装
ここから、ドラッグされたアイテムがドロップされる先の実装を行っていく。
要素の重なり
- ドラッグアイテムがドロップ先と重なったことを検知するためには
dragenter
を実装する必要がある。 - ここでは、ドラッグされた要素に対して
over
クラスを設定している。 - 要素の背景色が淡黄色の状態になり要素が重なっていることが分かる。
要素の重なりを解除
- ドラッグアイテムとドロップ先が離れたことを検知するためには
dragleave
イベントを実装する必要がある。 - ドロップ先の要素から
over
クラスを削除することで、淡黄色の状態が解除される。
ここまでのまとめ
- 以下が
dragenter
、dragleave
イベントを実装した状態である。 - 要素が重なった際に背景色が淡黄色となり、要素が離れた際に背景色が元に戻る。
See the Pen lesson3 by naoki-funawatari (@naoki-funawatari) on CodePen.
ドロップ処理の実装
ここから、実際にドロップされたときの処理を実装していく。
ブラウザ既定の処理を変更
-
dragover
、drop
の各イベント内にevent.preventDefault()
メソッドの記述を追加する。 - そうすることでドラッグしている最中、ドロップ時に発生する
ブラウザ既定のイベント
をキャンセルできる。 - ドロップ先の要素にドロップゾーンとしての役割を持たせるために必須の処理となる。
- これは、例えばブラウザの外からファイルをドロップした際、
JavaScript
でファイルを受け取りたい場合などにも役に立つ。
この処理を実装しなかった場合、単純にブラウザで当該ファイルが展開されてしまう。
要素のドロップ
- 要素がドロップされたことを検知するためには
drop
イベントを実装する必要がある。 - ここでは、
ok, dropped!!
とアラートを表示している。 - 同時にドロップ先の要素から
over
クラスを削除することで、淡黄色の状態が解除される。 - ドロップ時には
dragleave
イベントは発生しないため、drop
イベント内でも同様処理が必要となる。
ここまでのまとめ
- 以下が
dragover
、drop
イベントを実装した状態である。 - ブラウザ既定のイベントを解除し、要素がドロップされた際に背景色が元に戻る。
See the Pen lesson4 by naoki-funawatari (@naoki-funawatari) on CodePen.
要素の移動を実装
- 要素の移動を実装するには
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()
メソッドによるデータの転送がメインとなる。
const item1 = document.querySelector("#item1");
const box2 = document.querySelector(".box2");
box2.appendChild(item1);
ここまでのまとめ
- 以下が
dragstart
、dragover
、drop
イベントを実装した状態である。 -
dragstart
イベントでドラッグ要素のid
を転送、
drop
イベントでid
を取得し、それを元に要素を取得、及びドロップ先へ要素の追加を行っている
See the Pen lesson5 by naoki-funawatari (@naoki-funawatari) on CodePen.
問題点
ここまでの実装でドラッグ&ドロップに関する実装は一通り完了したが、一つ問題が残っている。
ドロップ先の子要素への移動、つまりitem
からitem
へ移動できてしまう。
子要素への移動を制限
- 子要素への移動を制限するには
dragenter
、dragover
イベントをカスタマイズする必要がある。 - ドラッグした要素が
item
であればドラッグを許可しない、といった形でreturn
してしまう。
// 子要素へのドラッグを制限
if ([...e.target.classList].includes("item")) {
return;
}
// 子要素へのドラッグを制限
if ([...e.target.classList].includes("item")) {
// ドラッグ不可のドロップ効果を設定
e.dataTransfer.dropEffect = "none";
return;
}
ブラウザ外からのファイルのドロップを制限
ついでにやる。
- ブラウザ外からのファイルのドロップを制限するには
drop
イベントをカスタマイズする必要がある。 - ドロップされたファイル数が
0件
を超えていたら後続の処理を実行しない、といった形でreturn
してしまう。 -
event.preventDefault()
メソッドを実行しているためブラウザ上にファイルが展開されるといったことは発生しないが、event.dataTransfer.getData()
メソッドから取得したデータを元に要素の移動などを実行する処理で例外が発生する。
// ブラウザ外からのファイルドロップを制限
if (e.dataTransfer.files.length > 0) {
return;
}
おそらく完成
これで必要な機能は揃ったと思う。
See the Pen lesson6 by naoki-funawatari (@naoki-funawatari) on CodePen.
応用
ここまで要素の移動のみを実装してきた。
実装次第ではそれ以外のことももちろん可能。
要素の複製
すぐに思いつくのは要素の複製など。
コントロールキーを押下しながらドロップした場合に要素が複製される、といった形で実装してみる。
-
dragstart
イベント内のドロップ効果を変更する。
移動と複製ができるようにする。
event.dataTransfer.effectAllowed = "copyMove";
-
dragover
イベント内に分岐を追加し、ドロップ効果を切り替える。
// ドロップ効果の設定
if (event.ctrlKey) {
e.dataTransfer.dropEffect = "copy";
} else {
e.dataTransfer.dropEffect = "move";
}
-
drop
イベント内に分岐を追加し、移動処理と複製処理を切り替える。 -
cloneNode()
メソッドではどうやらイベントリスナーは引き継がれないようなので、個別に設定する。
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));
}
全部入り
See the Pen lesson7 by naoki-funawatari (@naoki-funawatari) on CodePen.
精神力がほぼ底を尽きかけたために後半はかなり適当。