PhotoSwipeを使って
画像をポップアップ表示する
【Swiper(v7)との連携あり】

PhotoSwipeとは?

「PhotoSwipe」はページ上に設置した画像をクリックすると拡大してポップアップ表示することができるJavaScriptプラグインです。jQueryは必要なし。豊富なオプションやAPIが使えるので用途によってカスタマイズできます。

画像のポップアップ表示はWebサイトではよく使われるUIの一つで、PhotoSwipeもその中の一つです。いわゆる「Lightbox」系のプラグインになりますが、PhotoSwipeは数ある中でも高機能で洗練されたUIが備わっており、かつ無料で使えるのでかなりオススメです。

設置方法が少し手間ですが、カスタマイズしなくても公式で紹介されているデフォルトの手順で設置すれば、ただ単に画像をポップアップ表示するだけではなく、アニメーションやスワイプ、ズームなどリッチなUIが標準で機能します。もちろんレスポンシブにも対応しています。

https://photoswipe.com/

今回はその使い方をメモします。

▼(2022.10.13追記)この記事はPhotoSwipe v4の内容について書いています。v5についての基本的な使い方・Swiperとの連携については下記をご覧ください。

対応ブラウザ

モダンブラウザはすべて対応しています。
IE8以降が対応なので、しぶとく残っているIE11対応案件でも使えます。

PhotoSwipeのデモ

とりあえず作ってみたデモです。

PhotoSwipe DEMO

コードはGitHubに置いてます。

https://github.com/inos3910/photoswipe-demo

PhotoSwipeの使い方

ここからは実装方法を紹介します。
この記事では公式の「Getting Started」を参考に、SassWebpackなどに対応しながら実装してみます。

ファイルの読み込み

最新バージョン(執筆時はv4.1.3)を読み込ませます。
v5がベータ版として公開されていますが、今回はそちらについては保留します。

ファイル一式をダウンロードして使う場合

こちらは公式に書いてある通りの方法。

Githubからファイル一式ダウンロードしてきて、/distフォルダの中身をそのまま使います。
下記は/photoswipeというディレクトリを作って、その中に/distの中身を配置した場合。

<!-- CSSの読み込み -->
<link rel="stylesheet" href="./photoswipe/photoswipe.css"> 
<link rel="stylesheet" href="./photoswipe/default-skin/default-skin.css"> 

<!-- JSの読み込み -->
<script src="./photoswipe/photoswipe.min.js"></script> 
<script src="./photoswipe/photoswipe-ui-default.min.js"></script> 

Webpack・Sassを使う場合

Yarnでインストール

$ yarn add -D photoswipe

もちろんnpmでも同じなのでお好みで

$ npm i -D photoswipe

インストールできたらまずJSをインポート。

import PhotoSwipe from 'photoswipe/dist/photoswipe.min.js'
import PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default.min.js'

続いてSass。(dart-sassを利用)
node_modules/photoswipe/distからphotoswipe.cssdefault-skin.cssをコピーしてきて、それぞれ拡張子をscssに変更し、任意のディレクトリに配置します。筆者の場合は、CSS設計にFLOCSSを使っているので、sass/foundation/vendorに配置して読み込みます。

@use 'foundation/vendor/photoswipe';
@use 'foundation/vendor/default-skin';

ここで1つ注意。
プラグインのCSSはそのまま使うと、アイコン用の画像が指定されています。
プロジェクトによってアイコン画像の位置を任意で変更する必要があります。

ここでは下記の画像をnode_modules/photoswipe/dist/default-skin/からコピーしてきてimagesフォルダ配置します。

  • default-skin.png
  • default-skin.svg
  • preloader.gif

アイコン画像を配置できたら指定しているCSS(3ヶ所)を任意のパスに書き換えます。

_default-skin.scss

//background: url(default-skin.png) 0 0 no-repeat; の部分
background: inline('default-skin.png') 0 0 no-repeat;

//background-image: url(default-skin.svg); の部分
background-image: inline('default-skin.svg');

//background: url(preloader.gif) 0 0 no-repeat;
background: inline('preloader.gif') 0 0 no-repeat;

筆者の場合は、Sassをコンパイルするときにpostcss-assetsを使っているので、その中のinlineというオプションでアイコン画像をBase64エンコードして直接CSSファイルに埋め込むようにしました。特にそうしないといけないわけではないので、url('../images/default-skin.png')っていう形で普通に相対パス指定しても問題ありません。

Webpack・Sassを使う場合はちょっと面倒くさいですが、例えばBASEのように配置できるファイルの容量や種類に制限があるようなプロジェクトの場合に、JSはWebpackで、CSSはSassでそれぞれ1ファイルにまとめて読み込ませる、といったことが必要になる場合がありますので、こちらの方法も忘れないようにメモしています。

HTML

画像リストを作ります。任意の属性を加えたり、aタグが必要だったりします。
ひとまずサンプルコードを書きます。

<ul class="p-gallery" data-photoswipe="1">
  <li class="p-gallery__item">
    <a href="assets/images/sample-1.jpg" data-size="1080x1080">
      <img src="assets/images/sample-1.jpg" width="1080" height="1080" alt="">
    </a>
  </li>
  <li class="p-gallery__item">
    <a href="assets/images/sample-2.jpg">
      <img src="assets/images/sample-2.jpg" width="1080" height="1080" alt="">
    </a>
  </li>
  <li class="p-gallery__item">
    <a href="assets/images/sample-3.jpg">
      <img src="assets/images/sample-3.jpg" width="1080" height="1080" alt="">
    </a>
  </li>
  <li class="p-gallery__item">
    <a href="assets/images/sample-4.jpg">
      <img src="assets/images/sample-4.jpg" width="1080" height="1080" alt="">
    </a>
  </li>
</ul>
<!--  /.p-gallery -->

ポイントはいくつかあります。

  • ポップアップしたい画像のHTMLを要素で囲んでdata-photoswipe属性を振っておく。値は1や2など単純でOKだが、複数画像グループを設置する場合は一意にする。
  • data-photoswipe属性を振った直下の要素をリスト化する。ulliである必要はないが間に別要素をネストしない。
  • 各画像のimgタグをaタグで囲み、href属性に拡大したい画像のURLを入れておく。
  • aタグにdata-size属性を{横}x{縦}の形式をつけることで、ポップアップ画像のサイズを指定可能。
  • data-size属性は必須ではない。(なければ自動でaタグのhrefに指定した画像から実サイズを設定する)

aタグのhrefに設定したURLがポップアップ表示される画像の指定になります。
拡大後のポップアップ表示画像(aタグのhref)と拡大前の画像(imgタグのsrc)を分けられるので、通常表示では容量の小さい画像、ポップアップした時だけ解像度高めの容量の大きい画像、という具合で使えます。

JS

ただ読み込むだけでは機能しないプラグインなので、初期化のためのスクリプトを書きます。
公式サイトに初期化処理を書いてあるので、そのままコピペで使えるのですが、内容を理解した方が良いと思ったので公式を参考に自分で書いてみました。

コード全体

完成したコードはこちら。

import "core-js/stable";
import "regenerator-runtime/runtime";

import PhotoSwipe from 'photoswipe'
import PhotoSwipeUI_Default from 'photoswipe/src/js/ui/photoswipe-ui-default.js'

export default class Main {
  constructor() {
    this.photoswipe = [];
    this.photoswipeUi = PhotoSwipeUI_Default;

    this.initPhotoswipe('[data-photoswipe="1"]');

    this.initPhotoswipe('[data-photoswipe="2"]', {
      history: true,
      bgOpacity: 0.8,
      showHideOpacity : true,
      shareEl : true,
      shareButtons: [
      {id:'facebook', label:'Share on Facebook', url:'https://www.facebook.com/sharer/sharer.php?u={{url}}'},
      {id:'twitter', label:'Tweet', url:'https://twitter.com/intent/tweet?text={{text}}&url={{url}}'},
      {id:'download', label:'Download image', url:'{{raw_image_url}}', download:true}
      ],
    });
  }

  /**
  * PhotoSwipeの初期化
  * @param {string} elemName - 画像グループのHTML要素名
  * @param {object} options - PhotoSwipeのオプション
  * @return {void}
  **/
  initPhotoswipe(elemName, options = {}) {
    const galleryElements = document.querySelectorAll(elemName);
    if(!galleryElements[0]){
      return;
    }

    const template = this.addTemplate();
    const pswpElement = template.querySelector('.pswp');
    if(!pswpElement){
      return;
    }

    for(let i = 0, l = galleryElements.length; i < l; i++) {
      this.attachPhotoswipe(galleryElements[i], pswpElement, options, i);
    }
  }

  /**
  * Photoswipeの適用
  * @param {HTMLElement} elem - PhotoSwipeを適用するHTML要素
  * @param {HTMLElement} pswpElement - ポップアップテンプレートのHTML要素
  * @param {object} - PhotoSwipeのオプション
  * @param {number} -i 画像グループのインデックス
  * @return {void}
  **/
  async attachPhotoswipe(elem, pswpElement, options, i){

    const uidPrefix = elem.getAttribute('data-photoswipe');
    const uid = !uidPrefix ? i+1 : `${uidPrefix}-${i+1}`;
    elem.setAttribute('data-pswp-uid', uid);

    const items = await this.parseThumbnailElements(elem);
    const _options = this.initOptions(elem, items, options);

    this.onThumbnailsClick(pswpElement, items, _options, uid);

    if(('history' in _options) && _options.history){
      this.openByHash(pswpElement, items, _options, uid);
    }
  }

  /**
  * ポップアップ用のHTMLを追加
  * @return {void}
  **/
  addTemplate() {
    const elem = document.createElement("div");
    elem.innerHTML = '<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true"><div class="pswp__bg"></div><div class="pswp__scroll-wrap"><div class="pswp__container"><div class="pswp__item"></div><div class="pswp__item"></div><div class="pswp__item"></div></div><div class="pswp__ui pswp__ui--hidden"><div class="pswp__top-bar"><div class="pswp__counter"></div><button class="pswp__button pswp__button--close" title="Close (Esc)"></button><button class="pswp__button pswp__button--share" title="Share"></button><button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button><button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button><div class="pswp__preloader"><div class="pswp__preloader__icn"><div class="pswp__preloader__cut"><div class="pswp__preloader__donut"></div></div></div></div></div><div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap"><div class="pswp__share-tooltip"></div> </div><button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)"></button><button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)"></button><div class="pswp__caption"><div class="pswp__caption__center"></div></div></div></div></div>';
    document.body.appendChild(elem);
    return elem;
  }

  /**
  * 画像情報の配列を取得
  * @param {HTMLElement} target - 画像グループのHTML要素
  * @return {promise} - 画像情報の配列をPromiseで返す
  **/
  async parseThumbnailElements(target) {

    const items = [];
    let index = 0;

    for(let el of target.children){
      let src, width, height, size;

      const linkEl = el.getElementsByTagName('a')[0];
      if(!linkEl){
        return;
      }

      src = linkEl.href;

      const imgEl = el.getElementsByTagName('img')[0];
      if(!imgEl){
        return;
      }


      if(linkEl.getAttribute('data-size')){
        size = linkEl.getAttribute('data-size').split('x');
        width = parseInt(size[0], 10);
        height = parseInt(size[1], 10);
      }
      else{
        const img = await this.loadImage(src);
        width = img.naturalWidth;
        height = img.naturalHeight;
      }

      const item = {
        src : src,
        w: width,
        h: height,
        el: linkEl,
        index : index
      }

      items.push(item);

      ++index;
    };

    return items;
  }

  /**
  * 画像の非同期読み込み
  * @param {string} src - 画像のURL
  * @return {promise} - imageオブジェクトをPromiseで返す
  **/
  loadImage(src) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = (e) => reject(e);
      img.src = src;
    });
  }

  /**
  * オプションの初期化
  * @param {HTMLElement} galleryElement - 画像グループのHTML要素
  * @param {array} items - 画像情報の配列
  * @param {object} options - PhotoSwipeのオプション
  * @return {object}
  **/
  initOptions(galleryElement, items, options = {}) {
    const _options = JSON.parse(JSON.stringify(options))
    if(!('galleryUID' in _options)){
      _options.galleryUID = galleryElement.getAttribute('data-pswp-uid');
    }

    if(!('getThumbBoundsFn' in _options)){
      _options.getThumbBoundsFn = (index) => {
        const thumbnail = items[index].el.getElementsByTagName('img')[0],
        pageYScroll = window.pageYOffset || document.documentElement.scrollTop,
        rect = thumbnail.getBoundingClientRect();

        return {
          x:rect.left,
          y:rect.top + pageYScroll,
          w:rect.width
        };
      }
    }

    if(!('index' in _options)){
      _options.index = 0;
    }

    if(!('history' in _options)){
      _options.history = false;
    }

    if(!('shareEl' in _options)){
      _options.shareEl = false;
    }

    return _options;
  }

  /**
  * 画像のクリック
  * @param {HTMLElement} pswpElement - ポップアップテンプレートのHTML要素
  * @param {array} items - 画像情報の配列
  * @param {object} options - PhotoSwipeのオプション
  * @param {string} uid - 画像グループのuid
  * @return {void}
  **/
  onThumbnailsClick(pswpElement, items, options, uid) {
    items.forEach((item, index) => {
      item.el.addEventListener('click', (e) => {
        e.preventDefault();
        options.index = index;
        this.photoswipe[uid] = new PhotoSwipe( pswpElement, this.photoswipeUi, items, options);
        this.photoswipe[uid].init();
      }, {passive: false});
    });
  }


  /**
  * 起動時にURLに付くハッシュ値を解析
  * @return {void}
  **/
  parseHash() {
    const hash = window.location.hash.substring(1),
    params = {};

    if(hash.length < 5) {
      return params;
    }

    const vars = hash.split('&');
    for (let i = 0; i < vars.length; i++) {
      if(!vars[i]) {
        continue;
      }
      const pair = vars[i].split('=');
      if(pair.length < 2) {
        continue;
      }
      params[pair[0]] = pair[1];
    }

    return params;
  }

  /**
  * ハッシュ値に応じて起動
  * @param {HTMLElement} pswpElement - ポップアップテンプレートのHTML要素
  * @param {array} items - 画像情報の配列
  * @param {object} options - PhotoSwipeのオプション
  * @param {string} uid - 画像グループのuid
  * @return {void}
  **/
  openByHash(pswpElement, items, options, uid) {
    const hashData = this.parseHash();
    if(hashData.pid && hashData.gid) {
      options.galleryUID = hashData.gid;
      options.index = hashData.pid;
      this.photoswipe[uid] = new PhotoSwipe(pswpElement, this.photoswipeUi, items, options);
      this.photoswipe[uid].init();
    }
  }

}

new Main();

うん、毎度のことですが長くなってしまいました。笑
順番に内容を確認していきます。

まず先頭のこちら。

import "core-js/stable";
import "regenerator-runtime/runtime";

今回Webpack(5.52.1) + Babel(7.15.5)でビルドしますが、async/awaitを使いたいのでpolyfillを読み込みます。
npmでインストールしておきます。

$ npm install core-js@3 regenerator-runtime

次にMainクラスを作り、newでインスタンス化します。

export default class Main {
  constructor() {
    //メンバー変数
    this.photoswipe = []; //PhotoSwipeをインスタンス化した時に使う
    //関数を実行
    this.initPhotoswipe('[data-photoswipe="1"]');
  }
  //...
}

new Main();

constructorはクラスをインスタンス化した時に実行される初期化メソッドです。
ここではメンバー変数を2つ定義し、initPhotoswipe関数を実行しています。

PhotoSwipeの初期化処理

initPhotoswipe関数の中身を見ていきます。

/**
* PhotoSwipeの初期化
* @param {string} elemName - 画像グループのHTML要素名
* @param {object} options - PhotoSwipeのオプション
* @return {void}
**/
initPhotoswipe(elemName, options = {}) {
  //引数に指定した要素の存在チェック
  const galleryElements = document.querySelectorAll(elemName);
  if(!galleryElements[0]){
    return;
  }

    //テンプレートの挿入
  const template = this.addTemplate();
  const pswpElement = template.querySelector('.pswp');
  if(!pswpElement){
    return;
  }
  
  //見つかった要素を1グループとして、グループごとにPhotoSwipeを適用する
  for(let i = 0, l = galleryElements.length; i < l; i++) {
    //PhotoSwipeを画像グループごとに適用
    this.attachPhotoswipe(galleryElements[i], pswpElement, options, i);
  }
}

ここではPhotoSwipeの初期化処理を書きます。
公式サイトでは処理が一つの関数に全て書かれていて、流れが追いにくい感じだったので、関数を細かく分けて実行内容を細分化してみました。

まずここでは下記の内容を実行します。

  • addTemplate()
    ポップアップ表示用のHTMLを挿入する関数。HTMLは公式では手動でベタ書きする仕様になっているので自動で挿入します。
  • attachPhotoswipe()
    PhotoSwipeを適用する処理をまとめて実行する関数。画像グループごとに実行させる。

ここでのポイントはdata-photoswipe属性が複数あった場合でも、各画像グループごとにPhotoSwipeを実行できるようにしていることです。

具体的なPhotoSwipeの適用は、下記のattachPhotoswipe()関数の処理になります。
awaitを使いたいのでasyncを設定しておきます。

/**
* Photoswipeの適用
* @param {HTMLElement} elem - PhotoSwipeを適用するHTML要素
* @param {HTMLElement} pswpElement - ポップアップテンプレートのHTML要素
* @param {object} - PhotoSwipeのオプション
* @param {number} -i 画像グループのインデックス
* @return {void}
**/
async attachPhotoswipe(elem, pswpElement, options, i){
  //画像グループごとのユニークIDをdata属性に付与しておく
  const uid = addUid(elem, i);

  //画像情報を取得
  const items = await this.parseThumbnailElements(elem);
  //オプションの初期設定
  const _options = this.initOptions(elem, items, options);
  
  //画像をクリックした時の処理
  this.onThumbnailsClick(pswpElement, items, _options, uid);
  
  //historyオプションがtrueの場合のみ
  if(('history' in _options) && _options.history){
    //URLに#がある場合に該当の画像のポップアップを実行
    this.openByHash(pswpElement, items, _options, uid);
  }
}

ここでは下記の関数を実行します。

  • addUid()
    画像グループごとのユニークIDをdata-pswp-uid属性に設定する。
  • parseThumbnailElements()
    リストの画像情報をデータ化して返す関数。画像情報のデータはPhotoSwipeをインスタンス化して実行する時に引数に指定する必要があります。画像情報のデータを取得する時に非同期処理をはさむのでasyncで宣言しています。
  • initOptions()
    初期化に必要なオプションを返す関数。オプションはPhotoSwipeをインスタンス化する時に必要になります。
  • onThumbnailsClick()
    画像のクリックイベント(クリックすると画像を表示させる)処理の関数。PhotoSwipeにはクリックイベントは入っていないので、画像をクリックした時にポップアップを作動させるようにここで実装する必要があります。
  • openByHash()
    URLに#(ハッシュ)がある場合に該当の画像のポップアップを実行する関数。ポップアップした時の画像ごとに個別のURLを付ける場合(オプションがhistory:true)の対応。

この中でも画像情報を取得するparseThumbnailElements()関数が最初のポイントとなるので詳しく見ていきます。

画像情報を配列化する

parseThumbnailElements()関数で指定した要素の子要素から画像のリスト情報を配列化して取得します。

/**
* 画像情報の配列を取得
* @param {HTMLElement} target - 画像グループのHTML要素
* @return {promise} - 画像情報の配列をPromiseで返す
**/
async parseThumbnailElements(target) {
  //データを入れていく空の配列を用意
  const items = [];
  //ループを回す時にインデックスを取得する
  let index = 0;
  //直下の子要素をループ
  for(let el of target.children){
    let src, width, height, size;
    
    //aタグを取得
    const linkEl = el.getElementsByTagName('a')[0];
    //aタグがなければスキップ
    if(!linkEl){
      return;
    }

    //画像のURLを取得
    src = linkEl.href;
    
    //imgタグを取得
    const imgEl = el.getElementsByTagName('img')[0];
    //imgタグがなければスキップ
    if(!imgEl){
      return;
    }

    //aタグにdata-size属性がある場合
    if(linkEl.getAttribute('data-size')){
      //横x縦でサイズを取得
      size = linkEl.getAttribute('data-size').split('x');
      width = parseInt(size[0], 10);
      height = parseInt(size[1], 10);
    }
    //data-size属性がない場合
    else{
      //ポップアップ指定した画像のURLから画像の本来の横・縦サイズを取得
      const img = await this.loadImage(src);
      width = img.naturalWidth;
      height = img.naturalHeight;
    }
        
        //画像情報を作成
    const item = {
      src : src,
      w: width,
      h: height,
      el: linkEl,
      index : index
    }
    
    //配列に追加
    items.push(item);
    
    //インデックスをインクリメント
    ++index;
  };

  return items;
}

/**
* 画像の非同期読み込み
* @param {string} src - 画像のURL
* @return {promise} - imageオブジェクトをPromiseで返す
**/
loadImage(src) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    //画像を読み込んで情報を取得して返す
    img.onload = () => resolve(img);
    //画像の読み込みエラー処理
    img.onerror = (e) => reject(e);
    //画像のソースURLを設定
    img.src = src;
  });
}

ポイントは配列の内容です。
公式では下記のような形式の配列を指定しています。

var items = [
    {
        src: 'https://placekitten.com/600/400',
        w: 600,
        h: 400
    },
    {
        src: 'https://placekitten.com/1200/900',
        w: 1200,
        h: 900
    }
];

要するに画像URL(src)と画像の縦・横(wh)を各画像ごとにオブジェクト化して配列にまとめればOK。

srcはaタグに設定したhref属性から取得します。

//aタグを取得
const linkEl = el.getElementsByTagName('a')[0];
//画像のURLを取得
src = linkEl.href;

画像のサイズは同じaタグに設定したdata-size属性から取得します。

if(linkEl.getAttribute('data-size')){
  size = linkEl.getAttribute('data-size').split('x');
  width = parseInt(size[0], 10);
  height = parseInt(size[1], 10);
}

data-size属性がない場合は、画像の実サイズを取得します。

else{
  const img = await this.loadImage(src); //画像を読み込み
  width = img.naturalWidth;
  height = img.naturalHeight;
}

ここでasync/awaitを使ってloadImage関数から画像サイズを取得します。
loadImage関数は引数のURLからImageオブジェクトを作りPromiseで返します。

/**
* 画像の非同期読み込み
* @param {string} src - 画像のURL
* @return {promise} - imageオブジェクトをPromiseで返す
**/
loadImage(src) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    //画像を読み込んで情報を取得して返す
    img.onload = () => resolve(img);
    //画像の読み込みエラー処理
    img.onerror = (e) => reject(e);
    //画像のソースURLを設定
    img.src = src;
  });
}

これでdata-size属性がなくても実サイズ情報で画像がポップアップしてくれるようになります。

画像情報のオブジェクトには、後で使い回したいので他に取得したaタグのHTMLElement情報と、ループのインデックス(要素の順序)を入れておきます。

//画像情報を作成
const item = {
  src : src,
  w: width,
  h: height,
  el: linkEl, //aタグ
  index : index //インデックス(要素の順序)
}

//配列に追加
items.push(item);

クリックイベント

次にonThumbnailsClick()関数です。
クリックした時にPhotoSwipeを起動させます。

/**
* 画像のクリック
* @param {HTMLElement} pswpElement - ポップアップテンプレートのHTML要素
* @param {array} items - 画像情報の配列
* @param {object} options - PhotoSwipeのオプション
* @param {string} uid - 画像グループのuid
* @return {void}
**/
onThumbnailsClick(pswpElement, items, options, uid) {
  //画像情報をループで回す
  items.forEach((item, index) => {
    item.el.addEventListener('click', (e) => {
      e.preventDefault();
      options.index = index;
      //PhotoSwipeをインスタンス化してuidごとにメンバー変数に保存する
      this.photoswipe[uid] = new PhotoSwipe( pswpElement, this.photoswipeUi, items, options);
      //PhotoSwipe実行
      this.photoswipe[uid].init(); 
    }, {passive: false});
  });
}

先ほどparseThumbnailElements()で取得した情報を使ってループを回して処理します。
aタグをクリックしたら、optionにインデックスを指定して、クリックした要素をポップアップさせます。

PhotoSwipeのインスタンスはuidをプロパティにしたオブジェクトにメンバー変数として保存しておきます。

これで基本的な「画像をクリックしてポップアップさせる」動作はできました。

あとはinitOptions()関数によるオプションの初期化です。

オプションの初期化

/**
* オプションの初期化
* @param {HTMLElement} galleryElement - 画像グループのHTML要素
* @param {array} items - 画像情報の配列
* @param {object} options - PhotoSwipeのオプション
* @return {object}
**/
initOptions(galleryElement, items, options = {}) {
  //オプションをディープコピー
  const _options = JSON.parse(JSON.stringify(options));
  
  if(!('galleryUID' in _options)){
    //uidは初期化の時に設定しておいた'data-pswp-uid'を使う
    _options.galleryUID = galleryElement.getAttribute('data-pswp-uid')
  }

  if(!('getThumbBoundsFn' in _options)){
    //画像がポップアップする時の画像のアニメーションエフェクトに使う座標を設定(公式に説明あり)
    _options.getThumbBoundsFn = (index) => {
      const thumbnail = items[index].el.getElementsByTagName('img')[0],
      pageYScroll = window.pageYOffset || document.documentElement.scrollTop,
      rect = thumbnail.getBoundingClientRect();

      return {
        x:rect.left,
        y:rect.top + pageYScroll,
        w:rect.width
      };
    }
  }

  if(!('index' in _options)){
    //初期表示する画像を指定
    _options.index = 0;
  }

  if(!('history' in _options)){
    //PhotoSwipe起動時にURLに#を付けて個別のURLとして扱うかどうか
    _options.history = false;
  }

  if(!('shareEl' in _options)){
    //ポップアップ表示時の右上のシェアボタンの表示
    _options.shareEl = false;
  }

  return _options;
}

galleryUIDgetThumbBoundsFnは、公式のサンプルコードをほとんどそのまま使いました。
その他のオプションは初期化時に必要であれば指定しておきます。

ポイントは最初の行のオプションのディープコピーです。
画像グループごとに個別のオプションを適用したい場合にinitPhotoswipe()関数の引数で指定できるようにしているので、ディープコピーでコピー元とコピー先を明確に分けて処理します。

//第2引数に個別のオプションを指定
this.initPhotoswipe('[data-photoswipe="2"]', {
  history: true, //ポップアップ時の個別URLを付けるかどうか
  bgOpacity: 0.8, //背景の透過率(0~1)
  showHideOpacity : true, //ポップアップを閉じる時のアニメーションでに透過を入れるかどうか
  shareEl : true, //ポップアップ画面右上のシェアボタンを付けるかどうか
  shareButtons: [ // シェアボタンの内容
  {id:'facebook', label:'Share on Facebook', url:'https://www.facebook.com/sharer/sharer.php?u={{url}}'},
  {id:'twitter', label:'Tweet', url:'https://twitter.com/intent/tweet?text={{text}}&url={{url}}'},
  {id:'download', label:'Download image', url:'{{raw_image_url}}', download:true}
  ],
});

オプションについては公式を参考に。

https://photoswipe.com/documentation/options.html

URLのハッシュに応じて初期表示させる

PhotoSwipeは起動時にデフォルトでURLの末尾に「#&gid=1&pid=2」のようなハッシュが付いて起動した画面を個別のURLとして扱えるようになっています。使うにはオプションでhistory:true;を設定する必要がありますが、個人的にはあまり意味を感じないしなんか気持ち悪いのでオフってます。

ただ一応公式のサンプルコードに対応する内容が書いてあるので書いておきます。必要ない場合はオプションをhistory:false;に設定しておけば良いと思います。

/**
* ハッシュ値に応じて起動
* @param {HTMLElement} pswpElement - ポップアップテンプレートのHTML要素
* @param {array} items - 画像情報の配列
* @param {object} options - PhotoSwipeのオプション
* @param {string} uid - 画像グループのuid
* @return {void}
**/
openByHash(pswpElement, items, options, uid) {
  const hashData = this.parseHash();
  if(hashData.pid && hashData.gid) {
    options.galleryUID = hashData.gid;
    options.index = hashData.pid;
    this.photoswipe[uid] = new PhotoSwipe(pswpElement, this.photoswipeUi, items, options);
    this.photoswipe[uid].init();
  }
}

ハッシュのpidgidを取得して、オプションに設定してPhotoSwipeを起動します。
gidgalleryUIDpidindexの情報になるので、parseHash()関数でURLから解析して取得し、そのまま設定します。

実行

あとはinitPhotoswipe()関数に引数を指定して実行するだけです。

第1引数に画像グループの要素名、第2引数にPhotoSwipeのオプションを指定します。

//オプションがデフォルトで良ければ第2引数は省略化
this.initPhotoswipe('[data-photoswipe="1"]');

//必要であれば第2引数にオプションを加える
this.initPhotoswipe('[data-photoswipe="2"]', {
  history: true,
  bgOpacity: 0.8,
  showHideOpacity : true,
  shareEl : true,
  shareButtons: [
  {id:'facebook', label:'Share on Facebook', url:'https://www.facebook.com/sharer/sharer.php?u={{url}}'},
  {id:'twitter', label:'Tweet', url:'https://twitter.com/intent/tweet?text={{text}}&url={{url}}'},
  {id:'download', label:'Download image', url:'{{raw_image_url}}', download:true}
  ],
});

Swiperと連携

スライダープラグインのSwiperと連携させてみます。
使い所としては、ECサイトの商品詳細ページの商品画像など。

今回はSwiperのバージョン7を使います。
(過去記事はv6までしか掲載していないので、v7はまた別途記事更新します)

新しいバージョン(v5)のデモ(2022.8.19追加)

この記事は少し古いバージョン(PhotoSwipe[v4]+Swiper[v7])の内容になるので、新しいバージョンのデモとそれに関する記事を書きました。

PhotoSwipe(v5)+ Swiper(v8)のデモ

GitHubはこちら。
https://github.com/inos3910/photoswipe-swiper-demo

HTML

まずはHTMLから。
Swiperの詳しい説明は割愛いたしますが、swiperswiper-wrapperswiper-slideが必ず必要になります。
最も最上位の要素にswiperクラスを付けますが、v6まではswiper-containerでしたのでv7で変更されています。
その直下にswiper-wrapper、その直下に各スライドswiper-slideとクラス名を付けた要素を配置します。
PhotoSwipe用のdata-photoswipe属性やaタグなどの要素は、前述したデモと全く同じ記述方法で大丈夫です。

<!-- CSS読み込み -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/7.0.6/swiper-bundle.css">

<div class="swiper">
  <ul class="swiper-wrapper" data-photoswipe="3">
    <li class="swiper-slide">
      <a href="assets/images/sample-1.jpg">
        <img src="assets/images/sample-1.jpg" width="1080" height="1080" alt="">
      </a>
    </li>
    <li class="swiper-slide">
      <a href="assets/images/sample-2.jpg">
        <img src="assets/images/sample-2.jpg" width="1080" height="1080" alt="">
      </a>
    </li>
    <li class="swiper-slide">
      <a href="assets/images/sample-3.jpg">
        <img src="assets/images/sample-3.jpg" width="1080" height="1080" alt="">
      </a>
    </li>
    <li class="swiper-slide">
      <a href="assets/images/sample-4.jpg">
        <img src="assets/images/sample-4.jpg" width="1080" height="1080" alt="">
      </a>
    </li>
  </ul>
  <!-- /.swiper-wrapper -->
  <button type="button" class="swiper-button-next"></button>
  <button type="button" class="swiper-button-prev"></button>
  <div class="swiper-pagination"></div>
</div>

JS

MainクラスにSwiperを適用する関数を用意。

//Webpackを使う場合はモジュールをインポート
import Swiper, { Navigation, Pagination, EffectCreative } from 'swiper';

/**
* Swiperの初期化
* @param {string} elemNode - Swiperを適用するのHTML要素
* @return {void}
**/
initSwiper(elemNode) {
  const swiperOptions = {
    grabCursor: true,
    modules: [Navigation, Pagination, EffectCreative],
    navigation: {
      nextEl: ".swiper-button-next",
      prevEl: ".swiper-button-prev",
    },
    pagination: {
      el: ".swiper-pagination",
      clickable: true
    },
    slidesPerView: 1,
    effect: "creative",
    creativeEffect: {
      prev: {
        shadow: true,
        translate: ["-125%", 0, -800],
        rotate: [0, 0, -90],
      },
      next: {
        shadow: true,
        translate: ["125%", 0, -800],
        rotate: [0, 0, 90],
      },
    },
  };

  return new Swiper(elemNode, swiperOptions);
}

オプションは適当ですが、今回のデモはloopが使えません。
Swiperの場合、looptrueにすると要素のコピーが末尾に追加されていくことになるので、Swiperによってコピーされた要素をPhotoSwipeのAPIを使ってPhotoSwipe起動時に毎回加えていく処理が必要になります。
その処理については時間の都合で今回は割愛します。

そしてSwiperとPhotoSwipeを連携させるポイントが次のコードになります。
attachPhotoswipe()関数を修正します。

/**
* Photoswipeの適用
* @param {HTMLElement} elem - PhotoSwipeを適用するHTML要素
* @param {HTMLElement} pswpElement - ポップアップテンプレートのHTML要素
* @param {object} - PhotoSwipeのオプション
* @param {number} -i 画像グループのuid
* @return {void}
**/
async attachPhotoswipe(elem, pswpElement, options, i){

  const uid = addUid(elem, i);
  const items = await this.parseThumbnailElements(elem);
  const _options = this.initOptions(elem, items, options);

  // ここから追記
  let swiper;
  //PhotoSwipeを適用する要素にswiper-wrapperクラスが付いている場合
  if(elem.classList.contains('swiper-wrapper')){
    その親要素にSwiperを適用
    swiper = this.initSwiper(elem.parentNode);
  }

  this.onThumbnailsClick(pswpElement, items, _options, uid, swiper); //引数にswiperインスタンスを追加

  // 追記ここまで

  if(('history' in _options) && _options.history){
    this.openByHash(pswpElement, items, _options, uid);
  }
}

コメントに書いた通りですが、PhotoSwipeを適用する時に同じ要素にswiper-wrapperクラスが付いているか判定し、付いている場合はその親要素(swiperクラスの付いた要素)にSwiperを適用します。initSwiper関数の戻り値はSwiperインスタンスなので、それをonThumbnailsClick関数の引数に追加します。

次にonThumbnailsClick関数を修正します。

/**
* 画像のクリック
* @param {HTMLElement} pswpElement - ポップアップテンプレートのHTML要素
* @param {array} items - 画像情報の配列
* @param {object} options - PhotoSwipeのオプション
* @param {sting} uid - 画像グループのuid
* @param {Object} swiper - Swiperインスタンス
* @return {void}
**/
onThumbnailsClick(pswpElement, items, options, uid, swiper) {
  items.forEach((item, index) => {
    item.el.addEventListener('click', (e) => {
      e.preventDefault();
      options.index = index;
      this.photoswipe[uid] = new PhotoSwipe( pswpElement, this.photoswipeUi, items, options);
      this.photoswipe[uid].init();
      //Swiperインスタンスがある場合
      if(swiper){
        //PhotoSwipeの画像が左右のナビゲーションのクリックやスワイプで切り替わった時にSwiperも該当の画像に切り替える
        this.photoswipe[uid].listen('afterChange', () => {
          swiper.slideTo(this.photoswipe[uid].getCurrentIndex());
        });
      }

    }, {passive: false});
  });
}

クリックしてPhotoSwipeを実行(this.photoswipe[i].init();)した後、引数のSwiperインスタンスがある場合にPhotoSwipeのAPIの中でafterChangeイベントを使ってPhotoSwipeの画像が切り替わった時に処理を追加します。

ここでは同時にSwiperのAPIのswiper.slideTo(index)を使って、PhotoSwipeの表示とSwiperの表示を合わせます。
PhotoSwipeはインスタンスを使ってpswp.getCurrentIndex();でインデックス番号を取得できるので、そのままswiper.slideTo()の引数に指定してあげればOKです。

これでPhotoSwipeでポップアップ時にスワイプしたり、左右のナビゲーションでスライドを切り替えた際、ポップアップを閉じてもSwiperの表示位置とずれなく一致させることができます。

この処理をしていない場合は、ポップアップ上で画像を切り替えてから閉じた時にSwiperは最初にクリックした時の表示のままになっているので、閉じた時に画像のアニメーションがズレてしまいます。
その例がこちら↓

PhotoSwipeとSwiperを連携させずに使った場合のデモ

上記デモでは、画像をクリックするとポップアップ表示はされますが、ポップアップ画面上で左右の矢印ボタンで切り替えたり、スワイプしたりして、最初にポップアップした画像と違う画像を表示した状態にして、そのまま閉じると、本来シームレスに元の画像位置に戻っていくはずのアニメーションの動きが崩れます。

PhotoSwipe + Swiper(v7)のデモ

実際の完成デモはこちら。

PhotoSwipe + Swiperのデモ

Swiperのv7から追加されたcreativeEffectを使ってみたかったので、クセ動きスライダーになってますが、普通のスライドやフェードにしても全然問題ないです。

さいごに

今回は高機能なLightbox系プラグインのPhotoSwipeの使い方についてまとめました。

PhotoSwipeは制作案件でけっこう使っていて、制作案件でパパッと使えるものの中ではUIとして優秀でほとんど文句の付け所がありません。ただせめてJS読み込ませるだけである程度動いてくれたら助かるのになーっていつも思っていて、いい機会なので自分用にコードを整理してみました。今まで公式のサンプルコードや誰かが使いやすくまとめたプラグインを時短のために脳死で使っていたので、細かく見直すことで勉強になりました。Classを使ってVanillaJSで書いたのでTS化もしやすいかなと思います。

今回は現状の最新バージョンのv4.1.3を使いましたが、いまbeta版が公開されているv5はきっと使いやすくなってるはず!
期待しながら正式版を待つことにします。

記事一覧

RELATED

inoue
TIPS

【CSS + JS】テキストの無限ループアニメーション

inoue
TIPS

【Shopify】カートへの遷移を飛ばしてチェックアウトに進むボタンを設置する

inoue
TIPS

【JS】PhotoSwipe v5を使って画像をポップアップ表示する

inoue
TIPS

gulpでjpg・pngからAVIF画像を生成する

inoue
TIPS

【JS】MutationObserverでDOMを監視[PhotoSwipe(v5)+Swiper(v8)連携]

inoue
TIPS

【Google Sheets API】 スプレッドシートのデータをJSONで取得する

NEW POSTS

inoue
TRY

【WordPress】GA4連携の人気記事ランキング機能を自作プラグイン化してみた

inoue
TRY

【netlify】ビルドイメージを更新 [ Ubuntu Xenial 16.04 → Ubuntu Focal 20.04 ]

inoue
TIPS

【PHP】Google Business Profile APIを使ってクチコミを取得する

inoue
TRY

スクリーンショットのAPI「screendot」を使ってみた

inoue
TIPS

【window.matchMedia】メディアクエリでhoverが使えるデバイスを判定

inoue
TIPS

lax.jsの使い方【スクロール連動アニメーションの実装】

ブログ記事一覧
  • HOME
  • TIPS
  • PhotoSwipeを使って画像をポップアップ表示する【Swiper(v7)との連携あり】