Bootstrap でブレークポイント未満(スマホ時)のナビゲーションリンクにアンカーリンクがある場合、アンカーリンクをタップしてもナビゲーションリンクのメニューが開いたままアンカーリンクへ遷移するので、それを制御するコードを自前で書いていました。その部分を脱 jQuery したのでメモしておきます。
まずは Bootstrap 4 までの jQuery でのコード。
// ナビゲーションバー
const $navbar = $('#navbar');
// ブランド名とドロップダウンコンポーネント以外のナビゲーションリスト
$navbar.find('.navbar-brand, .nav-item:not(.dropdown) a, .dropdown-item').on('click', function (e) {
// 略
const navBarListID = 'navbarList';
if ($(e.currentTarget).closest('#' + navBarListID).length > 0) {
let breakpoint = 0;
if (/(^|\s)navbar-expand-(\S*)/g.test($navbar.children('.navbar').prop('class'))) {
switch (RegExp.$2) {
case 'sm':
breakpoint = 576;
break;
case 'md':
breakpoint = 768;
break;
case 'lg':
breakpoint = 992;
break;
case 'xl':
breakpoint = 1200;
break;
default:
breakpoint = 0;
break;
}
}
if ($(window).outerWidth() < breakpoint && !$navbar.find('.navbar-toggler[data-target="#' + navBarListID + '"]').hasClass('collapsed')) {
// 現在の表示がハンバーガーメニューの場合
$navbar.find('.navbar-toggler[data-target="#' + navBarListID + '"]').trigger('click'); // 移動したらハンバーガーを折りたたむ
} else if ($(e.currentTarget).hasClass('dropdown-item') && $(e.currentTarget).closest('.dropdown').hasClass('show')) {
// 現在の表示がハンバーガーメニューではなく、ドロップダウン内のメニューをクリックした場合
$(e.currentTarget).closest('.dropdown').trigger('click'); // 移動したらドロップダウンを折りたたむ
}
}
return false;
}
};
ざっくりこんなコードでした。やっていることとしては
.navbar-brand
), ドロップダウンではないナビゲーションリンク(.nav-item:not(.dropdown) a
, ドロップダウン内のリンク(.dropdown-item
) がクリックされた場合
#navbarList
の id属性 がある要素が存在する場合
.navbar-expand-XX
のクラスのブレークポイントの文字列を取得してブレークポイント値をセット.navbar-toggler
要素が .collapsed
のclass属性 を持っている (=メニュー展開) 場合
.dropdown-item
class属性 を持っている (=ドロップダウンメニュー) 、かつ直近の祖先要素で .dropdown
class属性 を持つ要素が .show
class属性 を持っている (=ドロップダウンが開かれている) 場合
という挙動。
これを Bootstrap 5 対応でプレーンな JavaScirpt に書き換え。
const navbar = document.querySelector('#navbar');
const navBarListID = 'navbarList';
if (
typeof e.currentTarget.closest(`#${navBarListID}`) !== 'undefined'
&& e.currentTarget.closest(`#${navBarListID}`) !== null
) {
let breakpoint = 0;
if (/(^|\s)navbar-expand-(\S*)/g.test(navbar.className)) {
switch (RegExp.$2) {
case 'sm':
breakpoint = 576;
break;
case 'md':
breakpoint = 768;
break;
case 'lg':
breakpoint = 992;
break;
case 'xl':
breakpoint = 1200;
break;
case 'xxl':
breakpoint = 1400;
break;
default:
breakpoint = 0;
break;
}
}
if (window.innerWidth < breakpoint) {
// ブレークポイント未満の幅のとき
const navbarTogglers = navbar.querySelectorAll(`.navbar-toggler[data-bs-target="#${navBarListID}"]`);
navbarTogglers.forEach((navbarToggler) => {
if(!navbarToggler.classList.contains('collapsed')) {
// 現在の表示がハンバーガーメニューの場合、
// 移動したらハンバーガーを折りたたむ
navbarToggler.dispatchEvent(new Event('click'));
}
else if(
e.currentTarget.classList.contains('dropdown-item')
&& e.currentTarget.closest('.dropdown').classList.contains('show')
) {
// 現在の表示がハンバーガーメニューではなく、ドロップダウン内のメニューをクリックした場合
// 移動したらドロップダウンを折りたたむ
e.currentTarget.closest('.dropdown').dispatchEvent(new Event('click'));
}
});
}
}
やっていることは大体同じです。ただし、いくつか置き換えが必要な部分があったので以下その点について触れていきます。
const elms = document.querySelectorAll('.hoge, .fuga');
jQuery のようにカンマ区切りで document.querySelectorAll()
でOK。
普通に .addEventListener('eventName', function)
でOK。
const elms = document.querySelectorAll('.hoge, .fuga');
elms.forEach(elm => {
elm.addEventListener('click', process);
},
false);
.querySelectorAll()
で取得した要素を .forEach()
で反復処理させます。
普通に .closet('.parent')
。
jQuery の .children('selectorName')
は一応プレーンな JavaScript にも .children
プロパティ がある模様。
ただし、 jQuery のように .children
へセレクタ指定はできなさそうなので、この部分は HTML のクラスを子要素の方に付けることで回避しました。
jQuery では .prop('class')
としていたところですが、 .className
でOK。
jQueryでは .find()
だったところを、 elm.querySelector('selectorName')
と指定すればOK。
jQuery で $(window).outerWidth()
としてたところを、今回の用途では window.innerWidth
へ置き換えました。
jQuery では .hasClass('className')
だったところを、 .classList.contains('className')
へ置き換え。
普通に関数指定で elm.addEventListener('click', hoge(elm), false)
と書いてしまっていました。
しかし、参考記事に拠るとこれだとその関数の実行結果が渡されるとのこと。しかもイベント発火時ではなく該当コード読み込み時に実行されてしまうため、挙動がおかしくなってしまいます。
これについては第二引数はオブジェクト (または JavaScript の純粋な関数) なので elm.addEventListener('click', hoge, false)
と関数名だけにしなければならず、その通りに書けばOK。
一方、 const hoge = (e) => { /* 処理 */ };
で普通にイベントは渡ってくるので、「クリックされた要素」をイベントハンドラ内で利用したい場合は e.currentTarget
とすれば問題ないですね。
最初これに気付かずしばらく嵌まっていました。参考記事に感謝。
イベントがクラスで渡す必要がありますがほぼ同じで、 elm.dispatchEvent(new Event('click'))
とすればOK。
このような形でガシガシ書き換えていけば大きな問題はなさそうです。
とはいえ、この置き換えは地味にアンカーリンクへのスクロールアニメーションを scroll-behavior: smooth;
に移行したおかげで上述以外の大半の JS を破棄しても問題ないと判断できたのが非常に大きいですね。そうでなければもっと大きなボリュームと対峙する必要がありました……。
しかも Scroll-margin-top
で上部固定ナビゲーションバーの裏側にアンカーリンクが隠れてしまう問題を回避できる、というのも JS コードを削減できた要因の一つなので、この2つのプロパティは個人的にかなり神がかっていると感じます。細かいイージングは犠牲になりますが、今回は全然目を瞑ることができるレベルなので良しとします。
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント