jQueryとcssで縦長メニューを画面内に収める（スクロール＆追随）

## こんな画面でサイドメニューがものすごく縦長に…
![scrollmenu.PNG](https://qiita-image-store.s3.amazonaws.com/0/86958/d63ab861-1923-f411-0292-fa479635d437.png)
メニューが多くなってなんだかやたらと縦長になってしまったときに。
まぁ、アコーディオン化するライブラリとか使えばいいかもしれませんが、自分で実装してみました。

## メニューのスクロールONとスクロールバーのデザイン
まずは`menu`部分の高さを表示高に固定し、画面からはみ出たらスクロールバーを出現させます。
ついでにスクロールバーを色々デザインしたいところですが、ブラウザによって可否はそれぞれでした。

```表示高に合わせる.js
$(function(){
	//メニューエリアの高さを調整
	function adjustMenuHeight() {
		$('#menu').height($(window).height());
	}

	$(window).on('load resize', function() {
		adjustMenuHeight();
	});
});
```

```スクロールバー.css
#menu {
	overflow: auto; /* スクロールバー必要なら表示 */
	-ms-overflow-style: -ms-autohiding-scrollbar; /* IE用：スクロールバー自動非表示 */
}

/* Chrome,Safari用：スクロールバーデザイン */
#menu::-webkit-scrollbar {
	width: 4px;
	-webkit-border-radius: 3px;
	border-radius: 3px;
}
#menu::-webkit-scrollbar-thumb {
	background: #abcdef;
	-webkit-border-radius: 3px;
	border-radius: 3px;
}
/* FireFoxはデザインできないらしい */
```

Chromeはwebkitで色々好みに合わせたデザインができました。
IEは自動非表示ぐらいでしょうか。
FireFoxはデザインできないようです…。

（参考）
[CSSでスクロールバーをカスタマイズ](https://qiita.com/hibikikudo/items/ee1b913479bceddb654c)
[スクロールさせる領域だけどスクロールバーは非表示にしたい。](https://qiita.com/naru0504/items/ff0c77775223dc5a9148)
[-ms-overflow-style](https://developer.mozilla.org/en-US/docs/Web/CSS/-ms-overflow-style)

## コンテンツ側のスクロールがあるので…
さて、`contents`側が長くなるページもあるのでその場合はメニューを追随させて常に画面に表示させるようにしたいと思います。

```ついてくるメニュー.js
$(function(){
	//コンテンツスクロールに追随するメニュー
	var fix = $('#menu'); //固定する要素
	var main = $('#contents'); //固定する要素を収める範囲
	var sideTop = fix.offset().top;
	var fixTop = fix.offset().top;
	var mainTop = main.offset().top;
	$(window).on('scroll', function() {
		fixTop = fix.css('position') === 'static' ? sideTop + fix.position().top : fixTop;
		var fixHeight = fix.outerHeight(true);
		var mainHeight = main.outerHeight();
		var winTop = $(window).scrollTop();
		if (winTop + fixHeight > mainTop + mainHeight) {
			fix.css({
				position: 'absolute',
				top: mainHeight - fixHeight
			});
		} else if (winTop >= fixTop) {
			fix.css({
				position: 'fixed',
				top: 0
			});
		} else {
			fix.css('position', 'static');
		}
	});
});
```

ここはもうほぼ参考サイトのまんまです…
[サイドバーをある範囲内で固定してスクロールに追尾させるjQuery](https://smileworks.biz/blog/jquery/1866)

## ヘッダとフッタがあった
`header`と`footer`があって、`contents`のスクロール状態によって画面上に出現したり見えなくなったりするのでそれも考慮してメニューの高さを決める必要がありました。

上で書いた`menu`の高さを求める箇所をいじります。

```高さ可変.js
$(function(){
	//メニューエリアの高さを調整
	function adjustMenuHeight() {
		//header,footerが表示されているかで変動
		var headerPos = $('#header').offset().top + $('#header').height();
		var footerPos = $('#footer').offset().top - $(window).height();
		var headerH = 0;
		var footerH = 0;
		//headerが見えている場合
		if ($(window).scrollTop() < headerPos) {
			headerH = $('#header').height() - $(window).scrollTop();
		}
		//footerが見えている場合
		if ($(window).scrollTop() > footerPos) {
			footerH = $(window).scrollTop() - footerPos;
		}
		var h = $(window).height() - headerH - footerH;
		$('#menu').height(h);
	}
});
```

また、`contents`のスクロールに合わせて`header`・`footer`の表示を判断するため、先ほどの`$(window).on('scroll', function() {...}`の終わりでもこの関数を呼びます。

（参考）
[【jQuery】スクロールする要素内で指定要素が表示状態かを判定する](https://qiita.com/bgn_nakazato/items/0ba30c044e5db3b46439)

## 選択したメニューの位置に移動させたい
ついでに、`menu`を選択するとページ読み込みが走って`menu`は常に先頭の位置が表示されているのも不親切なので、選択したメニューの位置まで自動スクロールするようにします。

なお、「選択したメニュー」を取る方法は色々あると思うので割愛しますが（class付与してるとか？ 自分のケースでは`background-color`だけが判別可能な要素でした…）、ポイントは以下です。

```選択位置まで自動スクロール.js
$(function(){
	$(window).on('load', function() {
		$('#menu').animate({scrollTop:$(選択メニュー).offset().top - $('#menu').offset().top});
	});
});
```

（参考）
[たった1行！jQueryで指定箇所までアニメーションつきで自動スクロールさせる方法](https://qiita.com/yamaguchi_takashi/items/edce735e825631993a74)

## jsまとめ

```menu.js
$(function(){
	//メニューエリアの高さを調整
	function adjustMenuHeight() {
		//header,footerが表示されているかで変動
		var headerPos = $('#header').offset().top + $('#header').height();
		var footerPos = $('#footer').offset().top - $(window).height();
		var headerH = 0;
		var footerH = 0;
		//headerが見えている場合
		if ($(window).scrollTop() < headerPos) {
			headerH = $('#header').height() - $(window).scrollTop();
		}
		//footerが見えている場合
		if ($(window).scrollTop() > footerPos) {
			footerH = $(window).scrollTop() - footerPos;
		}
		var h = $(window).height() - headerH - footerH;
		$('#menu').height(h);
	}

	//コンテンツスクロールに追随するメニュー
	var fix = $('#menu'); //固定する要素
	var main = $('#contents'); //固定する要素を収める範囲
	var sideTop = fix.offset().top;
	var fixTop = fix.offset().top;
	var mainTop = main.offset().top;
	$(window).on('scroll', function() {
		fixTop = fix.css('position') === 'static' ? sideTop + fix.position().top : fixTop;
		var fixHeight = fix.outerHeight(true);
		var mainHeight = main.outerHeight();
		var winTop = $(window).scrollTop();
		if (winTop + fixHeight > mainTop + mainHeight) {
			fix.css({
				position: 'absolute',
				top: mainHeight - fixHeight
			});
		} else if (winTop >= fixTop) {
			fix.css({
				position: 'fixed',
				top: 0
			});
		} else {
			fix.css('position', 'static');
		}

		adjustMenuHeight();
	});

	$(window).on('load resize', function() {
		adjustMenuHeight();
	});
	$(window).on('load', function() {
		//選択メニューへスクロール移動
		$('#menu').animate({scrollTop:$(選択メニュー).offset().top - $('#menu').offset().top});
	});
});
```

## 20180510追記：コンテンツの横スクロールがある場合
`contents`部分が縦だけでなく横にも長くて横スクロールがある場合（これはこれでダサいというのは別議論として）、
このままだと横スクロールしても`menu`は画面左に固定されたままです。
ここでは横スクロールの場合は`menu`も横に（画面外に）スクロールさせてみます。

```縦は固定で横にはスクロール.js
	$(window).on('scroll', function() {
		......
		if (winTop + fixHeight > mainTop + mainHeight) {
			......
		} else if (winTop >= fixTop) {
			//Left位置を取得
			var mainScrollLeft = parseFloat($(window).scrollLeft()) * -1;
			fix.css({
				position: 'fixed',
				top: 0,
				left: mainScrollLeft
			});
		} else {
			......
		}
		......
	});

```

fixのときにleftをセットしてあげます。
marginなど計算する必要があれば計算します。（ここではしていませんが、そのためにparseしています）

（参考）
[fixedの要素も横方向にはスクロールして欲しいという想い](http://kimizuka.hatenablog.com/entry/2016/01/24/162756)

以上です。