自作でLightbox(ライトボックス)風ギャラリーを作りました【JavaScript】

css

Javascriptの練習でライトボックス風のギャラリーを作成しました。スマホでスワイプできると思いますが、Androidでは確認しておりません。。
プラグインを使うほどではない!CDNを読み込みたくない!jQueryを使いたくない!場合に使えるかもしれないのでメモ。

クリックした画像を、ポップアップで大きく表示させる機能のことをライトボックスと呼びます。

完成したライトボックス風のギャラリーを確認する(デモ)

動作デモ

See the Pen lightbox-like by donguri2020 (@m-ke) on CodePen.

ソースコードを確認

長いコードになったので、ポイントだけメモしておきます。
なお、矢印やクローズボタンはFont AwesomeのWebアイコンを使っています。

HTML

<section class="class_modal-sample">
  //サムネイルを表示する領域
  <div class="thumb">
    <ul class="thumb__items">
      <li><img src="./images/photo01.jpg" data-caption="サムネイル1"></li>
      <li><img src="./images/photo02.jpg" data-caption="サムネイル2"></li>
      <li><img src="./images/photo03.jpg" data-caption="サムネイル3"></li>
      <li><img src="./images/photo04.jpg" data-caption="サムネイル4"></li>
    </ul>
  </div>
  //ポップアップウィンドウを表示する領域
  <div class="modal">
    <div class="modal__item"><img src=""></div>
    <div class="modal__caption"></div>
    <div class="modal__close"><i class="fas fa-times-circle"></i></div>
    <div class="modal__prev"><i class="fas fa-angle-left"></i></div>
    <div class="modal__next"><i class="fas fa-angle-right"></i></div>
  </div>
  //オーバーレイ
  <div class="modal-overlay"></div>
</section>

CSS

  .thumb {

  }
  .thumb__items {
    display: flex;
    flex-flow: wrap;
    position: relative;
    z-index: 10;
  }
  .thumb__items > li {
    flex: 0 1 25%;
    list-style: none;
    cursor: pointer;
    position: relative;
  }
  .thumb__items > li img {
    max-width: 100%;
    position: relative;
    vertical-align: bottom;
  }
  .thumb__items > li:before {
    content: "";
    display: block;
    opacity: 0;
    background: rgba(0, 0, 0, 0.6);
    width: 100%;
    height: 100%;
    position: absolute;
    left: 0;
    top: 0;
    z-index: 10;
    transition: all 0.5s 0.3s ease;
  }
  .thumb__items > li:hover::before {
    opacity: 1;
  }
  .modal {
    display: none;
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: 999;
    transform: translate(-50%, -50%);
    width: 100%;
    margin: auto;
    opacity: 0;
    z-index: 1;
    transition: all 0.5s 0s ease;
  }
  .modal.visible {
    display: block;
    opacity: 1;
    z-index: 999;
  }
  @media screen and (min-width: 980px) {
    .modal {
      width: auto;
      max-width: 880px;
      margin: 10px;
      position: relative;

    }
  }
  .modal__item {
    background: #5c5c5c;

  }
  .modal__item img {
    max-width: 100%;
    vertical-align: bottom;
  }
  .modal__caption {
    position: absolute;
    color: #fff;
    line-height: 1.6;
    padding: 10px;
  }
  @media screen and (min-width: 980px) {
    .modal__caption {
      padding: 10px 0;
    }
  }
  .modal__close {
    position: absolute;
    top: -60px;
    right: 15px;
    z-index: 9999;
  }
  @media screen and (min-width: 980px) {
    .modal__close {
      top: -15px;
      right: -15px;
    }
  }
  .modal__prev {
    position: absolute;
    top: 50%;
    transform: translate(0, -50%);
    left: 20px;
    z-index: 9999;
  }
  @media screen and (min-width: 980px) {
    .modal__prev {
      left: -30px;
    }
  }
  .modal__next {
    position: absolute;
    top: 50%;
    transform: translate(0, -50%);
    right: 20px;
    z-index: 9999;
  }
  @media screen and (min-width: 980px) {
    .modal__next {
      right: -30px;
    }
  }
  .modal-overlay {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 1;
    background: rgba(0, 0, 0, 0.6);
    width: 100%;
    height: 100%;
    opacity: 0;
    transition: all 0.5s 0s ease;
  }
  .modal-overlay.visible {
    display: block;
    opacity: 1;
    z-index: 100;
  }
  .fas {
    font-size: 40px;
    cursor: pointer;
    color: rgba(255, 255, 255, 0.6);
  }
  .fadein {
  animation: fadein .3s;
}

.fadeout {
    animation: fadeout .3s;
    animation-fill-mode: forwards;
}
@keyframes fadein {
    0% {
        opacity: 0;
    }
    100% {
        opacity: 1;
    }
}
@keyframes fadeout {
    0% {
        opacity: 1;
    }
    100% {
        opacity: 0;
        display: none;
    }
}

Javascript

class Modal {
  constructor() {
    //初期値(DOM)
    this.DOM = {};
    this.DOM.thumbItems = document.querySelectorAll('.thumb__items li');
    this.DOM.modal = document.querySelector('.modal');
    this.DOM.modalOverlay = document.querySelector('.modal-overlay');
    this.DOM.modalItem = document.querySelector('.modal__item');
    this.DOM.modalCaption = document.querySelector('.modal__caption');
    this.DOM.modalClose = document.querySelector('.modal__close');
    this.DOM.modalPrev = document.querySelector('.modal__prev');
    this.DOM.modalNext = document.querySelector('.modal__next');

    // スワイプ座標
    this.startX;
    this.endX;
    // ターゲット要素
    this.thumbTarget;
    // 何番めの要素か
    this.thumbCurrent;

    // 最初に実行する関数
    this._init();
  }

  _init() {
    this.DOM.thumbItems.forEach((thumbItem, index, array) => {
      thumbItem.addEventListener('click', this._appearModal.bind(this, thumbItem, index));
    });
    // クリックイベント実行
    this._clickEvent();
    // スワイプイベント実行
    this._swipeEvent();
  }
  
  // クリックイベント
  _clickEvent() {
    this.DOM.modalClose.addEventListener('click', this._disappearModal.bind(this));
    this.DOM.modalOverlay.addEventListener('click', this._disappearModal.bind(this));
    this.DOM.modalNext.addEventListener('click', this._next.bind(this));
    this.DOM.modalPrev.addEventListener('click', this._prev.bind(this));
  }
  // スワイプイベント
  _swipeEvent() {
    this.DOM.modalItem.addEventListener('touchmove', this._swipeStart.bind(this));
    this.DOM.modalItem.addEventListener('touchstart', this._swipeMove.bind(this));
    this.DOM.modalItem.addEventListener('touchend', this._swipeEnd.bind(this));
  }
  _swipeMove(event) {
    event.preventDefault();
    this.endX = event.touches[0].pageX;
  }
  _swipeStart(event) {
    event.preventDefault();
    this.startX = event.touches[0].pageX;
  }
  _swipeEnd(event) {
    event.preventDefault();
    console.log('startX:' + this.startX);
    console.log('endX:' + this.endX);
    if( 0 < (this.endX - this.startX) ) {
      this._next();
    } else {
      this._prev();
    }
  }
  // モーダル表示
  _appearModal(thumbItem, index) {
    this.thumbCurrent = index; // 現在のindex
    this.DOM.modal.classList.add('visible');
    this.DOM.modalOverlay.classList.add('visible');

    this.thumbTarget = thumbItem; //モーダル表示する要素
    
    this._modalShow(this.thumbTarget); // モーダル表示
  }
  // モーダル非表示
  _disappearModal() {
    this.DOM.modal.classList.remove('visible');
    this.DOM.modalOverlay.classList.remove('visible');
  }
  // モーダルに表示する画像
  _modalShow(self) {
    const imgElm = self.childNodes[0];
    const targetmodalItem = this.DOM.modalItem.childNodes[0];
    targetmodalItem.classList.add('fadein');
    targetmodalItem.src = imgElm.src;
    // フェードインのクラス
    setTimeout(function() {
      targetmodalItem.classList.add('fadeout');
      targetmodalItem.classList.remove('fadein');
      targetmodalItem.classList.remove('fadeout');
    },300);

    this._modalCaption(imgElm);
  }
  // モーダルに表示するキャプション
  _modalCaption(imgElm) {
    this.DOM.modalCaption.innerText = imgElm.dataset.caption
  }
  _next() {
    this.thumbCurrent = this._changeCount(1, this.thumbCurrent, this.DOM.thumbItems.length);
    this.thumbTarget = this.DOM.thumbItems[this.thumbCurrent];
    this._modalShow(this.thumbTarget); // モーダル表示
  }
  _prev() {
    this.thumbCurrent = this._changeCount(-1, this.thumbCurrent, this.DOM.thumbItems.length);
    this.thumbTarget = this.DOM.thumbItems[this.thumbCurrent];

    this._modalShow(this.thumbTarget); // モーダル表示
  }

  // 変更する番号を取得
  _changeCount(num, index, len) {
    return (index + num + len) % len;
  }
}

// インスタンス化
const modal = new Modal();

カスタマイズの方法

よくありそうなカスタマイズをメモしておきます。

キャプションの設定方法

imgタグのdata属性にキャプションを入力すれば、ポップアップウィンドウで画像と一緒に表示されるようになります。

# キャプションに「サムネイル3」を設定
<img src="./images/photo03.jpg" data-caption="サムネイル3">

キャプションの非表示

キャプションを非表示にする場合、CSSで非表示にするか、data属性の入力を空にしてください。
要素自体を削除するとundefinedが表示されてしまいます。

# キャプションを表示しない ※data属性を未入力にする
<img src="./images/photo03.jpg" data-caption="">

スワイプを停止する

スワイプの機能を停止するには、Javascriptのスワイプを実行している関数をコメントアウトします。
具体的には32行目あたりに記載があります。

    // クリックイベント実行
    this._clickEvent();
    // スワイプイベント実行
    // this._swipeEvent(); スワイプイベントを停止

まとめ

練習用に作成したライトボックスなので機能は不足していますが、それっぽくできた気がします!
ライトボックス系のプラグインは、探すと高機能なものがたくさんあるので、実際はそちらを使うと思います。