2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Web】配列の要素をドラッグ&ドロップで並べ替えられるようにする

Posted at

この記事の目的

配列の要素をWebページに表示し、それらをドラッグ&ドロップすることで配列の要素を並べ替えることができるようにする。

要素の並べ替え

配列の要素をWebページに表示する

まずは、配列の要素をWebページに表示する。
後に、ドラッグ&ドロップにより配列の最後に移動できるようにするため、最後の要素の後に1要素分空白を作る。

表示するためのDOM要素を入れる要素を用意する。

HTML
<div id="ddlistarea"></div>

適当に枠をつける。
後で使うため、表示するための要素は div を二重にし、枠は内側の要素につける。

CSS
#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要素は使い回すようにしてもいいかもしれないが、今回は簡単のため毎回全要素を撤去して全体を作り直す。

JavaScript
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" 属性を設定する。

JavaScript
elem.setAttribute("draggable", "true");

そして、ドラッグが開始されたことを表す dragstart イベントで、以下を行う。

  • setData メソッドにより、ドロップされる側に渡すデータを設定する
  • effectAllowed により、許可するドロップ時の操作を設定する

今回は、text/x-list-index としてドラッグされる要素 (すなわち、移動する要素) の位置を渡す。
また、移動のみを許可し、コピーおよびリンクは許可しない。

JavaScript
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 イベントが発生するようであった。
そこで、カーソルの出入りをカウントする変数を用い、クラスの設定が適切に行えるようにした。

JavaScript
elem.addEventListener("dragenter", (event) => {
	if (event.dataTransfer.types.includes("text/x-list-index")) {
		event.preventDefault();
		if (i < elemList.length) {
			elem.classList.add("draggedover");
			enterCount++;
		}
	}
});
CSS
#ddlistarea > div.draggedover {
	padding-top: 0.5em;
}

dragover イベントは、ドラッグ中のカーソルがドロップ先の上にある間短い間隔で発生し、カーソルがある位置を用いたフィードバックの描画などに役立つ。
今回は、そのような細かい描画は行わないが、ドロップを許可するために event.preventDefault(); を呼び出すことが求められるので、呼び出す。

JavaScript
elem.addEventListener("dragover", (event) => {
	if (event.dataTransfer.types.includes("text/x-list-index")) {
		event.preventDefault();
	}
});

dragleave イベントは、ドラッグ中のカーソルがドロップせずにドロップ先を離れたとき発生する。
このとき、カーソルの出入りのカウントを行い、完全に要素を出たと判断したら draggedover クラスを取り除く。

JavaScript
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 のデータを読み取り、有効であれば配列の更新と再描画を行う
JavaScript
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();
		}
	}
});

コード全体・デモ

HTML
<div id="ddlistarea"></div>
CSS
#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;
}
JavaScript
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ページに表示し、ドラッグ&ドロップ関係の属性やイベントハンドラーを設定することで、ドラッグ&ドロップにより配列の要素の並べ替え操作を行うことができるようにできた。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?