JavaScript 第12回 ウィンドウをドラッグ＆ドロップの改善・注意事項（リメイク版）

# はじめに
今回は、第11回の記事のリメイク版です。
アドバイスをいただきましたので、それをマージした記事です。
ところどころ記載を修正させていただきましたが、一番最後の「ポインターが`body`の範囲外に出た場合」が追加箇所です。

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

# 今回実施する内容
- HTMLのbodyの範囲の確認
- `pointermove`が該当要素の外へ出た場合の対応
![drag時の課題対応.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405480/81b11645-9d17-1f43-739c-e4f4f6132b29.gif)

# ソースコード(Git Hub)
https://github.com/Build-Mount/JS_12_drag_pointer3

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

# 参考
[position](https://developer.mozilla.org/ja/docs/Web/CSS/position)

# 用語

# 課題と原因の推測
第9回、10回と作成したウィンドウをドラッグ＆ドロップを速く実施したらどうなるか？特に`pointermove`の部分がどうなるのか？というのが疑問でした。
`pointmove`のイベントの発生とJavaScriptによる画面処理では処理が間に合うのか？ということ疑問でした。
![drag時の課題.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405480/4a4df68a-0699-aeb4-469c-785242a61455.gif)

第10回のソースコードで実際に試してみると、
- 要素の中心に近いところで、速くポインターを動かしても問題なさそう
- **要素の端で、速くポインターを動かすとウィンドウの移動が止まってしまう**

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

課題は2つ目の **ウィンドウの移動が止まってしまう** です。
この動作の原因を考えてみると、
- `pointermove`イベント発生して要素が移動するよりも早くにポインターが移動し、ポインターが要素の外に出てしまう
 
ためだと思いました。そのため、
- `pointermove`イベントを画面全体の`body`に付与すれば、ポインターが要素の外に出ることはなくなり、この課題は解決するのでは？

と考えました。
でも、色々試してみると、課題はもうひとつあって、
- 画面下端で、`pointerleave`イベントの発生によって`pointermove`イベント発生を止めてしまう

というのもありました。
`pointerleave`は`body`に対して`EventListener`を追加していました。しかし、**`body`＝ブラウザ画面内**ではなく、**ブラウザ画面の下部は`body`の外の可能性がある**ということでした。

ということで、課題は、
- **要素の端で、速くポインターを動かすとウィンドウの移動が止まってしまう**
- **画面下端で、`pointerleave`イベントの発生によって`pointermove`イベント発生を止めてしまう**

です。

# `body`の範囲
順番が変わってしまいますが、先に`body`の範囲を確認していきます。
結論から言えば、デフォルトでは、
- width：ブラウザの画面幅
- height：ソースで記載された内容の最下位の位置

のようです。
注意が必要なのは、
- `position`の設定によっては、`body`の対象かそうでないかは変わる
    - `static`や`relative`のような**通常のフロー**になるものは`body`の範囲の対象ですが、`absolute`や`fixed`のようなものは`body`の範囲の対象外のようで範囲だったりする

です。
`position`についてはよくわからないと思いますので、これから説明していきます。

:::note info
`position`については、[JavaScript(でもほぼCSS) 第7回 positionの動作](https://qiita.com/Mount/items/2931a2d92e23b345b9cb)参照ください。
:::

## `body`の範囲調査
`body`の範囲について確認します。
第10回のソースをもとにして、
- 「画面上部表示」を押下すると、コンソールに`body`の幅、高さを表示するようにします。`body`の幅や高さは未設定、すなわち、デフォルトの状態です。
- また、`topDiv`の`div`要素を押下して、ポインターを`body`範囲外に移動すると、その時のポインター位置をコンソールに表示します。

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

<details><summary>body_range.htmlのソースコード</summary>

```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>
```
</details>
<details><summary>body_range.cssのソースコード</summary>

```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%;
}
```
</details>

<details><summary>body_range.jsのソースコード</summary>

```javascript: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}`);
}
```
</details>

画面上部表示時に実行する処理は以下の部分です。
```javascript
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
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405480/3c708557-67e7-22c5-66a5-7dfe93bf0943.png)

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

`topDiv`の`div`要素を押下後ポインターを`body`範囲外に移動したときには、コンソールにポインター位置が表示されます（`div`要素はドラッグ＆ドロップできるようには構成しておりませんのでご注意ください）。今回の私の環境では、以下のような感じでした。
```
pointer pageX:155
pointer pageY:58
```
ポインターを別の場所に移動すると、

```
pointer pageX:303
pointer pageY:100
```
でした。この2回分の`pointerleave`イベントが発生した場所はそれぞれ違います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405480/88ed7e26-4a0f-300d-e7f2-ba43f452f7a8.png)
赤丸で示したところが発生した場所です。
1つ目の「pageX:155、pageY:58」のところは、`document.body.offsetHeight=48`であることを考えると、多少値が大きいですが、まあ妥当な値です。
2つ目の「pageX:303、pageY:100」のところは、`document.body.offsetHeight=48`であることを考えると、？？？となりませんか？
調査してみると、この`topDiv`の`div`要素も**通常のフローからは除外されているものの、`body`の一部**のようです。
したがって、`body`の範囲は、以下の図の赤枠のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405480/7848b8b8-b71e-12c1-f9f1-44c751b0ca0c.png)
ちなみに、`topDiv`の`div`要素をもっと下に配置して本来の`body`よりも下に配置しても、この`topDiv`の要素は`body`の一部で変わりません。

# `pointermove`による要素の移動が追い付かず、ポインターが要素の外に出てしまう動作
「課題と原因の推測」に載せているアニGIFの動作を、「`body`の範囲調査」をふまえてみてみると、**ポインターが要素の外に出てしまう**動作には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>`で改行を追加します。
<details><summary>modeless_drag_pointer2.htmlのソース（第10回から変更あり）</summary>

主な変更は、`body`の範囲を拡張するため、`<br>`で改行を追加であり、改行の追加自体は課題解決の本質ではなく、ポインターの移動が要素の外で`body`の範囲内になるようにするためです。
また、`body`の範囲外になるような場合は引き続き課題が起こることを確認できるような程度の改行追加としています。
```html: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>
```
</details>
<details><summary>modeless_drag_pointer2.cssのソース（第10回から変更なし）</summary>

```css: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%;
}
```
</details>
<details><summary>modeless_drag_pointer2.js（第10回から変更あり）</summary>

```javascript: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);
}
```
</details>

上記を動作させると以下の通りになります。
![drag時の課題対応.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405480/284e64f4-5d38-ceb4-e793-f7c4b558dda8.gif)

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

## ポインターが`body`の範囲外に出た場合
ポインターが`body`の範囲外に出た場合の対処というよりは、ポインターが`body`の範囲外に出ないようにする対処について、アドバイスをいただきましたので、それを試していきたいと思います。
- `body`の範囲を100%にする対処
- `body`ではなく`window`に`addEventHandler`を付与する対処

### `body`の範囲を100%にする対処
「ポインターが要素の外に出た場合の対策」のソースをもとに、`body`の範囲を100%にします。
`height`を`100vh`に設定します。
`width`は現状のソースだとデフォルトでは画面の幅になるため、対応なしとします。

<details><summary>modeless_drag_pointer3.htmlのソース（modeless_drag_pointer2.htmlからほぼ変更なし）</summary>

- CSSとJavaScriptファイルのファイル名だけ変更しています。
```html:modeless_drag_pointer3.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_pointer3.css" type="text/css">
    <script src="modeless_drag_pointer3.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>
```
</details>
<details><summary>modeless_drag_pointer3.cssのソース（modeless_drag_pointer2.cssから変更あり）</summary>

- `body`に`height:100vh`を追加しています。

```css:modeless_drag_pointer3.css
body {
  height: 100vh;
}

.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%;
}
```
</details>
<details><summary>modeless_drag_pointer3.js（modeless_drag_pointer2.jsから変更なし）</summary>

```javascript:modeless_drag_pointer3.js
/** modeless用の設定 */
document.getElementById("showTop").addEventListener("click", () => {
    console.log(`body offsetwidth:${document.body.offsetWidth}`);
    console.log(`body offsetHeight:${document.body.offsetHeight}`);

    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);
}
```
</details>

上記を動作させると、ブラウザの右にスクロールバーは表示されますが、ブラウザ画面内の下のほうも`body`となり、ポインターが`body`の範囲外に出ないようにすることができました。
![合格.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405480/d4d69457-c222-2e81-7f97-62ace5aa0f59.gif)
100vhにしたときにスクロールバーが表示される理由は、アドレスバー分を含んで100vhになるとういことで、アドレスバー分を減らした98vhくらいにするとスクロールバー自体は消すことができました。
もともとのhtmlが長いようならスクロールバーを消すことはできませんので、あまり気にすることではないかもしれません。

### `body`ではなく`window`に`addEventHandler`を付与する対処
「ポインターが要素の外に出た場合の対策」のソースをもとに、`body`に付与していた`pointermove`、`pointerup`、および、`pointerleave`の`addEventHandler`を`window`に付与します。


<details><summary>modeless_drag_pointer4.htmlのソース（modeless_drag_pointer2.htmlからほぼ変更なし）</summary>

- CSSとJavaScriptファイルのファイル名だけ変更しています。
```html:modeless_drag_pointer4.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_pointer4.css" type="text/css">
    <script src="modeless_drag_pointer4.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>
```
</details>
<details><summary>modeless_drag_pointer4.cssのソース（modeless_drag_pointer2.cssから変更なし）</summary>

```css:modeless_drag_pointer4.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%;
}
```
</details>
<details><summary>modeless_drag_pointer4.js（modeless_drag_pointer2.jsから変更あり）</summary>

- `body`に付与していた`pointermove`、`pointerup`、および、`pointerleave`の`addEventHandler`を`window`に付与します。
```javascript:modeless_drag_pointer4.js
/** modeless用の設定 */
document.getElementById("showTop").addEventListener("click", () => {
    console.log(`body offsetwidth:${document.body.offsetWidth}`);
    console.log(`body offsetHeight:${document.body.offsetHeight}`);

    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);
window.addEventListener("pointerup", handlePointerUp);

/**
 * マウス、もしくは、タッチで要素がアクティブになった時に、
 * - その座標の保存。
 * - ドラッグによる移動時のEventListnerを追加。×画面の上に表示されます。
 * - ドラッグしながらポインタが画面外移動時のEventListenerを追加。
 * 
 * @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
 */
function handlePointerDown(event) {
    getElemOffset(event);
    window.addEventListener("pointermove", dragWindow);
    window.addEventListener("pointerleave", cancelDragWindow);
}

/**
 * マウス、もしくは、タッチで要素が非アクティブになった時に、
 * - ドラッグによる移動時のEventListenerを削除。
 * 
 * @param {object} event コールバック関数は発生したイベントを説明するEventに基づくオブジェクト。
 */
function handlePointerUp(event) {
    window.removeEventListener("pointermove", dragWindow);
    window.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) {
    window.removeEventListener("pointermove", dragWindow);
    window.removeEventListener("pointerleave", cancelDragWindow);
}
```

上記を動作させると、`body`の範囲を100%にする対処と同様にブラウザ画面内の下のほうでも問題なく動作しました。そのうえ、スクロールバーも表示されませんでした。
この実装のほうが期待にそう結果になったと思いました。

# おわりに
今回はウィンドウのドラッグ＆ドロップの動作に高速ポインター移動にある程度対応できるようにソースの改善を行いました。
第11回で解決させなかった`body`の範囲外に移動した場合の動作についてもアドバイスをいただき解消できました。