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側の基準が枠線内側の場合、枠線幅を加算すればいいのですが、基準が枠線内側か外側かの条件は単純ではありません。特に、FireFox、Netscapeの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ボタンを押すと、IEやFirefox、Netscapeでは値が変わると思います。枠線は20pxを指定したので、+20です。prototype_extend.jsを読み込まないと、この値は変わりません。Operaでは、枠線があっても余計な加算はされません。
先に述べたように、TABLE要素内の要素に関しては対象外です。
不具合がありましたらご一報ください・・・。