#035
posted on 2021.04.22 (Thu) 2022.01.14 (Fri)

(ネイティブJavaScript版) ページ内リンクをクリックでスクロール移動。

ライブラリーを使いたくなかったので、ネイティブJavaScript(Vanilla JavaScript)だけで、アンカーリンクでページ内の任意の位置にアニメーションスクロールの動作を実装する方法のメモ。

 

jQueryを使った方法は前の記事を参照。

 

  1. スクロールアニメーションの動きを描画する関数を作成。
  2. クリックイベントでアニメーション描画の関数をコールする関数を作成。
  3. アンカーリンクを持ったa要素にだけイベントリスナーを登録。

 

スクロールアニメーションを描画する関数を作成

実際にwindowのY座標を動かしてアニメーションの描画を行う関数を作成。

  1. 「動作開始時のY座標」と「移動先のY座標」を引数として受け取る。
  2. (動作開始からの経過時間 / 指定しているduration)で現在の動作の「経過割合」を算出。
  3. 「経過割合」と「easing」から「現在の移動距離」を算出。
  4. 「動作開始時のY座標」に「現在の移動距離」を加えて「現在のY座標」を算出。
  5. 算出した「現在のY座標」でwindowを描画。

※ easingは、動作の経過割合をx、移動距離をyとして線形代数で実装。

※ アニメーション動作は、windowオブジェクトが持つrequestAnimationFrame()メソッドで実装。

※ ブラウザ上で実行されるJavaScriptではwindowオブジェクトはグローバルオブジェクトなので、「window.メソッド()」の「window.」は省略でき、省略した方が僅かに速くなる(速度未検証)。

//durationとeasingの設定
const ag2setteings = {
	duration: 1000, //ミリ秒
	easeLinear: function(x){ //easingにLinearを指定する場合
		return x;
	},
	easeOutExpo: function(x){ //easingにeaseOutExpoを指定する場合
		return x === 1 ? 1 : 1 - Math.pow(2, -10 * x);
	}
};

//アニメーション停止用の変数
let scrollId = null;

//第1引数は開始時のY座標, 第2引数は移動先のY座標
const ag2scroller = function(a, b){
	//前回の呼び出しが動作中の場合はキャンセルして停止させる
	cancelAnimationFrame(scrollId);
	//初期値
	let startTime = performance.now(), // 開始時間
		startPos = a, // 開始時Y座標
		endPos = b, // 移動先Y座標
		distance = b - a, // 総移動距離
		scrollDuration = ag2setteings.duration, // duration
		scrollEasing = ag2setteings.easeOutExpo; // easing

	//スクロールの動きを描画する関数
	let runScroll = function(){
		let elapsedTime = performance.now() - startTime; //経過時間
		//durationの時間を超えたら描画を終了して移動先Y座標へ補正
		if(elapsedTime > scrollDuration){
			cancelAnimationFrame(scrollId);
			scrollTo(0, endPos);
			return;
		}
		//durationの時間を超えていなければ描画の処理
		let progress = elapsedTime / scrollDuration; //動作時間の経過の割合
		//現在の経過割合での移動距離にあるY座標
		let movePos = startPos + distance * scrollEasing(progress);
		//windowを算出したY座標にジャンプ
		scrollTo(0, movePos);

		//自身を呼び出して再描写の処理
		scrollId = requestAnimationFrame(runScroll);
	};

	//最初の描画処理を実行と実行したアニメーションのIDを保持
	scrollId = requestAnimationFrame(runScroll);
};

Math.pow(底, 指数) : 指定の底と指定の指数の累乗の値を返す。

cancelAnimationFrame(ID) : 呼び出し時に指定のIDを返しているrequestAnimationFrame()メソッドでスケジュールされたフレームアニメーションのリクエストをキャンセルする。

performance.now() : そのコンテキストが生成された時点からの経過時間を返す。Date.now()メソッドよりも精度が高い。

scrollTo(X座標, Y座標) : 指定の座標に移動する。

requestAnimationFrame(関数) : ブラウザが次のフレームを再描画をする前に指定した関数をコールして実行する。コールバックリスト内のエントリーを一意に識別するための、倍精度整数値のrequestIDを返す。このコールバックの回数は大抵毎秒60回で、ブラウザタブや該当要素が非表示の場合は自動的に低減される。

 

 

アニメーション描画の関数を呼び出す関数を作成

クリックイベントでコールされたら「現在のY座標」と「移動先のY座標」を算出し、アニメーションの描画を実行する関数(上記のag2scroller)に渡して呼び出すの関数を作成。

  1. アンカータグの通常の機能を無効化。
  2. クリックされたa要素のhref属性を取得。
  3. windowの「現在のY座標」を取得。
  4. 「移動先のHTML要素のY座標」を取得。
  5. 取得した各「Y座標」を引数にして「アニメーション描画の関数」を実行。

※ イベントリスナーでコールされた関数は、発生したイベントに関する情報をプロパティーとして保持しているイベントオブジェクトを引数として受け取る。eventオブジェクト自体は、すべてのイベントで共通のプロパティーやメソッドを持つ。

※ フラグメント識別子が「#」1文字だけだった場合、hashプロパティーは「empty string」(空文字)を返す。

const ag2anchorLink = function(e){
	//アンカーの機能を無効化
	event.preventDefault();

	//アンカーリンク(フラグメント識別子)取得
	let href = e.target.hash;

	//hrefが#のみか指定idの要素が無い場合はfalseを返して動作終了
	if(href === '' || !document.querySelector(href)) return false;

	//移動先のHTML要素を取得
	let target = document.querySelector(href);

	//windowの現在のY座標(スクロール量)を取得(IE8も対応するならscrollTop)
	let currentY = pageYOffset !== undefined ? pageYOffset : document.documentElement.scrollTop;

	//ブラウザ左上を基準点とした移動先要素の相対座標
	let targetRect = target.getBoundingClientRect();
	//document左上を基準点とした相対座標(サイト内での絶対座標)に変換
	//ブラウザ相対座標に現在のスクロール量を加算
	let targetTop = targetRect.top + currentY;

	//移動先Y座標の最低値 (ドキュメントの最上部)
	if(targetTop < 0) targetTop = 0;
	//移動先Y座標の最高値 (ドキュメントの最底部 - ウィンドウの高さ)
	if(targetTop > document.documentElement.scrollHeight - document.documentElement.clientHeight) targetTop = document.documentElement.scrollHeight - document.documentElement.clientHeight;

	//スクロールアニメーション描画の関数を実行
	ag2scroller(currentY, targetTop);
};

event.preventDefault() : eventオブジェクトのメソッド。発生したイベントの規定の動作を行わない。イベントの伝播は止めない。

event.target : イベントを発生させたオブジェクトへの参照。オブジェクトはその情報をプロパティーに持つ。

document.querySelector(‘セレクター’) : 指定したCSSセレクターを取得する。最初に一致したHTMLElementオブジェクトだけを返す。無ければ「null」を返す。

window.pageYOffset : 読み取り専用プロパティー。原点から垂直方向にスクロールした量をピクセル数で表す倍精度浮動小数点値を返す。(「scrollY」のエイリアスで、「scrollY」ではなく「pageYOffset」ならIE9以降に対応。)

左辺 || 右辺 : 論理和の論理演算子。左辺が「true」なら左辺、「false」なら右辺を返す。

document.documentElement : 読み取り専用。ドキュメントのルート要素を返す。空ではないHTML文章の場合は常にhtml要素を返す。

要素.scrollTop : 要素の中身が垂直方向にスクロールした量をピクセル数で取得または設定する。(スクロールバーが発生しない要素の場合、値は「0」。)

要素.getBoundingClientRect() : 指定した要素の寸法と、そのビューポート(ブラウザのウィンドウ表示領域)の左上からの相対位置を返す。読み取り専用の「left, top, right, bottom, x, y, width, height」のプロパティーを持つ。

要素.scrollHeight : 読み取り専用プロパティー。ブラウザの画面上からはみ出している部分も含めた、要素の中身の幅の寸法。指定した要素すべてをスクロールバーを使用せずに表示するのに必要なビューポート(ブラウザのウィンドウ表示領域)の最低限の高さに等しい。(指定した要素のpaddingは含めるが、borderとmarginは含まない。)

要素.clientHeight : 読み取り専用プロパティー。要素の内側の寸法をピクセル単位で表す。(指定した要素のpaddingは含めるが、borderとmarginは含まない。) ルート要素(html要素、または文書が後方互換モードである場合はbody要素)に使用された場合、ビューポート(ブラウザのウィンドウ表示領域)の高さが返される。

 

 

クリック対象のa要素を取得してイベントリスナーを登録

動作を開始する関数(上記のag2anchorLink)をクリックイベントでコールするように、クリック対象となるa要素にイベントリスナーを登録する。

  1. DOMから、href属性に「#」を付与しているa要素をすべて取得。
  2. 取得したa要素にクリックのイベントリスナーを登録。

 

イベントリスナーを登録

取得したすべてのa要素にイベントリスナーを登録するには、for文かforEach()メソッドを使う。

※ for文の方が処理速度は速い。(速度は未検証。)

※ IE11とEdgeではNodelistオブジェクトにforEach()メソッドを使えないので、call()メソッドを使って組み込み関数のArrayオブジェクトのprototypeを参照して処理する。もしくは、取得したNodelistオブジェクトからArrayオブジェクトを作成してから処理する。(IEとEdgeを無視するならこの対処をせず、「Nodelistオブジェクト.forEach(‘関数’)」で動作する。)

 

(1) for文でイベントリスナーを登録する場合。

//対象セレクターのNodelistを取得
const anchorLinks = document.querySelectorAll('a[href^="#"]');

const anchorLinksLength = anchorLinks.length;
for(let i = 0; i < anchorLinksLength; i++){
  anchorLinks[i].addEventListener('click', ag2anchorLink);
};

(2) Arrayオブジェクトのprototypeを参照してcallメソッドでイベントリスナーを登録する場合。

Array.prototype.forEach.call(anchorLinks, function(t){
  t.addEventListener('click', ag2anchorLink);
});

(3) Nodelistオブジェクトを配列オブジェクトに変換してから登録する場合。

const anchorLinksArr = Array.from(anchorLinks);
anchorLinksArr.forEach(function(t){
  t.addEventListener('click', ag2anchorLink);
});

document.querySelectorAll(‘セレクター’) : 指定したセレクターのNodelistオブジェクトをDOMから取得する。

Nodelistオブジェクト.length : Nodelist内のitemの数を返す。

配列.forEach(‘関数’) : 配列の各要素の先頭から順に一度ずつコールバック関数を実行する。

オブジェクト.メソッド.call(‘オブジェクト’, ‘関数’) : あるオブジェクトに所属する関数やメソッドを、第1引数で指定したオブジェクトに割り当てて呼び出し、呼び出したメソッドを実行して第2引数で指定したコールバック関数を実行する。

対象要素.addEventListener(‘イベントのタイプ’, ‘関数’, ‘イベント伝播順’) : 対象要素に指定のイベントでコールする関数を指定して登録。第3引数(初期値 : false)でイベントの伝播する方向を指定できる。falseでDOM階層の下位から上位に伝播。

Array.from(‘オブジェクト’) : 指定した配列風オブジェクトまたは反復可能オブジェクトからArrayインスタンスを生成。

 

 

scrollIntoView()メソッドを使う方法

上記の方法とは別に、scrollIntoView()メソッドを使えば、要素の位置情報やrequestAnimationFrame()メソッドでのアニメーション描画などが不要となり、下記のコードだけで実装できるが、現状、Firefox、Chrome、Edgeのみが対応している。

対応状況はCan I Useを参照。

const ag2anchorSIV = function(e){
	event.preventDefault();
	let href = e.target.hash;
	if(href === '') return false;
	let target = document.querySelector(href);
	if(!target) return false;

	target.scrollIntoView({
		behavior: 'smooth', //スクロール方法
		block: 'start', //移動先要素の垂直方向のどの位置に行くか
		inline: 'start' //水平方向の位置
	});
};
Array.from(document.querySelectorAll('a[href^="#"]')).forEach(function(t){
  t.addEventListener('click', ag2anchorSIV);
});

 

 

この記事のURL

https://memo.ag2works.tokyo/post-1614/

Copyコピー
この記事のタイトル

(ネイティブJavaScript版) ページ内リンクをクリックでスクロール移動。 | memo メモ [AG2WORKS]

Copyコピー
この記事のリンクタグ

<a href="https://memo.ag2works.tokyo/post-1614/" target="_blank" rel="noopener">(ネイティブJavaScript版) ページ内リンクをクリックでスクロール移動。 | memo メモ [AG2WORKS]</a>

Copyコピー
※ フィールドをクリックでコピーするテキストの編集ができます。

この記事へのコメント

コメントの書き込みはまだありません。

  • コメント内のタグはエスケープ処理され、文字列として出力されます。
  • セキュリティーのため、投稿者のIPアドレスは取得されます。
  • 管理者が内容を不適切と判断したコメントは削除されます。
  • このフォームにはスパム対策として、Googleの提供するreCAPTCHAシステムが導入されています。
    (Google Privacy Policy and Terms of Service.)