読者です 読者をやめる 読者になる 読者になる

nicovio Thumbinfo popupで使っている要素の位置取得関数

ニコニコ大百科のキーワードをポップアップ表示させるGreaseMonkeyスクリプト『Popup Nico Dict』リリースしました - 5.1さらうどん - 過去ログを見て。大百科ポップアップはnicovideo Thumbinfo popupにそのうち実装しようとおもっていた機能だったので先を越された―!という気分ですw
で,リンク先記事のコメント欄でid:Sore_0さんが要素の位置取得方法の参考としてnicovideo Thumbinfo popupを挙げてくださりました。ありがとうございます。
この位置取得関数ですが,現在製作中の新版thumbinfo popupで新たに書き直したものがあるので,せっかくなのでここに載せておこうと思います。
ニコニコ動画内で使うだけだったら,calcAreaPosition関数は不要だと思います(area要素を使っている場所が無いため)。

追記

元記事を読み直してみたら完全に読み違えていたwポップアップを表示させるリンクの位置取得ではなく,ポップアップ自体を表示させる位置の導出が問題になっているようでした。そっちの処理も新版で書き直したので載せておきます。
先に挙げたスクリプトはポップアップをposition: absolute;で表示させているので,いくらか改造が必要だと思いますが,参考になれば幸いです。

追追記

要素の位置取得関数が一部バグってたので修正しました。

要素の位置取得関数

function getPosition(elem) {
  var pos = {
    top: elem.offsetTop, left: elem.offsetLeft,
    bottom: elem.offsetTop + elem.offsetHeight,
    right: elem.offsetLeft + elem.offsetWidth };

  // top, bottom, left, right を子要素を考慮したものにする
  calcChildrenOffset(elem.childNodes);

  pos.height = pos.bottom - pos.top;
  pos.width = pos.right - pos.left;

  // 親要素の位置を取得
  var target = elem, dx = 0, dy = 0;
  while((target = target.offsetParent) !== null && target != document.body) {
    var p = target;
    dy += target.offsetTop;
    dx += target.offsetLeft;
  }

  // 親要素のスクロールによる要素位置の変化を取得
  // 標準準拠モード: documentElement, 後方互換モード: body のスクロール量を取得
  var scTop = document.documentElement.scrollTop + document.body.scrollTop;
  var scLeft = document.documentElement.scrollLeft + document.body.scrollLeft;
  target = elem;
  // position: fixed; でポップアップを表示させるので,計算した位置から html のスクロール分を引く
  pos.top -= scTop;
  pos.left -= scLeft;
  while((target = target.parentNode) !== null && target != document.body) {
    dy -= target.scrollTop;
    dx -= target.scrollLeft;
    // position: fixed; な親が存在したらそこで探索を止めて html のスクロール分を足して終了
    if(getComputedStyle(target, '').position == 'fixed') {
      dy += scTop;
      dx += scLeft;
      break;
    }
  }
  pos.top += dy;
  pos.bottom = pos.top + pos.height; // 修正
  pos.left += dx;
  pos.right = pos.left + pos.width;  // 修正

  if(elem.nodeName == 'AREA')
    calcAreaPosition();

  return pos;

  // 以下,補助関数

  function toInt(s) { return parseInt(s, 10); }

  function calcChildrenOffset(children) {
    Array.forEach(
      children,
      function(target) {
        // target は Element で,elem は target.offsetParent の子孫でなければならない
        if(target.nodeType != 1 ||
           (elem.compareDocumentPosition(target.offsetParent) & 8) == 0)
          return;

        if(target.offsetTop < pos.top)
          pos.top = target.offsetTop;
        if(target.offsetLeft < pos.left)
          pos.left = target.offsetLeft;

        var tBottom = target.offsetTop + target.offsetHeight;
        if(tBottom > pos.bottom)
          pos.bottom = tBottom;
        var tRight = target.offsetWidth + target.offsetLeft;
        if(tRight > pos.right)
          pos.right = tRight;

        calcChildrenOffset(target.childNodes);
      }
    );
  }

  function calcAreaPosition() {
    var coords = elem.coords.replace(/\s*,\s*/g, ',')
      .replace(/\s+/g, ' ').replace(/^\s|\s$/g, '');

    switch(elem.shape) {
    case 'rect':
      var [left, top, right, bottom] = coords.split(/,|\s/g).map(toInt);
      pos.top += top;
      pos.left += left;
      pos.height = bottom - top;
      pos.width = right - left;
      break;
    case 'circle':
      var [cx, cy, r] = coords.split(/,/g).map(toInt);
      pos.top += cy - r;
      pos.left += cx - r;
      pos.height = 2*r;
      pos.width = 2*r;
      break;
    case 'poly':
      var minX = Infinity, maxX = -Infinity;
      var minY = Infinity, maxY = -Infinity;
      coords.split(/\s/).forEach(
        function(xy) {
          var [x, y] = xy.split(/,/).map(toInt);
          if(x < minX) minX = x; if(x > maxX) maxX = x;
          if(y < minY) minY = y; if(y > maxY) maxY = y;
        });
      pos.top += minY;
      pos.left += minX;
      pos.height = maxY - minY;
      pos.width = maxX - minX;
      break;
    }

    pos.bottom = pos.top + pos.height;
    pos.right = pos.left + pos.width;
  }
}

ポップアップ表示位置調整関数

関数中の this.frame はポップアップ表示させている div 要素

var PopupFrame = function(...) {...};
(略)
PopupFrame.prototype = {
  (略)
  adjustPosition: function() {
    const POPUP_VERTICAL_MARGIN = 10;
    const POPUP_HORIZONTAL_MARGIN = 10;

    if(this.manuallyMoved || this.createElement === null)
      return;
    var linkPos = getPosition(this.creatorElement);

    // リンクの上側に表示
    var top = linkPos.top - this.frame.offsetHeight - POPUP_VERTICAL_MARGIN;
    // 画面上部からはみ出すのなら下側に表示
    if(top < 0)
      top = linkPos.bottom + POPUP_VERTICAL_MARGIN;
    // 画面下部からもはみ出すのなら最上部に表示
    if(top + this.frame.offsetHeight > window.innerHeight)
      top = POPUP_VERTICAL_MARGIN;
    this.frame.style.top = top + 'px';

    var maxLeft = document.documentElement.clientWidth
      - POPUP_HORIZONTAL_MARGIN - this.frame.offsetWidth;
    // 左揃え
    var left = linkPos.left;
    // 右端がはみ出すなら中央揃えに
    if(left > maxLeft) {
      left = linkPos.left + linkPos.width / 2 - this.frame.offsetWidth / 2;
      // 中央揃えでも右端がはみ出すなら右揃え
      if(left > maxLeft) {
        left = linkPos.right - this.frame.offsetWidth;
        // インライン要素が折り返してるときなどはそれでもはみ出すので画面右にそろえる
        if(left > maxLeft)
          left = maxLeft;
      }
    }
    if(left < POPUP_HORIZONTAL_MARGIN)
      left = POPUP_HORIZONTAL_MARGIN;
    this.frame.style.left = left + 'px';
  }
};