photoswipeのプレビュー 下部にサムネイル一覧を表示する

photoswipeというjavascriptを使った画像ギャラリーを簡単に作成できるプラグインがあります。
[photoswipe](http://photoswipe.com/)

非常に使いやすく、コーディングも楽なので重宝させていただいています。
スライドショー的な意味合いで使わせていただいていたのですが、客先から画面下部にスライド（サムネイル）一覧を表示したいとの要望があがり、不完全ながら対応できたので紹介します。
その後特に不満等は上がってないようなので概ね使いやすいようです。


# バージョン
- photoswipe 最新版
- jquery : 2.1.1


# 仕様
- 一覧のボタンクリックでギャラリーを表示する
- ギャラリー下部にサムネイル一覧を表示する。サムネイルクリックで画像切り換え
- ギャラリーの画像が1枚のみの場合はサムネイル一覧は表示しない

## 画像

### 種類と枚数
| No.       |   種類     |    画像枚数    |
|:----------:|:----------|------------------:|
| 1         |       壁紙 |        10        |
| 2         |         花 |       5       |
| 3         |    Windows |        1        |

### サイズ
- 全画面用：1920x1200
- サムネイル：150x93

# サンプル

とりあえずサンプル（[Photoswipeサムネイル一覧付き動的ギャラリー](http://www.elegant-house-c.sakura.ne.jp/demo/201802_thumb/)）。
javascriptとかphotoswipeとか使ってるよって人はソース見てください。


# ディレクトリ構成

``` ディレクトリ構成
 ├ css <-- CSS
 ├ js  <-- javascript
 └ 201802_thumb
       ├ index.html <-- メインHTML
       └ images <-- 画像格納ディレクトリ
           └ gallery_*
               ├ **_01.jpg
               └ **_01_thumb.jpg
```

# HTML
## header部

``` index.html
<!DOCTYPE html>
<html>
<head>
<title>Photoswipeサムネイル一覧付き動的ギャラリー</title>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>

<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-touch-fullscreen" content="yes">
<link rel="stylesheet" type="text/css" href="../css/lib/bootstrap.min.css"  />
<link rel="stylesheet" type="text/css" href="../css/lib/photoswipe/photoswipe.css" />
<link rel="stylesheet" type="text/css" href="../css/lib/photoswipe/default-skin.css" />
<style>
<!--
table thead tr th {
    background-color: #2e6da4;
    color: #fff;
}
table tbody tr td:nth-of-type(1), table tbody tr td:nth-of-type(3) {
    text-align: center;
}
#thumbnails-wrapper {
    position: absolute;
    width: 100%;
    bottom: 30px;
    display: block;
    z-index: 10;
    background-color: rgba(0, 0, 0, 0.5);
}
#thumbnails {
    padding: 20px 0 0 0;
    height: 200px;
    overflow-y: hidden;
    overflow-x: scroll;
    white-space: nowrap;
    position: relative;
    z-index: 3001;
}
.thumb-selected {
    border: 5px solid #ef8122;
    border-radius: 3px;
}
-->
</style>
</head>
```

ここのポイントは特になし。
`<style>`～`</style>`でサムネイル部分のCSSを定義しています。

## 一覧

``` index.html
<body>
    <div class="container-fluid">
        <h1>Photoswipeサムネイル一覧付き動的ギャラリー</h1>
        <div class="row">
            <div class="col-lg-6">
                <table class="table" style="width: 100%;">
                    <colgroup>
                        <col style="width: 50px;" />
                        <col style="width: 150px;" />
                        <col style="width: 100px;" />
                        <col style="width: 150px;" />
                    </colgroup>
                    <thead>
                        <tr>
                            <th>No.</th>
                            <th>ギャラリー名</th>
                            <th>詳細</th>
                            <th>備考</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>1</td>
                            <td>壁紙</td>
                            <td><button type="button" class="btn btn-primary btn-block" onclick="imageViewer.view(1);">詳細</button></td>
                            <td></td>
                        </tr>
                        <tr>
                            <td>2</td>
                            <td>花</td>
                            <td><button type="button" class="btn btn-primary btn-block" onclick="imageViewer.view(2);">詳細</button></td>
                            <td></td>
                        </tr>
                        <tr>
                            <td>3</td>
                            <td>Windows</td>
                            <td><button type="button" class="btn btn-primary btn-block" onclick="imageViewer.view(3);">詳細</button></td>
                            <td></td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
```

ただの一覧です。
` onclick="imageViewer.view(*);"`でボタンをクリックしたときのイベントを定義しています。


## photoswipeギャラリー部（非表示）

``` index.html
    <!-- photoswipe -->
    <div id="images" style="display: none;">
    </div>
```

初期表示では非表示ですが、ここに動的に画像ギャラリーを作成しますので必要な部分です。


## photoswipe部

``` index.html
    <!-- Root element of PhotoSwipe. Must have class pswp. -->
    <div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

        <!-- Background of PhotoSwipe. 
             It's a separate element as animating opacity is faster than rgba(). -->
        <div class="pswp__bg"></div>

        <!-- Slides wrapper with overflow:hidden. -->
        <div class="pswp__scroll-wrap">

            <!-- Container that holds slides. 
                PhotoSwipe keeps only 3 of them in the DOM to save memory.
                Don't modify these 3 pswp__item elements, data is added later on. -->
            <div class="pswp__container">
                <div class="pswp__item"></div>
                <div class="pswp__item"></div>
                <div class="pswp__item"></div>
            </div>

            <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
            <div class="pswp__ui pswp__ui--hidden">
                <div class="pswp__top-bar">

                    <!--  Controls are self-explanatory. Order can be changed. -->
                    <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>

                    <!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
                    <!-- element will get class pswp__preloader--active when preloader is running -->
                    <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 id="thumbnails-wrapper">
                    <ul id="thumbnails">
                    </ul>
                </div>

                <div class="pswp__caption">
                    <div class="pswp__caption__center"></div>
                </div>
            </div>
        </div>
    </div>
```

このエリアの大部分はphotoswipeの[公式サイト](http://photoswipe.com/documentation/getting-started.html)の記述と同じですが、
一部分サムネイル用に追加しています。
それが以下。

``` index.html
                <div id="thumbnails-wrapper">
                    <ul id="thumbnails">
                    </ul>
                </div>
```

ここにサムネイル一覧を表示します。


##  footer部

``` index.html
</body>
<script type="text/javascript" src="../js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="../js/lib/bootstrap.min.js"></script>
<script type="text/javascript" src="../js/lib/photoswipe/photoswipe.min.js"></script>
<script type="text/javascript" src="../js/lib/photoswipe/photoswipe-ui-default.min.js"></script>
<script type="text/javascript" src="../js/image_viewer.js"></script>
</html>
```

ただjavascriptを読んでいるだけなので特に説明はありません。


# javascript
## 起動時
``` image_viewer.js
$(function () {

    // ギャラリーのサムネイルクリック
    $(document).on('click touchend', '.thumbnail', function (e) {
        var thumbIndex = $(this).attr('id').replace(/thumbnail/, '');
        photoswipe.goTo(parseInt(thumbIndex));

        // サムネイルの選択状態を変化させる
        $('.thumb-selected').removeClass('thumb-selected');
        $(this).addClass('thumb-selected');

        e.stopPropagation();
    });

    $(document).on('touchmove', '#thumbnails-wrapper', function (e) {
        e.stopPropagation();
    });
});
```

サムネイルがクリックされた場合にサムネイルの選択状態を切り替える処理。
`thumb-selected`クラスの付け替え。
`e.stopPropagation`で以降のイベントの伝播を抑止。


## ギャラリー起動

### 1枚の場合

``` image_viewer.js
        if (galleryType == 3) {
            // 1ページのみの場合
            var imageItems = [
                {
                    src     : 'images/gallery_' + galleryType + '/' + ("0" + galleryType).slice(-2) + '_01.jpg',
                    w       : 1920,
                    h       : 1200,
                    title   : '画像1'
                }
            ];

            // サムネイルエリアは非表示にする
            $('#thumbnails-wrapper').hide();

            // photoswipeを起動
            var pswpElement = document.querySelectorAll('.pswp')[0];

            var options = {
                index           : 0,
                shareEl         : false,    // シェアボタン非表示
                zoomEl          : false,    // ズームボタン非表示
                fullscreenEl    : false,    // フルスクリーンボタン非表示
                closeOnScroll   : false,    // スクロールで終了しない
            };

            photoswipe = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, imageItems, options);
            photoswipe.init();
        }
```

No.3は1枚のみなので、サムネイル表示処理はありません。

### 複数枚の場合
``` image_viewer.js
        else {
            // 複数ページの場合
            // サムネイルを追加
            var thumbElements = '<ul id="thumbnails">';
            var imageItems = [];
            if (galleryType == 1) {
                thumbCount = 10;
            }
            else if (galleryType == 2) {
                thumbCount = 5;
            }

            for (var i = 0; i < thumbCount; i++) {
                var pageNo = i + 1;
                var imageInfo = {
                    thumb : 'images/gallery_' + galleryType + '/' + ("0" + galleryType).slice(-2) + '_' + ("0" + pageNo).slice(-2) + '_thumb.jpg',
                    name  : '画像' + pageNo,
                    page  : pageNo
                };

                imageItems[i] = {
                    src     : 'images/gallery_' + galleryType + '/' + ("0" + galleryType).slice(-2) + '_' + ("0" + pageNo).slice(-2) + '.jpg',
                    w       : 1920,
                    h       : 1200,
                    title   : imageInfo.name
                };

                // サムネイルエリアを表示状態にする
                $('#thumbnails-wrapper').show();

                var thumbSelectedClass = (parseInt(i) == 0) ? " thumb-selected" : "";

                // thumbnail
                thumbElements += "<li style='display: inline-block; width: 180px; height: 180px; margin-left: 10px; text-align: center; z-index: 15;'>";
                thumbElements += "<a style='display: inline;'>";
                thumbElements += "<img src='" + imageInfo.thumb + "' alt='" + imageInfo.name + "' width='150px' style='display: inline; z-index: 20;' class='thumbnail" + thumbSelectedClass + "' id='thumbnail" + i + "' />";
                thumbElements += "</a>";
                thumbElements += "<p style='text-align: center; display: block; color: #fff; font-size: 2.0em; font-weight: bold;'>" + imageInfo.page + "</p>"
                thumbElements += "</li>";
            }
            thumbElements += '</ul>';

            $('#thumbnails').remove();
            $('#thumbnails-wrapper').append(thumbElements);

            // photoswipeを起動
            var pswpElement = document.querySelectorAll('.pswp')[0];

            var options = {
                index           : 0,
                shareEl         : false,    // シェアボタン非表示
                zoomEl          : false,    // ズームボタン非表示
                fullscreenEl    : false,    // フルスクリーンボタン非表示
                closeOnScroll   : false,    // スクロールで終了しない
            };

            photoswipe = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, imageItems, options);
            photoswipe.init();

            photoswipe.listen('afterChange', function () {
                // サムネイルの選択状態を変化させる
                $('.thumb-selected').removeClass('thumb-selected');
                var currItem = '#thumbnail' + photoswipe.getCurrentIndex();
                $(currItem).addClass('thumb-selected');

                if ($(currItem)[0].offsetLeft > $(window).width() + $('#thumbnails').scrollLeft() ||
                    $(currItem)[0].offsetLeft < $('#thumbnails').scrollLeft()) {
                    // スクロール位置調整
                    $('#thumbnails').scrollLeft(parseInt($(currItem)[0].offsetLeft));
                }
            });

            $('#thumbnails').scrollLeft(0);

            var target = $('.pswp__ui');
            target.on('classChange', catchClassChangeEvent);
            var dsw = domStyleWatcher.Start(target, $('.pswp__ui').attr('class'));  // 監視開始
        }
```

基本的な部分と1枚の場合と同じ部分は省きます。

#### サムネイル追加
``` image_viewer.js
                // サムネイルエリアを表示状態にする
                $('#thumbnails-wrapper').show();

                var thumbSelectedClass = (parseInt(i) == 0) ? " thumb-selected" : "";

                // thumbnail
                thumbElements += "<li style='display: inline-block; width: 180px; height: 180px; margin-left: 10px; text-align: center; z-index: 15;'>";
                thumbElements += "<a style='display: inline;'>";
                thumbElements += "<img src='" + imageInfo.thumb + "' alt='" + imageInfo.name + "' width='150px' style='display: inline; z-index: 20;' class='thumbnail" + thumbSelectedClass + "' id='thumbnail" + i + "' />";
                thumbElements += "</a>";
                thumbElements += "<p style='text-align: center; display: block; color: #fff; font-size: 2.0em; font-weight: bold;'>" + imageInfo.page + "</p>"
                thumbElements += "</li>";
```

#### ギャラリー切り替えイベントの捕捉
``` image_viewer.js
            photoswipe.listen('afterChange', function () {
                // サムネイルの選択状態を変化させる
                $('.thumb-selected').removeClass('thumb-selected');
                var currItem = '#thumbnail' + photoswipe.getCurrentIndex();
                $(currItem).addClass('thumb-selected');

                if ($(currItem)[0].offsetLeft > $(window).width() + $('#thumbnails').scrollLeft() ||
                    $(currItem)[0].offsetLeft < $('#thumbnails').scrollLeft()) {
                    // スクロール位置調整
                    $('#thumbnails').scrollLeft(parseInt($(currItem)[0].offsetLeft));
                }
            });

            $('#thumbnails').scrollLeft(0);
```

画像が切り換えられた場合に、サムネイルも追随するようにします。
また、画面外のサムネイルが選択状態になった時にスクロールをずらすようにします。
初期表示はスクロールを一番左にします。


# ヘッダ部分とサムネイルの表示/非表示を連動させる

この状態だとサムネイル一覧が表示されたままなので、ヘッダ部分と表示を連動させるようにします。

## クラス変更イベントの監視

### 監視開始処理

``` image_viewer.js
            var target = $('.pswp__ui');
            target.on('classChange', catchClassChangeEvent);
            var dsw = domStyleWatcher.Start(target, $('.pswp__ui').attr('class'));  // 監視開始
```

`.pswp__ui`要素のクラスが変わった場合にイベントを呼ぶ処理を定義して、監視を開始。

### ウォッチャー

``` image_viewer.js
    // クラス変更ウォッチャー
    var domStyleWatcher = {
        Start: function (tgt, styleobj) {
            // 発生
            function eventHappen(data1, data2) {
                var throwval = $(tgt).attr('class');
                tgt.trigger('classChange', [throwval]); // eventを投げる
            }
            // 監視の登録
            var tge = tgt[0]; // jQueryオブジェクトをelementに変えてる
            var filter = ['class']; // classを見る
            var options = { // オプション
                attributes: true,
                attributeFilter: filter
            };
            var mutOb = new MutationObserver(eventHappen);  // 監視用インスタンス作成
            mutOb.observe(tge, options);    // 監視開始
            return mutOb;
        },
        Stop: function (mo) {
            mo.disconnect();
        }
    };
```

### クラス変更時の切り替え処理

``` image_viewer.js
    // クラス変更イベント検出
    function catchClassChangeEvent(event, value) {
        if (value.match(/pswp__ui--idle/) || value.match(/pswp__ui--hidden/)) {
            $('#thumbnails').hide();
        }
        else {
            $('#thumbnails').show();
        }
    }
```

`pswp__ui--idle`または`pswp__ui--hidden`クラスが付加された場合は非表示、それ以外は表示するようにします。


以上で、ギャラリーは完成です。