Sortableを利用したレコードの並び替え

著者名: 落合 雄一 (External link)

目次

caution
警告

jQuery UI は v1.13 をもってメンテナンスモードになりました。

はじめに

jQuery UI とは、インタラクティブな Web サイトを開発するために使用される、jQuery をベースにした JavaScript のライブラリです。
今回は Sortable (External link) を使って、レコードをドラッグ&ドロップで並び替えし、並び順を保存する Tips を紹介します。
なおコード中の jQuery は、$ とします。

デモ環境

デモ環境で実際に動作を確認できます。
https://dev-demo.cybozu.com/k/174/ (External link)

ログイン情報は cybozu developer network デモ環境 で確認してください。

jQuery UI Sortable

jQuery UI Sortable (External link)

一覧画面で、要素をドラッグ&ドロップで並び替えたいと思うことがあるかと思います。
Sortable は、こんな場合にピッタリです。
Web サイトのサンプルを見るとイメージがつかめるかのではないでしょうか?

基本的な使い方は、並び替えを行いたい要素(li や tr)の親(ul や tbody)の id を指定するだけです。

1
$('#id').sortable();

たったこれだけで、並び替えを実現できます。
すばらしいですね。

サンプルアプリ

サンプルアプリのフィールドは、以下のようになります。

フィールド名 フィールドタイプ フィールドコード 備考
並び順 数値 orderNum 初期値:0
都道府県 文字列(1行) prefecture

一覧にはカスタマイズビューを利用します。
カスタマイズビューには以下の HTML を指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!--
  * Sort kintone records with Sortable
  * Copyright (c) 2014 Cybozu
  *
  * Licensed under the MIT License
  * https://opensource.org/license/mit/
-->
  
<table class="sortable-recordlist">
  <thead id="sortable_thead">
    <tr><th>レコード番号</th><th>並び順</th><th>都道府県</th></tr>
  </thead>
  <tbody id="sortable_tbody"></tbody>
</table>

レコードをいくつか追加してから表示すると以下のようになります。

JavaScriptとCSS

サンプルコードの他に、jQuery と jQuery UI のライブラリを読み込ませる必要があります。
Cybozu CDN を利用しましょう。

  • JavaScript
    • https://js.cybozu.com/jquery/3.6.1/jquery.min.js
    • https://js.cybozu.com/jqueryui/1.13.2/jquery-ui.min.js
    • sample.js(以下のコードをコピーして保存し、アップロードしてください)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/*
 * Sort kintone records with Sortable
 * Copyright (c) 2014 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

(() => {
  'use strict';

  /**
   * 並び替え可能なviewを表示
   */
  class SortableRecordsManager {
    constructor(records) {
      this.records = records;
      this.recordOrderMap = records.map((recordData) => {
      // orderNumが0のときは、レコード番号で初期化
        if (recordData.orderNum.value === '0') {
          recordData.orderNum.value = recordData.$id.value;
        }
        return {$id: recordData.$id.value, orderNum: recordData.orderNum.value};
      });
    }

    // 並び替えられたidの配列を一つ前のidの配列と比較し、レコード番号と並び順の対を取得するメソッド
    update(updatedArray) {
      const tmp = $.extend(true, {}, this.recordOrderMap);
      const updateRecordOrderMap = $.extend(true, {}, this.recordOrderMap);
      const len = updatedArray.length;
      for (let i = 0; i < len; i++) {
        const prev = tmp[i].$id;
        const cur = updatedArray.shift();
        if (prev !== cur) {
          updateRecordOrderMap[i].$id = cur;
        }
      }
      this.recordOrderMap = $.extend(true, {}, updateRecordOrderMap);
      return this.recordOrderMap;
    }

    // すべてのレコードについて行追加するメソッド
    createTableRecords() {
      const tb = document.getElementById('sortable_tbody');
      // 追加取得できたレコードの件数
      const len = this.records.length;

      // すべてのレコードについて行追加
      for (let i = 0; i < len; i++) {
        const record = this.records[i];

        // 行作成
        const row = tb.insertRow(tb.rows.length);
        row.id = record.$id.value;
        const cell1 = row.insertCell(0);
        const cell2 = row.insertCell(1);
        const cell3 = row.insertCell(2);
        cell1.textContent = record.$id.value;
        cell2.textContent = record.orderNum.value;
        cell3.textContent = record.prefecture.value;
      }
    }

    // テーブルをクリアするメソッド
    destroyTableRecords() {
      document.getElementById('sortable_tbody').innerHTML = '';
    }
  }

  /**
  * Kintoneと通信を行うクラス
  */
  class KintoneRecordManager {
    constructor() {
      this.appId = kintone.app.getId();
      this.records = [];
      this.query = '';
      this.limit = 100;
      this.offset = 0;
    }

    // すべての並び順を更新するメソッド
    updateOrderNums(recordOrderNumArray) {
      const records = [];
      Object.keys(recordOrderNumArray).forEach(key=>{
        records.push(
          {
            id: recordOrderNumArray[key].$id,
            record: {
              orderNum: {
                value: recordOrderNumArray[key].orderNum
              }
            }
          }
        );
      });
      kintone.api(kintone.api.url('/k/v1/records.json', true), 'PUT', {
        app: this.appId,
        records: records
      });
    }

    // ”並び順”でソート済みのすべてのレコード取得するメソッド
    getSortedRecords(callback) {
      this.query = kintone.app.getQueryCondition() + 'order by orderNum asc';
      this.getRecords(callback);
    }

    // すべてのレコード取得するメソッド
    getRecords(callback) {
      kintone.api(kintone.api.url('/k/v1/records.json', true), 'GET', {
        app: this.appId,
        query: this.query + (' limit ' + this.limit + ' offset ' + this.offset)
      }, ((_this) => {
        return (res) => {
          Array.prototype.push.apply(_this.records, res.records);
          const len = res.records.length;
          _this.offset += len;
          if (len < _this.limit) {
            _this.ready = true;
            if (callback !== null) {
              callback(_this.records);
            }
          } else {
            _this.getRecords(callback);
          }
        };
      })(this));
    }
  }

  // レコード一覧画面
  kintone.events.on('app.record.index.show', (event) => {
    const kintoneRecordManager = new KintoneRecordManager();
    let sortableRecordsManager = null;

    kintoneRecordManager.getSortedRecords((sortedRecords) => {
      sortableRecordsManager = new SortableRecordsManager(sortedRecords);
      sortableRecordsManager.destroyTableRecords();
      sortableRecordsManager.createTableRecords();
    });

    $('#sortable_tbody').sortable({
      // 並び替えが終了し、要素の位置が変更されたときに呼び出されるイベント
      update: (e, ui) => {
        // 並び替えられた要素のidの配列を取得
        const updated = $('#sortable_tbody').sortable('toArray');
        // 並び替えられたidの配列を一つ前のidの配列と比較し、レコード番号と並び順の対を取得する
        const result = sortableRecordsManager.update(updated);
        // kintoneに結果を反映させる
        kintoneRecordManager.updateOrderNums(result);
      }
    });

    return event;
  });
})();
  • CSS
    • sample.css(以下のコードをコピーして保存し、アップロードしてください)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
 * Sort kintone records with Sortable
 * Copyright (c) 2014 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */
 
@charset "UTF-8";
.sortable-recordlist {
  border-radius: 3px;
  border-collapse: separate;
  border-top: 1px solid #f2f2f2;
  border-left: 1px solid #f2f2f2;
}
#sortable_thead th {
  background: #f2f2f2;
  padding: 10px;
}
#sortable_tbody th, td {
  padding: 10px;
} 
#sortable_tbody tr:nth-child(even) {
  background: #f2f2f2;
}
#sortable_tbody tr:nth-child(odd) {
  background: #ffffff;
}

JavaScriptの説明

①Sortableによる並び替え

jQuery UI Sortable に、カスタマイズビューで指定した HTML の tbody 要素(id が sortable_tbody の要素)を指定します。

②並び順の保存

並び順を保存するには、Sortable に update イベントを追加します。
これで”並び替えられた”を検知できます。
update イベント内の $('#id').sortable('toArray') で「並び替えられた要素の id の配列」を取得します。
この情報を kintone に反映させれば、並び順が保存できます。
kintone REST API の records.json の PUT メソッドで更新します。

北海道と岩手を並び替えてみると、以下のようになります。

しっかり並び替えが行われていますね。

最後に

今回、 Sortable (External link) を使ったレコードの並び替えについて説明しました。
並び替えまでは簡単ですが、並び順の保存まで行うと少し複雑になりますね。

information

この Tips は、2022 年 12 月版 kintone で動作を確認しています。