2012年3月1日木曜日

スクロールテーブルを実装する際の注意点

前回ヘッダー・フッター・列固定可能なテーブルを実現するためのjQueryプラグインを作ってみたので,その際に気がついた点についてまとめてみた.すこしづつ増えてる・・・
2011/02/14更新.
2011/03/01更新.
2011/05/30更新.
2013/12/24更新.
2018/01/24更新. (position:sticky法を追加…素晴らしい)
2018/04/27更新. display:grid+position:stickyによる実装例を追加


スクロール可能なテーブルを実現する手段としてはだいたい次の6つの方法が挙げらる.まあここらへんはアイディアの問題なので細かく考えていけばもっとあるかもしれない.
  1. テーブル分割法
    テーブル要素を複数のテーブルに分割して対処する方法
    いろいろな場所で公開されている.
  2. pushpin header法
    cssのposition設定を利用する方法.
    ここで紹介されている.
  3. display:block法
    display:blockとjavascriptを使ってセルをスライドさせることで位置の固定を行う方法
    拙作プラグインによるもの.
  4. freezing法
    ie専用.cssのexpression拡張とposition:relativeを組み合わせて位置を固定する方法
    windows環境では有名な方法
  5. tbody-float法
    tbodyとtrにfloat:leftを使ってボディ部のみをスクロールさせる方法
    table要素をいじくっててたまたま見つけた方法.大体3のバリエーションと言っていいかも.
  6. element関数法
    css3で追加されそうなelement関数を使ってtable要素のコピーを作る方法.
    現在firefoxのみで利用可能であるが,他のブラウザが追随すればメジャーになりうる.
  7. position:sticky法(NEW)
    新たに追加されたsticky配置を使い, thead要素を固定する方法
    ほぼ理想の出力が得られるため, スクロールテーブル実装の新たなデファクトスタンダードとなる可能性大.
    http://defghi1977-onblog.blogspot.jp/2018/04/css.html
    にグリッドレイアウトとの組み合わせ例を作ってみました.
そのいずれも一長一短があり,要件によっては複数の手段を使い分ける必要が出てくる.以下でその特徴について考えてみよう.


1.テーブル分割法

テーブルを固定部-スクロール部の2から9個のブロックに分割し,スクロール部の位置を元にレイアウトを再構成するパターン.
最も素朴かつ確実な実装方法と言えるもので,いかなる要件・環境においても対処が可能.
その一方で,テーブルを分割する為の処理(クライアントサイド・サーバーサイド)やスクロールを制御する為のスクリプトの実装が煩雑となりがちである.
なおこの手法をとったjQueryプラグインは様々なものが提供されているが,元となるテーブル要素の内容を改変・複写するものが多く,例えばform部品が含まれていた場合に不都合が起こる可能性が高い.また,行の追加等の動的な変更に弱い.
テーブルを静的なビューとして使う場合はプラグインの利用を検討してみよう.

2.pushpin header法

テーブルを2枚ないし3枚のdivタグで囲み,固定したい要素(tr,th,td)をposition:relativeにより表の外部に追い出すもの.
スクリプトに依る処理は一切存在せず,幅と高さと位置のcss設定のみで対処可能.テーブルの構造が維持されるため,サーバーサイド等に依るテーブル生成,行追加等が極めて明快である.このことからヘッダー・フッターの固定(つまり縦スクロール) のみに対応する場合の最適解と言える.
一方で列の固定は出来無いのだが,3の方法と組み合わせることで対応が可能なケースがある.
 なお,ie系のブラウザではtableのcellspacingが残ってしまう為,若干のレイアウトの調整が必要となる.

3.display:block法

tableを構成する要素の一部・全体にdisplay:block+position:relativeを指定し,スクロールバーの位置によって対象となるセルの位置をスライドさせてあたかも位置が固定されているように見せる方法.
2と同様にテーブルの構造が維持されるので,テーブルの生成・行追加が単純である.その一方でtable(table-cell)のもつ機能(例えばvertival-align)が無効となる点に気をつけよう.
スクリプトによる位置の補正を行っているため,環境によってはちらついたり,スライドさせるセルの数が大量となった際にスクリプトが応答しなくなるといった潜在的な問題をはらむ.
なお,ie系のブラウザではそもそもdisplay:blockに対応していない一方で,なぜかposition:relativeが効くのでこれを利用してセルをスライドすることが可能である.
加えてie8においては更にposition:relativeを指定したセルの描画がおかしくなる(back-groundcolorの描画がborderの上に被さってしまう)挙動をを示すので注意が必要となる.

4.freezing法

固定したいセル・行に下記のスタイルを指定し,expression拡張によりセルの位置をスライドさせる方法.
.Freezing {
    z-index: 10; 
    position: relative; 
    top: expression(this.offsetParent.scrollTop)
}
この方法では幅の固定を行うことすら不必要で実に簡単であるが,対応ブラウザがie6,7という極めて狭い環境でしか動作しないため,ie9以降が主流となる昨今ではこの方法を選択すべきではない.但しexpressionはコストの高い処理である点には注意である.

5.tbody-float法

スクロールしたいテーブルのtbodyにfloatを指定してoverflow:autoを有効化する方法.floatを指定した要素はdisplay:blockを指定した場合と同様にテーブルレイアウトの束縛から解放されるため,positionによる相対位置指定が可能となる.tableの構造やレイアウトをほとんど汚さない為,pushpin header法よりも手軽に使える方法.短いcssの記述だけで容易にヘッダーとフッターを固定できる.対応ブラウザがie6,7以外であり,動作環境が広めである点も魅力.列固定処理も概ね単純だが,当方ではie8において不具合が出てしまった為に注意が必要.

6.element関数法

cssのelement関数を使い,table要素の見た目をdiv要素の背景に設定し,このdiv要素をテーブルの上に重ねてしまう方法.理屈から言えばテーブル分割法をテーブル構造を破壊せずに行える可能性がある.
列固定だけなら最も簡単に実装可能である.
現状firefox(かつ新しいもの)のみの実装であるし,ベンダープリフィクス付きであるし,何より将来的な削除の可能性があるので,運用には注意が必要となる.

7.position:sticky法

固定したい要素に対してposition:stickyを設定するもの.
こちらで詳しく解説されている.
行固定・列固定共にスクリプトレスで行えるため, 極めてメンテナンス性が良い.

これらの特徴を表にまとめると次のようになる.

特徴実現法行追加列固定
(javascript)
対応ブラウザ
1テーブル分割 どんなに複雑でもOK
一方自作は辛い

自力
プラグインを利用する手も

構造上最も単純

頑張れば
2pushpin header法テーブルをdiv要素で囲んでスタイルを指定
css記述のみ
プラグイン

3と組み合わせて

ie系がネック
3display:block法display:block
+ position:relative
+ javascript
ちょっとちらつく
生のテーブルとはちょっと異なる挙動

部分的な実装なら割と簡単

ie系がネック
4freezing法expressionでスライドさせる
css記述のみ

ie6+7のみ
5tbody-float法tbodyにfloatを指定してスクロールさせる.
css記述のみ

ie8で不具合の可能性

ie6,7以外
6element関数法element関数を使ってテーブルをコピーする
他の方法の応用

列固定だけならcssだけで可能

firefoxのみ
7position:sticky法display:stickyを使ってヘッダ等を固定
CSS記述のみ

CSS記述のみ
△ie系を除く
一番無難なのはpushpin headerで実装し,列固定処理をdisplay:block法により行う方法か.5番はいい感じに見えるけれど,動作実績的にどうよ?ってところも.6番は限定的にちょー使えるかも.7は互換性(ie/edge系)を無視して良いなら最善.


NOTE:CSSのみで実装できる道筋がつきましたので, 下記はもはや過去のものとなりました.


スクロールテーブルはcssで実装しちゃいけないの?


個人的にはcssで実装すべき(実装できないほうがおかしい)と思っている口ですが,これはケースバイケースだと思うのです.どこでも同じ見た目となることが最優先事項であるなら,テーブル分割がベストだとは思いますが,単に長いテーブルを使いやすくするためにスクロールさせたいだけなら,pushpin headerでも要件は充たせます.要は実装に費やした労力に見合った,もしくはそれ以上のリターンが得られるかってことなのです.cssだけで完璧を目指すのは現状ブラウザ間の動作の違いを考えると非現実的であり,この意味では「cssで実装しちゃいけない」のですが,javascriptだけで実現する場合も,いちいちテーブルの構造をばらす必要があるなど別の問題を引き起こします.従ってcssとjavascriptを用いることのメリットをよく理解し,定められた要件を満たすために双方のいいとこ取りをし,できるだけ実装の労力を減らすというのが理想的と言えるでしょう.

その仕組み無理にtable要素で実装する必要はありますか?

2013/12/24追記
根底から記事の内容をひっくり返すようだけれど,あなたが実装しようとしている表は既にtable要素の限界を超えているかも知れないって話。
スクロールテーブルの実装を阻む原因としては
  • table要素に設定されているデフォルトスタイル
  • border-collapseによる枠線の結合
  • rowspan/colspan属性によるセルの結合
  • table要素に含まれるform部品
  • カラムの固定
  • スクロールの方向
  • ブラウザごとの微妙な動作の違い
が挙げられて,これらが複雑に絡み合うからこそスクロールテーブルは難しい.

そこで提案.だったらtable要素なんて使わずに全てdiv要素で実装したらどうだろう?
(敢えてtable要素に拘るのなら,tableを構成する要素全てにdisplay:blockを指定するんだ.)

おせっかいなtable機能をバッサリ切り捨てて扱いやすいdiv要素で代用するんだ. つまりdiv要素をtable要素のように階層化し,cssでスタイルを1から定義するわけだ.確かにcssの記述が面倒だけれど,こうすることであれこれ制御不能なtable要素に悩む必要が無くなってかえって実装が楽に行える(かも).

※個人的にはテーブルのスクロール有無はcss側でサポートすべき仕組みだと思っています.table要素の使われ方が昔と今とでは変化しているんだから.普通はこんな単純なことで時間を取られたく無いよね.


スクロール処理を実現する際に憶えておくと便利な事項

  • tableに関わる要素をtable配置の呪縛(ex.幅・高さの調整,tfootがテーブル下部に表示される等)から解くには次の2つの方法がある.
    1. display:blockを指定してブロック要素化する.
    2. float:left/rightを指定し浮動配置化する.
    いずれもposition:relativeやoverflowが有効となるため,位置の微調整が可能となる.
    設定する要素によっては,tableのもつ各種レイアウト機能を残しつつ配置の調整が出来るようになるので非常に便利である.
  • ie8は6,7までの機能(expression)が削られている上に代替の機能が不完全であるため要注意である.場合によっては逃げ道が無いことも・・・.仕方がない場合は互換モードという手もあるにはあるが信用ならない.とにかくieは最新の9ですら要注意.特に8は挙動がbuggyなので重点的にチェックするとよい.
  • css3に対応したブラウザであれば行の中の列を指定するセレクタnth-child(n),nth-last-child(n)が使えるので縦方向のcss指定が簡単.そうでなければ列ごとに予めclassを指定しておく必要がある.
  • スクロールバーの幅は次のスクリプトで取得できる.右列,フッタの位置の計算に利用できる.
    function getScrollbarWidth(){
     var body = jQuery("body");
     var outer = jQuery("<div>");
     outer.css({
      width: 100,
      height: 100, 
      overflow: "scroll",
      border: "none",
      padding: 0,
      margin: 0,
      visibility: "hidden"
     });
     var inner = jQuery("<div>&nbsp;</div>");
     inner.css({
      width: 200,
      height: 200,
      border: "none",
      padding: 0,
      margin: 0
     });
     outer.append(inner).appendTo(body);
     outer.scrollTop(200);
     var barWidth = outer.scrollTop() - 100;
     outer.remove();
     return barWidth;
    }
    

0 件のコメント:

コメントを投稿