Webページ内で各セクションが表示されたかを計測するためのサンプルコード

ページがどこまで読まれているのかを計測するにあたり、ページ内におけるスクロール率ではなく、各セクションの先頭が表示されたかで計測したい場合のサンプルコードです。

# 確認環境

- Windows 10
- Chrome 73.0
- InternetExplorer 11

# 用件定義

- スクロールで素通りしている場合は対象外
- 各セクションの先頭位置が、画面の上から90％の範囲に来たらなんやかやする（今回はコンソールにidを出力するだけ）
- なんやかやは1度だけでよい

# 完成コード

```html:HTML例
<section id="Section1" class="js_target">
  <h2>見出し１</h2>
  <p>
    本文
  </p>
</section>
<section id="Section2" class="js_target">
  <h2>見出し２</h2>
  <p>
    本文
  </p>
</section>
<section id="Section3" class="js_target">
  <h2>見出し３</h2>
  <p>
    本文
  </p>
</section>
```

```js:スクロールを監視する処理
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`内に計測処理を記述して開始を遅延させます。
スクロール中はすぐに次のイベントが発生するので、タイマーは解除され再び設定されます。
これによりスクロール中は計測処理は実行されません。

```js
let timer = null;
window.addEventListener('scroll', function checkView(e) {
  // 予約取り消し
  clearTimeout(timer);
  // 200ミリ秒後に実行予約
  timer = setTimeout(() => {
    // 処理
  }, 200);
},false);
```

# 一度だけイベント処理を実行する

イベントのハンドリング自体を一度だけにしたい場合は、オプションで`once:true`を指定します。(初期値`false`)
この場合、一度でも登録したイベントリスナーが呼び出されたら、そのままリスナーは削除されます。

```js
targetElement.addEventListener([eventType], (event) => {

}, { once: true });
```

今回はイベントリスナーの削除に条件があるため、イベントリスナー内で`removeEventListener()`を呼び出しています。

```js
// イベントリスナーを名前付き関数で定義
window.addEventListener('scroll', function checkView(e) {
  // リスナー内で関数への参照をremoveEventListenerに渡す
  window.removeEventListener('scroll', checkView);
});
```

[EventTarget.addEventListener() | MDN](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener)
[EventTarget.removeEventListener | MDN](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/removeEventListener)

# イベントを強制的に発火させる

ページ表示時は基本的にスクロールイベントは発火しないので、その時点での表示状況を計測するために、イベントを強制的に発火させます。

```js
// scrollイベントオブジェクトを生成
const triggerEvent = new Event('scroll');
// 作成したオブジェクトを用いて、対象DOM要素でイベントを発火させる
window.dispatchEvent(triggerEvent);
```

[イベントの作成と発火 | MDN](https://developer.mozilla.org/ja/docs/Web/Guide/Events/Creating_and_triggering_events)
[Event() | MDN](https://developer.mozilla.org/ja/docs/Web/API/Event/Event)
[EventTarget.dispatchEvent() | MDN](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/dispatchEvent)

# 各セクションごとにスクロールの監視を行う

計測処理実行時、全てのセクションの表示位置を点検するのではなく、各セクションごとに自身が規定の範囲に位置しているかを確認するようにしています。
セクションの数だけイベントリスナーを登録することになりますが、スクロール中はタイマーのオンオフしかしていないので、さほど負荷にはならないかと思います。

```js
const sectionList = document.getElementsByClassName('js_target');
// 取得したHTMLCollectionをスプレッド構文で配列に変換
[...sectionList].forEach((element) => {

  window.addEventListener('scroll', function checkView(e) {

    // 各セクション要素を参照する処理

  }, false);
});
```

[スプレッド構文 |MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax)

# 要素の表示位置の取得

セクションの先頭部分が、ビューポートに対してどの位置にあるかは`getBoundingClientRect()`メソッドで取得できます。
戻り値の`DOMRect`オブジェクトは、ビューポート左上を基準とした要素の位置情報が格納されています。
`DOMRect.top`が`0`よりも小さい時、要素の左上はビューポートの左上よりも上部に位置しています。
`DOMRect.top`がビューポートの高さよりも大きい時、要素の左上はビューポートの左下よりも下部に位置しています。

```js
// ビューポートに対する要素の位置を取得する
const domRect = element.getBoundingClientRect();
// 上から90%の高さを取得
const scope = window.innerHeight * 0.9;
// 要素上辺が画面上辺より上　または　下辺から10%より下
if (domRect.top < 0 || domRect.top > scope) {
  return;
}
```

[Element.getBoundingClientRect() | MDN](https://developer.mozilla.org/ja/docs/Web/API/Element/getBoundingClientRect)

# jQueryプラグインにしてみる（es5)

jQueryプラグインでも作ってみました。es5の構文で記述してあるので、IE11でもそのまま利用できます。
計測範囲および判定後の処理をオプションで指定できます。

```js:プラグインの定義
(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));

```

```js:プラグインの利用
// ビューポート上辺より-50pxから、ビューポート中央までの範囲で指定
$('.js_target').observeScroll({
  top: '-50px',
  bottom: '50%',
  callBack: function (element) {
    console.log('読み始めた！', element.id);
  }
});
```

[How to Create a Basic Plugin](http://learn.jquery.com/plugins/basic-plugin-creation/)