今更ながら、モーダルを作ってみる
Oculus Questの予想を超えた性能の良さに感激しつつ、毎晩ライトセーバーを嬉々として振り回し続けている kouraku です。おはこんばんちわ。
さてさて今回は、モーダルを作ってみたいと思います。
これもよくライブラリにお世話になりがちなのですが、CSSでflexを使うのが当たり前になって来た昨今、要素を画面中央に配置することなど、もはや朝飯前。 表示非表示を切り替え、そこにアニメーションをつける程度であれば、わざわざライブラリを使うほどでも無いわけです。 いやむしろ、こだわったデザインや細かな動きなどの注文が来ると、ライブラリを使ったがためにエラく面倒なことをする羽目になることも多々ありますよね。
ということで、できるだけシンプルなモーダルを作りたいと思います(アニメーションは省きます)。
まずはポイントを確認
上記でも軽く触れたとおり、モーダルのコンテンツが画面中央に配置されるデザインの場合は、CSSのflexを使用すれば簡単に実装できます。
なお、モーダルを作るにあたって押さえておきたいポイントは以下3点です。
- $(window).scrollTop でモーダルを開く前のスクロール値を取得し、モーダル開閉時に活用
- モーダル展開時は、後ろ(元)の画面はスクロールさせない(固定する)
- CSSのflexで画面中央に配置した場合、モーダルコンテンツの高さがウィンドウの高さより大きい場合を考慮
この辺りはスマホ表示にも影響してきますので、対応は必須ですね。
さらに、クライアントによっては以下のような注文も来ます。
- モーダルを開く際に、後ろ(元)の画面をガクッとさせない
- 再度モーダルを開いたとき、改めてモーダルコンテンツの上部から表示したい
- モーダルコンテンツ以外の部分をクリックしたら閉じるようにしたい
こうした要望も含めてみると、ライブラリを使った方が楽かな・・・と思ってしまいます。ところがどっこい、痒いところに手が届かない、それがライブラリです。
作ってみる
とりあえず、今回のHTMLの構成は、下図のようにメインとモーダルを完全に分離することにします。
上図の内容を元にJSの流れを考えてみます。
- メインのモーダルリンク
.modal-link
をクリックしたら、 - スクロール値を取得し、
- bodyタグにモーダルが開いたことを認識させるためのクラス
.modal-open
を付与し、 - モーダルリンクのdata-idで指定している対象のモーダルコンテンツ
#modal-1 or #modal-2
に表示させるためのクラスactive
を付与しモーダルを展開し、 - メインコンテンツを固定した上で、スクロール値分topをマイナスする。
- 閉じるボタン
.modal-close
をクリックしたら、 - 2と3で付与したクラスを外し、モーダルを閉じて、
- メインコンテンツの固定設定を全て解除し、ページ上部からのスクロール値分移動させる。
これをコードにすると、次のような感じになるかと思います。
// モーダルリンクをクリックしたときの処理(1)
var scrollTop = 0;
var $body = $('body');
var $wrapper = $('.wrapper');
$('.modal-link').on('click', function() {
scrollTop = $(window).scrollTop(); // (2)
$body.addClass('modal-open'); // (3)
$('#'+$(this).data('id')).addClass('active'); // (4)
$wrapper.css({ // (5)
'top': -(that.scrollTop)
});
});
// 閉じるボタンをクリックしたときの処理(6)
$('.modal-close').on('click', function() {
$body.removeClass('modal-open'); // (7)
$('.modal-inner').removeClass('active'); // (7)
$wrapper.css({ // (8)
'top': 0
});
$('html, body').prop({ // (8)
scrollTop: scrollTop
});
});
次にCSSです。ここまでの流れから、
- bodyに
.modal-open
が付与された場合- メインコンテンツはスクロールできなくする
- モーダルエリアを表示する
- 対象のモーダルコンテンツに
active
が付与された場合- 対象のモーダルコンテンツを表示する
といった点を考慮して、スタイルを当てると良いかと思います。
では、ここまでの内容をざっくりとまとめて作ってみたサンプルが次のものになります。
1個目と2個目のモーダルコンテンツのレイアウトを違うタイプにしています。同じ構成で同じ処理をさせていても、スタイルを変えるだけで見た目も簡単に変更できます。
また、メインコンテンツの状態がわかるようモーダルコンテンツを透過させています。モーダル展開時にメインコンテンツがしっかり固定されていることが分かります。
これだけで、なんとなくモーダルができているっぽい気がしますよね?
しかし、このサンプルには1点問題があり、まだ未完成の状態なのです。
サンプルの問題点
画面を広げている状態で確認していると気づきませんが、実はモーダル1のようにCSSのflexで中央配置にしたとき、画面の高さがコンテンツよりも小さい場合、コンテンツ(上部)が隠れてしまうという問題が起こります。
この問題について、何か簡単な対応方法はないか調査してみましたが・・・何も見つからなかったので、JSに頼ることにします。
処理の内容は、画面の高さを判定し、必要に応じてプロパティ値を切り替える、という単純なものです。
コードで書くと、次のような感じでしょうか。
var $body = $('body');
var $target;
var checkPos = function() {
if ($body.hasClass('modal-open') && $target.css('align-items') === 'center') {
if ($target.find('.modal-content').outerHeight() > $(window).height()) {
$target.css({
'align-items': 'flex-start'
});
} else {
$target.css({
'align-items': 'center'
});
}
}
}
$('.modal-link').on('click', function() {
$target = $('#' + $(this).data('id')); // 開く対象のモーダルコンテンツを取得
checkPos();
});
$(window).on('resize', function() { // ウィンドウリサイズでもプロパティが切り替わるように
checkPos();
});
さらに機能を追加してみる
この他にも先に述べた機能も追加してみたいと思います。
モーダルを開く際に、後ろ(元)の画面をガクッとさせない
こちらは、以前 fujihara が書いた記事を参考にします。
再度モーダルを開いたとき、改めてモーダルコンテンツの上部から表示したい
対象となるモーダルコンテンツ展開時に、そのコンテンツのトップへ移動するようにしましょう。
var $target;
$('.modal-link').on('click', function() {
$target = $('#' + $(this).data('id')); // 開く対象のモーダルコンテンツを取得
$target.animate({scrollTop: 0}, 0);
});
モーダルコンテンツ以外の部分をクリックしたら閉じるようにしたい
よくある以下のようなコードを使いたいと思います。
$(document).on('click', function(e) {
if (!$(e.target).closest(ターゲット名).length) {
閉じる処理
}
});
ただし、オブジェクト化した中にこの記述を入れると、モーダルを開くリンクが複数ある場合、何度も同じ処理が走り誤作動を起こしやすいので、正しく処理を行うために次の2点制限を設けたいと思います。
- クリックした対象が、メインのモーダルリンク
.modal-link
以外のとき - モーダルが開いているとき
この条件を含めたものをコード化すると、次のような感じになります。
$(document).on('click', function(e) {
if (!$(e.target).hasClass('modal-link') &&
$body.hasClass('modal-open') && $target.hasClass('active') &&
!$(e.target).closest('#開いているモーダルコンテンツのID .modal-content').length) {
閉じる処理
}
});
以上の内容を踏まえて改良したサンプルが次のものです。
これで気になる部分も解消され、注文にも十分応えられそうです。
まとめ
このモーダルの考え方はスマホ用メニューなどにも流用できるので、自分の中で情報整理するために書いてみました。
ただ今回は、ちょっと長く、説明も大夫端折ってしまい分かりづらい内容になってしまいました・・・。
さてさて、そんなちょっと残念な気持ちも、家に帰ってOculus Questを被ってライトセーバーを振り回せば綺麗さっぱり忘れることができます。ホント、素晴らしいモノを手に入れることができました。