経緯
Bootstrap でブレークポイント未満(スマホ時)のナビゲーションリンクにアンカーリンクがある場合、アンカーリンクをタップしてもナビゲーションリンクのメニューが開いたままアンカーリンクへ遷移するので、それを制御するコードを自前で書いていました。その部分を脱 jQuery したのでメモしておきます。
コード
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属性 を持っている (=メニュー展開) 場合- ハンバーガーメニューを折りたたむ (ハンバーガーアイコンを1度クリックする)
- またはクリックされた要素が
.dropdown-itemclass属性 を持っている (=ドロップダウンメニュー) 、かつ直近の祖先要素で.dropdownclass属性 を持つ要素が.showclass属性 を持っている (=ドロップダウンが開かれている) 場合- ドロップダウン要素を折りたたむ
- ナビゲーションバー要素のクラスにある
- クリック(タップ)された要素の直近の祖先要素に
という挙動。
プレーン JavaScript
これを 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。
.on()
普通に .addEventListener('eventName', function) でOK。
複数の要素に対するイベントハンドラ
const elms = document.querySelectorAll('.hoge, .fuga');
elms.forEach(elm => {
elm.addEventListener('click', process);
},
false);
.querySelectorAll() で取得した要素を .forEach() で反復処理させます。
親要素・祖先要素
普通に .closet('.parent') 。
子要素
- 脱jQuery!DOM要素取得コードの素のJavaScriptへの書き換え │ Webty Staff Blog
- 【JavaScript】脱JQuery!?メソッドを比べてみた!要素取得編 – Web.fla
jQuery の .children('selectorName') は一応プレーンな JavaScript にも .childrenプロパティ がある模様。
ただし、 jQuery のように .children へセレクタ指定はできなさそうなので、この部分は HTML のクラスを子要素の方に付けることで回避しました。
クラス名全てを取得
jQuery では .prop('class') としていたところですが、 .className でOK。
子孫要素の中から探す
jQueryでは .find() だったところを、 elm.querySelector('selectorName') と指定すればOK。
ウインドウ幅
- スクリーン・ウインドウ・画面サイズをjavascriptで取得する方法まとめ | WEMO
- 脱jQuery .innerHeight() .innerWidth() .outerHeight() .outerWidth() | q-Az
- ウィンドウサイズとスクローリング
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つのプロパティは個人的にかなり神がかっていると感じます。細かいイージングは犠牲になりますが、今回は全然目を瞑ることができるレベルなので良しとします。
参考
複数のクラス指定で要素を取得
.on()
複数の要素に対するイベントハンドラ
親要素・祖先要素
子要素
- 脱jQuery!DOM要素取得コードの素のJavaScriptへの書き換え │ Webty Staff Blog
- 【JavaScript】脱JQuery!?メソッドを比べてみた!要素取得編 – Web.fla
クラス名全てを取得
子孫要素の中から探す
ウインドウ幅
- スクリーン・ウインドウ・画面サイズをjavascriptで取得する方法まとめ | WEMO
- 脱jQuery .innerHeight() .innerWidth() .outerHeight() .outerWidth() | q-Az
- ウィンドウサイズとスクローリング