【JS】Intersection Observerでスクロールとナビを連動させてみよう
概要
現在、とある案件でナビをクリックするとページ内リンクでスクロールするようにしているが、普通にスクロールしてアンカーリンクのコンテンツが表示されたらナビゲーションも連動して背景色を変えてみようっていう仕組み。
今回のDEMOはこちら
これまではscrollイベントが主流
これまでは下記のようなscrollイベントでの設定が一般的だった。
window.addEventListener("scroll", () => {
const sroll = window.pageYOffset;
if (srollVal > contentsPosition) {
// フェードアニメーションを呼び出す
fadeAnime();
}
});
// スクロールイベント
$(window).scroll(function() {
// フェードアニメーションを呼び出す
let scroll = $(window).scrollTop();
fadeAnime();
});
ただこの方法だと、スクロールがいつも発生しているから場合によって処理が重くなる。
Intersection Observer API
そこで代わりになりそうなIntersection Observer APIというのがある。
詳細は以下のサイトで説明されてるが、一部引用すると下記のように要素が交差した時にコールバック関数が呼び出されるので、最適化することができる。
交差オブザーバー API では、監視したい要素が他の要素(またはビューポート)に入ったり出たりしたとき、あるいは両者が交差する量が要求された量だけ変化したときに実行されるコールバック関数をコードに登録することができます。この方法により、サイトはこの種の要素の交差を監視するためにメインスレッドで何もする必要がなくなり、ブラウザーは自由に交差の管理を最適化することができます。
実装
実際にDEMOのように実装する内容は以下。
よくあるナビゲーションをクリックするとスムーズスクロールでページ内リンクするページとする。
スクロールしてコンテンツがウィンドウの中央あたりに表示、、つまり交差したら該当するナビゲーションにactive
クラスを付与して背景色を設定して現在を示す。
またせっかくなので画像の切り替えもやってみるが仕組みはナビゲーションと同様active
を付与する。
CSSに関しては見た目の話なので割愛する。
html
<!--ナビゲーション-->
<nav id="js-nav" class="nav">
<ul class="nav__list">
<li><a href="#content1">コンテンツ1</a></li>
<li><a href="#content2">コンテンツ2</a></li>
<li><a href="#content3">コンテンツ3</a></li>
<li><a href="#content4">コンテンツ4</a></li>
</ul>
</nav>
<!--切り替え用の画像-->
<div id="js-photo" class="photo">
<p>写真をフェードイン</p>
<div class="inner">
<img class="photo__img content1" src="./images/photo1.jpg" alt="">
<img class="photo__img content2" src="./images/photo2.jpg" alt="">
<img class="photo__img content3" src="./images/photo3.jpg" alt="">
<img class="photo__img content4" src="./images/photo4.jpg" alt="">
</div>
</div>
<!--監視対象のターゲット-->
<article class="scroll">
<div id="content1" class="scroll__target">
<h1>コンテンツ1</h1>
<p>このブロックがスクロールのターゲットとなる。</p>
</div>
<div id="content2" class="scroll__target">
<h1>コンテンツ2</h1>
<p>このブロックがスクロールのターゲットとなる。</p>
</div>
<div id="content3" class="scroll__target">
<h1>コンテンツ3</h1>
<p>このブロックがスクロールのターゲットとなる。</p>
</div>
<div id="content4" class="scroll__target">
<h1>コンテンツ4</h1>
<p>このブロックがスクロールのターゲットとなる。</p>
</div>
</article>
idとclassを設定する
スクロールの監視対象とするスクロールのコンテンツにはscroll__target
というクラスを付与する。またアンカーリンクのidを設定しておく。
ナビゲーションと画像にはアンカーリンクのidをクラス名として設定しておく。
こちらのクラスを名でどのコンテンツと連動させるか決めている。
JavaScript
実際のJavaScriptのコードは以下。
Intersection Observerは交差した際の処理以外は決まり文句のような書き方と感じるので、余計な処理を省いたソースコードを見たほうが理解できるかも。
Intersection Observerの全体像
まずは、Intersection Observerの全体像として以下のようにテンプレのような感じ。
//コールバック関数
const callback = (entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
//要素が交差した
} else {
//交差から外れた
}
})
}
//オプション
const options = { rootMargin: '-50% 0px' }
//IntersectionObserverのインスタンス生成
let observer = new IntersectionObserver(callback, options)
//監視要素を取得して監視登録
let targets = document.querySelectorAll('.scroll__target');
targets.forEach(target => {
observer.observe(target);
});
コールバック関数
IntersectionObserverのインスタンスの引数にはコールバック関数とオプションを指定できる。
実際に監視対象の要素はentries
に入っているので、isIntersecting
で交差したかどうかをチェックできる。
true
の場合は交差しているので、交差した場合の処理、、今回でいえばactive
クラスの付与という処理となる。(後述)
オプション
オプションは省略しているがroot
というオプションで交差する要素を指定できる
root
に要素を指定すると監視対象の領域とすることができます。
デフォルトはブラウザのビューポートがでnull
と指定できるが、ビューポートの場合は省略ができる。
rootMargin
は交差する領域を指定する。
例えばウィンドウの中央に設定したい場合は以下のように"-50%"を指定すると中央になる。
なかなか理解しずらいので参考サイトを見てほしい。
const options = {
root:document.querySelector('.wrapper'),
rootMargin: '-50% 0px'
}
IntersectionObserverのインスタンス生成
IntersectionObserverのインスタンス生成して引数にコールバック関数とオプションを指定する。
監視要素を取得してobserveに登録する
一番最後にquerySelectorAll
で監視対象の要素を取得して、ループでIntersectionObserverのインスタンスにあるobserve
メソッドに登録する。
実際のソースコード
上記を踏まえて上で実際のコード。
IntersectionObserverの説明してるので、交差した際の処理。
//コールバック関数
const callback = (entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
//すでにアクティブになっているものが0個の時(=null)以外は、activeクラスを除去
const currentActiveImg = document.querySelector("#js-photo .active");
if (currentActiveImg !== null) {
currentActiveImg.classList.remove("active");
}
let targetId = entry.target.id;
let imgElement = document.querySelector(`.${targetId}`);
if (imgElement) {
imgElement.classList.add("active");
}
// すでにアクティブになっているものが0個の時(=null)以外は、activeクラスを除去
const currentActiveIndex = document.querySelector("#js-nav .active");
if (currentActiveIndex !== null) {
currentActiveIndex.classList.remove("active");
}
// 引数で渡されたDOMが飛び先のaタグを選択し、activeクラスを付与
const newActiveIndex = document.querySelector(`#js-nav a[href='#${entry.target.id}']`);
newActiveIndex.classList.add("active");
} else {
if (entry.target.id === 'content1') {
let getActiveIndex = document.querySelector("#js-nav .active");
let getActiveImg = document.querySelector("#js-photo .active");
if (getActiveIndex !== null) {
//active削除
getActiveIndex.classList.remove("active");
}
}
});
}
//オプション
const options = {
root: null, // ビューポートをルートとする
rootMargin: "-50% 0px", // ビューポートの中心を判定基準にする
}
// IntersectionObserverオブジェクトを作成
let observer = new IntersectionObserver(callback, options);
// 監視したい要素を取得
let targets = document.querySelectorAll('.scroll__target');
targets.forEach(target => {
if (target) {
observer.observe(target);
} else {
console.log('target element not found');
}
});
すでにアクティブになっているものがある場合は、activeクラスを除去
ソースコードを抜粋。
画像とナビゲーションにactive
クラスの削除と付与は同じロジック。
const currentActiveImg = document.querySelector("#js-photo .active");
if (currentActiveImg !== null) {
currentActiveImg.classList.remove("active");
}
基本的にactive
クラスは1つしかついてないはずなので、qureySelector
でactive
クラスがある要素を取得。
もし、存在するのであればクラスを削除。
let targetId = entry.target.id;
let imgElement = document.querySelector(`.${targetId}`);
if (imgElement) {
imgElement.classList.add("active");
}
そのあとに監視しているコンテンツ要素からidを取得したいので、entry.target.id
でid属性にアクセスできるので、画像要素にもid名と同じクラス名を設定しているので、querySelector
で取得して、active
クラスを設定する。
ナビゲーションの場合はhref
属性にid名と同じ#content1
などアンカー名を設定している要素をquerySelector
で取得してactive
クラスを付与する。
// 引数で渡されたDOMが飛び先のaタグを選択し、activeクラスを付与
const newActiveIndex = document.querySelector(`#js-nav a[href='#${entry.target.id}']`);
newActiveIndex.classList.add("active");
監視している領域から外れた際の処理について
基本的に監視対象に入った際の処理でまずはactive
クラスを持っている要素からactive
クラスを削除しているが、なぜか最初のコンテンツ要素がスクロールダウン(ページ上部に戻る)で交差対象から外れた際にクラスが外れなかったので以下の処理を追加した。
if (entry.target.id === 'content1') {
let getActiveIndex = document.querySelector("#js-nav .active");
let getActiveImg = document.querySelector("#js-photo .active");
if (getActiveIndex !== null) {
//active削除
getActiveIndex.classList.remove("active");
}
}
すごく限定的だが、一番最初に交差するコンテンツのidがcontent1
なので交差から外れたコンテンツのidをentry.target.id
で取得して、一番最初の要素がもしactive
クラスを持っていたら、削除するという処理。
まとめ
これまで、scroll
イベントで処理していたが、細かい交差する境界線の設定はscroll
イベントで各要素のtop
要素などを取得したほうが便利そうかも。
ただ総合的に、処理速度はこちらのほうがいいかも。
以下のサイトを参考にしました。
Discussion