ユーザ用ツール

サイト用ツール


サイドバー

サイドバー

スタートページ

it技術:web開発:handsontable

Handsontable

概要

「Handsontable(ハンズオンテーブル)」は、WEBでExcelのようなスプレッドシートライクな入力を可能にしてくれるJavaScriptライブラリです。

デモ で見るととわかりますが、Excelのようにデータ入力ができるだけでなく、セルの書式の指定やチャートが作れたりと、機能が多いのも魅力です。

数あるJavascript用のフリーのグリッドライブラリの中でも使い勝手が一番いい。
フリーで商用利用可能なグリッドライブラリ - しぐれがきのブログ

公式サイト:https://handsontable.com/
ライセンスは、MITとなっている。有料版「Handsontable Pro」がある。

導入

Visual Studio上では、メニューの[ツール]→[NuGetパッケージマネージャー]→[ソリューションの NuGet パッケージの管理]にて、「handsontable」を検索してインストールすることが出来る。

使い方

Tips

セルの横移動の設定

Enter でセルを抜ける時、デフォルトだと下のセルに移動する。
これを横に移動するようにする。

enterMoves: { row: 0, col: 1}

セルの範囲選択を無効

範囲選択をできなくする
注意として一覧表示のみの際に範囲選択でコピーしてExcelに貼り付けるなどが出来なくなる。

  • 'area' を設定すると、ドラッグによる範囲選択ができなくなる。
  • 個々のセルを選択することは可能。
new Handsontable(grid, {
    disableVisualSelection: 'area'
});

Handsontable 使い方メモ2(グリッドのオプション)

読み取り専用(ReadOnly)の文字色を変更する

デフォルト値は「color: #777;」と薄いので、少し濃くする。

.handsontable .htDimmed {
    color: #444;
}

Handsontable ›Change background color of readonly columns

フィルハンドルを無効

フィルハンドルは、選択したセルの右下にカーソルをドラッグすることで、セルの内容を上下 or 左右にコピーできる。

new Handsontable(grid, {
    fillHandle: false
});

Handsontable 使い方メモ2(グリッドのオプション)

列の幅をドラッグで変更できるようにする

ドラッグで列の幅を変更できるようにする。

  • ダブルクリックすれば、セルの内容に合わせて幅を最小サイズにできる。
  • manualRowResize を使えば行のサイズ変更もできるようになる。
new Handsontable(grid, {
    manualColumnResize: true
});

Handsontable 使い方メモ2(グリッドのオプション)

最小余白の設定

最小余白行の設定

minSpareRows: 1,

最小余白列の設定

minSpareCols: 3,

行の高さが自動で変わる事象の解消

表示内容が見切れないように折り返して表示させる場合、行の高さがデータごとに変わる。
このとき、表の列が多く横スクロールが必要となる場合、表示している内容で行の高さが自動で変わり、どのデータにフォーカスが当たっているのか見分けがつかなくなってしまう。
以下の設定を行うことで、行の高さが自動で変わる事象を解消することができる。

autoRowSize: true

変更マークの付与

先頭列(Edit)に同じ行のいずれかの列の変更があった場合に「*」を付ける。
※例では、2行目は対象行のチェックボックス(Select)がある。

var  table = new Handsontable(grid,xxx);
 
// changesには[[row, col, pretext, aftertext]] でセットされる。
afterChange: function (changes, source) {
    if (source === 'loadData') return;
 
    for (var i = 0; i < changes.length; i++) {
        var change = changes[i];
        // 編集と選択は対象外
        if (change[1] === 'Edit' || change[1] === 'Select') continue;
        // 変更前と変更後が同じは対象外
        if (change[2] === change[3]) continue;
 
        // 編集に"*"を付ける
        table.setDataAtCell(changes[0][0], 0, '*');
    }
}

必須項目チェック

先頭列に編集マークがある場合のみ必須項目チェックを行う。
こうすることで新規行はチェックされない。
※編集マークがなければ beforeValidate でもいいと思う。

var  table = new Handsontable(grid,xxx);
 
afterValidate: function (isValid, value, row, prop, source) {
    if (table.getDataAtCell(row, 0) === '*' && (value === null || value === ""))
        return false;
}

桁数入力制限(MaxLength)

Handsontableのセル入力では、MaxLengthがサポートされていません。よって、ユーザー側でカスタムエディタで対応するようにしています。

Handsontable Limit Cell Characters

(function (Handsontable) {
 
'use strict';
 
var MaxLengthEditor = Handsontable.editors.TextEditor.prototype.extend();
 
MaxLengthEditor.prototype.prepare = function () {
    Handsontable.editors.TextEditor.prototype.prepare.apply(this, arguments);
 
    this.TEXTAREA.maxLength = this.cellProperties.maxLength;
};
 
Handsontable.editors.MaxLengthEditor = MaxLengthEditor;
Handsontable.editors.registerEditor('maxlength', MaxLengthEditor);
 
})(Handsontable);
 
// 使い方
var table = new Handsontable(grid, {
    data: data, 
    columns: [
       { type: 'text', maxLength: 5, editor: 'maxlength' }
    ]
}

バリデーション後の動作

バリデーションを組み込み、特定の条件を満たす値のみした許可しないようにすることができる。
また、設定によって、条件を満たさない場合の値が入力された場合の挙動を変えることが可能。

allowInvalid: true → 条件を満たさない値を入力した場合、セル色が赤くなる
allowInvalid: false → 条件を満たす値が入力するまで、フォーカスが外れない

カスタムバリデーション

例として日付の未来日はエラーとしたい場合

afterValidateの方法

日付の妥当性チェック後に未来日をチェックします。

afterValidate: function(isValid, value, row, prop, source) {
   if (this.propToCol(prop) === 4 && isValid){
      if (value > moment(new Date()).format("YYYY/MM/DD")) {
          return false;
      }
  }
}

customValidateの方法

未来日以外なら既存の日付の妥当性チェックを呼びます。

const dtFormat = 'YYYY/MM/DD';
 
var isFuture = function (value, callback) {
    if (moment(value, dtFormat).isValid() && value > moment(new Date()).format(dtFormat)) {
        callback(false);
    } else {
        Handsontable.validators.DateValidator.call(this, value, callback);
    }
};
 
var  table = new Handsontable(grid, {
    data: data, 
    columns: [
        {type: 'date',
            dateFormat: dtFormat,
            datePickerConfig: {
                maxDate: new Date()
            },
            validator: isFuture
         },

beforeValidateとafterValidateが効かない

text入力で afterValidateでまとめて桁数チェックをしようとしたが、afterValidateが動作しない。
無理矢理、validatorを経由させることでafterValidateの処理に入ってくるようになった。
※IE11対応なのでアロー演算子は使用していない。

{ data: 'Line', type: 'text', validator: function (value, callback) { callback(true); } }

ボタンでバリデーションを実行する

明示的に検証するには、validateCells() を使用する。他にvalidateColumns()、validateRows() がある。
バリデーションは末行末列から開始されるので順番には注意。

var  table = new Handsontable(grid,xxx);
 
table.validateCells(function(isValid) {
    if (isValid) {
        alert("Success");
    } else {
        alert("Error");
        e.preventDefault();
    }
})

複数列セルのバリデーションを順次実行する

非同期処理を操作する Promise を利用する。

列単位に順次チェック

validateColumnsで列ごとにエラーをチェックするが非同期なので、Promise でメソッドチェーン化して実行している。
エラーは catch で一括チェックする。
※validateCells だと未チェック列もエラーならセルが赤くなってしまう。

var  table = new Handsontable(grid,xxx);
 
function checkItem(item) {
    return new Promise(function (resolve, reject) {
        table.validateColumns([item], function (valid) {
            if (valid) {
                resolve();
            } else {
                reject();
            }
        });
    });
}
 
var check0002 = function () { return checkItem(2) }
var check0003 = function () { return checkItem(3) }
 
// 非同期チェック処理
var promise = Promise.resolve();
 
promise
.then(check0002)
.then(check0003)
.then(function () {
    return alert("成功");
}).catch(function () {
    // エラー表示
    return alert("失敗");
});

まとめてチェック

全てエラー箇所が赤くなる。最終的にはこっちを採用した。
エラーは catch で一括チェックする。

var  table = new Handsontable(grid,xxx);
 
var result = { msg: '', row: 0, col: 0 };
 
var checkAllItem = function () {
    return new Promise(function (resolve, reject) {
        table.validateCells(function (valid) {
            if (valid) {
                resolve();
            } else {
                result = { msg: 'All', row: 0, col: 0 };
                reject(result);
            }
        });
    });
}
 
// 非同期チェック処理
var promise = Promise.resolve();
 
promise
    .then(checkAllItem)
    .then(function () {
        return alert("成功");
}).catch(function (result) {
        return alert("失敗:" + result);
});

セルのバリデーション状態を取得する

validの戻り値は「false:不正値、true:正常値、undefined:なし」の3値あるので注意する。

var  table = new Handsontable(grid,xxx);
 
var isValid = table.getCellMeta(row, col).valid;
if (isValid === false) {
    alert("不正値");
}

フォーカスロストした時に選択状態を維持

選択位置は、getSelected() で取得することが出来る。
しかし、ボタンを押してフォーカスがGridから離れると getSelected() では取れなくなってしまう。
以下の設定を行うことで、フォーカスロストした時も選択状態が維持される。

outsideClickDeselects: false

デフォルトのoutsideClickDeselects: true で選択位置を取得するには、afterSelectionイベントで位置を保持する必要がある。
HandsonTableのサンプル

var selectedRow = 0;
var selectedCol = 0;
 
afterSelection: function (r, c, r2, c2) {
    // ボタンとかおしてフォーカスがGridから離れると
    // getSelected() では取れなくなるので、
    // このタイミングで常に保持しておいたほうが無難
    selectedRow = r2;
    selectedCol = c2;
}

指定セルのフォーカスをセット

指定は行、列の順番

var  table = new Handsontable(grid,xxx);
 
table.selectCell(row, col);

フォーカスの行単位移動

一覧表示のみの場合、フォーカスを行単位で移動させたい
Programmatically select entire row

<style type="text/css">
    .handsontable td.currentRow, .handsontable td.current {
        background: rgb(230, 239, 254);
    }
</style>
 
<script type="text/javascript">
 var grid = document.getElementById('grid');
 var table = new Handsontable(grid, {
        data: [],
        currentRowClassName: 'currentRow'
  });
</script>

下に行を挿入

コンテキストメニューには「上に行を挿入:Insert Row Above」と「下に行を挿入:Insert Row Below」と2種類あるが、メソッドとしては、上側に行追加するしか用意されていない。

行追加位置を +1 加算して実現する。

var  table = new Handsontable(grid,xxx);
 
var sel = table.getSelected();
table.alter('insert_row', (sel[0][0]) + 1);

最下行に追加

現在の行数で追加すればいい。

var  table = new Handsontable(grid,xxx);
 
table.alter('insert_row', table.countRows());

チェックした行を削除

先頭列にチェックボックスがあって、行削除ボタンでチェック対象を行削除する。
※ループ処理で行を削除する場合は、下から上に向かって行うのが鉄則です。

var  table = new Handsontable(grid,xxx);
 
for (var i = table.countRows() - 1; i >= 0; i--) {
    if (table.getDataAtCell(i, 0) === true)
    {
        table.alter('remove_row', i);
    }
}

全選択/全解除チェック

全選択/全解除チェックボックスがあり、全行の先頭列チェックボックスを全選択/全解除チェックボックスの値を同じにする。
※IE11では40件データで15秒かかった。Chromeは速い。仕方ないのでデータを読み直す方式に変更した。

var  table = new Handsontable(grid,xxx);
 
for (var i = 0; i < table.countRows() ; i++) {
    table.setDataAtCell(i, 0, Checkbox.checked)
}

その後に調査した結果、populateFromArrayで高速化できることが分かった。

var  table = new Handsontable(grid,xxx);
 
var col = table.propToCol('Select');
table.populateFromArray(0, col, [[Checkbox.checked]], table.countRows() - 1, col, null, null, 'down');

setDataAtCellのパフォーマンスの問題

Performance issue with instance.setDataAtCell()

パフォーマンスサンプル
http://jsfiddle.net/1kruLmjo/

setDataAtCell は遅いので、getDataにアクセスして変更する。
※全選択/全解除チェックに書いたが、populateFromArrayで高速化できることが分かった。

setDataAtCell.addEventListener('click', function() {
    for(var i = 0; i < hot1.countRows(); i++) {
      hot1.setDataAtCell(i, 0, 'value')
    }
});
 
loadData.addEventListener('click', function() {
    var tab = hot1.getData();
    for(var i = 0; i < hot1.countRows(); i++){
        tab[i].shift();
        tab[i].unshift('value');
    }
    hot1.loadData(tab)
});

行の文字色を変更する

一覧表示で結果列に「エラー」があった場合に、対象行を赤色の文字にする。

listRenderer = function (instance, td, row, col, prop, value, cellProperties) {
    Handsontable.renderers.TextRenderer.apply(this, arguments);
    if (instance.getDataAtCell(row, 4) === 'エラー') {
        td.style.color = 'red';
    }
};
 
var  table = new Handsontable(grid,xxx);
 
cells: function (row, col, prop) {
    if (this.instance.getData().length == 0) return;
    this.renderer = listRenderer;
},

最初は次のようにcellsイベント側でエラー判定をしていたが、列ソート(columnSorting:true)させると赤色になる行が崩れることが分かった。列ソートさせないなら、こちらでも良い。

listRenderer = function (instance, td, row, col, prop, value, cellProperties) {
    Handsontable.renderers.TextRenderer.apply(this, arguments);
    td.style.color = 'red';
};
 
var  table = new Handsontable(grid,xxx);
 
cells: function (row, col, prop) {
    if (this.instance.getData().length == 0) return;
    if (this.instance.getDataAtCell(row, 4) === 'エラー') {
        this.renderer = listRenderer;
    }
},

セルの文字色を変更する

結果列に「エラー」のセルのみを赤色の文字にする。
valueの値を使用して判定する。

listRenderer = function (instance, td, row, col, prop, value, cellProperties) {
    Handsontable.renderers.TextRenderer.apply(this, arguments);
    if (value === 'エラー') {
        td.style.color = 'red';
    }
};
 
var  table = new Handsontable(grid,xxx);
cells: function (row, col, prop) {
    if (this.instance.getData().length == 0) return;
    this.renderer = listRenderer;
},

Conditional formatting demo

リサイズでサイズ変更する

リサイズ後にサイズを変更する。リサイズ後(resized)イベントは、下記サイトを参考
JavaScript で window リサイズ終了後のタイミングをハンドリングする方法

Boostrapのグリッドシステムを採用している場合、divタグに“id=Box”属性を付けて横幅を取得する。
その横幅を少し小さめにHandsonTableのwidthにセットしている。そうしないと縦スクロールバーが見えなくなってしまう。
縦幅は最低限表示するサイズ(150)と、最大で表示するサイズ(400)を指定している。

var resizeTimer;
var interval = Math.floor(1000 / 60 * 10);
 
// リサイズ完了時
window.addEventListener('resize', function (event) {
    if (resizeTimer !== false) {
        clearTimeout(resizeTimer);
    }
    resizeTimer = setTimeout(function () {
        resize();
    }, interval);
})
 
// リサイズ
function resize() {
    var client_w = document.getElementById('box').clientWidth;
    var client_h = window.parent.screen.height;
    //alert("Resized client_w:" + client_w + " x client_h:" + client_h);
 
    table.updateSettings({
        width: client_w - 20,
        height: Math.min(Math.max(150, client_h - 440), 400)
    });
}

セレクトボックス

一般的にデータをセット

columns: [
      {
        type: 'dropdown',
        source: ['yellow', 'red', 'orange', 'green', 'blue', 'gray', 'black', 'white']
      }
]

type: 'handsontable' とすると複数列のセレクトボックスの表示が可能
https://docs.handsontable.com/5.0.2/demo-handsontable.html

関数にて配列を返す

ASP.NET + MVC5

columns: [
{ type: 'dropdown', source: time_list() }
]
 
 
// 時間(分 00~23)のプルダウンリスト
function time_list() {
    list = [];
    for (i = 0 ; i < 24; i++) {
        list.push(('0' + i).slice(-2));
    }
    return list;
}

関数にてサーバーのデータを返す

columns: [
{ type: 'dropdown', source: store_list() }
]
 
// ストアをプルダウンリスト
function store_list() {
    var list = [];
 
    jQuery.ajax({
        url: '/Home/GetStore',
        type: "GET",
        dataType: "json",
        contentType: 'application/json; charset=utf-8',
        async: true,
        processData: false,
        cache: false
    }).done(function (data) {
        for (i in data) list.push(data[i]);
    });
 
    return list;
}

リストの高さ

リストが少ないとスクロールバーが付いてしまう場合の対応
Dropdown/autocomplete menu hidden with preventOverflow

.handsontableEditor.autocompleteEditor,
.handsontableEditor.autocompleteEditor .ht_master .wtHolder {
  min-height: 138px;
}

リストが多い場合の高さ制御
Handsontable in Handsontable Dropdown Height Adjustment

リストの背景色と選択色

Handsontableのリスト表示が非常に見難い。またHandsontableの高さを指定しているとリストが下に隠れてしまうが、解消方法は見つかっていない。

最低限、色を変更することで見やすくする。
How to change default highlighted color of dropdown in handsontable to another color

.handsontable.listbox tr td.current, .handsontable.listbox tr:hover td {
    background: #d1ecf1;
}
 
.handsontable.listbox tr td {
    background: #fafaca;
}

その他

ドラッグで行追加を禁止にする

ドラッグすると固定にしたシートの行が自動で追加されてしまう。
afterCreateRowのfunctionにdata.splice(index, amount)を追加すると行が固定されるようになる。

[Handsontable.js]ドラッグで行追加を禁止にする方法

列の非表示

特定の列の幅を0.1ピクセルに縮小できます。技術的にはテーブルの一部であり、.getData()は列のデータを返しますが、人間の目には見えません。※IE11では、チェックボックスにしていたためかセルが表示されてしまう不具合があった。
セル移動した際に、非表示列にもフォーカスが移動して値が見えてしまう。

handsontable.updateSettings({
    colWidths: [0.1,0.1,50],
});

非表示列の値の取得

バインドしてある前提で、幅を0.1にしなくても非表示列の値が取得できる方法がある。

var table = new Handsontable(grid,xxx);
 
if (table.getSourceDataAtCell(row,"DBRead") === true)

※DBから読んだデータと新規入力のデータと分ける上でDBRead列がtrueで判定する場合を想定。

getSourceDataAtCell returns same as getDataAtCell

セルにボタン表示

標準ではボタンは用意されていないが、HTMLを表示出来ることを利用する。

DateTimePickerの時間を組み合わせて、ボタンを押したら時間入力ダイアログを表示して時間をセットする。

<link href="../../Content/tempusdominus-bootstrap-4.min.css" rel="stylesheet">    
<script src="../../scripts/moment-with-locales.min.js"></script>
<script src="../../scripts/tempusdominus-bootstrap-4.min.js"></script>
 
<div class="modal" id="timeModal" tabindex="-1" role="dialog" aria-labelledby="modal" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered modal-sm" role="document">
        <div class="modal-content">
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
            <div class='input-group justify-content-center' id='datetimepicker1'></div>
        </div>
    </div>
</div>
$('#datetimepicker1').on('change.datetimepicker', function(e) {
    if(selectedCol === 0) return;
    table.setDataAtCell(selectedRow, selectedCol, e.date.format("HH:mm"));
});
 
function myBtns(instance, td, row, col, prop, value, cellProperties) {
    Handsontable.renderers.TextRenderer.apply(this, arguments);
    if (value === null) value = "00:00";
    td.innerHTML = '<button class="myBtn bt-' + row + '">' + value + '</button>'
}
 
var  table = new Handsontable(grid, {
 
    cells: function(row, col) {
        var cellPrp = {};
        if (col === 5) {
            cellPrp.renderer = myBtns;
            cellPrp.readOnly = true;
        }
        return cellPrp
    },
    afterOnCellMouseDown: function(event, cords, TD) {
        if (event.realTarget.className.indexOf('myBtn') < 0) {
            return;
        }
 
        var time = table.getDataAtCell(selectedRow, selectedCol);
        if (time === null) time = "00:00";
        $('#datetimepicker1').data("datetimepicker").defaultDate(moment(moment().format('L') + ' ' + time));
        $('#timeModal').modal();
    }
    afterSelection: function (r, c, r2, c2) {
        // ボタンとかおしてフォーカスがGridから離れると
        // getSelected() では取れなくなるので、
        // このタイミングで常に保持しておいたほうが無難
        selectedRow = r2;
        selectedCol = c2;
    }
 
}

セルにカスタムHTML表示

HTMLを表示できるので、リンクや画像を表示できる。
Rendering custom HTML in cells

Jsonデータのロード

handsontableのcolumns内にバインド名(例 data: “price”)を指定しないと表示されない。

※カラムのタイプの指定が無い場合はバインド名の指定が無くても表示される。
※カラムのタイプがチェックボックスの場合、JSONデータは true/false にしないとエラーになる。

var data = [
    { "price": "1100"  , "mt": "7"  ,  "sts": "8316"  },
    { "price": "200"   , "mt": "3"  ,  "sts": "648"   },
    { "price": "1200"  , "mt": "2"  ,  "sts": "2592"  }
];
 
$('#example').handsontable({
   data: data,
   minSpareRows: 0,
   colHeaders: ["価格", "本数", "小計(税込)"],
   columns: [
               { data: "price" ,  type: 'numeric' , format: '0,0' },
               { data: "mt"    ,  type: 'numeric' },
               { data: "sts"   ,  type: 'numeric' , format: '0,0' , readOnly: true },
   ],

参照

Jsonデータのまま保存する

JsonデータをloadDataメソッドでHandsontableでデータにロードした場合、データを保存する時にもJsonデータのままサーバー側に渡したい場合、getDataではなくgetSorceData を使用する。

Posting Handontable JSON using Ajax to ASP.net MVC controller not working Since "0.22.0"

getSourceDataとgetDataの違い

  • getSourceData() で取得したデータは元となったデータ src と同じインスタンスを指している。
  • 一方で、 getData() で取得したデータは、元のデータとは別のインスタンスになっている。

getSourceData:ソースデータをそのまま取得する

カレンダーの日本語化

プログラムで設定変更することは出来そうもないので、handsontable.full.js を直接変更する。
変更後はツールを使うなどして、handsontable.full.min.js に変換するなりしておく。
Windows環境でYUI Compressorを使う

datePickerConfig で細かな変更が可能。

datePickerConfig: {
    // First day of the week (0: Sunday, 1: Monday, etc)
    firstDay: 1
}

Handsontable 使い方メモ3(カラム・セルオプション)

※各設定の意味は、PikaDay.jsメモ を参考にする。

例えば、firstDayを1にすると月曜始まりのカレンダーとなる。工場を持つ企業などで使われている。

「moment.locale('ja');」を実行すれば、monthsとweekdaysとweekdaysShortは、Momentから取得できる。

moment.locale('ja');
 
datePickerConfig: {
  // first day of week (0: Sunday, 1: Monday etc)
  firstDay: 0,
 
  // Additional text to append to the year in the calendar title
  yearSuffix: '年',
 
  // Render the month after year in the calendar title
  showMonthAfterYear: true,
 
  // Render days of the calendar grid that fall in the next or previous month
  showDaysInNextAndPreviousMonths: true,
 
  // internationalization
  i18n: {
      previousMonth : '前月',
      nextMonth     : '次月',
      months: moment.localeData()._months,
      weekdays: moment.localeData()._weekdays,
      weekdaysShort: moment.localeData()._weekdaysShort
  }
},

土日の色を変更する

CSS3の擬似クラスを使用して、色を変更する。handsontable用のcssファイルに含めて置き換えてもいいし、外出しにしてもいい。
何番目系の便利なCSSまとめ

日曜始まり
.pika-lendar th:first-child,
.pika-lendar td:first-child .pika-button {
    color: #f00;
}
.pika-lendar th:last-child,
.pika-lendar td:last-child .pika-button {
    color: #00f;
}
月曜始まり
.pika-lendar th:nth-last-child(2),
.pika-lendar td:nth-last-child(2) .pika-button {
    color: #00f;
}
.pika-lendar th:last-child,
.pika-lendar td:last-child .pika-button {
    color: #f00;
}

Pikaday.js 祝日対応とMoment不使用フォーマット整形

変更後イメージ

it技術/web開発/handsontable.txt · 最終更新: 2018/09/23 23:05 by yajuadmin