(ネイティブJavaScript版) ページ内リンクをクリックでスクロール移動。
ライブラリーを使いたくなかったので、ネイティブJavaScript(Vanilla JavaScript)だけで、アンカーリンクでページ内の任意の位置にアニメーションスクロールの動作を実装する方法のメモ。
jQueryを使った方法は前の記事を参照。
- スクロールアニメーションの動きを描画する関数を作成。
- クリックイベントでアニメーション描画の関数をコールする関数を作成。
- アンカーリンクを持ったa要素にだけイベントリスナーを登録。
スクロールアニメーションを描画する関数を作成
実際にwindowのY座標を動かしてアニメーションの描画を行う関数を作成。
- 「動作開始時のY座標」と「移動先のY座標」を引数として受け取る。
- (動作開始からの経過時間 / 指定しているduration)で現在の動作の「経過割合」を算出。
- 「経過割合」と「easing」から「現在の移動距離」を算出。
- 「動作開始時のY座標」に「現在の移動距離」を加えて「現在のY座標」を算出。
- 算出した「現在の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)に渡して呼び出すの関数を作成。
- アンカータグの通常の機能を無効化。
- クリックされたa要素のhref属性を取得。
- windowの「現在のY座標」を取得。
- 「移動先のHTML要素のY座標」を取得。
- 取得した各「Y座標」を引数にして「アニメーション描画の関数」を実行。
※ イベントリスナーでコールされた関数は、発生したイベントに関する情報をプロパティーとして保持しているイベントオブジェクトを引数として受け取る。eventオブジェクト自体は、すべてのイベントで共通のプロパティーやメソッドを持つ。
※ フラグメント識別子が「#」1文字だけだった場合、hashプロパティーは「
空文字)を返す。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要素にイベントリスナーを登録する。
- DOMから、href属性に「#」を付与しているa要素をすべて取得。
- 取得した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);
});
- ネイティブのJavaScriptで汎用的なスムーススクロールを実装する
- 意外と簡単!jQueryを使わずにJavaScriptだけでスムーススクロールを実装する(IE11以上版)
- 精度の高いタイムスタンプを取得する (performance.now())
- Javascript で高さ幅とか
- [JavaScript] ページの横幅、高さ、スクロール量を取得する
- JavaScript の call( ) メソッドを雑に説明
- forEachをIE11で使う方法
- 配列風オブジェクトをArray.from()で本物の配列へ変換。(配列とかおれおれAdvent Calendar2018 – 05日目)
- ネイティブJavaScriptによるDOM操作の概要と各種基本操作
- あまり知られてなさそうなメソッド element.scrollIntoView()
https://memo.ag2works.tokyo/post-1614/
(ネイティブJavaScript版) ページ内リンクをクリックでスクロール移動。 | memo メモ [AG2WORKS]
<a href="https://memo.ag2works.tokyo/post-1614/" target="_blank" rel="noopener">(ネイティブJavaScript版) ページ内リンクをクリックでスクロール移動。 | memo メモ [AG2WORKS]</a>
この記事へのコメント
コメントの書き込みはまだありません。