【JavaScript】サイズ・座標 取得   〜 実践編 ＜ツールチップの実装＞ 〜

# JavaScriptを用いて要素のサイズ、座標を取得してツールチップを実装してみます。
この記事は、下記の記事の続編にあたります。
[【JavaScript】サイズ・座標 取得チートシート(Chrome)](https://qiita.com/sho_U/items/a8234132c8b2248ca01b)


## ツールチップの仕様

2種類のボタンがあり、それぞれのボタンの上にマウスカーソルが乗った場合に、上部中央（以下「基本位置」）に、あらかじめ設定しておいたメッセージを出現させます。
![56de76e4f35fecdc933dd39fe24c72a0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/2d3254ed-11f9-6933-0edd-963e014360ea.gif)

ただし、上部にツールチップが収まるスペースがない場合は下部に表示させます。
![ded1efb45389fd8c82372b4614e34da2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/f05f1a64-65b9-eb0b-b6f9-e17d06bd6f52.gif)

また、ボタン中央に合わせて配置するスペースが左右にない場合は、それぞれのフチに合わせます。
![420414cb40feaa66d73fad570cbd0776.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/5f0886fb-0d40-4879-424f-3437455d87f8.gif)


## まずはページの大枠を作ります。

```toolTip.html
<!DOCTYPE HTML>
<html>

<body>
  <iframe id="flame" src="./ifame.htm"></iframe>
  <!-- 表示箇所はiflameで表示。 -->
</body>

<style>
  body {
    margin: 10px;
  }

  #flame {
    width: 600px;
    height: 300px;
  }
</style>

</html>
```

```iflame.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="inner">
    <button class="btn" data-tooltip="sbtn">short button !!</button>
    <button class="btn" data-tooltip="long message tooltip long message tooltip">long button !!</button>
  </div>
  <script>
    let tooltipElem; //作成されたツールチップ要素

    function createTooltip() {}; //ツールチップを作成する関数

    function setTop() {}; //ツールチップのtopを決定する関数

    function setLeft() {}; //ツールチップのleftを決定する関数

    document.addEventListener("mouseover", () => {}); //mouseoverイベントで発火

    document.addEventListener('mouseout',() => {}); //mouseoutイベントで発火

  </script>
</body>

<style>
  body {
    width: 585px;
    height: 300px;
  }

  .tooltip {
    position: fixed;
    padding: 3px;
    border: 1px solid #b3c9ce;
    border-radius: 4px;
    text-align: center;
    color: #333;
    background: #fff;
    box-shadow: 3px 3px 3px rgba(0, 0, 0, .3);
  }

  #inner {
    width: 1500px;
    height: 700px;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .btn {
    margin: 70px;
  }
</style>

</html>
```

## 基本位置（垂直方向）にツールチップを出現させる実装。

マウスカーソルがボタン上に来た場合にツールチップを基本位置（垂直方向）に表示させる実装を行います。

ツールチップの位置を決定するにはツールチップに指定するべきtopを計算する必要があります。
topは以下図で計算すると取得することがでます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/788c4f02-6a6d-a019-17c7-9d47b82a9a4c.png" width="500px">

注 : DOMRectは実際の可視範囲ではなくiflame枠組みからの距離を取ります。 

```diff_html
  <script>
    let tooltipElem;

-   function createTooltip(message) {};
    function createTooltip(message) {
      tooltipElem = document.createElement('div');
      tooltipElem.innerText = message;
      // ボタン要素のdata-tooltipをテキストにする。
      tooltipElem.className = "tooltip"
      document.body.append(tooltipElem);
    }

-   function setTop() {};
    function setTop(buttonRect, tooltipRect) {     
      tooltipElem.style.top = `${buttonRect.top - tooltipRect.height}px`
    }

-   function setLeft() {};
    function setLeft() {
      tooltipElem.style.left = "10px"
      //左右は仮置き
    };

-   document.addEventListener("mouseover", () => {});
    //ここから処理開始
    document.addEventListener("mouseover", (ev) => { // 
    // どこかの要素上にカーソルがホバーしたら発火。
      if (ev.target.tagName !== "BUTTON") return;
      //mouseoverイベントがbuttonタグ上以外で発生した場合は処理終了。

      const overButton = ev.target;
      createTooltip(overButton.dataset.tooltip);
      // ツールチップを作成し、body要素に追加

      const buttonRect = overButton.getBoundingClientRect();
      //ホバーしたボタン要素のDOMRectを取得
      const tooltipRect = tooltipElem.getBoundingClientRect();
      //作成したツールチップの要素のDOMRectを取得
     
      setTop(buttonRect, tooltipRect);
      // ツールチップの垂直位置を指定する関数

      setLeft();
    })

-   document.addEventListener('mouseout',() => {});
    document.addEventListener('mouseout',() => {
    //ボタンからカーソルが離れたらツールチップ消す。
      if (tooltipElem) {
        tooltipElem.remove();
        tooltipElem = null;
      }
    };
  </script>
```
## ボタン下部に表示させる実装
ボタン上部にツールチップを表示させるスペースがない場合はボタン下部に表示させるためsetTop関数を書き換えます。

スペースがあるか無いかの判定は、ボタンの上部スペース（top）が、ツールチップの高さより狭い場合です。
スペースがない場合のツールチップのtopは以下図で計算すると取得することがでます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/99810a79-8c6c-6e2f-816a-0be230c3e23b.png" width="600px">

```diff_javascript
    function setTop(buttonRect, tooltipRect) {
+     if (tooltipRect.height >= buttonRect.top) {
+       // スペースがない場合
+        tooltipElem.style.top = `${buttonRect.bottom}px`
+      } else {
+        // スペースがある場合
        tooltipElem.style.top = `${buttonRect.top - tooltipRect.height}px`
+     }
    }
```

## 基本位置（水平方向）の計算

水平方向の位置を決定するためには、ボタンとツールチップの左側の幅の差分を計算する必要があります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/18d1e58c-81d1-c05b-e7f5-f304ead124aa.png" width="600px">

ボタンとツールチップの左側の幅の差分の計算は以下図で計算すると取得することがでます。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/4e8d07aa-38c1-bf12-8fbe-65bc26b42158.png" width="600px">

```diff_javascript
const buttonBaseDiff = (buttonRect.width - tooltipRect.width) / 2;
```
なお、ツールチップの方がボタンより幅広の場合は値は **「負の値」** となります。

ツールチップのleftの計算は以下図で計算すると取得することがでます。

- **ツールチップが短い場合**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/cd26fd1d-29a2-54db-9db3-f9586830900f.png" width="600px">

- **ツールチップが長い場合**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/06f4a375-1039-046d-b3b6-dc00ec78a60d.png" width="600px">

**↑どちらの場合も計算方法は同じ**


```diff_javascript
- function setLeft() {
-   tooltipElem.style.left = "10px"
- };
+ function setLeft(buttonRect, buttonBaseDiff) {
+  tooltipElem.style.left = `${buttonRect.left + buttonBaseDiff}px`;
+ }
```

```diff_javascript
+ const buttonBaseDiff = (buttonRect.width - tooltipRect.width) / 2;
  // 左側の幅の差分を取得
  
  setTop(buttonRect, tooltipRect);

- setLeft()
+ setLeft(buttonRect, buttonBaseDiff);
  //左端からのツールチップの距離を設定
```

## 基本位置（水平方向）に配置するスペースが左側にない場合
### 左側にスペースがない場合の判定方法

ツールチップの方が長い場合で考えてみます。

ツールチップを基本位置（水平方向）配置できる限界点は下記の図の状態です。

※ ボタンのleftを4、差分を-5と仮定

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/9d6ca5db-2c5b-f166-fb75-9ddc99a423fe.png" width="600px">

一方、基本位置（水平方向に配置できない状態とは下記の図のような状態です。
※ ボタンのleftを5、差分を-5と仮定
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/79ba74c1-0547-1ad2-f27e-ae1cde1004e5.png" width="600px">

つまり、下記のことが言えます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/c720015e-c780-1da5-9452-a719d09f792e.png" width="800px">


次にツールチップの方が短い場合で考えてみます。

ツールチップを基本位置（水平方向）に配置できる限界点は下記の図の状態です。
※ ボタンのleftを-5(5左にはみ出している。)、差分を5と仮定
注：DOMRectは、画面からはみ出している場合は負の値となる。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/5ec94443-9ac5-5e03-dc70-357118fd8553.png" width="600px">

一方、基本位置（水平方向）に配置できない状態とは下記の図のような状態です。
※ ボタンのleftを-6、差分を5と仮定

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/a6f72248-ce26-8046-e12b-0102f2dc80a3.png" width="600px">

つまり、ツールチップとボタンの差分の長短で場合わけせずとも下記のことが言えます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/c720015e-c780-1da5-9452-a719d09f792e.png" width="800px">

```diff_javascript
function setLeft(buttonRect, buttonBaseDiff) {
+ if (buttonRect.left + buttonBaseDiff < 0) {
+ // 中央に配置できない場合
+  tooltipElem.style.left = "0px";
+ // 画面左端にツールチップの左端を配置
+  return;
+  }
  tooltipElem.style.left = `${buttonRect.left + buttonBaseDiff}px`;
}
```

### 右側にスペースがない場合の判定方法

ツールチップの方が長い場合で考えてみます。

ツールチップを基本位置（水平方向）に配置できる限界点は下記の図の状態です。

※ ボタンのrightを10、差分を-5、bodyの横幅を15と仮定

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/5e35abcc-0622-fc3d-3293-c0b4dab21808.png" width="600px">

一方、基本位置（水平方向）に配置できない状態とは下記の図のような状態です。
※ ボタンのrightを11、差分を-5、bodyの横幅を15と仮定

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/16e4b24f-3ed0-a041-088f-563d9105fa04.png" width="600px">

つまり、下記のことが言えます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/c106144a-da9c-c4d7-5de8-1d93fa49c0bc.png" width="800px">

次にツールチップの方が短い場合で考えてみます。

ツールチップを中央に配置できる限界点は下記の図の状態です。

※ ボタンのrightを20、差分を5、bodyの横幅を15と仮定
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/48b9b545-f0c8-ee9b-1f65-c5ec7bff9d0d.png" width="600px">


一方、中央に配置できない状態とは下記の図のような状態です。
※ ボタンのrightを21、差分を5、bodyの横幅を15と仮定

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/2fc9d861-8bac-bc34-65b6-0f4841f6e31a.png" width="600px">

つまり、ツールチップとボタンの差分の長短で場合わけせずとも下記のことが言えます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/c106144a-da9c-c4d7-5de8-1d93fa49c0bc.png" width="800px">

```diff_javascript
function setLeft(buttonRect, buttonBaseDiff) {
 if (buttonRect.left + buttonBaseDiff < 0) {
   tooltipElem.style.left = "0px";
   return;
 }

+ if (buttonRect.right - buttonBaseDiff > document.body.clientWidth) {
+ // 中央に配置できない場合
+   tooltipElem.style.right = "0px";
+ // 画面右端にツールチップの右端を配置
+   return;
+ }

 tooltipElem.style.left = `${buttonRect.left + buttonBaseDiff}px`;
}
```

これでツールチップを正しい位置に配置することができました。

最後に、画面スクロールした場合にツールチップが残り続けると気持ち悪いので非表示にするように修正します。

```diff_javascript
+ function removeToolTip() {
+   if (tooltipElem) {
+   tooltipElem.remove();
+   tooltipElem = null;
+   }
+ }

+ document.addEventListener('mouseout', removeToolTip);
+ document.addEventListener('scroll', removeToolTip);
```

## 全文

```tooltip.html
<!DOCTYPE HTML>
<html>

<body>
  <iframe id="flame" src="./ifame.htm"></iframe>
</body>

<style>
  body {
    margin: 10px;
  }

  #flame {
    width: 600px;
    height: 300px;
  }
</style>

</html>
```

```iflame.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="inner">
    <button class="btn" data-tooltip="sbtn">short button !!</button>
    <button class="btn" data-tooltip="long message tooltip long message tooltip">long button !!</button>
  </div>
  <script>
    let tooltipElem;

    function createTooltip(message) {
      tooltipElem = document.createElement('div');
      tooltipElem.innerText = message;
      tooltipElem.className = "tooltip"
      document.body.append(tooltipElem);
    }

    function setTop(buttonRect, tooltipRect) {
      if (tooltipRect.height >= buttonRect.top) {
        tooltipElem.style.top = `${buttonRect.bottom}px`
      } else {
        tooltipElem.style.top = `${buttonRect.top - tooltipRect.height}px`
      }
    }

    function setLeft(buttonRect, buttonBaseDiff) {
      if (buttonRect.left + buttonBaseDiff < 0) {
        tooltipElem.style.left = "0px";
        return;
      }

      if (buttonRect.right - buttonBaseDiff > document.body.clientWidth) {
        tooltipElem.style.right = "0px";
        return;
      }
      tooltipElem.style.left = `${buttonRect.left + buttonBaseDiff}px`;
    }

    document.addEventListener("mouseover", (ev) => {
      if (ev.target.tagName !== "BUTTON") return;

      const overButton = ev.target;

      createTooltip(overButton.dataset.tooltip)

      const buttonRect = overButton.getBoundingClientRect();
      const tooltipRect = tooltipElem.getBoundingClientRect();

      const buttonBaseDiff = (buttonRect.width - tooltipRect.width) / 2;

      setTop(buttonRect, tooltipRect);
      setLeft(buttonRect, buttonBaseDiff);
    })

    function removeToolTip() {
      if (tooltipElem) {
        tooltipElem.remove();
        tooltipElem = null;
      }
    }

    document.addEventListener('mouseout', removeToolTip);
    document.addEventListener('scroll', removeToolTip);
  </script>
</body>

<style>
  body {
    width: 585px;
    height: 300px;
  }

  .tooltip {
    position: fixed;
    padding: 3px;
    border: 1px solid #b3c9ce;
    border-radius: 4px;
    text-align: center;
    color: #333;
    background: #fff;
    box-shadow: 3px 3px 3px rgba(0, 0, 0, .3);
  }

  #inner {
    width: 1500px;
    height: 700px;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .btn {
    margin: 70px;
  }
</style>

</html>
```