0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ここのえAdvent Calendar 2023

Day 19

IntersectionObserverを使いやすくしたい

Last updated at Posted at 2023-12-18

この記事は ここのえ Advent Calendar 2023 Day 19の記事です。

Intersection Observer すぐ必要になりがち

Webフロントエンドを書いていると、どうしても付きまとうのが「スクロールに応じて特定の動きをしたい」というヤツです。

これに対応するためよく使うのが、Intersection Observerです。ターゲットとなる要素が表示されたとき(=ビューポートと交差した時)に、特定のアクションを呼び出すことができます。

ホームページを作る際にあまりにも頻繁に使うため、Typescriptベースでクラスにまとめて使いまわしています。

今回は threshold を用いた表示領域に応じての操作などは行っていないため、単純に表示された・消えた時にイベントが発生するシンプルな構成になっています。

実装

全体を先に出しておきます。
Typescriptを使っているため、JSの場合は読み替えて下さい。

export class AppearEvent<T extends () => any> {
  el: HTMLElement
  isIntersecting: boolean
  observer: IntersectionObserver
  appearCallback: T[]
  disappearCallback: T[]
  mode: 'forward' | 'backward' | 'both'

  constructor(el: HTMLElement, mode?: 'forward' | 'backward' | 'both') {
    this.el = el
    this.isIntersecting = false
    this.observer = new IntersectionObserver(this.intersect.bind(this))
    this.observer.observe(el)
    this.appearCallback = []
    this.disappearCallback = []
    this.mode = mode === undefined ? 'both' : mode
  }

  intersect(entry: IntersectionObserverEntry[]) {
    if (entry[0].isIntersecting) {
      if (
        this.mode === 'both' ||
        (this.mode === 'forward' && entry[0].boundingClientRect.y > 0) ||
        (this.mode === 'backward' && entry[0].boundingClientRect.y <= 0)
      )
        this.appeared()
    } else {
      this.disappeared()
    }
    this.isIntersecting = entry[0].isIntersecting
  }

  addAppearEvent(callback: T) {
    this.appearCallback.push(callback)
  }

  clearAppearEvents() {
    this.appearCallback = []
  }

  addDisappearEvent(callback: T) {
    this.disappearCallback.push(callback)
  }

  clearDisappearEvents() {
    this.disappearCallback = []
  }

  clearEvents() {
    this.clearAppearEvents()
    this.clearDisappearEvents()
  }

  appeared() {
    for (const callback of this.appearCallback) {
      callback()
    }
  }

  disappeared() {
    for (const callback of this.disappearCallback) {
      callback()
    }
  }
}

解説

大前提として、Intersection Observerのイベントが起きるタイミングは「ビューポートに対して指定した要素が交差した時」です。
言い換えれば、要素が画面に表示・非表示になったタイミングで発生するとも言えます。

クラスのフィールドはよく使うものを置いています。

el: HTMLElement
isIntersecting: boolean
observer: IntersectionObserver
appearCallback: T[]
disappearCallback: T[]
mode: 'forward' | 'backward' | 'both'

コールバックを複数実装できるようにするため、appearCallback[]disappearCallback[]を定義しておき、addAppearEvent(callback: T) で追加できるようにしています。

isIntersectingについてはIntersectionObserverEntry.isIntersecting の値をそのまま取ってきて、クラス側に保持することでいつでも参照しやすいようにしています。

modeはスクロールイベントの発生条件を定義しており、特定の方向からスクロールされた時のみ発生させるか、どちらでも発生させるか判断するために使用しています。

ここで実装上問題になるのが、「上下方向どちらからスクロールされた?」という判定です。
IntersectionObserverはあくまで画面と交差したタイミングでコールバックが呼び出されるだけなので、その判定については自前で実装する必要があります。

intersect(entry: IntersectionObserverEntry[]) {
  if (entry[0].isIntersecting) {
    if (
      this.mode === 'both' ||
      (this.mode === 'forward' && entry[0].boundingClientRect.y > 0) ||
      (this.mode === 'backward' && entry[0].boundingClientRect.y <= 0)
    )
      this.appeared()
  } else {
    this.disappeared()
  }
  this.isIntersecting = entry[0].isIntersecting
}

今回の実装では、InsersectionObserverEntry.boudingClientRect を使って対象のy座標を特定し、それを判断材料に使用しています。

このboudingClientRectは、getBoudingRectで計算される値と同一のものです。オブジェクトの型としてはDOMRectReadOnly型が返ってきますが、DOMRectReadOnly.yでオブジェクトのy座標を取得できます。

要はDOM要素の一番上のy座標が返ってくるのですが、この時の値は viewport に対しての相対値になるので、上から下のスクロールでは y はビューポートのheightに近い値に、下から上のスクロールでは上にはみ出している分だけマイナスの値が返ってきます。

# 下スクロール
DOMRectReadOnly {x: 0, y: 561.8125, width: 1146.375, height: 660.015625, top: 561.8125, …}

# 上スクロール
DOMRectReadOnly {x: 0, y: -659.09375, width: 1146.375, height: 660.015625, top: -659.09375, …}

これを利用して特定のスクロール向きのみに動作するモード(forward, backward)を設定した際に、それぞれyの値が満たしているかを判断しています。

対して disappear については実装をシンプルにしており、IntersectionObserverEntry.isIntersecting をチェックするだけにしています。

まとめ

Typescriptベースでライブラリ依存せず実装しているので、VueでもReactでも使えると思います。

ちなみにサイト側では「コールバックにanime.jsの処理を書いておき、IntersectionObserverでアニメーションを開始する」といった用途で使用しています。

IntersectionObserverは高機能ですがちょっと取り扱いづらいので、ちょっとした用途なら雑に使えるようにまとめとくと楽だよね、という話でした。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?