Bootstrap4 のタブ内から他のタブへスクロール位置を調整しつつ移動するアンカーリンクを貼ったら、思いの外シビアだったのでメモしておきます。
サンプルコード
例えば以下のようなコード。
<div class="tabWrapper py-3">
<ul class="nav nav-tabs nav-fill" role="tablist">
<li class="nav-item" role="presentation">
<a href="#lorem" id="tabLorem" class="nav-link active" role="tab" data-toggle="tab" aria-controls="lorem" aria-selected="true">Lorem Ipsum</a>
</li>
<li class="nav-item" role="presentation">
<a href="#etaoin" id="tabEtaoin" class="nav-link" role="tab" data-toggle="tab" aria-controls="etaoin" aria-selected="false">etaoin shrdlu</a>
</li>
<li class="nav-item" role="presentation">
<a href="#hujiko" id="tabHujiko" class="nav-link" role="tab" data-toggle="tab" aria-controls="hujiko" aria-selected="false">ふじこ</a>
</li>
</ul>
</div>
<div id="tabPanes" class="tab-content">
<div id="lorem" class="tab-pane show active" role="tabpanel" aria-labelledby="lorem-tab">
<!-- 略 -->
<p>他のタブへのアンカーリンクです。<a href="index.html#etaoin" class="tab_link mx-2" data-anchor="etaoin">こちら</a>をクリックしてみてください。</p>
</div>
<div id="etaoin" class="tab-pane" role="tabpanel" aria-labelledby="etaoin-tab">
<!-- 略 -->
<p>他のタブへのアンカーリンクです。<a href="index.html#hujiko" class="tab_link mx-2" data-anchor="hujiko">こちら</a>をクリックしてみてください。</p>
</div>
<div id="hujiko" class="tab-pane" role="tabpanel" aria-labelledby="hujiko-tab">
<!-- 略 -->
<p>他のタブへのアンカーリンクです。<a href="index.html#lorem" class="tab_link mx-2" data-anchor="lorem">こちら</a>をクリックしてみてください。</p>
</div>
</div>
このような感じで各々のタブから他のタブへのアンカーリンクを貼ります。
1つのページの中での遷移なので、 href
はダミーとして JS で表示・非表示を制御するようにします。
const tabChange = (screlm) => {
$('.tab_link').on('click', function (e) {
e.preventDefault();
const targetHref = $(this).attr('data-anchor');
// タブ表示
$(`.nav-item a[href="#${targetHref}"]`).tab('show');
return false;
});
};
$(window).on('load', function () {
tabChange(screlm);
});
このような感じに。普通に「アンカーリンクのクリックイベントを検出」して「対象タブ要素に .tab('show')
でタブ表示を強制」させているだけのシンプルなコードです。
スクロール位置の調整
さて、続いてスクロール位置を調整、つまり「アンカーリンクをクリックしたらタブペインの先頭にスクロール位置が行く(ただしスクロールアニメーションは不要)」ようにしようとしたのですが……。
const scrollElm = () => {
if ('scrollingElement' in document) {
return document.scrollingElement;
}
if (navigator.userAgent.indexOf('WebKit') !== -1) {
return document.body;
}
return document.documentElement;
};
const tabChange = (screlm) => {
$('.tab_link').on('click', function (e) {
e.preventDefault();
const targetHref = $(this).attr('data-anchor');
// 表示イベント後にアンカー位置へ瞬間移動
// shown.bs.tab イベントを拾わないと、非表示のタブは display: none; になっているので .offset().top で要素の位置が正しく取得できない
const targetTabId = `#tab${targetHref.charAt(0).toUpperCase()}${targetHref.slice(1)}`
$(targetTabId).on('shown.bs.tab', function () {
let position = Math.ceil($(`#${targetHref}`).offset().top);
$(screlm).animate({ scrollTop: position }, 0);
$(this).off('shown.bs.tab');
});
// タブ表示
$(`.nav-item a[href="#${targetHref}"]`).tab('show');
return false;
});
};
$(window).on('load', function () {
const screlm = scrollElm();
tabChange(screlm);
});
やろうとしたことは、
- 先程と同様、アンカーリンクのクリックイベントを検出して対象タブ要素に
.tab('show')
でタブ表示を強制 - 直後に
.offset().top
で対象タブペインの位置を取得して0秒のスクロールアニメーションで移動
というもの。
ところが、タブのようにアクティブでない要素に display: none;
の指定が付いていると、 .offset().top
で要素の正しい位置が取得できません。
これについては、モーダルの中で Slick を動かす場合の諸注意(setPotion指定、 Uncaught TypeError: Cannot read property ‘add’ of null エラー回避のために slick-initialized クラスの確認、 unslick 指定)の setPotion
の項目で説明したのと同じ現象になります。
また、実行順もわりと大事で、タブ表示→アンカーリンクの移動、の処理の順番では上手く動きませんでした(HTML に fade
クラスを付与してフェードアニメーションをさせると、タブの表示が遅れるためアンカーリンクの移動が正しく動くようになる)。
タブ表示→要素の描画がされる→ .offset().top
で位置が取得できる→アンカーリンクが作動する、という順番で考えると逆っぽく思えるのですが、そうではない模様。