2
1

【VanillaJS】ドラッグ&ドロップで要素を並び替える方法(1次元方向)

Last updated at Posted at 2024-02-26

はじめに

ナイトウ(@engineer_naito)と申します。

ドラッグして要素の並び替えする方法について今回は解説していきます。

今回は1次元方向のみの場合です。
(また、見やすさのため li 要素を垂直方向に並べています。)
グリッド状(2次元)の並び替えはこの記事では扱いません。

対象となるコード

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <ul class="draggable-list">
      <li id="item1">項目 1</li>
      <li id="item2">項目 2</li>
      <li id="item3">項目 3</li>
      <li id="item4">項目 4</li>
      <li id="item5">項目 5</li>
    </ul>
  </body>
</html>
stylee.css
.draggable-list {
  list-style-type: none;
}

.draggable-list li {
  margin: 10px;
  padding: 10px;
  border: 1px solid #ddd;
}

今回は上記コードについて、 ul.draggable-list の子要素である li の表示順を入れ替える機能を実装します。

HTMLのドラッグ&ドロップ

を見ると、典型的なドラッグ操作は

ユーザーがドラッグ可能な要素を選択

ドロップ可能な要素へドラッグしたときに継続

要素を離して終了

となっています。

これを今回のケースに当てはめると、

  1. li 要素をドラッグ
  2. 並び替え先の要素で離す

となります。
とりあえずのところまで勢いで実装していってしまいましょう。

実装

はじめに

まずはじめに、そのHTML要素がドラッグ可能な要素であるかどうか(draggable)を設定する必要があります。
要素をdraggableにするには draggable 属性を追加します。(draggable属性について)

  <ul class="draggable-list">
    <li id="item1" draggable="true">項目 1</li>
    <li id="item2" draggable="true">項目 2</li>
    <li id="item3" draggable="true">項目 3</li>
  </ul>

これで li 要素がdraggableとなりました。
次にドラッグし始めた際に、どの要素をドラッグしたのかを保持する仕組みを実装します。

li 要素をドラッグ(dragstart イベント)

ondragstart = (event) => {
  // ここに処理を記述
};

を使用します。

ドラッグ&ドロップ操作の間にデータを転送(保持)する仕組みがあります。
それが dataTransfer プロパティです。

setData() メソッドを利用して、ドラッグされた要素の id を保持しましょう。

ondragstart = (event) => {
  event.dataTransfer.setData('text/plain', event.target.id);
};

次に、ドロップした際に並び替える処理を書いていきます。

並び替え先の要素で離す(drop イベント)

ondrop = (event) => {
  // ここに処理を記述
};

まずは、どの要素がドラッグされたのかを取得しましょう。
ドラッグされた要素の id はすでに指定してあるので、 dataTransfer.getData() で取得できます。

ondrop = (event) => {
  // ドロップできるように規定の動作を停止
  event.preventDefault();

  const draggedElementId = event.dataTransfer.getData('text');
  const draggedElement = document.getElementById(draggedElementId);
};

次に、並び替えのためにドラッグされた要素の表示順の番号とドロップ先の要素の表示順をそれぞれ取得します。

  const listElements = [...event.target.parentNode.children];
  const targetIndex = listElements.indexOf(event.target);
  const draggedElementIndex = listElements.indexOf(draggedElement);

最後に並び替えを行います。

  const insertionPoint = draggedElementIndex < targetIndex ? event.target.nextSibling : event.target;
  event.target.parentNode.insertBefore(draggedElement, insertionPoint);

insertBefore() はその名の通り、何かの前に挿入することを指しています。
insertAfter() は存在しません。
何かの後への挿入の際には nextSibling プロパティを活用する必要があります。

あとはこれまで書いてきたイベントハンドラープロパティを ul.draggable-list の子要素である li 全てに適用してやればよさそうです。
動かしてみましょう。

完成...?

出来上がったコードは以下のようになります。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <link rel="stylesheet" type="text/css" href="style.css" />
  <script src="index.js" async></script>
</head>
<body>
  <ul class="draggable-list">
    <li id="item1" draggable="true">項目 1</li>
    <li id="item2" draggable="true">項目 2</li>
    <li id="item3" draggable="true">項目 3</li>
    <li id="item4" draggable="true">項目 4</li>
    <li id="item5" draggable="true">項目 5</li>
  </ul>
</body>
</html>
index.js
document.querySelector('.draggable-list').querySelectorAll('li').forEach(element => {
  element.ondragstart = (event) => {
    event.dataTransfer.setData('text/plain', event.target.id);
  };

  element.ondrop = (event) => {
    event.preventDefault();

    const draggedElementId = event.dataTransfer.getData('text');
    const draggedElement = document.getElementById(draggedElementId);

    const listElements = [...event.target.parentNode.children];
    const targetIndex = listElements.indexOf(event.target);
    const draggedElementIndex = listElements.indexOf(draggedElement);

    const insertionPoint = draggedElementIndex < targetIndex ? event.target.nextSibling : event.target;
    event.target.parentNode.insertBefore(draggedElement, insertionPoint);
  };
});

しかし、このままでは動きません。
ドロップできるようにするため、 dragover イベントで規定の動作を停止する必要があります。

完成

dragover イベントを追加して完成したコードが以下です。

index.js
document.querySelector('.draggable-list').querySelectorAll('li').forEach(element => {
  element.ondragstart = (event) => {
    event.dataTransfer.setData('text/plain', event.target.id);
  };

  element.ondragover = (event) => { 
  	event.preventDefault(); 
  };

  element.ondrop = (event) => {
    event.preventDefault();

    const draggedElementId = event.dataTransfer.getData('text');
    const draggedElement = document.getElementById(draggedElementId);

    const listElements = [...event.target.parentNode.children];
    const targetIndex = listElements.indexOf(event.target);
    const draggedElementIndex = listElements.indexOf(draggedElement);

    const insertionPoint = draggedElementIndex < targetIndex ? event.target.nextSibling : event.target;
    event.target.parentNode.insertBefore(draggedElement, insertionPoint);
  };
});

課題

UI/UX面に課題があります。
一般的なドラッグによる並び替え機能においてドロップ先が明確で、並び替え後の順番がとてもわかりやすく親切で使いやすいです。

c.f. https://github.com/SortableJS/Vue.Draggable

exmaple_by_vuedraggabe

上記vue.draggableでは、ドロップ前にすでに並び替えされたイメージが表示されています。
要素と要素の間にドラッグした要素を挿入しようとすると、それらを押しのけるような見た目にもなっています。

今回はグリッド状の並び替えについても実装できておりません。

また、今回はもともとある li 要素に対してのみドラッグ&ドロップを行う機能でしたが、あとから li 要素を追加する機能があることもあります。

最後に

今後もVanillaJSについて勉強していくつもりなので、上記課題について進捗あれば記事にしたいと思います。
フレームワークの偉大さを実感してきました。
もっとフレームワークに感謝できるように、フレームワークを利用して作ることもやりたいです。

何かご指摘などございましたら、コメントなどいただけるとありがたいです。
よろしくお願いします。

最後まで読んでいただきありがとうございました!

2
1
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
1