ネイティブjavascript+cssでスムーススクロールもどきを実装する

きっかけ：画面トップへ戻るボタンを作る際に調べてみたら長いコードが出てきた
→指定位置までスクロールする機能は必要ないから短いの書こう
（「もどき」の理由は厳密にはスクロールしていないからです）
##コード
###html
```html:sample.html
<!DOCTYPE html>
<html>
<head>
	<title>サンプル</title>
	<link rel="stylesheet" href="sample.css">
	<script src="sample.js"></script>
</head>
<body>
	<div style="height: 25vh;background-color:#fcc;"></div>
	<div style="height: 25vh;background-color:#cfc;"></div>
	<div style="height: 25vh;background-color:#ccf;"></div>
	<div style="height: 25vh;background-color:#fcc;"></div>
	<div style="height: 25vh;background-color:#cfc;"></div>
	<div style="height: 25vh;background-color:#ccf;"></div>
	<button id="toTop" onclick="backToTop()">トップに戻る</button>
</body>
</html>
```
###css
```css:sample.css
body{
	overflow-y: scroll;
	width:100%;
	height: 100%;
	margin: 0;
	padding: 0;
	transition:1s;
}

body.fixed{
	position: fixed;
	left: 0;
	background-attachment : fixed;
}
```
###JavaScript
```javascript:sample.js
function backToTop(){
	let scrollPosition = window.pageYOffset;

	document.body.classList.add('fixed');
	document.body.setAttribute('style', 'top:-' + scrollPosition + 'px;');
	window.setTimeout(
		function(){
			document.body.setAttribute('style', 'top:0px');
		},0
	);
	window.setTimeout(
		function(){
			document.body.removeAttribute('class');
			document.body.removeAttribute('style');
		},1000
	);
}
```

##解説
まず挙動についてですが「スクロールして画面トップへ戻る」ではなく「トップへ戻った後でスクロールしているように見せる」が正確です。

```javascript:sample.js
	//スクロール量を取得
	let scrollPosition = window.pageYOffset;

	//bodyをfixedで固定
	document.body.classList.add('fixed');
	//topでスクロール量と同じだけbodyをずらす
	document.body.setAttribute('style', 'top:-' + scrollPosition + 'px;');
```

この1行目にてスクロール量を取得しています。
次に```body```に対して```display:fixed```が働くので強制的に画面トップに戻されます。
ただ、これだけだと一瞬で遷移してしまってスムースさの欠片もないので、画面が動かないように固定された```body```をスクロール量の分だけ上に動かすことで、見かけ上は動いていないように見せることができます。
（モーダルウィンドウを開いた際の背景固定と同様の手段です）

```javascript:sample.js
	//ずらしたbodyを一番上に戻す
	window.setTimeout(
		function(){
			document.body.setAttribute('style', 'top:0px');
		},10
	);
```

次に```top```の値を0に変更します。
この時```setTimeout()```を使って、遅延させることで```transition```が働かせています。
これによって```body```があたかもスクロールしているように見えます。
実際には固定された領域が動いているだけでスクロールはしていません。
何度も言いますが、画面トップへ戻るという目的は```body```に```display:fixed```を適用した時点で達成しています。

```javascript:sample.js
	//スクロール完了後、追加したクラス・インラインスタイルを削除する
	window.setTimeout(
		function(){
			document.body.removeAttribute('class');
			document.body.removeAttribute('style');
		},1000
	);
}
```

最後に一連の動作でつけたクラスとインラインスタイルを消去します。
こちらもアニメーションが終わるまでの間は実行されないように```transition:1s```と同じだけ遅延させます。

###メリット
・アニメーション部分をCSSに任せているのでややこしい計算が不要
・ease-inなどスクロール速度の調整も```transition-timing-function```で簡単に実装できる
・15行程度の簡単なことしかしていない分管理が楽

###デメリット
・指定位置までスクロールさせることはできない
・一瞬画面を固定にすることでスクロールバーが消え、**アニメーション中だけ横幅が変わる**ので```overflow-y:scroll```でスクロールバーを常時表示にしておく必要がある
（```::-webkit-scrollbar{display:none}```でも可
・背景画像を使っている場合調整が少し面倒

##おまけ(id位置までスクロールさせたい）
せっかくなのでid位置までスクロールさせられるようにしました。
最初からこっちだけ書けばよかった気もしますね。

###html
```html:omake.html
<!DOCTYPE html>
<html>
<head>
	<title>サンプル</title>
	<link rel="stylesheet" href="sample.css">
	<script src="omake.js"></script>
</head>
<body>
	<button id="toTop" onclick="backToTop(r1)">2つめの赤</button>
	<button id="toTop" onclick="backToTop(r2)">3つめの赤</button>
	<button id="toTop" onclick="backToTop(r3)">4つめの赤</button>
	<div style="height: 25vh;background-color:#fcc;"></div>
	<div style="height: 25vh;background-color:#cfc;"></div>
	<div style="height: 25vh;background-color:#ccf;"></div>
	<div id="r1" style="height: 25vh;background-color:#fcc;"></div>
	<div style="height: 25vh;background-color:#cfc;"></div>
	<div style="height: 25vh;background-color:#ccf;"></div>
	<div id="r2" style="height: 25vh;background-color:#fcc;"></div>
	<div style="height: 25vh;background-color:#cfc;"></div>
	<div style="height: 25vh;background-color:#ccf;"></div>
	<div id="r3" style="height: 25vh;background-color:#fcc;"></div>
	<div style="height: 25vh;background-color:#cfc;"></div>
	<div style="height: 25vh;background-color:#ccf;"></div>
	<button id="toTop" onclick="backToTop()">トップに戻る</button>
</body>
</html>
```

###JavaScript

```javascript:omake.js

//id位置まで飛べるスムーススクロール風
function backToTop(id){

	//現在位置を取得とbodyの高さ以上にスクロールしないよう制限
	let scrollPosition = window.pageYOffset;
	let scrollLimit = document.body.clientHeight - window.innerHeight;　//高さ制限 = bodyの高さ - 表示画面の高さ

		//bodyをfixedで固定
		document.body.classList.add('fixed');
		//topで現在表示位置までbodyをずらす
		document.body.setAttribute('style', 'top:-' + scrollPosition + 'px;');
		//ずらしたbodyを一番上に戻す
		window.setTimeout(
		//引数＝idの有無で動作を分ける
			function(){
				if(typeof id == "undefined"){ //引数がない場合は画面トップまで
					document.body.setAttribute('style', 'top:0px');

				}else if(typeof id == "object"){ //引数＝idがある場合はidの位置まで
					if (id.getBoundingClientRect().top > scrollLimit) {
						document.body.setAttribute('style', 'top:-' + scrollLimit + 'px');
					}else{
						document.body.setAttribute('style', 'top:-' + id.getBoundingClientRect().top + 'px');
					}
				}
			},10
		);
	window.setTimeout(
		function(){
			//スクロール完了後に追加したクラス・インラインスタイルを削除する
			document.body.removeAttribute('class');
			document.body.removeAttribute('style');
			//実際のidの位置までスクロールする
			if(typeof id == "object"){
				if (id.getBoundingClientRect().top > scrollLimit) {
					window.scrollTo(0, scrollLimit);
				}else{
					window.scrollTo(0, id.getBoundingClientRect().top);
				}
			}
		},1000
	);
}
```
###簡単な解説
やっていることはほぼ同じです。
```onclick='backToTop(id)'```と引数を渡すことでidまでスクロールするようにしただけです。
こちらは最後に```scrollTo()```を使っているのでかろうじてスムースクロールといってもいいかもしれません。
なお、引数を入れなければ画面トップに移動します。

こうなるとファンクション名が不適当すぎるので、使う際はちゃんと```goTo()```とかに変更してくださいね