この記事は ここのえ 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は高機能ですがちょっと取り扱いづらいので、ちょっとした用途なら雑に使えるようにまとめとくと楽だよね、という話でした。
参考