かもメモ

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

ある要素までスクロールしたらアクションをさせる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の原則

Javascript 連想配列(オブジェクト)をforEachでループさせたい。

いい加減配列のループにはArray#forEachを使っていきたいと思っています。
通常の配列であれば下記のような感じ。

var array = ['暁', '響', '雷', '電'];

array.forEach(function(val, i) {
  console.log(i, elmval; 
});
/* ↓ 出力
0 "暁"
1 "響"
2 "雷"
3 "電"
*/

連想配列をforEachでループさせる

しかし連想配列(オブジェクト)の場合

var obj = { first: '暁', second: '響', third: '雷', fourth: '電' };

obj.forEach(function(val, key) {
  console.log(key, val); 
}); // => Uncaught TypeError: obj.forEach is not a function

そのまま Object.forEach とするとエラーになってしまいます。

Object.keys() で連想配列のキーを配列にして利用する

連想配列(オブジェクト)をforEachで回すにはキーの配列を利用すればOK... なのだけど

var obj = { first: '暁', second: '響', third: '雷', fourth: '電' };

Object.keys(obj).forEach(function(key) {
  console.log(key, obj[key]);
});
/* ↓ 出力
first 暁
second 響
third 雷
fourth 電
*/

ループ内で obj[key] ってループの外にある変数を参照するのがイケてない...

forEachの第二引数にオブジェクトを渡してあげればOK!

array.forEach(callback[, thisObj]);
引数
callback ... 各要素に対して実行するコールバック関数で、3つの引数をとります。
thisObj ... 任意。callback 内で this として使用する値

Array.prototype.forEach() - JavaScript | MDN

どうやら、forEach()の第二引数に渡した値がループ内でthisとして使用できるようなので、第二引数にオブジェクトを渡すと良い感じになりそうです。

var obj = { first: '暁', second: '響', third: '雷', fourth: '電' };

// 第二引数に obj を渡す
Object.keys(obj).forEach(function(key) {
  var val = this[key]; // this は obj
  console.log(key, val);
}, obj);
/* ↓ 出力
first 暁
second 響
third 雷
fourth 電
*/

ループ内でループ外の変数を参照させること無く連想配列(オブジェクト)をforEach()でループさせることが出来ました!

 
今頃こんなエントリーを書いているのは、未だにES2015は疎か、ES5もちゃんと把握できてないって事...
そろそろヤバイ感あるぞ... (今後もまっとうなエンジニアとしてやっていくのなら)


[参考]

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

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

javascript 文字列中の文字を取得

文字列中のn番目の文字を取得したいとか、があるかもしれません。
javascriptの文字列ではlengthで文字数が測ることができ、配列のようにアクセスすると文字列の先頭から順番に文字を取得することができます。
需要あるんでしょうかね。まぁいいや。

var str = 'abcdefghijklmn';
// 文字数
cosole.log( str.length ); // 14
// 10番目の文字 ※ 0からカウントする
cosole.log( str[9] ); // "j"

文字列から1文字だけ取得したいとかの時はslice()より添字で取得してしまった方が簡単かもしれません。
文字列をオブジェクトのように扱えるのは、このようなアクセスをした時、一時的にnew String()されているかららしいです。

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

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