htmlのli要素をドラッグして順序を変更できるようにする

# はじめに
Chromeの機能拡張を作っていて、`li`要素を変更できる機能を実装しようと思いました。
その部分だけを抜き出して説明します。

ソースは下記にあります。（この記事中ですべて説明できていますが、一応）

https://github.com/yo16/html_li_draggable_prac

動作サンプルと最低限必要な全ソースはこちら

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="gOyNGGM" data-user="yo16" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/yo16/pen/gOyNGGM">
  draggable_li</a> by yoichiro ikeda (<a href="https://codepen.io/yo16">@yo16</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


# 手順
## １．各`li`に、`draggable="true"`を設定する

```html:draggable_li.html（抜粋）
<ul id="myUl">
    <li draggable="true" style="background-color:#fcc">list item1 赤</li>
    <li draggable="true" style="background-color:#ffc">list item2 黄</li>
    <li draggable="true" style="background-color:#ccf">list item3 青</li>
</ul>
```
そのままですが、`li`に`draggable="true"`を追加します。

styleは、ドラッグを視覚的にわかりやすくしているだけです。


## ２．`ul`にドラッグ系の３イベントを追加する

３イベントとは、`dragstart`と`dragover`と`dragend`です。

### 2.1. 初期化
ドラッグ対象の`ul`を捕まえておきます。
`draggingElm`は、`li`が入る予定の変数です。

```JavaScript
const list = document.getElementById('myUl');
let draggingElm = null;    // ドラッグしているli要素
```

### 2.2. `dragstart`ハンドラーを追加
`ul`に`dragstart`イベントのハンドラーを追加します。「とある要素のドラッグを始めた！」というイベントです。
```JavaScript
list.addEventListener('dragstart', function(e) {
    draggingElm = e.target;
    //console.log(draggingElm); // つかんだli要素
    e.dataTransfer.effectAllowed = 'move';
});
```

`e.dataTransfer.effectAllowed = 'move';`は、moveを許可するという意味です。デフォルトはallになっていてmoveに絞るのですが、デフォルトで動作するのであえて絞る意図はあまりないかもしれません。

### 2.3. `dragover`ハンドラーを追加
次は`dragover`です。こちらは「何かが上に来た！」というイベントです。`dragstart`とはイベントを起こす要素が異なっていて、受け側になります。
```JavaScript
// ドラッグしている要素が何かの上にoverしたときのハンドラ
// ここでeは、ドラッグして覆いかぶされた要素＝targetとしている
// 最初はドラッグ直後なので自分自身で、マウスを移動すると変わる
list.addEventListener('dragover', function(e) {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';

    const target = e.target;
    //console.log(target);
    // 自分でもなく、li要素の場合に変更する
    if (target && target !== draggingElm && target.nodeName === 'LI') {
        const rect = target.getBoundingClientRect();
        // 0.5より小さい=マウスが上半分にある=false, 0.5より大きい=下半分にある=true
        const next = (e.clientY - rect.top) / (rect.bottom - rect.top) > 0.5;
        // ドラッグしているものの上半分にある場合は、次の要素のbeforeにinsert
        // 下半分にある場合は、今の要素のbeforeにinsert
        // 一番下の場合はnextSiblingはnullで、nullをinsertBeforeに渡すと最後になる
        list.insertBefore(draggingElm, next && target.nextSibling || target);
    }
});
```
デフォルトの動作を停止し、moveをしている旨をdropEffectに設定。

target、つまり乗られた要素と、draggingElm、つまりドラッグしている要素を比較します。それらが違う要素で、targetがLIの場合に移動します。

マウス位置が、targetの上半分か下半分かで挿入位置を確定して、`insertBefore`で位置を変更します。


### 2.4. `dragend`ハンドラーを追加
ドラッグを終了しマウスボタンを離したときの、ドラッグ元要素のイベントハンドラーです。
```JavaScript
list.addEventListener('dragend', function(e) {
    draggingElm = null;
});
```
初期化で定義した変数をnullにします。

正直やらなくても大丈夫かもしれませんが、バグ防止のためにきっちり消しておきます。
（起こる事象のすべてを把握しているわけではないのだから、やるべき。例えば、ドラッグしたままマウスをブラウザの外に出した、その後とか、想定外のことはいくらでもあるので。）


# おわりに
これくらいなら、何かのライブラリを入れるとかせずとも、小手先の技で十分対応できることがわかりました。これでChrome機能拡張が捗ります。

なお今回の内容は、ChatGPTに聞いて進めした。以前は、話半分で参考にする程度だったような気がしますが、今やほぼ丸々使える答えが返ってきてすごいです。
わからない行はもっと詳しく教えてって言えば教えてもらえるから、動くものを作るだけでなく、普通に勉強にもなる。
さすがに、htmlとかJavaScriptの基礎くらいはわかっていないと、その情報を組み立てることすらできないですが。

あと宗教問題かもしれませんが、関数の書き方は、`()=>{}`より、`function(){}`が世界標準だとどこかで見ました。後者の方が速いケースがあるそうで。ChatGPTは後者でした。日本人は前者が好きだそうで、私も何となく前者が好き。読め/書けと言われればどちらでもできるけど、好きの感覚は説明できない。不思議。

# 参考
https://developer.mozilla.org/ja/docs/Web/API/DataTransfer/effectAllowed