背景
webページの一部のパーツがブラウザのスクロールに付いてくるようにしたい
例えばECサイトによくある、購入ボタンをブラウザのスクロールに追尾させて、
ユーザーがコンテンツのどこにを見ていても即座に商品をカートに入れれるようにしたい。
僕.「cssだけでできる?」
G .「position: stickyはどう」
僕.「いいやんけこれ」
僕.「IEもpolyfillあるし」
僕.「なんかうまいこといかん」
G .「親要素にoverflow(style)が使ってると動かんよ」
僕.「ぐぬぬ」
僕.「overflowいっぱい使ってるなあ・・・」
僕.「overflow外して良い?」
デ.「だめ」
僕.「ぐぬぬ」
※Gはgoogle検索
※デはデザイナーさん
僕.「ではscriptで対応するか・・・。」
G .「これなんか参考にならんか? あとこれとか」
僕.「JQuery使ってるやつばっかやん。」
僕.「うちJQuery使ってないねん。」
僕.「あー。あとriot使ってるんでそっちで対応したい」
G .「該当ありません」
僕.「ぐぬぬ」
条件
- ページの一部のコンテンツがブラウザスクロールについてくる
- 特定の部分まできたらスクロールが止まる(フッターの上までとか)
- riotjs使っているのでパーツをriotのtagで用意したい
- IEでも動く
- jQueryは使わない
今回も上記縛りから色々苦労した。
実装方法
スクロールに合わせて動くやつ
<scroller>
<div id="start_position"></div>
<div id="scroller">
スクローラー
</div>
<style>
div#scroller {
background-color: #af0101;
height: 100px;
position: fixed;
}
</style>
<script>
let tag = this;
tag.stop_line = 0;
tag.stop_line_top = 0;
tag.scroll_element = null;
tag.on('mount', function() {
tag.getStopLine();
});
tag.on('updated', function() {
tag.getStopLine();
});
window.onscroll = function () {
// 現在のスクロール位置のトップ
let scroll_top = window.pageYOffset || document.documentElement.scrollTop;
if (scroll_top >= tag.stop_line) {
// スクローラーが止めるラインに達したら固定する
tag.scroller.style.position = "absolute";
tag.scroller.style.top = tag.stop_line_top + 'px';
} else {
// スクローラーが止めるラインより上ならば動かす
tag.scroller.style.position = "fixed";
tag.scroller.style.top = "auto";
}
}
tag.getStopLine = function() {
tag.scroller = document.getElementById('scroller')
let scroll_top = window.pageYOffset || document.documentElement.scrollTop;
// スクローラーの高さを計算
let scroller_height = parseFloat(document.defaultView.getComputedStyle(tag.scroller, null).height);
// スクローラーの初期の基準位置となるtopを、マウント時のスクロール位置を考慮して計算
// マウント時にスクロール位置により基準がずれるので同一基準の不変のエレメントを用意する
let start_position_top = document.getElementById('start_position').getBoundingClientRect().top + scroll_top;
// ストッパーからエレメントの止まる位置を、マウント時のスクロール位置を考慮して計算
let stopper_top = document.getElementById(tag.opts.stoper_id).getBoundingClientRect().top + scroll_top;
// どのくらい上で止めるか(px)
let padding = tag.opts.padding
tag.stop_line_top = stopper_top - padding - scroller_height
tag.stop_line = tag.stop_line_top - start_position_top
}
</script>
</scroller>
端的に言うと、scrollerのstyle「position」を「fixed」と「absolute」の二つを計算して更新しているだけです。
コメントにfixedだと動く、ってなるのがなんか逆な感じ受けますが、画面に対して絶対位置でfixedしている感じなので、
スクロールに対しては動いてるように見える、ってことです
呼びかた
<scroll-field>
<div class="content">
コンテンツ領域1
<scroller stoper_id="stopper" padding="0"></scroller>
</div>
<div id="stopper">
ストッパー
</div>
<div class="content2">
コンテンツ領域2
</div>
<style>
div.content {
background-color: #99cc00;
height: 1000px;
}
div#stopper {
background-color: #216a94;
height: 30px;
}
div.content2 {
background-color: #e4b9b9;
height: 1000px;
}
</style>
</scroll-field>
コンテンツ領域の1と2はスクロールの幅を持たしてストッパーで止まるかを確認するために用意した。
要はscrollerを止めたい要素のidとどのくらい上かの距離(padding)を渡してやってマウントすれば良いだけ。
今回は呼ぶところもriotのtagだったけど別にhtmlから読んでも良い。
なお、riotのマウント方法などは割愛する。
ポイント説明
start_position
わざわざ同位置にエレメント用意してやっているのは、画面ロード時にスクロール済みのパターンを考慮しています。
計算がずれます。
tag.on('updated', function()
何かしらのDom更新が走って領域に変化が起きた時に再計算するようにしています
なくても動きます。
雑感
なんか不毛な感じも。
もっと考えて設計しておけば必要なかった気もする。