HTML要素の位置取得

趣旨

ウェブページとして描画された HTML 要素の画面上の位置を取得する。一見簡単そうに見えるこの作業が、現在実装されているブラウザ上ではとてつもなく難しい。そのことを以下で説明していく。

情報ソース

この問題に関して調べたところ、最もよく出来ているエントリは、susie-t 氏による

offsetTop/offsetLeft/offsetParentの闇

である。とてつもない力作で、実に多くのケースにわたって、包括的に探究が行われている。まるで犯人を追跡する刑事のような執拗さである。氏の自己紹介では「ナマケモノプログラマ」とか謙遜されているが、これはとてもナマケモノにできる仕事ではない。

基本中の基本として W3C CSS 2.1 の次の章を抑えておきたい。

仕様書を英語で読むの?と私も最初は尻込みしていたのだが、思い切ってやってみるとそれほどではなかった。CSS 2.1 もそうだが W3C の仕様書は、きちんと書かれているので、案外読みやすい。正確な理解のためには、やはり原文を英語で読むことをお勧めする。

基本概念

ボックスモデル

本論に進む前にまず、基本概念として、CSS のボックスモデルを説明しておかねばならない。CSS はつぎのような状況を想定している。ユーザーエージェント(ブラウザ)はまず HTML 文書をパースして、ノードツリーを形成する。次にノードツリーを解釈して、画面上にHTML 要素を描画していくわけだが、このとき各 HTML 要素は、対応するボックスを生成すると考えるのである。
これは、ボーダーを持つ div 要素なら自明だろうが、それに限定されず、テキストなどでも同様なのである。たとえば "abc" という文字列にも、その文字列に内接する長方形を考え、その中で文字列が描画されると考えるのだ。

ここまでは、簡単だ。CSS がかくにも複雑なのは、このボックスは実は4重の長方形であるからだ。

上図を見てほしい。

普通に目にする文字列などは、一番内側(黄色)の長方形の内側に描画される。この黄色の長方形を「コンテンツエッジ」と呼ぶ。その外側にはパディング領域とよばれる、空白地帯があり、それを取り囲む長方形(青の領域の内側)をパディングエッジと呼ぶ。青色の部分はボーダー(枠線)である。このボーダーの外側を囲む長方形をボーダーエッジと呼ぶ。さらに外側には、マージンと呼ばれる、他の要素との緩衝地帯がある。この外側を囲む長方形をマージンエッジと呼ぶ。

要素がボックスを生成する、と言った場合、このボックスは実は4つの互いに包含関係にある長方形から成り立つのだ、ということを常に心に留めておく必要がある。このことは CSS においてはいくら強調しても強調しすぎることはない。

描画モード

要素は、display, position, float というプロパティによって、描画のされ方が大きく違ってくる。たとえば display:inline のときには、left や top という位置指定のスタイルは無視されるし、float:right ならば右側に浮動化される、等々だ。描画方法のパタンをここでは便宜的に「描画モード」と呼ぶことにしよう。前回のエントリCSS における display, position, float プロパティの相互関係で、display/position/float の取りうる可能性は次の4通りと結論した。

display position float
block static|relative none
block static|relative right|left
block absolute none
inline static|relative none
これは、描画モードの分類としてはやや不正確だ。position:static と position:relative では描画方法が異なってくるからだ。実際には次の7通りの描画モードが考えられる。(右浮動化と左浮動化は対称的な操作なので、ここでは依然同じものとして扱う)
display position float
block static none
block relative none
block static right|left
block relative right|left
block absolute none
inline static none
inline relative none

本論

offset系プロパティ

まずは offsetTop, offsetLeft, offsetParent というプロパティについて、簡単に説明しよう。これらは、画面の要素位置情報を返してくれる要素のプロパティである。しかし、どこの標準の仕様書にも載っていない。W3C CSS にも W3C DOM Level 2 HTML にも ECMA-262 3rd Editionにも載ってない。offsetTop/offsetLeft/offsetParentの闇によれば、「offset〜のプロパティはIEがVer5で独自に実装したものらしく、その後他のブラウザが追随して実装したようです」とのことだ。つまり IE の独自仕様にすぎず、便利そうだからたまたま他のブラウザも実装しているだけ、という非常に不安定な状態に置かれている、といえる。実際、そのために実装がブラウザごとにかなりまちまちで、これはプログラマに大きな苦悩を投げかけている。

原則

offsetTop, offsetLeft, offsetParent は原則として次のような関係がある。*1

element2 の offsetTop は、その基準となる外側の要素(offsetParent)が element1 であるとき、element1 のパディングエッジの上辺から element2 のボーダーエッジの上辺までの距離である。同様に、element2 の offsetLeft は、その基準となる外側の要素(offsetParent)が element1 であるとき、element1 のパディングエッジの左辺から element2 のボーダーエッジの左辺までの距離である。

ここで注意してほしいのは、offsetTop, offsetLeft ともに 基準になる要素のパディングエッジから、自分の要素のボーダーエッジまでの距離であるということである。このことはよく覚えておいてほしい。*2

この図だと、外側の基準となる要素(offsetParent)がノードツリー上の親要素であるかのような印象を与えるが、実際にはあるルールによって求められる先祖要素であり、親要素とは限らない。

ある要素のキャンバス座標(ドキュメントの左上が (0, 0) となるような座標系)を計算しようとするには、一番基本となる要素(body または html)からスタートして、上図のような計算を、目的の要素に達するまで繰り返し行わなければならないことになる。offset はあくまでも外側の基準となる要素(offsetParent)のパディングエッジから内側の要素のボーダーエッジまでであるから、外側の基準となる要素のボーダーの幅分、計算からもれてしまうことになる。理論的には、このボーダーの幅分を足していけばいいことになるが、そう簡単に話が終わらないところが、この「offset系プロパティの闇」なのである・・・。

現実

offsetTop/offsetLeft/offsetParentの闇は実に力作である。ただ、それでもいくつかの論点が抜けている。たとえば、このエントリはインラインレベル要素への言及がない。((ブロックレベル要素とインラインレベル要素については CSS21:92CSS3BOX を参照のこと) つまり上の描画モードで言えば inline/static/none と inline/relative/none というパタンについての考察がない。この点に関しては私自身が、このエントリに掲載されているテストプログラムを使って、さまざまに試してみた。その結果、offset 系プロパティに関しては、「ほぼ」インラインレベル要素に関してもブロックレベル要素同様に考えてよいことがわかった。(実際には上の描画モードの数の2乗のパタン(7x7 = 49通り)を試さなければならず、全部のパタンまで網羅しているとはいえない。しかし、「ありがちな」パタンについては確認が取れている)ということで susie-t 氏の考察を中心に考えて行きたい。

susie-t 氏が種々の試行錯誤の結果たどり着いた結論は以下の通りである。

対象要素のoffsetParentになる要素の条件

    * ノードツリー上方で以下のいずれかとなる直近の要素。
          o スタイル指定が「position:relative;」「position:absolute;」の要素。
          o 対象要素がposition指定なし(またはstatic)のとき、タグ名が「table」「caption」「td」「th」の要素。
          o 以下【IEのみ】
                + タグ名が「marquee」「map」の要素。*6
                + 対象要素がposition指定なし(またはstatic)のとき
                      # スタイルでwidthもしくはheightが指定されている要素。
                      # スタイル指定が「float:left;」「float:right;」の要素。
                      # タグ名が「fieldset」「legend」の要素。
    * 以上に該当がなければbody要素。【IE】のStandards modeでは、html要素となる。ただし、対象要素がposition無指定もしくはstaticである場合は除く(body要素になる)。

offsetLeft、offsetTopについての注意点

    * offsetParent 要素側の基準は、基本的に、IE、Netscape、FireFoxで基準要素の枠線内側(左辺、上辺)、Operaで基準要素の枠線外側であるが、例外もある(後述)。これと、対象要素の枠線外側(左辺、上辺)との距離がoffsetLeft、offsetTopになる。
    * offsetParentがbody要素でposition:absolute;以外の場合、offsetLeft、offsetTopにはbody要素のmarginが加算される。ただし【IE】のStandards modeでは加算されない。
          o body要素のborder-widthは加算されない(【Opera】はもともとoffsetLeft、offsetTopがborder-widthを含んでいる)。
          o 【Firefox】ではbody要素のborder-widthが減算される。


TABLE系要素

    *  IE
          o offsetParentがTABLE要素の場合、基準は枠線外側。
    * Firefox
          o offsetParentがTABLE, TH, TD要素の場合でposition:static;の場合、すべての要素について基準は枠線外側。
          o position:relative;であるTD要素内の要素について
                + position:absolute;の場合、offsetParentはBODY要素。
                + position:relative;の場合、offsetParentはTD要素で基準は枠線内側。
                + position:static;の場合、offsetParentはTD要素で基準は枠線外側。
          o CAPTION要素内のposition:static;な要素のoffsetParentはTABLE要素となるが、offsetTop/Leftの値はBODY要素を基準にしている。
    * Netscape
          o offsetParentがTABLE, TH, TD要素の場合でposition:static;の場合、すべての要素について基準は枠線外側。
          o position:relative;およびabsolute;であるTD要素内の要素について
                + position:absolute;の場合、offsetParentはBODY要素。
                + position:relative;の場合、offsetParentはTD要素で基準は枠線内側。
                + position:static;の場合、offsetParentはTD要素で基準は枠線外側。
          o CAPTION要素内のposition:static;な要素のoffsetParentはTABLE要素となるが、offsetTop/Leftの値はBODY要素を基準にしている。

うーむ、この複雑さに圧倒されないだろうか?私は、思わず圧倒されそうである。詳細は元のエントリをみていただくとして、大雑把に整理すると次の3つのことが言える。

  1. 通常の要素とその offsetParent(body要素以外) の関係では、offsetTop / offsetLeft は、offsetParent のパディングエッジから当該要素へのボーダーエッジへの距離を原則とし、一部 TABLE 系要素において offsetParent のボーダーエッジから当該要素のボーダーエッジへの距離となる例外がある。
  2. BODY 要素がボーダーやマージンを持つとき、不思議な振る舞いをするときがある。
  3. Firefox における TABLE の CAPTION 要素の振る舞いだけは変態的。offsetParent が TABLE 要素であるにもかかわらず、offsetTop / offsetLeft は BODY 要素からの距離で計算されるなんて。しかも、susie-t 氏の考察にはなかったのだが、TABLE の position を relative や absolute にすると、この変態的な振る舞いはなくなり、offsetTop / offsetLeft は TABLE 要素からの距離で計算されるようになるようである。これは、Firefox のバグといってもよいのではないか?(私が試したのは Windows XP SP2 上の Firefox 2.0.0.11 である)

対策

offset 系プロパティのこの錯綜した現実に対して、susie-t 氏は実装を以て応えている。その勇気は称賛に値するだろう。

Positionオブジェクトの枠線幅問題対策

susie-t 氏は、TABLE 系要素はサポート外であると断った上で、offsetTop / offsetLeft を累積していくときに、基準要素のボーダー幅を足しこむことによって、要素の正しいキャンバス座標を得ようと試みている。この実装は、大抵のケースでうまく動くはずだ。

だたこの実装では基準要素のボーダー幅が正しく算出されないときがある。この実装は基準要素のボーダー幅が px 単位で指定されていることを前提としているが、実際には em や pt といった別の単位でも与えうる。Firefox ではスタイルの計算値がかならず px 単位になるので、うまく行く場合が多いのだが、IE では残念ながら計算値も、指定値と同じ単位で算出される。(これは CSS 仕様に反しており IE のバグだと思われる)

結論

これだけ考察しておいて陳腐な結論で恐縮だが、結局のところ、要素のキャンバス座標の「近似値」として、offsetTop / offsetLeft の単純な累積値を使えばいいのではないだろうか?というのが私の意見である。これは prototype.js の Position.cumulativeOffset の実装そのものである。その根拠は以下のようなものだ。

  1. offsetTop / offsetLeft の累積値から、正しいキャンバス座標を求めるには、基準要素(群)のボーダー幅を足すときと足さないときがある。足すにしても、前節で見たように常に正しくボーダー幅が得られるとは限らない。現実のデザインにおいて、ボーダーというのは、大抵 5px 以下であろう。祖先要素に基準要素が複数あり、それぞれボーダーを持っていたとしても、全体として 10px を超えるケースはまれではないか。10px の誤差を許容すれば、実装は大幅に単純になりうる。
  1. 「BODY 要素がボーダーやマージンを持つとき、不思議な振る舞いをするときがある」という問題がある。これは FireFox で BODY 要素がボーダーを持つときや、IE6 が標準モード(DOCTYPE を指定して CSS の仕様により忠実に動くようにしたモード) でマージンを持つ場合に当たる。ただし、すべての要素が等しく影響を受ける分には、各要素間の相対位置は変わらないわけで、相対位置のみが問題であるようなアプリケーションには問題を引き起こさないだろう。(そしてそういうアプリがほとんどであるように思われる)そもそも公開されているウェブサイトで BODY要素でボーダーやマージンがあるものはそれほど多くないのではないか?
  1. FireFox における CAPTION 要素について。CAPTION 要素はあまり使われていない印象があるので、切り捨てても大きな問題はないのではないか?

ぼやき

それにしても offsetTop / offsetLeft / offsetPadding をめぐる状況のしょっぱさはいかがなものだろうか?いわゆる「Ajax ぐりぐり」の Web アプリを作るにおいて、描画された要素のキャンバス座標を求める API は、基本中の基本であるはずだ。それがこのヘタレぶり。ブラウザは、結局要素をブラウザのクライアント領域のどこかに描画するのだから、その実装レベルでは、要素の描画位置を知っているはずなのだ。それをそのまま返してくれればよさそうなものを、まるで嫌がらせのように offset 系のなんとも中途半端なプロパティ値を返してくる。ウェブアプリ全盛の時代を一日でも遅らせたいという Microsoft の陰謀なのではないか、と邪推したくなる。(というのは、冗談だが。でも案外当たっていたりして)

より自由なウェブアプリケーション構築のため、

  1. キャンバス座標系で、要素の存在位置を返すインターフェイス
  2. キャンバス座標系で、要素の位置を設定するインターフェイス

が標準化されるべきである。あるいは、もうすでに標準化団体は動き出しているのかもしれない。そう期待したいものである。

*1:仕様というより、種々の実装を試してみて得られた経験則にすぎない。

*2:Opera では offsetTop / offsetLef は原則として基準要素のボーダーエッジ(パディングエッジではなく!)から、対象要素のボーダーエッジまでの距離になるようである