左端のセル固定ヘッダーのテーブルをプレーンなjavascriptのみで実装する方法

# 目的
* 横幅が表示領域より長い表を横スクロールした時に、左端の見出しセルを横スクロールに追従して表示する

# 前提
* tableタグのhtmlエレメントにidが付与してあること

```html
<table id="testTable1">(略)</table>
```

* 各行先頭の要素が固定したい見出しセルであること  
* 表内左端の見出しセル内にコピーされてはいけないエレメントが含まれていないこと  
（id付きのエレメントやformで送信対象とする ```<input type="hidden" name="xxx" />``` など）  
※横スクロールに追従するラベルを左端のセルをコピーして生成するため

# 使い方

* 以下のクラスをjsファイル等に転記して読み込む

```javascript
class FixedHeaderTable {
    constructor(id) {
        // 対象のDOM
        this.table = document.getElementById(id);

        // コピーstyle要素
        this.copyStyleNames = ["margin", "padding", "border", "width", "height", "line-height", "background-color", "color", "font-size", "font-family"];

        // 初期化
        this.initialize();
    }

    /**
     * 初期化
     */
    initialize() {
        // tableをrelative要素でラップ
        this.wrapper = this.wrapTable();

        // 表示用の要素を生成
        this.leftHeaders = this.createLeftHeaderView();
    }

    /**
     * 対象のテーブルDOMを位置調整用DIVで包む
     */
    wrapTable() {
        const wrapper = document.createElement("div");
        wrapper.style.cssText = `position: relative; max-width: ${this.table.parentNode.clientWidth}px; overflow: auto;`;

        const scrollArea = document.createElement("div");
        scrollArea.style.cssText = `max-width: ${this.table.parentNode.clientWidth}px; overflow: auto;`;

        this.table.parentNode.insertBefore(wrapper, this.table);
        wrapper.appendChild(scrollArea);
        scrollArea.appendChild(this.table);

        return wrapper;
    }

    /**
     * 各行の左端のセルを取得
     */
    getLeftHeaders() {
        return Array.prototype.slice.call(this.table.getElementsByTagName("tr"))
            .map(row => row.firstElementChild);
    }

    /**
     * 左端の表示用セルを生成
     */
    createLeftHeaderView() {
        const view = document.createElement("div");
        view.style.cssText = "display: inline-block; position: absolute; top: 0; left: 0;";

        this.getLeftHeaders().forEach(source => {
            const cell = document.createElement('div');
            this.copyStyleNames.forEach(styleName => cell.style[styleName] = window.getComputedStyle(source).getPropertyValue(styleName));
            cell.innerHTML = source.innerHTML; // ヘッダーセル内にhidden等コピーされてはいけない項目が入っていないことが前提

            view.appendChild(cell);
        });

        this.wrapper.appendChild(view);

        return view;
    }

    /**
     * 描画後に左端の見出しセルの内容が変わった場合などに表示用のdivを更新する
     */
    refresh() {
        this.leftHeaders.remove();
        this.leftHeaders = this.createLeftHeaderView();
    }
}
```

* 画面描画後に以下を実行する  
第1引数に対象のテーブル要素のIDを文字列で渡す。  
※vue.js等の動的な描画を伴う場合は、描画が終わった後に実行する必要あり。

```javascript
const table1 = new FixedHeaderTable("testTable1");
```

## 補足

* 描画後に動的に行数の増減や左端の見出しセルの内容に変更があり、追従するラベルの更新が必要となった場合は```refresh```を実行する  
※事前にFixedHeaderTableをnewした時のインスタンスをどこかに保持しておくこと。（例は ```table1``` 変数）

```javascript
table1.refresh();
```

# 理屈
* 対象のテーブルをrelativeなdivでラップすることで表の左上に基準点を設定する
* 左端の見出しセルに適用されている幅やフォントなど表示に関連するstyle要素をコピーした表示用のdiv要素を生成する
* 表示用のdiv要素を基準点にabsoluteで固定することで、あたかも左端のセルが横スクロールに追従しているようになる

# さて、ここまでお話ししましたが
* CSS3が対応していれば以下のスタイルを適用するだけで上記を実現できます。

```css
/* tableタグを包むラッパー要素に付与するクラス */
div.table-wrapper {
  max-width: 任意の表示領域の幅px;
  overflow: auto;
  position: relative;
}

/* 各行先頭のセル */
table tr td:first-child {
  position: sticky;
  top: 0;
  left: 0;
}

/* 以下も適用すれば1行目も固定できます */
table thead tr:first-child {
  position: sticky;
  top: 0;
  left: 0;
  z-index: 1;
}
```
上の苦労は一体なんだったんだ・・・