はじめに
今回は、第8回で作成したモードレスウィンドウをドラッグ&ドロップで移動できるようにします。
モードレスウィンドウの作成については、第8回の記事を参照ください。
なお、もう少し改善の余地があることがわかりましたので、そのあたりは今後また記事にしたい と思います。
今回実施する内容
モードレスウィンドウをマウスによるドラッグ&ドロップで移動できるようにします。
ソースコード(Git Hub)
環境
OS: Windows 11 JP (64bit)
Microsoft Edge:バージョン 120.0.2210.91 (公式ビルド) (64 ビット)
参考
mousedown
mousemove
mouseup
mouseleave
MouseEvent: pageX プロパティ
MouseEvent: pageY プロパティ
MouseEvent: offsetX プロパティ
MouseEvent: offsetY プロパティ
用語
なし
ウィンドウのドラッグ&ドロップ動作
ウィンドウのマウスによるドラッグ&ドロップの動作は、主に以下の3つです。
- ドラッグでウィンドウ移動開始
- ドラッグ中ウィンドウの移動
- ドロップでウィンドウ移動終了
これに加えて、
- マウスポインタがブラウザーの画面外に移動でウィンドウ移動終了
を考慮します。
ドラッグやドロップ動作は、マウスイベントをEventListener
として実装することで検知することができます。
マウスイベントが検知されたタイミングで、ウィンドウの移動を開始、終了することで、ウィンドウのドラッグ&ドロップを実現できます。
対応するマウスイベントは、以下の通りです。
マウス動作 | マウスイベント | ウィンドウ動作 |
---|---|---|
ドラッグ | mousedown | 移動開始 |
ドラッグ中 | mousemove | 移動 |
ドロップ | mouseup | 移動終了 |
画面外 | mouseleave | 移動終了 |
マウスイベントの概要については、Element配下に記載があります。
マウスイベント | 概要 |
---|---|
mousedown |
mousedown イベントは、ポインターが要素の中にあるときにポインティングデバイスのボタンが押下されたとき、その要素 (Element ) に発行されます。 |
mousemove |
mousemove イベントは、カーソルのホットスポットが要素内にあるときに、ポインティングデバイス (通常はマウス) が移動されると、その要素に発行されます。 |
mouseup |
mouseup イベントは、ポインターが要素の中にあるときに、ポインティングデバイス (マウスやトラックパッドなど) のボタンが離されるとその要素 (Element ) に発行されます。 |
mouseleave |
mouseleave イベントは、ポインティングデバイス(ふつうはマウス)のカーソルが要素 (Element ) の外に移動したときに発行されます。 |
ウィンドウのドラッグ&ドロップ動作の実装動作
ウィンドウのドラッグ&ドロップイベントと実装タイミング
マウスイベントに記載した通り、ドラッグ&ドロップ用のマウスイベントは存在しません。
上記で記載したmousemove
は、 カーソルのホットスポットが要素内にあるときに、ポインティングデバイス (通常はマウス) が移動されると、その要素に発行されます から、ドラッグ中でなくても 、要素内にマウスポインターが入れば連続で発行されます。
したがって、マウスイベントからドラッグ&ドロップ動作を模擬する必要があります。
例えば、mousemove
は、ドラッグ&ドロップをしているときにだけ、すなわち、ドラッグしてからドロップするまでの間だけ、mousemove
が発行されるようにする必要があり、その間だけドラッグしている要素を移動するような動作が必要です。
また、画面外に移動したときのイベントmouseleave
は、mousemove
と同様にドラッグ&ドロップのウィンドウ移動の終了の契機にする必要があります。そうしないと、マウスポインターが画面外に出て、再度画面内に戻ってきた時に、ウィンドウの移動動作を実施してしまいます。
上記をふまえて、どの状態でどのマウスイベントが必要かを示します。
状態 | 必要なマウスイベント |
---|---|
ドラッグ前 | mousedown |
ドラッグ中 |
mousemove 、mouseleave 、mouseup
|
ドロップ後 | mousedown |
「ドラッグ前」と「ドロップ後」は状態としては同じですから、結局、2つの状態にわかれます。
これを実現するために、どのイベントをいつ、どの要素にEventListener
で実装するかを示します。
マウスイベント | 要素 |
EventListener タイミング |
補足 |
---|---|---|---|
mousedown |
ウィンドウ | 常に実装 | 「ドラッグ中」でも悪影響がないため。 |
mouseup |
ウィンドウ | 常に実装 | 「ドラッグ前後」でも悪影響がないため。 |
mousemove |
ウィンドウ | 「ドラッグ」時に追加 「ドロップ」で削除 |
「ドラッグ中のみ動作させたいため。」 |
mouseleave |
body |
「ドラッグ」時に追加 「ドロップ」で削除 |
「ドラッグ中のみ動作させたいため。」 |
登録する要素は、ウィンドウとbody
としました。
mousedown
、mouseup
、はmousemove
は移動する該当の要素で開始、終了が必要なためこのようにしました。
mouseleave
はbody
としており、ブラウザ自身の画面外へ出たときに発生することを想定するためです。
ドラッグ、ウィンドウの座標取得と移動時の座標
ドラッグ&ドロップによるウィンドウを移動の動作は、以下の通りです。
1. ドラッグ時のマウスポインターの座標取得
2. ウィンドウの座標算出
3. マウスポインター移動(mousemove
イベント)検出時に、マウスポインターの座標取得
4. 移動すべきウィンドウの座標算出し、ウィンドウを移動
5. 3, 4繰り返し
6. ドロップ(mouseup`イベント)検出で、ウィンドウ移動終了
今回のウィンドウは、div
要素で実現します。
div
要素をドラッグ時のマウスポインターの座標取得、div
要素の座標取得、および、div
要素の座標設定で使用するプロパティは以下の通りです。
取得/設定 | 使用するプロパティ |
---|---|
ドラッグ時のマウスポインター座標取得 |
pageX 、pageY 、offsetX 、offsetY
|
div 要素座標取得 |
pageX 、pageY 、offsetX 、offsetY から算出 |
div 要素座標設定 |
top 、left
|
ドラッグ時のマウスポインター座標、ウィンドウの座標取得
ドラッグ時のマウスポインターの座標は、pageX
、pageY
、offsetX
、offsetY
で示されます。
MouseEvent: pageX プロパティには、以下の記載があります。
pageX
はMouseEvent
インターフェイスの読み取り専用プロパティで、マウスがクリックされた位置の X(水平)座標を、文書全体の左端からの相対座標で返します。 これには文書の現在見えていない範囲にあるものも含みます。
MouseEvent: pageY プロパティには、以下の記載があります。
pageY
はMouseEvent
インターフェイスの読み取り専用プロパティで、マウスがクリックされた位置の Y (垂直)座標を、文書全体の相対座標で返します。 このプロパティはページの垂直スクロールを加味します。
MouseEvent: offsetX プロパティには、以下の記載があります。
offsetX
はMouseEvent
インターフェイスの読み取り専用プロパティで、マウスポインターの X 座標におけるこのイベントと対象ノードのパディング辺との間のオフセットを提供します。
MouseEvent: offsetY プロパティには、以下の記載があります。
offsetY
はMouseEvent
インターフェイスの読み取り専用プロパティで、マウスポインターの Y 座標におけるこのイベントと対象ノードのパディング辺との間のオフセットを提供します。
offsetX
、および、offsetY
の対象ノードとパディング辺については、説明を割愛します。
ウィンドウの左上の座標は、pageX
、pageY
、offsetX
、offsetY
から算出します。
- 左上のX軸座標:
pageX
-offsetX
- 左上のY軸座標:
pageY
-offsetY
ウィンドウの移動
ウィンドウ移動時のtop
、left
の値は、以下のように算出します。
-
left:
pageX
-offsetX
-
top:
pageY
-offsetY
ウィンドウの移動は、ドラッグ中mousemove
イベントが常に発生しますので、連続でtop
、left
を設定することで、ウィンドウを移動させることができます。
ドラッグ時、および、ドラッグで移動した時のmousemove
イベント発生時に着目すると、図の通りとなり、ドラッグ中はpageX
とpageY
はマウスの移動で変化しますが、ウィンドウは一緒に移動させるため、offsetX
、および、offsetY
は変化しないことがわかると思います。
ということは、ドラッグ開始時に、pageX
、pageY
、offsetX
、および、offsetY
を取得したら、ドラッグ中は、pageX
、および、pageY
だけ取得すれば、ウィンドウの移動座標を算出できることがわかります。
ウィンドウの座標設定にあたり、以下に注意する必要があります。
-
position
の設定値については、JavaScript(でもほぼCSS) 第7回 positionの動作で取り上げましたが、画面の左上からの位置にするならば、position: fixed
が良いと思いますが、ウィンドウの親要素がbody
となるのであれば、position: absolute
でもよいと思います。今回のサンプルでは、position: absolute
で試します。 -
JavaScript(でもほぼCSS) 第7回 positionの動作で取り上げましたが、
position: fixed
やposition: absolute
を使用する場合、top
とbottom
を設定しheight
を設定しないと、height
が自動調整されますし、left
とright
を設定しwidth
を設定しないと、width
が自動調整されますので、注意ください。もし、そのような設定を行うとドラッグ中にウィンドウのサイズが自動調整によって変更され、ドラッグ&ドロップ動作になりません。
ウィンドウのドラッグ&ドロップ動作に加えて座標位置取得と移動を図にまとめると以下の通りです。
Boldしたところが前図からの更新箇所です。
ウィンドウをドラッグ&ドロップで移動する具体的な実装
ソース
modeless_drag.htmlのソースコード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Position</title>
<link rel="stylesheet" href="modeless_drag.css" type="text/css">
<script src="modeless_drag.js" defer></script>
</head>
<body>
<div>- 画面表示 -</div>
<div id="showTop">画面上部表示</div>
<div id="topDiv">
<div class="closeCircle" id="closeTop">×</div>
画面の上に表示されます。
</div>
</body>
</html>
modeless_drag.cssのソースコード
.closeCircle {
width: 18px;
height: 18px;
border-radius: 50%;
font-size: 16px;
line-height: 18px;
text-align: center;
position: absolute;
right: 3px;
top: 2px;
}
#topDiv .closeCircle {
background-color: orange;
}
#topDiv {
visibility: hidden;
background-color:yellow;
position: absolute;
width: 300px;
height: 100px;
top: 0px;
left: 20%;
}
modeless_drag.jsのソースコード
/** modeless用の設定 */
document.getElementById("showTop").addEventListener("click", () => {
showWindow("topDiv");
});
document.getElementById("closeTop").addEventListener("click", () => {
closeWindow("topDiv");
});
function showWindow(divId) {
document.getElementById(divId).style.visibility = "visible";
}
function closeWindow(divId) {
document.getElementById(divId).style.visibility = "hidden";
}
/** ドラッグ&ドロップで移動する設定*/
var elemId; //ドラッグした要素のId
var elemOffsetX; //ドラッグした要素のoffsetX
var elemOffsetY; //ドラッグした要素のoffsetY
//"topDiv"にマウスダウン、マウスアップ時のEventLisnterを追加。
document.getElementById("topDiv").addEventListener("mousedown", funcMouseDown);
document.getElementById("topDiv").addEventListener("mouseup", funcMouseUp);
/**
* マウスで要素をマウスダウンした時に、
* - その座標の保存。
* - ドラッグによる移動時のEventListnerを追加。
* - ドラッグしながらマウスポインタが画面外移動時のEventListenerを追加。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function funcMouseDown(event) {
getElemOffset(event);
document.getElementById(elemId).addEventListener("mousemove", dragWindow);
document.body.addEventListener("mouseleave", cancelDragWindow);
}
/**
* マウスで要素をマウスアップした時に、
* - ドラッグによる移動時のEventListenerを削除。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function funcMouseUp(event) {
document.getElementById(elemId).removeEventListener("mousemove", dragWindow);
document.body.removeEventListener("mouseleave", cancelDragWindow);
}
/**
* 要素のオフセット(offsetX、offsetY)、およびその要素のIdを保存。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function getElemOffset(event) {
elemOffsetX = event.offsetX;
elemOffsetY = event.offsetY;
elemId = event.target.id;
}
/**
* マウスで要素をドラッグ時に、ドラッグした要素を移動。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function dragWindow(event) {
document.getElementById(elemId).style.left = event.pageX - elemOffsetX + "px";
document.getElementById(elemId).style.top = event.pageY - elemOffsetY + "px";
}
/**
* bodyからマウス移動時のdragWindowのEventListenerを削除。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function cancelDragWindow(event) {
document.getElementById(elemId).removeEventListener("mousemove", dragWindow);
document.body.removeEventListener("mouseleave", cancelDragWindow);
}
modeless_drag.htmlの説明
本ソースは、第8回の記事で説明しています。
modeless_drag.htmlの説明
HTMLは、「画面上部表示」のdiv
タグ作成と、その画面上部用のdiv
タグ作成を行います。
「画面上部表示」は、id="showTop"
でdivタグを作成します。 これを押下すると、画面上部のモードレスウィンドウを開くようにしますが、それはJavaScriptで設定します。
id="topDiv"の
div`タグは、画面上部に表示するモードレスウィンドウの本体です。
この中には、
- モードレスウィンドウをクローズするxボタン(クローズボタン)。
- 「画面の上に表示されます。」の文字列。
が含まれます。
クローズボタンは、<div class="closeCircle" id="closeTop">×</div>
でdiv
タグで作成します。
class="closeCircle"は、クローズボタンを作成するためのクラスです。
id="closeTop"`は、クローズボタンを押下時にモードレスウィンドウを非表示に切り替えるためのJavaScriptが必要ですので、関連付けるためのidとして設定します。
ボタンとなる文字は、「×」を記載しています。フォントは特に指定していませんが、ユーザー環境によっては「x」に見えない可能性もありますが、簡単のためこのようにします。せんが、ユーザー環境によっては「x」に見えない可能性もありますが、簡単のためこのようにします。
modeless_drag.cssの説明
本ソースは、ほとんど第8回の記事で説明しています。
第8回との違いは、#topDiv
でwidth
、height
、top
、および、left
を設定していることくらいです。
これを設定した理由は、「ウィンドウの移動」のところで説明したとおり、ウィンドウのサイズを確定するためです。
modeless_drag.jsの説明
/** modeless用の設定 */
の部分は、第8回の記事で説明していますので、/** ドラッグ&ドロップで移動する設定*/
から説明します。
var elemId; //ドラッグした要素のId
var elemOffsetX; //ドラッグした要素のoffsetX
var elemOffsetY; //ドラッグした要素のoffsetY
elemId
は、ドラッグした要素のId
を保持する変数です。複数の要素が存在する場合にどの要素をドラッグ&ドロップするかを設定できるようにします。
elemOffsetX
、および、elemOffsetY
はすでに説明したウィンドウの座標を算出するために使用します。
//"topDiv"にマウスダウン、マウスアップ時のEventLisnterを追加。
document.getElementById("topDiv").addEventListener("mousedown", funcMouseDown);
document.getElementById("topDiv").addEventListener("mouseup", funcMouseUp);
mousedown
、および、mouseup
は常に実装するためtopDiv
に付与します。それぞれ、funcMouseDown
、および、funcMouseUp
のコールバック関数を実行します。
/**
* マウスで要素をマウスダウンした時に、
* - その座標の保存。
* - ドラッグによる移動時のEventListnerを追加。
* - ドラッグしながらマウスポインタが画面外移動時のEventListenerを追加。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function funcMouseDown(event) {
getElemOffset(event);
document.getElementById(elemId).addEventListener("mousemove", dragWindow);
document.body.addEventListener("mouseleave", cancelDragWindow);
}
「ウィンドウの移動」やソース内のコメントに記載した通り、ドラッグ時にgetElemOffset
関数でoffsetX
、offsetY
を取得し、その後、mousemove
、および、mouseleave
を実装します。
mousemove
、および、mouseleave
は、dragWindow
、および、cancelDragWindow
のコールバック関数を実行します。
/**
* マウスで要素をマウスアップした時に、
* - ドラッグによる移動時のEventListenerを削除。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function funcMouseUp(event) {
document.getElementById(elemId).removeEventListener("mousemove", dragWindow);
document.body.removeEventListener("mouseleave", cancelDragWindow);
}
mouseup
時にmousemove
とmouseleave
を削除します。
/**
* 要素のオフセット(offsetX、offsetY)、およびその要素のIdを保存。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function getElemOffset(event) {
elemOffsetX = event.offsetX;
elemOffsetY = event.offsetY;
elemId = event.target.id;
}
「ウィンドウの移動」やソース内のコメントに記載した通り、ドラッグ時にgetElemOffset
関数でoffsetX
、offsetY
を取得します。elemId
は複数のウィンドウのドラッグ&ドロップを実現できるようにドラッグをした対象の要素のIdを保持します。
/**
* マウスで要素をドラッグ時に、ドラッグした要素を移動。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function dragWindow(event) {
document.getElementById(elemId).style.left = event.pageX - elemOffsetX + "px";
document.getElementById(elemId).style.top = event.pageY - elemOffsetY + "px";
}
すでに説明した通り、ウィンドウを移動する動作です。
/**
* bodyからマウス移動時のdragWindowのEventListenerを削除。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function cancelDragWindow(event) {
document.getElementById(elemId).removeEventListener("mousemove", dragWindow);
document.body.removeEventListener("mouseleave", cancelDragWindow);
}
画面外に移動時にmousemove
とmouseleave
を削除します。
おわりに
今回は、モーダルウィンドウの流れで、ウィンドウをドラッグ&ドロップする操作を作りました。
試したところ、これでは不十分なことが色々とありましたので、次回以降でその辺をまとめていこうと思います。