Vue+compositionAPIでiframe内のスクロールを検知する

# はじめに
webアプリでよく見る利用規約を下までスクロールするとボタンが活性化し、同意できる機能を実装したい

![](https://storage.googleapis.com/zenn-user-upload/d7656d9786b1-20211122.gif)

# コード全体

```html
<template>
  <div class="container">
    <div class="content">
      <div class="term-of-use-container">
        <iframe
          class="term-of-use"
          ref="termOfUseRef"
          src="/term_of_use.html"
          frameborder="0"
        />
      </div>
      <div class="button-container">
        <button
          class="button color-orange"
          :class="{ 'color-gray': isDisabled }"
          :disabled="isDisabled"
          @click="onClickButton"
        >
          同意する
        </button>
      </div>
    </div>
  </div>
</template>
```

```ts
<script lang="ts">
import { defineComponent, ref, onMounted } from "vue";

export default defineComponent({
  setup() {
    const termOfUseRef = ref<HTMLIFrameElement>();
    const isDisabled = ref<boolean>(true);

    const onClickButton = () => alert("同意しました！");

    onMounted(() => {
      termOfUseRef.value?.contentWindow?.addEventListener("scroll", () => {
        if (!termOfUseRef.value) return;
        if (!termOfUseRef.value.contentWindow) return;
        const scrollAdjustmentValue = 50;
        // テキスト全体の高さ
        const scrollHeight =
          termOfUseRef.value.contentDocument?.documentElement.scrollHeight || 0;
        // スクロール量
        const scrollAmount = termOfUseRef.value.contentWindow.scrollY || 0;
        // ウィンドウ（表示される枠）の高さ
        const windowHeight = termOfUseRef.value.contentWindow.innerHeight || 0;

        if (
          scrollAmount + windowHeight + scrollAdjustmentValue >
          scrollHeight
        ) {
          isDisabled.value = false;
        }
      });
    });

    return {
      termOfUseRef,
      isDisabled,
      onClickButton,
    };
  },
});
</script>
```

```css
<style scoped>
.container {
  display: flex;
  justify-content: center;
  width: 100vw;
  height: 100vh;
}
.content {
  margin-top: 50px;
  margin-bottom: 50px;
}
.term-of-use {
  width: 500px;
  height: 500px;
}
.button-container {
  display: flex;
  justify-content: center;
  margin-top: 20px;
}
.button {
  width: 200px;
  color: #fff;
  border-radius: 100vh;
}
.color-orange {
  background-color: orange;
}
.color-gray {
  background-color: gray;
}
</style>
```

# 解説

```html
<iframe
  class="term-of-use"
  ref="termOfUseRef"
  src="/term_of_use.html"
  frameborder="0"
/>
```

vueテンプレート内に`ref="termOfUseRef"`のようにテンプレート参照を付与します。

```ts
const termOfUseRef = ref<HTMLIFrameElement>();
```

setup関数内で参照の変数を定義します。このとき変数名はテンプレート内に記述した`ref="termOfUseRef"`と同じ名前にします。

```ts
onMounted(() => {
  termOfUseRef.value?.contentWindow?.addEventListener("scroll", () => {
    // 省略
  });
});
```

`onMounted`内でイベントリスナを呼び出しスクロールイベントを検知します。
ポイントは`contentWindow`の部分で、これによりiframe内のDOMにアクセスすることができます。そこでイベントリスナを呼び出すことでiframe内のスクロールを検知することができます。

https://developer.mozilla.org/ja/docs/Web/API/HTMLIFrameElement/contentWindow

```ts
const scrollAdjustmentValue = 50;
// テキスト全体の高さ
const scrollHeight =
  termOfUseRef.value.contentDocument?.documentElement.scrollHeight || 0;
// スクロール量
const scrollAmount = termOfUseRef.value.contentWindow.scrollY || 0;
// ウィンドウ（表示される枠）の高さ
const windowHeight = termOfUseRef.value.contentWindow.innerHeight || 0;

if (
  scrollAmount + windowHeight + scrollAdjustmentValue >
  scrollHeight
) {
  isDisabled.value = false;
}
```

最後にこちらのコードでスクロール量を計算し、下までスクロールした時にボタン活性化のフラグを更新します。

# 参考にさせていただきました
https://qiita.com/sinsia/items/54865025821cb9203cfc