この記事の目的
配列の要素をWebページに表示し、それらをドラッグ&ドロップすることで配列の要素を並べ替えることができるようにする。
配列の要素をWebページに表示する
まずは、配列の要素をWebページに表示する。
後に、ドラッグ&ドロップにより配列の最後に移動できるようにするため、最後の要素の後に1要素分空白を作る。
表示するためのDOM要素を入れる要素を用意する。
<div id="ddlistarea"></div>
適当に枠をつける。
後で使うため、表示するための要素は div
を二重にし、枠は内側の要素につける。
#ddlistarea {
border: 1px solid black;
padding: 2px;
display: inline-grid;
width: 10em;
}
#ddlistarea > div {
min-height: 1em;
text-align: center;
box-sizing: border-box;
}
#ddlistarea > div:not(:last-child) {
padding-bottom: 2px;
}
#ddlistarea > div:not(:last-child) > div {
border: 1px solid black;
}
配列と、その中身をDOM要素に入れるコードを用意する。
使い回せるDOM要素は使い回すようにしてもいいかもしれないが、今回は簡単のため毎回全要素を撤去して全体を作り直す。
const elemList = ["A", "B", "C", "D", "E"];
const area = document.getElementById("ddlistarea");
function render() {
while (area.firstChild) area.removeChild(area.firstChild);
for (let i = 0; i <= elemList.length; i++) {
const elem = document.createElement("div");
if (i < elemList.length) {
const labelElem = document.createElement("div");
labelElem.textContent = elemList[i];
elem.appendChild(labelElem);
}
area.appendChild(elem);
}
}
render();
ドラッグ&ドロップができるようにする
ドラッグ操作 - Web API | MDN
に沿って、要素をドラッグ&ドロップできるようにする。
ドラッグされる側
ドラッグされる側としてのセットアップは、一番下の空白を表す要素には行わず、それ以外の要素 (配列の要素を表す要素) に対して行う。
まず、要素に draggable="true"
属性を設定する。
elem.setAttribute("draggable", "true");
そして、ドラッグが開始されたことを表す dragstart
イベントで、以下を行う。
-
setData
メソッドにより、ドロップされる側に渡すデータを設定する -
effectAllowed
により、許可するドロップ時の操作を設定する
今回は、text/x-list-index
としてドラッグされる要素 (すなわち、移動する要素) の位置を渡す。
また、移動のみを許可し、コピーおよびリンクは許可しない。
elem.addEventListener("dragstart", (event) => {
event.dataTransfer.setData("text/x-list-index", elemIndex.toString());
event.dataTransfer.effectAllowed = "move";
});
ドロップされる側
ドラッグされる側としてのセットアップは、一番下の空白を表す要素を含むすべての要素に対して行う。
以下のイベントのハンドラーを登録する。
イベント | 発生する場面 |
---|---|
dragenter |
ドラッグ中のカーソルが要素内に入った |
dragover |
ドラッグ中のカーソルが要素内にある |
dragleave |
ドラッグ中のカーソルがドロップせずに要素内から出た |
drop |
ドラッグ中のカーソルが要素内でドロップをした |
dragenter
イベントでは、今回用いる text/x-list-index
のデータがあるかを確認し、ある場合は以下を行う。
- ドロップを許可するため、
event.preventDefault();
を呼び出す - ドロップ先が配列の要素を表す要素の場合は、その前に挿入されることを表すため、
draggedover
クラスをつける
draggedover
クラスをつけると、CSSの設定により、要素上部のパディングが増える。
このパディングによって要素を囲む枠を大きくするのではなくずらすため、当たり判定となる外側の div
と枠を表示する内側の div
に分けた構造とした。
ただし、今回の構造では、外側の div
内にカーソルがある状態でも、内側の div
にカーソルが出入りすると、外側と内側それぞれの div
について dragenter
イベントや dragleave
イベントが発生するようであった。
そこで、カーソルの出入りをカウントする変数を用い、クラスの設定が適切に行えるようにした。
elem.addEventListener("dragenter", (event) => {
if (event.dataTransfer.types.includes("text/x-list-index")) {
event.preventDefault();
if (i < elemList.length) {
elem.classList.add("draggedover");
enterCount++;
}
}
});
#ddlistarea > div.draggedover {
padding-top: 0.5em;
}
dragover
イベントは、ドラッグ中のカーソルがドロップ先の上にある間短い間隔で発生し、カーソルがある位置を用いたフィードバックの描画などに役立つ。
今回は、そのような細かい描画は行わないが、ドロップを許可するために event.preventDefault();
を呼び出すことが求められるので、呼び出す。
elem.addEventListener("dragover", (event) => {
if (event.dataTransfer.types.includes("text/x-list-index")) {
event.preventDefault();
}
});
dragleave
イベントは、ドラッグ中のカーソルがドロップせずにドロップ先を離れたとき発生する。
このとき、カーソルの出入りのカウントを行い、完全に要素を出たと判断したら draggedover
クラスを取り除く。
elem.addEventListener("dragleave", (event) => {
if (event.dataTransfer.types.includes("text/x-list-index")) {
if (enterCount > 0 && --enterCount <= 0) elem.classList.remove("draggedover");
}
});
dragleave
イベントは、ドラッグ中のカーソルがドロップ先を離れずにドロップをすることでドラッグが終了した場合、発生しないようである。
そのため、ドロップ時のイベント drop
においても、このカウント処理が必要である。
drop
イベントは、ドラッグ中のカーソルがドロップをしたとき発生する。
今回用いる text/x-list-index
のデータがある場合、以下を行う。
- ドロップのデフォルト動作 (ページの移動など) をキャンセルする
- カーソルの出入りのカウントを更新する
-
text/x-list-index
のデータを読み取り、有効であれば配列の更新と再描画を行う
elem.addEventListener("drop", (event) => {
if (event.dataTransfer.types.includes("text/x-list-index")) {
event.preventDefault();
if (enterCount > 0 && --enterCount <= 0) elem.classList.remove("draggedover");
const fromIndex = parseInt(event.dataTransfer.getData("text/x-list-index"), 10);
if (!isNaN(fromIndex) && 0 <= fromIndex && fromIndex < elemList.length) {
// fromIndex の要素を、この要素 (elemIndex) の直前に挿入する
const [data] = elemList.splice(fromIndex, 1);
elemList.splice(elemIndex - (fromIndex < elemIndex ? 1 : 0), 0 ,data);
// 再描画を行う
render();
}
}
});
コード全体・デモ
<div id="ddlistarea"></div>
#ddlistarea {
border: 1px solid black;
padding: 2px;
display: inline-grid;
width: 10em;
}
#ddlistarea > div {
min-height: 1em;
text-align: center;
box-sizing: border-box;
}
#ddlistarea > div:not(:last-child) {
padding-bottom: 2px;
}
#ddlistarea > div:not(:last-child) > div {
border: 1px solid black;
}
#ddlistarea > div.draggedover {
padding-top: 0.5em;
}
const elemList = ["A", "B", "C", "D", "E"];
const area = document.getElementById("ddlistarea");
function render() {
while (area.firstChild) area.removeChild(area.firstChild);
for (let i = 0; i <= elemList.length; i++) {
const elemIndex = i;
const elem = document.createElement("div");
let enterCount = 0;
if (i < elemList.length) {
const labelElem = document.createElement("div");
labelElem.textContent = elemList[i];
elem.appendChild(labelElem);
elem.setAttribute("draggable", "true");
elem.addEventListener("dragstart", (event) => {
event.dataTransfer.setData("text/x-list-index", elemIndex.toString());
event.dataTransfer.effectAllowed = "move";
});
}
elem.addEventListener("dragenter", (event) => {
if (event.dataTransfer.types.includes("text/x-list-index")) {
event.preventDefault();
if (i < elemList.length) {
elem.classList.add("draggedover");
enterCount++;
}
}
});
elem.addEventListener("dragover", (event) => {
if (event.dataTransfer.types.includes("text/x-list-index")) {
event.preventDefault();
}
});
elem.addEventListener("dragleave", (event) => {
if (event.dataTransfer.types.includes("text/x-list-index")) {
if (enterCount > 0 && --enterCount <= 0) elem.classList.remove("draggedover");
}
});
elem.addEventListener("drop", (event) => {
if (event.dataTransfer.types.includes("text/x-list-index")) {
event.preventDefault();
if (enterCount > 0 && --enterCount <= 0) elem.classList.remove("draggedover");
const fromIndex = parseInt(event.dataTransfer.getData("text/x-list-index"), 10);
if (!isNaN(fromIndex) && 0 <= fromIndex && fromIndex < elemList.length) {
// fromIndex の要素を、この要素 (elemIndex) の直前に挿入する
const [data] = elemList.splice(fromIndex, 1);
elemList.splice(elemIndex - (fromIndex < elemIndex ? 1 : 0), 0 ,data);
// 再描画を行う
render();
}
}
});
area.appendChild(elem);
}
}
render();
See the Pen array drag & drop demo by MikeCAT (@mike_cat) on CodePen.
まとめ
JavaScriptの配列の要素をDOM要素を用いてWebページに表示し、ドラッグ&ドロップ関係の属性やイベントハンドラーを設定することで、ドラッグ&ドロップにより配列の要素の並べ替え操作を行うことができるようにできた。