先日bootstrapのtabを実装していたのですが、tab部分がまったく動かなくなるバグに直面しました。
調べてみるとこの画面の右下にもある「ページトップへボタン」の挙動が原因であることがわかりました。
いざ直面すると結構厄介な罠だったので、備忘の意味も含めて文書に残しておきます。
bootstrapのtabってなあに?
↓↓bootstrapのtabはこんなかんじの画面の要素です。↓↓
※この例では、例えば9月分のタブをクリックすると9月分のデータが下の方に表示されるようなイメージです。
各タブのナビゲーション部分にはアンカー1)aタグでhref=”#hoge”のように設定するブツが設定されています。
ナビゲーション部分をクリックすると、アンカーとして設定されている要素をタブの中身として表示するような作りになっています。
この表示を切り替える処理は、bootstrapのtab.jsにて、documentに対してバインドされているようです。
ページトップへボタンのよくある実装
続いてページトップへボタンのよくある実装についてです。
ページトップボタンは、クリックすると画面の一番へにスクロールする便利ボタンです。
同じことを実現するのであれば、aタグのhrefに”#”を設定するだけでいいのですが、
特に何もしないと一瞬で表示が切り替わってしまい、ユーザを驚かせてしまいます。
そのため、イマドキのwebシステムでは滑らかにスクロールさせるためにjavascriptを使って、スムーズにスクロールさせることがよくあります。
今回問題になった処理では以下のように実装されていました。
(エスケープされてしまう記号は全角にしています。ご了承ください。)
$(’a[href^=”#”]’).click(function(){return false;}); // 中略 jQuery(document).ready(function () { jQuery(’a[href *= ”#”]’).click(function () { var $target = jQuery(this.hash); $target = $target.length && $target || $(’[name=’ + this.hash.slice(1) + ’]’); if ($target.length) { var targetOffset = $target.offset().top - 30; jQuery(’html,body’).animate({scrollTop: targetOffset}, 1000, ’quart’); return false; } }); });
元々のアンカー処理を無効にして、独自のスムーズスクロールを実装しているイメージですね。
「jquery スムーズスクロール」で検索するとよく見つかる、とても妥当な処理に見えます。
そして引き起こされる問題
このスムーズスクロールが有効になっている画面でタブ要素が含まれている画面を表示してみたところ、タブのナビゲーション部分をクリックしても中身が変わらなくなってしまいました。
その代わり、ナビゲーションをクリックしたときに画面が微妙に上下に動いたり動かなかったりします。
タブとしてはまったくつかいものにならないですね・・・。
原因
察しのいい読者の方はもうお気づきかもしれませんが、スムーズスクロールを実装しているjsに記載されている
jQuery(’a[href^=”#”]’).click(function(){return false;});
と
jQuery(’a[href *= ”#”]’).click(function () { /* なんやかんやスムーズスクロールの処理 */ return false; });
こそがtabが動かなくなってしまった原因です。2)微妙にセレクタが違いますがほぼ同じ要素をターゲットにしています
上の方の処理はもともとのアンカーとしての動きを止めることを意図しています。
これによりdocumentにclickイベントがバブリングされず、タブ切り替えの処理が実行されない状態になっていました。3)詳細は「javascript バブリング return false」で検索してみてください
下の方の処理は、スマートスクロールの処理を終えたら他のclickイベントを処理しないようにするための return false;
です。
仮に上の方の処理を削除してもここでdocumentにclickイベントがバブリングされないようになっていたので、やはりタブ切り替え処理が実行されないようになっていました。
これらふたつの処理がtabのナビゲーション部分をクリックしたときの処理を無効化していました。
そのあとに display: none;
で非表示になっている要素へのスムーズスクロールの処理が動いてしまうため、ナビゲーション部分をクリックするたびに画面がガタガタ動いていたのでした。。。4)隙を生じぬ二段構え・・・まさにヒテンミ○ルギスタイル
対応方法
そもそも上の処理を2カ所とも削除できれば一番よかったのですが、サービス全体で使われている処理なので変更するときの影響範囲が広すぎるのでそこには手を出さない方針にしました。
その代わり、タブを使っているところで何かしらの対応を行うことにしました。
実際にクリックされるaタグ要素にタブ切り替え処理をバインドしてみた
問題が起こっていた箇所はこのようになっていました
<ul class=”nav nav-pills nav-justified” role=”tablist”> <li role=”presentation” class=”active”> <a href=”#hogehoge” aria-controls=”foryou” role=”tab” data-toggle=”tab” class=”tab_target”>hogeを表示</a> </li> <li role=”presentation”> <a href=”#fugafuga” aria-controls=”aboutus” role=”tab” data-toggle=”tab” class=”tab_target”>fugaを表示</a> </li> </ul> ・・・ <div class=”tab-content”> <div role=”tab-panel” class=”tab-pane active” id=”hogehoge”> hogehogeの中身 </div> <div role=”tab-panel” class=”tab-pane” id=”fugafuga”> fugafugaの中身 </div> </div>
問題点はbootstrapがバインドしてくれたタブ切り替えの処理が動かなくなっていることです。
この問題をなるべく楽に解決するために、タブをクリックしたときに表示を切り替える処理をaタグに対して手動でバインドしなおすことにしました。
といっても何も難しいことはしていません。
<ul class=”nav nav-pills nav-justified” role=”tablist”> <li role=”presentation” class=”active”> <a href=”#hogehoge” aria-controls=”foryou” role=”tab” data-toggle=”tab” class=”tab_target” onClick=”$(this).tab(’show’); return false;”>hogeを表示</a> </li> <li role=”presentation”> <a href=”#fugafuga” aria-controls=”aboutus” role=”tab” data-toggle=”tab” class=”tab_target” onClick=”$(this).tab(’show’); return false;”>fugaを表示</a> </li> </ul> 以下略
というように、onClickプロパティを追加しただけです。
補足説明
ポイントは↓です。
onClick=”$(this).tab(’show’); return false;”
bootstrapのリファレンスにもあるように、bootstrapのtabには対象のタブ要素を表示するための処理が用意されています。
その処理をonClick時に呼んであげるようにしただけです。
タブをクリックしたときに多少画面もスクロールしてしまいますが、タブが動くようになったのでひとまず良しとします。
同じことができればいいので、
var targets = document.getElementsByClassName(’tab_target’); Array.prototype.filter.call(targets, function(target){ target.addEventListener(’click’, function(){ $(this).tab(’show’);return false; }); });
としてjavascriptでイベントをバインドしなおしても同じ結果となります。
個人的な最善の解決方法
今回はスムーズスクロール周りの処理には手を出せませんでしたが、せっかくなのでもしそれが可能だったらどう修正しようかも一応考えてみました。
$(’a[href^=”#”][class!=”deny-smooth-scroll”]’).click(function(){return false;}); // 中略 jQuery(document).ready(function () { jQuery(’a[href *= ”#”][class!=”deny-smooth-scroll”]’).click(function () { var $target = jQuery(this.hash); $target = $target.length && $target || $(’[name=’ + this.hash.slice(1) + ’]’); if ($target.length) { var targetOffset = $target.offset().top - 30; jQuery(’html,body’).animate({scrollTop: targetOffset}, 1000, ’quart’); return false; } }); });
スムーズスクロールしないようにするためのクラスを用意して、それが設定されていたらスムーズスクロールしないようにするイメージの処理です。
・・・ん?これ影響少ないし楽だしアリかも?
終わりに
大元の処理を変更できれば話は早いんだけど、影響範囲が広いのでそれができない・・・という場面はプログラマあるあるだと思います。
今回もそんなよくあるシチュエーションで、しかも結構てこずったので記事にしてみました。
同じような現象に直面した方の一助になれば幸いです。