この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

"overflow:auto"&固定テーブルヘッダ

最近、いまさらですがjQuery始めました。


で、今日のお題です。


tableタグ表形式のデータを表示する場合で、もし画面内に収まらない場合に
自動的にスクロールバーを表示するには

<div style="overflow: auto; height: 600px">
    <table>
        ...(略)
    </table>
</div>

なんてやりますが、overflowってヘッダ行(th)を固定してくれないので、ヘッダ行ごとスクロールしちゃう。
これはExcelに慣れた身からすると、やや不便。

作ってみた

そこで、overflow:autoを適用しつつ、自動的に固定ヘッダを表示してくれるスクリプトを、
jQueryの練習も兼ねて書いてみました。


"HTML テーブル ヘッダ 固定"でググれば多種多様な対応策を見つけられますが、
僕が書いたスクリプトの特徴は『素直なやり方』ということ。


スタイルシートの奥義を駆使するわけでもなく、
Javascriptでゴニョゴニョするわけでもなく、
実行後はタダのHTMLです。


やっていることを簡単に説明すると、
『テーブルのTHタグを取り出して、固定ヘッダとしてのテーブル要素を新規に追加する』
だけ。スクリプトの半分以上の処理は単なるサイズ調整です。
サイズ調整も、ピクセル単位で細かく計算しているわけではなく
パーセンテージで合わせているだけ。


なので、

  • 既にスクリプトやスタイルシートの調整が入りまくったテーブルに対して適用しても、副作用が少ない
  • ウィンドウのリサイズに対応(2011/03/02追記、ウィンドウリサイズ対応用のスクリプトを足しました)

というメリットがあります。

/*
 * Copyright (C) 2011 root42 Inc. All rights reserved.
 */
/**
 * テーブルにoverflow:autoを適用し、ヘッダ行を固定化します。
 *
 * ※テーブルの中身は由緒正しくtheadとtbodyで囲まないとダメ。たぶん。
 *
 * @param listTableSelector
 * 固定ヘッダを表示するテーブルを指定するセレクタ
 * @param listTableHeight
 * 固定ヘッダを表示するテーブルの高さ(オプション)。デフォルト値は550px。
 *
 * @author root42 Inc.
 */
function setFixedHeaderTableScrolling(/* String */ listTableSelector, /* int */ listTableHeight) {

    listTableHeight = listTableHeight || 550;

    // コレをそのまま利用
    // http://stackoverflow.com/questions/986937/javascript-get-the-browsers-scrollbar-sizes
    /* int */ var scrollBarWidth = function() {
        document.body.style.overflow = 'hidden';
        var width = document.body.clientWidth;
        document.body.style.overflow = 'scroll';
        width -= document.body.clientWidth;
        if(!width) width = document.body.offsetWidth - document.body.clientWidth;
        document.body.style.overflow = '';
        return width;
    }();

    $(listTableSelector).each(function() {

        /* Table */ var listTable = $(this);

        /*
          対象のテーブルをdivで囲む。
          要素のデタッチ→アタッチが走るため、
          内部のスクリプトが再実行されてしまうことに注意!!
        */
        listTable.wrap("<div />").parent()
            .css('overflow', 'auto')
            .css('height', listTableHeight + 'px');

        /* boolean */ var overflowed =
            (listTable.parent().height() < listTable.parent().attr('scrollHeight'));

        /* スクロールバーが表示されないなら何もしない */
        if (!overflowed) {
            return;
        }

        /* 固定ヘッダ(table) */
        /* Table */ var fixedHeader = $("<table><thead><tr /></thead></table>")
            // 見た目を同じにするため、クラスをコピー
            .attr('class', listTable.attr('class'));

        /* 固定ヘッダのtr */
        listTable.find('>thead>tr>th').each(function() {

            /* 固定ヘッダに、対象テーブルのthタグの内容をコピーする */
            fixedHeader.find('tr').append($('<th />').html($(this).html()));
        });

        /* 対象テーブルを囲むdivタグの一つ前に固定ヘッダを挿入する */
        listTable.parent().before(fixedHeader);

        /* int */ var tableWidth = listTable.parent().width();

        /* 固定ヘッダと対象テーブルのカラムの比率を一致させる */
        fixedHeader.find('th').each(function(/* int */ idx) {

            /* String */ var tdSelector =
                '>tbody>tr:first-child>td:nth-child(' + (idx + 1) + ')';
            /* Td */ var td = listTable.find(tdSelector);
            /* int */ var colWidth = Math.max(td.width() , $(this).width());
            /* String */ var colRatio = Math.round((colWidth / tableWidth) * 100) + '%';

            $.each([$(this), td], function() { this.width(colRatio); });
        });

        // ウインドウリサイズに対応
        $(window).resize(function() {

            /* overflow:auto分のスクロールバーの幅だけマージンを取る */
            fixedHeader.css('width', listTable.parent().width() - scrollBarWidth);
            return arguments.callee;
        }());

        /* 対象テーブルのtheadを消す */
        listTable.find('>thead').detach();
    });
}

僕の環境では大体うまく動いてます。(Chrome10 beta、IE8、Firefox3.6)


ただし前述の通り、
"width: 98%"などとしている所から分かる通り、
一応はスクロールバーのサイズを計算して調整していますが、
ヘッダ列とデータ列の位置を、ピクセル単位でピッタリ合わせているわけではないです。
CSSを、なんとなく違和感がないように後付で調整する*1ことで誤魔化してます。



まぁ、パッと見で気が付かなければ、まぁいいかな、と。
ぶっちゃけ手抜きです。ハイ。だってめんどくさいんだもん。


不具合や、『jQueryの???を使うと、もっと短く書けるよ!』
などがあったらお知らせ下さい。忙しくなければ出来る限り対応します。

それにしても

jQueryってなんだか楽しいですね。。。ワンライナーに目一杯、処理を詰め込めるところが特に。
ソースは壊滅的に読みづらくなるけど。


『何でもワンライナーでやっちまうのが好き!』という方にはピッタリ。

2011-02-22追記

サンプルのアプリケーション込みの全体のHTMLはコチラ。
そのまま実行できます。

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 
    <meta http-equiv="Content-Style-Type" content="text/css" /> 
    <meta http-equiv="Content-Script-Type" content="text/javascript" /> 
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.0/jquery.min.js"></script>
</head>

<body onload="setFixedHeaderTableScrolling('#data', 100);">
    <script type="text/javascript">
/*
 * Copyright (C) 2011 root42 Inc. All rights reserved.
 */
/**
 * テーブルにoverflow:autoを適用し、ヘッダ行を固定化します。
 *
 * ※テーブルの中身は由緒正しくtheadとtbodyで囲まないとダメ。たぶん。
 *
 * @param listTableSelector
 * 固定ヘッダを表示するテーブルを指定するセレクタ
 * @param listTableHeight
 * 固定ヘッダを表示するテーブルの高さ(オプション)。デフォルト値は550px。
 *
 * @author root42 Inc.
 */
function setFixedHeaderTableScrolling(/* String */ listTableSelector, /* int */ listTableHeight) {

    listTableHeight = listTableHeight || 550;

    // コレをそのまま利用
    // http://stackoverflow.com/questions/986937/javascript-get-the-browsers-scrollbar-sizes
    /* int */ var scrollBarWidth = function() {
        document.body.style.overflow = 'hidden';
        var width = document.body.clientWidth;
        document.body.style.overflow = 'scroll';
        width -= document.body.clientWidth;
        if(!width) width = document.body.offsetWidth - document.body.clientWidth;
        document.body.style.overflow = '';
        return width;
    }();

    $(listTableSelector).each(function() {

        /* Table */ var listTable = $(this);

        /*
          対象のテーブルをdivで囲む。
          要素のデタッチ→アタッチが走るため、
          内部のスクリプトが再実行されてしまうことに注意!!
        */
        listTable.wrap("<div />").parent()
            .css('overflow', 'auto')
            .css('height', listTableHeight + 'px');

        /* boolean */ var overflowed =
            (listTable.parent().height() < listTable.parent().attr('scrollHeight'));

        /* スクロールバーが表示されないなら何もしない */
        if (!overflowed) {
            return;
        }

        /* 固定ヘッダ(table) */
        /* Table */ var fixedHeader = $("<table><thead><tr /></thead></table>")
            // 見た目を同じにするため、クラスをコピー
            .attr('class', listTable.attr('class'));

        /* 固定ヘッダのtr */
        listTable.find('>thead>tr>th').each(function() {

            /* 固定ヘッダに、対象テーブルのthタグの内容をコピーする */
            fixedHeader.find('tr').append($('<th />').html($(this).html()));
        });

        /* 対象テーブルを囲むdivタグの一つ前に固定ヘッダを挿入する */
        listTable.parent().before(fixedHeader);

        /* int */ var tableWidth = listTable.parent().width();

        /* 固定ヘッダと対象テーブルのカラムの比率を一致させる */
        fixedHeader.find('th').each(function(/* int */ idx) {

            /* String */ var tdSelector =
                '>tbody>tr:first-child>td:nth-child(' + (idx + 1) + ')';
            /* Td */ var td = listTable.find(tdSelector);
            /* int */ var colWidth = Math.max(td.width() , $(this).width());
            /* String */ var colRatio = Math.round((colWidth / tableWidth) * 100) + '%';

            $.each([$(this), td], function() { this.width(colRatio); });
        });

        // ウインドウリサイズに対応
        $(window).resize(function() {

            /* overflow:auto分のスクロールバーの幅だけマージンを取る */
            fixedHeader.css('width', listTable.parent().width() - scrollBarWidth);
            return arguments.callee;
        }());

        /* 対象テーブルのtheadを消す */
        listTable.find('>thead').detach();
    });
}
</script>

    <div>
        <style>
            #data {
                width: 100%;
            }
            th, td {
                border: solid 1px;
                word-break: break-all;
            }
        </style>
        <table id="data">
            <thead>
            </thead>
            <tbody>
            </tbody>
        </table>
        <script type="text/javascript">

            var charNum = 10;
            var colNum = 10;
            var rowNum = 3;

            $('#data>thead').append($("<tr />"));

            for (var i = 0; i < colNum; i++) {
                $('#data>thead>tr').append($("<th />"));

                for (var m = Math.ceil(Math.random() * charNum); 0 <= m ; m--) {
                    $('#data>thead>tr>th:last-child').append('あ');
                }
            }

            for (var i = 0; i < rowNum; i++) {
                $('#data>tbody').append($("<tr />"));

                $('#data>tbody>tr:last-child').append($("<td>" + i + "</td>"));

                for (var k = 1; k < colNum; k++) {
                    $('#data>tbody>tr:last-child').append($("<td />"));

                    for (var m = Math.ceil(Math.random() * charNum); 0 <= m ; m--) {
                        $('#data>tbody>tr:last-child>td:last-child').append('い');
                    }
                }
            }
        </script>
    </div>
</body>
</html>

*1: この処理は上記スクリプトに入っていないので、自分でやる必要アリ