はじめに
最近Webアプリ周辺の技術を学び始めた者です。普段は製造業で設計業務を担当しておりますが、社内システム構築用にいろいろと勉強しています。主に使うのは下記。
- 言語 : python/C#
- Web framework : django
記事に書き出すことで自身の理解も深まると考え、今回初投稿をさせて頂きます。
動機
休日に妻と共同でアプリ開発をしています(この開発記録もつけられたらなーと思っています)。
tableを多用するのですが、その際列ヘッダーと行ヘッダーを固定してtbodyのデータセルだけスクロールできないかなと考え、いろいろ調べていました。
便利なプラグインもたくさんありましたが、
- 複数ヘッダーを固定できるものが限られていた
- なるべく既存のtableに変更を加えたくない
という理由で手を出せず。position:stickyというステキなオプションがあるので、どうにかこれを使ってできないかと思い、やってみました。
なおこちらのstackoverflowの質問を参考にしました→Table with fixed header and fixed column on pure css
html
下記のようなtableと、wrapperとなるdivを用意します。class名はbootstrapを意識しています。
<div class="table-wrapper">
<table class="table text-nowrap sticky-table table-borderless">
<thead class="thead-light">
<tr class="fixed-header-0">
<th class="fixed-column-0">日付</th>
<th>1/1</th>
<th>1/2</th>
<th>1/3</th>
<th>1/4</th>
<th>1/5</th>
<th>1/6</th>
<th>1/7</th>
</tr>
<tr class="fixed-header-1">
<th class="fixed-column-0">曜日</th>
<th>月</th>
<th>火</th>
<th>水</th>
<th>木</th>
<th>金</th>
<th>土</th>
<th>日</th>
</tr>
</thead>
<tbody>
<tr>
<th class="fixed-column-0 table-light">AM</th>
<td>〇</td>
<td>〇</td>
<td>〇</td>
<td>×</td>
<td>×</td>
<td>〇</td>
<td>〇</td>
</tr>
<tr>
<th class="fixed-column-0 table-light">PM</th>
<td>×</td>
<td>〇</td>
<td>〇</td>
<td>×</td>
<td>〇</td>
<td>×</td>
<td>〇</td>
</tr>
<tr>
<th class="fixed-column-0 table-light">夜</th>
<td>〇</td>
<td>〇</td>
<td>〇</td>
<td>〇</td>
<td>〇</td>
<td>〇</td>
<td>×</td>
</tr>
</tbody>
</table>
</div>
ゴールは上記tableの「日付」「曜日」行を固定しつつ、「AM」「PM」「夜」も常に表示することです。
wrapperにはclass=table-wrapper、tableにはclass=sticky-tableを指定します。また、固定したい行ヘッダーとなる各trには上から順に**class=fixed-header-n(nは0始まりの番号)を指定し、固定したい列ヘッダーとなる各thには左から順にclass=fixed-column-m(mは0始まりの番号)**を指定します。
tableの左上の場所は行列方向に拘束したいので、tr.fixed-header-nおよびth.fixed-column-mの両方の指定が必要です。
ちなみにヘッダーにtable-lightやthead-lightで色を付けているのは、透明のままだとヘッダー固定したときに他のセルと重なってしまうからです。
css/js
下記のようなcssとjsを作成します。
/*ラッパー*/
div.table-wrapper {
overflow: scroll;
max-height:200px; /*任意*/
max-width:400px; /*任意*/
}
/*行ヘッダーを固定する。topの値はjsで動的に指定*/
table.sticky-table thead tr[class*="fixed-header-"] th {
position: -webkit-sticky; /* for Safari */
position: sticky;
/* tbody tdより手前に表示する */
z-index: 1;
}
/*行ヘッダーと列ヘッダーが重なる部分を固定する。top,leftの値はjsで動的に指定*/
table.sticky-table thead tr[class*="fixed-header-"] th[class*="fixed-column-"] {
/* 全てのセルより手前に表示する */
z-index: 2;
}
/*列ヘッダーを固定する。leftの値はjsで動的に指定*/
table.sticky-table tbody th[class*="fixed-column-"] {
position: -webkit-sticky; /* for Safari */
position: sticky;
/* tbody tdより手前に表示する */
z-index: 1;
}
コメントを添えていますが、
div.table-wrapper {
overflow: scroll;
max-height:200px; /*任意*/
max-width:400px; /*任意*/
}
はwrapperの挙動です。max-height/max-widthは任意の値に設定してください。
続いて下記のような.jsを作成します。結局jqueryで書いてしまった。
//行ヘッダーに対しtopを設定
height = 0;
for (var i = 0; i < fixed_header_num; i++) {
$(".fixed-header-" + i + " th").css('top', height);
height += $(".fixed-header-" + i + " th").outerHeight();
}
//列ヘッダーに対しleftを設定
width = 0;
for (var j = 0; j < fixed_column_num; j++) {
$("th.fixed-column-" + j).css('left', width);
width += $("th.fixed-column-" + j).outerWidth(true);
}
jsでは固定したい各ヘッダーに対し、「どこまでの位置に達したら上/左方向への移動を拘束するか」の値となるtop/leftの値を動的に設定しています。一番上のヘッダーはtop=0でよいのですが、二番目以降のヘッダーは自身の上にあるヘッダーの累積高さ分の値を設定しています。列ヘッダーも同様。
fixed_header_num, fixed_column_numはそれぞれ、固定したい行/列ヘッダーの数なのですが、これらは使用シーンに合わせて変わると思うので、グローバルで宣言することにします。
使用例
下記のツリー構造を仮定します。
sticky-table/
├ sample.html
└ static/
├ css/
│ └ sticky-table.css
└ js/
└ sticky-table.js
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta charset="utf-8" />
<title>ヘッダー固定</title>
<!--bootstrap-->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<!--sticky-table-->
<link rel="stylesheet" href="static/css/sticky-table.css">
</head>
<body>
<!--https://stackoverflow.com/questions/15811653/table-with-fixed-header-and-fixed-column-on-pure-css-->
<div class="container">
<div class="table-wrapper">
<table class="table text-nowrap sticky-table table-borderless">
<thead class="thead-light">
<tr class="fixed-header-0">
<th class="fixed-column-0">日付</th>
<th>1/1</th>
<th>1/2</th>
<th>1/3</th>
<th>1/4</th>
<th>1/5</th>
<th>1/6</th>
<th>1/7</th>
</tr>
<tr class="fixed-header-1">
<th class="fixed-column-0">曜日</th>
<th>月</th>
<th>火</th>
<th>水</th>
<th>木</th>
<th>金</th>
<th>土</th>
<th>日</th>
</tr>
</thead>
<tbody>
<tr>
<th class="fixed-column-0 table-light">AM</th>
<td>〇</td>
<td>〇</td>
<td>〇</td>
<td>×</td>
<td>×</td>
<td>〇</td>
<td>〇</td>
</tr>
<tr>
<th class="fixed-column-0 table-light">PM</th>
<td>×</td>
<td>〇</td>
<td>〇</td>
<td>×</td>
<td>〇</td>
<td>×</td>
<td>〇</td>
</tr>
<tr>
<th class="fixed-column-0 table-light">夜</th>
<td>〇</td>
<td>〇</td>
<td>〇</td>
<td>〇</td>
<td>〇</td>
<td>〇</td>
<td>×</td>
</tr>
</tbody>
</table>
</div>
</div>
<!--jquery+bootstrap-->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<!--sticky-table-->
<script type="text/javascript">
//固定するヘッダーの数
var fixed_header_num = 2;
//固定するカラムの数
var fixed_column_num = 1;
</script>
<script src="static/js/sticky-table.js"></script>
</body>
</html>
これで任意の数の行/列ヘッダーをcss/jsのみで固定することができます。
課題
Chromeでは動作確認しましたが、IEだとpolyfillが必要みたいです。
stickyfill
また、globalで変数指定が必要だったり、洗練されてないイメージもあるので、もっとうまいやり方がありましたらご教示頂ければ幸いです。
参考
Table with fixed header and fixed column on pure css
stickyfill