目次

ドラッグ&ドロップで項目の並び替えが可能なリストを作成する

ドラッグ&ドロップで項目の並べ替えができるリストを作成するコードを紹介します。

概要

HTML5のドラッグ&ドロップ機能を利用して、項目の並べ替えができるリストを作成します。

コード

下記のHTML,CSSファイルを作成します。
[draggable] {
    user-select: none;
}

#columns {
    list-style-type: none;
}

.column {
    width: 200px;
    padding-bottom: 5px;
    padding-top: 5px;
    text-align: center;
    cursor: move;
}

.column header {
    height: 20px;
    width: 200px;
    color: black;
    background-color: #ccc;
    border: 2px solid #666666;
}

.column.dragElem {
    opacity: 0.4;
}

.column.over {
  border-top: 2px solid blue;
}
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title></title>
  <link rel="stylesheet" href="ListDragDropSort2.css" />

  <script type="text/javascript">
    var dragSrcEl = null;

    function handleDragStart(e) {
      dragSrcEl = this;
      e.dataTransfer.effectAllowed = 'move';
      e.dataTransfer.setData('text/html', this.outerHTML);
      this.classList.add('dragElem');
    }

    function handleDragOver(e) {
      if (e.preventDefault) {
        e.preventDefault();
      }
      this.classList.add('over');
      e.dataTransfer.dropEffect = 'move'; 
      return false;
    }

    function handleDragEnter(e) {
    }

    function handleDragLeave(e) {
      this.classList.remove('over'); 
    }

    function handleDrop(e) {
      if (e.stopPropagation) {
        e.stopPropagation(); 
      }

      if (dragSrcEl != this) {
        this.parentNode.removeChild(dragSrcEl);
        var dropHTML = e.dataTransfer.getData('text/html');
        this.insertAdjacentHTML('beforebegin', dropHTML);
        var dropElem = this.previousSibling;
        addDnDHandlers(dropElem);

      }
      this.classList.remove('over');
      return false;
    }

    function handleDragEnd(e) {
      this.classList.remove('over');
    }

    function addDnDHandlers(elem) {
      elem.addEventListener('dragstart', handleDragStart, false);
      elem.addEventListener('dragenter', handleDragEnter, false)
      elem.addEventListener('dragover', handleDragOver, false);
      elem.addEventListener('dragleave', handleDragLeave, false);
      elem.addEventListener('drop', handleDrop, false);
      elem.addEventListener('dragend', handleDragEnd, false);
    }

    function PageLoad() {
      var cols = document.querySelectorAll('#columns .column');
      [].forEach.call(cols, addDnDHandlers);
    }
  </script>
</head>
<body onload="PageLoad();">
  <ul id="columns">
    <li class="column" draggable="true"><header>ぺんぎんクッキー</header></li>
    <li class="column" draggable="true"><header>あひるサブレ</header></li>
    <li class="column" draggable="true"><header>しろくまアイス</header></li>
    <li class="column" draggable="true"><header>にわとりタルト</header></li>
    <li class="column" draggable="true"><header>かるがもクラッカー</header></li>
  </ul>
</body>
</html>

解説

リストのアイテムliタグにドラッグができるよう draggable 属性に true を設定します。また、ドラッグを開始時のイベントにdragStarted() 関数を、ドラッグ中のイベントにdraggingOver() 関数を、ドロップ時のイベントにdropped() 関数を設定します。
  <li class="column" draggable="true"><header>ぺんぎんクッキー</header></li>

bodyタグのonloadにPageLoad()関数が指定されているため、ページのロード後に下記のコードが実行されます。querySelectorAll()メソッドを呼び出し、IDがcolumnsの要素の子要素でクラスがcolumnの要素を取得します。取得した要素に対し、addDnDHandlersを呼び出します。
function PageLoad() {
  var cols = document.querySelectorAll('#columns .column');
  [].forEach.call(cols, addDnDHandlers);
}

addDnDHandlers()関数は下記のコードになります。引数に与えられた要素に対して、dragstart dragenter dragover dragleave drop dragend のイベントを設定します。~
function addDnDHandlers(elem) {
  elem.addEventListener('dragstart', handleDragStart, false);
  elem.addEventListener('dragenter', handleDragEnter, false)
  elem.addEventListener('dragover', handleDragOver, false);
  elem.addEventListener('dragleave', handleDragLeave, false);
  elem.addEventListener('drop', handleDrop, false);
  elem.addEventListener('dragend', handleDragEnd, false);
}

リストの要素のドラッグ開始時に下記のhandleDragStart関数が呼び出されます。ドラッグされる要素をdragSrcEl変数に代入します。また、dataTransferオブジェクトに値を設定してドラッグを開始します。値にはドラッグされる要素のouterHTMLを設定します。また、effectAllowed に move を設定しドラッグで移動できる状態にします。合わせてclassListにdragElem を追加しドラッグ状態のスタイルを設定します。今回のコードでは dragElemには、opacity: 0.4;が指定されているため、ドラッグを開始すると半透明の状態に表示が変化します。
function handleDragStart(e) {
  dragSrcEl = this;
  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/html', this.outerHTML);
  this.classList.add('dragElem');
}

ドラッグ中は下記のコードが実行されます。preventDefault メソッドによりデフォルトの動作を無効にします。また、dataTransfer.dropEffectをmoveに設定することでドロップが可能な状態に設定します。また、ドラッグオーバーされた要素の classListにoverクラスを追加します。overクラスには border-top: 2px solid blue; が設定されているため、ドラッグオーバーされた要素の上部に青色の線が表示される動作になります。この表示が要素が挿入される位置を示すガイドになります。
function handleDragOver(e) {
  if (e.preventDefault) {
    e.preventDefault();
  }
  this.classList.add('over');
  e.dataTransfer.dropEffect = 'move'; 
  return false;
}

ドラッグオーバーされた要素から抜けた場合は、ドラッグオーバーされた要素の青線表示を消す必要があるため、DragLeaveイベントで、classListからoverクラスを削除します。
function handleDragLeave(e) {
  this.classList.remove('over'); 
}

要素がドロップされた場合には下記のコードが実行されます。stopPropagationにより、親要素の処理を無効にします。if (dragSrcEl != this) によりドラッグされた要素とドロップされた要素が異なる場合に処理をします。
removeChildメソッドを呼び出し、ドラッグした要素を元のHTMLからいったん削除します。ドロップされたHTMLの情報は、getDataメソッドにより取得します。取得できる値は、ドラック開始時にsetDataで設定した、ドラッグされた要素のouterHTMLになります。
insertAdjacentHTML()メソッドを呼び出し、ドロップされた要素の位置の手前にドラッグされた要素を挿入します。要素を新たに挿入したため、再度ドラッグできるようにするために、イベントリスナ―を設定する必要があります。挿入された項目の要素のオブジェクトをpreviousSiblingプロパティで取得し、addDnDHandlers()でイベントリスナを設定します。
insertAdjacentHTMLメソッドの詳細はこちらの記事を参照してください。
function handleDrop(e) {
  if (e.stopPropagation) {
    e.stopPropagation(); 
  }

  if (dragSrcEl != this) {
    this.parentNode.removeChild(dragSrcEl);
    var dropHTML = e.dataTransfer.getData('text/html');
    this.insertAdjacentHTML('beforebegin', dropHTML);
    var dropElem = this.previousSibling;
    addDnDHandlers(dropElem);
  }
  this.classList.remove('over');
  return false;
}

表示結果

上記のHTMLファイルをWebブラウザで表示します。下図のウィンドウが表示されます。


2番目の[あひるサブレ]の項目をクリックしてドラッグします。ドラッグすると、元のアイテムが半透明になることが確認できます。また、ドラッグされた要素の挿入位置を示す青線も表示されます。


4つ目の[にわとりタルト]の項目にドラッグします。挿入位置を示す青線の位置も変わります。ドラッグ&ドロップを終了します。


[にわとりタルト]の項目の手前に[あひるサブレ]の項目が移動されました。


同様の手順でほかの要素もリスト内での位置を変更できます。



ドラッグ&ドロップで項目の順番を変更できるリストが作成できました。

著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
掲載日: 2019-02-12
iPentec all rights reserverd.