drawer.js, iscroll.js によるドロワーメニュー(ハンバーガーメニュー)を実装し、かつ bootstrapによるドロップダウンメニューがあるサイトにおいて、スマホでメニューをスクロールすると閉じてしまったり、背景がスクロールしてしまう事象が起きたので対処記録を残しておきます。
基本はこちらのQiita記事がほとんど答えだったが、こちらはAndroidでハンバーガーメニューが動かない場合の対処記録なので、今回は不要な修正があったり修正する行が違っていたりした。
主な原因はdrawer.jsの開発が2018年に終わっており、かつChromeの更新でpassiveという奴がデフォルトでtrueになってしまったことでおきた不具合だった。
Contents
解決策
iscroll.js と drawer.js を自前で用意する
drawerを使う場合は本家のサイトの案内の通り以下のようにCDNから各ファイルを読み込むだろう。
<!-- drawer.css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/drawer/3.2.2/css/drawer.min.css">
<!-- jquery & iScroll -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/iScroll/5.2.0/iscroll.min.js"></script>
<!-- drawer.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/drawer/3.2.2/js/drawer.min.js"></script>
しかし今回は iscroll.js と drawer.js のコードを修正したいので、それぞれ以下のコードをコピペしてjsファイルを作成し、サーバーにアップロードしパスを通す。
https://cdnjs.cloudflare.com/ajax/libs/iScroll/5.2.0/iscroll.js
https://cdnjs.cloudflare.com/ajax/libs/drawer/3.2.2/js/drawer.js
<!-- drawer.css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/drawer/3.2.2/css/drawer.min.css">
<!-- jquery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<!-- iScroll(相対パスになっている) -->
<script src="/js/iscroll.js"></script>
<!-- drawer.js(相対パスになっている) -->
<script src="/js/drawer.js"></script>
<!-- bootstrap(ドロップダウンメニューにするなら必要) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
$(document).ready(function() {
$('.drawer').drawer();
});
</script>
デバッグしやすいように min.js でなく圧縮前の .js ファイルを使用しているが、修正し終わったら min.js のファイルに置き換えて少しでも速度パフォーマンスを高めるといいかもしれない。
また今回はWordPressを使ったサイトであり、jQueryはWordPressが標準で読み込む1.12.4のバージョンでも動作したので、私の場合は最終的に以下のコードとなった。
<?php wp_head(); ?>
<!-- drawer.css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/drawer/3.2.2/css/drawer.min.css">
<!-- iScroll -->
<script src="/wp-content/themes/hoge-child/js/iscroll.js"></script>
<!-- drawer.js -->
<script src="/wp-content/themes/hoge-child/js/drawer.js"></script>
<!-- bootstrap -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script type="text/javascript">
jQuery(function ($) {
$(document).ready(function() {
$('.drawer').drawer();
});
});
</script>
wp_head() より下で iscroll.js, drawer.js, bootstrap.min.js を読み込むことと、ドロワー起動のための .drawer() がWordPress独自のjQuery記法になっていることに注意。
iscroll.js を修正する
43行目。黄色い警告が出ていた部分で passive
を false
にする。
me.addEvent = function (el, type, fn, capture) {
el.addEventListener(type, fn, {passive: false});
};
47行目。Qiitaの記事にはなかったが、上で addEvent
したものを removeEvent
するコードであり、どれを remove するかは引数が同じかどうかで判定しているらしい。だからこちらも書き換えた。
me.removeEvent = function (el, type, fn, capture) {
el.removeEventListener(type, fn, {passive: false});
};
これでコンソールの黄色い警告は消えるが、スクロールしたときの jquery.js
にでる赤いエラーは消えてくれない。もちらんスマホ実機の挙動も改善しない。
431行目。これはQiitaの記事では行数がずれていたようだが、_start:
の時の e.preventDefault()
を無効(コメントアウト)にしている。この _start: 部分がスマホ実機のみしか呼ばれないようで、このpreventDefault によってドロップダウンメニューの開閉が無効にされていたらしい(行数だけ言うとどこか分かりにくいので敢えて上の方のコードから書く)。
_start: function (e) {
// React to left mouse button only
if ( utils.eventType[e.type] != 1 ) {
// for button property
// http://unixpapa.com/js/mouse.html
var button;
if (!e.which) {
/* IE case */
button = (e.button < 2) ? 0 :
((e.button == 4) ? 1 : 2);
} else {
/* All others */
button = e.button;
}
if ( button !== 0 ) {
return;
}
}
if ( !this.enabled || (this.initiated && utils.eventType[e.type] !== this.initiated) ) {
return;
}
if ( this.options.preventDefault && !utils.isBadAndroid && !utils.preventDefaultException(e.target, this.options.preventDefaultException) ) {
// e.preventDefault(); ここをコメントアウト
}
}
571行目。ここもQiitaの記事だと行数がずれていたが、_end:
部分の e.preventDefault()
を無効(コメントアウト)にしている。
_end: function (e) {
if ( !this.enabled || utils.eventType[e.type] !== this.initiated ) {
return;
}
if ( this.options.preventDefault && !utils.preventDefaultException(e.target, this.options.preventDefaultException) ) {
// e.preventDefault(); ここをコメントアウト
}
}
drawer.js を修正する
19行目 preventDefault
を true
にする。
preventDefault: true
107行目も修正する。
if (touches) {
document.addEventListener('touchmove.' + namespace, function disableTouch(event) {
event.preventDefault();
}, {passive: false});
}
ここまでやればコンソールの警告とエラーは出なくなり、かつスマホ実機でも綺麗にドロワーメニューの中だけがスクロールしてくれ、さらにドロップダウンメニューも問題なく開閉するようになった。
解説
起きている事象
ドロワーメニューを実装したサイトにおいて、スマホ(iPhoneのSafari, AndroidのChrome)でメニューをスクロールすると急にメニューが閉じてしまったり、背景だけがスクロールしてしまう。
内部で何が起こっているのか
Chrome Developer Tools のコンソールには以下の警告(Violation)が出ている。iscroll.jsの43行目のaddEventListenerにおける警告文だ。
[Violation] Added non-passive event listener to a scroll-blocking ‘wheel’ event. Consider marking event handler as ‘passive’ to make the page more responsive.
[警告] scroll-blocking ‘wheel’ イベントに passive でない event listener が追加されました。このページをよりレスポンシブにするために、イベントハンドラーを passive にすることを検討してください。
またChrome Developer Tools をレスポンシブモード(スマホモード)にしてメニューをスクロールしようとすると、今度はjquery.js側でスクロールの度に以下のエラーがコンソールに出力される。
Unable to preventDefault inside passive event listener due to target being treated as passive.
passive event listener の中では、ターゲットは passive として扱われるので preventDefault できません。
まず preventDefault とは、iscroll.js や drawer.js, jquery.js 中にも多く出てくる preventDefault() メソッドの実行のことで、「デフォルトのイベント挙動を妨害する(キャンセルする)」というメソッドらしい。
passive とは、直訳で受動的という意味であり、技術的な意味合いはよく分からないが、とにかくpassiveである(passive: true) とpreventDefaultが実行されないとのこと。
そしてGoogleが最近Chromeの仕様を変更し、パフォーマンスのためpassiveのデフォルト値をtrueに変更した(preventDefaultをデフォルトで無効にした)らしい。
どうすればいいのか
本来Chromeのデフォルトの設定で passive: true になっている(passiveである)はずなのに、最初の黄色の警告文ではなぜか passive にしろと警告されている(iscroll.jsにおいて)。
一方で赤いエラー文では「passiveだからpreventDefaultが実行できないよ」と言っている(jquery.jsにおいて)。
ここはエラーの方を優先して passive を false にすることにした(実際 passive: false にしてやるとなぜか警告文の方も消える…)。それぞれの詳しい意味は難しく上手く説明できないが、とにかく今回の場合だと「passive を false にして、preventDefault が実行されるようにしたい」のだ。
preventDefaultが実行されると、ドロワーメニューの背景のスクロールやスクロールでメニューが閉じてしまう挙動を無効にできるっぽい。
ややこしい点
passive を false にして、preventDefault が実行されるようにしてやると、確かに背景スクロールやメニューが勝手に閉じてしまう現象はなくなった。コンソールに警告もエラーも表示されず、PCのChrome レスポンシブモードにおけるドロワーメニューは正常に機能する。
しかし、なぜかスマホにおいて今度は「ドロップダウンメニューが開かなくなる」という現象がおきた。
これは passive を false にして、preventDefault が実行されるようにした(タッチイベントの挙動が無効になった)ことで、ドロップダウンメニューの開閉も無効化されてしまったことによるものと考えられる。
PCでは開閉するのに、実機であるスマホでは動いてくれないのはPCのChrome Developer Tools におけるタップと実機のそれが、厳密には違うイベントとして認識され、それぞれで別の処理が走っている(つまり実機のタップだけpreventDefaultが実行されている)ためと思われる。
まとめると「基本はpassive を false にして、preventDefault が実行されるようにしたいが、ドロップダウンの開閉イベントだけは preventDefault は実行されないようにしたい」ということだ。
メモ:試したデバッグ方法
実際の解決方法は上記で書いた通りなので、ここでは今回試したデバッグ方法やデバッグ時の注意点についてメモしておく。
Chrome Developer Tools によるデバッグ
本家のサイトと見比べる
drawer.jsの本家のサイトもドロワーメニューがあるので、ここのscript呼び出しやHTMLマークアップを自身のサイトと見比べた。
マークアップの見比べはいいとしてもscriptとcssの呼び出しは、本家のサイトに案内があるCDNとは別のところから読み込んでいるものもありあまり参考にならなかった(見比べても問題の原因は判明しなかった)。
タッチイベントをスマホ実機のものと同じ挙動にする
今回、PCのChromeによるスクロール・タップでは問題がないのに、スマホ実機では問題が起きるというケースだった。この原因はPCのChrome Developer Tools のレスポンシブモード(mousewheel? と click か mousedown?)と、実機のそれ(wheel? と touch?)が違うものとして検知され別々の処理が走ったことによると思われる。
そこでPCのChrome Developer Tools でもスマホと同じタップ判定を行えるという情報があったので試してみたが、結果的に変わらなかった(PCのクリックとして検知されたままだったようだ)。
一応、そのやり方をメモしておく。
escキーをおして下の方からニュッともう1個のウィンドウを出す(これもdrawer window という言うらしいからややこしい)。「⋮」を押して「Sensors」を選択し、下の方の「Touch」を「Force enabled」にするとスマホ実機と同じタッチイベントになるらしい(今回はならなかったようなのだが…)。
以前は Settings>Overrides>Emulate touch events という設定項目の場所だったが、今は上記に変わったらしい。
Breakpointの設定
以下のChromeデバッグ術を参考にdrawer.js と iscroll.js にBreakpointを設定し「どこを押したらどのメソッドが呼ばれるか」を探る。しかしpreventDefaultの記述は22箇所もあるので果てしない作業だった。
https://qiita.com/snoguchi/items/8f6bb62a3166eca23ac3
実機によるデバッグ
キャッシュクリアを忘れずに
この一言に尽きる。iPhoneのSafariなら設定>Safari>詳細>Webサイトデータ>該当のサイトのデータ(キャッシュ)を左スワイプで削除できる。
という一言に尽きる。Safariなら設定>Safari>詳細>Webサイトデータ>該当のサイトのデータ(キャッシュ)を左スワイプで削除できる。
という一言に尽きる。Safariなら設定>Safari>詳細>Webサイトデータ>該当のサイトのデータ(キャッシュ)を左スワイプで削除できる。
iPhoneのChromeアプリはどうやってキャッシュできるか分からなかった…。ただシークレットモードでサイトを表示すればキャッシュでなくサーバーから読み込んで来てくれるが絶対である自信はない…。
(iOS開発で使うXcodeみたいに、実機でもBreakpointとか設定し、ConsoleのログをみながらデバッグできるといいのだがWebではやり方が分からない。やはりある程度の断片的な知識を身につけたら、本や会社でChromeデバッグ術を1から10まで習った方がいいと感じる。)
参考リンク
tratailに全く同じ事象の人がいたが、こちらの回答のpassiveをtrueにせよというのは間違いなので注意。