ページがどこまで読まれているのかを計測するにあたり、ページ内におけるスクロール率ではなく、各セクションの先頭が表示されたかで計測したい場合のサンプルコードです。
確認環境
- Windows 10
- Chrome 73.0
- InternetExplorer 11
用件定義
- スクロールで素通りしている場合は対象外
- 各セクションの先頭位置が、画面の上から90%の範囲に来たらなんやかやする(今回はコンソールにidを出力するだけ)
- なんやかやは1度だけでよい
完成コード
<section id="Section1" class="js_target">
<h2>見出し1</h2>
<p>
本文
</p>
</section>
<section id="Section2" class="js_target">
<h2>見出し2</h2>
<p>
本文
</p>
</section>
<section id="Section3" class="js_target">
<h2>見出し3</h2>
<p>
本文
</p>
</section>
const setObserving = (element) => {
let timer = null;
window.addEventListener('scroll', function checkView(e) {
clearTimeout(timer);
timer = setTimeout(() => {
const domRect = element.getBoundingClientRect();
const scope = window.innerHeight * 0.9;
if (domRect.top < 0 || domRect.top > scope) {
return;
}
console.log('読み始めた!', element.id);
window.removeEventListener('scroll', checkView);
}, 200);
},false);
// 初期表示時にもイベント発火
const triggerEvent = new Event('scroll');
window.dispatchEvent(triggerEvent);
};
const sectionList = document.getElementsByClassName('js_target');
[...sectionList].forEach(setObserving);
スクロール停止時のみ処理を実行する
スクロールイベントは、スクロールしている間中絶えず発生します。
スクロールが止まったタイミングで1回だけ計測処理を実行するには、setTimeout
内に計測処理を記述して開始を遅延させます。
スクロール中はすぐに次のイベントが発生するので、タイマーは解除され再び設定されます。
これによりスクロール中は計測処理は実行されません。
let timer = null;
window.addEventListener('scroll', function checkView(e) {
// 予約取り消し
clearTimeout(timer);
// 200ミリ秒後に実行予約
timer = setTimeout(() => {
// 処理
}, 200);
},false);
一度だけイベント処理を実行する
イベントのハンドリング自体を一度だけにしたい場合は、オプションでonce:true
を指定します。(初期値false
)
この場合、一度でも登録したイベントリスナーが呼び出されたら、そのままリスナーは削除されます。
targetElement.addEventListener([eventType], (event) => {
}, { once: true });
今回はイベントリスナーの削除に条件があるため、イベントリスナー内でremoveEventListener()
を呼び出しています。
// イベントリスナーを名前付き関数で定義
window.addEventListener('scroll', function checkView(e) {
// リスナー内で関数への参照をremoveEventListenerに渡す
window.removeEventListener('scroll', checkView);
});
EventTarget.addEventListener() | MDN
EventTarget.removeEventListener | MDN
イベントを強制的に発火させる
ページ表示時は基本的にスクロールイベントは発火しないので、その時点での表示状況を計測するために、イベントを強制的に発火させます。
// scrollイベントオブジェクトを生成
const triggerEvent = new Event('scroll');
// 作成したオブジェクトを用いて、対象DOM要素でイベントを発火させる
window.dispatchEvent(triggerEvent);
イベントの作成と発火 | MDN
Event() | MDN
EventTarget.dispatchEvent() | MDN
各セクションごとにスクロールの監視を行う
計測処理実行時、全てのセクションの表示位置を点検するのではなく、各セクションごとに自身が規定の範囲に位置しているかを確認するようにしています。
セクションの数だけイベントリスナーを登録することになりますが、スクロール中はタイマーのオンオフしかしていないので、さほど負荷にはならないかと思います。
const sectionList = document.getElementsByClassName('js_target');
// 取得したHTMLCollectionをスプレッド構文で配列に変換
[...sectionList].forEach((element) => {
window.addEventListener('scroll', function checkView(e) {
// 各セクション要素を参照する処理
}, false);
});
要素の表示位置の取得
セクションの先頭部分が、ビューポートに対してどの位置にあるかはgetBoundingClientRect()
メソッドで取得できます。
戻り値のDOMRect
オブジェクトは、ビューポート左上を基準とした要素の位置情報が格納されています。
DOMRect.top
が0
よりも小さい時、要素の左上はビューポートの左上よりも上部に位置しています。
DOMRect.top
がビューポートの高さよりも大きい時、要素の左上はビューポートの左下よりも下部に位置しています。
// ビューポートに対する要素の位置を取得する
const domRect = element.getBoundingClientRect();
// 上から90%の高さを取得
const scope = window.innerHeight * 0.9;
// 要素上辺が画面上辺より上 または 下辺から10%より下
if (domRect.top < 0 || domRect.top > scope) {
return;
}
Element.getBoundingClientRect() | MDN
jQueryプラグインにしてみる(es5)
jQueryプラグインでも作ってみました。es5の構文で記述してあるので、IE11でもそのまま利用できます。
計測範囲および判定後の処理をオプションで指定できます。
(function ($) {
/**
* スクロールを監視する
* @param {String|Number} top 画面上辺を基準値とした監視範囲の上辺
* @param {String|Number} bottom 画面上辺を基準値とした監視範囲の下辺
* @param {Function} callBack 監視範囲に対象コンテンツが表示された場合に実行する処理
*/
$.fn.observeScroll = function (options) {
var settings = $.extend({
top: 0,
bottom: '100%',
callBack: function () { }
}, options);
// 判定基準の取得
function getScope(settingsProp, winH) {
var num = parseFloat(settingsProp);
// %指定以外はpx指定とみなす
if (/^\d+%$/.test(settingsProp)) {
return winH * num * 0.01;
} else {
return num;
}
}
var $win = $(window);
this.each(function (index, element) {
// イベントのネームスペースを設定
var eventName = 'scroll.observe' + index;
var timer = null;
$win.on(eventName, function (e) {
clearTimeout(timer);
timer = setTimeout(function () {
var elementTop = element.getBoundingClientRect().top;
var winH = window.innerHeight;
var scopeTop = getScope(settings.top, winH);
var scopeBottom = getScope(settings.bottom, winH);
if (elementTop < scopeTop || elementTop > scopeBottom) {
return;
}
settings.callBack(element, e);
// ネームスペースを指定してリスナーを削除
$win.off(eventName);
}, 200);
});
});
$win.trigger('scroll');
return this;
};
}(jQuery));
// ビューポート上辺より-50pxから、ビューポート中央までの範囲で指定
$('.js_target').observeScroll({
top: '-50px',
bottom: '50%',
callBack: function (element) {
console.log('読み始めた!', element.id);
}
});