スキップしてメイン コンテンツに移動

transform:transition で sticky しているサイドバー内の要素の位置を Safari 3.x, Chrome 1 で取得できない、ついでに Gecko 0.9.2 以下でも要素の位置を取得できない

ウェブ文書のサイドバーを JavaScript で sticky していた際に遭遇した問題です。

僕が開発した SidebarFixer では、タブ・フォーカスされたリンク要素を元にサイドバーの表示位置をよしなにする機能があります。

Safari 3 以降, Chrome 1 以降で offsetParent が不正

このフォーカスイン・イベントの際に、getBoundingClientRect を実装しない Safari 3.x, Chrome 1 では、offsetTop を使って、サイドバー内の要素の位置を求めます。

しかし、offsetTop ではサイドバーの左上を基準とする要素の位置を求めることが出来ませんでした。あえなく transform から position:fixed にフォールバックしました。

ページ座標を求める関数を一月以上に渡って開発した今にして気付きましたが、transform を指定した要素は、offsetParent になるところ、Safari, Chrome でそうでは無いのが原因でした。

サイドバーが offsetParent にならなかったため、サイドバーより上位の要素との offset 値になっていました。そこで、サイドバー自体の offset 値を減算することで補正できました。

簡単に確認したところ、Chrome 49 でも offsetParent にならず、比較的新しい Chrome 126 では offsetParent になっていました。これを更に絞り込むのはしんどいです(汗

ちなみに Gecko では、1.9.1 から -moz-transform を実装し、-moz-transform 指定のある要素は offsetParent になります。

Gecko 0.9.2 以下の offsetParent がバギー

Gecko 0.9.2 以下では、位置指定祖先要素が target.offsetParent にはならず、<body> ないし null が返ります。ポジティブに考えるなら、offsetParent を辿るループが不要で、要素のページ座標を、次の一撃で求められることになります。

var offsetTop = target.offsetTop + (body = target.offsetParent) ? body.offsetTop : 0;

とはいえ getComputedStyle をサポートしない Gecko 0.9.5 以下で、offsetParent を駆使して position を調べようと検討していた際に頭が白くなりました。付け加えると、Gecko 0.7~1.8.1 は、document.getBoxBobjectFor が使えるので、実際に offsetTop コースに入るのは、Gecko 0.6 だけです。

制限有のライト版・要素間の offsetTop を求める関数

次の関数は対象要素とその祖先要素の y 軸方向の差を求める簡易な関数です。ancestor が offsetParent に出現しないケースでも、意図した位置が取得できます。しかし、汎用的なものではなく、次の条件で正しい値が得られます。

  1. target はインライン要素ではない
  2. target は ancester の子孫
  3. 対象要素(target)とその祖先要素(ancestor)の間に、スクロールする要素を含まない
  4. 対象要素と <html> 要素の間に、transform 指定されている要素を含まない
    • Gecko 3.5~11 は getBoundingClientRect が transform を反映しない為
    • offsetTop を使うコースでは transform を反映しない為、-webkit-transform を実装しながら、これに該当するのは、Safari 3.x と Chrome 1
  5. 対象要素とその祖先要素の間の要素と、祖先要素自体に borderTop が指定されていない

条件を守った上で、サイドバー内のリストアイテムといった限定的な状況で使うのが良いでしょう。

/**
 * @param {!Element} target
 * @param {!Element} ancestor
 * @return {number} */
function getOffsetTopBetween(target, ancestor){
  if (5 <= _ie_ || 9.5 <= _opr_ || 1.9 <= _gck_ || 4 <= _safari_ || 2 <= _chrome_) {
    return target.getBoundingClientRect().top - ancestor.getBoundingClientRect().top;
  } else (0.7 <= _gck_) {
    return document.getBoxBobjectFor(target).y - document.getBoxBobjectFor(ancestor).y;
  } else if (_gck0_ < 9.3 && _gckdate_ < 20010726) { // Gecko 0.9.2 以下, 0.9.2.1 は除外
    var y = target.offsetTop - ancestor.offsetTop,
        body = ancestor.offsetParent;

    if (!target.offsetParent && body) {
      y -= body.offsetTop;
    };
    return y;
  } else if (_ie_ || _opr_ || 9 <= _gck_ || _safari_ || _chrome_) {
    // element.contains を実装する環境用
    var current = target, y = 0;

    while (current && ancestor.contains(current)) {
      y += current.offsetTop;
      current = current.offsetParent;
    };
    if (current !== ancestor) {
      y -= ancestor.offsetTop;
    };
    return y;
  };
  // element.contains を実装しないブラウザ用、例えば Gecko の contains 実装は 9 からと遅い
  var current = target,
      y = current.offsetTop,
      offsetParent = current.offsetParent;

  while (ancestor !== current) {
    if (offsetParent === current) {
      y += current.offsetTop;
      offsetParent = current.offsetParent;
    };
    current = current.parentNode;
  };
  if (offsetParent !== ancestor) {
    y -= ancestor.offsetTop;
  };
  return y;
};