intranet-pictures 1.0.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/intranet/pictures/json_db_provider.rb +5 -3
- data/lib/intranet/pictures/responder.rb +27 -18
- data/lib/intranet/pictures/version.rb +1 -1
- data/lib/intranet/resources/haml/pictures_browse.haml +0 -1
- data/lib/intranet/resources/haml/pictures_home.haml +0 -3
- data/lib/intranet/resources/locales/en.yml +0 -1
- data/lib/intranet/resources/locales/fr.yml +0 -1
- data/lib/intranet/resources/www/jpictures.js +32 -14
- data/lib/intranet/resources/www/photoswipe/photoswipe-dynamic-caption-plugin.css +47 -0
- data/lib/intranet/resources/www/photoswipe/photoswipe-dynamic-caption-plugin.esm.js +400 -0
- data/lib/intranet/resources/www/photoswipe/photoswipe-lightbox.esm.js +1382 -0
- data/lib/intranet/resources/www/photoswipe/photoswipe-lightbox.esm.js.map +1 -0
- data/lib/intranet/resources/www/photoswipe/photoswipe-lightbox.esm.min.js +5 -0
- data/lib/intranet/resources/www/photoswipe/photoswipe.css +383 -142
- data/lib/intranet/resources/www/photoswipe/photoswipe.esm.js +5279 -0
- data/lib/intranet/resources/www/photoswipe/photoswipe.esm.js.map +1 -0
- data/lib/intranet/resources/www/photoswipe/photoswipe.esm.min.js +5 -0
- data/lib/intranet/resources/www/style.css +16 -3
- data/spec/intranet/pictures/json_db_provider_spec.rb +20 -7
- data/spec/intranet/pictures/responder_spec.rb +83 -63
- data/spec/intranet/pictures/sample-db.json +14 -2
- metadata +29 -32
- data/lib/intranet/resources/haml/pictures_photoswipe.haml +0 -23
- data/lib/intranet/resources/www/photoswipe/LICENSE +0 -21
- data/lib/intranet/resources/www/photoswipe/default-skin/default-skin.css +0 -484
- data/lib/intranet/resources/www/photoswipe/default-skin/default-skin.png +0 -0
- data/lib/intranet/resources/www/photoswipe/default-skin/default-skin.svg +0 -1
- data/lib/intranet/resources/www/photoswipe/default-skin/preloader.gif +0 -0
- data/lib/intranet/resources/www/photoswipe/photoswipe-ui-default.js +0 -861
- data/lib/intranet/resources/www/photoswipe/photoswipe-ui-default.min.js +0 -4
- data/lib/intranet/resources/www/photoswipe/photoswipe.js +0 -3734
- 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
|