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

FirefoxのBODY枠線問題に対応しました。

【関連】
Positionオブジェクト(1) - Backstage of theater.js
Positionオブジェクト(3) - Backstage of theater.js
offsetTop/offsetLeft/offsetParentの闇 - Backstage of theater.js

prototype.jsのPosition.cumulativeOffset、Position.positionedOffset、Position.pageメソッドは、対象要素の上位offsetParentに枠線幅があると、正しく値を取得できない場合があります。これは、要素のoffsetTop/LeftプロパティがoffsetParent側の基準を枠線内側においている場合があり、このためメソッド処理中の各offsetTop/Left累積時に枠線幅分が加算されないのが理由です。

対策としては、offsetParent側の基準が枠線内側の場合、枠線幅を加算すればいいのですが、基準が枠線内側か外側かの条件は単純ではありません。特に、FireFoxNetscapeのTABLE要素内の要素に関しては嫌になるほど複雑です。

・・・なので、TABLE要素に関してはあきらめることにしました^^;

それ以外の場合ですが、現在分かっているのは

なのですが、今後の変更への対応、また他の環境にも対応させるために、以下のように判定することにします。

  • 枠線幅を持つDIV要素A内に、DIV要素Bをひとつだけ作成。要素Aにはposition:absolute;を指定してoffsetParentの資格を与える。
  • 要素BのoffsetLeftをチェック。0なら基準は枠線内側。1以上(=要素Aの枠線幅)なら基準は枠線外側。

上記チェックで基準枠線内側の場合のみ、枠線幅を加算するようにします。

以下、改修案です。ここではファイル名を「prototype_extend.js」とし、prototype.jsの後に読み込んで上書きすることを想定しています。

【prototype_extend.js】

Object.extend(Position, {
  positionedOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;

      if(!this.isIncludeBorder){
        var border = this.getBorder(element.offsetParent);
        valueT += border[0];
        valueL += border[1];
        if(element.offsetParent == document.body
        && this.isMinusBodyBorder){
          valueT += border[0];
          valueL += border[1];
        }
      }

      element = element.offsetParent;
      if (element) {
        if(element.tagName=='BODY') break;
        var p = Element.getStyle(element, 'position');
        if (p == 'relative' || p == 'absolute') break;
      }
    } while (element);
    return [valueL, valueT];
  },

  page: function(forElement) {
    var valueT = 0, valueL = 0;

    var element = forElement;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;

      if(!this.isIncludeBorder){
        var border = this.getBorder(element.offsetParent);
        valueT += border[1];
        valueL += border[0];
        if(element.offsetParent == document.body
        && this.isMinusBodyBorder){
          valueT += border[0];
          valueL += border[1];
        }
      }
      
      // Safari fix
      if (element.offsetParent==document.body)
        if (Element.getStyle(element,'position')=='absolute') break;

    } while (element = element.offsetParent);

    element = forElement;
    do {
      valueT -= element.scrollTop  || 0;
      valueL -= element.scrollLeft || 0;
    } while (element = element.parentNode);

    return [valueL, valueT];
  },
  
  getBorder: function(element){
    if(element == null){
      return [0, 0];
    }
    var top , left, style;
    style = Element.getStyle(element, 'border-top-style');
    if(style != 'none'){
      top  = parseInt(Element.getStyle(element, 'border-top-width'));
    }
    style = Element.getStyle(element, 'border-left-style');
    if(style != 'none'){
      left = parseInt(Element.getStyle(element, 'border-left-width'));
    }
    top  = (isNaN(top))  ? 0 : top;
    left = (isNaN(left)) ? 0 : left;
    return [left, top];
  },
  
  setIsIncludeBorder: function(){
    var id = (new Date()).getTime();
    new Insertion.Bottom(document.body,
      "<div id='parent_" + id + "'" 
      + " style='border:solid blue 10px; padding:0px;"
      + " position:absolute; visibility:hidden;'>"
      + " <div id='child_" + id + "'></div></div>");
    
    if($('child_' + id).offsetTop == 0){
      this.isIncludeBorder = false;
    }else{
      this.isIncludeBorder = true;
    }
    Element.remove('parent_' + id);

    var top = document.body.offsetTop;
    var margin = parseInt(Element.getStyle(document.body, "margin-top"));
    if(isNaN(margin)) margin = 0;
    if(top < margin){
      this.isMinusBodyBorder = true;
    }else{
      this.isMinusBodyBorder = false;
    }
  }
});
Event.observe(window, 'load', Position.setIsIncludeBorder.bind(Position));

if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
  Position.cumulativeOffset = function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      
      if(!this.isIncludeBorder){
        var border = this.getBorder(element.offsetParent);
        valueT += border[1];
        valueL += border[0];
        if(element.offsetParent == document.body
        && this.isMinusBodyBorder){
          valueT += border[0];
          valueL += border[1];
        }
      }
      
      if (element.offsetParent == document.body)
        if (Element.getStyle(element, 'position') == 'absolute') break;

      element = element.offsetParent;
    } while (element);

    return [valueL, valueT];
  }
}else{
  Position.cumulativeOffset = function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      
      if(!this.isIncludeBorder){
        var border = this.getBorder(element.offsetParent);
        valueT += border[1];
        valueL += border[0];
        if(element.offsetParent == document.body
        && this.isMinusBodyBorder){
          valueT += border[0];
          valueL += border[1];
        }
      }
      element = element.offsetParent;
    } while (element);
    return [valueL, valueT];
  }
}

以下、例です。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 //EN">
<html>
<head>
<title></title>
<style>
#parent{
  left:200px;
  background-color:yellow;
  position:relative;
  width:200px;
  height:200px;
}
#mark{
  background-color:red;
  position:relative;
  top:100px;
  left:100px;
  width:20px;
  height:20px;
}
</style>
<script language="javascript" src="prototype.js" charset="utf-8"></script>
<script language="javascript" src="prototype_extend.js" charset="utf-8"></script>
<script>
function test(){
  var str = "Position.cumulativeOffset($('mark')) = "
          + Object.inspect(Position.cumulativeOffset($('mark'))) + "<br/>"
          + "$('mark').offsetLeft/Top = "
          + $('mark').offsetLeft + ", " +  $('mark').offsetTop;
  Element.update('view', str);
}
function switchBorder(elm){
  var style = (elm.checked) ? {'border':'solid blue 20px'} : {'border-style':'none'};
  Element.setStyle('parent', style);
}
</script>
</head>
<body>
<div id="parent">
<div id="mark">
MARK
</div>
</div>
<button id="test" onclick="test();">TEST</button>
<input type='checkbox' onclick='switchBorder(this);'/>
<div id="view"></div>
</body>
</html>

TESTボタンを押すと、要素MARKのPosition.cumulativeOffsetの結果を表示します。チェックボックスにチェックを入れると、親要素(黄色)に枠線(青)が付きます。それから再度TESTボタンを押すと、IEFirefoxNetscapeでは値が変わると思います。枠線は20pxを指定したので、+20です。prototype_extend.jsを読み込まないと、この値は変わりません。Operaでは、枠線があっても余計な加算はされません。

先に述べたように、TABLE要素内の要素に関しては対象外です。

不具合がありましたらご一報ください・・・。