ドラッグ&ドロップは優れたインターフェースソリューションです。何かを掴んでドラッグ&ドロップすることは、ドキュメントのコピーや移動(ファイルマネージャーのように)から、注文(カートへのアイテムのドロップ)まで、さまざまなことを行うための明確でシンプルな方法です。
現代の HTML 標準では、dragstart
, dragend
などの特別なイベントを含む ドラッグアンドドロップに関するセクションがあります。
これらのイベントにより、OS のファイルマネージャーからファイルをドラッグしてブラウザウィンドウにドロップするなどの、特別な種類のドラッグ&ドロップをサポートできます。その後、JavaScript はそのようなファイルの内容にアクセスできます。
ただし、ネイティブのドラッグイベントには制限もあります。例えば、特定の領域からのドラッグを防止することはできません。また、ドラッグを「水平」または「垂直」のみにすることもできません。また、それらを使用して実行できないドラッグ&ドロップタスクも多くあります。また、モバイルデバイスでのこのようなイベントのサポートは非常に弱いです。
したがって、ここではマウスイベントを使用してドラッグ&ドロップを実装する方法を見ていきます。
ドラッグ&ドロップアルゴリズム
基本的なドラッグ&ドロップアルゴリズムは次のようになります
mousedown
時 – 必要に応じて移動のための要素を準備します(クローンを作成したり、クラスを追加したりなど)。- 次に
mousemove
時、position:absolute
でleft/top
を変更して移動します。 mouseup
時 – ドラッグ&ドロップの完了に関連するすべてのアクションを実行します。
これらは基本です。後ほど、ドラッグ中に現在の下の要素を強調表示するなど、他の機能を追加する方法を見ていきます。
これがボールをドラッグする実装です。
ball.onmousedown = function(event) {
// (1) prepare to moving: make absolute and on top by z-index
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
// move it out of any current parents directly into body
// to make it positioned relative to the body
document.body.append(ball);
// centers the ball at (pageX, pageY) coordinates
function moveAt(pageX, pageY) {
ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
}
// move our absolutely positioned ball under the pointer
moveAt(event.pageX, event.pageY);
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
}
// (2) move the ball on mousemove
document.addEventListener('mousemove', onMouseMove);
// (3) drop the ball, remove unneeded handlers
ball.onmouseup = function() {
document.removeEventListener('mousemove', onMouseMove);
ball.onmouseup = null;
};
};
コードを実行すると、何か奇妙なことに気づくでしょう。ドラッグ&ドロップの開始時に、ボールが「フォーク」します。つまり、その「クローン」をドラッグし始めます。
以下が実際に動作する例です。
マウスでドラッグ&ドロップを試すと、このような動作が見られます。
これは、ブラウザが画像やその他の要素に対して独自のドラッグ&ドロップサポートを持っているためです。これは自動的に実行され、私たちが行った処理と競合します。
それを無効にするには
ball.ondragstart = function() {
return false;
};
これで全て問題ありません。
実際に動作する様子
もう1つの重要な側面は、ball
ではなく、document
で mousemove
を追跡することです。一見すると、マウスは常にボールの上にあるように見え、mousemove
をボールに配置できる可能性があります。
しかし、覚えているように、mousemove
は頻繁にトリガーされますが、すべてのピクセルに対してトリガーされるわけではありません。そのため、迅速に移動した後、ポインタはボールからドキュメントの中央(またはウィンドウの外)にジャンプする可能性があります。
そのため、それをキャッチするために document
でリッスンする必要があります。
正しい位置決め
上記の例では、ボールは常にその中心がポインタの下になるように移動します。
ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
悪くはありませんが、副作用があります。ドラッグ&ドロップを開始するには、ボールの任意の場所で mousedown
を行うことができます。しかし、端から「掴む」と、ボールが突然「ジャンプ」してマウスポインタの下の中心になります。
ポインタに対する要素の初期シフトを維持する方が良いでしょう。
たとえば、ボールの端をドラッグし始めた場合、ドラッグ中はポインタが端の上にある必要があります。
アルゴリズムを更新してみましょう。
-
訪問者がボタン(
mousedown
)を押すと、ポインタからボールの左上隅までの距離をshiftX/shiftY
変数に記憶します。ドラッグ中はその距離を維持します。これらのシフトを取得するには、座標を減算できます。
// onmousedown let shiftX = event.clientX - ball.getBoundingClientRect().left; let shiftY = event.clientY - ball.getBoundingClientRect().top;
-
次に、ドラッグ中に、ボールをポインタに対して同じシフトで配置します。このように
// onmousemove // ball has position:absolute ball.style.left = event.pageX - shiftX + 'px'; ball.style.top = event.pageY - shiftY + 'px';
より良い位置決めを備えた最終コード
ball.onmousedown = function(event) {
let shiftX = event.clientX - ball.getBoundingClientRect().left;
let shiftY = event.clientY - ball.getBoundingClientRect().top;
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
document.body.append(ball);
moveAt(event.pageX, event.pageY);
// moves the ball at (pageX, pageY) coordinates
// taking initial shifts into account
function moveAt(pageX, pageY) {
ball.style.left = pageX - shiftX + 'px';
ball.style.top = pageY - shiftY + 'px';
}
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
}
// move the ball on mousemove
document.addEventListener('mousemove', onMouseMove);
// drop the ball, remove unneeded handlers
ball.onmouseup = function() {
document.removeEventListener('mousemove', onMouseMove);
ball.onmouseup = null;
};
};
ball.ondragstart = function() {
return false;
};
実際に動作する様子 (<iframe>
内)
ボールを右下隅でドラッグすると、違いが特に顕著になります。前の例では、ボールがポインタの下に「ジャンプ」しました。今では、現在の位置からポインタを滑らかに追跡します。
潜在的なドロップターゲット(ドロッパブル)
前の例では、ボールは単に「どこにでも」ドロップして留めることができました。実際の生活では、通常、ある要素を別の要素にドロップします。たとえば、「ファイル」を「フォルダ」に入れるなどです。
抽象的に言えば、「ドラッグ可能」な要素を取得し、「ドロップ可能」な要素にドロップします。
知っておく必要があるのは
- ドラッグ&ドロップの最後に要素がドロップされた場所 – 対応するアクションを実行するために、
- また、できれば、ドラッグしているドロッパブルを知って、それをハイライトする必要があります。
解決策は少し興味深く、少しトリッキーなので、ここで説明します。
最初のアイデアは何でしょう?おそらく、潜在的なドロッパブルに mouseover/mouseup
ハンドラーを設定することでしょうか?
しかし、それはうまくいきません。
問題は、ドラッグしている間、ドラッグ可能な要素が常に他の要素の上にあることです。そして、マウスイベントは、下にある要素ではなく、一番上の要素でのみ発生します。
たとえば、以下は2つの <div>
要素で、赤い方が青い方の上にあります(完全にカバーしています)。赤い方が上にあるため、青い方でイベントをキャッチする方法はありません。
<style>
div {
width: 50px;
height: 50px;
position: absolute;
top: 0;
}
</style>
<div style="background:blue" onmouseover="alert('never works')"></div>
<div style="background:red" onmouseover="alert('over red!')"></div>
ドラッグ可能な要素も同様です。ボールは常に他の要素の上にあり、イベントはボール上で発生します。下位要素に設定したハンドラーは機能しません。
そのため、潜在的なドロッパブルにハンドラーを配置するという最初のアイデアは実際には機能しません。それらは実行されません。
では、どうすればいいのでしょうか?
document.elementFromPoint(clientX, clientY)
というメソッドがあります。これは、指定されたウィンドウ相対座標にある最もネストされた要素を返します(または、指定された座標がウィンドウ外にある場合は null
を返します)。同じ座標上に複数の重複する要素がある場合、一番上の要素が返されます。
それをマウスイベントハンドラーで使用して、ポインタの下にある潜在的なドロッパブルを検出できます。このように
// in a mouse event handler
ball.hidden = true; // (*) hide the element that we drag
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
// elemBelow is the element below the ball, may be droppable
ball.hidden = false;
注意してください: (*)
を呼び出す前に、ボールを非表示にする必要があります。そうしないと、これらの座標にある一番上の要素としてボールが常に表示されます: elemBelow=ball
。したがって、それを非表示にして、すぐに再度表示します。
そのコードを使用して、いつでも「上を飛んでいる」要素を確認できます。そして、それが起こったときにドロップを処理できます。
「ドロッパブル」要素を見つけるための onMouseMove
の拡張コード
// potential droppable that we're flying over right now
let currentDroppable = null;
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
ball.hidden = true;
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
ball.hidden = false;
// mousemove events may trigger out of the window (when the ball is dragged off-screen)
// if clientX/clientY are out of the window, then elementFromPoint returns null
if (!elemBelow) return;
// potential droppables are labeled with the class "droppable" (can be other logic)
let droppableBelow = elemBelow.closest('.droppable');
if (currentDroppable != droppableBelow) {
// we're flying in or out...
// note: both values can be null
// currentDroppable=null if we were not over a droppable before this event (e.g over an empty space)
// droppableBelow=null if we're not over a droppable now, during this event
if (currentDroppable) {
// the logic to process "flying out" of the droppable (remove highlight)
leaveDroppable(currentDroppable);
}
currentDroppable = droppableBelow;
if (currentDroppable) {
// the logic to process "flying in" of the droppable
enterDroppable(currentDroppable);
}
}
}
以下の例では、ボールがサッカーゴールの上をドラッグされると、ゴールがハイライトされます。
#gate {
cursor: pointer;
margin-bottom: 100px;
width: 83px;
height: 46px;
}
#ball {
cursor: pointer;
width: 40px;
height: 40px;
}
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<p>Drag the ball.</p>
<img src="https://en.js.cx/clipart/soccer-gate.svg" id="gate" class="droppable">
<img src="https://en.js.cx/clipart/ball.svg" id="ball">
<script>
let currentDroppable = null;
ball.onmousedown = function(event) {
let shiftX = event.clientX - ball.getBoundingClientRect().left;
let shiftY = event.clientY - ball.getBoundingClientRect().top;
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
document.body.append(ball);
moveAt(event.pageX, event.pageY);
function moveAt(pageX, pageY) {
ball.style.left = pageX - shiftX + 'px';
ball.style.top = pageY - shiftY + 'px';
}
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
ball.hidden = true;
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
ball.hidden = false;
if (!elemBelow) return;
let droppableBelow = elemBelow.closest('.droppable');
if (currentDroppable != droppableBelow) {
if (currentDroppable) { // null when we were not over a droppable before this event
leaveDroppable(currentDroppable);
}
currentDroppable = droppableBelow;
if (currentDroppable) { // null if we're not coming over a droppable now
// (maybe just left the droppable)
enterDroppable(currentDroppable);
}
}
}
document.addEventListener('mousemove', onMouseMove);
ball.onmouseup = function() {
document.removeEventListener('mousemove', onMouseMove);
ball.onmouseup = null;
};
};
function enterDroppable(elem) {
elem.style.background = 'pink';
}
function leaveDroppable(elem) {
elem.style.background = '';
}
ball.ondragstart = function() {
return false;
};
</script>
</body>
</html>
これで、プロセス全体で変数 currentDroppable
に現在の「ドロップターゲット」があり、「上を飛んでいる」要素を強調表示したり、その他のことを行うことができます。
まとめ
基本的なドラッグ&ドロップアルゴリズムについて検討しました。
主要なコンポーネント
- イベントフロー:
ball.mousedown
→document.mousemove
→ball.mouseup
(ネイティブのondragstart
をキャンセルすることを忘れないでください)。 - ドラッグ開始時 – ポインタの要素に対する初期シフトを覚えておいてください:
shiftX/shiftY
でドラッグ中にそれを維持します。 document.elementFromPoint
を使用して、ポインタの下にあるドロップ可能な要素を検出します。
この基礎の上に多くを築くことができます。
mouseup
で、ドロップをインテリジェントに完了できます。データの変更、要素の移動など。- 上を飛んでいる要素を強調表示できます。
- ドラッグを特定の領域または方向に制限できます。
mousedown/up
にイベント委任を使用できます。event.target
を確認する大きな領域のイベントハンドラーは、数百の要素のドラッグ&ドロップを管理できます。- などなど。
それをベースにアーキテクチャを構築するフレームワークがあります: DragZone
、Droppable
、Draggable
およびその他のクラス。それらのほとんどは、上記で説明したことと同様のことを行うため、今では理解しやすいはずです。または、独自に作成することもできます。サードパーティのソリューションを適応させるよりも簡単である場合があります。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグで囲み、10 行を超える場合はサンドボックス (plnkr、jsbin、codepen…) を使用してください。