JavaScriptでドラッグ&ドロップによるリストの並び替えを実装する例
1年近く前に「JavaScriptでドラッグ&ドロップによる画像の入れ替えを実装する」という記事を投稿してその存在自体忘れていたのですが、先日「JavaScript ドラッグ&ドロップで並べ替え」と検索した際、かなり上位(検索エンジンによっては1番目…)に表示されて顔から火の出る思いをしました。
過去に書いた自分のコードはもはや他人のコード…というのはプログラマーの方なら共感して頂けるかと思いますが、1年も前のコードともなると読み返すのも恥ずかしい限り。
実際、先述の記事では画像の入れ替えはたしかに実現しているものの、要素を入れ替えるのではなく、img.srcの中身を入れ替えるというなかなかの暴挙に出ておりますw
まぁそのときやりたかったことは実現できているのだから良いっちゃ良いのですが…。大抵の場合は画像そのものではなく、divやul/li等の要素ごと並べ替えたいという需要のほうが多いのではないでしょうか。
必要最小限のコードでドラッグ&ドロップによるリストの並び替えを実装してみる
実行例
下のリストから項目をドラッグし、任意の項目へドロップすると、その要素の「上に」リストが移動して並び替えられます。
- 1.ドラッグ&ドロップで
- 2.リストの順番が
- 3.入れ替わります
HTMLサンプル
<ul class="drag-list">
<li id="item1" draggable="true">1.ドラッグ&ドロップで</li>
<li id="item2" draggable="true">2.リストの順番が</li>
<li id="item3" draggable="true">3.入れ替わります</li>
</ul>
<style>
.drag-list li {
line-height:3em;
border:1px solid #cccccc;
cursor:pointer;
}
</style>
特に難しいタグもないと思います。普通のul/liのリストで、liにidを付けていることと、draggable属性をtrueにしているくらいです。
CSSのほうは1行をちょっと広くしてマウスで掴みやすいようにしているのと、カーソルポインターを手の形状にしているくらいですかね。
JavaScriptサンプル
<script>
document.querySelectorAll('.drag-list li').forEach (elm => {
elm.ondragstart = function () {
event.dataTransfer.setData('text/plain', event.target.id);
};
elm.ondragover = function () {
event.preventDefault();
this.style.borderTop = '2px solid blue';
};
elm.ondragleave = function () {
this.style.borderTop = '';
};
elm.ondrop = function () {
event.preventDefault();
let id = event.dataTransfer.getData('text/plain');
let elm_drag = document.getElementById(id);
this.parentNode.insertBefore(elm_drag, this);
this.style.borderTop = '';
};
});
</script>
こちらもシンプルで20行くらいのコードです。
最近、querySelectorが便利でお気に入りなので、クラス名drag-listの下のli要素をquerySelectorでリストアップしています。
そしてその全てのliに ondragstart / ondragover / ondragleave / ondrop という4つのイベントを追加。
ondragstartは文字どおり、ドラッグ開始なので、その対象要素であるevent.target.idをdataTransferオブジェクトに保存(setData)しています。
そして、別の要素の上にドラッグして重なったとき ondragover イベントが発生。そこでは単純にドロップ先候補の要素の上部分に青い線(2px solid blue)を書いているだけです。ここにドロップしますよ~、という予告のためですね。
ドラッグ要素が対象からハズレたときは ondragleave イベントが発生するので、青い線を消しています。
そして最後に実際にドロップされた際の ondrop では、あらかじめ保存しておいたドラッグ元のidを取得(elm_drag)。
ドロップ先(li)の親要素(ul)のinsertBefore関数を使い、ドロップ先(li)の前にドラッグ元の要素(elm_drag)を挿入。
という流れです。わざわざ文章で書くとややこしいですねw こんな冗長な説明よりコードを見たほうが理解が早そうです。
insertBeforeだけでは直感的ではない
最初から長いコードだとウッとなる方もいるかなと思い、出来るだけ短いコードでドラッグ&ドロップソートを実現してみましたが、やや直感的じゃないですよね。
例えば「1.」の要素を「3.」の要素までドラッグしていったら、「3.」の前に「1.」が挿入される。文字で書くと、まぁおかしくはないのですが、「3.」より下にもっていったら、「3.」の下に「1.」を挿入してほしくないですか?
これを実現するためには余分なli要素をもうひとつ「3.」の下に書けば一応実現可能です。実際に改善例を見てみましょう。
改善例
- 1.ドラッグ&ドロップで
- 2.リストの順番が
- 3.入れ替わります
これで一応、「1.」の要素を「3.」の要素の下にドラッグすることが出来るようになったため、一番最初のよりは直感的になったでしょうか。
この例ではJavaScriptは一切触っておらず、単純にHTMLコードをこう↓書き換えただけです。
<ul class="drag-list">
<li id="item1" draggable="true">1.ドラッグ&ドロップで</li>
<li id="item2" draggable="true">2.リストの順番が</li>
<li id="item3" draggable="true">3.入れ替わります</li>
<li style="border:0;list-style-type:none;"> </li>
</ul>
見えない4行目を入れているんですね。
4行目の高さをもう少し低くしても良いでしょう。
でも、もうちょっと改善したくないですか?
より直感的なドラッグ&ドロップによる並び替え
- 1.ドラッグ&ドロップで
- 2.リストの順番が
- 3.入れ替わります
このリストはどうでしょう?
ドラッグした後、ドロップ先の要素の上半分だったら上に、下半分だったら下にリストが挿入されます。先述した2例よりも更に直感的ではないでしょうか。
これを実現するためにはマウスカーソルの位置を判断しなければならないので、少しだけコードが長くなります。
JavaScript例
document.querySelectorAll('.drag-list li').forEach (elm => {
elm.ondragstart = function () {
event.dataTransfer.setData('text/plain', event.target.id);
};
elm.ondragover = function () {
event.preventDefault();
let rect = this.getBoundingClientRect();
if ((event.clientY - rect.top) < (this.clientHeight / 2)) {
//マウスカーソルの位置が要素の半分より上
this.style.borderTop = '2px solid blue';
this.style.borderBottom = '';
} else {
//マウスカーソルの位置が要素の半分より下
this.style.borderTop = '';
this.style.borderBottom = '2px solid blue';
}
};
elm.ondragleave = function () {
this.style.borderTop = '';
this.style.borderBottom = '';
};
elm.ondrop = function () {
event.preventDefault();
let id = event.dataTransfer.getData('text/plain');
let elm_drag = document.getElementById(id);
let rect = this.getBoundingClientRect();
if ((event.clientY - rect.top) < (this.clientHeight / 2)) {
//マウスカーソルの位置が要素の半分より上
this.parentNode.insertBefore(elm_drag, this);
} else {
//マウスカーソルの位置が要素の半分より下
this.parentNode.insertBefore(elm_drag, this.nextSibling);
}
this.style.borderTop = '';
this.style.borderBottom = '';
};
});
HTMLコードは一番最初に掲載したものから変わりありません。
JavaScript側だけマウスカーソルの位置を判断するコードが増えています。
既に解説したところは端折るとして、追加した大事な部分についてのみ解説しますね。
まず、getBoundingClientRectでドロップ先の要素の大きさを取得しています。
let rect = this.getBoundingClientRect();
マウスカーソルの位置はY座標だけわかれば良いので event.clientY で取得できるものの、これはブラウザのウィンドウ内での位置です。
それだとドロップ先の要素のどのへんなのかわからないため、event.clientY から getBoundingClientRect で取得した rect.top (要素の上辺の位置)を引いて相対的なY座標を算出。
そして、その相対的なY座標がドロップ先の要素の上半分か下半分かを (this.clientHeight / 2) で判断しているわけです。
ondragover イベントの部分では上半分だったら上辺のborderを青くし、下半分だったら下辺のborderを青くしています。挿入位置をお知らせするためですね。
ondrop イベントでも同様に上半分か下半分かを判断していますが、上半分の場合は最初のコードと何らかわらないので単にinsertBeforeをしているだけ。
下半分だった場合は insertAfter …………と言いたいところなのですが、残念ながら insertAfter関数はないので、ドロップ先要素の次の要素を nextSibling で取得して、「ドロップ先の要素の次の要素の前にドラッグした要素を挿入する」というややこしい書き方になりますw
まとめ
1年ほど前に書いた「JavaScriptでドラッグ&ドロップによる画像の入れ替えを実装する」という記事ではたしかに画像の入れ替えを実現していたものの、並び替えとは違うし、divやul/liへ応用できる方法ではなかったので、あらためてドラッグ&ドロップによる要素の並び替えについて解説してみました。
記事自体はちょっと長くなってしまいましたが、先述したとおり、簡易版であればほんの20行ほどで実装できますし、より直感的なバージョンもそこにほんの10数行追加するだけなので、落ち着いて見ればそれほど難しくないのではないでしょうか。
PCとスマホ両対応が求められる時代なので、ドラッグ&ドロップによるUIを追加したいと考える人がどれだけいるかわかりませんが、個人的にはPC/スマホの両対応をするからといって、PCまでスマホと同じ操作を強いられるのはちょっとどうかなぁ~と思うので、この記事を見てWebページにドラッグ&ドロップを導入してくれる人が増えてくれたら嬉しいです。