マチダの生活

92026 10

【jQuery】ページ内スクロール(ページ遷移もOK)を本気出して解説してみた【js】

 ウェブページにおいて、同じページ内の任意の位置や、別ページの指定した場所に直接ジャンプするリンクを特に「アンカーリンク」と呼ぶ。目指す要素にidを付与し、リンク元のhref末尾にはそのidと同じワード を『#(ハッシュ)』付きでくっつけてやることで2者を紐づけるのだ。

 そしてそのリンクからジャンプする際、パッと画面が切り替わって移動するのではなく、クリックした位置から目的地まで滑り降りていくようなアニメーションをjQuery(もしくはJavaScript)で実装するのは、ウェブページ制作では定番の作業なのである。

 当マチダの生活にもその機能を付け加えるにあたって、いつもなあなあで使ってしまっているそのスクリプトを本気出して見直してみようかなと思って。

わかったつもりで使っているスクリプトを見直してスッキリしたい

 果たして、今までわたしが関わってきたいくつのサイトにこの機能を実装してきたのだろうか。正確な数はわからないが、そのほとんどの場合においてコピペ(とほんの少しのカスタマイズ)で済ませてきたように思う。じゃあそのコピペ元を一つ一つ遡っていって、オリジナルにたどり着いたところでそれもまた1から自分で作ったものではなく、たぶんどこかネットから拾ってきたスクリプトなのだろう。もう覚えてないけど。

 この機会にぜひとも全てをスッキリさせて、「これは自分でちゃんと理解して使っているスクリプトですよ」と胸を張って言えるようになりたいんだ。なので記事タイトルを今一度正確に書くとすれば「本気出して”自分のために”解説してみた」ということになるのである。

ページ内リンクのスクリプト解説

 ちなみにこの記事を書いている時点で件のスクロール機能は実装されている。以下のリンクをクリックみてほしい。

参考リンクページ内リンクだね
別ページへ遷移した場合だね(植物Part4 ガクアジサイへ)

 別ページへ遷移した場合にスクロールが2段階になっているのは、画像に遅延読み込み(Lazy Load)を適用している関係でズレが生じることへの対策で、やや不自然だが今のところこんな方法しか思いつかないのでしょうがない。

 始めにページ内リンクのスクリプトから解説していこう。

// ページ内スクロール
$(function() {
	$('a[href*="#"]').on('click', function (e) {
		var target = $(this.hash);
		if (!target.length) return;
		
		e.preventDefault();
		
		if (window.matchMedia('(min-width: 960px)').matches) {
			navHeight = $nav.outerHeight();
		}else{
			navHeight = 60;
		}
		
		var position = Math.max(
			target.offset().top - navHeight,
			0
		);
		
		$('html, body').stop().animate(
			{ scrollTop: position },
			500,
			'swing'
		);
		
		if (this.hash !== '#') {
			history.pushState(null, '', this.hash);
		}
	});
});

jQueryの決まり文句であるコレ

$(function() {
	//何らかの処理
});

 まずは一番基本的なところから。一番外側のコレは「HTMLの読み込みが全て終わったら中身の処理を実行してね」というイベントハンドラの一種である。やや専門的には「DOMツリーの構築が完了したタイミングで実行してね」と言い換えることもできる。

  • $() :jQueryの呼び出し
  • function() { ~ } :この中に処理したい具体的な内容を書く

クリックイベントハンドラ

$('a[href*="#"]').on('click', function (e) {
	//何らかの処理
});

 その直下にあるコレ。本体は【 on(‘click’, function (e) {~処理~}); 】であり、クリックすることで{~処理~}を開始するためのイベントハンドラだ。頭の【 $(‘a[href*=”#”]’) 】は発火する要素の指定で、文章にすると「aタグで、hrefの中に#が含まれるもの(【 *= 】がそういう意味のセレクタ)がクリックされたら{~処理~}をスタートせよ、ということになる。

 だったら先述の

$(function() {
	//何らかの処理
});

 これいらないんじゃね?と一瞬思ってしまうが、それを省いてしまうと、クリックハンドラが読み込まれたタイミングで対象となるaタグのDOMが構築されていない可能性が生まれてしまうので、やはりあった方が確実だろう。

ブラウザのデフォルト動作を止める

 少し飛ばして先に

e.preventDefault();

 の解説を済ませておきたい。

 クリックハンドラのfunction部分をよく見ると、eという引数を渡しているのがおわかりだろうか。

//これね
function (e)

 eはただの変数なのでabcでもiWillBeBackでもいいのだが、一応「Event Object」の頭文字として一般的にeが使われることが多い。この変数eにはクリックされたボタンの要素や座標、その際に押されたキーの情報など、そのイベントに関するさまざまな情報が詰まっている。

 そしてそのイベント情報の一つとして、ブラウザが元々持っているデフォルトの動作をキャンセルして止める「preventDefault()」という機能があるわけだ。

 ブラウザ本来の動作としては、ハッシュの付いたリンクをクリックすると該当IDの地点までジャンプしてしまうので、その動作をeのpreventDefault()で止めてからあらためてjQueryの機能でスクロールする、というのが全体の流れとなる。

ハッシュが存在するかどうかチェック

var target = $(this.hash);
if (!target.length) return;

 thisというのはまさにクリックされた要素そのもののことで、例えばそれが【 <a href=”/about/#hash”> 】だとすればこれがそのまま格納されているということになる。

 .hashは、そのthisがもつハッシュを取得するスクリプトなので、この場合は「#hash」の文字列を取得、すなわち$(this.hash)は$(‘#hash’)と同義であり、当該ページにスクロールの目標要素である【id=”hash”】がある場合は変数targetに代入される。続くif文はそれが無かった場合にreturnして処理を中断するためのものだ。

 まとめると【id=”hash”】が付与された要素がある場合は、スクロールの目標として変数targetに代入され、e.preventDefault();でブラウザの動作が止められたうえで、jQueryの処理が続いていくことになる。

ヘッダーのheightを取得

//この部分なんだけど
if (window.matchMedia('(min-width: 960px)').matches) {
	navHeight = $nav.outerHeight();
}else{
	navHeight = 60;
}

 当マチダの生活は固定ヘッダー制を採用しており、その高さを考慮にいれないとスクロールが意図した場所に着地しないので、それを調節するためのセクションだ。しかしページ内スクロールの本質とはあまり関係がない部分なので、詳しい説明は省くことにする。変数navHeightには固定ヘッダーのheight値が入っているということがわかればOK。

ページ上部にスクロール着地点があった場合の対処

var position = Math.max(
	target.offset().top - navHeight,
	0
);

 ここまでの処理でスクロール目標となる要素が代入されている変数target。続く【.offset().top】で、その要素がページ最上部からみてどれだけ離れているかをピクセル数で取得すれば、基本的にはその分だけスクロールすればいいということになるはずだ。
 
 しかし当サイトの場合、先ほど触れたようにヘッダーがposition:fixedで固定されているので、スクロール着地点と固定ヘッダーが重なり合わないようにヘッダーのheight分をスクロール値からマイナスしてやらなくてはならない。

//それがここまで
target.offset().top - navHeight

 じゃあその外側にあるのは何かというと。

//これね
Math.max()

 こいつは複数の数字のうち一番大きいものを返すmethodで、例えばMath.max(700, 0)なら700が返ってくる。そしてそれこそがスクロールすべき値として変数positionに代入されることになるわけだ。
 
 そんなパターンがこの先あるかどうかはわからないが、スクロールの目標地点がページのかなり上にある場合、要するにtarget.offset().topの値がめちゃめちゃ小さくなってしまうようなときに、そこからnavHeightが引かれて値がマイナスにならないようにするための措置である。ページの一番上が0なんですからね。それ未満になってもらってはなにかと不具合が起きかねない。

いよいよスクロール開始

$('html, body').stop().animate(
	{ scrollTop: position },500,'swing'
);

 先頭のこれ ⇒【 $(‘html, body’) 】

 スクロールのスクリプトにはつきものの書き方で、わたしも毎回脊髄反射のように使っていたがその意味は全くわかっていなかった。

 調べてみたら、ブラウザによってスクロールする際に<html>をスクロール要素として扱う場合と<body>をスクロール要素として扱う場合とがあるらしく、しかも、DOCTYPE、ブラウザバージョンなどによっても違いがあったそうな。昔は。だからとりあえず【 $(‘html, body’) 】こうしておけばどちらでもいけるね、みたいな。現在においてはこのように書く必要もないようだが、せっかく覚えたので今回はこうしておくとしよう。

.stop()

 何かの理由でスクロールが連打された際に処理がたまってしまうのを防ぐため、まずは止める。

.animate( { scrollTop: position },500,'swing' );

 それからアニメーションスタートだ。その中身であるところの【scrollTop】は縦方向にどれだけスクロールしているかを表す値。ページ最上部なら【scrollTop = 0】。100pxぶんスクロールしていれば【scrollTop = 100】。

 文章にすると「スクロール位置をposition値の場所に変更して。ただし一瞬で変更するんじゃなく、500msかけてアニメーションさせてね。そのアニメパターンはswingで」という意味になる。

URLにハッシュを残す

if (this.hash !== '#') {
	history.pushState(null, '', this.hash);
}

 e.preventDefault()でブラウザ本来の処理を止めてしまった副作用として、現段階ではURLに付与されるはずだったハッシュが付与されていない。このままではこうしたページ内遷移がブラウザの履歴に残らず、ブラウザバックした時に正しく戻ることが出来なくなってしまうので、最後の仕上げとして修正しておこう。

history

 これがブラウザの履歴を操作するためのオブジェクトだ。ここにpushState() で新しい履歴を1個追加してやる。その際にページを再読み込みしたり、別ページへ移動したりはしない。

history.pushState(null, '', this.hash);

 nullのところは一緒に保存しておきたいデータを指定する場所。その隣はtitleという引数で、昔からある仕様だが現在のブラウザでは基本的に使われないんだそう。一番終わりが大事なところで、this.hashとしておくとハッシュタグが付与されたリンクを履歴に加えることができる。

 以上!総まとめをしておくぞ。

 DOMの構築が終わるのをまつ ⇒ ハッシュの含まれるaタグをクリックすると処理発動 ⇒ そのハッシュと同じワードのID名を持つ要素があるか検索、あれば処理続行 ⇒ ブラウザのデフォルト動作を止める ⇒ 目標要素のoffset値を取得・調節してスクロールする値を決定 ⇒ その値をもとに目標地点までアニメーションスクロール ⇒ ハッシュ付きURLを履歴に加えて終了

別ページへ遷移した場合のスクリプト解説

 基本的な部分はほとんど同じなので、違う部分について補足するに留める。

// 別ページ遷移
$(function() {
	var urlHash = location.hash;
	if (!urlHash) return;
	
	var target = $(urlHash);
	if (!target.length) return;
	
	if (window.matchMedia('(min-width: 960px)').matches) {
		navHeight = $nav.outerHeight();
	}else{
		navHeight = 60;
	}
	
	$('html, body').scrollTop(0);
	
	var position = Math.max(
		target.offset().top - navHeight,
		0
	);
	
	$('html, body').animate(
		{ scrollTop: position },
		500,
		'swing'
	);
	
	// Lazy Loadによる位置ズレを補正
	setTimeout(function() {
		
		if (window.matchMedia('(min-width: 960px)').matches) {
			navHeight = $nav.outerHeight();
		}else{
			navHeight = 60;
		}
		
		var newPosition = Math.max(
			target.offset().top - navHeight,
			0
		);
		
		if (position !== newPosition) {
			$('html, body').animate(
				{ scrollTop: newPosition },
				300,
				'swing'
			);
		}
		
	}, 500);
	
});

URLにハッシュがあったら発動

var urlHash = location.hash;
if (!urlHash) return;

 ノーマルなページ内スクロールではクリックハンドラを利用したが、別ページに遷移したうえでスクロールする場合には、そのURLにハッシュが付いているかどうかという事実そのものをきっかけに使う。

 【location.hash】は現在ブラウザに表示されているURLについているハッシュを取得するものだ。あれば続行、無ければreturn。シンプルですな。

Lazy Load対策

 その他の処理はほとんど同じなのでまるっと割愛しつつ、一応こいつのことだけは軽く書いておこうかしらね。

// Lazy Loadによる位置ズレを補正
setTimeout(function() {
	
	if (window.matchMedia('(min-width: 960px)').matches) {
		navHeight = $nav.outerHeight();
	}else{
		navHeight = 60;
	}
	
	var newPosition = Math.max(
		target.offset().top - navHeight,
		0
	);
	
	if (position !== newPosition) {
		$('html, body').animate(
			{ scrollTop: newPosition },
			300,
			'swing'
		);
	}
	
}, 500);

 画像に遅延読み込みであるLazy Loadが適用されているせいで、スクロールするにつれ新たな画像が読み込まれてきちゃうのよ。そうするとその画像のぶんコンテンツの高さが増して、目標地点がずれちゃうの。

//そのためのこれ
setTimeout(function() {
	//何らかの処理
}, 500);

 setTimeout()は「指定した時間が経過した後に1回だけ処理を実行する」というタイマー機能で、ここでは最後の500(単位:ms)がその待ち時間にあたる。

 スクロールが開始されると、ページを滑り降りながらその先にある画像がどんどん遅延表示されていく。そして計算に必要な画像がある程度出そろったであろう500ms後、再びヘッダーの高さや目標地点までのスクロール値などを再計算し、そこからもう一度スクロールをぶちかますというかなり強引な手法なのだ。だってこれしか思いつかなかったんだもん。

 今度こそ本当に以上~!!

 いやぁ疲れた…こんな基本中の基本みたいなスクリプトなのに。もちろん相棒のロボ太(わたしのchat GPT)に教えてもらいつつ、自分でも調べて納得しつつというなかで、自分が今までいかに適当にやっていたかがよぉぉぉぉくわかってしまった。決してつまらない勉強ではなかったので、また機会を見つけてこういうのを一つ一つやっつけていければいいな。

おまけ ここがページ内リンクのジャンプ先です

 よくぞここまでおいで下さいました。お帰り下さい。

ライン
Articles

Web関連の記事TOP5

Tag cloud

タグクラウド

トップに戻る