当番管理アプリで使ったドラッグ＆ドロップでテーブルの要素並び替え

この記事は[ニフティグループ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nifty)の19日目の記事です。
昨日は @yukiex@githubさんの[ARMベースのEC2インスタンスを利用する](https://qiita.com/yukiex@github/items/4975bddc25bb5b25eae6)でした。
私はAWSはまだまだ勉強中ですが、最近チームでもAWSを積極的に使おうという流れがきてます。AWSはサービス多すぎて、どんどん追加されて追いつけなくなりそうなので、今のうちに勉強しようと思います。（今からワンピースを追うようなものだな）

# はじめに
Qiita初投稿! & ニフティグループ Advent Calendar 2019に参加します。

今年新卒入社したものですが、チーム内で新人含めたミニスクラムチームを作って、勉強も兼ねて、社内向けツールを開発していました。

そこで使ったjQuery UIを使ったドラッグ&ドロップについて紹介したいと思います。
# こんなかんじのもの
![qiita_prod.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533977/46c098b7-f5f5-c076-7e7c-c3c5c11cd0e4.gif)

社内向けの当番管理アプリです。Django + AWS Elastic Beanstalkで開発しました。
このアプリができる前はExcelで管理していたので、いろいろと面倒でした。
機能としては、web上で確認、管理できる他に、当番が近づいた日と交代したときにSlackに通知してくれます。

上のgifのようにドラッグ&ドロップで要素を動かして交代するのも個人的には売りなのですが、当番の交代はほぼないので、みんな使わないだろうな・・・。なので、ドラッグ&ドロップについて共有したいと思います。

他にも、スケジュール自動生成機能を現在開発中です。

# jQuery UI ~動かすのは簡単~
最初はドラッグ&ドロップで実装したらかっこいいだろうなー、でも難しそうだなーと考えていたときに、先輩がjQuery UIを使えばできるのではないかと教えてくれました。

jQuery UIを使うと予想より簡単に要素を動かせることがわかったので、このツールを使うことにしました。

jQuery UI
https://jqueryui.com/
公式ページでは、実際に動かせるデモがついていて、ソースもみれるので、見てるだけでおもしろいです。

ただ単に要素を動かしてみるだけなら簡単です。
CDNを読み込ませて、要素に名前をつけて、その名前の要素は動くよって指定するだけです。

下は簡単なサンプルです。図形を動かします。
![動く.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533977/e4d3b624-270b-52f7-01f9-fcc90dc0b183.gif)

```HTML:draggable.html
<head>
    <style>
        .draggable {
            width: 100px;
            height: 100px;
            background-color:green;
        }
    </style>
    <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
    <script>
        $(function () {
            $(".draggable").draggable();
        });
    </script>
</head>
<body>
    <div class="draggable"></div>
</body>
</html>
```

その他いろいろあるので、気になる方は公式ページを見てみてください。

# テーブルで要素並び替え sortable
上ではdraggableを使っていましたが、sortableというものを使うと、要素の並び替えなどもできます。

当初はsortableを使って一人ずつ要素を動かして、交代するように開発していました。しかし結果的に、バリデーションが複雑になりすぎて、後に紹介する交換型に乗り換えました。

sortableを使った、テーブルで要素並び替えの簡単なサンプルがこちらです。CSSの調整を省いているので若干デザインがずれてます。

![一個ずつ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533977/3e194cf8-b9d1-8b53-ba09-6b372f1276aa.gif)

```HTML:sortable.html
<head>
    <style>
        .member span {
            border-width: thin;
            border-style: solid;
        }
    </style>
    <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
    <script>
        $(function () {
            $('.connectedSortable').sortable({
                // このクラス間だけ相互移動できる。
                connectWith: '.connectedSortable',
            }).disableSelection();
        });
    </script>
</head>

<body>
    <div class="container">
        <table border="1" class="table table-hover">
            <thead>
                <tr>
                    <th scope="col">Date</th>
                    <th scope="col">Area</th>
                    <th scope="col">Member</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td scope="row">今日</td>
                    <td>A</td>
                    <td class="member connectedSortable">
                        <span>いわかみ</span>
                        <span>はしうち</span>
                        <span>そりまち</span>
                    </td>
                </tr>
                <tr>
                    <td scope="row">明日</td>
                    <td>B</td>
                    <td class="member connectedSortable">
                        <span>みやさか</span>
                        <span>むらやま</span>
                        <span>たかさき</span>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</body>

</html>
```
.connectedSortableというクラス名がある要素間で移動できます。

tdタグでなく、spanタグなのは、CSSで見た目を整える時に、枠線などいろいろずれてしまうので、結果的にspanタグを使うことになりました。bootstrapなどを使うならspanタグを使うほうがいいかも知れません。

# 交換型の移動に乗り換えるまで

とりあえず、一人ずつ移動して、交代する方向で進んでました。ただバリデーション&テストで新たなパターンがどんどん見つかり、チームのみんなも一番大変だったと言っていたくらいです。
また開発に携わっていない人に試してもらうと、やはり一人ずつ移動させて交代するのは分かりづらいとのことでした。

やはり、強制的にひゅっと交換できればいいなあと色々調べていると、ある記事を見つけました。
https://qiita.com/NickelCreate/items/8dd804279ea1eab75abe
まさにこれだ！というものでした。

開発当初はJSすら研修で触った程度でしたが、ある程度理解できるようになってきたので、テーブルでも使えるようチャレンジして、結果的にできたのでよかったです。
# 交換型のサンプル
![交換.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533977/d03ae1ad-e600-6252-3c37-a4fe513b783a.gif)
sortableのサンプルで使ったテーブルを交換型の並び替えにしてみると、このようになります。
基本的には上の記事を参考にしていますが、そのまま流用できず、テーブルの要素を動かす用に変更する必要がありました。

できる限りコメントで説明しています。今の$(this)が何なのか意識しながらだと理解しやすいです。

大まかな説明では、ドラッグするときに交換元の要素の、位置と隣や親要素をもっておいて、ドロップするときに、その位置に合った要素と入れ替える感じです。

色や移動スピードは変えられます。jQueryUIのコツは要素のclassがアクションごとに変わることです。ドラッグしているとき、ドラッグしている要素を他の要素の上に来た時、他の要素がドラッグされたときなどいろいろ変化するので、アクションごとデザインなど変えたいときはclassを見てみるとわかりやすいです。

```HTML:koukan.html
<head>
    <style>
        .member span {
            border-width: thin;
            border-style: solid;
        }
    </style>
    <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
    <script>
        // メンバーの交代
        $(function () {
            // ドラッグ中のオブジェクト情報
            var $dragObj;
            var $prevObj;
            var $dragDefPosition;
            var $dragStartPosition;
            var $revert = true;

            $('.draggable span').draggable({
                // 画面外に要素を持っていったときにスクロールする
                scroll: true,
                // ドラッグ中の不透明度
                opacity: 0.8,
                // 要素の重ね合わせの順序。値が大きいほど全面
                zIndex: 10,
                // 交換せず要素が元の位置に戻るまでの時間 単位:ミリ秒
                revertDuration: 1,
                start: function () {
                    //ドラッグ開始時に座標保存
                    // $(this)はイベントが発生した要素
                    $dragObj = $(this);
                    // 移動始めの縦と横の位置
                    $dragDefPosition = $(this).position();
                    // prev()は今の要素の隣の要素を取得する　今持っている要素の隣
                    $prevObj = $(this).prev();
                    // trueを指定すると、ドラッグ終了時に、ドラッグ開始位置に要素が戻ります。
                    $revert = true;
                    // .parent()は親要素をとる ここでは$(this)を含むtdタグ
                    $dragStartPosition = $(this).parent();
                },
                revert: function () {
                    // droppableの後に実行されるので、droppable()内でfalseにして無効化
                    return $revert;
                },
            });

            $('.draggable span').droppable({
                drop: function (event, ui) {
                    // ドラッグ無効
                    $('.draggable span').draggable('disable');
                    // 元に戻さない
                    $revert = false;
                    // dropされた位置に元々いた要素をdropObjとする
                    $dropObj = $(this);
                    // 今持っている要素の位置をdropPosition
                    $dropPosition = $dropObj.position();
                    $dragObj.animate({
                        top: $dropPosition.top - $dragDefPosition.top,
                        left: $dropPosition.left - $dragDefPosition.left,
                    }, 200// 要素の移動の速さ 単位:ミリ秒
                        , function () {
                            $dropObj.animate({
                                top: $dragDefPosition.top - $dropPosition.top,
                                left: $dragDefPosition.left - $dropPosition.left,
                            }, 200// 要素の移動の速さ 単位:ミリ秒
                                , function () {
                                    //ドロップした時に元々いた要素の手前にドラッグしてきた要素を移動
                                    // .before 要素の前に()内の要素を挿入
                                    $dropObj.before($dragObj);
                                    // 交換時に位置調整と、色をつけて何が移動したかわかるようにする
                                    $dragObj.css({ 'top': 0, 'left': 0, 'background-color': '#FEE8E9' });

                                    // ドラッグ要素の元位置にドロップ要素を移動
                                    // $prevObj.lengthは要素の隣の要素の数
                                    // 移動元に要素があれば、その要素の後ろに追加
                                    if ($prevObj.length != 0) {//
                                        // .after() 要素の後に()内の要素を挿入
                                        $prevObj.after($dropObj);
                                    }
                                    // 移動元に要素が無ければ、移動元のtdタグに要素を追加
                                    else {
                                        // .prepends() 先頭に要素を追加する
                                        $dragStartPosition.prepend($dropObj);
                                    }
                                    // 交換時に位置調整と、色をつけて何が移動したかわかるようにする
                                    $dropObj.css({ 'top': 0, 'left': 0, 'background-color': '#FEE8E9' });
                                    //ドラッグ有効
                                    $('.draggable span').draggable('enable');
                                });
                        });
                },
            });
        });
    </script>
</head>

<body>
    <div class="container">
        <table border="1" class="table table-hover">
            <thead>
                <tr>
                    <th scope="col">Date</th>
                    <th scope="col">Area</th>
                    <th scope="col">Member</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td scope="row">今日</td>
                    <td>A</td>
                    <td class="member draggable">
                        <span>いわかみ</span>
                        <span>はしうち</span>
                        <span>そりまち</span>
                    </td>
                </tr>
                <tr>
                    <td scope="row">明日</td>
                    <td>B</td>
                    <td class="member draggable">
                        <span>みやさか</span>
                        <span>むらやま</span>
                        <span>たかさき</span>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</body>

</html>
```

# 【おまけ】テーブルのデータ取得
ドラッグ&ドロップで動かすだけでも楽しいのですが、
もちろんテーブルを入れ替えただけでは、管理はできません。入れ替えた後のデータを取得して、DB等で管理しなければいけません。

実際のアプリでは、JSONデータにまとめてPOSTして、受け取った側でDBに反映をしてました。

入れ替え後のデータの取得法のサンプルも紹介します。ここでは、ブラウザのconsoleで確認できるところまでです。

![json.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533977/85bf2408-8abd-6bea-a58c-47cd18253e58.gif)

大まかな流れとしては、find()を使ってテーブルの情報を１行ごと取得して、最後の状態がわかるように保持します。
データとして必要なのは、変更後の最後の状態です。
よって、行ごとの最新の状態を配列に入れていきます。
取得したデータは扱いやすいようにJSONに変換します。

取得するタイミングについては、ドラッグ&ドロップで入れ替わった後のドラッグ時とドロップ時の行を取得します。文で書くと分かりづらい（言い訳）のですが、とにかく細分化して動作を順に追っていくとわかると思います。

正直正しいやり方かわからないですが、一応共有します。

###### 先程のサンプルに３箇所追加するとできます。

#### 1. テーブルのデータを取得する関数を追加する。

```<script>``` タグの中に１つ関数を追加します。

```JAVASCRIPT
var get_data = {};
var latest_data = [];
var post_data = {};

// 変化した行のデータを取得し、json形式で保持
/* 大まかな流れ
- メンバーが変化した行のデータをget_dataに入れる
- latest_dataに各行の最新の状態のデータを入れる（同じ日付とエリアがある場合は上書きしていき、
ない場合は新規に追加)
- post_dataにlatest_dataをJSON形式にしたデータを入れる
*/
function createPostDate (row_data){
    var schedule_day = $(row_data).find(".schedule").text();
    var schedule_area = $(row_data).find(".area").text();
    // メンバーだけは配列に入れてからテキストにする
    var schedule_member = [];
    $(row_data).find(".member").children("span").each(function(i) {
        schedule_member.push($(this).text());
    });
    // latest_dataに入れるためのデータを連想配列（辞書型）に
    get_data = {date:schedule_day, member:schedule_member, area:schedule_area};
    // 同日かつ同エリアの場合(true)は上書きする（行の最新のデータを保持）
    var update_flg = false;
    for (var i=0; i<latest_data.length; i++) {
        if (latest_data[i]['date'] == get_data['date'] && latest_data[i]['area'] == get_data['area']){
            update_flg = true;
            latest_data[i] = get_data;
        }
    }
    // 同日かつ同エリアではない場合はlatest_dataに新しく追加
    if (!update_flg){
        latest_data.push(get_data);
        // post_dataをjson形式に変換
        post_data = {data: latest_data}
    }
}
```

#### 2.次に要素の交換時にこの関数が呼べるようにします。
以下のように追加してください。
droppableの下の方です。

```JAVASCRIPT
$('.draggable span').droppable({
〜〜〜
〜〜〜
〜〜〜
//ドラッグ有効
$('.draggable span').draggable('enable');
// この下に追加!!!!!!!!
// 変化があった行のデータを作成
// 移動先（ドロップした場所の行）
createPostDate($dragObj.closest('tr')[0])
// 移動元(ドラッグし始めた場所の行)
createPostDate($dropObj.closest('tr')[0])
// consoleで確認用
console.log(JSON.stringify(post_data,undefined,1));
```

#### 3. 最後にtdタグに名前をつけます。

関数でclassの名前から、どのカラムの情報なのか判別して取得しているため、追加が必要です。
下のような感じで、classにscheduleとmemberを入れてください。
他にもある場合は同じように追加してください。

```HTML
<td class="schedule" scope="row">今日</td>
<td class="area">A</td>
<td class="member draggable">
```

##### これでコンソールに出力されるはずです。
あとはPOSTするなりして、データを管理します。
ちなみにDjangoでAjaxでPOSTしましたが、結構癖がありました。トークンをつけてあげたり、JSの方でリダイレクトしたりと大変でした。

# 終わりに
ドラッグ&ドロップは意外に簡単にできることがわかりました。ちょっと動かせるだけでいい気分になります。
またそれぞれオプションでいろいろできるので、気になる方は見てみてください。
このアプリ開発で多く学べたので、協力してくださった方々には感謝します。

明日は@shin27さんです。