これでできる! クロスブラウザJavaScript入門

第22回 JavaScriptによるUIの実装:ドラッグ

この記事を読むのに必要な時間:およそ 5.5 分

改良版ドラッグのJavaScript

var IE6 = !window.XMLHttpRequest && window.ActiveXObject;
var element = document.getElementById('js-drag-2');
var rect = element.getBoundingClientRect();
var root = document.documentElement;
document.body.appendChild(element);
var left = window.pageXOffset || root.scrollLeft;
var top  = window.pageYOffset || root.scrollTop;
element.style.left = (rect.left + left) + 'px';
element.style.top  = (rect.top  + top ) + 'px';

var dragging = false;
var x, y; // 相対位置を保存しておく変数
element.onmousedown = function(evt){
  if(!evt){
    evt = window.event;
  }
  var target = evt.target || evt.srcElement;
  if (target === element) {
    x = evt.offsetX || evt.layerX;//相対位置
    y = evt.offsetY || evt.layerY;
    dragging = true;
    // ドラッグ中はabsoluteに
    var rect = element.getBoundingClientRect();
    var left = window.pageXOffset || root.scrollLeft;
    var top  = window.pageYOffset || root.scrollTop;
    element.style.left = left + rect.left + 'px';
    element.style.top  = top  + rect.top  + 'px';
    element.style.position = 'absolute';
    if (IE6){
      element.style.removeExpression('behavior');
    }
    return false;
  }
};
document.onmouseup = function(evt){
  dragging = false;
  // ドラッグが終わったらfixedに
  var rect = element.getBoundingClientRect();
  element.style.left = rect.left + 'px';
  element.style.top  = rect.top  + 'px';
  element.style.position = 'fixed';
  if (IE6) {
    element.style.position = 'absolute';
    var L = 'document.documentElement.scrollLeft';
    var T = 'document.documentElement.scrollTop';
    element.style.setExpression('behavior', 
       'this.style.left=('+rect.left+'+'+L+')+"px",'+
       'this.style.top =('+rect.top +'+'+T+')+"px"');
    document.body.style.backgroundImage='url(about:blank)';
    document.body.style.backgroundAttachment = 'fixed';
  }
};
document.onmousemove = function(evt){
  if(!evt){
    evt = window.event;
  }
  if(dragging){
    var left = window.pageXOffset || root.scrollLeft;
    var top  = window.pageYOffset || root.scrollTop;
    element.style.left = left + evt.clientX - x + 'px';
    element.style.top  = top  + evt.clientY - y + 'px';
    return false;
  }
};

コードはやや複雑になりましたが,第20回のfixed対応,第19回で取り上げたスクロール量の取得に,getBoundingClientRectなど,これまで取り上げた内容ばかりで特に新しいトピックはありません。

さて,ここまでのコードではひとつの要素に決め打ちでドラッグしていたので,もう少し汎用的に使えるように考えてみます。

まず,document.onmouseup,document.onmousemoveなどはほかのスクリプトから上書きされてしまう可能性が高く,逆にほかのスクリプトを上書きしてしまう可能性もあります。こういった場合は,やや久しぶりの登場となるaddEvent関数の出番ですね。

また,mousemoveなどはイベントが大量に発生するので,監視は最小限に抑えたいところです。実際,mousemoveを監視するのはドラッグを開始してから終了するまでの間だけでよいので,mousedownで監視を始め,mouseupで監視を解除するようにしましょう。

イベントを解除するためにはイベントリスナーとして登録する関数を参照可能な状態にする必要があります。具体的には,次のようなコードになります。

イベントの登録と解除

var addEvent = document.addEventListener ?
  function(node,type,listener){
    node.addEventListener(type,listener,false);
  } :
  function(node,type,listener){
    node.attachEvent('on'+type, listener);
  }

var removeEvent = document.removeEventListener ?
  function(node,type,listener){
    node.removeEventListener(type,listener,false);
  } :
  function(node,type,listener){
    node.detachEvent('on'+type, listener);
  }

addEvent(document,'mousedown', function(evt){
  addEvent(document, 'mousemove', mousemove);
});
addEvent(document, 'mouseup', function(evt){
  removeEvent(document, 'mousemove', mousemove);
});
function mousemove(evt){
}

イベントを登録する際にはmousemoveという関数がそのまま登録され,解除する際もやはりmousemoveに対して解除を行います。

さてもう一つ,今まではドラッグする要素をbodyの直下に移動することで位置の計算を単純にしていました。今回はそのままの位置で移動させてみます。ドラッグする要素の親・祖先要素がposition:relativeなどのスタイルを持つ場合,その位置からの絶対位置になるため,計算がかなり複雑になってしまいます。そこで,getBoundingClientRectで絶対値を求め,offsetLeft/offsetTopとの差を取ることで簡単に位置関係を求めることができます。

例えば,100px,100pxの位置にposition:relativeな要素Aがあり,その子孫要素Bはさらに200px,200pxの位置に存在するとして,⁠スクロールしていない状態で)getBoundingClientRectはleft:300,top:300を返します。その時BはoffsetLeft:200,offsetTop:200となります。要素Bを絶対値で動かすには,100pxの差を加味する必要があります。

最後に,position:fixedの場合は常にページ基点からの配置になるので,上記の差は消えてくれます。つまり,fixedのときは単純に画面左上からの相対位置で動かすだけでよいということです(直感的にもfixedのときのドラッグが簡単であることは予想できると思います)⁠

著者プロフィール

太田昌吾(おおたしょうご,ハンドルネーム:os0x)

1983年生まれ。JavaScriptをメインに,HTML/CSSにFlashなどのクライアントサイドを得意とするウェブエンジニア。2009年12月より、Google Chrome ExtensionsのAPI Expertとして活動を開始。

URLhttp://d.hatena.ne.jp/os0x/