はじめに
Handsontableで、ヘッダーの複数行表示やセルの結合を行うことになり、実装方法の調査を行った。機能としては有料版にあるが、無料で実装したかったので、@reds26さんの『Handsontableのヘッダーをカスタマイズして複数行表示や結合に対応』を拝借させていただくことにした。
問題点
ただ、自身が実装するテーブルにおいて、以下の問題点が発生した。
- 列数が多く幅が長くなると、スクロールした時に、ヘッダーとデータのカラムの位置がずれてしまう。
- ヘッダーとデータの列数が異なる時、列幅調整を行うと、異なる列の列幅が調整されてしまう。(結合されている列数分ずれた位置を調整する。)
原因
afterGetColHeader gets called many times for the same column
上記のリンクにafterGetColHeaderイベントに関する内容が記載されていたので、読んでみたところ、このイベントはrender()が呼ばれる度に発生すると書かれていた。自身で検証した際には、列幅調整をしたりスクロールすると、各列に対してイベントが複数回呼ばれていた。(スクロールでは、イベントが発生しないときもあった)
afterGetColHeaderでのヘッダーの生成がうまく処理しきれず、ヘッダーとデータのカラムの位置ずれが発生していたものと思われる。
列幅調整のカラムの位置ずれは、列幅調整時にセルの結合が考慮されていないせいだと思われる。(無料版で意図しない拡張であるから、当然ではあるが。)
実装方法
上記の問題を回避する為に、afterGetColHeaderの第2引数にヘッダーのTH要素が渡されるので、そのTH要素の内容を書き換えることにした。TH要素にdivタグを複数埋め込んで複数行に見せたり、罫線の指定で、セルの結合をしているかのように見せかけたりしている。
<div id="grid"></div>
.handsontable th {border-right: none }
.inner-border { border-bottom: solid thin #ccc }
.border-right { border-right: solid thin #ccc }
.student-name { height: 48px; border-right: solid thin #ccc }
document.addEventListener("DOMContentLoaded", function() {
var data = [
['*** ***', 90, 70, 88, 100, 92, 95, , 98, 99, 100, 55, 60 ],
['*** ***', 89, , 88, 100, 92, 95, 97, 98, 55, 92, 55, 60 ],
['*** ***', 100, 70, 82, 99, 92, 95, 97, , 69, 88, 55, ],
['*** ***', 77, 91, 81, 75, 91, 75, 96, 91, 77, 96, 55, 60 ],
];
var periods = [
{year: 2018, startMonth: 7, endMonth: 12},
{year:2019, startMonth: 1, endMonth: 6},
];
var headerHtml = getHeaderHtml(periods); /* THに埋め込むhtmlを列ごとに作成しておく */
var colWidths = [ 100 ];
for (var i = 1; i < colWidths.length; i++) colWidths.push(60);
new Handsontable(document.getElementById('grid'), {
data: data,
manualColumnResize: true,
colHeaders: true,
colWidths: colWidths,
afterGetColHeader: function(col, TH) {
TH.innerHTML = headerHtml[col];
},
});
}, false);
function getHeaderHtml(periods) {
var headerHtml = [getThHtml('氏名', 'student-name')];
for (var idx in periods) {
var period = periods[idx];
for (var month = period.startMonth; month <= period.endMonth; month++) {
var year = '';
var yearClsName = 'inner-border';
if (month == period.startMonth) {
year = period.year;
yearClsName += ' empty';
}
if (month == period.endMonth) yearClsName += ' border-right';
headerHtml.push(getThHtml(year, yearClsName) + getThHtml(month, 'border-right'));
}
}
return headerHtml;
}
function getThHtml(text, clsName) {
// Handsontableが自動で生成するヘッダーの内容に合わせる
return '<div class="relative '+ clsName +'"><span class="colHeader">' + text + '</span></div>';
};
最後に
Handsontableのように、ブラックボックスな部分で複雑な処理を行っているものは、提供されている機能以外で無理に拡張しようとすると不具合を回避するのが難しくなる。今回はやりたいことができて良かったが、基本的には、他のライブラリや有料版の購入を検討した方が良いと思われる。