0
1

JavaScript 第11回 ウィンドウをドラッグ&ドロップの改善・注意事項

Posted at

はじめに

第9回、10回と作成したウィンドウをドラッグ&ドロップのソースについて、もう少し改善の余地があり、また、注意することもあるなと思いましたので、まとめます。
なお、第10回のソースをベースとしますので、第9回、第10回を見ていることを前提として記載します。

今回実施する内容

  • HTMLのbodyの範囲の確認
  • pointermoveが該当要素の外へ出た場合の対応
    drag時の課題対応.gif

ソースコード(Git Hub)

環境

OS: Windows 11 JP (64bit)
Microsoft Edge:バージョン 121.0.2277.98 (公式ビルド) (64 ビット)

参考

position

用語

課題

第9回、10回と作成したウィンドウをドラッグ&ドロップを速く実施したらどうなるか?特にpointermoveの部分がどうなるのか?というのが疑問でした。
要するにpointmoveのイベントの発生とJavaScriptによる画面処理では処理が間に合うのか?ということです。
drag時の課題.gif

実験してみると

  • 要素の中心に近いところで、速くポインターを動かしても問題なさそう
  • 要素の端で、速くポインターを動かすとウィンドウの移動が止まってしまう
    という状況でした。

pointmoveのイベント発生の解像度がどのくらいなのか?と気になって調べてみましたが、具体的なものはよくわかりませんでした。

課題の原因

課題となった2つの動作の要因を考えてみると、

  • pointermoveとそれによる要素の移動が追い付かず、ポインターが要素の外に出てしまう
    ためだと思いました。
    そのため、
  • pointermoveイベントを画面全体のbodyに付与すればよいのでは?

と考えました。
でも、実はもうひとつ落とし穴があって、

  • pointerleaveイベントの発生によってpointermove処理を止めてしまう
    というのもありました。
    pointerleavebodyに対してEventListenerを追加していました。body=ブラウザ画面内ではなく、ブラウザ画面の下部はbodyの外の可能性があるということでした。

というのもありました。
ということで、動作を確認していきます。

bodyの範囲調査

順番が変わってしまいますが、先にbodyの範囲を見ていきます。
結論から言えば、デフォルトでは

  • width:ブラウザの画面幅
  • height:ソースで記載された内容の最下位の位置

のようです。
もうひとつ注意が必要なのは、

  • positionの設定によってbodyの範囲は変わりうる
    • staticrelativeのような通常のフローになるものはbodyの範囲の対象ですが、absolutefixedのようなものは範囲の対象外

です。

positionについては、JavaScript(でもほぼCSS) 第7回 positionの動作参照ください。

bodyの範囲調査

bodyの範囲について確認します。
第10回のソースをもとにして、

  • 「画面上部表示」を押下すると、コンソールにbodyの幅、高さを表示するようにします。bodyの幅や高さは未設定、すなわち、デフォルトの状態です。
  • また、topDivdiv要素を押下して、ポインターをbody範囲外に移動すると、その時のポインター位置をコンソールに表示します。

画面のイメージは「課題」のところのGIFイメージを参照ください。

body_range.htmlのソースコード
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のソースコード
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のソースコード
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.offsetWidthdocument.body.offsetHeightの値をコンソールに表示します。
「画面上部表示」を押下すると、今回の私の環境ではコンソールにbodyの幅と高さが表示されました。

body offsetwidth:818
body offsetHeight:48

image.png

赤枠部分がbodyの範囲で、黒枠がブラウザの画面範囲です。
これをみてわかるように、黄色のdiv要素はbodyの範囲ではないのです。この要素はCSSでposition:absoluteとしたため、要素は文書の通常のフローから除外されているのだと思います。

topDivdiv要素を押下して、ポインターをbody範囲外に移動したときには、コンソールにポインター位置が表示されます。今回の私の環境では、以下のような感じでした。

pointer pageX:155
pointer pageY:58
pointer pageX:303
pointer pageY:100

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

pointermoveによる要素の移動が追い付かず、ポインターが要素の外にでてしまう動作

「課題」に載せているアニGIFの動作を、「bodyの範囲調査」をふまえてみてみると、ポインターが要素の外にでてしまう動作には2つの要因がありそうだと想像つきました。
すなわち、要素の移動が追い付かない動作は、以下の時に発生しそうということです。

  • ポインターが要素の外にでた場合
  • ポインターがbodyの範囲外にでた場合

1つ目については対策をこの後記載します。2つ目についてはbodyの範囲が今回のソースのようにウィンドウの移動する範囲を考慮した大きさにしておくくらいしかないのかなと思いました。

ポインターが要素の外にでた場合の対策

第9回で本動作を作成した時の想定動作は、

  • ドラッグでウィンドウ移動開始
  • ドラッグ中ウィンドウの移動
  • ドロップでウィンドウ移動終了

でした。
ウィンドウの移動がポインターの移動に追い付いておらず、かつ、pointermoveを実装している要素を、div要素に限定しており、要素の外にポインターがでてしまうということなので、pointermoveを実装する要素をdiv要素ではなく、bodyにすればよい と思いました。
ということで、第10回のJavaScriptのソースに以下の4点の修正を加えます。

  • pointeruptopDivdiv要素からbodyへ実装するように変更
  • handlePointerDown関数内のpointermoveelemId(要するにtopDiv)からbodyへ実装するように変更
  • handlePointerUp関数内のpointermoveelemId(要するにtopDiv)からbodyへ実装削除するように変更
  • cancelDragWindow関数内のpointermoveelemId(要するにtopDiv)からbodyへ実装削除するように変更

また、htmlのソースもbodyの範囲を拡張するため、<br>で改行を追加します。

modeless_drag_pointer2.htmlのソース(第10回から変更あり)
modeless_drag_pointer2.html
<!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回から変更なし)
modeless_drag_pointer2.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_pointer2.js(第10回から変更あり)
modeless_drag_pointer2.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("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);
}

上記を動作させると以下の通りになります。
drag時の課題対応.gif

bodyの範囲内であれば、ポインターを速く動作させてもウィンドウは追従して動作します。
しかし、bodyの範囲外の場合、ポインターを速く動作させるとウィンドウは追従せず止まります(<br>ででbodyの範囲を広げましたが、それより下にポインターが移動すると対応できないということです)。

おわりに

今回はウィンドウのドラッグ&ドロップの動作に高速ポインター移動にある程度対応できるようにソースの改善を行いました。
bodyの範囲外に移動した場合の動作については解消できませんでしたが、画面の下端はある程度なんとでもなるのかなと思います。

0
1
4

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
0
1