モーダルウインドウを背景固定にしてスクロールバー分のガタつき問題を解決しつつ、iOS Safariにも対応してしかもjQuery非依存にしたいワガママなアナタへ捧ぐ愛のコード
posted : 2020.05.31
こんにちは、ma-ya’s CREATE[まーやずくりえいと]です。
もはやモーダルウインドウの実装なんて当たり前の要件となってきた昨今、
サクッとプラグインを使えば、世の中の優秀なフロントエンド開発者が実装した機能を簡単に使うことができますね。
ただそれでも、JSに対する興味や情熱から、自前のコードで実装したいなんてこともあるかもしれません(無い?)。
今日はそんなお話。
追加機能が増えると意外と悩ましいモーダルウインドウの実装
モーダルウインドウは単に表示するだけならかなりシンプルに実装が可能です。
過去当サイトでは以下の通りjQuery版・jQuery非依存版で超シンプルなモーダルウインドウの実装を紹介させてもらってます(GMOインターネットグループの中の人がこの記事を参考に実装しててビビった)。
ただモーダルウインドウは追加機能を要望されることも多いのが現実。
表示するだけなら簡単なモーダルですが、追加機能を実装すると結構悩ましい問題があったりします。
モーダルウインドウが表示された時の背景を固定したい
あるあるの機能かと思いますが、これを実装するだけで問題が一気に増えます。
ざっくりまとめると↓のような感じ。
- iOS(iPadOSを含む)のSafariのみoverflow: hidden;によるスクロール禁止処理(一番シンプルな対処法)が上手く適用されない
- 2019年に誕生したiPadOSにより今までのコードではiPadが判別できなくなった
- position: fixed;を使った時のスクロール情報の喪失問題
- スクロール禁止処理により発生する、モーダルウインドウ切替時のスクロールバー幅分のガタつき
などなど。
overflow: hidden;で一発解決できれば全人類皆幸せなんですが、現実は厳しいもんです。
IEですら動くのに…それでいいのかiOS Safari。
背景固定&スクロールバー分のガタつき&iOS Safari&jQuery非依存に“おおむね”対応した背景固定用関数のサンプル
だらだら書いててもしょうがないのでとりあえずデモをば。
See the Pen
pure modal with vanilla JS – apply scroll prevent by mycreatesite (@mycreatesite)
on CodePen.
※codepen上だとスマホの挙動が不安定な場合があるようです
基本的なモーダルのHTML構成やCSS、表示非表示のスクリプトはコピペで実装!脱jQueryでも簡単モーダルウィンドウ[HTML / CSS / Javascript]をベースとしています。
肝は、スクロール制御とそれに紐づく問題に対処する専用の関数を用意すること
ポイントはモーダルの表示非表示を切り替えるロジックとは別に、スクロール制御専用の関数を用意することです。
スクロール制御関数部分の抜粋は以下の通り。
今回は処理を1つの関数に全て納めていますが、一回だけイベント発火させる関数「addEventListenerOnce」なんかはグローバルに分離して再利用できるようにしても良いかも。
事細かに説明すると大変なことになりそうなので割愛しますが、コード内コメントに大まかな処理の説明を記載してるので、
それを見ればざっくり何をやろうとしているかわかるかと。
関数化出来たら、モーダルウインドウ表示切替ロジック内で関数を実行する
関数化が終わったら、モーダル表示切替を制御しているロジック内で関数を引数と共に呼び出してあげます。
↑のスクリプトのまま使う前提であれば、引数にはtrue(スクロール禁止) / false(スクロール解除)を指定してやります。
//モーダル表示切替ロジック(抜粋) //////////モーダル表示時////////// if(!modalArea.classList.contains('is-show')){ modalArea.classList.add('is-show'); bodyScrollPrevent(true); //スクロール禁止 //////////モーダル非表示時////////// } else { modalArea.classList.remove('is-show'); bodyScrollPrevent(false,modalArea); //スクロール解除 }
モーダルウインドウにトランジションをかけている場合はtransitionendイベントで一工夫必要
↑のコードでモーダルウインドウ非表示時、関数の第二引数にモーダルウインドウを包括するDOMを渡しています。
何に使われるかというと、引数として渡されたDOMのトランジション終了を検知して、そのタイミングでスタイルをリセットするため。
(今回のデモではモーダルの表示切り替えをCSSクラスのつけ外しのみで実装しているため、CSSトランジションが終わったタイミングを検知して何かをする、という場合にはそのロジックを組む必要があります)
で、実際にこの処理無しで試してみるとわかるんですが、モーダル非表示時のみ、スクロールバー分のガタつきがフェードアウトしているモーダル側に発生してしまうんですね(何言ってるかわかんなかったらごめんなさい)。
これが結構悩んだところだったんですが、今回のデモではモーダルを包括しているDOMにトランジション(visibilityとopacity)をかけていたので、そのDOMのトランジションが終わってからスタイルをリセットするためにtransitionendイベントをかませています。
jQueryではまあまあ知られているかもしれませんが、意外とvanilla JSでもtransitionendイベントが使えます(必要に応じて要ベンダープレフィクス)。
あとCSSのvisibilityがtransitionの対象ということは過去記事で書いてるのでよかったら見てみて下さい。
※これ知った時は目からウロコでした。
タイトルの口調が今回の記事とほぼ一致してますが特に理由はありまてん。
最後の最後にもうひと手間。一回だけイベントを実行する関数を用意する(vanilla JS)
これでどやあ!と思ったらまだ問題が(もうやだ…)。
モーダル非表示時に普通にtransitionendイベントをaddEventListenerで設定すると、二回目以降のモーダル切り替えが意図しない挙動となります。
ざっくりいうと一度モーダルDOMにtransitionendが設定されると、二回目以降は表示時にもtransitionendイベントが発火しちゃう…といった感じ。
ここも結構悩んだんですが、最終的にはtransitionendイベントを実行した直後にイベントリスナを削除する、つまり一度だけイベントを発火させる「addEventListenerOnce」関数を用意して、その中でスタイルのリセット処理を行うことにしました(jQueryの.one()みたいなやつのイメージ)。
上述しましたが、この「addEventListenerOnce」関数はグローバルに置いといて使いまわし出来るようにしてもいいかも。
まあ適宜環境に合わせてカスタマイズしてもらえればと思います。
今回のデモはギュっと一つの関数に盛り込んじゃったので、やや冗長かもしれないですね。
何はともあれ、これでおおむね意図した挙動になりました。
jQuery環境で実装する際の注意点
jQuery環境でスクロール制御関数を用いる場合は、ちょっとだけ注意が必要です。
デモを用意したので参照して頂ければと思います
See the Pen
pure modal with jQuery – apply scroll prevent by mycreatesite (@mycreatesite)
on CodePen.
※codepen上だとスマホの挙動が不安定な場合があるようです
ポイント
- モーダル非表示の際にスクロール制御関数に渡す第二引数は、jQueryオブジェクト→DOMエレメントに変換する
- モーダルの表示制御をfadeInなどのアニメーションメソッドで実装していると意図しない挙動になると思うので、スクリプトをカスタマイズするかクラスのつけ外しで表示切替を制御する
ポイント[1]jQueryオブジェクト→DOMエレメントへの変換
jQueryでもモーダル表示切替をクラスのつけ外しのみで制御する場合の話ですが、
スクロール制御関数「bodyScrollPrevent」は第二引数で受け取ったDOMオブジェクトをvanilla JSのメソッドで処理するので、第二引数にjQueryオブジェクトを指定する時は、
bodyScrollPrevent(false, $('#hoge')[0]);
のようにDOMエレメントへの変換を行う必要があります。
方法については過去記事でまとめているので↓の記事もご参考下さい。
ポイント[2]モーダル表示をfadeIn / fadeOutなどのアニメーションメソッドで実装する場合(上記jQueryサンプルでの実装方法)
以前当ブログで紹介した下記記事のようなjQuery版のモーダルの場合、モーダル表示切替はjQueryのアニメーションメソッド(fadeIn / fadeOut)で実装していました。
今回のjQuery版サンプルもfadeIn / fadeOutで実装していますが、こういった場合は少しスクリプトを書き換える必要があります。
jQueryのfadeInやfadeOutはアニメーション終了後のコールバック関数が指定できます(超便利)。
つまり初めに紹介したスクリプト内の「トランジションを検知するロジック」はこの場合不要になります。
そこでモーダルクローズ時のスクリプトを少し書き換えます。
モーダルクローズ時のJS(抜粋)
$('#closeModal , #modalBg').on('click',function(){ modalArea.fadeOut(function(){ bodyScrollPrevent(false); }); });
スクロール制御の関数(抜粋)
function bodyScrollPrevent(flag, modal){ →「, modal」を削除 ~~~~
addEventListenerOnce(modal,'transitionend',function(){ →削除 body.style.paddingRight = ''; if(isiOS){ scrollPosition = parseInt(body.style.top.replace(/[^0-9]/g,'')); body.style.position = ''; body.style.width = ''; body.style.top = ''; window.scrollTo(0, scrollPosition); }else { body.style.overflow = ''; } }); →削除
function addEventListenerOnce(node, event, callback) { const handler = function(e) { callback.call(this, e); node.removeEventListener(event, handler); }; node.addEventListener(event, handler); } →関数ごと削除
↑のような感じでfadeOutメソッドのコールバック関数内でスクロール制御関数を呼び出し、さらにスクロール制御関数から「トランジション検知のロジック」を削除します。
念のため改めて言っておくと、jQuery環境下でもアニメーションメソッドを使わず、モーダル表示切替をクラスのつけ外しのみで行う場合(CSSトランジションで制御する場合)はトランジション検知のロジックは必要になるかと思います。
要は、
jQuery環境下でfadeIn / fadeOutを使わない場合はポイント[1]、
fadeIn / fadeOutを使う場合はポイント[2]を参考にしてねという話です。
※例外はあると思うので適宜スクリプトをカスタマイズしてくださいmm
これでjQuery環境下でもデモの通りスクリプトが使えるかと思います。
とはいえ闇はまだまだ深い…かも?
そんな感じで今回のデモは一応IE11、その他モダンブラウザやiOS、macOSにて概ね動作確認済みです。
全てvanilla JSでまとめたので、jQuery非依存の環境、jQuery導入環境双方で使えるかと思います。
が、なかなかに闇の深い問題でもありそうなので念を押すように「おおむね」と保険をかけさせていただきます苦笑
※実際、position: absoluteで配置されてる要素などはこのスクリプトでもモーダル表示切替時にスクロールバー分のガタつきが発生してしまうなど、サイトデザイン・レイアウトによっていくつか問題もあります。
そしてだいぶ釣り気味のタイトルになってしまったのもごめんなさいです。
ということで、「万能」とは言えないスクリプトかもしれませんがどなたかの参考になればこれ幸い。
ではでは。
コピペでカンタン!モーダルウインドウのjQuery版・jQuery非依存版はこちら
その他コピペ・サンプル系記事
- コピペで簡単!シンプルなローディング画面
- コピペで簡単!超シンプルスライドショー[HTML / CSS / jQuery]
- コピペも可!CSSでラインが一周するホバーアニメーション
- コピペで実装!脱jQueryでも簡単モーダルウィンドウ[HTML / CSS / Javascript]