Positionオブジェクト(1)

とうとう最後のオブジェクトです。ただ、ここの解読は苦労しそうな気がします。。。「http://www.imgsrc.co.jp/~kuriyama/prototype/prototype.js.html#Position」には仮ドキュメントしかなく、参考資料が少ないです。また、ちょっとテストしてみただけで、いくつか不具合が見つかったりしてるので、その辺の調査にも時間がかかりそうです。それから、個人的な事情により少しペースも遅くなると思います。

とりあえず、少しずつ進めていきます・・・。

【抜粋】一部省略
var Position = {
  // set to true if needed, warning: firefox performance problems
  // NOT neeeded for page scrolling, only if draggable contained in
  // scrollable elements
  includeScrollOffsets: false,
(省略)
  relativize: function(element) {
(省略)
  }
}

// Safari returns margins on body which is incorrect if the child is absolutely
// positioned.  For performance reasons, redefine Position.cumulativeOffset for
// KHTML/WebKit only.
if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
(省略)
}

要素の表示位置を扱うオブジェクトです。属するメソッドは、主に要素を引数として取ります。

includeScrollOffsetsプロパティ

【抜粋】
  // set to true if needed, warning: firefox performance problems
  // NOT neeeded for page scrolling, only if draggable contained in
  // scrollable elements
  includeScrollOffsets: false,

コメントのおおまかな意味は以下(たぶん^^;)。

「必要であればtrueに設定する。firefoxではパフォーマンスに注意。スクロール可能要素がドラッガブル要素を含んでいる場合のみ必要。ページスクロールだけなら必要ない。」

後述withinメソッドで、これがtrueなら、処理をwithinIncludingScrolloffsetsメソッドに任せる、という使われ方をしています。

withinメソッドとwithinIncludingScrolloffsetsメソッドは、どちらも引数の要素が指定の座標を含んでいるかを調べるためのものです。ただし、withinIncludingScrolloffsetsメソッドは、要素のスクロールを考慮して判定します。詳細は後述します。

includeScrollOffsetsプロパティがデフォルトでfalseになっているのは、通常はwithinメソッドを使い、スクロール量算出等の不必要な処理を行わせないためと思われます。

【例】
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
<title></title>
<style>
<!--
#draggable{
  border:solid blue 2px;
  width:200px;
  height:200px;
  margin:10px;
}
#scroll{
  border:solid blue 2px;
  overflow:scroll;
  position:relative;
  width:400px;
  height:400px;
}
#mark{
  background-color:red;
  width:20px;
  height:20px;
  position:absolute;
  top:200px;
  left:300px;
}

-->
</style>
<script language="javascript" src="prototype.js" charset="utf-8"></script>
<script>
<!--
//要素ドラッガブル化関数
//説明用に急造したのでいろいろとショボいです。
//ちゃんと実装するならscript.aculo.us等を使用しましょう^^;
//(prototype.jsに該当機能はありません。)
function makeDraggable(element){
  element = $(element);
  Element.makePositioned(element);
  Event.observe(element, 'mousedown', start, false);
  Event.observe(element, 'mousemove', move, false);
  Event.observe(element, 'mouseup', end, false);
  var isMoving = false;
  var dx = dy = 0;
  function start(event){
    event = event || window.event;
    var x = Element.getStyle(element, 'left');
    var y = Element.getStyle(element, 'top');
    x = (!x || x == 'auto') ? 0 : parseInt(x);
    y = (!y || y == 'auto') ? 0 : parseInt(y);
    dx = Event.pointerX(event) - x;
    dy = Event.pointerY(event) - y;
    isMoving = true;
  }
  function move(event){
    if(!isMoving) return;
    event = event || window.event;
    var x = parseInt(Event.pointerX(event) - dx);
    var y = parseInt(Event.pointerY(event) - dy);
    Element.setStyle(element, {left: x + "px", top : y + "px"});
    Event.stop(event);
  }
  function end(){
   isMoving = false;
  }
}
function test(){
  var mark = $('mark');
  Position.prepare();
  alert(Position.within($('draggable'), mark.offsetLeft, mark.offsetTop));
}
function change(obj){
  if(obj.checked){
    Position.includeScrollOffsets = true;
  }else{
    Position.includeScrollOffsets = false;
  }
}
function init(){
  makeDraggable('draggable');
  Event.observe('test', 'click', test, false);
  Position.includeScrollOffsets = true;
}
Event.observe(window, 'load', init, false);
//-->
</script>
</head>
<body>
<div id="scroll">
<div id="draggable">
DRAGGABLE
</div>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/>
</div>
<button id="test">TEST</button>
<input type="checkbox" onclick="change(this);" checked/>
Position.includeScrollOffsets
<div id="mark">&nbsp;</div>
</body>
</html>

スクロール可能要素がドラッガブル要素を含んでいる状態です。要素draggableはドラッグアンドドロップが可能です*1。TESTボタンは、要素draggableが要素markの左上座標を含んでいるかどうかを判定し、結果を表示します(要素markは絶対座標で配置しています)。

初期配置のままTESTボタンを押すと「false」が表示されます。要素draggableをドラッグして、要素mark(赤い四角)を含めてからTESTボタンを押すと「true」が表示されます。

ここで実験。要素draggableの下のほうに要素markを含めてください。このときTESTボタンを押すと「true」が表示されます。

それから、スクロール可能要素を下へスクロールさせて要素draggableから要素markを外してください。そこでTESTボタンを押すと「false」が表示されます。

次に、そのまま下のチェックボックスのチェックを外してください。このときincludeScrollOffsetsプロパティがtrueからfalseになります。そしてTESTボタンを押すと「true」が表示されます。

次に、要素draggableの上のほうに要素markを含めてください。そのままTESTボタンを押すと「false」が表示されると思います。それから、下のチェックボックスにチェックを入れるとincludeScrollOffsetsプロパティがfalseからtrueになります。そこでTESTボタンを押すと、今度は「true」が表示されます。

つまり、includeScrollOffsetsプロパティがfalseだと、withinメソッドで要素のスクロールは考慮されない、ということです。

枠線幅問題(2006/10/17追記)

Positionオブジェクトのメソッドは、要素に枠線幅があると、不具合を起こす場合があります。原因は、後述cumulativeOffsetやpositionedOffset、pageメソッドで、offsetParent要素の枠線幅が考慮されていないためです。以降のサンプルはこの問題を避けるため、枠線幅をつけていない場合があります。

ほとんどのブラウザで、要素のoffsetLeft、offsetTopプロパティは以下の値です。

  • 要素の枠線外側と、offsetParent要素の枠線内側との距離。

上記のメソッドにはoffsetLeft、offsetTopを累積する処理がありますが、このとき枠線幅は加算されません。このため、その分のずれが生じてしまいます。

ただし、Opera

  • 要素の枠線外側と、offsetParent要素の枠線外側との距離。

になるため、ずれは生じません。

2007/12/18追記。改修案を以下に記載しました。

Positionオブジェクトの枠線幅問題対策 - Backstage of theater.js

*1:本当はscript.aculo.usとかを使ったほうがいいのかもしれませんが、よく分かっていないのでパス^^;