インタラクティブなテーブルを作成するjQueryプラグイン"Tabulator"のカスタマイズ

# はじめに

Webサイトのスクレイピング技術等を学ぶため，家庭/大学の研究室/小規模事業所向けの自動商品発注システムを作成してみました．[前回](http://qiita.com/nobu_e753/items/e18483abc77585b95bf8)はいわゆる"ダッシュボタン"タイプのシステムを構築しましたが，今回は一般的なWebUI画面を利用した発注システムを作成してみました．

![overview.png](https://qiita-image-store.s3.amazonaws.com/0/156705/3ba0228a-a69b-88fd-193c-e45bd4d03d5f.png)

UI画面をポチポチして品数を入れておくと，週1回，月1回といった間隔で指定のECサイトにて商品を自動で注文してくれます．

- 「家庭で月1回，風呂用洗剤を1つ買います」
- 「研究室で月1回，缶コーヒーを2箱買います」
- 「小規模事業所で月1回，トイレットペーパーを3セット買います」

といった用途を想定しています．．

# とりくみ

このシステムを作成する際，**手間のかかる表示系（HTML画面上でのテーブル）の作成を「何とか楽にしたい！（＝素のHTMLとJavascriptで一から書くにはつらい）」**という思いがありました．いろいろ探した結果，jQueryのプラグインである[Tabulator](https://olifolkerd.github.io/tabulator/)を使うことにしました．今回はTabulatorのカスタマイズ方法についてまとめてみます．

なお"他サービス/プラットフォームと連携してもよい"という条件ならばUI部は Google Spreadsheet / App Script での作成をお勧めします（個人的に一押し）．実は最初の構成は Google Spreadsheet / App Script + 発注サービスAPIという構成だったのですが，UI部とAPIを別々におくとセキュリティなどがいろいろと面倒そうに思えたのでフルスクラッチで構成することにしました（GCPを利用すればこの辺もカバーできるのでしょうか？？）．

# 環境

- Javascript
- jQuery（1.11.1）
- [Tabulator(2.12)](https://olifolkerd.github.io/tabulator/)

# Tabulatorとは

HTML上で「そこそこ見栄えが良く，インタラクティブなテーブルを作成すためのjQueryライブラリ」です．本家ページに[デモサイト](https://olifolkerd.github.io/tabulator/examples/)があるので，こちらを参照ください．こんな感じのテーブルを楽に作成てきます．基本的な使い方は，[こちら](https://www.webprofessional.jp/dynamic-tables-json/)や[こちら](http://suzaku-tec.hatenadiary.jp/entry/2017/01/01/170415)のページが参考になります．

類似のライブラリとしては

- [SlickGrid](https://github.com/mleibman/SlickGrid)
- [w2ui Grid](http://w2ui.com/web/home)

があるようです．GitHubでの更新頻度やカスタマイズしやすそうなことから，今回はTabulatorを選びました．

# TabulatorでのUI作成

商品発注画面は以下のようなものにしました．「個数」の欄をクリックすると数が変更できるようになっています．また，定期発注の結果を「発注」欄に表示するようにしています（発注成功なら"チェック"，売り切れなどで発注失敗の場合は"×"，注文なき場合は"-"）

![table.png](https://qiita-image-store.s3.amazonaws.com/0/156705/a3b6cb60-0457-833b-9775-cf988ed25bcc.png)

上記テーブルを構成するための書式は以下になります．プリセットを使うだけでなく，いくつか独自のカスタマイズを行っています．以下に個々の例について説明してゆきます．

```javascript:TableHeader.js
order_table : [
    {title:"ID", field:"id",
        align:"center", editable:false, sortable:false, sorter:custom_sorter.number},
    {title:"商品", field:"product",
        editable:false, sortable:false, formatter:"plaintext",  width:310},
    {title:"カテゴリー", field:"category",
        visible:false},
    {title:"購入単位", field:"quantity",
        editable:false, sortable:false, formatter:"plaintext",  width:105},
    {title:"レーティング", field:"rating",
        editable:false, sortable:false, formatter:"star", formatterParams:{stars:6}, width:115},
    {title:"個数", field:"unit",
        align:"center", editor:custom_editor.number, sortable:false, editable:true, cssClass:"colored_number"},
    {title:"発注", field:"status",
        align:"center", formatter:custom_formatter.tristate, sortable:false, editable:false}
]

$("#order_table").tabulator({
    groupBy: "category",
    columns: order_table,
    cellEdited: changeUnit
});
```

## IDを昇順に（`sorter`のカスタマイズ）

（私のやり方が悪いのか）デフォルトの状態ではIDが降順（IDが大きいものが一番上）表示になってしまいました．別の問題ないと言えば問題ないのですが，やっぱり昇順で表示したいです．そこで，`sorter`を自身で定義したものに置き換えることで，昇順にソート/表示できるようにしました．

```javascript:TableHeader.js（再掲）
order_table : [
    {title:"ID", field:"id",
        align:"center", editable:false, sortable:false, sorter:custom_sorter.number},
    ...
```

```javascript:TableCustomize.js
// custom_sorter の定義
custom_sorter : {
    number : function(a, b, aData, bData, field, dir){
        return b - a;  // 昇順
        //return a - b;  // 降順
    }
};
```

同様のインタフェースを持った関数を定義しヘッダ定義時に渡してやることで，好きなソート方法を定義・適用してやることができます．`aData`,`bData`には列の情報が入っているので「個数が1個以上の商品を一番上に表示する」といった`sorter`の定義も可能です．

## 個数を0以上に限定（`editor`のカスタマイズ）

個数の欄をクリックすると数を変更できるのですが，デフォルト（`editor="number"`）だとマイナスや現実にあり得ない数値も入力できてしまいます．「コーヒーをマイナス2箱」「トイレットペーパー100セット」などという注文が来ても困りますので，UI側で防止できるようにしました（注文をうけとったサーバー側で処理する，というスタイルもあり得るでしょうが）．

```javascript:TableHeader.js（再掲）
order_table : [
    ....
    {title:"個数", field:"unit",
        align:"center", editor:custom_editor.number, sortable:false, editable:true, 
    ....
```

```javascript:TableCustomize.js
// custom_editor の定義
custom_editor : {
    number : function(cell, value, data){
        // tabulator.js の line[4381]から抜粋・修正
        // 元の定義は `$('<input type="number"/>')` になっている
        const input = $('<input type="number" min="0" max="8"/>');
        input.css({
            "padding":"4px",
            "width":"100%",
            "box-sizing":"border-box",
            "font-size": "16px",
        })
        .val(value);

        if(cell.hasClass("tabulator-cell")){
            setTimeout(function(){
                input.focus();
            },100);
        }

        //submit new value on blur
        input.on("blur", function(e){
            cell.trigger("editval", input.val());
        });

        //submit new value on enter
        input.on("keydown", function(e){
            if(e.keyCode == 13){
                cell.trigger("editval", input.val());
            }
        });

        return input;
    },
};
```

## 発注状況を3状態で表現できるように（`formatter`のカスタマイズ）

表示方式は`formatter`を再定義することでカスタマイズできます．プリセットには2状態を表す`formatter="tick"`, `formatter="tickCross"`があるのですが，「発注失敗」と「発注せず」を区別するの3状態で表現したかったので以下のように再定義しました．また，編集機能は不要と判断したので削ってあります．

```javascript:TableHeader.js（再掲）
order_table : [
    ....
    {title:"発注", field:"status",
        align:"center", formatter:custom_formatter.tristate, sortable:false, editable:false}
]
```

```javascript:TableCustomize.js
// custom_formatter の定義
custom_formatter: {
    tristate : function(value, data, cell, row, options){
        // tabulator.js の line[4579]から抜粋・修正
        // 元の定義は `$('<input type="number"/>')` になっている
        const tTrue = '<svg enable-background="new 0 0 24 24" height="14" width="14" viewBox="0 0 24 24" xml:space="preserve" ><path fill="#2DC214" clip-rule="evenodd" d="M21.652,3.211c-0.293-0.295-0.77-0.295-1.061,0L9.41,14.34  c-0.293,0.297-0.771,0.297-1.062,0L3.449,9.351C3.304,9.203,3.114,9.13,2.923,9.129C2.73,9.128,2.534,9.201,2.387,9.351  l-2.165,1.946C0.078,11.445,0,11.63,0,11.823c0,0.194,0.078,0.397,0.223,0.544l4.94,5.184c0.292,0.296,0.771,0.776,1.062,1.07  l2.124,2.141c0.292,0.293,0.769,0.293,1.062,0l14.366-14.34c0.293-0.294,0.293-0.777,0-1.071L21.652,3.211z" fill-rule="evenodd"/></svg>';
        const tFalse = '<svg enable-background="new 0 0 24 24" height="14" width="14"  viewBox="0 0 24 24" xml:space="preserve" ><path fill="#CE1515" d="M22.245,4.015c0.313,0.313,0.313,0.826,0,1.139l-6.276,6.27c-0.313,0.312-0.313,0.826,0,1.14l6.273,6.272  c0.313,0.313,0.313,0.826,0,1.14l-2.285,2.277c-0.314,0.312-0.828,0.312-1.142,0l-6.271-6.271c-0.313-0.313-0.828-0.313-1.141,0  l-6.276,6.267c-0.313,0.313-0.828,0.313-1.141,0l-2.282-2.28c-0.313-0.313-0.313-0.826,0-1.14l6.278-6.269  c0.313-0.312,0.313-0.826,0-1.14L1.709,5.147c-0.314-0.313-0.314-0.827,0-1.14l2.284-2.278C4.308,1.417,4.821,1.417,5.135,1.73  L11.405,8c0.314,0.314,0.828,0.314,1.141,0.001l6.276-6.267c0.312-0.312,0.826-0.312,1.141,0L22.245,4.015z"/></svg>';
        const tNone = '<svg enable-background="new 0 0 24 24" height="14px" width="14px" viewBox="0 0 24 24" xml:space="preserve"><rect style="stroke-linejoin: round; fill: rgb(189, 189, 189);" x="1.968" y="10.005" width="20.077" height="4.23"/><rect transform="matrix(1, 0, 0, 0.999996, -55.325764, -37.965452)" style="fill: rgb(216, 216, 216);" x="25.881" y="25.081" width="1.334" height="0.267"/></svg>';

        if (value === "" || value === null || value === 0)
            return tNone;
        else if (value === true || value === "true" || value === "True" || value > 0)
            return tTrue;
        else
            return tFalse;
    }
};
```

表示する図表についてはsvg形式で記述しましたが，画像を貼り付ける方式でもできそうです．Face icon <i class="fa fa-smile-o"></i> にするとか，サムズアップ <i class="fa fa-thumbs-o-up"></i> にするのもよいでしょう．

# これ以外の項目をカスタマイズするには？？

表示/編集方法についてはTabulatorの定義ファイル（[tabulator.js](https://github.com/olifolkerd/tabulator/blob/master/tabulator.js)）のどこかで定義がなされています．カスタマイズしたい項目（`formatter`, `editor`, `sorter`など）が定義されている箇所を検索し，同じ関数インタフェースを持った辞書オブジェクトとすればよさそうです．

# まとめ

HTML/Javascriptにてインタラクティブなテーブルを作成すためのjQueryライブラリ"Tabulator"の，表示/編集方法をカスタマイズする方法をまとめました．