PhotoSwipeの使い方メモ。本家の設定がガチガチだったのでカスタム。

下記内容で簡単なLightBox系のプラグインを探していまして、「PhotoSwipe」を発見したので使い方のメモ。

  1. jQueryは使わない。
  2. 軽量。
  3. あんまりハデでない。
  4. カスタムが簡単。
  5. IE9+ & タッチデバイス対応。

PhotoSwipe?

タッチデバイスに対応したLightBox系の軽量で高機能なプラグイン。
オプションなども多すぎず少なすぎずでいい感じです。カスタムもしやすそうな気がします。

オプション機能

よく使いそうなもの

設定スクリプト

本家のスクリプトがかなりガチガチだったので使いやすくカスタム。

したかったこと

  • ダイアログコンテナをJavascriptから生成
  • 子要素をclass指定
  • クリック時に毎回画像リストを取得しているので、一回だけにする
  • エフェクトをいい感じに(個人的好み)
  • ダイアログOPEN時は完全にスクロール禁止
  • 遅延ロード系のプラグインと共存
  • css変数(カスタムプロパティ)でJavascriptと設定を共有

設定スタイル(Sass)

1
$transition-speed: 300ms !default;
2
:root {    
3
    --ps-transition-speed: #{inspect($transition-speed)};
4
}
5

6
body.noscroll {
7
    overflow: hidden;
8
    position: fixed;
9
    top: 0;
10
    right: 0;
11
    bottom: 0;
12
    left: 0;
13
    margin: auto;
14
}
15

16
// PhotoSwipe
17
figure.images {
18
    img {
19
        opacity: 0;
20
        transform: translate3d(0, 0, 0);
21
        transition: none;
22
    }
23
    img.lazyloaded {
24
        opacity: 1;
25
    }
26
    &.pswd-loaded {
27
        img {
28
            opacity: 0;
29
        }
30
    }
31
}

設定スクリプト(Javascript)

1
(() => {
2

3
    const initPhotoSwipeFromDOM = galleryClassName => {
4

5
        // デフォルト
6
        const galleries = document.getElementsByClassName(galleryClassName);
7
        const galleryNum = galleries.length;
8
        const itemClassName = 'images';
9
        const itemDOM = 'figure';
10
        const itemCaptionDOM = 'figcaption';
11
        const itemLoadedClass = 'pswd-loaded';
12
        const itemTargetAttr = 'src';
13
        const delay = 30;
14

15
        // フレームワーク
16
        const framework = {
17
            bind(target, type, listener, unbind) {
18
                const methodName = `${unbind ? 'remove' : 'add'}EventListener`;
19
                type = type.split(' ');
20
                for (let i = 0; i < type.length; i++) {
21
                    if (type[i]) {
22
                        target[methodName](type[i], listener, false);
23
                    }
24
                }
25
            },
26
            unwrap(el) {
27
                while (el.firstChild) {
28
                    el.parentNode.insertBefore(el.firstChild, el);
29
                }
30
                el.remove();
31
            },
32
            closest(el, fn) {
33
                return el &#038;&#038; (fn(el) ? el : framework.closest(el.parentNode, fn));
34
            },
35
            toggleClass(el, className) {
36
                if (framework.hasClass(el, className)) {
37
                    framework.removeClass(el, className);
38
                } else {
39
                    framework.addClass(el, className);
40
                }
41
            },
42
            removeClass(el, className) {
43
                const reg = new RegExp(`(\\s|^)${className}(\\s|$)`);
44
                el.className = el.className.replace(reg, ' ').replace(/^\s\s*/, '').replace(/\s\s*$/, '');
45
            },
46
            addClass(el, className) {
47
                if (!framework.hasClass(el, className)) {
48
                    el.className += (el.className ? ' ' : '') + className;
49
                }
50
            },
51
            hasClass(el, className) {
52
                return el.className &#038;&#038; new RegExp(`(^|\\s)${className}(\\s|$)`).test(el.className);
53
            }
54
        };
55

56
        // :rootのカスタムプロパティを取得
57
        const getCustomProperty = property => {
58
            const style = getComputedStyle(document.documentElement);
59
            return String(style.getPropertyValue(property)).trim();
60
        };
61

62
        // :rootのカスタムプロパティをセット
63
        const setCustomProperty = (property, default_value) => {
64
            const value = getCustomProperty(property);
65
            return value ? value : default_value;
66
        };
67

68
        // スクロール位置
69
        const scrollPosition = (() => {
70
            if ('scrollingElement' in document) return document.scrollingElement;
71
            if (navigator.userAgent.includes('WebKit')) return document.body;
72
            return document.documentElement;
73
        })();
74

75
        // スクロール禁止
76
        const fixedScroll = offsetY => {
77
            const _body = document.body;
78
            _body.style.marginTop = `${-offsetY}px`;
79
            framework.addClass(_body, 'noscroll');
80
        };
81

82
        // スクロールを戻す
83
        const resetScroll = offsetY => {
84
            const _body = document.body;
85
            setTimeout(() => {
86
                framework.removeClass(_body, 'noscroll');
87
                _body.style.marginTop = 0;
88
                scrollPosition.scrollTop = offsetY;
89
            });
90
        };
91

92
        // 開いている画像の元画像にclass名を追加
93
        const activeDialogItems = (index, galleryIndex) => {
94
            const galleryItems = galleries[galleryIndex].getElementsByClassName(itemClassName);
95
            setTimeout(() => {
96
                framework.addClass(galleryItems[index], itemLoadedClass);
97
            }, delay);
98
        };
99

100
        // 開いている画像用のclass名を削除
101
        const resetDialogItems = galleryIndex => {
102
            const galleryItems = galleries[galleryIndex].getElementsByClassName(itemClassName);
103
            const numGalleryItems = galleryItems.length;
104
            for (let i = 0; i < numGalleryItems; ++i) {
105
                framework.removeClass(galleryItems[i], itemLoadedClass);
106
            }
107
        };
108

109
        const speed = parseInt(setCustomProperty('--ps-transition-speed', '300ms'));
110

111
        // ギャラリー用のダイアログ
112
        const pswd_wrap = document.createElement("div");
113
        pswd_wrap.id = "pswd_landing";
114
        pswd_wrap.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>';
115

116
        document.body.appendChild(pswd_wrap);
117
        framework.unwrap(document.querySelector('#pswd_landing'));
118

119
        // 画像情報
120
        const galleryArray = [];
121
        for (var index = 0; index < galleryNum; ++index) {
122
            const galleryItems = galleries[index].getElementsByClassName(itemClassName);
123
            const numGalleryItems = galleryItems.length;
124
            const galleryItem = [];
125
            let targetEl;
126
            let linkEl;
127
            let size;
128
            let item;
129

130
            for (let i = 0; i < numGalleryItems; ++i) {
131

132
                targetEl = galleryItems[i];
133
                if (targetEl.nodeType !== 1) {
134
                    continue;
135
                }
136

137
                linkEl = targetEl.children[0];
138
                size = linkEl.getAttribute('data-size').split('x');
139
                item = {
140
                    src: linkEl.getAttribute('href'),
141
                    w: parseInt(size[0], 10),
142
                    h: parseInt(size[1], 10)
143
                };
144
                if (targetEl.children.length > 1) {
145
                    const itemCaption = targetEl.getElementsByTagName(itemCaptionDOM.toLowerCase());
146
                    item.title = '';
147
                    if (itemCaption) {
148
                        item.title = itemCaption.innerHTML;
149
                    }
150
                }
151
                if (linkEl.children.length > 0) {
152
                    item.msrc = linkEl.children[0].getAttribute(itemTargetAttr);
153
                }
154

155
                item.el = targetEl;
156
                galleryItem.push(item);
157
            }
158
            galleryArray[index] = galleryItem;
159
        };
160

161
        // クリックイベント
162
        const onThumbnailsClick = (galleryIndex, e) => {
163
            e = e || window.event;
164
            const eTarget = e.target || e.srcElement;
165

166
            if (eTarget.tagName.toUpperCase() !== "IMG") {
167
                return;
168
            }
169
            e.preventDefault ? e.preventDefault() : e.returnValue = false;
170

171
            const clickedListItem = framework.closest(eTarget, el => el.tagName && el.tagName.toUpperCase() === itemDOM.toUpperCase());
172

173
            if (!clickedListItem) {
174
                return;
175
            }
176

177
            const clickedListItemParent = framework.closest(clickedListItem, el => el.className && el.classList.contains(galleryClassName) == true);
178

179
            const childNodes = clickedListItemParent.getElementsByClassName(itemClassName);
180
            const numChildNodes = childNodes.length;
181
            let nodeIndex = 0;
182
            let itemIndex;
183

184
            for (let i = 0; i < numChildNodes; i++) {
185
                if (childNodes[i].nodeType !== 1) {
186
                    continue;
187
                }
188

189
                if (childNodes[i] === clickedListItem) {
190
                    itemIndex = nodeIndex;
191
                    break;
192
                }
193
                nodeIndex++;
194
            }
195

196
            if (itemIndex >= 0) {
197
                openPhotoSwipe(itemIndex, galleryIndex);
198
            }
199

200
            return false;
201
        };
202

203
        // ギャラリーオープン時
204
        var openPhotoSwipe = (itemIndex, galleryIndex, disableAnimation, fromURL) => {
205
            const pswpElement = document.querySelectorAll('.pswp')[0];
206
            let gallery;
207
            let options;
208
            let items;
209
            let offsetY;
210

211
            items = galleryArray[galleryIndex];
212
            offsetY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
213
            options = {
214
                galleryUID: galleryIndex + 1,
215
                getThumbBoundsFn(index) {
216
                    const thumbnail = items[index].el.getElementsByTagName('img')[0];
217
                    const pageYScroll = offsetY;
218
                    const rect = thumbnail.getBoundingClientRect();
219
                    return { x: rect.left, y: rect.top + pageYScroll, w: rect.width };
220
                },
221
                index: parseInt(itemIndex, 10),
222
                showAnimationDuration: speed,
223
                closeOnScroll: false,
224
                closeOnVerticalDrag: false,
225
                shareEl: false
226
            };
227

228
            // リロード前にギャラリーが開いていたらギャラリーを開いたままにする
229
            if (fromURL) {
230
                if (options.galleryPIDs) {
231
                    for (let j = 0; j < items.length; j++) {
232
                        if (items[j].pid == itemIndex) {
233
                            options.index = j;
234
                            break;
235
                        }
236
                    }
237
                } else {
238
                    options.index = parseInt(itemIndex, 10) - 1;
239
                }
240
            } else {
241
                options.index = parseInt(itemIndex, 10);
242
            }
243
            if (isNaN(options.index)) {
244
                return;
245
            }
246
            if (disableAnimation) {
247
                options.showAnimationDuration = 0;
248
            }
249

250
            // ギャラリーを実行
251
            gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options);
252
            gallery.init();
253

254
            // スクロール禁止
255
            fixedScroll(offsetY);
256

257
            // Active dialog
258
            activeDialogItems(itemIndex, galleryIndex);
259
            gallery.listen('close', function () {
260
                activeDialogItems(this.getCurrentIndex(), galleryIndex);
261
            });
262

263
            // ダイアログをリセット
264
            gallery.listen('afterChange', () => {
265
                resetDialogItems(galleryIndex);
266
            });
267
            gallery.listen('destroy', () => {
268
                resetDialogItems(galleryIndex);
269
                resetScroll(offsetY);
270
            });
271

272
            // フルスクリーンを閉じた時
273
            framework.bind(document, 'fullscreenchange webkitfullscreenchange mozfullscreenchange msfullscreenchange', () => {
274
                if (document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement !== null) {
275
                    setTimeout(() => {
276
                        resetDialogItems(galleryIndex);
277
                    }, delay);
278
                }
279
            });
280

281
            // リサイズ
282
            gallery.listen('resize', () => {
283
                pswdDummy.style.height = `${pswdSticky.clientHeight + 1}px`;
284
            });
285
        };
286

287
        for (var index = 0; index < galleryNum; ++index) {
288
            const _gallery = galleries[index];
289
            _gallery.setAttribute('data-pswp-uid', index + 1);
290
            _gallery.onclick = onThumbnailsClick.bind(null, index);
291
        };
292

293
        // ブラウザ履歴
294
        const photoswipeParseHash = () => {
295
            const hash = window.location.hash.substring(1);
296
            const params = {};
297
            if (hash.length < 5) {
298
                return params;
299
            }
300
            const vars = hash.split('&#038;');
301
            for (let i = 0; i < vars.length; i++) {
302
                if (!vars[i]) {
303
                    continue;
304
                }
305
                const pair = vars[i].split('=');
306
                if (pair.length < 2) {
307
                    continue;
308
                }
309
                params[pair[0]] = pair[1];
310
            }
311
            if (params.gid) {
312
                params.gid = parseInt(params.gid, 10);
313
            }
314
            return params;
315
        };
316

317
        const hashData = photoswipeParseHash();
318
        if (hashData.pid &#038;&#038; hashData.gid) {
319
            openPhotoSwipe(hashData.pid - 1, hashData.gid - 1, true, true);
320
        }
321
    };
322

323
    // PhotoSwipeを実行(className)
324
    initPhotoSwipeFromDOM('image-gallery');
325

326
    // 元画像が表示されたらリンク先の画像をプリロード
327
    const preloadObserver = new IntersectionObserver(changes => {
328
        for (const change of changes) {
329
            if (change.intersectionRatio > 0) {
330
                let target = change.target;
331
                let img = target.querySelector('img');
332
                img.className = `${img.className} preloaded`;
333

334
                preloadObserver.unobserve(target);
335
                preloadImage(target.href);
336
            }
337
        }
338
    }, {
339
        rootMargin: '5%'
340
    });
341

342
    const targetImages = [...document.querySelectorAll('.image-gallery figure.images a')];
343
    targetImages.forEach(image => {
344
        preloadObserver.observe(image);
345
    });
346

347
    let preloadImage = url => {
348
        let img = new Image();
349
        img.src = url;
350
    };
351
})();