drawer.jsでスマホだけ挙動がおかしい時の対処法

drawer.js, iscroll.js によるドロワーメニュー(ハンバーガーメニュー)を実装し、かつ bootstrapによるドロップダウンメニューがあるサイトにおいて、スマホでメニューをスクロールすると閉じてしまったり、背景がスクロールしてしまう事象が起きた。この現象の対処記録を残しておきます。

結論

基本は以下のQiita記事がほとんど答えだった。しかしこちらはAndroidでハンバーガーメニューが動かない場合の対処記録なので、今回は不要な修正があったり、修正する行が違っていたりした。

https://qiita.com/YujiHatanaka/items/64b6a6c8facb3a2f69b5#iscrolljsv520

主な原因はdrawer.jsの開発が2018年に終わっており、かつChromeの更新でpassiveという奴がデフォルトでtrueになってしまったことでおきた不具合だった。

やるべきこと

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 & iScroll -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<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)でメニューをスクロールすると急にメニューが閉じてしまったり、背景だけがスクロールしてしまう現象がおきた。

事象としては以下のtratailの質問と全く同じであった(ただしこちらの回答のpassiveをtrueにせよというのは間違いなので注意)。

https://teratail.com/questions/127838

何が起こっているのか

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まで習った方がいいと感じる。