はじめに
第9回、10回と作成したウィンドウをドラッグ&ドロップのソースについて、もう少し改善の余地があり、また、注意することもあるなと思いましたので、まとめます。
なお、第10回のソースをベースとしますので、第9回、第10回を見ていることを前提として記載します。
今回実施する内容
ソースコード(Git Hub)
環境
OS: Windows 11 JP (64bit)
Microsoft Edge:バージョン 121.0.2277.98 (公式ビルド) (64 ビット)
参考
用語
課題
第9回、10回と作成したウィンドウをドラッグ&ドロップを速く実施したらどうなるか?特にpointermoveの部分がどうなるのか?というのが疑問でした。
要するにpointmoveのイベントの発生とJavaScriptによる画面処理では処理が間に合うのか?ということです。

実験してみると
- 要素の中心に近いところで、速くポインターを動かしても問題なさそう
-
要素の端で、速くポインターを動かすとウィンドウの移動が止まってしまう
という状況でした。
pointmoveのイベント発生の解像度がどのくらいなのか?と気になって調べてみましたが、具体的なものはよくわかりませんでした。
課題の原因
課題となった2つの動作の要因を考えてみると、
-
pointermoveとそれによる要素の移動が追い付かず、ポインターが要素の外に出てしまう
ためだと思いました。
そのため、 -
pointermoveイベントを画面全体のbodyに付与すればよいのでは?
と考えました。
でも、実はもうひとつ落とし穴があって、
-
pointerleaveイベントの発生によってpointermove処理を止めてしまう
というのもありました。
pointerleaveはbodyに対してEventListenerを追加していました。body=ブラウザ画面内ではなく、ブラウザ画面の下部はbodyの外の可能性があるということでした。
というのもありました。
ということで、動作を確認していきます。
bodyの範囲調査
順番が変わってしまいますが、先にbodyの範囲を見ていきます。
結論から言えば、デフォルトでは
- width:ブラウザの画面幅
- height:ソースで記載された内容の最下位の位置
のようです。
もうひとつ注意が必要なのは、
- positionの設定によって
bodyの範囲は変わりうる-
staticやrelativeのような通常のフローになるものはbodyの範囲の対象ですが、absoluteやfixedのようなものは範囲の対象外
-
です。
positionについては、JavaScript(でもほぼCSS) 第7回 positionの動作参照ください。
bodyの範囲調査
bodyの範囲について確認します。
第10回のソースをもとにして、
- 「画面上部表示」を押下すると、コンソールに
bodyの幅、高さを表示するようにします。bodyの幅や高さは未設定、すなわち、デフォルトの状態です。 - また、
topDivのdiv要素を押下して、ポインターをbody範囲外に移動すると、その時のポインター位置をコンソールに表示します。
画面のイメージは「課題」のところのGIFイメージを参照ください。
body_range.htmlのソースコード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>body_range</title>
<link rel="stylesheet" href="body_range.css" type="text/css">
<script src="body_range.js" defer></script>
</head>
<body>
<div background-color="red">- 画面表示 -</div>
<div id="showTop">画面上部表示</div>
<div id="topDiv">
<div class="closeCircle" id="closeTop">×</div>
画面の上に表示されます。
</div>
</body>
</html>
body_range.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: visible;
background-color:yellow;
position: absolute;
width: 300px;
height: 100px;
top: 0px;
left: 20%;
}
body_range.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";
console.log(`body offsetwidth:${document.body.offsetWidth}`);
console.log(`body offsetHeight:${document.body.offsetHeight}`);
}
function closeWindow(divId) {
document.getElementById(divId).style.visibility = "hidden";
}
/** ドラッグ&ドロップで移動する設定*/
var elemId; //ドラッグした要素のId
var elemOffsetX; //ドラッグした要素のoffsetX
var elemOffsetY; //ドラッグした要素のoffsetY
//"topDiv"にマウスダウン、マウスアップ時のEventLisnterを追加。
document.getElementById("topDiv").addEventListener("pointerdown", handlePointerDown);
//document.getElementById("topDiv").addEventListener("pointerup", handlePointerUp);
/**
* マウス、もしくは、タッチで要素がアクティブになった時に、
* - その座標の保存。
* - ドラッグによる移動時のEventListnerを追加。×画面の上に表示されます。
* - ドラッグしながらポインタが画面外移動時のEventListenerを追加。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function handlePointerDown(event) {
getElemOffset(event);
// document.getElementById(elemId).addEventListener("pointermove", dragWindow);
document.body.addEventListener("pointerleave", showPointerPos);
}
/**
* 要素のオフセット(offsetX、offsetY)、およびその要素のIdを保存。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function getElemOffset(event) {
elemOffsetX = event.offsetX;
elemOffsetY = event.offsetY;
elemId = event.target.id;
}
/**
* body外移動時にポインターの位置を表示。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function showPointerPos(event) {
console.log(`pointer pageX:${event.pageX}`);
console.log(`pointer pageY:${event.pageY}`);
}
画面上部表示時に実行する処理は以下の部分です。
function showWindow(divId) {
document.getElementById(divId).style.visibility = "visible";
console.log(`body offsetwidth:${document.body.offsetWidth}`);
console.log(`body offsetHeight:${document.body.offsetHeight}`);
}
document.body.offsetWidthとdocument.body.offsetHeightの値をコンソールに表示します。
「画面上部表示」を押下すると、今回の私の環境ではコンソールにbodyの幅と高さが表示されました。
body offsetwidth:818
body offsetHeight:48
赤枠部分がbodyの範囲で、黒枠がブラウザの画面範囲です。
これをみてわかるように、黄色のdiv要素はbodyの範囲ではないのです。この要素はCSSでposition:absoluteとしたため、要素は文書の通常のフローから除外されているのだと思います。
topDivのdiv要素を押下して、ポインターをbody範囲外に移動したときには、コンソールにポインター位置が表示されます。今回の私の環境では、以下のような感じでした。
pointer pageX:155
pointer pageY:58
pointer pageX:303
pointer pageY:100
これは、2回分のpointerleaveの結果ですが、発生した場所はそれぞれ違います。

赤丸で示したところが発生した場所です。
1つ目の「pageX:155、pageY:58」のところは、document.body.offsetHeight=48であることを考えると、妥当な値です。
2つ目の「pageX:303、pageY:100」のところは、document.body.offsetHeight=48であることを考えると、???となりませんか?
調査してみると、このtopDivのdiv要素も通常のフローからは除外されているものの、bodyの一部のようです。
したがって、bodyの範囲は、以下の図の赤枠のようになります。

ちなみに、topDivのdiv要素をもっと下に配置して本来のbodyよりも下に配置しても、このtopDivの要素はbodyの一部で変わりません。
pointermoveによる要素の移動が追い付かず、ポインターが要素の外にでてしまう動作
「課題」に載せているアニGIFの動作を、「bodyの範囲調査」をふまえてみてみると、ポインターが要素の外にでてしまう動作には2つの要因がありそうだと想像つきました。
すなわち、要素の移動が追い付かない動作は、以下の時に発生しそうということです。
- ポインターが要素の外にでた場合
- ポインターが
bodyの範囲外にでた場合
1つ目については対策をこの後記載します。2つ目についてはbodyの範囲が今回のソースのようにウィンドウの移動する範囲を考慮した大きさにしておくくらいしかないのかなと思いました。
ポインターが要素の外にでた場合の対策
第9回で本動作を作成した時の想定動作は、
- ドラッグでウィンドウ移動開始
- ドラッグ中ウィンドウの移動
- ドロップでウィンドウ移動終了
でした。
ウィンドウの移動がポインターの移動に追い付いておらず、かつ、pointermoveを実装している要素を、div要素に限定しており、要素の外にポインターがでてしまうということなので、pointermoveを実装する要素をdiv要素ではなく、bodyにすればよい と思いました。
ということで、第10回のJavaScriptのソースに以下の4点の修正を加えます。
-
pointerupをtopDivのdiv要素からbodyへ実装するように変更 -
handlePointerDown関数内のpointermoveをelemId(要するにtopDiv)からbodyへ実装するように変更 -
handlePointerUp関数内のpointermoveをelemId(要するにtopDiv)からbodyへ実装削除するように変更 -
cancelDragWindow関数内のpointermoveをelemId(要するにtopDiv)からbodyへ実装削除するように変更
また、htmlのソースもbodyの範囲を拡張するため、<br>で改行を追加します。
modeless_drag_pointer2.htmlのソース(第10回から変更あり)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dragpointer2</title>
<link rel="stylesheet" href="modeless_drag_pointer2.css" type="text/css">
<script src="modeless_drag_pointer2.js" defer></script>
</head>
<body>
<div>- 画面表示 -</div>
<div id="showTop">画面上部表示</div>
<div id="topDiv">
<div class="closeCircle" id="closeTop">×</div>
画面の上に表示されます。
</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
</body>
</html>
modeless_drag_pointer2.cssのソース(第10回から変更なし)
.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_pointer2.js(第10回から変更あり)
/** 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("pointerdown", handlePointerDown);
document.body.addEventListener("pointerup", handlePointerUp);
/**
* マウス、もしくは、タッチで要素がアクティブになった時に、
* - その座標の保存。
* - ドラッグによる移動時のEventListnerを追加。×画面の上に表示されます。
* - ドラッグしながらポインタが画面外移動時のEventListenerを追加。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function handlePointerDown(event) {
getElemOffset(event);
document.body.addEventListener("pointermove", dragWindow);
document.body.addEventListener("pointerleave", cancelDragWindow);
}
/**
* マウス、もしくは、タッチで要素が非アクティブになった時に、
* - ドラッグによる移動時のEventListenerを削除。
*
* @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
*/
function handlePointerUp(event) {
document.body.removeEventListener("pointermove", dragWindow);
document.body.removeEventListener("pointerleave", 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.body.removeEventListener("pointermove", dragWindow);
document.body.removeEventListener("pointerleave", cancelDragWindow);
}
bodyの範囲内であれば、ポインターを速く動作させてもウィンドウは追従して動作します。
しかし、bodyの範囲外の場合、ポインターを速く動作させるとウィンドウは追従せず止まります(<br>ででbodyの範囲を広げましたが、それより下にポインターが移動すると対応できないということです)。
おわりに
今回はウィンドウのドラッグ&ドロップの動作に高速ポインター移動にある程度対応できるようにソースの改善を行いました。
bodyの範囲外に移動した場合の動作については解消できませんでしたが、画面の下端はある程度なんとでもなるのかなと思います。


