【JavaScript】カーソルの移動速度によってツールチップの表示を制御する。

## 実装仕様

以下のように、ボタン上を追加するマウスカーソルのスピードによってツールチップを出すか出さないかを制御する実装を行います。

![1e68e816cfb65f6042f31a3ad92d0992.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/771397ef-ea2c-1580-6ba3-b8b0a7ba01da.gif)

### 実装の考え方
マウスがボタン上に乗ったら、一定時間ごとのマウスの移動距離を計測します。移動距離が基準値以上の場合（素早く移動している場合）ツールチップは表示させません。
さらにマウスが移動し、移動距離が基準値以下になった場合（ゆっくり移動している場合）はツールチップを表示させます。
一度、ツールチップが表示されたら、以後はボタンからマウスが外れるまで表示しっぱなしとなるため距離計測はしません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/a1c37e75-9ea5-c8f2-44cd-4f2767762b60.png" width="800px">

### コードの雛形
今後、ツールチップを表示させる必要があるその他の要素にも適応させるためクラス化します。
```html
<!DOCTYPE HTML>
<html>
<button id="btn">ボタン</button>
<div id="tooltip" class="hidden">Tooltip</div>
<body>
  <script>

    class HoverIntent {
      constructor(
        elem, //ボタン要素
        actionOnSlowlyMove, //ゆっくり動いた時の処理
        actionOnLeave, //外れた時の処理
        baseDistance = 30, //基準となる距離
        interval = 50, //計測する時間の間隔
      ) {
        this.elem = elem; 
        this.actionOnSlowlyMove = actionOnSlowlyMove; 
        this.actionOnLeave = actionOnLeave;  
        this.baseDistance = baseDistance; 
        this.interval = interval; 

        //各メソッドのthisをインスタンスに紐付け
        this.trackDistance = this.trackDistance.bind(this);
        this.onMouseOver = this.onMouseOver.bind(this);
        this.onMouseMove = this.onMouseMove.bind(this);
        this.onMouseOut = this.onMouseOut.bind(this);

        this.elem.addEventListener("mouseover", this.onMouseOver); //要素にマウスオーバーイベントを登録
        this.elem.addEventListener("mouseout", this.onMouseOut); //要素にマウスアウトイベントを登録
      }

      trackDistance() {
        // マウスの距離を計測する処理
      }

      onMouseOver(event) {
        //マウスがボタン上に乗った時の処理      
      }

      onMouseMove(event) {
        //マウスがボタン上で動いた場合の処理       
      }

      onMouseOut() {
        // マウスがボタンから外れた時の処理
      }
    }

    const tt = document.getElementById("tooltip");
    const btn = document.getElementById("btn");

    new HoverIntent( //インスタンス作成
      btn,
      () => { tt.classList.remove("hidden") },//乗った時の関数
      () => { tt.classList.add("hidden") }//外れた時の関数
    )

  </script>
</body>
</html>

<style>
  省略
</style>
```

マウスがボタン上に乗った時の関数(onMouseOver)を作成します。

```javascript
onMouseOver(event) {
  this.prevX = event.pageX; //乗った瞬間のカーソルの垂直位置を格納
  this.prevY = event.pageY;//乗った瞬間のカーソルの水平位置を格納

  this.elem.addEventListener("mousemove", this.onMouseMove);//ボタンにマウスムーブイベントを登録
  this.checkDistanceInterval = setInterval(this.trackDistance, this.interval);//一定時間毎にマウス移動距離を計測(trackDistanceを実行)
}
```

マウスの距離を計測する関数(trackDistance)を作成します。(一定時間毎に実行される。)

```javascript
trackDistance() {
  let distance; //一定時間毎のマウスの移動距離

  distance = Math.sqrt(
    Math.pow(this.prevX - this.lastX, 2) +
    Math.pow(this.prevY - this.lastY, 2)
  )
}
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/eadd4c26-e491-fcb7-1a35-dd858b864196.png" width="500px">

【trackDistance関数】に【マウスがゆっくり移動した場合】の実装を追加します。

```diff_javascript

trackDistance() {
  let distance;//一定時間毎のマウスの移動距離

  distance = Math.sqrt(
    Math.pow(this.prevX - this.lastX, 2) +
    Math.pow(this.prevY - this.lastY, 2)
  )
+  if (distance < this.baseDistance) { //基準距離より移動距離が大きい
+    // ゆっくり移動した場合の処理
+    clearInterval(this.checkDistanceInterval); //インターバルをクリア
+    this.isSlowHover = true; // フラグを立てる
+    this.actionOnSlowlyMove(); // ゆっくり移動した場合の処理を実行(インスタンス化した際に引数で渡される関数)
+  } else {
+    //素早く移動した場合の処理
+  }
}
```

【trackDistance関数】に【マウスが素早く移動した場合】の実装を追加します。

```diff_javascript
trackDistance() {
  // マウスの距離を計測する処理
  let distance;//一定時間毎のマウスの移動距離

  distance = Math.sqrt(
    Math.pow(this.prevX - this.lastX, 2) +
    Math.pow(this.prevY - this.lastY, 2)
  )
 if (distance < this.baseDistance) { //基準距離より移動距離が大きい
    clearInterval(this.checkDistanceInterval); //インターバルをクリア
    this.isSlowHover = true; // フラグを立てる
    this.actionOnSlowlyMove(); // ゆっくり移動した場合の処理を実行
  } else {
+    //素早く移動した場合の処理
+    // 現在のマウスの位置を次回計測開始時点のマウス位置に登録
+    this.prevX = this.lastX;
+    this.prevY = this.lastY;
  }
}
```
【マウスが要素から外れた場合の関数】の実装を追加します。
```javascript
onMouseOut() {
  // マウスがボタンから外れた時の処理
  this.elem.removeEventListener("mousemove", this.onMouseMove);//要素からマウスムーブイベントを解除
  clearInterval(this.checkDistanceInterval);//インターバルをクリア
  if (this.isSlowHover) { //ツールチップが表示されていた場合
    this.actionOnLeave(); //カーソルがボタンから離れた場合の処理を実行(インスタンス化した際に引数で渡される関数)
    this.isSlowHover = false; //フラグを消す
  }
}
```

### 全文

```html
<!DOCTYPE HTML>
<html>

<body>
  <button id="btn">ボタン</button>
  <div id="tooltip" class="hidden">Tooltip</div>

  <script>

    class HoverIntent {
      constructor(
        elem,
        actionOnSlowlyMove,
        actionOnLeave,
        baseDistance = 30,
        interval = 50,
      ) {
        this.elem = elem;
        this.actionOnSlowlyMove = actionOnSlowlyMove;
        this.actionOnLeave = actionOnLeave;
        this.baseDistance = baseDistance;
        this.interval = interval;
        this.trackDistance = this.trackDistance.bind(this);
        this.onMouseOver = this.onMouseOver.bind(this);
        this.onMouseMove = this.onMouseMove.bind(this);
        this.onMouseOut = this.onMouseOut.bind(this);

        this.elem.addEventListener("mouseover", this.onMouseOver);
        this.elem.addEventListener("mouseout", this.onMouseOut);
      }

      trackDistance() {
        let distance;

        distance = Math.sqrt(
          Math.pow(this.prevX - this.lastX, 2) +
          Math.pow(this.prevY - this.lastY, 2)
        )
        if (distance < this.baseDistance) {
          clearInterval(this.checkDistanceInterval);
          this.isSlowHover = true;
          this.actionOnSlowlyMove();
        } else {
          this.prevX = this.lastX;
          this.prevY = this.lastY;
        }
      }

      onMouseOver(event) {
        this.prevX = event.pageX;
        this.prevY = event.pageY;

        this.elem.addEventListener("mousemove", this.onMouseMove);
        this.checkDistanceInterval = setInterval(this.trackDistance, this.interval);
      }

      onMouseMove(event) {
        this.lastX = event.pageX;
        this.lastY = event.pageY;
      }

      onMouseOut() {
        this.elem.removeEventListener("mousemove", this.onMouseMove);
        clearInterval(this.checkDistanceInterval);
        if (this.isSlowHover) {
          this.actionOnLeave();
          this.isSlowHover = false;
        }
      }
    }

    const tt = document.getElementById("tooltip");
    const btn = document.getElementById("btn");

    new HoverIntent(
      btn,
      () => { tt.classList.remove("hidden") },
      () => { tt.classList.add("hidden") }
    )
  </script>
</body>

</html>
<style>
  body {
    margin: 0px;
  }

  button {
    margin-left: 100px;
    margin-top: 100px;
    padding: 10px 50px;
  }

  #tooltip {
    margin-left: 100px;
    width: 100px;
    border: solid 1px gray;
    border-radius: 3px;
    text-align: center;
  }

  .hidden {
    display: none;
  }

</style>
```