要素をドラッグアンドドロップする方法
はじめに
ドラッグアンドドロップが出来れば、Web の UI を簡素化し、かつ直感的に作り上げられる場合があります。ドラッグアンドドロップの実装方法はいろいろあるでしょうが、ここでは単にマウスのダウン、移 動、アップのイベントを上手に組み合わせることによって、ドラッグアンドドロップの実装を試みます。
1. 解説
この資料では、ドラッグする要素及びドロップするターゲット要素共に position スタイルを absolute にして、絶対位置指定しています。絶対位置指定ですから、要素の移動は簡単です。
HTML では次のように、ドラッグする要素、ドロップするターゲットのクラスをそれぞれ "dragged_elm"、"drop_target" とします。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <title>Drag and Drop Sample</title> <style type="text/css"> body { font-size: 10px; font-family: Verdana, Helvetica; } div.dragged_elm { position: absolute; z-index: 100; width: 50px; height: 50px; border: 1px solid black; background: rgb(195, 217, 255); text-align: center; color: #333; } div.drop_target { position: absolute; border: 1px solid black; background: rgb(46, 180, 87); text-align: center; color: #333; } </style> <script type="text/javascript" src="dnd.js"></script> </head> <body> <div did="A1" class="dragged_elm" id="d1" style="left:250px; top:10px; filter: alpha(opacity=100); opacity: 1;" > A1 </div> <div did="A2" class="dragged_elm" id="d2" style="left:350px; top:10px; filter: alpha(opacity=100); opacity: 1;" > A2 </div> <div tid="B1" class="drop_target" id="t1" style="left:100px; top:150px; width: 100px; height: 100px;" > Drop Here.<br />B1 </div> <div tid="B2" class="drop_target" id="t2" style="left:500px; top:150px; width: 100px; height: 100px;" > Drop Here!<br />B2 </div> </body> </html>
また要素が複数ある場合、ドラッグする要素の集合、ドロップする要素の集合、それぞれの集合で要素が識別できるようにするため、ドラッ グする要素、 ドロップする要素のそれぞれに "did" 、"tid" を割り当てました。この識別子名 (属性名) は任意ですが、今回のコードの中では識別子名をハードコードしてあるので、変更する場合には JavaScript のコードも変更してください。
1.1 ドラッグの開始
まず、要素が mousedown イベントを受け取れるように、ドラッグするオブジェクトに mousedown イベントをセットアップします。今回のやり方では、要素の class が 'dragged_elm'であるものがドラッグする要素ですから、次のようにして、ドキュメント中のすべての div 要素のなかから class が 'dragged_elm' であるものに onMouseDown イベントをセットします。
function init() { //.... var all_elms = document.getElementsByTagName('div'); for (var i = 0; i < all_elms.length; i++) { if (getAttrValue(all_elms[i], 'class') == 'dragged_elm') { addEvent(all_elms[i], 'mousedown', onMouseDown, false); } } }
マウスをクリックしたときに行うことは、主に二つ。「クリックしたときの情報の保存」 と 「イベントのセットアップ」 です。
「クリックしたときの情報」 はグローバル変数に格納しています。その情報とは、「ドラッグする要素」 = gDragged 、「ドラッグする要素の識別情報 'did'」 = gDraggedId 、「座標」 = gOrgX, gOrgY 及び 「クリックしたポイントとドラッグする要素の左上のポイントとの差分」 = gDeltaX, gDeltaY です。
function onMouseDown(evt) { var target = evt.target ? evt.target : evt.srcElement; var x = getEventX(evt); var y = getEventY(evt); // // クリックしたときの情報を保存 // gDragged = target; gDraggedId = getAttrValue(gDragged, 'did'); gOrgX = getX(gDragged); gOrgY = getY(gDragged); gDeltaX = x - getX(gDragged); gDeltaY = y - getY(gDragged); //... }
gDeltaX と gDeltaY はクリックしたポイント、すなわちイベントが発生したポイントと、要素の左上の座標との差分です。ドラッグしたときに発生するイベントの座標を使って、要 素を移動するわけですが、要素を移動するときに使う座標は要素の左上です。gDeltaX と gDeltaY はイベントの座標から、要素の左上座標を計算するために使います。
//... setOpacity(gDragged, 0.6); // クリックしたときに要素を半透明にする setCursor(gDragged, 'move'); // カーソルを十字矢印にセット addEvent(document, 'mousemove', onMouseMove, false); // mousemove イベントハンドラのセット addEvent(document, 'mouseup', onMouseUp, false); // mouseup イベントハンドラのセット }
ドラッグアンドドロップの実装ではこのように、マウスを押下したときに 'mousemove' と 'mouseup' のイベントをセットするのがポイントです。後で見ますが、マウスをアップしたときに、これらのイベントを削除 (remove) します。
1.2 移動
mousemove イベントに応じて要素を移動するだけです。このとき、移動先座標は (イベント発生座標) - (gDelta(X/Y)) であることに注意します。
function onMouseMove(evt) { var x = getEventX(evt); var y = getEventY(evt); moveElm(gDragged, x - gDeltaX, y - gDeltaY); }
ここで使用している、getEventX, getEventY 及び moveElm 関数の詳細は私が定義したものです。具体的には下記のコードを見てください。
1.3 ドラッグ完了とドロップの判定
マウスを離したときにドラッグは完了します。mouseup イベントで対処します。
function onMouseUp(evt) { setOpacity(gDragged, 1); // 要素を不透明にします。 setCursor(gDragged, 'default'); //マウスカーソルを元に戻します。 removeEvent(document, 'mousemove', onMouseMove, false); // イベントの削除 removeEvent(document, 'mouseup', onMouseUp, false); // イベントの削除 if (isOnDropTarget(evt)) { // ドロップした先の要素を判定し、「ドロップ可能要素」であれば... onDropped(evt); // 何かをする。 } else { moveElm(gDragged, gOrgX, gOrgY); // 意味の無い場所へのドロップであれば、要素を元の位置に戻す } }
今回ドロップの判定ルーチン isOnDropTarget は次のように実装しています。ドロップの座標がある要素の内部にあり、かつその要素が tid 属性を持っていればドロップ可能要素であるとしています。
function isOnDropTarget(evt) { var all_divs = document.getElementsByTagName('div'); for (var i = 0; i < all_divs.length; i++) { if (isEventOnElm(evt, all_divs[i].id)) { if (all_divs[i].attributes['tid']) { gDropId = getAttrValue(all_divs[i], 'tid'); return true; } } } return false; } function isEventOnElm(evt, drop_target_id) { var evtX = getEventX(evt); var evtY = getEventY(evt); var drop_target = document.getElementById(drop_target_id); var x = getX(drop_target); var y = getY(drop_target); var width = getWidth(drop_target); var height = getHeight(drop_target); return evtX > x && evtY > y && evtX < x + width && evtY < y + height; }
座標さえキチンととれれば何も面倒なところはありません。
2. リスト
以上、完全なリストは以下となります。
var gDragged; var gDeltaX, gDeltaY; var gOrgX, gOrgY; var gDraggedId; var gDropId; //////////////////////////////////////////////////////////////////////// function addEvent(elm, evtType, fn, useCapture) { if (elm.addEventListener) { elm.addEventListener(evtType, fn, useCapture); return true; } else if (elm.attachEvent) { var r = elm.attachEvent('on' + evtType, fn); return r; } else { elm['on' + evtType] = fn; } } //////////////////////////////////////////////////////////////////////// function removeEvent(elm, evtType, fn, useCapture) { if (elm.removeEventListener) { elm.removeEventListener(evtType, fn, useCapture); return true; } else if (elm.detachEvent) { var r = elm.detachEvent('on' + evtType, fn); return r; } else { elm['on' + evtType] = fn; } } //////////////////////////////////////////////////////////////////////// function setCursor(elm, curtype) { elm.style.cursor = curtype; } //////////////////////////////////////////////////////////////////////// function setOpacity(node, val) { if (node.filters) { try { node.filters['alpha'].opacity = val * 100; } catch (e) {} } else if (node.style.opacity) { node.style.opacity = val; } } //////////////////////////////////////////////////////////////////////// function getAttrValue(elm, attrname) { return elm.attributes[attrname].nodeValue; } //////////////////////////////////////////////////////////////////////// function getX(elm) { return parseInt(elm.style.left); } //////////////////////////////////////////////////////////////////////// function getY(elm) { return parseInt(elm.style.top); } //////////////////////////////////////////////////////////////////////// function getEventX(evt) { return evt.pageX ? evt.pageX : evt.clientX; } //////////////////////////////////////////////////////////////////////// function getEventY(evt) { return evt.pageY ? evt.pageY : evt.clientY; } //////////////////////////////////////////////////////////////////////// function getWidth(elm) { return parseInt(elm.style.width); } //////////////////////////////////////////////////////////////////////// function getHeight(elm) { return parseInt(elm.style.height); } //////////////////////////////////////////////////////////////////////// function moveElm(elm, x, y) { elm.style.left = x + 'px'; elm.style.top = y + 'px'; } //////////////////////////////////////////////////////////////////////// function onMouseDown(evt) { var target = evt.target ? evt.target : evt.srcElement; var x = getEventX(evt); var y = getEventY(evt); // // Save Information to Globals // gDragged = target; gDeltaX = x - getX(gDragged); gDeltaY = y - getY(gDragged); gDraggedId = getAttrValue(gDragged, 'did'); setCursor(gDragged, 'move'); gOrgX = getX(gDragged); gOrgY = getY(gDragged); // // Set // setOpacity(gDragged, 0.6); addEvent(document, 'mousemove', onMouseMove, false); addEvent(document, 'mouseup', onMouseUp, false); } //////////////////////////////////////////////////////////////////////// function onMouseMove(evt) { var x = getEventX(evt); var y = getEventY(evt); moveElm(gDragged, x - gDeltaX, y - gDeltaY); } //////////////////////////////////////////////////////////////////////// function onMouseUp(evt) { setOpacity(gDragged, 1); setCursor(gDragged, 'default'); removeEvent(document, 'mousemove', onMouseMove, false); removeEvent(document, 'mouseup', onMouseUp, false); if (isOnDropTarget(evt)) { onDropped(evt); } else { moveElm(gDragged, gOrgX, gOrgY); } } //////////////////////////////////////////////////////////////////////// function isOnDropTarget(evt) { var all_divs = document.getElementsByTagName('div'); for (var i = 0; i < all_divs.length; i++) { if (isEventOnElm(evt, all_divs[i].id)) { if (all_divs[i].attributes['tid']) { gDropId = getAttrValue(all_divs[i], 'tid'); return true; } } } return false; } //////////////////////////////////////////////////////////////////////// function isEventOnElm(evt, drop_target_id) { var evtX = getEventX(evt); var evtY = getEventY(evt); var drop_target = document.getElementById(drop_target_id); var x = getX(drop_target); var y = getY(drop_target); var width = getWidth(drop_target); var height = getHeight(drop_target); return evtX > x && evtY > y && evtX < x + width && evtY < y + height; } //////////////////////////////////////////////////////////////////////// function onDropped(evt) { alert('Dropped! Dragged: [' + gDraggedId + '] Dropped at: [' + gDropId + ']'); } //////////////////////////////////////////////////////////////////////// function init() { document.body.ondrag = function() { return false; }; document.body.onselectstart = function() { return false; }; // // Assign Event Handlers // var all_elms = document.getElementsByTagName('div'); for (var i = 0; i < all_elms.length; i++) { if (getAttrValue(all_elms[i], 'class') == 'dragged_elm') { addEvent(all_elms[i], 'mousedown', onMouseDown, false); } } } addEvent(window, 'load', init, false);