[CSS/jsのみ]tableの行/列ヘッダーを固定する

# はじめに
最近Webアプリ周辺の技術を学び始めた者です。普段は製造業で設計業務を担当しておりますが、社内システム構築用にいろいろと勉強しています。主に使うのは下記。

* 言語 : python/C#
* Web framework : django

記事に書き出すことで自身の理解も深まると考え、今回初投稿をさせて頂きます。

# 動機
休日に妻と共同でアプリ開発をしています(この開発記録もつけられたらなーと思っています)。
tableを多用するのですが、その際列ヘッダーと行ヘッダーを固定してtbodyのデータセルだけスクロールできないかなと考え、いろいろ調べていました。
便利なプラグインもたくさんありましたが、

* 複数ヘッダーを固定できるものが限られていた
* なるべく既存のtableに変更を加えたくない

という理由で手を出せず。position:stickyというステキなオプションがあるので、どうにかこれを使ってできないかと思い、やってみました。
なおこちらのstackoverflowの質問を参考にしました→[Table with fixed header and fixed column on pure css](https://stackoverflow.com/questions/15811653/table-with-fixed-header-and-fixed-column-on-pure-css)

# html
下記のようなtableと、wrapperとなるdivを用意します。class名はbootstrapを意識しています。

```html:sample.html
<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>
        
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/607181/7ac91a75-08f0-b4ba-475a-b1c28fddb72f.png)

ゴールは上記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を作成します。

```css:sticky-table.css
/*ラッパー*/
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;
}
```

コメントを添えていますが、

```css
div.table-wrapper {
  overflow: scroll;
  max-height:200px; /*任意*/
  max-width:400px; /*任意*/
}
```
はwrapperの挙動です。max-height/max-widthは任意の値に設定してください。

続いて下記のような.jsを作成します。結局jqueryで書いてしまった。

```javascript:sticky-table.js

//行ヘッダーに対し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:sample.html
<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>

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/607181/323b48ef-a1c3-fa5c-6e3c-e5ab4007bd7d.png)


これで任意の数の行/列ヘッダーをcss/jsのみで固定することができます。

# 課題
Chromeでは動作確認しましたが、IEだとpolyfillが必要みたいです。
[stickyfill](https://github.com/wilddeer/stickyfill)

また、globalで変数指定が必要だったり、洗練されてないイメージもあるので、もっとうまいやり方がありましたらご教示頂ければ幸いです。

## 参考
[Table with fixed header and fixed column on pure css](https://stackoverflow.com/questions/15811653/table-with-fixed-header-and-fixed-column-on-pure-css)
[stickyfill](https://github.com/wilddeer/stickyfill)