HTML5 のドラッグアンドドロップ API を使って並び替え可能なリストを作る

管理画面で表示順を変更するときなど、ドラッグアンドドロップでリストを並び替えられるようにすると、直感的な操作ができます。こうした UI の実装は、従来 jQuery UI の SortableSortableJS を使って行われていましたが、HTML5 のドラッグアンドドロップ API を使うことで、ライブラリを追加することなく割と簡単に実装できます。

モバイルブラウザの大半が DragEvent に対応していないため、デスクトップブラウザに限られてしまうのが難点ですが、管理画面で使う分にはそれほど影響ないかな、と。

実際に使う場面では、変更した並び順をフォームで送信して DB に保存することが多いと思うので、リストの data-id の並び順をカンマ区切りで <input type="hidden" name="list_order" id="list_order"> に格納するようにします。

見栄えの調整のために Bootstrap 4 を使っているので、ついでに(?)jQuery も使っています。ネイティブの JavaScript での実装と比較すると、dataOffsetYinsertAfter が使える分、楽ができます。

今回の実装にあたっては、JavaScriptでドラッグ&ドロップによるリストの並び替えを実装する例を参考にさせていただきました(特にドラッグしたときにどの場所に要素を挿入するかを直感的に理解できるようにする部分)。

実装例

<!doctype html>
<html lang="ja">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
          integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">

    <title>ドラッグアンドドロップで並び替えられるリスト</title>
</head>
<body>
<main role="main" class="container">
    <h1>ドラッグアンドドロップで並び替えられるリスト</h1>

    <form>
        <ul class="list-group">
            <li class="list-group-item list-group-item-action" data-id="1" draggable="true">項目1</li>
            <li class="list-group-item list-group-item-action" data-id="2" draggable="true">項目2</li>
            <li class="list-group-item list-group-item-action" data-id="3" draggable="true">項目3</li>
            <li class="list-group-item list-group-item-action" data-id="4" draggable="true">項目4</li>
            <li class="list-group-item list-group-item-action" data-id="5" draggable="true">項目5</li>
        </ul>

        <input type="hidden" name="list_order" id="list_order">
        <button type="submit" class="btn btn-primary">送信</button>
    </form>
</main>

<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
        integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
        crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx"
        crossorigin="anonymous"></script>

<script>
  jQuery(function ($) {

    let lists = $('ul.list-group li')
    lists.on('dragstart', onDragStart)
    lists.on('dragover', onDragOver)
    lists.on('dragleave', onDragLeave)
    lists.on('drop', onDrop)

    $('form').submit(updateOrder)

    function onDragStart (e) {
      e.originalEvent.dataTransfer.setData('text', $(this).data('id'))
    }

    function onDragOver (e) {
      e.preventDefault()

      if ((e.offsetY) < ($(this).innerHeight() / 2)) {
        //マウスカーソルの位置が要素の半分より上
        $(this).css({
          'border-top': '2px solid blue',
          'border-bottom': '',
        })
      } else {
        //マウスカーソルの位置が要素の半分より下
        $(this).css({
          'border-top': '',
          'border-bottom': '2px solid blue',
        })
      }
    }

    function onDragLeave (e) {
      $(this).css({
        'border-top': '',
        'border-bottom': '',
      })
    }

    function onDrop (e) {
      e.preventDefault()
      let id = e.originalEvent.dataTransfer.getData('text')

      if ((e.offsetY) < ($(this).innerHeight() / 2)) {
        $('li[data-id=\'' + id + '\']').insertBefore(this)
      } else {
        $('li[data-id=\'' + id + '\']').insertAfter(this)
      }
      $(this).css({
        'border-top': '',
        'border-bottom': '',
      })

    }

    function updateOrder () {
      let listOrder = []
      $('ul.list-group li').each(function () {
        listOrder.push($(this).data('id'))
      })

      $('#list_order').val(listOrder.join(','))

      console.log($('#list_order').val())
    }

  })
</script>
</body>
</html>

この記事を書いた人
グッドネイバー

“ Webに悩むお客さまの「よき隣人」でありたい ” をモットーに、Web システム開発(主に Laravel)、Web マーケティング支援の仕事をしています。お仕事のご依頼・ご相談はこちらからお気軽にどうぞ。