Div要素に穴をぶちあけて特定のボタンのみクリックさせる方法

# はじめに
ユーザーに特定の箇所以外操作してほしくない時ってありますよね？
素のcssとjavascriptだけで実装できる方法があるので健忘録としてまとめます。

# 実装例
仕組みとしては単純で、画面を覆う要素(ここでは`cover`としています)を作成し、[clip-path](https://developer.mozilla.org/ja/docs/Web/CSS/clip-path)スタイルで指定箇所を切り抜くだけです。
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="RwLJrge" data-user="rahhi555" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/rahhi555/pen/RwLJrge">
  Untitled</a> by rahhi555 (<a href="https://codepen.io/rahhi555">@rahhi555</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# 解説
一つずつ順を追って解説していきます
### 1. 全体を覆うdiv要素を作成して、全てのクリック操作を封じる
~~~html
<button id="button1">button1</button>
<button id="button2">button2</button>

<!-- このdiv要素が最前面かつ画面いっぱいに広がることで、　クリックイベント等を吸い取ります -->
<div id="cover"></div>
~~~
~~~css
/*
 　　position: absolute だと親にposition持ちが存在した場合そっちに基準が向かうので,
　　　　position: fixed で親要素に関係なくhtml要素の4辺を基準にします
 */
#cover {
  background-color: rgba(0,0,0,0.5);
  position: fixed;
  inset: 0;
  z-index: 2147483647;
}
~~~
### 2. クリックさせたいボタンのパスを作成する
複数の座標を指定することで、コンピューターがその間を線で補完して図形を描写するベクタ形式と呼ばれる画像タイプがあります。
[パス](https://developer.mozilla.org/ja/docs/Web/SVG/Tutorial/Paths)はベクタ形式で用いられる記法で、座標を絶対的に指定するほか、記述した座標から相対的に移動したポイントを指定することが可能です。

~~~javascript
/** coverのパスと操作箇所(button)のパスを取得し、coverのclipPathスタイルに適用させる */
const setCoverClipPath = (button) => {
  // getBoundingClientRectメソッドでスクリーンから見たボタンの位置４点を取得する
  const { top, left, width, height } = button.getBoundingClientRect()

  // 大文字は絶対位置で小文字は相対位置です
  // M: 始点(x及びy), h: 水平に移動(x), v: 垂直に移動(y), z: Mへ戻る直線
  const buttonPath = `M ${left} ${top}  h ${width} v ${height} h -${width} z`
  // 3に続く
~~~

### 3. coverのパスを2とは逆方向に作成する
ここが少し工夫をしなければならないポイントで、[ドーナツ状のパスを作成するには時計回りと反時計回りのパスを合体](https://stackoverflow.com/questions/3349651/how-to-achieve-donut-holes-with-paths-in-raphael/3365621)させなければなりません(理由は知りません！)
というわけで2とは逆方向にcoverのパスを取得します。

~~~javascript
  // 2で書いた処理
  
  // 表示されているスクリーンの幅と高さを取得する
  const { clientWidth, clientHeight } = document.getElementById('cover')

  // 2は右回転だったので左回転でパス作成
  const coverPath = `M 0 0 v ${clientHeight} h ${clientWidth} v -${clientHeight} z`

  // 4に続く
~~~

### 4. coverにclipPathスタイルを適用させる
作成した２つのパスを合体して適用させるだけです

~~~javascript
  // 3までの処理 

  cover.style.clipPath = `path('${buttonPath} ${coverPath}')`  
}
~~~

### 5. 完成
~~~javascript
/** coverのパスと操作箇所(button)のパスを取得し、coverのclipPathスタイルに適用させる */
const setCoverClipPath = (button) => {
  // getBoundingClientRectメソッドでスクリーンから見たボタンの位置４点を取得する
  const { top, left, width, height } = button.getBoundingClientRect()

  // 大文字は絶対位置で小文字は相対位置です
  // M: 始点(x及びy), h: 水平に移動(x), v: 垂直に移動(y), z: Mへ戻る直線
  const buttonPath = `M ${left} ${top}  h ${width} v ${height} h -${width} z`

  // 表示されているスクリーンの幅と高さを取得する
  const { clientWidth, clientHeight } = document.getElementById('cover')

  // 2は右回転だったので左回転でパス作成
  const coverPath = `M 0 0 v ${clientHeight} h ${clientWidth} v -${clientHeight} z`

  cover.style.clipPath = `path('${buttonPath} ${coverPath}')`  
}
~~~
ちなみにカバーしている範囲は表示している画面内のみなので、スクロールは無効にしたほうがいいと思います。
基本的なことはこれだけなので、vueやreactにも導入しやすいのではないでしょうか。
頑張れば実際に動かすことのできるチュートリアル等も作成することができます。
![チュートリアル.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/863781/8aa4c9f8-3c23-ef36-b669-36d2ddfe091b.gif)

# 追記
https://introjs.com/
よくよく調べたら普通にライブラリありました。なんという車輪の再発明。
しかも中身を読んでいくと自分の実装より遥かにシンプルに書かれていました。
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="zYELwEj" data-user="rahhi555" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/rahhi555/pen/zYELwEj">
  changeTarget</a> by rahhi555 (<a href="https://codepen.io/rahhi555">@rahhi555</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

画面をカバーするdiv要素より高いz-indexを持ったクラスを付け替えるだけです。
ただし[スタックコンテキスト](https://ics.media/entry/200609/)の関係で常にカバー要素をターゲットと同じ階層に置かなければ正常に順序付けされない可能性があるので、そういったしがらみから逃れたい場合に紹介した方法が役に立つかもしれません。