flint>flint blog>2022年>12月> 4日>HTMLテーブルのデータ部スクロール
<< 前のエントリ | 次のエントリ >>

HTMLテーブルのデータ部スクロール

部分スクロール

HTMLでテーブルを記述するには <table>要素 (タグ) を使用します。 その際、「行」を表す <tr>要素 を<table> 要素の直下に配置しても構わないのですが、セマンティクス、すなわち

それが「見出し行」と「データ行」のいずれであるのかをブラウザに明示する

という観点から、<table> 要素の下に「見出し」セクションを表す <thead>要素 および <tbody>要素 を配置し、<tr> 要素はそれらの中に記述するのが好ましいとされています。 現状では <thead>, <tbody> 要素を使用してもしなくても、レンダリングにおける差異はないのですが、MDNのページにある以下の記述:

表が (ウィンドウのような) 画面に表示される場合で、表全体を表示するのに十分な大きさがないとき、ユーザーエージェントは <thead>, <tbody>, <tfoot>, <caption> ブロックを親である表から独立してユーザーがスクロールさせることができるようにするかもしれません。

を読んで「それは素晴らしい!」と感動して以来、テーブルを書く際は可能な限り <tbody> を記述するようにしているのですが、それから十余年経ってもテーブルデータ部の独立スクロールがWeb標準として実装されるという話は寡聞にして一切聞こえてきません。 面倒くさいのを我慢して律儀に <tbody> タグを記述し続けた俺の10年を返せ。 ......などと言っても詮無き事。 せっかく <tbody> タグを記述したのだから、スクロール機能も自分で実装すればいいじゃない。

というわけで、今回は CSSJavaScript を駆使してテーブルデータ部の独立スクロールに挑戦しようと思います。

CSSでスクロールすれば?

なんだか難しいことをやろうとしているように聞こえますが、CSS には次のようなプロパティがあります:

overflow は CSS の一括指定プロパティで、要素のオーバーフロー時、すなわち、要素の内容が多すぎてブロック整形コンテキストに収まらない場合の望ましい動作を両方向について設定します。

scroll
内容は、必要に応じてパディングボックスに合わせて切り取られます。 コンテンツが変化したときに、スクロールバーが現れたり消えたりするのを防ぐため、ブラウザーは内容がクリッピングされるかどうかに関わらず、スクロールバーを常に表示します。 プリンターはあふれた部分の内容を印刷する可能性があります。

なんだ、これ使えば簡単じゃん。 というわけで、早速 <tbody> 要素に、クライアント領域に収まらない内容をスクロール表示するスタイルを設定します:

<tbody style="height: 320px; overflow-y: scroll;">
<tbody> に overflow-y: scroll;

......スクロールバーが表示されない上、表示領域の高さにも制限がかかりません。 これは、<tbody> がブロック要素ではない (display: table-row-group) ため。 それなら、スタイルでブロック表示を指定すれば解決ですね。 勝ったな。風呂入ってくる。

<tbody style="display:block; height: 320px; overflow-y: scroll;">
<tbody> に display: block;

表示領域の高さが制限され、右端にスクロールバー出てきたのはよいのですが、最初の見出しセルの幅が、すべてのデータセル幅の合計値になってしまいました。 これは、<thead> がブロック要素でないことが原因。 これも先ほど同様、ブロック表示を指定すればOKでしょう。 勝ったな。風呂入ってくる。

<thead style="display:block;">
  <tr>
    <th scope="col">Index</th>
    <th scope="col">Field A</th>
    <th scope="col">Field B</th>
    <th scope="col">Field C</th>
  </tr>
</thead>
<tbody style="display:block; height: 320px; overflow-y: scroll;">
<thead> に display: block;

見出し部とデータ部の間での列幅の連動がなくなり、それぞれが独自に列幅を決定するようになってしまいました。 どうやら一筋縄ではいかないようです。

この問題を解決する手立てのひとつは列幅を決め打ちにすること。 例えば、以下のようなスタイルシートを適用すれば問題は解決できるでしょう:

#target th:nth-child(1),
#target td:nth-child(1) {
  width: 50px;
}
#target th:nth-child(2),
#target td:nth-child(2) {
  width: 150px;
}
#target th:nth-child(3),
#target td:nth-child(3) {
  width: 400px;
}
#target th:nth-child(4),
#target td:nth-child(4) {
  width: 100px;
}

しかしながら、「内容に応じてセル幅が調整・融通される」というテーブルの利点を殺してしまうこの手法は大変美しくないため、今回は採用しないこととします。

手動でセル幅を合わせる

見出し部とデータ部との間でセル幅が連動しなくなったということは、裏を返せば個別にセル幅を指定できるようになったということ。 となれば、JavaScript で対応する列の幅を、見出し部とデータ部の間で大きい方に合わせるようにすれば上手くいくのではないでしょうか:

function adjustRowWidths(){

  var eTable = dom.getElement("target", true);
  var eTHead = dom.getElementByTagName(eTable, "thead", true);
  var eTBody = dom.getElementByTagName(eTable, "tbody", true);

  var eHeadRow = dom.getElementByTagName(eTHead, "tr", true);
  var eDataRow = dom.getElementByTagName(eTBody, "tr");
  if (!eDataRow) return;

  //PLACEHOLDER 1

  eTHead.style.display = "block";

  eTBody.style.display = "block";
  eTBody.style.height  = "320px";

  for (var i = 0; i < eHeadRow.children.length; i++){

    var eHeadCell = eHeadRow.children[i];
    var eDataCell = eDataRow.children[i];

    if (eHeadCell.clientWidth < eDataCell.clientWidth){
      eHeadCell.style.width = "calc(" + eDataCell.clientWidth + "px - 2ex)";
    }
    else {
      eDataCell.style.width = "calc(" + eHeadCell.clientWidth + "px - 2ex)";
    }
  } //i

  //PLACEHOLDER 2

  return;
adjustTableWidths

できました! データ部とヘッダ部で列幅が同じになっています。

セル要素の width プロパティ設定時に - 2ex としているのは、すべてのセルに左右それぞれ 1ex のパディングが設定されているため。 このパディング値が異なる場合は、当該部分の値を書き換えてください。

関数 getElementByTagName の実装は、この後提示するリンクよりご覧頂けますが、簡単に説明すると

第1引数で指定された要素の内容を、第2引数で指定されたタグ名で深さ優先探索し、最初に見つかったものを返す

というもの。 第三引数に true を指定すると、該当する要素が見つからなかった場合に例外がスローされます。

仕上げのラッピング

上記の JavaScript で「データ部だけをスクロールできるテーブルを作る」という目的は概ね達成されました。 しかし、ページの幅が狭い場合などに、style による列幅指定が無視されてレイアウトが崩れてしまうという問題があります。 これを防ぐため、「テーブルを十分な幅を持つ <div> で囲む」コードを追加します。 追加する位置は、先のコード内で "PLACEHOLDER" と書かれたコメントのある2個所です:

//PLACEHOKDER 1 
var eWrap = app.wrapElement(eTable);
eWrap.style.width = "4096px";
//PLACEHOKDER 2 
eWrap.style.width = eTable.clientWidth + "px";

関数 wrapElement の実装も、後に提示するリンクより確認頂けますが、簡単に言うと次のような動作をします:

第1引数で指定された要素 e とその親要素の間に新しく生成した <div class="wrap"> 要素を割り込ませる。
(e と元の親要素の関係は「孫-祖父」となる。)

下記URLに今回紹介した手法のサンプルを置いておきます:

サンプルページ
  • 最初に50個のデータ行を持つテーブルが表示されます。(内容はランダム生成)
  • [ 実行 ] ボタンを押すと、上記テーブルのデータ部がスクロール可能になります。
成田 (要素幅固定は悪い文明)
このエントリーをはてなブックマークに追加

コメント

投稿者
URI
メールアドレス
表題
本文