テーブル行ヘッダ列ヘッダ固定化Extender バージョン1.0 その1 から引き続き、ヘッダ固定化Extender の解説を行います。

ページ下部に動作確認用のテストソリューションつきのソリューションがダウンロードできるようになっています。

前回から続きです。テーブルの列、行をリサイズするためのjavascript の編集を行います。

1.5 TableResizeBehavior.js の編集

TableResizeBehavior.js を編集します。ビルドアクションが埋め込まれたリソースとなっていることを確認してください。このエクステンダーで行列ヘッダのリサイズと行ヘッダダブルクリック時に、サイズを文字幅にあわせて自動拡張できるようにしています(自動収縮はしません)。

/// <reference name="MicrosoftAjax.js"/>

Type.registerNamespace("ComponentGeek.Extender");


///////////////////
ComponentGeek.Extender.TableXResizeBehavior = function(element) {
    ComponentGeek.Extender.TableXResizeBehavior.initializeBase(this, [element]);

    this._header; // ヘッダ
    this._body;   // ボディ
    this._target;   // サイズ変更対象セル
    this._startX;   // 幅変更開始X座標
    this._endX;     // 幅変更終了X座標
    this._xResizing = false;     // 幅変更中フラグ
    this._resizeThreashold = 8;  // 最小変更幅
    this._edgeThreshold = 8;     // 幅,高さ変更カーソル表示閾値
    this._sizeThreshold = 20;    // 最小列,幅高さ
    this._vBarID = "vBarID";     // 変更時に表示する縦棒のID
    this._xResizeCursorVisible = false;  // 幅の変更カーソル表示フラグ
    //    this._resizingCssClass = "";  // サイズ変更時のCss
    //    this._originalCssClass = "";
}

ComponentGeek.Extender.TableXResizeBehavior.prototype = {
    initialize: ComponentGeek$Extender$TableXResizeBehavior$initialize,
    dispose: ComponentGeek$Extender$TableXResizeBehavior$dispose,
    _onMouseDown: ComponentGeek$Extender$TableXResizeBehavior$_onMouseDown,
    _onMouseUp: ComponentGeek$Extender$TableXResizeBehavior$_onMouseUp,
    _onMouseMove: ComponentGeek$Extender$TableXResizeBehavior$_onMouseMove,
    _onDblClick: ComponentGeek$Extender$TableXResizeBehavior$_onDblClick,
    createBar: ComponentGeek$Extender$TableXResizeBehavior$createBar,
    clear: ComponentGeek$Extender$TableXResizeBehavior$clear,
    getFirstRowCell: ComponentGeek$Extender$TableXResizeBehavior$getFirstRowCell,

    get_header: function() {
        return this._header;
    },
    set_header: function(value) {
        if (this._header !== value) {
            this._header = value;
            this.raisePropertyChanged('header');
        }
    },
    get_body: function() {
        return this._body;
    },
    set_body: function(value) {
        if (this._body !== value) {
            this._body = value;
            this.raisePropertyChanged('body');
        }
    }
}

function ComponentGeek$Extender$TableXResizeBehavior$initialize() {
    ComponentGeek.Extender.TableXResizeBehavior.callBaseMethod(this, 'initialize');

    if (!this._header) {
        throw Error.create("headerを指定してください");
    }

    $addHandler(this._header, "mousedown", Function.createDelegate(this, this._onMouseDown));
    $addHandler(this._header, "mouseup", Function.createDelegate(this, this._onMouseUp));
    $addHandler(this._header, "mousemove", Function.createDelegate(this, this._onMouseMove));
    $addHandler(this._header, "dblclick", Function.createDelegate(this, this._onDblClick));

    this.createBar();
}
function ComponentGeek$Extender$TableXResizeBehavior$dispose() {
    $clearHandlers(this._header);
    ComponentGeek.Extender.TableXResizeBehavior.callBaseMethod(this, 'dispose');
}

function ComponentGeek$Extender$TableXResizeBehavior$getFirstRowCell(tbl, cellIndex) {
    ///<summary>ヘッダセル取得</summary>
    // テーブルの1行目のセルを取得する
    var headerCell = tbl.rows(0).cells(cellIndex);
    return headerCell;
}

function ComponentGeek$Extender$TableXResizeBehavior$createBar() {
    ///<summary>変更バー作成</summary>
    // リサイズ時に表示する縦横バー作成
    // 横幅変更バー作成
    var elem = $get(this._vBarID);
    if (!elem) {
        elem = document.createElement("span");
        elem.id = this._vBarID;
        elem.style.position = "absolute";
        elem.style.top = "0";
        elem.style.left = "0";
        elem.style.height = "0";
        elem.style.width = "2";
        elem.style.background = "silver";
        elem.style.borderLeft = "1px solid black";
        elem.style.display = "none";

        document.body.appendChild(elem);
    }
}

function ComponentGeek$Extender$TableXResizeBehavior$clear() {
    // プロパティをクリア
    var elem = $get(this._vBarID);
    if (elem) {
        elem.runtimeStyle.display = "none";
    }
    this._target = null;
    this._startX = null;
    this._endX = null;
    this._xResizing = false;
}

function ComponentGeek$Extender$TableXResizeBehavior$_onMouseDown(e) {
    // マウスダウンイベント
    // 変更カーソル表示中の場合はリサイズ開始
    var vBar = $get(this._vBarID);
    if (!vBar) return;

    if (!this._header) return;

    if (this._xResizeCursorVisible) {
        this._target = e.target;
        this._startX = e.clientX;
        this._xResizing = true;

        this._header.setCapture();

        var pos = getElementPosition(this._body);

        vBar.runtimeStyle.top = pos.top;
        vBar.runtimeStyle.left = e.clientX + document.documentElement.scrollLeft;
        if (this._body.parentElement.offsetHeight < this._body.offsetHeight) {
            vBar.runtimeStyle.height = this._body.parentElement.offsetHeight;
        } else {
            vBar.runtimeStyle.height = this._body.offsetHeight;
        }
        vBar.runtimeStyle.display = "inline";
    }
}

function ComponentGeek$Extender$TableXResizeBehavior$_onMouseMove(e) {
    if (!this._header) return;

    // マウス移動イベント
    // マウスが列の左端,下端に近づくとカーソル変更
    if (this._xResizing) {
        var vBar = $get(this._vBarID);
        if (!vBar) return;

        // 変更バー移動
        vBar.runtimeStyle.left = e.clientX + document.documentElement.scrollLeft;
        document.selection.empty();

        return;
    }
    if (e.offsetX >= (e.target.offsetWidth - this._edgeThreshold)) {
        this._xResizeCursorVisible = true;
        //        e.target.runtimeStyle.cursor = "e-resize";
        this._header.runtimeStyle.cursor = "e-resize";
    } else {
        this._xResizeCursorVisible = false;
        // 行ヘッダかつ列ヘッダの場合にマウス移動イベントの競合対策
        // 水平、垂直方向変更用にマウス変更イベントが重複して発生するため
        // 垂直方向変更中にカーソルをクリアしないようにする
        if (this._header.runtimeStyle.cursor == "s-resize") return;
        
        if (this._header.style.cursor) {
            debugger;
            this._header.runtimeStyle.cursor = this._header.style.cursor;
        } else {
            this._header.runtimeStyle.cursor = "";
        }
    }
}
function ComponentGeek$Extender$TableXResizeBehavior$_onMouseUp(e) {
    // マウスアップイベント
    // サイズ変更中の場合は変更を行う
    if (this._target == null) return;
    this._endX = e.clientX;

    if (!this._header) return;

    if (this._xResizing && this._startX != null && Math.abs(this._endX - this._startX) > this._resizeThreashold) {
        // 新しい幅
        var newWidth = this._target.offsetWidth + (this._endX - this._startX);
        // 閾値以上の場合はサイズを適用,そうでない場合、最小幅設定
        if (newWidth > this._sizeThreshold) {
            // ヘッダセル取得
            var head = this.getFirstRowCell(this._header, this._target.cellIndex);
            if (head) head.style.width = newWidth;

            var body = this.getFirstRowCell(this._body, this._target.cellIndex);
            if (body) body.style.width = newWidth;
        }
    }

    this._header.releaseCapture();
    this.clear();
}
function ComponentGeek$Extender$TableXResizeBehavior$_onDblClick(e) {
    ///<summary>リサイズカーソル表示外でダブルクリックが行われた場合</summary>
    if (this._xResizeCursorVisible) return;

    if (e.target.cellIndex == 'undefined') return;

    var preferredWidth = this._sizeThreshold;
    for (var i = 0; i < this._body.rows.length; ++i) {
        var cell = this._body.rows(i).cells(e.target.cellIndex);
        if (cell.scrollWidth > preferredWidth) {
            preferredWidth = cell.scrollWidth;
        }
    }
    // 求まったサイズで幅を広げる
    var header = this.getFirstRowCell(this._header, e.target.cellIndex);
    if (!header) return;
    var width = eval(header.style.width.replace('px', ''));
    // 変更幅が小さい場合は処理なし
    if (Math.abs(width - preferredWidth) < this._resizeThreashold) return;

    // サイズ変更
    if (header) header.style.width = preferredWidth;
    var body = this.getFirstRowCell(this._body, e.target.cellIndex);
    if (body) body.style.width = preferredWidth;
}
ComponentGeek.Extender.TableXResizeBehavior.registerClass('ComponentGeek.Extender.TableXResizeBehavior', Sys.UI.Behavior);

/////////////////////////

ComponentGeek.Extender.TableYResizeBehavior = function(element) {
    ComponentGeek.Extender.TableYResizeBehavior.initializeBase(this, [element]);

    this._header;   // ヘッダ
    this._body;     // ボディ
    this._target;   // サイズ変更対象セル
    this._startY;   // 高さ変更開始Y座標
    this._endY;     // 高さ変更開始X座標
    this._yResizing = false;     // 高さ変更中フラグ
    this._resizeThreashold = 8;  // 最小変更幅
    this._edgeThreshold = 8;     // 幅,高さ変更カーソル表示閾値
    this._sizeThreshold = 20;    // 最小列,幅高さ
    this._hBarID = "hBarID";     // 変更時に表示する横棒のID
    this._yResizeCursorVisible = false;  // 高さの変更カーソル表示フラグ
}

ComponentGeek.Extender.TableYResizeBehavior.prototype = {
    initialize: ComponentGeek$Extender$TableYResizeBehavior$initialize,
    dispose: ComponentGeek$Extender$TableYResizeBehavior$dispose,
    _onMouseDown: ComponentGeek$Extender$TableYResizeBehavior$_onMouseDown,
    _onMouseUp: ComponentGeek$Extender$TableYResizeBehavior$_onMouseUp,
    _onMouseMove: ComponentGeek$Extender$TableYResizeBehavior$_onMouseMove,
    createBar: ComponentGeek$Extender$TableYResizeBehavior$createBar,
    clear: ComponentGeek$Extender$TableYResizeBehavior$clear,
    getFirstRowCell: ComponentGeek$Extender$TableYResizeBehavior$getFirstRowCell,

    get_header: function() {
        return this._header;
    },
    set_header: function(value) {
        if (this._header !== value) {
            this._header = value;
            this.raisePropertyChanged('header');
        }
    },
    get_body: function() {
        return this._body;
    },
    set_body: function(value) {
        if (this._body !== value) {
            this._body = value;
            this.raisePropertyChanged('body');
        }
    }
}

function ComponentGeek$Extender$TableYResizeBehavior$initialize() {
    ComponentGeek.Extender.TableYResizeBehavior.callBaseMethod(this, 'initialize');

    if (!this._header) {
        throw Error.create("headerを指定してください");
    }

    $addHandler(this._header, "mousedown", Function.createDelegate(this, this._onMouseDown));
    $addHandler(this._header, "mouseup", Function.createDelegate(this, this._onMouseUp));
    $addHandler(this._header, "mousemove", Function.createDelegate(this, this._onMouseMove));

    this.createBar();
}
function ComponentGeek$Extender$TableYResizeBehavior$dispose() {
    $clearHandlers(this._header);
    ComponentGeek.Extender.TableYResizeBehavior.callBaseMethod(this, 'dispose');
}

function ComponentGeek$Extender$TableYResizeBehavior$getFirstRowCell(tbl, cellIndex) {
    ///<summary>ヘッダセル取得</summary>
    // テーブルの1行目のセルを取得する
    var headerCell = tbl.rows(0).cells(cellIndex);
    return headerCell;
}

function ComponentGeek$Extender$TableYResizeBehavior$createBar() {
    ///<summary>変更バー作成</summary>
    // リサイズ時に表示する縦横バー作成
    // 横幅変更バー作成
    elem = $get(this._hBarID);
    if (!elem) {
        //elem = document.createElement("hr");
        elem = document.createElement("span");
        elem.id = this._hBarID;
        elem.style.position = "absolute";
        elem.style.top = "0";
        elem.style.left = "0";
        elem.style.height = "2";
        elem.style.width = "0";
        elem.style.background = "silver";
        elem.style.borderLeft = "1px solid black";
        elem.style.display = "none";

        document.body.appendChild(elem);
    }
}

function ComponentGeek$Extender$TableYResizeBehavior$clear() {
    // プロパティをクリア
    var elem = $get(this._hBarID);
    if (elem) {
        elem.runtimeStyle.display = "none";
    }
    this._target = null;
    this._startY = null;
    this._endY = null;
    this._yResizing = false;
}

function ComponentGeek$Extender$TableYResizeBehavior$_onMouseDown(e) {
    // マウスダウンイベント
    // 変更カーソル表示中の場合はリサイズ開始
    var hBar = $get(this._hBarID);
    if (!hBar) return;

    if (!this._header) return;

    if (this._yResizeCursorVisible) {
        this._target = e.target;
        this._startY = e.clientY;
        this._yResizing = true;

        this._header.setCapture();

        var pos = getElementPosition(this._body);

        hBar.runtimeStyle.top = e.clientY + document.documentElement.scrollTop;

        //hBar.runtimeStyle.left = this._table.parentElement.offsetLeft + document.documentElement.scrollLeft + document.body.offsetLeft;
        hBar.runtimeStyle.left = pos.left;
        if (this._body.parentElement.offsetWidth < this._body.offsetWidth) {
            hBar.runtimeStyle.width = this._body.parentElement.offsetWidth;
        } else {
            hBar.runtimeStyle.width = this._body.offsetWidth;
        }
        hBar.runtimeStyle.display = "inline";
    }
}

function ComponentGeek$Extender$TableYResizeBehavior$_onMouseMove(e) {
    // マウス移動イベント
    // マウスが列の左端,下端に近づくとカーソル変更
    
    if (this._yResizing) {
        var hBar = $get(this._hBarID);
        if (!hBar) return;

        // 変更バー移動
        if (this._yResizing) {
            hBar.runtimeStyle.top = e.clientY + document.documentElement.scrollTop;
            document.selection.empty();
        }
        return;
    }
    if (e.offsetY >= (e.target.offsetHeight - this._edgeThreshold)) {
        this._yResizeCursorVisible = true;
        this._header.runtimeStyle.cursor = "s-resize";
    } else {
        this._yResizeCursorVisible = false;
        // 行ヘッダかつ列ヘッダの場合にマウス移動イベントの競合対策
        // 水平、垂直方向変更用にマウス変更イベントが重複して発生するため
        // 水平方向変更中にカーソルをクリアしないようにする
        if (this._header.runtimeStyle.cursor == "e-resize") return;

        if (this._header.style.cursor) {
            this._header.runtimeStyle.cursor = this._header.style.cursor;
        } else {
            this._header.runtimeStyle.cursor = "";
        }
    }
}
function ComponentGeek$Extender$TableYResizeBehavior$_onMouseUp(e) {
    // マウスアップイベント
    // サイズ変更中の場合は変更を行う
    if (this._target == null) return;
    this._endY = e.clientY;

    if (!this._header) return;

    if (this._yResizing && this._startY != null && Math.abs(this._endY - this._startY) > this._resizeThreashold) {
        // 縦幅変更中
        // 新しい高さ
        var newHeight = this._target.offsetHeight + (this._endY - this._startY);
        var rowSpan = this._target.rowSpan;
        for (var i = 0; i < rowSpan; ++i) {
            if (newHeight / rowSpan > this._sizeThreshold) {
                newHeight = newHeight / rowSpan;
            } else {
                newHeight = this._sizeThreshold;
            }
            var headertr = this._header.rows(this._target.parentElement.rowIndex + i);
            headertr.style.height = newHeight;
            var bodytr = this._body.rows(this._target.parentElement.rowIndex + i);
            bodytr.style.height = newHeight;
        }
    }

    this._header.releaseCapture();
    this.clear();
}

ComponentGeek.Extender.TableYResizeBehavior.registerClass('ComponentGeek.Extender.TableYResizeBehavior', Sys.UI.Behavior);


if (typeof (Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();

/// 位置指定要素ではない要素の位置を取得する汎用関数
function getElementPosition(elem) {
    var offsetTrail = elem;
    var offsetLeft = 0;
    var offsetTop = 0;
    while (offsetTrail) {
        offsetLeft += offsetTrail.offsetLeft;
        offsetTop += offsetTrail.offsetTop;
        offsetTrail = offsetTrail.offsetParent;
    }
    if (navigator.userAgent.indexOf("Mac") != -1 &&
        typeof document.body.leftMargin != 'undefined') {
        offsetLeft += document.body.leftMargin;
        offsetTop += document.body.topMargin;
    }
    return { left: offsetLeft, top: offsetTop };
}

1.6 AssemblyInfo.cs の編集

プロジェクトのアセンブリインフォを編集します。変更と追加箇所をのみ記載します。javascript ファイルをダウンロードできるようにリソースとして設定しています。

// アセンブリのバージョン情報は、以下の 4 つの値で構成されています:
//
//      Major Version
//      Minor Version 
//      Build Number
//      Revision
//
// すべての値を指定するか、下のように '*' を使ってリビジョンおよびビルド番号を
// 既定値にすることができます:
[assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: WebResource("ComponentGeek.Extender.FreezeHeaderBehavior.js", "text/javascript")]
[assembly: WebResource("ComponentGeek.Extender.TableResizeBehavior.js", "text/javascript")]
[assembly: ScriptResource("ComponentGeek.Extender.FreezeHeaderBehavior.js",
   "ComponentGeek.Extender.FreezeHeaderBehavior", "ComponentGeek.Extender.Resource")]
[assembly: ScriptResource("ComponentGeek.Extender.TableResizeBehavior.js",
   "ComponentGeek.Extender.TableResizeBehavior", "ComponentGeek.Extender.Resource")]

2. ソリューションのダウンロード

Visual Studio 2008 で作成した Extender のソリューションを公開しますので、よかったら使ってみてください。プログラムはオープンソースです。利用する場合はご連絡をいただけるとうれしいです。

3. まとめ

今回の説明は以上です。といってもあまり解説していませんが。ソリューションをダウンロードできるようにしているので、動作確認やASP.NET AJAX 1.0 用に書き換えて使用できると思います。

指摘点等あります場合はご連絡ください。