はじめに
第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
の範囲外に移動した場合の動作については解消できませんでしたが、画面の下端はある程度なんとでもなるのかなと思います。