HTML+CSS+JavaScriptでLightbox風のモーダルウィンドウを作る方法です。
Demo
全体の構成を定義する
始めに以下のHTMLを書いて全体の構成を定義します。
<div id="overlay" class="overlay"></div> <div id="modal-content" class="modal-content"> <ul id="pager" class="pager"> <li class="prev"><button class="prev-btn"><</button></li> <li class="next"><button class="next-btn">></button></li> </ul> <button id="close-btn" class="close-btn">☓</button> <div id="enlarged-images" class="enlarged-images"></div> </div> <div id="thumbnails" class="thumbnails"> </div>
HTMLではオーバーレイ#overlay
とモーダルウィンドウのコンテンツ#modal-content
、サムネイル一覧#thumbnails
を定義しています。
#modal-content
内は前や次の拡大画像に切り替えるためのページャー#pager
やモーダルウィンドウを閉じるボタン#close-btn
、拡大画像を表示する要素enlarged-images
を含んでいます。
#enlarged-image
と#thumbnails
内は後にJavaScriptで画像要素を挿入するため現時点では空にしておきます。
全体の見た目を作る
次に以下のCSSを書いて全体の見た目を作ります。
@mixin translate($hVal, $vVal) { transform: translate($hVal, $vVal); } @mixin top-left($pVal, $tVal, $lVal) { position: $pVal; top: $tVal; left: $lVal; } @mixin top-right($pVal, $tVal, $rVal) { position: $pVal; top: $tVal; right: $rVal; } %inactive { opacity: 0; visibility: hidden; } * { box-sizing: border-box; } body { background: #333; } .thumbnails { > div { background: #ccc; border: 5px solid #fff; cursor: pointer; display: block; float: left; overflow: hidden; position: relative; padding-bottom: 40%; width: 50%; @media (min-width: 800px) { padding-bottom: 15%; width: 25%; } } img { user-select: none; -webkit-user-select: none; -webkit-user-drag: none; display: block; max-height: 200%; @include top-left(absolute, 0, 0); width: 100%; } } .overlay { @extend %inactive; background: #000; height: 100%; @include top-left(fixed, 0, 0); transition: 0.5s; width: 100%; z-index: 1000; &.active { opacity: 0.6; visibility: visible; } } .modal-content { @extend %inactive; background: #ccc; height: 50vw; overflow: hidden; @include top-left(fixed, 50%, 50%); @include translate(-50%, -50%); transition: 0.5s; width: 50%; z-index: 2000; &.active { opacity: 1; visibility: visible; } img { //@extend %inactive; @include top-left(absolute, 50%, 50%); @include translate(-50%, -50%); transition: 0.5s; width: 100%; /* &.active { opacity: 1; visibility: visible; } */ } @media (min-width: 800px) { height: 400px; width: 500px; } } .close-btn { background: #333; border: none; color: #fff; cursor: pointer; height: 8vw; line-height: 8vw; @include top-right(absolute, 0, 0); text-align: center; width: 8vw; z-index: 4000; @media (min-width: 800px) { line-height: 44px; height: 44px; width: 44px; } } .pager { height: 8vw; list-style: none; margin: 0; padding: 0; @include top-left(absolute, 50%, 0); @include translate(0, -50%); width: 100%; z-index: 4000; @media (min-width: 800px) { height: 44px; } > .prev, > .next { position: absolute; top: 0; > button { background: #333; border: none; color: #fff; cursor: pointer; height: 8vw; line-height: 8vw; text-align: center; width: 8vw; z-index: 5000; @media (min-width: 800px) { height: 44px; line-height: 44px; width: 44px; } } } > .prev { left: 0; } > .next { right: 0; } }
CSSのポイントは以下の通りです。
Mixinを定義
transform
とposition
プロパティは頻繁に使うため以下のようなMixinを定義します。
transformプロパティのMixin
@mixin translate($hVal, $vVal) { transform: translate($hVal, $vVal); }
positionプロパティのMixin
@mixin top-left($pVal, $tVal, $lVal) { position: $pVal; top: $tVal; left: $lVal; } @mixin top-right($pVal, $tVal, $rVal) { position: $pVal; top: $tVal; right: $rVal; }
プレースホルダーを定義
要素を非表示にするスタイルは頻繁に記述するため以下のようなプレースホルダーとしておきます。
%inactive { opacity: 0; visibility: hidden; }
初期状態を設定
初期状態としてオーバーレイとモーダルウィンドウのコンテンツは予め非表示にしておきます。また、JavaScriptでこれらの要素をフェードイン・アウトさせるために必要な.active
クラスも併せて作成します。
オーバーレイの初期状態
.overlay { @extend %inactive; &.active { opacity: 0.6; visibility: visible; } }
モーダルコンテンツの初期状態
.modal-content { @extend %inactive; &.active { opacity: 1; visibility: visible; } }
JavaScriptでこれらの要素に.active
クラスが追加・削除されるとフェードイン・アウトが行われます。
機能を実装する
最後に以下のJavaScriptを書いてHTMLとCSSによって出来上がったものに機能を実装します。
(() => { class Asset { constructor() { this.urls = [ 'http://tsukulog.local/wp-content/uploads/2015/06/HIRA86_MBAkey-thumb-1000xauto-17206.jpg', 'http://tsukulog.local/wp-content/uploads/2015/06/GAK88_nekokafeneko-thumb-1000xauto-16206.jpg', 'http://tsukulog.local/wp-content/uploads/2015/06/N825_benchinouenoshironeko-thumb-autox1000-14809.jpg', 'http://tsukulog.local/wp-content/uploads/2015/06/https-www.pakutaso.com-assets_c-2015-05-26NJ_retuwotukuruahirucyan-thumb-1000xauto-14005.jpg' ]; this.images = []; } createImage() { for(let i = 0; i < this.urls.length; i++) { const image = new Image(); image.src = this.urls[i]; this.images[i] = image; } } } class Thumbnail { constructor() { //サムネイル一覧 this.elem = document.getElementById('thumbnails'); this._image = this.elem.getElementsByTagName('img'); } setupListener(modalContent, overlay) { const thumbnail = this._image; for(let i = 0; i < thumbnail.length; i++) { thumbnail[i].addEventListener('click', (e) => { e.preventDefault(); //クリックしたサムネイルを取得 const target = e.target; const clone = target.cloneNode(true); //クリックしたサムネイルの番号 const currentIndex = [].slice.call(thumbnail).indexOf(target); //console.log(currentIndex); //console.log(thumbnail[currentIndex-2]); //オーバーレイをフェードイン overlay.show(); //モーダルウィンドウをフェードイン modalContent.show(); modalContent.removeImage(); modalContent.addImage(clone); modalContent.currentIndex = currentIndex; }); } } get image() { return this._image; } insertImages(images) { images.forEach((image) => this.elem.appendChild(image)); } wrapImages(images) { images.forEach((image) => image.outerHTML = `<div class="thumbnail">${image.outerHTML}</div>`); } } //モーダルウィンドウに関するクラス class ModalContent { constructor() { //モーダルウィンドウ this.elem = document.getElementById('modal-content'); //拡大画像を子に持つ親要素 this.enlargedImages = document.getElementById('enlarged-images'); this._currentIndex = 0; } get currentIndex() { return this._currentIndex; } set currentIndex(value) { if(typeof value === 'number') { this._currentIndex = value; } } //モーダルウィンドウを表示するメソッド show() { this.elem.classList.add('active'); } //モーダルウィンドウを非表示にするメソッド hide() { this.elem.classList.remove('active'); } addImage(image) { this.enlargedImages.appendChild(image); } removeImage() { if(this.enlargedImages.firstChild) { this.enlargedImages.removeChild(this.enlargedImages.firstChild); } } slide(index, image) { //番号の最大値 const max = image.length - 1; //渡された番号が0未満であればmaxに変更 if(index < 0) { index = max; } //渡された番号がmax超過であれば0に変更 if(index > max) { index = 0; } //渡された番号の拡大画像を表示 const clone = image[index].cloneNode(true); this.removeImage(); this.addImage(clone); //現在の番号を更新 this.currentIndex = index; } } //オーバーレイに関するクラス class Overlay { constructor() { this.elem = document.getElementById('overlay'); } setupListener(modalContent) { this.elem.addEventListener('click', (e) => { this.hide(); modalContent.hide(); }); } show() { this.elem.classList.add('active'); } hide() { this.elem.classList.remove('active'); } } //ページャーに関するクラス class Pager { constructor() { this.elem = document.getElementById('pager'); this.btn = this.elem.getElementsByTagName('button'); } setupListener(modalContent, thumbnail) { const pagerBtn = this.btn; //.prev-btnをクリック→一つ前の画像に切り替え //.next-btnをクリック→一つ後の画像に切り替え for(let i = 0; i < pagerBtn.length; i++) { pagerBtn[i].addEventListener('click', (e) => { if(pagerBtn[i].classList.contains('prev-btn')) { modalContent.currentIndex = modalContent.currentIndex - 1; console.log(modalContent.currentIndex); //console.log(modalWindow.image[modalWindow.currentIndex]); //modalWindow.slide(modalWindow.currentIndex, modalWindow.image); }else{ modalContent.currentIndex = modalContent.currentIndex + 1; console.log(modalContent.currentIndex); //modalWindow.slide(currentIndex + 1, modalWindow.image); } modalContent.slide(modalContent.currentIndex, thumbnail.image); }); } } } //閉じるボタンに関するクラス class CloseBtn { constructor() { this.elem = document.getElementById('close-btn'); } //.close-btnをクリックできる状態にするメソッド setupListener(overlay, modalContent) { this.elem.addEventListener('click', (e) => { overlay.hide(); modalContent.hide(); }); } } //インスタンス化 const asset = new Asset(); const thumbnail = new Thumbnail(); const modalContent = new ModalContent(); const overlay = new Overlay(); const pager = new Pager(); const closeBtn = new CloseBtn(); //全体を初期化する関数 function init() { asset.createImage(); thumbnail.insertImages(asset.images); thumbnail.wrapImages(asset.images); setupListeners(); } //モーダルウィンドウを操作できる状態にする関数 function setupListeners() { thumbnail.setupListener(modalContent, overlay); overlay.setupListener(modalContent); pager.setupListener(modalContent, thumbnail); closeBtn.setupListener(overlay, modalContent); } init(); })();
事前準備
機能を実装する前に次の事前準備に関するコードを書きます。
画像を生成する
Asset
クラスのコンストラクタで画像4枚分のURLを定義します。
this.urls = [ 'http://tsukulog.local/wp-content/uploads/2015/06/HIRA86_MBAkey-thumb-1000xauto-17206.jpg', 'http://tsukulog.local/wp-content/uploads/2015/06/GAK88_nekokafeneko-thumb-1000xauto-16206.jpg', 'http://tsukulog.local/wp-content/uploads/2015/06/N825_benchinouenoshironeko-thumb-autox1000-14809.jpg', 'http://tsukulog.local/wp-content/uploads/2015/06/https-www.pakutaso.com-assets_c-2015-05-26NJ_retuwotukuruahirucyan-thumb-1000xauto-14005.jpg' ];
その後createImage
メソッドで画像を生成します。生成した画像はimages
配列に格納します。
createImage() { for(let i = 0; i < this.urls.length; i++) { const image = new Image(); image.src = this.urls[i]; this.images[i] = image; } }
サムネイルを生成する
Thumbnail
クラスのwrapImages
メソッドで先程生成した画像を.thumbnail
要素で包んでサムネイルを生成します。
wrapImages(images) { images.forEach((image) => image.outerHTML = `<div class="thumbnail">${image.outerHTML}</div>`); }
サムネイルを挿入する
Thumbnail
クラスのinsertImages
メソッドで生成したサムネイルを.thumbnails
内に挿入します。
insertImages(images) { images.forEach((image) => this.elem.appendChild(image)); }
実装する機能
実装する機能は次の通りです。
サムネイルをクリックするとモーダルウィンドウを表示する
Thumbnail
クラスのsetupListener
メソッドでサムネイルをクリックするとモーダルウィンドウがフェードイン表示されるようにします。
setupListener(modalContent, overlay) { const thumbnail = this._image; for(let i = 0; i < thumbnail.length; i++) { thumbnail[i].addEventListener('click', (e) => { e.preventDefault(); //クリックしたサムネイルを取得 const target = e.target; const clone = target.cloneNode(true); //クリックしたサムネイルの番号 const currentIndex = [].slice.call(thumbnail).indexOf(target); //オーバーレイをフェードイン overlay.show(); //モーダルウィンドウをフェードイン modalContent.show(); modalContent.removeImage(); modalContent.addImage(clone); modalContent.currentIndex = currentIndex; }); } }
仕組みは次の通りです。
- クリックしたサムネイルの画像
target
を複製する。 overlay
のshow
メソッドでオーバーレイをフェードイン表示するmodalContent
のshow
メソッドでモーダルコンテンツをフェードイン表示する。modalContent
のremoveImage
メソッドで#enlarged-image
内の直前まで表示されていた画像を削除する。modalContent
のaddImage
メソッドで#enlarged-image
内に複製した画像clone
を挿入する。
又、取得したクリックしたサムネイル画像の番号currentIndex
をmodalContent
のcurrentIndex
に代入して更新します。
ページャーをクリックすると前後の画像に切り替える
Pager
クラスのsetupListener
メソッドでページャーボタンをクリックするとモーダルウィンドウ内の画像が切り替わるようにします。
setupListener(modalContent, thumbnail) { const pagerBtn = this.btn; //.prev-btnをクリック→一つ前の画像に切り替え //.next-btnをクリック→一つ後の画像に切り替え for(let i = 0; i < pagerBtn.length; i++) { pagerBtn[i].addEventListener('click', (e) => { if(pagerBtn[i].classList.contains('prev-btn')) { modalContent.currentIndex = modalContent.currentIndex - 1; }else{ modalContent.currentIndex = modalContent.currentIndex + 1; } modalContent.slide(modalContent.currentIndex, thumbnail.image); }); } }
仕組みは以下の通りです。
- クリックしたボタンが戻るボタン
.prev-btn
であればmodalContent
をデクリメントする。 - クリックしたボタンが進むボタン
.next-btn
であればmodalContent
をインクリメントする。 modalContent
のslide
メソッドで1.であれば一つ前の画像に戻し、2.であれば一つ後の画像に進める。
modalContent
のslide
メソッドは以下のようになっています。
slide(index, image) { //番号の最大値 const max = image.length - 1; //渡された番号が0未満であればmaxに変更 if(index < 0) { index = max; } //渡された番号がmax超過であれば0に変更 if(index > max) { index = 0; } //渡された番号の拡大画像を表示 const clone = image[index].cloneNode(true); this.removeImage(); this.addImage(clone); //現在の番号を更新 this.currentIndex = index; }
閉じるボタンをクリックするとモーダルウィンドウを非表示にする
CloseBtn
クラスのsetupListener
メソッドで閉じるボタンがクリックされるとモーダルウィンドウがフェードアウトで非表示されるようにします。
//.close-btnをクリックできる状態にするメソッド setupListener(overlay, modalContent) { this.elem.addEventListener('click', (e) => { overlay.hide(); modalContent.hide(); }); }
仕組みは.close-btn
がクリックされるとoverlay
とmodalContent
のhide
メソッドで両者をフェードアウトで非表示にしています。
overlay
とmodalContent
のhide
メソッドは同じ処理内容です。
hide() { this.elem.classList.remove('active'); }
オーバーレイをクリックするとモーダルウィンドウを非表示にする
Overlay
クラスのsetupListener
メソッドでオーバーレイがクリックされるとモーダルウィンドウがフェードアウトで非表示されるようにします。
setupListener(modalContent) { this.elem.addEventListener('click', (e) => { this.hide(); modalContent.hide(); }); }
仕組みはCloseBtn
クラスのsetupListener
メソッドと同様で自身とmodalContent
のhide
メソッドでオーバーレイとモーダルコンテンツの両者を非表示にしています。
モーダルウィンドウを完成させる
最後に以下のことを行って実装した機能を働かせてモーダルウィンドウを完成させます。
インスタンス化
作成した全てのクラスをインスタンス化して実装した機能を使えるようにします。
//インスタンス化 const asset = new Asset(); const thumbnail = new Thumbnail(); const modalContent = new ModalContent(); const overlay = new Overlay(); const pager = new Pager(); const closeBtn = new CloseBtn();
全体を初期化する
init
関数を実行して全体を初期化すればモーダルウィンドウの完成です。
init();
init
関数は以下のようになっています。
//全体を初期化する関数 function init() { asset.createImage(); thumbnail.insertImages(asset.images); thumbnail.wrapImages(asset.images); setupListeners(); }
init
関数では以下のようにしてそれぞれのオブジェクトの初期化を行っています。
asset
のcreateImage
メソッドで画像を生成する。thumbnail
のinsertImages
メソッドで1.を挿入する。thumbnail
のwrapImages
メソッドでサムネイルを生成する(ここまでが事前準備)。setupListeners
関数で各要素をクリックできる状態にする。
setupListeners
関数は以下のようになっています。
//モーダルウィンドウを操作できる状態にする関数 function setupListeners() { thumbnail.setupListener(modalContent, overlay); overlay.setupListener(modalContent); pager.setupListener(modalContent, thumbnail); closeBtn.setupListener(overlay, modalContent); }
setupListeners
関数ではthumbnail
・overlay
・pager
・closeBtn
オブジェクトのsetupListener
メソッドを実行しているだけです。
おわりに
以上で、【JavaScript】ページャー付きのLightbox風モーダルウィンドウを作る方法を終わります。