少し前の記事ですが、JQuery Advent Calendar 2017 に寄稿させていただきます。
なにがしたい?
たとえばサイドバーに置いたローカルメニューや広告エリアなどの特定のエリアをスクロールしてもずっと表示しておく、というのをよく見かけます。
基本は「スクロールがその要素の位置を超えたら position を fixed に変更する」だけで考え方はとてもカンタンなのですが、これをそのままそれだけ実装するとどうなるか。1つは、fixed にしたことで親要素からの束縛から解放されてとても伸び伸びとした要素になってしまうことがあります。もう1つは、前後関係というしがらみからも開放されるため後続要素が詰められて位置が変わってしまうことがあります。
1つ目の問題には、予めCSSでフローティングした後のサイズを書いておく、という手法がメジャーですが……、これ、ちょとめんどくさい。というよりレスポンシブとかどうするんですか!? そのレスポンシブの位置関係がBootstrapなど外部ライブラリで定義されているともっとめんどくさいですよーー!
2つ目の問題は、例えばこれが、画面トップのNavbar的なものだと、BODY要素に padding-top を設定したり、BEFORE疑似セレクタで高さを確保させたりします。が、これってNavbar的なものにしか使えなくないですか? もっと色んな所に使いたーーい!
という問題を解決ようとJSを書いていたら、そこそこキレイにまとまったものができたので、良ければ使っていただきたい、というのが本校の趣旨です(๑•̀ㅂ•́)و✧
結論
出来上がったコードがコチラです。
<style>
.js-floating-floater.fixed { position: fixed; z-index: 100; }
</style>
<script>
$(function(){
$(window).scroll(fixedFloater);
$(window).resize(fixedFloater);
fixedFloater();
function fixedFloater(){
var header_height = 0; // 固定ヘッダがある場合、その高さ
$('.js-floating').each(function(){
$placeholder = $(this);
$floater = $(this).children('.js-floating-floater');
if( window.scrollY + header_height > $(this).offset().top ){
$floater.width( $placeholder.width() );
$placeholder.height( $floater.height() );
$floater.addClass('fixed').css('top',header_height+'px');
} else {
$floater.width( '' );
$placeholder.height( '' );
$floater.removeClass('fixed');
}
});
}
});
</script>
<div class="js-floating">
<div class="js-floating-floater">
フローティングメニュー
</div>
</div>
具体的にBootstrapで適用してみたコードは末尾に用意しますね。
解説
Placeholder を用意する
ポイントは、実際にフローティングするfloaterを、フローティングしない placeholder で囲っておくところです。floaterはfixedにしたとたん文字通り位置の制約から解き放たれてどこともなく羽ばたいていきコントロール困難になります。それをムリに追いかけようとするよりも、もともとの位置をマーキングしておくだけでコントロールがとても簡単になります。
Placeholderからは幅 Floaterから高さを取得して交換する
fixedされたfloaterは相対的な位置やサイズ情報を失います。そして、placeholderは中身が空っぽになり高さを失います。でも、placeholderはまだ幅を持っています。なのでこの幅をfloaterに与えます。幅を与えられたfloaterはそれに合わせて中身を整形し高さを取り戻します。なので、これをplaceholderに返します。こうすると、フローティングする前後で、placeholderもfloaterも、サイズ情報が変わらなくなります。
課題
まずはスッキリシンプルに実装してみよう!というところなので、いくつか未実装の課題があります。
- スクロールイベントで毎回サイズ調整をしている。ちょっと負荷が気になります。scrollとresizeで中身をちょっと変えれば解決される問題ですが、本稿の本筋とそれるので省略……。
- iOSの慣性スクロール中は位置が変更されない。touchmoveを使うとちょっと改善されますが理想的な動きにはなりません。どうも仕様のようで世界的な未解決問題。
- placeholderやfloaterにデフォルトの高さや幅が指定されているとクリアされてしまう。
$placeholder.height( '' );
とやっているところです。基本的にはplaceholderやfloaterには幅や高さを指定せず、placeholderの外側や、floaterの内側でなんとかする前提です。
Bootstrapに応用
実際にBootstrapの②カラムレイアウトに適用してみた例です。下記コードはHTMLもBODYも無いけど、そのままコピーしてindex.htmlとして保存すればブラウザで開きます。固定ヘッダの高さ分だけ下がったところにフローティングする小技も。
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
<style>
.js-floating-floater.fixed { position: fixed; z-index: 100; }
</style>
<script>
$(function(){
$(window).scroll(fixedFloater);
$(window).resize(fixedFloater);
fixedFloater();
function fixedFloater(){
var header_height = $('.navbar').outerHeight() + 15; // ここで固定メニューの高さ(と余白)を与えています
$('.js-floating').each(function(){
$placeholder = $(this);
$floater = $(this).children('.js-floating-floater');
if( window.scrollY + header_height >= $(this).offset().top ){
$floater.width( $placeholder.width() );
$placeholder.height( $floater.height() );
$floater.addClass('fixed').css('top',header_height+'px');
} else {
$floater.width( '' );
$placeholder.height( '' );
$floater.removeClass('fixed');
}
});
}
});
</script>
<style>
body { padding-top:70px; }
main { background: #eee; }
.menu { background: rgba(246,225,141,0.5); }
/* js-floating...はJS用のクラスなのでデザイン用に別クラスを用意するのがベター */
.floater { background: rgba(91,174,218,0.5); }
.placeholder { box-shadow: 10 10 10 rgba(0,0,0,0.2); }
</style>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</nav>
<div class="container">
<div class="row">
<main class="col-8 mb-4">
メインコンテンツ
<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
</main>
<aside class="col-4 mb-4">
<div class="menu mb-4">サイドメニュー<br><br><br><br></div>
<div class="js-floating floating_menu mb-4 placeholder">
<div class="js-floating-floater floater">
<!-- この中身はいろいろ自由にできます -->
フローティングメニュー
<br><br><br><br><br><br>
</div>
</div>
<div class="menu mb-4">サイドメニュー<br><br><br></div>
</aside>
感想・余談
ここまで書いてきてあれですが、BootstrapのプラグインでAffix Pluginというのがあるそうです……。
https://www.w3schools.com/bootstrap/bootstrap_affix.asp
最近、フローティングに限らず、この placeholder を置くというやりかたをよくするようになりました。その1つのパターン、でしょうか。