jQuery

jQueryUIでドラッグ&ドロップ出来る表を作成する

はじめに

今回業務でWebページでドラッグ&ドロップで追加、移動ができるような表を作成する案件がありました。ドラッグ&ドロップの実装をしたことがなかったのでその際にいろいろ調べて完成まで至った流れをまとめていこうと思います。

実際に作ったもの

このようなかんじのものを作成しました。

ポイントとしては、

  • 表の外にあるリストから表のマスにドラッグ&ドロップして追加することが出来る
  • 表の中からドラッグで移動することが出来る
  • 同じマスに複数要素が入ることも可能
  • ホバーした時に背景色が変わる(おまけ)

というようにしています。

仕様したライブラリ

ドラッグ&ドロップの実装をしたと書きましたが、正確にはドラッグ&ドロップが簡単に使えるようにしてくれるライブラリを使用して実装しました。
ということで使用したのが、jQueryUIです。

jQueryUIはjQueryの公式のUI用のライブラリになっていて、今回はドラッグ&ドロップを実装するために使いましたが他にもUI周りで便利な機能が多数あります。
↓こちらが公式ページになります
https://jqueryui.com/

jQueryUIを使って出来るドラッグ&ドロップの機能

先程の公式のリファレンスに使用例がたくさん乗っていたので、いくつかピックアップしていこうと思います。

Draggable

ドラッグ出来る機能です。
こんなかんじでドラッグできます。

右の「Examples」に応用例が色々乗っています。いくつか見ていきましょう。

移動可能な方向を縦方向横方向だけに制限したり、指定の範囲の中だけしか移動できない設定もできます。

スクロールのイベントも取ることが出来ます。
ドラッグし始め、最中、話した瞬間にそれぞれイベントを取ることが出来ます。

Droppable

ドロップできる領域を定義することが出来ます。

Sortable

順番を入れ替えるようにすることが出来ます。

リストではなくグリッド形式も使うことが出来ます。

いま紹介したのが基本的な機能で、これを応用したものが他にも例として上がっているので興味のある人は見てみてください。

どの使用例もサンプルコードが乗っているので参考にできると思います。

jQueryUIの導入方法

jQueryUIの導入の方法はいたって簡単で、

<script type="text/javascript" src="https://code.jquery.com/ui/1.11.4/jquery-ui.min.js">

この1行を追加するだけです。

とりあえずまず簡単に作ってみました。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>ページタイトル</title>
        <meta name="keywords" content="キーワード1,キーワード2">
        <meta name="description" content="ページの説明">
    </head>
    <body>
        <div class="box" style="background-color:red;width:100px;height:100px"></div>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.1/jquery.min.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
        <script type="text/javascript">
            $(function() {
                $('.box').draggable();
            });
        </script>
    </body>
</html>

htmlファイルを作成してこのコードを貼り付けてブラウザで開けば実行できるのでよかったら試してみてください。

これを実行してみるとこのようになります。

<script type="text/javascript">
    $(function() {
        $('.box').draggable();
    });
</script>

というようにすると、boxというクラスの要素がドラッグ可能になります。

ここで、どのようにしてドラッグ可能にしているかを探っていきます。
先程のGif画像の右側にChromeの「要素の検証」を載せておいたのですがそこに答えが隠されています。
ドラッグするごとに以下のようにcssが自動で変更されていることがわかると思います。

position: relative; left: xpx; right: ypx;

jQueryUIを使えばdraggable()を1行書くだけでドラッグ可能になりますが、内部的にはcssを先程のように書き換えることで実装されていることが分かるのではないかなと思います。

表を実装していく

前置きが長くなってしまいましたがjQueryUIを使用してここから表を実装していきます。

実際に作成したものは、冒頭でも載せましたがこのようなものです。

満たしたい要件としては、

  • 表の外にあるリストから表のマスにドラッグ&ドロップして追加することが出来る
  • 表の中からドラッグで移動することが出来る
  • 同じマスに複数要素が入ることも可能

ということでした。

実際のコードを以下に載せます。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>ページタイトル</title>
        <meta name="keywords" content="キーワード1,キーワード2">
        <meta name="description" content="ページの説明">
    </head>
    <body>
        <div class="table"></div>
        <div class="addable-contents"></div>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.1/jquery.min.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
        <script type="text/javascript">
            $(function() {
                const add_list = ["あ", "い", "う"];
                const column = 4;
                const row = 4;
                var now_drag_object = null;
                var elements = [];

                function create_keywords_table() {
                    var table_html = '</p><tr class="head"><th></th><p>';
                    for (var j = 1; j <= column; j++) {
                        table_html += '</p><th>' + j + '</th><p>';
                    };
                    for (var i = 1; i <= row; i++) {
                        table_html += '</p><tr class="content"><th>' + i + '</th><p>';
                        for (var j = 1; j <= column; j++) {
                            table_html += '</p><td class="droppable" data-row="'+ i +'" data-column="' + j + '"><p>';
                            $.each(elements, function(index, element) {
                                if (element["row"] != i || element["column"] != j) return true;
                                table_html += '</p><div class="table-content" data-id="' + element["id"] + '">' + element["name"] + '</div>';
                            });
                            table_html += '</td><p>';
                        };
                        table_html += '</tr><p>';
                    };
                    $('#table').html(table_html);
                }

                function create_keyword_list() {
                    const keyword_list_div = add_list.map(function(element, index, array) { return '</p><div class="addable-content">' + element + '</div><p>'}).join('');
                    $('.addable-contents').html(keyword_list_div);
                }
  
                function bind_droppable() {
                    $('.table-content').draggable({
                        start: function() {
                            now_drag_object = $(this);
                        },
                        stop: function(event, ui) {
                            create_contents();
                            now_drag_object = null;
                        }
                    });

                    $('.addable-content').draggable({
                        start: function() {
                            now_drag_object = $(this);
                        },
                        stop: function(event, ui) {
                            create_contents();
                            now_drag_object = null;
                        }
                    });

                    $('.droppable').droppable({
                        classes: {
                            "ui-droppable-hover": "ui-state-hover"
                        },
                        drop: function(event, ui) {
                            console.log("drop");
                            const row = $(this).data("row");
                            const column = $(this).data("column");
                            const id = now_drag_object.data("id");
                            const index = elements.map(function(element, index, array) { return element.id }).indexOf(id);
                            if (now_drag_object.hasClass("addable-content")) {
                                elements.push({"id": elements.length + 1, "name": now_drag_object.text(), "row": row, "column": column});
                            } else {
                                elements[index]["row"] = row;
                                elements[index]["column"] = column;
                            }
                        }
                    });
                };

                function create_contents() {
                    create_keywords_table();
                    create_keyword_list();
                    bind_droppable();
                }

                create_contents();
            });
        </script>
    </body>
</html>

コードをいきなり見てもわかりにくいと思うので1つずつ説明していこうと思います。
まず最初にドラッグ可能にするために、先程も簡単に説明しましたが、draggableメソッドを用います。

$('.table-content').draggable({
    start: function() {
        now_drag_object = $(this);
    },
    stop: function(event, ui) {
        create_contents();
        now_drag_object = null;
    }
});

$('.addable-content').draggable({
    // ドラッグ開始時の処理
    start: function() {
        now_drag_object = $(this);
    },
    // ドラッグ終了時の処理
    stop: function(event, ui) {
        create_contents();
        now_drag_object = null;
    }
});

このようにしました。

表の中の要素と表の下にある要素にそれぞれdraggableを適用しました。
先程の例だと$('.box').draggable();というように何も引数に渡していませんでしたが、このように関数を引数に渡すこともできます。
startの部分でドラッグし始めのタイミングで行う処理、stopで離したタイミングで行う処理を渡すことが出来ます。

今回の場合now_drag_objectを定義して、ドラッグし始めにドラッグしてるDOMのオブジェクトを格納し、離したときにnullにすることで、ドラッグ中のオブジェクトを一時的に格納するようにしています。

次にdroppableを定義していきます。
droppableでドロップできる場所を定義することが出来ます。

$('.droppable').droppable({
    classes: {
        "ui-droppable-hover": "ui-state-hover"
    },
    drop: function(event, ui) {
        const row = $(this).data("row");
        const column = $(this).data("column");
        if (now_drag_object.hasClass("addable-content")) {
            elements.push({"id": elements.length + 1, "name": now_drag_object.text(), "row": row, "column": column});
        } else {
            const id = now_drag_object.data("id");
            const index = elements.map(function(element, index, array) { return element.id }).indexOf(id);
            elements[index]["row"] = row;
            elements[index]["column"] = column;
        }
    }
});

このようにしました。

dropにdroppable上でdraggableがドロップされた場合に実行される処理を渡すことが出来ます。
classesの部分ではこのように定義することで、hoverされたときにui-state-hoverというクラスがつくように出来て、今回の場合は背景色を変えるために使用しています。

今回の実装で使用したjQueryUIの機能は以上です。
これら以外は普通のjQueryで実装していきました。

どのように実装したかというと、elementsという配列を定義して、表の中に入った要素を管理するようにしています。
どのように実装したかというと、まずdraggableとdroppableのイベントが発行されるたびにelements,add_listを参照してtableとaddable-contentsを1から全て作り直しています。
このようにしたのは、ドラッグして離したときに、マスの中に入っていればマスの中にいい感じにフィットさせ、マスの中に入っていなかったらもとの位置に戻すといった挙動を実現しようと思うと、draggableとdroppableの機能を組み合わせただけでは実現できなかった(もしかしたら用意されているかもしれませんが)ので無理やり実装しました。

table-contentとaddable-contentにそれぞれdraggableを適用させていて、stopでcreate_contentsメソッドを呼ぶようにしています。
create_contentsメソッドでは、

function create_contents() {
    create_keywords_table(); //tableを再生成する
    create_keyword_list(); //add_listを再生成する
    bind_droppable(); //draggableとdroppableを貼り直す
}

このように3つのメソッドを呼んでいます。tableとadd_listを再生成するようにしたのは先程説明したとおりです。
bind_droppable()を呼んで、draggableとdroppableを貼り直しているのは何故かと言うと、毎回tableとadd_listを作り直すので作り直したDOMにはdraggableとdroppableが適用されていないからです。
onメソッドでやれるようにしたかったのですがうまくいく方法が見つからなかったのでこのように強引にやってしまいました。(いいやり方があれば教えてほしいです)

ここまでで、要素をドラッグできドロップした瞬間に最初の状態に戻るというところまで実装できます。次はマスの中でドロップした際に表のマスにいい感じにすっぽりはまるようにする部分を説明していきます。

表のマスそれぞれにdroppableを適用していて、draggableの要素をドラッグ&ドロップするとdropに渡した関数を実行します。
表のマスにdata-rowとdata-column属性を持っていてどのマスかが分かるようになっていて、droppableでdropに渡した関数内で$(this)でドロップされたマスのDOMが取得できます。表内にある要素を{“id”,”name”,”row”,”column”}を要素に持つelementsという配列で管理するようにしています。

表の下にある要素を表内にドロップした場合はelementsに追加、表のマスの中の要素からマスの中にドロップした場合はelements内のその要素のrowとcolumnを書き換えるようにしています。
それを分岐させるために、先程も説明したようにドラッグしている要素はnow_drag_objectに格納されているのでそこからクラスが何かによって分岐するように実装しています。

その後、先ほど説明した、draggableのstopに渡した関数が呼ばれ、表を1から作成し直すcreate_contentsメソッドを呼び出し、その後にnow_drag_objectをnullにします。

以上が大体の流れになります。
draggableとdroppableの基本的な機能だけ利用して、残りはjQueryの基本的な操作を組み合わせて実装しています。

1つ実装する際に悩んだ点があるので書いておくと、
先程説明した流れは、ドロップした際に

  1. droppableのdropに渡した関数でelementsを書き換える
  2. draggableのstopに渡した関数で表を再生成して、now_drag_objectをnullにする

という流れで実装しています。この順番が逆になってしまうとうまくいかない実装方法になっています。表のマスにドロップした際に、droppableのdropとdraggableのstopは発火条件は同じなのでこの2つが呼ばれる順番が逆だったり、同時にスタートする可能性もあるかなって最初思いました。その場合の実装がちょっとめんどくさくなるなーって思ってました。

しかし実験したりして探ってみると、そこはうまく作られていて毎回、droppableのdropイベントが先に呼ばれるようになっていて、それが終わり次第draggableのstopイベントが呼ばれるという流れになっていました。なのでこの実装方法で大丈夫となりました。

まとめ

今回jQueryUIを使って簡単なサンプルを作成してみましたが、拡張性も高いので幅広く対応できるのではないかなと思います。この記事が参考になれば嬉しいです。