JavaScript Diary

A要素の位置を取得 [ 2001/11/21 ]

あるメニュースクリプトを作成していたのですが、そこで「ユーザが任意で作成するTABLEタグ内のAタグの位置を正確に取得」しなくてはいけない状況になってしまい、しかもクロスブラウザということなので、まぁなんとも「スクリプター泣かせ」な話だなあと思いつつ、半ば諦め状態であったがそれなりに実装できたので紹介しておく。

テスト環境は IE5,NS4,NS6/WIN であるが、兎に角参ったのはIE5である。
ご存知の通り、IE5はTABLEがHTML構造からある意味独立してしまっている。これはスタイルシートで BODY{ font-size:13px; } などとしていてもTABLEのセルには継承されず、恐らく BODY,TH,TD{ font-size:13px; } と書いている人も少なくないと思う。
これが JavaScript にも影響しているのが痛い。つまり、要素の offsetLeft, offsetTop プロパティ(以下、offsetプロパティ) はドキュメントの構造に関係なく常に「ブラウザの表示可能部分の左上端からの位置」を返すべきである。
しかしIE5の場合、この「ある意味独立」している状況のため、TABLE内に存在する要素に関しては正確な位置を返さない。
つまり、TABLEの各セルはドキュメントから独立しているため offsetプロパティ は、ドキュメントの左上端からではなく、セルの左上端からの位置を返す(間違っていたらごめんなさい)。

と思ってましたが調べたところoffsetプロパティは「親要素の左上端からの位置」を返すのが正しいようです。下のプログラムでは以前の間違った解釈で作成していますが、きちんとNS6でも正確な値が返ってくるのでどっちが正しいのかは知りませんが・・・保留 ^^);

ということで↓のプログラムを簡単に説明しておく。

IEでは上述したようにTABLEタグでネストされている状態では正確な位置を返さないため、要素の上の階層を調べTABLEタグが存在するか調べる必要がある(実際にはTD,THタグを調べる)。
存在した場合、TABLEのoffset量とセルのoffset量、そして自分自身のoffset量を足した値を返す。つまり、
(TABLEのドキュメント左上端からの位置)+(セルのTABLE左上端からの位置)+(自分自身のセル左上端からの位置)
ということになる。プラスY座標に関しては枠線(ボーダー)の量1を足す必要がある(border="1"としても幅は+1であることに関係しているっぽい)。
あとはネスト状態を調べるために再帰で同じ操作を繰り返すのみです。

次に NS6 であるがIEのような仕様ではないのでごく普通に offsetLeft, offsetTopプロパティ を調べれば良い。

最後に NS4 である。つくづく任意の要素ではなくて良かったと思わせるが、A要素には位置を知るために x, yプロパティ が用意されている。これは <A name="ID"> のような使い方をしているとき、スクリプトでそこまでスクロールを可能にするためだけに用意されているものであろう。
このプロパティもまたIEのようなことはなく、きちんとドキュメント左上端からの位置を返してくれるので問題はない。

さて、ここまでは問題なく実装できたわけだが、そんなことなら苦労はしない。要求される機能はA要素は動くことも想定しなければならない。そう、動的な位置配置で位置を変えられてしまうのである(これは「absolute な要素でネストされている状態」ということです)。

今回初めて分かったことですが、IE, NS6 の offsetプロパティ は absolute な要素でネストされているある状況に関しては、これまた正確な値を返しません。
この点に関しては非常に難しく 僕はoffsetプロパティは常に「ドキュメント左上端からの位置を返す」と勝手に理解していましたが、IE/NS6 の挙動を見る限りでは「ある独立したブロックの左上端からの位置を返す」と解釈した方が正しいのかも知れません。
さて、そのある状況というのはTABLE要素を含むときのことで、動的に変更したあともう一回調べると最初の位置が返ってきます。

ということでもう一回見直す必要がありますが、IE/NS6 は簡単で「そのabsoluteな要素が移動した分を加えればいい」だけの話です。
しかし、NS4 はこうはいきません。↓の関数はあとで使用例を紹介しますが、引数は唯一つでそれはリンクオブジェクトです。NS4 には階層がどうなっているのか知る方法がないのでレイヤーでネストされているかをリンクオブジェクトから推測することは出来ません。
何か他の方法を探す必要がありますが、どう考えても無理です。ただし、これは作成者の操作になりますが、親レイヤーの移動量が分かれば IE/NS6 と同じように移動量を加えればよいことになります。
ということでリンクオブジェクトに新たに $x, $y プロパティを用意し、こいつに正確な位置を記憶させておきましょう(本当は x, y プロパティに補正量を加えたいところですが、どうやら書き込み禁止のプロパティっぽいです)。ただし、これは移動量が分かる場合のみです。これのため、この関数は本当の意味でのクロスはしていません(経験上大概は分かる場合が殆どなので問題はないでしょう)。

// 引数は一つで、位置を調べたい要素を与える
// クロスブラウザ(NS4,NS6,IE4,5)を考えるなら、その要素はリンクオブジェクトである
// 返される値はオブジェクトで x, yプロパティ を持つ
// 使用例
function getElementPosition( e, root, x, y ){
    if( document.layers !== void 0 ){
        x = e.$x !== void 0 ? e.$x : e.x ;
        y = e.$y !== void 0 ? e.$y : e.y ;
        return { x:x, y:y };
    }else{
        // 親要素に存在する任意の要素(タグ)を調べる関数
        // 但し、tagName に "cell" を指定した場合のみ、td, th を同時に探索する
        // 存在する場合その要素を、存在しない場合 null を返す
        // BODY タグを発見した場合に再帰は終了する
        function searchParentHTMLElement( e, tagName ){
            if( e === document.body ){
                return null ;
            }else{
                var tname = e.tagName.toLowerCase();
                if( tagName == "cell" ){
                    return tname == "td" || tname == "th" ? e : arguments.callee( e.parentNode, tagName );
                }else{
                    return tname == tagName ? e : arguments.callee( e.parentNode, tagName );
                }
            }
        }
        // 親要素に absolute な要素を含むかを調べる関数
        // 存在する場合その要素を、存在しない場合 null を返す
        // BODY タグを発見した場合に再帰は終了する
        function searchParentDynamicElement( e ){
            if( e === document.body ){
                return null ;
            }else{
                return e.style.position == "absolute" ? e : arguments.callee( e.parentNode );
            }
        }
        // 引数が一つ、つまり最初はここに制御が入る
        if( arguments.length == 1 ){
            // absolute要素でネストされているかを調べ、されている場合
            // rootオブジェクト(それ以上探索する必要のない最高位オブジェクト)に設定する。
            // abolute要素で二重にネストされている場合、恐らくヤバい^^);
            var dynElement = searchParentDynamicElement( e );
            // NS6では調べた値をそのまま返す
            // IEの場合、TABLEタグを調べる必要があるので再帰する
            if( dynElement == null ){
                if( document.all !== void 0 ){
                    return arguments.callee( e, document.body, e.offsetLeft, e.offsetTop );
                }else{
                    return { x:e.offsetLeft, y:e.offsetTop };
                }
            }else{
                if( document.all !== void 0 ){
                    return arguments.callee( e, dynElement,
                        dynElement.offsetLeft+e.offsetLeft,
                        dynElement.offsetTop+e.offsetTop );
                }else{
                    return {
                        x:dynElement.offsetLeft+e.offsetLeft,
                        y:dynElement.offsetTop+e.offsetTop };
                }
            }
        }
        // rootオブジェクトまで調べたら再帰を終了する
        if( e === root ){
            return { x:x, y:y };
        }else{
            // th, td タグの存在を調べる
            var elementCELL = searchParentHTMLElement( e, "cell" );
            if( elementCELL == null ){
                return { x:x, y:y };
            }else{
                // ネストされている場合、各種offset量を加えて再帰する
                var elementTABLE = searchParentHTMLElement( elementCELL, "table" );
                x += elementTABLE.offsetLeft+elementCELL.offsetLeft ;
                y += elementTABLE.offsetTop+elementCELL.offsetTop ;
                if( parseInt( elementTABLE.border ) != 0 ) y += 1 ;
                return arguments.callee( elementTABLE, root, x, y );
            }
        }
    }
}

このプログラムは IE6 での確認はしていません。うろ覚えですが IE6 ではスタイルシートがTABLE要素にも継承されるようになったというのを聞きました。ということはこれでは正確な値が返らないことになります^^);
あと注意ですが、STYLEタグ内で absolute 指定をすると 〜.style.position の値は空です。その場合、きちんとスクリプト側で "absolute" の値を代入しないとこの関数は正確な値を返しません。

余談ですがIEでセル(TD,TH)の parentNode を調べていくと書いていなくても TBODYタグに当たります。