2012年1月31日火曜日

css3とjavascriptによるヘッダ固定/フッタ固定/列固定テーブルの実装

ドラキュラHDのアイテム検索アプリを組んでいてヘッダー固定のtableを何とか列固定に拡張できないかと考えていたのだけれど,割とキレイな解決策がたまたま見つかったので公開してみる.(ieいじってたときに閃いたのさ.)
※2012/02/01 若干改良.
※2012/02/09 まとめてみた
※2012/02/14 display:blockの他にfloat:leftも便利に使えるっぽい.詳しくは上の記事を参照.

  1. [対象ブラウザ]
    動作を確認したブラウザ:firefox,chrome,opera(大体css3に対応したブラウザ)
  2. [経緯]
    世間一般ではテクニックとしてcssのみでヘッダーを固定する方法が有名だけれど,これを拡張して見出しの列を固定できないかと考えた.
    巷には既にこの機能を実現するjQueryプラグイン等が存在するようだが,そのほとんどがソースとなるtableを切り貼りして処理しているため,例えば内部にinput要素が含まれていたり,行を挿入したりソートしたりする際に無用な記述が必要となる.
    そのためあくまでtableの機能を損なわない範囲でヘッダー,見出しの固定を実現する方法はないか探ってみた.
  3. [アイディア]
    ヘッダー(フッター) の固定はこれまでどおりPushpin Headerテクニックで固定するとし,横方向のスクロールが行われた際,その移動量を元に固定したい列のセルの位置を(見た目が移動していないように)左右にずらせれば問題は解決するはずである.
    しかし通常のth,tdはテーブル内の要素として振舞うので,position:relativeによる相対位置指定が機能しない.
    一方,このテーブル内の要素はcssのdisplay 属性に対応しており,たとえdiv要素であってもdisplay属性を正しく設定すればtableの如く振る舞う.
    ならば逆にtableの要素にdisplay属性をblockとしたら通常のdiv要素と同様に相対位置指定が可能となるのではないか?
    下はその実験結果です.実装の簡便さからjQueryを使ってるけれど,素朴な作りをしているので他のツールや手組でも上手く行くと思います.


  4. [ポイント]
    • table自体には全く手を入れていないので,おそらくどんな用途にも応用ができると思う.
    • Pushpin Headerよりも更に1枚divを噛ませるところ.
      table幅を固定する-スクロールバーを表示する-絶対位置指定を有効化するdiv要素がそれぞれ連携して動作する.
    • 固定したい列のセルにdisplay:blockを指定しているところ.これでjavascriptによる位置指定が動作するようになる.
    • スクロールバー表示div要素のスクロールイベントに,固定列のtd要素の相対位置の設定処理を記述する.
    • display:blockを指定したことで,通常のtd要素で有効だったvertical-align属性が無効になる
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
  <style type="text/css">
    /*absolute配置の基準*/
    #container{
      width:410px;
      height:200px;
      position:relative;
      padding-top:79px;
      padding-bottom:83px;
      overflow:hidden;
      border:solid 1px black;
    }
    /*スクロールバーを表示させる*/
    #scroller{
      width:410px;
      height:200px;
      overflow:auto;
    }
    /*テーブル幅・高さを固定する*/
    #fixer{
      width:500px;
    }
    /*スクロール対象のテーブル*/
    #scrollee th, td, tr{
      border:solid 1px black;
    }
    #scrollee td, th{
      width:100px;
      height:75px;
      background-color:white;
      vertical-align:middle;
      text-align:center;
    }
    /*header*/
    #scrollee thead{
      /*absoluteを指定し,containerに追い出す.*/
      position:absolute;
      width:500px;
      top:0;
      left:0;
      z-index:2;
    }
    #scrollee thead tr th:first-child{
      /*block表示を指定し,相対位置指定を有効化する.*/
      display:block;
      position:relative;
      z-index:3;
    }
    #scrollee thead tr th:last-child{
      display:block;
      position:relative;
      right:0;
      z-index:3;
    }
    /*body*/
    #scroller tbody{
      width:500px;
    }
    #scrollee tbody tr td:first-child{
      display:block;
      position:relative;
      z-index:1;
    }
    #scrollee tbody tr td:last-child{
      display:block;
      position:relative;
      right:0;
      z-index:1;
    }
    /*footer*/
    #scrollee tfoot{
      position:absolute;
      width:500px;
      bottom:0;
      left:0;
      z-index:2;
    }
    #scrollee tfoot tr td:first-child{
      display:block;
      position:relative;
      z-index:3;
    }
    #scrollee tfoot tr td:last-child{
      display:block;
      position:relative;
      right:0;
      z-index:3;
    }
  </style>
  <script type="text/javascript" charset="UTF-8" src="./jquery-1.7.1.min.js"></script>
  <script type="text/javascript">
    window.onload = function(){
      //イベントを発生させるdiv
      var scroller = document.getElementById("scroller");
      var thead, theadF, theadL, tbodyF, tbodyL, tfoot, tfootF, tfootL;
      init();

      //スクロール時の処理
      scroller.onscroll = function(){
        //scrollerのスクロール位置
        var left = scroller.scrollLeft;
        var right = 106 - left;
        thead.css("left", left * -1);
        theadF.css("left", left);
        theadL.css("right",  right);
        tbodyF.css("left", left);        
        tbodyL.css("right", right);
        tfoot.css("left",  left * -1)
        tfootF.css("left", left);
        tfootL.css("right",  right);
      };
      scroller.onscroll();
      
      //行を変化させたらjQeryオブジェクトを再構成.
      /*
      $("#scrollee tbody").append("<tr><td>A</td><td>B</td><td>C</td><td>D</td><td>E</td></tr>");
      init();
      */

      //jQueryオブジェクトを取得する.
      function init(){ 
        thead = $("#scrollee thead");
        theadF = $("#scrollee thead tr th:first-child");
        theadL = $("#scrollee thead tr th:last-child");
        tbodyF = $("#scrollee tbody tr td:first-child");
        tbodyL = $("#scrollee tbody tr td:last-child");
        tfoot = $("#scrollee tfoot");
        tfootF = $("#scrollee tfoot tr td:first-child");
        tfootL = $("#scrollee tfoot tr td:last-child");
      }
    };
  </script>
</head>
<body>
  <div id="container"><!--absolute表示する際の基準-->
    <div id="scroller"><!--スクロールバーを表示する-->
      <div id="fixer"><!--tableの幅を固定する-->
        <table id="scrollee">
          <thead>
            <tr>
              <th>H1</th><th>H2</th><th>H3</th><th>H4</th><th>H5</th>
            </tr>
          </thead>
          <tfoot>
            <tr>
              <td>F1</td><td>F2</td><td>F3</td><td>F4</td><td>F5</td>
            </tr>
          </tfoot>
          <tbody>
            <tr>
              <td>11</td><td>12</td><td>13</td><td>14</td><td>15</td>
            </tr>
            …
          </tbody>
        </table>
      </div>
    </div>
  </div>
</body>
</html>

0 件のコメント:

コメントを投稿