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

# はじめに

ナイトウ([@engineer_naito](https://twitter.com/engineer_naito))と申します。

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

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

# 対象となるコード

```html: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>
```

```css:stylee.css
.draggable-list {
  list-style-type: none;
}

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

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

# HTMLのドラッグ&ドロップ

https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API

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

ユーザーがドラッグ可能な要素を選択
↓
ドロップ可能な要素へドラッグしたときに継続
↓
要素を離して終了

となっています。

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

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

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

# 実装

## はじめに

まずはじめに、そのHTML要素がドラッグ可能な要素であるかどうか(draggable)を設定する必要があります。 
要素をdraggableにするには `draggable` 属性を追加します。([draggable属性について](https://developer.mozilla.org/ja/docs/Web/HTML/Global_attributes/draggable))

```html
  <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` イベント)

https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/dragstart_event

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

を使用します。

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

https://developer.mozilla.org/ja/docs/Web/API/DataTransfer

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

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

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

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

https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/drop_event

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

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

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

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

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

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

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

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

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

https://developer.mozilla.org/ja/docs/Web/API/Node/insertBefore

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

## 完成...?

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

```html: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>
```

```js: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` イベントを追加して完成したコードが以下です。

```js: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

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

![exmaple_by_vuedraggabe](https://raw.githubusercontent.com/SortableJS/Vue.Draggable/master/example.gif)

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

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

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

# 最後に

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

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

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