1
0

JavaScript 第9回 ウィンドウをドラッグ&ドロップで移動

Last updated at Posted at 2024-01-25

はじめに

今回は、第8回で作成したモードレスウィンドウをドラッグ&ドロップで移動できるようにします。
モードレスウィンドウの作成については、第8回の記事を参照ください。
なお、もう少し改善の余地があることがわかりましたので、そのあたりは今後また記事にしたい と思います。

今回実施する内容

モードレスウィンドウをマウスによるドラッグ&ドロップで移動できるようにします。
dandd.gif

ソースコード(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) の外に移動したときに発行されます。

図にすると以下の通りです。
image.png

image.png

ウィンドウのドラッグ&ドロップ動作の実装動作

ウィンドウのドラッグ&ドロップイベントと実装タイミング

マウスイベントに記載した通り、ドラッグ&ドロップ用のマウスイベントは存在しません。
上記で記載したmousemoveは、 カーソルのホットスポットが要素内にあるときに、ポインティングデバイス (通常はマウス) が移動されると、その要素に発行されます から、ドラッグ中でなくても 、要素内にマウスポインターが入れば連続で発行されます。

したがって、マウスイベントからドラッグ&ドロップ動作を模擬する必要があります。

例えば、mousemoveは、ドラッグ&ドロップをしているときにだけ、すなわち、ドラッグしてからドロップするまでの間だけ、mousemoveが発行されるようにする必要があり、その間だけドラッグしている要素を移動するような動作が必要です。

また、画面外に移動したときのイベントmouseleaveは、mousemoveと同様にドラッグ&ドロップのウィンドウ移動の終了の契機にする必要があります。そうしないと、マウスポインターが画面外に出て、再度画面内に戻ってきた時に、ウィンドウの移動動作を実施してしまいます。

上記をふまえて、どの状態でどのマウスイベントが必要かを示します。

状態 必要なマウスイベント
ドラッグ前 mousedown
ドラッグ中 mousemovemouseleavemouseup
ドロップ後 mousedown

「ドラッグ前」と「ドロップ後」は状態としては同じですから、結局、2つの状態にわかれます。
これを実現するために、どのイベントをいつ、どの要素にEventListenerで実装するかを示します。

マウスイベント 要素 EventListenerタイミング 補足
mousedown ウィンドウ 常に実装 「ドラッグ中」でも悪影響がないため。
mouseup ウィンドウ 常に実装 「ドラッグ前後」でも悪影響がないため。
mousemove ウィンドウ 「ドラッグ」時に追加
「ドロップ」で削除
「ドラッグ中のみ動作させたいため。」
mouseleave body 「ドラッグ」時に追加
「ドロップ」で削除
「ドラッグ中のみ動作させたいため。」

図にまとめると以下の通りです。
image.png

登録する要素は、ウィンドウとbodyとしました。
mousedownmouseup、はmousemoveは移動する該当の要素で開始、終了が必要なためこのようにしました。
mouseleavebodyとしており、ブラウザ自身の画面外へ出たときに発生することを想定するためです。

ドラッグ、ウィンドウの座標取得と移動時の座標

ドラッグ&ドロップによるウィンドウを移動の動作は、以下の通りです。
1. ドラッグ時のマウスポインターの座標取得
2. ウィンドウの座標算出
3. マウスポインター移動(mousemoveイベント)検出時に、マウスポインターの座標取得
4. 移動すべきウィンドウの座標算出し、ウィンドウを移動
5. 3, 4繰り返し
6. ドロップ(mouseup`イベント)検出で、ウィンドウ移動終了

今回のウィンドウは、div要素で実現します。
div要素をドラッグ時のマウスポインターの座標取得、div要素の座標取得、および、div要素の座標設定で使用するプロパティは以下の通りです。

取得/設定 使用するプロパティ
ドラッグ時のマウスポインター座標取得 pageXpageYoffsetXoffsetY
div要素座標取得 pageXpageYoffsetXoffsetYから算出
div要素座標設定 topleft

ドラッグ時のマウスポインター座標、ウィンドウの座標取得

ドラッグ時のマウスポインターの座標は、pageXpageYoffsetXoffsetYで示されます。

MouseEvent: pageX プロパティには、以下の記載があります。

pageXMouseEvent インターフェイスの読み取り専用プロパティで、マウスがクリックされた位置の X(水平)座標を、文書全体の左端からの相対座標で返します。 これには文書の現在見えていない範囲にあるものも含みます。

MouseEvent: pageY プロパティには、以下の記載があります。

pageYMouseEvent インターフェイスの読み取り専用プロパティで、マウスがクリックされた位置の Y (垂直)座標を、文書全体の相対座標で返します。 このプロパティはページの垂直スクロールを加味します。

MouseEvent: offsetX プロパティには、以下の記載があります。

offsetXMouseEvent インターフェイスの読み取り専用プロパティで、マウスポインターの X 座標におけるこのイベントと対象ノードのパディング辺との間のオフセットを提供します。

MouseEvent: offsetY プロパティには、以下の記載があります。

offsetYMouseEvent インターフェイスの読み取り専用プロパティで、マウスポインターの Y 座標におけるこのイベントと対象ノードのパディング辺との間のオフセットを提供します。

上記を図にすると以下の通りです。
image.png

offsetX、および、offsetYの対象ノードとパディング辺については、説明を割愛します。

ウィンドウの左上の座標は、pageXpageYoffsetXoffsetYから算出します。

  • 左上のX軸座標:pageX - offsetX
  • 左上のY軸座標:pageY - offsetY

これを図にすると以下の通りです。
image.png

ウィンドウの移動

ウィンドウ移動時のtopleftの値は、以下のように算出します。

  • leftpageX - offsetX
  • toppageY - offsetY

これを図にすると以下の通りです。
image.png

ウィンドウの移動は、ドラッグ中mousemoveイベントが常に発生しますので、連続でtopleftを設定することで、ウィンドウを移動させることができます。
ドラッグ時、および、ドラッグで移動した時のmousemoveイベント発生時に着目すると、図の通りとなり、ドラッグ中はpageXpageYはマウスの移動で変化しますが、ウィンドウは一緒に移動させるため、offsetX、および、offsetYは変化しないことがわかると思います。
image.png

ということは、ドラッグ開始時に、pageXpageYoffsetX、および、offsetYを取得したら、ドラッグ中は、pageX、および、pageYだけ取得すれば、ウィンドウの移動座標を算出できることがわかります。

ウィンドウの座標設定にあたり、以下に注意する必要があります。

  • positionの設定値については、JavaScript(でもほぼCSS) 第7回 positionの動作で取り上げましたが、画面の左上からの位置にするならば、position: fixedが良いと思いますが、ウィンドウの親要素がbodyとなるのであれば、position: absoluteでもよいと思います。今回のサンプルでは、position: absoluteで試します。
  • JavaScript(でもほぼCSS) 第7回 positionの動作で取り上げましたが、position: fixedposition: absoluteを使用する場合、topbottomを設定しheightを設定しないと、heightが自動調整されますし、leftrightを設定しwidthを設定しないと、widthが自動調整されますので、注意ください。もし、そのような設定を行うとドラッグ中にウィンドウのサイズが自動調整によって変更され、ドラッグ&ドロップ動作になりません。

ウィンドウのドラッグ&ドロップ動作に加えて座標位置取得と移動を図にまとめると以下の通りです。
image.png

Boldしたところが前図からの更新箇所です。

ウィンドウをドラッグ&ドロップで移動する具体的な実装

ソース

modeless_drag.htmlのソースコード
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のソースコード
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_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回との違いは、#topDivwidthheighttop、および、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関数でoffsetXoffsetYを取得し、その後、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時にmousemovemouseleaveを削除します。

/**
 * 要素のオフセット(offsetX、offsetY)、およびその要素のIdを保存。
 * 
 * @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
 */
function getElemOffset(event) {
    elemOffsetX = event.offsetX;
    elemOffsetY = event.offsetY;
    elemId = event.target.id;
}

「ウィンドウの移動」やソース内のコメントに記載した通り、ドラッグ時にgetElemOffset関数でoffsetXoffsetYを取得します。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);
}

画面外に移動時にmousemovemouseleaveを削除します。

おわりに

今回は、モーダルウィンドウの流れで、ウィンドウをドラッグ&ドロップする操作を作りました。
試したところ、これでは不十分なことが色々とありましたので、次回以降でその辺をまとめていこうと思います。

1
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0