intranet-pictures 1.0.6 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/lib/intranet/pictures/responder.rb +27 -18
  3. data/lib/intranet/pictures/version.rb +1 -1
  4. data/lib/intranet/resources/haml/pictures_browse.haml +0 -1
  5. data/lib/intranet/resources/haml/pictures_home.haml +0 -3
  6. data/lib/intranet/resources/locales/en.yml +0 -1
  7. data/lib/intranet/resources/locales/fr.yml +0 -1
  8. data/lib/intranet/resources/www/jpictures.js +32 -14
  9. data/lib/intranet/resources/www/photoswipe/photoswipe-dynamic-caption-plugin.css +47 -0
  10. data/lib/intranet/resources/www/photoswipe/photoswipe-dynamic-caption-plugin.esm.js +400 -0
  11. data/lib/intranet/resources/www/photoswipe/photoswipe-lightbox.esm.js +1382 -0
  12. data/lib/intranet/resources/www/photoswipe/photoswipe-lightbox.esm.js.map +1 -0
  13. data/lib/intranet/resources/www/photoswipe/photoswipe-lightbox.esm.min.js +5 -0
  14. data/lib/intranet/resources/www/photoswipe/photoswipe.css +383 -142
  15. data/lib/intranet/resources/www/photoswipe/photoswipe.esm.js +5279 -0
  16. data/lib/intranet/resources/www/photoswipe/photoswipe.esm.js.map +1 -0
  17. data/lib/intranet/resources/www/photoswipe/photoswipe.esm.min.js +5 -0
  18. data/lib/intranet/resources/www/style.css +13 -0
  19. data/spec/intranet/pictures/responder_spec.rb +78 -58
  20. metadata +26 -28
  21. data/lib/intranet/resources/haml/pictures_photoswipe.haml +0 -23
  22. data/lib/intranet/resources/www/photoswipe/LICENSE +0 -21
  23. data/lib/intranet/resources/www/photoswipe/default-skin/default-skin.css +0 -484
  24. data/lib/intranet/resources/www/photoswipe/default-skin/default-skin.png +0 -0
  25. data/lib/intranet/resources/www/photoswipe/default-skin/default-skin.svg +0 -1
  26. data/lib/intranet/resources/www/photoswipe/default-skin/preloader.gif +0 -0
  27. data/lib/intranet/resources/www/photoswipe/photoswipe-ui-default.js +0 -861
  28. data/lib/intranet/resources/www/photoswipe/photoswipe-ui-default.min.js +0 -4
  29. data/lib/intranet/resources/www/photoswipe/photoswipe.js +0 -3734
  30. data/lib/intranet/resources/www/photoswipe/photoswipe.min.js +0 -4
@@ -0,0 +1,1382 @@
1
+ /*!
2
+ * PhotoSwipe Lightbox 5.2.4 - https://photoswipe.com
3
+ * (c) 2022 Dmytro Semenov
4
+ */
5
+ /**
6
+ * Creates element and optionally appends it to another.
7
+ *
8
+ * @param {String} className
9
+ * @param {String|NULL} tagName
10
+ * @param {Element|NULL} appendToEl
11
+ */
12
+ function createElement(className, tagName, appendToEl) {
13
+ const el = document.createElement(tagName || 'div');
14
+ if (className) {
15
+ el.className = className;
16
+ }
17
+ if (appendToEl) {
18
+ appendToEl.appendChild(el);
19
+ }
20
+ return el;
21
+ }
22
+
23
+ /**
24
+ * Get transform string
25
+ *
26
+ * @param {Number} x
27
+ * @param {Number|null} y
28
+ * @param {Number|null} scale
29
+ */
30
+ function toTransformString(x, y, scale) {
31
+ let propValue = 'translate3d('
32
+ + x + 'px,' + (y || 0) + 'px'
33
+ + ',0)';
34
+
35
+ if (scale !== undefined) {
36
+ propValue += ' scale3d('
37
+ + scale + ',' + scale
38
+ + ',1)';
39
+ }
40
+
41
+ return propValue;
42
+ }
43
+
44
+ /**
45
+ * Apply width and height CSS properties to element
46
+ */
47
+ function setWidthHeight(el, w, h) {
48
+ el.style.width = (typeof w === 'number') ? (w + 'px') : w;
49
+ el.style.height = (typeof h === 'number') ? (h + 'px') : h;
50
+ }
51
+
52
+ const LOAD_STATE = {
53
+ IDLE: 'idle',
54
+ LOADING: 'loading',
55
+ LOADED: 'loaded',
56
+ ERROR: 'error',
57
+ };
58
+
59
+
60
+ /**
61
+ * Check if click or keydown event was dispatched
62
+ * with a special key or via mouse wheel.
63
+ *
64
+ * @param {Event} e
65
+ */
66
+ function specialKeyUsed(e) {
67
+ if (e.which === 2 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
68
+ return true;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Parse `gallery` or `children` options.
74
+ *
75
+ * @param {Element|NodeList|String} option
76
+ * @param {String|null} legacySelector
77
+ * @param {Element|null} parent
78
+ * @returns Element[]
79
+ */
80
+ function getElementsFromOption(option, legacySelector, parent = document) {
81
+ let elements = [];
82
+
83
+ if (option instanceof Element) {
84
+ elements = [option];
85
+ } else if (option instanceof NodeList || Array.isArray(option)) {
86
+ elements = Array.from(option);
87
+ } else {
88
+ const selector = typeof option === 'string' ? option : legacySelector;
89
+ if (selector) {
90
+ elements = Array.from(parent.querySelectorAll(selector));
91
+ }
92
+ }
93
+
94
+ return elements;
95
+ }
96
+
97
+ /**
98
+ * Check if variable is PhotoSwipe class
99
+ *
100
+ * @param {*} fn
101
+ * @returns Boolean
102
+ */
103
+ function isPswpClass(fn) {
104
+ return typeof fn === 'function'
105
+ && fn.prototype
106
+ && fn.prototype.goTo;
107
+ }
108
+
109
+ /**
110
+ * Base PhotoSwipe event object
111
+ */
112
+ class PhotoSwipeEvent {
113
+ constructor(type, details) {
114
+ this.type = type;
115
+ if (details) {
116
+ Object.assign(this, details);
117
+ }
118
+ }
119
+
120
+ preventDefault() {
121
+ this.defaultPrevented = true;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * PhotoSwipe base class that can listen and dispatch for events.
127
+ * Shared by PhotoSwipe Core and PhotoSwipe Lightbox, extended by base.js
128
+ */
129
+ class Eventable {
130
+ constructor() {
131
+ this._listeners = {};
132
+ this._filters = {};
133
+ }
134
+
135
+ addFilter(name, fn, priority = 100) {
136
+ if (!this._filters[name]) {
137
+ this._filters[name] = [];
138
+ }
139
+
140
+ this._filters[name].push({ fn, priority });
141
+ this._filters[name].sort((f1, f2) => f1.priority - f2.priority);
142
+
143
+ if (this.pswp) {
144
+ this.pswp.addFilter(name, fn, priority);
145
+ }
146
+ }
147
+
148
+ removeFilter(name, fn) {
149
+ if (this._filters[name]) {
150
+ this._filters[name] = this._filters[name].filter(filter => (filter.fn !== fn));
151
+ }
152
+
153
+ if (this.pswp) {
154
+ this.pswp.removeFilter(name, fn);
155
+ }
156
+ }
157
+
158
+ applyFilters(name, ...args) {
159
+ if (this._filters[name]) {
160
+ this._filters[name].forEach((filter) => {
161
+ args[0] = filter.fn.apply(this, args);
162
+ });
163
+ }
164
+ return args[0];
165
+ }
166
+
167
+ on(name, fn) {
168
+ if (!this._listeners[name]) {
169
+ this._listeners[name] = [];
170
+ }
171
+ this._listeners[name].push(fn);
172
+
173
+ // When binding events to lightbox,
174
+ // also bind events to PhotoSwipe Core,
175
+ // if it's open.
176
+ if (this.pswp) {
177
+ this.pswp.on(name, fn);
178
+ }
179
+ }
180
+
181
+ off(name, fn) {
182
+ if (this._listeners[name]) {
183
+ this._listeners[name] = this._listeners[name].filter(listener => (fn !== listener));
184
+ }
185
+
186
+ if (this.pswp) {
187
+ this.pswp.off(name, fn);
188
+ }
189
+ }
190
+
191
+ dispatch(name, details) {
192
+ if (this.pswp) {
193
+ return this.pswp.dispatch(name, details);
194
+ }
195
+
196
+ const event = new PhotoSwipeEvent(name, details);
197
+
198
+ if (!this._listeners) {
199
+ return event;
200
+ }
201
+
202
+ if (this._listeners[name]) {
203
+ this._listeners[name].forEach((listener) => {
204
+ listener.call(this, event);
205
+ });
206
+ }
207
+
208
+ return event;
209
+ }
210
+ }
211
+
212
+ class Placeholder {
213
+ /**
214
+ * @param {String|false} imageSrc
215
+ * @param {Element} container
216
+ */
217
+ constructor(imageSrc, container) {
218
+ // Create placeholder
219
+ // (stretched thumbnail or simple div behind the main image)
220
+ this.element = createElement(
221
+ 'pswp__img pswp__img--placeholder',
222
+ imageSrc ? 'img' : '',
223
+ container
224
+ );
225
+
226
+ if (imageSrc) {
227
+ this.element.decoding = 'async';
228
+ this.element.alt = '';
229
+ this.element.src = imageSrc;
230
+ this.element.setAttribute('role', 'presentation');
231
+ }
232
+
233
+ this.element.setAttribute('aria-hiden', 'true');
234
+ }
235
+
236
+ setDisplayedSize(width, height) {
237
+ if (!this.element) {
238
+ return;
239
+ }
240
+
241
+ if (this.element.tagName === 'IMG') {
242
+ // Use transform scale() to modify img placeholder size
243
+ // (instead of changing width/height directly).
244
+ // This helps with performance, specifically in iOS15 Safari.
245
+ setWidthHeight(this.element, 250, 'auto');
246
+ this.element.style.transformOrigin = '0 0';
247
+ this.element.style.transform = toTransformString(0, 0, width / 250);
248
+ } else {
249
+ setWidthHeight(this.element, width, height);
250
+ }
251
+ }
252
+
253
+ destroy() {
254
+ if (this.element.parentNode) {
255
+ this.element.remove();
256
+ }
257
+ this.element = null;
258
+ }
259
+ }
260
+
261
+ class Content {
262
+ /**
263
+ * @param {Object} itemData Slide data
264
+ * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
265
+ * @param {Slide|undefined} slide Slide that requested the image,
266
+ * can be undefined if image was requested by something else
267
+ * (for example by lazy-loader)
268
+ */
269
+ constructor(itemData, instance, index) {
270
+ this.instance = instance;
271
+ this.data = itemData;
272
+ this.index = index;
273
+
274
+ this.width = Number(this.data.w) || Number(this.data.width) || 0;
275
+ this.height = Number(this.data.h) || Number(this.data.height) || 0;
276
+
277
+ this.isAttached = false;
278
+ this.hasSlide = false;
279
+ this.state = LOAD_STATE.IDLE;
280
+
281
+ if (this.data.type) {
282
+ this.type = this.data.type;
283
+ } else if (this.data.src) {
284
+ this.type = 'image';
285
+ } else {
286
+ this.type = 'html';
287
+ }
288
+
289
+ this.instance.dispatch('contentInit', { content: this });
290
+ }
291
+
292
+ removePlaceholder() {
293
+ if (this.placeholder && !this.keepPlaceholder()) {
294
+ // With delay, as image might be loaded, but not rendered
295
+ setTimeout(() => {
296
+ if (this.placeholder) {
297
+ this.placeholder.destroy();
298
+ this.placeholder = null;
299
+ }
300
+ }, 500);
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Preload content
306
+ *
307
+ * @param {Boolean} isLazy
308
+ */
309
+ load(isLazy, reload) {
310
+ if (!this.placeholder && this.slide && this.usePlaceholder()) {
311
+ // use -based placeholder only for the first slide,
312
+ // as rendering (even small stretched thumbnail) is an expensive operation
313
+ const placeholderSrc = this.instance.applyFilters(
314
+ 'placeholderSrc',
315
+ (this.data.msrc && this.slide.isFirstSlide) ? this.data.msrc : false,
316
+ this
317
+ );
318
+ this.placeholder = new Placeholder(
319
+ placeholderSrc,
320
+ this.slide.container
321
+ );
322
+ }
323
+
324
+ if (this.element && !reload) {
325
+ return;
326
+ }
327
+
328
+ if (this.instance.dispatch('contentLoad', { content: this, isLazy }).defaultPrevented) {
329
+ return;
330
+ }
331
+
332
+ if (this.isImageContent()) {
333
+ this.loadImage(isLazy);
334
+ } else {
335
+ this.element = createElement('pswp__content');
336
+ this.element.innerHTML = this.data.html || '';
337
+ }
338
+
339
+ if (reload && this.slide) {
340
+ this.slide.updateContentSize(true);
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Preload image
346
+ *
347
+ * @param {Boolean} isLazy
348
+ */
349
+ loadImage(isLazy) {
350
+ this.element = createElement('pswp__img', 'img');
351
+
352
+ if (this.instance.dispatch('contentLoadImage', { content: this, isLazy }).defaultPrevented) {
353
+ return;
354
+ }
355
+
356
+ if (this.data.srcset) {
357
+ this.element.srcset = this.data.srcset;
358
+ }
359
+
360
+ this.element.src = this.data.src;
361
+
362
+ this.element.alt = this.data.alt || '';
363
+
364
+ this.state = LOAD_STATE.LOADING;
365
+
366
+ if (this.element.complete) {
367
+ this.onLoaded();
368
+ } else {
369
+ this.element.onload = () => {
370
+ this.onLoaded();
371
+ };
372
+
373
+ this.element.onerror = () => {
374
+ this.onError();
375
+ };
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Assign slide to content
381
+ *
382
+ * @param {Slide} slide
383
+ */
384
+ setSlide(slide) {
385
+ this.slide = slide;
386
+ this.hasSlide = true;
387
+ this.instance = slide.pswp;
388
+
389
+ // todo: do we need to unset slide?
390
+ }
391
+
392
+ /**
393
+ * Content load success handler
394
+ */
395
+ onLoaded() {
396
+ this.state = LOAD_STATE.LOADED;
397
+
398
+ if (this.slide) {
399
+ this.instance.dispatch('loadComplete', { slide: this.slide, content: this });
400
+
401
+ // if content is reloaded
402
+ if (this.slide.isActive
403
+ && this.slide.heavyAppended
404
+ && !this.element.parentNode) {
405
+ this.slide.container.innerHTML = '';
406
+ this.append();
407
+ this.slide.updateContentSize(true);
408
+ }
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Content load error handler
414
+ */
415
+ onError() {
416
+ this.state = LOAD_STATE.ERROR;
417
+
418
+ if (this.slide) {
419
+ this.displayError();
420
+ this.instance.dispatch('loadComplete', { slide: this.slide, isError: true, content: this });
421
+ this.instance.dispatch('loadError', { slide: this.slide, content: this });
422
+ }
423
+ }
424
+
425
+ /**
426
+ * @returns {Boolean} If the content is currently loading
427
+ */
428
+ isLoading() {
429
+ return this.instance.applyFilters(
430
+ 'isContentLoading',
431
+ this.state === LOAD_STATE.LOADING,
432
+ this
433
+ );
434
+ }
435
+
436
+ isError() {
437
+ return this.state === LOAD_STATE.ERROR;
438
+ }
439
+
440
+ /**
441
+ * @returns {Boolean} If the content is image
442
+ */
443
+ isImageContent() {
444
+ return this.type === 'image';
445
+ }
446
+
447
+ /**
448
+ * Update content size
449
+ *
450
+ * @param {Number} width
451
+ * @param {Number} height
452
+ */
453
+ setDisplayedSize(width, height) {
454
+ if (!this.element) {
455
+ return;
456
+ }
457
+
458
+ if (this.placeholder) {
459
+ this.placeholder.setDisplayedSize(width, height);
460
+ }
461
+
462
+ if (this.instance.dispatch('contentResize', { content: this, width, height }).defaultPrevented) {
463
+ return;
464
+ }
465
+
466
+ setWidthHeight(this.element, width, height);
467
+
468
+ if (this.isImageContent() && !this.isError()) {
469
+ const image = this.element;
470
+ // Handle srcset sizes attribute.
471
+ //
472
+ // Never lower quality, if it was increased previously.
473
+ // Chrome does this automatically, Firefox and Safari do not,
474
+ // so we store largest used size in dataset.
475
+ if (image.srcset
476
+ && (!image.dataset.largestUsedSize || width > image.dataset.largestUsedSize)) {
477
+ image.sizes = width + 'px';
478
+ image.dataset.largestUsedSize = width;
479
+ }
480
+
481
+ if (this.slide) {
482
+ this.instance.dispatch('imageSizeChange', { slide: this.slide, width, height, content: this });
483
+ }
484
+ }
485
+ }
486
+
487
+ /**
488
+ * @returns {Boolean} If the content can be zoomed
489
+ */
490
+ isZoomable() {
491
+ return this.instance.applyFilters(
492
+ 'isContentZoomable',
493
+ this.isImageContent() && (this.state !== LOAD_STATE.ERROR),
494
+ this
495
+ );
496
+ }
497
+
498
+ /**
499
+ * @returns {Boolean} If content should use a placeholder (from msrc by default)
500
+ */
501
+ usePlaceholder() {
502
+ return this.instance.applyFilters(
503
+ 'useContentPlaceholder',
504
+ this.isImageContent(),
505
+ this
506
+ );
507
+ }
508
+
509
+ /**
510
+ * Preload content with lazy-loading param
511
+ *
512
+ * @param {Boolean} isLazy
513
+ */
514
+ lazyLoad() {
515
+ if (this.instance.dispatch('contentLazyLoad', { content: this }).defaultPrevented) {
516
+ return;
517
+ }
518
+
519
+ this.load(true);
520
+ }
521
+
522
+ /**
523
+ * @returns {Boolean} If placeholder should be kept after content is loaded
524
+ */
525
+ keepPlaceholder() {
526
+ return this.instance.applyFilters(
527
+ 'isKeepingPlaceholder',
528
+ this.isLoading(),
529
+ this
530
+ );
531
+ }
532
+
533
+ /**
534
+ * Destroy the content
535
+ */
536
+ destroy() {
537
+ this.hasSlide = false;
538
+ this.slide = null;
539
+
540
+ if (this.instance.dispatch('contentDestroy', { content: this }).defaultPrevented) {
541
+ return;
542
+ }
543
+
544
+ this.remove();
545
+
546
+ if (this.isImageContent() && this.element) {
547
+ this.element.onload = null;
548
+ this.element.onerror = null;
549
+ this.element = null;
550
+ }
551
+ }
552
+
553
+ /**
554
+ * Display error message
555
+ */
556
+ displayError() {
557
+ if (this.slide) {
558
+ let errorMsgEl = createElement('pswp__error-msg');
559
+ errorMsgEl.innerText = this.instance.options.errorMsg;
560
+ errorMsgEl = this.instance.applyFilters(
561
+ 'contentErrorElement',
562
+ errorMsgEl,
563
+ this
564
+ );
565
+ this.element = createElement('pswp__content pswp__error-msg-container');
566
+ this.element.appendChild(errorMsgEl);
567
+ this.slide.container.innerHTML = '';
568
+ this.slide.container.appendChild(this.element);
569
+ this.slide.updateContentSize(true);
570
+ this.removePlaceholder();
571
+ }
572
+ }
573
+
574
+ /**
575
+ * Append the content
576
+ */
577
+ append() {
578
+ this.isAttached = true;
579
+
580
+ if (this.state === LOAD_STATE.ERROR) {
581
+ this.displayError();
582
+ return;
583
+ }
584
+
585
+ if (this.instance.dispatch('contentAppend', { content: this }).defaultPrevented) {
586
+ return;
587
+ }
588
+
589
+ if (this.isImageContent()) {
590
+ // Use decode() on nearby slides
591
+ //
592
+ // Nearby slide images are in DOM and not hidden via display:none.
593
+ // However, they are placed offscreen (to the left and right side).
594
+ //
595
+ // Some browsers do not composite the image until it's actually visible,
596
+ // using decode() helps.
597
+ //
598
+ // You might ask "why dont you just decode() and then append all images",
599
+ // that's because I want to show image before it's fully loaded,
600
+ // as browser can render parts of image while it is loading.
601
+ if (this.slide
602
+ && !this.slide.isActive
603
+ && ('decode' in this.element)) {
604
+ this.isDecoding = true;
605
+ // Make sure that we start decoding on the next frame
606
+ requestAnimationFrame(() => {
607
+ // element might change
608
+ if (this.element && this.element.tagName === 'IMG') {
609
+ this.element.decode().then(() => {
610
+ this.isDecoding = false;
611
+ requestAnimationFrame(() => {
612
+ this.appendImage();
613
+ });
614
+ }).catch(() => {
615
+ this.isDecoding = false;
616
+ });
617
+ }
618
+ });
619
+ } else {
620
+ if (this.placeholder
621
+ && (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR)) {
622
+ this.removePlaceholder();
623
+ }
624
+ this.appendImage();
625
+ }
626
+ } else if (this.element && !this.element.parentNode) {
627
+ this.slide.container.appendChild(this.element);
628
+ }
629
+ }
630
+
631
+ /**
632
+ * Activate the slide,
633
+ * active slide is generally the current one,
634
+ * meaning the user can see it.
635
+ */
636
+ activate() {
637
+ if (this.instance.dispatch('contentActivate', { content: this }).defaultPrevented) {
638
+ return;
639
+ }
640
+
641
+ if (this.slide) {
642
+ if (this.isImageContent() && this.isDecoding) {
643
+ // add image to slide when it becomes active,
644
+ // even if it's not finished decoding
645
+ this.appendImage();
646
+ } else if (this.isError()) {
647
+ this.load(false, true); // try to reload
648
+ }
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Deactivate the content
654
+ */
655
+ deactivate() {
656
+ this.instance.dispatch('contentDeactivate', { content: this });
657
+ }
658
+
659
+
660
+ /**
661
+ * Remove the content from DOM
662
+ */
663
+ remove() {
664
+ this.isAttached = false;
665
+
666
+ if (this.instance.dispatch('contentRemove', { content: this }).defaultPrevented) {
667
+ return;
668
+ }
669
+
670
+ if (this.element && this.element.parentNode) {
671
+ this.element.remove();
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Append the image content to slide container
677
+ */
678
+ appendImage() {
679
+ if (!this.isAttached) {
680
+ return;
681
+ }
682
+
683
+ if (this.instance.dispatch('contentAppendImage', { content: this }).defaultPrevented) {
684
+ return;
685
+ }
686
+
687
+ // ensure that element exists and is not already appended
688
+ if (this.slide && this.element && !this.element.parentNode) {
689
+ this.slide.container.appendChild(this.element);
690
+
691
+ if (this.placeholder
692
+ && (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR)) {
693
+ this.removePlaceholder();
694
+ }
695
+ }
696
+ }
697
+ }
698
+
699
+ /**
700
+ * PhotoSwipe base class that can retrieve data about every slide.
701
+ * Shared by PhotoSwipe Core and PhotoSwipe Lightbox
702
+ */
703
+
704
+
705
+ class PhotoSwipeBase extends Eventable {
706
+ /**
707
+ * Get total number of slides
708
+ */
709
+ getNumItems() {
710
+ let numItems;
711
+ const { dataSource } = this.options;
712
+ if (!dataSource) {
713
+ numItems = 0;
714
+ } else if (dataSource.length) {
715
+ // may be an array or just object with length property
716
+ numItems = dataSource.length;
717
+ } else if (dataSource.gallery) {
718
+ // query DOM elements
719
+ if (!dataSource.items) {
720
+ dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
721
+ }
722
+
723
+ if (dataSource.items) {
724
+ numItems = dataSource.items.length;
725
+ }
726
+ }
727
+
728
+ // legacy event, before filters were introduced
729
+ const event = this.dispatch('numItems', {
730
+ dataSource,
731
+ numItems
732
+ });
733
+ return this.applyFilters('numItems', event.numItems, dataSource);
734
+ }
735
+
736
+ createContentFromData(slideData, index) {
737
+ return new Content(slideData, this, index);
738
+ }
739
+
740
+ /**
741
+ * Get item data by index.
742
+ *
743
+ * "item data" should contain normalized information that PhotoSwipe needs to generate a slide.
744
+ * For example, it may contain properties like
745
+ * `src`, `srcset`, `w`, `h`, which will be used to generate a slide with image.
746
+ *
747
+ * @param {Integer} index
748
+ */
749
+ getItemData(index) {
750
+ const { dataSource } = this.options;
751
+ let dataSourceItem;
752
+ if (Array.isArray(dataSource)) {
753
+ // Datasource is an array of elements
754
+ dataSourceItem = dataSource[index];
755
+ } else if (dataSource && dataSource.gallery) {
756
+ // dataSource has gallery property,
757
+ // thus it was created by Lightbox, based on
758
+ // gallerySelecor and childSelector options
759
+
760
+ // query DOM elements
761
+ if (!dataSource.items) {
762
+ dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
763
+ }
764
+
765
+ dataSourceItem = dataSource.items[index];
766
+ }
767
+
768
+ let itemData = dataSourceItem;
769
+
770
+ if (itemData instanceof Element) {
771
+ itemData = this._domElementToItemData(itemData);
772
+ }
773
+
774
+ // Dispatching the itemData event,
775
+ // it's a legacy verion before filters were introduced
776
+ const event = this.dispatch('itemData', {
777
+ itemData: itemData || {},
778
+ index
779
+ });
780
+
781
+ return this.applyFilters('itemData', event.itemData, index);
782
+ }
783
+
784
+ /**
785
+ * Get array of gallery DOM elements,
786
+ * based on childSelector and gallery element.
787
+ *
788
+ * @param {Element} galleryElement
789
+ */
790
+ _getGalleryDOMElements(galleryElement) {
791
+ if (this.options.children || this.options.childSelector) {
792
+ return getElementsFromOption(
793
+ this.options.children,
794
+ this.options.childSelector,
795
+ galleryElement
796
+ ) || [];
797
+ }
798
+
799
+ return [galleryElement];
800
+ }
801
+
802
+ /**
803
+ * Converts DOM element to item data object.
804
+ *
805
+ * @param {Element} element DOM element
806
+ */
807
+ // eslint-disable-next-line class-methods-use-this
808
+ _domElementToItemData(element) {
809
+ const itemData = {
810
+ element
811
+ };
812
+
813
+ const linkEl = element.tagName === 'A' ? element : element.querySelector('a');
814
+
815
+ if (linkEl) {
816
+ // src comes from data-pswp-src attribute,
817
+ // if it's empty link href is used
818
+ itemData.src = linkEl.dataset.pswpSrc || linkEl.href;
819
+
820
+ if (linkEl.dataset.pswpSrcset) {
821
+ itemData.srcset = linkEl.dataset.pswpSrcset;
822
+ }
823
+
824
+ itemData.width = parseInt(linkEl.dataset.pswpWidth, 10);
825
+ itemData.height = parseInt(linkEl.dataset.pswpHeight, 10);
826
+
827
+ // support legacy w & h properties
828
+ itemData.w = itemData.width;
829
+ itemData.h = itemData.height;
830
+
831
+ if (linkEl.dataset.pswpType) {
832
+ itemData.type = linkEl.dataset.pswpType;
833
+ }
834
+
835
+ const thumbnailEl = element.querySelector('img');
836
+
837
+ if (thumbnailEl) {
838
+ // msrc is URL to placeholder image that's displayed before large image is loaded
839
+ // by default it's displayed only for the first slide
840
+ itemData.msrc = thumbnailEl.currentSrc || thumbnailEl.src;
841
+ itemData.alt = thumbnailEl.getAttribute('alt');
842
+ }
843
+
844
+ if (linkEl.dataset.pswpCropped || linkEl.dataset.cropped) {
845
+ itemData.thumbCropped = true;
846
+ }
847
+ }
848
+
849
+ this.applyFilters('domItemData', itemData, element, linkEl);
850
+
851
+ return itemData;
852
+ }
853
+ }
854
+
855
+ function getViewportSize(options, pswp) {
856
+ if (options.getViewportSizeFn) {
857
+ const newViewportSize = options.getViewportSizeFn(options, pswp);
858
+ if (newViewportSize) {
859
+ return newViewportSize;
860
+ }
861
+ }
862
+
863
+ return {
864
+ x: document.documentElement.clientWidth,
865
+
866
+ // TODO: height on mobile is very incosistent due to toolbar
867
+ // find a way to improve this
868
+ //
869
+ // document.documentElement.clientHeight - doesn't seem to work well
870
+ y: window.innerHeight
871
+ };
872
+ }
873
+
874
+ /**
875
+ * Parses padding option.
876
+ * Supported formats:
877
+ *
878
+ * // Object
879
+ * padding: {
880
+ * top: 0,
881
+ * bottom: 0,
882
+ * left: 0,
883
+ * right: 0
884
+ * }
885
+ *
886
+ * // A function that returns the object
887
+ * paddingFn: (viewportSize, itemData, index) => {
888
+ * return {
889
+ * top: 0,
890
+ * bottom: 0,
891
+ * left: 0,
892
+ * right: 0
893
+ * };
894
+ * }
895
+ *
896
+ * // Legacy variant
897
+ * paddingLeft: 0,
898
+ * paddingRight: 0,
899
+ * paddingTop: 0,
900
+ * paddingBottom: 0,
901
+ *
902
+ * @param {String} prop 'left', 'top', 'bottom', 'right'
903
+ * @param {Object} options PhotoSwipe options
904
+ * @param {Object} viewportSize PhotoSwipe viewport size, for example: { x:800, y:600 }
905
+ * @param {Object} itemData Data about the slide
906
+ * @param {Integer} index Slide index
907
+ * @returns {Number}
908
+ */
909
+ function parsePaddingOption(prop, options, viewportSize, itemData, index) {
910
+ let paddingValue;
911
+
912
+ if (options.paddingFn) {
913
+ paddingValue = options.paddingFn(viewportSize, itemData, index)[prop];
914
+ } else if (options.padding) {
915
+ paddingValue = options.padding[prop];
916
+ } else {
917
+ const legacyPropName = 'padding' + prop[0].toUpperCase() + prop.slice(1);
918
+ if (options[legacyPropName]) {
919
+ paddingValue = options[legacyPropName];
920
+ }
921
+ }
922
+
923
+ return paddingValue || 0;
924
+ }
925
+
926
+
927
+ function getPanAreaSize(options, viewportSize, itemData, index) {
928
+ return {
929
+ x: viewportSize.x
930
+ - parsePaddingOption('left', options, viewportSize, itemData, index)
931
+ - parsePaddingOption('right', options, viewportSize, itemData, index),
932
+ y: viewportSize.y
933
+ - parsePaddingOption('top', options, viewportSize, itemData, index)
934
+ - parsePaddingOption('bottom', options, viewportSize, itemData, index)
935
+ };
936
+ }
937
+
938
+ /**
939
+ * Calculates zoom levels for specific slide.
940
+ * Depends on viewport size and image size.
941
+ */
942
+
943
+ const MAX_IMAGE_WIDTH = 4000;
944
+
945
+ class ZoomLevel {
946
+ /**
947
+ * @param {Object} options PhotoSwipe options
948
+ * @param {Object} itemData Slide data
949
+ * @param {Integer} index Slide index
950
+ * @param {PhotoSwipe|undefined} pswp PhotoSwipe instance, can be undefined if not initialized yet
951
+ */
952
+ constructor(options, itemData, index, pswp) {
953
+ this.pswp = pswp;
954
+ this.options = options;
955
+ this.itemData = itemData;
956
+ this.index = index;
957
+ }
958
+
959
+ /**
960
+ * Calculate initial, secondary and maximum zoom level for the specified slide.
961
+ *
962
+ * It should be called when either image or viewport size changes.
963
+ *
964
+ * @param {Slide} slide
965
+ */
966
+ update(maxWidth, maxHeight, panAreaSize) {
967
+ this.elementSize = {
968
+ x: maxWidth,
969
+ y: maxHeight
970
+ };
971
+
972
+ this.panAreaSize = panAreaSize;
973
+
974
+ const hRatio = this.panAreaSize.x / this.elementSize.x;
975
+ const vRatio = this.panAreaSize.y / this.elementSize.y;
976
+
977
+ this.fit = Math.min(1, hRatio < vRatio ? hRatio : vRatio);
978
+ this.fill = Math.min(1, hRatio > vRatio ? hRatio : vRatio);
979
+
980
+ // zoom.vFill defines zoom level of the image
981
+ // when it has 100% of viewport vertical space (height)
982
+ this.vFill = Math.min(1, vRatio);
983
+
984
+ this.initial = this._getInitial();
985
+ this.secondary = this._getSecondary();
986
+ this.max = Math.max(
987
+ this.initial,
988
+ this.secondary,
989
+ this._getMax()
990
+ );
991
+
992
+ this.min = Math.min(
993
+ this.fit,
994
+ this.initial,
995
+ this.secondary
996
+ );
997
+
998
+ if (this.pswp) {
999
+ this.pswp.dispatch('zoomLevelsUpdate', { zoomLevels: this, slideData: this.itemData });
1000
+ }
1001
+ }
1002
+
1003
+ /**
1004
+ * Parses user-defined zoom option.
1005
+ *
1006
+ * @param {Mixed} optionPrefix Zoom level option prefix (initial, secondary, max)
1007
+ */
1008
+ _parseZoomLevelOption(optionPrefix) {
1009
+ // zoom.initial
1010
+ // zoom.secondary
1011
+ // zoom.max
1012
+ const optionValue = this.options[optionPrefix + 'ZoomLevel'];
1013
+
1014
+ if (!optionValue) {
1015
+ return;
1016
+ }
1017
+
1018
+ if (typeof optionValue === 'function') {
1019
+ return optionValue(this);
1020
+ }
1021
+
1022
+ if (optionValue === 'fill') {
1023
+ return this.fill;
1024
+ }
1025
+
1026
+ if (optionValue === 'fit') {
1027
+ return this.fit;
1028
+ }
1029
+
1030
+ return Number(optionValue);
1031
+ }
1032
+
1033
+ /**
1034
+ * Get zoom level to which image will be zoomed after double-tap gesture,
1035
+ * or when user clicks on zoom icon,
1036
+ * or mouse-click on image itself.
1037
+ * If you return 1 image will be zoomed to its original size.
1038
+ *
1039
+ * @return {Number}
1040
+ */
1041
+ _getSecondary() {
1042
+ let currZoomLevel = this._parseZoomLevelOption('secondary');
1043
+
1044
+ if (currZoomLevel) {
1045
+ return currZoomLevel;
1046
+ }
1047
+
1048
+ // 3x of "fit" state, but not larger than original
1049
+ currZoomLevel = Math.min(1, this.fit * 3);
1050
+
1051
+ if (currZoomLevel * this.elementSize.x > MAX_IMAGE_WIDTH) {
1052
+ currZoomLevel = MAX_IMAGE_WIDTH / this.elementSize.x;
1053
+ }
1054
+
1055
+ return currZoomLevel;
1056
+ }
1057
+
1058
+ /**
1059
+ * Get initial image zoom level.
1060
+ *
1061
+ * @return {Number}
1062
+ */
1063
+ _getInitial() {
1064
+ return this._parseZoomLevelOption('initial') || this.fit;
1065
+ }
1066
+
1067
+ /**
1068
+ * Maximum zoom level when user zooms
1069
+ * via zoom/pinch gesture,
1070
+ * via cmd/ctrl-wheel or via trackpad.
1071
+ *
1072
+ * @return {Number}
1073
+ */
1074
+ _getMax() {
1075
+ const currZoomLevel = this._parseZoomLevelOption('max');
1076
+
1077
+ if (currZoomLevel) {
1078
+ return currZoomLevel;
1079
+ }
1080
+
1081
+ // max zoom level is x4 from "fit state",
1082
+ // used for zoom gesture and ctrl/trackpad zoom
1083
+ return Math.max(1, this.fit * 4);
1084
+ }
1085
+ }
1086
+
1087
+ /**
1088
+ * Lazy-load an image
1089
+ * This function is used both by Lightbox and PhotoSwipe core,
1090
+ * thus it can be called before dialog is opened.
1091
+ *
1092
+ * @param {Object} itemData Data about the slide
1093
+ * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox
1094
+ * @param {Integer} index
1095
+ * @returns {Object|Boolean} Image that is being decoded or false.
1096
+ */
1097
+ function lazyLoadData(itemData, instance, index) {
1098
+ // src/slide/content/content.js
1099
+ const content = instance.createContentFromData(itemData, index);
1100
+
1101
+ if (!content || !content.lazyLoad) {
1102
+ return;
1103
+ }
1104
+
1105
+ const { options } = instance;
1106
+
1107
+ // We need to know dimensions of the image to preload it,
1108
+ // as it might use srcset and we need to define sizes
1109
+ const viewportSize = instance.viewportSize || getViewportSize(options);
1110
+ const panAreaSize = getPanAreaSize(options, viewportSize, itemData, index);
1111
+
1112
+ const zoomLevel = new ZoomLevel(options, itemData, -1);
1113
+ zoomLevel.update(content.width, content.height, panAreaSize);
1114
+
1115
+ content.lazyLoad();
1116
+ content.setDisplayedSize(
1117
+ Math.ceil(content.width * zoomLevel.initial),
1118
+ Math.ceil(content.height * zoomLevel.initial)
1119
+ );
1120
+
1121
+ return content;
1122
+ }
1123
+
1124
+
1125
+ /**
1126
+ * Lazy-loads specific slide.
1127
+ * This function is used both by Lightbox and PhotoSwipe core,
1128
+ * thus it can be called before dialog is opened.
1129
+ *
1130
+ * By default it loads image based on viewport size and initial zoom level.
1131
+ *
1132
+ * @param {Integer} index Slide index
1133
+ * @param {Object} instance PhotoSwipe or PhotoSwipeLightbox eventable instance
1134
+ */
1135
+ function lazyLoadSlide(index, instance) {
1136
+ const itemData = instance.getItemData(index);
1137
+
1138
+ if (instance.dispatch('lazyLoadSlide', { index, itemData }).defaultPrevented) {
1139
+ return;
1140
+ }
1141
+
1142
+ return lazyLoadData(itemData, instance, index);
1143
+ }
1144
+
1145
+ /**
1146
+ * PhotoSwipe lightbox
1147
+ *
1148
+ * - If user has unsupported browser it falls back to default browser action (just opens URL)
1149
+ * - Binds click event to links that should open PhotoSwipe
1150
+ * - parses DOM strcture for PhotoSwipe (retrieves large image URLs and sizes)
1151
+ * - Initializes PhotoSwipe
1152
+ *
1153
+ *
1154
+ * Loader options use the same object as PhotoSwipe, and supports such options:
1155
+ *
1156
+ * gallery - Element | Element[] | NodeList | string selector for the gallery element
1157
+ * children - Element | Element[] | NodeList | string selector for the gallery children
1158
+ *
1159
+ */
1160
+
1161
+ class PhotoSwipeLightbox extends PhotoSwipeBase {
1162
+ constructor(options) {
1163
+ super();
1164
+ this.options = options || {};
1165
+ this._uid = 0;
1166
+ }
1167
+
1168
+ init() {
1169
+ this.onThumbnailsClick = this.onThumbnailsClick.bind(this);
1170
+
1171
+ // Bind click events to each gallery
1172
+ getElementsFromOption(this.options.gallery, this.options.gallerySelector)
1173
+ .forEach((galleryElement) => {
1174
+ galleryElement.addEventListener('click', this.onThumbnailsClick, false);
1175
+ });
1176
+ }
1177
+
1178
+ onThumbnailsClick(e) {
1179
+ // Exit and allow default browser action if:
1180
+ if (specialKeyUsed(e) // ... if clicked with a special key (ctrl/cmd...)
1181
+ || window.pswp // ... if PhotoSwipe is already open
1182
+ || window.navigator.onLine === false) { // ... if offline
1183
+ return;
1184
+ }
1185
+
1186
+ // If both clientX and clientY are 0 or not defined,
1187
+ // the event is likely triggered by keyboard,
1188
+ // so we do not pass the initialPoint
1189
+ //
1190
+ // Note that some screen readers emulate the mouse position,
1191
+ // so it's not ideal way to detect them.
1192
+ //
1193
+ let initialPoint = { x: e.clientX, y: e.clientY };
1194
+
1195
+ if (!initialPoint.x && !initialPoint.y) {
1196
+ initialPoint = null;
1197
+ }
1198
+
1199
+ let clickedIndex = this.getClickedIndex(e);
1200
+ clickedIndex = this.applyFilters('clickedIndex', clickedIndex, e, this);
1201
+ const dataSource = {
1202
+ gallery: e.currentTarget
1203
+ };
1204
+
1205
+ if (clickedIndex >= 0) {
1206
+ e.preventDefault();
1207
+ this.loadAndOpen(clickedIndex, dataSource, initialPoint);
1208
+ }
1209
+ }
1210
+
1211
+ /**
1212
+ * Get index of gallery item that was clicked.
1213
+ *
1214
+ * @param {Event} e click event
1215
+ */
1216
+ getClickedIndex(e) {
1217
+ // legacy option
1218
+ if (this.options.getClickedIndexFn) {
1219
+ return this.options.getClickedIndexFn.call(this, e);
1220
+ }
1221
+
1222
+ const clickedTarget = e.target;
1223
+ const childElements = getElementsFromOption(
1224
+ this.options.children,
1225
+ this.options.childSelector,
1226
+ e.currentTarget
1227
+ );
1228
+ const clickedChildIndex = childElements.findIndex(
1229
+ child => child === clickedTarget || child.contains(clickedTarget)
1230
+ );
1231
+
1232
+ if (clickedChildIndex !== -1) {
1233
+ return clickedChildIndex;
1234
+ } else if (this.options.children || this.options.childSelector) {
1235
+ // click wasn't on a child element
1236
+ return -1;
1237
+ }
1238
+
1239
+ // There is only one item (which is the gallery)
1240
+ return 0;
1241
+ }
1242
+
1243
+ /**
1244
+ * Load and open PhotoSwipe
1245
+ *
1246
+ * @param {Integer} index
1247
+ * @param {Array|Object|null} dataSource
1248
+ * @param {Point|null} initialPoint
1249
+ */
1250
+ loadAndOpen(index, dataSource, initialPoint) {
1251
+ // Check if the gallery is already open
1252
+ if (window.pswp) {
1253
+ return false;
1254
+ }
1255
+
1256
+ // set initial index
1257
+ this.options.index = index;
1258
+
1259
+ // define options for PhotoSwipe constructor
1260
+ this.options.initialPointerPos = initialPoint;
1261
+
1262
+ this.shouldOpen = true;
1263
+ this.preload(index, dataSource);
1264
+ return true;
1265
+ }
1266
+
1267
+ /**
1268
+ * Load the main module and the slide content by index
1269
+ *
1270
+ * @param {Integer} index
1271
+ */
1272
+ preload(index, dataSource) {
1273
+ const { options } = this;
1274
+
1275
+ if (dataSource) {
1276
+ options.dataSource = dataSource;
1277
+ }
1278
+
1279
+ // Add the main module
1280
+ const promiseArray = [];
1281
+
1282
+ const pswpModuleType = typeof options.pswpModule;
1283
+ if (isPswpClass(options.pswpModule)) {
1284
+ promiseArray.push(options.pswpModule);
1285
+ } else if (pswpModuleType === 'string') {
1286
+ throw new Error('pswpModule as string is no longer supported');
1287
+ } else if (pswpModuleType === 'function') {
1288
+ promiseArray.push(options.pswpModule());
1289
+ } else {
1290
+ throw new Error('pswpModule is not valid');
1291
+ }
1292
+
1293
+ // Add custom-defined promise, if any
1294
+ if (typeof options.openPromise === 'function') {
1295
+ // allow developers to perform some task before opening
1296
+ promiseArray.push(options.openPromise());
1297
+ }
1298
+
1299
+ if (options.preloadFirstSlide !== false && index >= 0) {
1300
+ this._preloadedContent = lazyLoadSlide(index, this);
1301
+ }
1302
+
1303
+ // Wait till all promises resolve and open PhotoSwipe
1304
+ const uid = ++this._uid;
1305
+ Promise.all(promiseArray).then((iterableModules) => {
1306
+ if (this.shouldOpen) {
1307
+ const mainModule = iterableModules[0];
1308
+ this._openPhotoswipe(mainModule, uid);
1309
+ }
1310
+ });
1311
+ }
1312
+
1313
+ _openPhotoswipe(module, uid) {
1314
+ // Cancel opening if UID doesn't match the current one
1315
+ // (if user clicked on another gallery item before current was loaded).
1316
+ //
1317
+ // Or if shouldOpen flag is set to false
1318
+ // (developer may modify it via public API)
1319
+ if (uid !== this._uid && this.shouldOpen) {
1320
+ return;
1321
+ }
1322
+
1323
+ this.shouldOpen = false;
1324
+
1325
+ // PhotoSwipe is already open
1326
+ if (window.pswp) {
1327
+ return;
1328
+ }
1329
+
1330
+ // Pass data to PhotoSwipe and open init
1331
+ const pswp = typeof module === 'object'
1332
+ ? new module.default(this.options) // eslint-disable-line
1333
+ : new module(this.options); // eslint-disable-line
1334
+
1335
+ this.pswp = pswp;
1336
+ window.pswp = pswp;
1337
+
1338
+ // map listeners from Lightbox to PhotoSwipe Core
1339
+ Object.keys(this._listeners).forEach((name) => {
1340
+ this._listeners[name].forEach((fn) => {
1341
+ pswp.on(name, fn);
1342
+ });
1343
+ });
1344
+
1345
+ // same with filters
1346
+ Object.keys(this._filters).forEach((name) => {
1347
+ this._filters[name].forEach((filter) => {
1348
+ pswp.addFilter(name, filter.fn, filter.priority);
1349
+ });
1350
+ });
1351
+
1352
+ if (this._preloadedContent) {
1353
+ pswp.contentLoader.addToCache(this._preloadedContent);
1354
+ this._preloadedContent = null;
1355
+ }
1356
+
1357
+ pswp.on('destroy', () => {
1358
+ // clean up public variables
1359
+ this.pswp = null;
1360
+ window.pswp = null;
1361
+ });
1362
+
1363
+ pswp.init();
1364
+ }
1365
+
1366
+ destroy() {
1367
+ if (this.pswp) {
1368
+ this.pswp.destroy();
1369
+ }
1370
+
1371
+ this.shouldOpen = false;
1372
+ this._listeners = null;
1373
+
1374
+ getElementsFromOption(this.options.gallery, this.options.gallerySelector)
1375
+ .forEach((galleryElement) => {
1376
+ galleryElement.removeEventListener('click', this.onThumbnailsClick, false);
1377
+ });
1378
+ }
1379
+ }
1380
+
1381
+ export default PhotoSwipeLightbox;
1382
+ //# sourceMappingURL=photoswipe-lightbox.esm.js.map