かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

ある要素までスクロールしたらアクションをさせるJavascriptを作成する時に気をつけること。

画面をスクロールさせて、特定の要素の場所に来た時にアニメーションをさせるなどスクロールに応じたアクションの作成をすることがあります。
複雑なものの場合ライブラリを使うのが手っ取り早いと思いますが、簡易なものの時や大きなライブラリを読み込ませるのもなぁ〜って場合独自実装したりしますが、その際に気をつけることのメモです。

例えば下記のように単純にスクロール量が要素の位置にくると動作をさせるといった実装をしたとします。

var $w = $(window),
    $target = $('#js-target-element'),
    actionPos = $target.offset().top,
    timer;

// スクロールの監視
$w.on('scroll.myScrollAction', function(){
  clearTimeout(timer);
  timer = setTimeout(function() {
    // スクロール量
    var st = $w.scrollTop();
    // 動作させる要素の位置までスクロールされている場合はアクション
    if( st >= actionPos ) {
      // $target をアニメーションさせるとかの処理をココに記述
      targetActionStart();
      // スクロールの監視を停止
      $w.off('scroll.myScrollAction');
    }
  }, 100);
});

しかし、これでは要素が画面の下の方にあったり、サイトの高さが無いとか縦長のモニターでスクロール量が足りないとか、そもそもスクロールが無いとかでアクションが実行されないといったバグを出してしまう可能性があります。 f:id:kikiki-kiki:20160819175604p:plain

画面にどれだけスクロール量があるのかを事前に取得してアクション開始の条件を変更する

このバグを回避するには、表示されている画面にどれだけのスクロール量があるのかを事前に取得して、スクロール量が要素のアクションを起こす作業まで足りない時や、スクロールが無い時には直ぐにアクションを実行させるとかの処理をすればOKです。

ページのスクロール可能な量の取得

ページのスクロール可能な量は下記で取得が可能です。

スクロール可能な量 = ドキュメント全体の高さ - 表示エリアの高さ

また、下図のように上の計算で求められたスクロール可能な量0以下の時はスクロールが発生していないと判定することができます。
f:id:kikiki-kiki:20160819203400p:plain

ドキュメント全体の高さbody.clientHeight表示エリアの高さhtml.clientHeightで取得が可能のようです。
ただ、スクロール可能な量がマイナス値を求める必要は無いので、ドキュメント全体の高さはスクロール量を含めた高さを返すhtml.scrollHeightを使えば、

  1. ドキュメントの高さが表示エリアの高さより大きい場合はスクロールを含めたbody.clientHeightと同じ値で、スクロール量は正の値
  2. ドキュメントの高さが表示エリアの高さより小さい場合は表示されているエリアの高さhtml.clientHeightと同じ値となり、ドキュメントの高さがどれだけ小さいかに関わらず、スクロール可能な量の計算結果は常に0にする事ができます。

[参考] 結局「下からのスクロール位置」を取得するにはどうすればいいのか - Qiita

スクロール可能な量を返す関数

使い勝手が良いように、関数化します。

/**
 * html.scrollHeight = $('body')[0].scrollHeight
 * html.clientHeight = $('html').height(), $('body').height(), $('body')[0].offsetHeight
 */
var getPageScrollAmount = function() {
  var html = window.document.documentElement;
  return html.scrollHeight - html.clientHeight;
};

スクロール量が足りない場合でも動作するようにする

ページ全体のスクロール可能な量(高さ)動作させたい要素の位置(高さ)を比較して、動作させたい要素の位置(高さ)の方が下にある(値が大きい)場合は、動作を開始する位置を変更するようにします。

var $w = $(window),
    $target = $('#js-target-element'),
    targetPos = $target.offset().top,
    actionPos,
    timer;

// スクロール可能な量 を返す関数
var getPageScrollAmount = function() {
  var html = window.document.documentElement;
  return html.scrollHeight - html.clientHeight;
};

// アクションを開始する位置を調整する関数
var adjustActionPos = function() {
  var pageScrollAmount = getPageScrollAmount(),
     pos = targetPos;
  
  if( pageScrollAmount <= 0 ) {
    // スクロールがない時
    pos = 0;
  } else if( actionPos > pageScrollAmount ) {
    // スクロール量が足りない時
    pos = pageScrollAmount - 100; // ex ページ最下部より100px上の位置にする
  }
  return pos;
};

actionPos = adjustActionPos();

// スクロールの監視
$w.on('scroll.myScrollAction', function(){
  clearTimeout(timer);
  timer = setTimeout(function() {
    // スクロール量
    var st = $w.scrollTop();
    // 動作させる要素の位置までスクロールされている場合はアクション
    if( st >= actionPos ) {
      // $target をアニメーションさせるとかの処理をココに記述
      targetActionStart();
      // スクロールの監視を停止
      $w.off('scroll.myScrollAction');
    }
  }, 100);
})
.on('resize.myResize', function() {
  // 画面サイズが変更された時、アクション開始位置を調整し直す。
  actionPos = adjustActionPos();
});
// triggerでスクロールアクションを実行させてスクロールが無い時は直ぐにアクションを実行させる
$w.trigger('scroll.myScrollAction');

こんな感じで、スクロールがない時やスクロール量が足りない時に、アクションが実行されないといったバグが解消しました。
例によって古いIEとか色んなブラウザでの動作チェックしていません。
また、説明のしやすさを重視しているので、変数の定義の仕方とかイケてないので参考にされる場合は適時変更してください。

久々にjavascriptの記事書いた気がします。(だが、jQuery...


[参考]

オブジェクト指向JavaScriptの原則

オブジェクト指向JavaScriptの原則