PageSourceSearch

https://www.worldphoto.org/themes/custom/wpo/libraries/photoswipe/dist/photoswipe.esm.js

js worldphoto.org collected 2026-09-24 08:48:27 UTC 187,718 bytes, 7,081 lines download raw bytes

vendor: 112,716 bytes, lines 1-4393
1/*!
2  * PhotoSwipe 5.4.4 - https://photoswipe.com
3  * (c) 2024 Dmytro Semenov
4  */
5/** @typedef {import('../photoswipe.js').Point} Point */
6
7/**
8 * @template {keyof HTMLElementTagNameMap} T
9 * @param {string} className
10 * @param {T} tagName
11 * @param {Node} [appendToEl]
12 * @returns {HTMLElementTagNameMap[T]}
13 */
14function createElement(className, tagName, appendToEl) {
15  const el = document.createElement(tagName);
16
17  if (className) {
18    el.className = className;
19  }
20
21  if (appendToEl) {
22    appendToEl.appendChild(el);
23  }
24
25  return el;
26}
27/**
28 * @param {Point} p1
29 * @param {Point} p2
30 * @returns {Point}
31 */
32
33function equalizePoints(p1, p2) {
34  p1.x = p2.x;
35  p1.y = p2.y;
36
37  if (p2.id !== undefined) {
38    p1.id = p2.id;
39  }
40
41  return p1;
42}
43/**
44 * @param {Point} p
45 */
46
47function roundPoint(p) {
48  p.x = Math.round(p.x);
49  p.y = Math.round(p.y);
50}
51/**
52 * Returns distance between two points.
53 *
54 * @param {Point} p1
55 * @param {Point} p2
56 * @returns {number}
57 */
58
59function getDistanceBetween(p1, p2) {
60  const x = Math.abs(p1.x - p2.x);
61  const y = Math.abs(p1.y - p2.y);
62  return Math.sqrt(x * x + y * y);
63}
64/**
65 * Whether X and Y positions of points are equal
66 *
67 * @param {Point} p1
68 * @param {Point} p2
69 * @returns {boolean}
70 */
71
72function pointsEqual(p1, p2) {
73  return p1.x === p2.x && p1.y === p2.y;
74}
75/**
76 * The float result between the min and max values.
77 *
78 * @param {number} val
79 * @param {number} min
80 * @param {number} max
81 * @returns {number}
82 */
83
84function clamp(val, min, max) {
85  return Math.min(Math.max(val, min), max);
86}
87/**
88 * Get transform string
89 *
90 * @param {number} x
91 * @param {number} [y]
92 * @param {number} [scale]
93 * @returns {string}
94 */
95
96function toTransformString(x, y, scale) {
97  let propValue = `translate3d(${x}px,${y || 0}px,0)`;
98
99  if (scale !== undefined) {
100    propValue += ` scale3d(${scale},${scale},1)`;
101  }
102
103  return propValue;
104}
105/**
106 * Apply transform:translate(x, y) scale(scale) to element
107 *
108 * @param {HTMLElement} el
109 * @param {number} x
110 * @param {number} [y]
111 * @param {number} [scale]
112 */
113
114function setTransform(el, x, y, scale) {
115  el.style.transform = toTransformString(x, y, scale);
116}
117const defaultCSSEasing = 'cubic-bezier(.4,0,.22,1)';
118/**
119 * Apply CSS transition to element
120 *
121 * @param {HTMLElement} el
122 * @param {string} [prop] CSS property to animate
123 * @param {number} [duration] in ms
124 * @param {string} [ease] CSS easing function
125 */
126
127function setTransitionStyle(el, prop, duration, ease) {
128  // inOut: 'cubic-bezier(.4, 0, .22, 1)', // for "toggle state" transitions
129  // out: 'cubic-bezier(0, 0, .22, 1)', // for "show" transitions
130  // in: 'cubic-bezier(.4, 0, 1, 1)'// for "hide" transitions
131  el.style.transition = prop ? `${prop} ${duration}ms ${ease || defaultCSSEasing}` : 'none';
132}
133/**
134 * Apply width and height CSS properties to element
135 *
136 * @param {HTMLElement} el
137 * @param {string | number} w
138 * @param {string | number} h
139 */
140
141function setWidthHeight(el, w, h) {
142  el.style.width = typeof w === 'number' ? `${w}px` : w;
143  el.style.height = typeof h === 'number' ? `${h}px` : h;
144}
145/**
146 * @param {HTMLElement} el
147 */
148
149function removeTransitionStyle(el) {
150  setTransitionStyle(el);
151}
152/**
153 * @param {HTMLImageElement} img
154 * @returns {Promise<HTMLImageElement | void>}
155 */
156
157function decodeImage(img) {
158  if ('decode' in img) {
159    return img.decode().catch(() => {});
160  }
161
162  if (img.complete) {
163    return Promise.resolve(img);
164  }
165
166  return new Promise((resolve, reject) => {
167    img.onload = () => resolve(img);
168
169    img.onerror = reject;
170  });
171}
172/** @typedef {LOAD_STATE[keyof LOAD_STATE]} LoadState */
173
174/** @type {{ IDLE: 'idle'; LOADING: 'loading'; LOADED: 'loaded'; ERROR: 'error' }} */
175
176const LOAD_STATE = {
177  IDLE: 'idle',
178  LOADING: 'loading',
179  LOADED: 'loaded',
180  ERROR: 'error'
181};
182/**
183 * Check if click or keydown event was dispatched
184 * with a special key or via mouse wheel.
185 *
186 * @param {MouseEvent | KeyboardEvent} e
187 * @returns {boolean}
188 */
189
190function specialKeyUsed(e) {
191  return 'button' in e && e.button === 1 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey;
192}
193/**
194 * Parse `gallery` or `children` options.
195 *
196 * @param {import('../photoswipe.js').ElementProvider} [option]
197 * @param {string} [legacySelector]
198 * @param {HTMLElement | Document} [parent]
199 * @returns HTMLElement[]
200 */
201
202function getElementsFromOption(option, legacySelector, parent = document) {
203  /** @type {HTMLElement[]} */
204  let elements = [];
205
206  if (option instanceof Element) {
207    elements = [option];
208  } else if (option instanceof NodeList || Array.isArray(option)) {
209    elements = Array.from(option);
210  } else {
211    const selector = typeof option === 'string' ? option : legacySelector;
212
213    if (selector) {
214      elements = Array.from(parent.querySelectorAll(selector));
215    }
216  }
217
218  return elements;
219}
220/**
221 * Check if browser is Safari
222 *
223 * @returns {boolean}
224 */
225
226function isSafari() {
227  return !!(navigator.vendor && navigator.vendor.match(/apple/i));
228}
229
230// Detect passive event listener support
231let supportsPassive = false;
232/* eslint-disable */
233
234try {
235  /* @ts-ignore */
236  window.addEventListener('test', null, Object.defineProperty({}, 'passive', {
237    get: () => {
238      supportsPassive = true;
239    }
240  }));
241} catch (e) {}
242/* eslint-enable */
243
244/**
245 * @typedef {Object} PoolItem
246 * @prop {HTMLElement | Window | Document | undefined | null} target
247 * @prop {string} type
248 * @prop {EventListenerOrEventListenerObject} listener
249 * @prop {boolean} [passive]
250 */
251
252
253class DOMEvents {
254  constructor() {
255    /**
256     * @type {PoolItem[]}
257     * @private
258     */
259    this._pool = [];
260  }
261  /**
262   * Adds event listeners
263   *
264   * @param {PoolItem['target']} target
265   * @param {PoolItem['type']} type Can be multiple, separated by space.
266   * @param {PoolItem['listener']} listener
267   * @param {PoolItem['passive']} [passive]
268   */
269
270
271  add(target, type, listener, passive) {
272    this._toggleListener(target, type, listener, passive);
273  }
274  /**
275   * Removes event listeners
276   *
277   * @param {PoolItem['target']} target
278   * @param {PoolItem['type']} type
279   * @param {PoolItem['listener']} listener
280   * @param {PoolItem['passive']} [passive]
281   */
282
283
284  remove(target, type, listener, passive) {
285    this._toggleListener(target, type, listener, passive, true);
286  }
287  /**
288   * Removes all bound events
289   */
290
291
292  removeAll() {
293    this._pool.forEach(poolItem => {
294      this._toggleListener(poolItem.target, poolItem.type, poolItem.listener, poolItem.passive, true, true);
295    });
296
297    this._pool = [];
298  }
299  /**
300   * Adds or removes event
301   *
302   * @private
303   * @param {PoolItem['target']} target
304   * @param {PoolItem['type']} type
305   * @param {PoolItem['listener']} listener
306   * @param {PoolItem['passive']} [passive]
307   * @param {boolean} [unbind] Whether the event should be added or removed
308   * @param {boolean} [skipPool] Whether events pool should be skipped
309   */
310
311
312  _toggleListener(target, type, listener, passive, unbind, skipPool) {
313    if (!target) {
314      return;
315    }
316
317    const methodName = unbind ? 'removeEventListener' : 'addEventListener';
318    const types = type.split(' ');
319    types.forEach(eType => {
320      if (eType) {
321        // Events pool is used to easily unbind all events when PhotoSwipe is closed,
322        // so developer doesn't need to do this manually
323        if (!skipPool) {
324          if (unbind) {
325            // Remove from the events pool
326            this._pool = this._pool.filter(poolItem => {
327              return poolItem.type !== eType || poolItem.listener !== listener || poolItem.target !== target;
328            });
329          } else {
330            // Add to the events pool
331            this._pool.push({
332              target,
333              type: eType,
334              listener,
335              passive
336            });
337          }
338        } // most PhotoSwipe events call preventDefault,
339        // and we do not need browser to scroll the page
340
341
342        const eventOptions = supportsPassive ? {
343          passive: passive || false
344        } : false;
345        target[methodName](eType, listener, eventOptions);
346      }
347    });
348  }
349
350}
351
352/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
353
354/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
355
356/** @typedef {import('../photoswipe.js').Point} Point */
357
358/** @typedef {import('../slide/slide.js').SlideData} SlideData */
359
360/**
361 * @param {PhotoSwipeOptions} options
362 * @param {PhotoSwipeBase} pswp
363 * @returns {Point}
364 */
365function getViewportSize(options, pswp) {
366  if (options.getViewportSizeFn) {
367    const newViewportSize = options.getViewportSizeFn(options, pswp);
368
369    if (newViewportSize) {
370      return newViewportSize;
371    }
372  }
373
374  return {
375    x: document.documentElement.clientWidth,
376    // TODO: height on mobile is very incosistent due to toolbar
377    // find a way to improve this
378    //
379    // document.documentElement.clientHeight - doesn't seem to work well
380    y: window.innerHeight
381  };
382}
383/**
384 * Parses padding option.
385 * Supported formats:
386 *
387 * // Object
388 * padding: {
389 *  top: 0,
390 *  bottom: 0,
391 *  left: 0,
392 *  right: 0
393 * }
394 *
395 * // A function that returns the object
396 * paddingFn: (viewportSize, itemData, index) => {
397 *  return {
398 *    top: 0,
399 *    bottom: 0,
400 *    left: 0,
401 *    right: 0
402 *  };
403 * }
404 *
405 * // Legacy variant
406 * paddingLeft: 0,
407 * paddingRight: 0,
408 * paddingTop: 0,
409 * paddingBottom: 0,
410 *
411 * @param {'left' | 'top' | 'bottom' | 'right'} prop
412 * @param {PhotoSwipeOptions} options PhotoSwipe options
413 * @param {Point} viewportSize PhotoSwipe viewport size, for example: { x:800, y:600 }
414 * @param {SlideData} itemData Data about the slide
415 * @param {number} index Slide index
416 * @returns {number}
417 */
418
419function parsePaddingOption(prop, options, viewportSize, itemData, index) {
420  let paddingValue = 0;
421
422  if (options.paddingFn) {
423    paddingValue = options.paddingFn(viewportSize, itemData, index)[prop];
424  } else if (options.padding) {
425    paddingValue = options.padding[prop];
426  } else {
427    const legacyPropName = 'padding' + prop[0].toUpperCase() + prop.slice(1); // @ts-expect-error
428
429    if (options[legacyPropName]) {
430      // @ts-expect-error
431      paddingValue = options[legacyPropName];
432    }
433  }
434
435  return Number(paddingValue) || 0;
436}
437/**
438 * @param {PhotoSwipeOptions} options
439 * @param {Point} viewportSize
440 * @param {SlideData} itemData
441 * @param {number} index
442 * @returns {Point}
443 */
444
445function getPanAreaSize(options, viewportSize, itemData, index) {
446  return {
447    x: viewportSize.x - parsePaddingOption('left', options, viewportSize, itemData, index) - parsePaddingOption('right', options, viewportSize, itemData, index),
448    y: viewportSize.y - parsePaddingOption('top', options, viewportSize, itemData, index) - parsePaddingOption('bottom', options, viewportSize, itemData, index)
449  };
450}
451
452/** @typedef {import('./slide.js').default} Slide */
453
454/** @typedef {Record<Axis, number>} Point */
455
456/** @typedef {'x' | 'y'} Axis */
457
458/**
459 * Calculates minimum, maximum and initial (center) bounds of a slide
460 */
461
462class PanBounds {
463  /**
464   * @param {Slide} slide
465   */
466  constructor(slide) {
467    this.slide = slide;
468    this.currZoomLevel = 1;
469    this.center =
470    /** @type {Point} */
471    {
472      x: 0,
473      y: 0
474    };
475    this.max =
476    /** @type {Point} */
477    {
478      x: 0,
479      y: 0
480    };
481    this.min =
482    /** @type {Point} */
483    {
484      x: 0,
485      y: 0
486    };
487  }
488  /**
489   * _getItemBounds
490   *
491   * @param {number} currZoomLevel
492   */
493
494
495  update(currZoomLevel) {
496    this.currZoomLevel = currZoomLevel;
497
498    if (!this.slide.width) {
499      this.reset();
500    } else {
501      this._updateAxis('x');
502
503      this._updateAxis('y');
504
505      this.slide.pswp.dispatch('calcBounds', {
506        slide: this.slide
507      });
508    }
509  }
510  /**
511   * _calculateItemBoundsForAxis
512   *
513   * @param {Axis} axis
514   */
515
516
517  _updateAxis(axis) {
518    const {
519      pswp
520    } = this.slide;
521    const elSize = this.slide[axis === 'x' ? 'width' : 'height'] * this.currZoomLevel;
522    const paddingProp = axis === 'x' ? 'left' : 'top';
523    const padding = parsePaddingOption(paddingProp, pswp.options, pswp.viewportSize, this.slide.data, this.slide.index);
524    const panAreaSize = this.slide.panAreaSize[axis]; // Default position of element.
525    // By default, it is center of viewport:
526
527    this.center[axis] = Math.round((panAreaSize - elSize) / 2) + padding; // maximum pan position
528
529    this.max[axis] = elSize > panAreaSize ? Math.round(panAreaSize - elSize) + padding : this.center[axis]; // minimum pan position
530
531    this.min[axis] = elSize > panAreaSize ? padding : this.center[axis];
532  } // _getZeroBounds
533
534
535  reset() {
536    this.center.x = 0;
537    this.center.y = 0;
538    this.max.x = 0;
539    this.max.y = 0;
540    this.min.x = 0;
541    this.min.y = 0;
542  }
543  /**
544   * Correct pan position if it's beyond the bounds
545   *
546   * @param {Axis} axis x or y
547   * @param {number} panOffset
548   * @returns {number}
549   */
550
551
552  correctPan(axis, panOffset) {
553    // checkPanBounds
554    return clamp(panOffset, this.max[axis], this.min[axis]);
555  }
556
557}
558
559const MAX_IMAGE_WIDTH = 4000;
560/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
561
562/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
563
564/** @typedef {import('../photoswipe.js').Point} Point */
565
566/** @typedef {import('../slide/slide.js').SlideData} SlideData */
567
568/** @typedef {'fit' | 'fill' | number | ((zoomLevelObject: ZoomLevel) => number)} ZoomLevelOption */
569
570/**
571 * Calculates zoom levels for specific slide.
572 * Depends on viewport size and image size.
573 */
574
575class ZoomLevel {
576  /**
577   * @param {PhotoSwipeOptions} options PhotoSwipe options
578   * @param {SlideData} itemData Slide data
579   * @param {number} index Slide index
580   * @param {PhotoSwipe} [pswp] PhotoSwipe instance, can be undefined if not initialized yet
581   */
582  constructor(options, itemData, index, pswp) {
583    this.pswp = pswp;
584    this.options = options;
585    this.itemData = itemData;
586    this.index = index;
587    /** @type { Point | null } */
588
589    this.panAreaSize = null;
590    /** @type { Point | null } */
591
592    this.elementSize = null;
593    this.fit = 1;
594    this.fill = 1;
595    this.vFill = 1;
596    this.initial = 1;
597    this.secondary = 1;
598    this.max = 1;
599    this.min = 1;
600  }
601  /**
602   * Calculate initial, secondary and maximum zoom level for the specified slide.
603   *
604   * It should be called when either image or viewport size changes.
605   *
606   * @param {number} maxWidth
607   * @param {number} maxHeight
608   * @param {Point} panAreaSize
609   */
610
611
612  update(maxWidth, maxHeight, panAreaSize) {
613    /** @type {Point} */
614    const elementSize = {
615      x: maxWidth,
616      y: maxHeight
617    };
618    this.elementSize = elementSize;
619    this.panAreaSize = panAreaSize;
620    const hRatio = panAreaSize.x / elementSize.x;
621    const vRatio = panAreaSize.y / elementSize.y;
622    this.fit = Math.min(1, hRatio < vRatio ? hRatio : vRatio);
623    this.fill = Math.min(1, hRatio > vRatio ? hRatio : vRatio); // zoom.vFill defines zoom level of the image
624    // when it has 100% of viewport vertical space (height)
625
626    this.vFill = Math.min(1, vRatio);
627    this.initial = this._getInitial();
628    this.secondary = this._getSecondary();
629    this.max = Math.max(this.initial, this.secondary, this._getMax());
630    this.min = Math.min(this.fit, this.initial, this.secondary);
631
632    if (this.pswp) {
633      this.pswp.dispatch('zoomLevelsUpdate', {
634        zoomLevels: this,
635        slideData: this.itemData
636      });
637    }
638  }
639  /**
640   * Parses user-defined zoom option.
641   *
642   * @private
643   * @param {'initial' | 'secondary' | 'max'} optionPrefix Zoom level option prefix (initial, secondary, max)
644   * @returns { number | undefined }
645   */
646
647
648  _parseZoomLevelOption(optionPrefix) {
649    const optionName =
650    /** @type {'initialZoomLevel' | 'secondaryZoomLevel' | 'maxZoomLevel'} */
651    optionPrefix + 'ZoomLevel';
652    const optionValue = this.options[optionName];
653
654    if (!optionValue) {
655      return;
656    }
657
658    if (typeof optionValue === 'function') {
659      return optionValue(this);
660    }
661
662    if (optionValue === 'fill') {
663      return this.fill;
664    }
665
666    if (optionValue === 'fit') {
667      return this.fit;
668    }
669
670    return Number(optionValue);
671  }
672  /**
673   * Get zoom level to which image will be zoomed after double-tap gesture,
674   * or when user clicks on zoom icon,
675   * or mouse-click on image itself.
676   * If you return 1 image will be zoomed to its original size.
677   *
678   * @private
679   * @return {number}
680   */
681
682
683  _getSecondary() {
684    let currZoomLevel = this._parseZoomLevelOption('secondary');
685
686    if (currZoomLevel) {
687      return currZoomLevel;
688    } // 3x of "fit" state, but not larger than original
689
690
691    currZoomLevel = Math.min(1, this.fit * 3);
692
693    if (this.elementSize && currZoomLevel * this.elementSize.x > MAX_IMAGE_WIDTH) {
694      currZoomLevel = MAX_IMAGE_WIDTH / this.elementSize.x;
695    }
696
697    return currZoomLevel;
698  }
699  /**
700   * Get initial image zoom level.
701   *
702   * @private
703   * @return {number}
704   */
705
706
707  _getInitial() {
708    return this._parseZoomLevelOption('initial') || this.fit;
709  }
710  /**
711   * Maximum zoom level when user zooms
712   * via zoom/pinch gesture,
713   * via cmd/ctrl-wheel or via trackpad.
714   *
715   * @private
716   * @return {number}
717   */
718
719
720  _getMax() {
721    // max zoom level is x4 from "fit state",
722    // used for zoom gesture and ctrl/trackpad zoom
723    return this._parseZoomLevelOption('max') || Math.max(1, this.fit * 4);
724  }
725
726}
727
728/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
729/**
730 * Renders and allows to control a single slide
731 */
732
733class Slide {
734  /**
735   * @param {SlideData} data
736   * @param {number} index
737   * @param {PhotoSwipe} pswp
738   */
739  constructor(data, index, pswp) {
740    this.data = data;
741    this.index = index;
742    this.pswp = pswp;
743    this.isActive = index === pswp.currIndex;
744    this.currentResolution = 0;
745    /** @type {Point} */
746
747    this.panAreaSize = {
748      x: 0,
749      y: 0
750    };
751    /** @type {Point} */
752
753    this.pan = {
754      x: 0,
755      y: 0
756    };
757    this.isFirstSlide = this.isActive && !pswp.opener.isOpen;
758    this.zoomLevels = new ZoomLevel(pswp.options, data, index, pswp);
759    this.pswp.dispatch('gettingData', {
760      slide: this,
761      data: this.data,
762      index
763    });
764    this.content = this.pswp.contentLoader.getContentBySlide(this);
765    this.container = createElement('pswp__zoom-wrap', 'div');
766    /** @type {HTMLElement | null} */
767
768    this.holderElement = null;
769    this.currZoomLevel = 1;
770    /** @type {number} */
771
772    this.width = this.content.width;
773    /** @type {number} */
774
775    this.height = this.content.height;
776    this.heavyAppended = false;
777    this.bounds = new PanBounds(this);
778    this.prevDisplayedWidth = -1;
779    this.prevDisplayedHeight = -1;
780    this.pswp.dispatch('slideInit', {
781      slide: this
782    });
783  }
784  /**
785   * If this slide is active/current/visible
786   *
787   * @param {boolean} isActive
788   */
789
790
791  setIsActive(isActive) {
792    if (isActive && !this.isActive) {
793      // slide just became active
794      this.activate();
795    } else if (!isActive && this.isActive) {
796      // slide just became non-active
797      this.deactivate();
798    }
799  }
800  /**
801   * Appends slide content to DOM
802   *
803   * @param {HTMLElement} holderElement
804   */
805
806
807  append(holderElement) {
808    this.holderElement = holderElement;
809    this.container.style.transformOrigin = '0 0'; // Slide appended to DOM
810
811    if (!this.data) {
812      return;
813    }
814
815    this.calculateSize();
816    this.load();
817    this.updateContentSize();
818    this.appendHeavy();
819    this.holderElement.appendChild(this.container);
820    this.zoomAndPanToInitial();
821    this.pswp.dispatch('firstZoomPan', {
822      slide: this
823    });
824    this.applyCurrentZoomPan();
825    this.pswp.dispatch('afterSetContent', {
826      slide: this
827    });
828
829    if (this.isActive) {
830      this.activate();
831    }
832  }
833
834  load() {
835    this.content.load(false);
836    this.pswp.dispatch('slideLoad', {
837      slide: this
838    });
839  }
840  /**
841   * Append "heavy" DOM elements
842   *
843   * This may depend on a type of slide,
844   * but generally these are large images.
845   */
846
847
848  appendHeavy() {
849    const {
850      pswp
851    } = this;
852    const appendHeavyNearby = true; // todo
853    // Avoid appending heavy elements during animations
854
855    if (this.heavyAppended || !pswp.opener.isOpen || pswp.mainScroll.isShifted() || !this.isActive && !appendHeavyNearby) {
856      return;
857    }
858
859    if (this.pswp.dispatch('appendHeavy', {
860      slide: this
861    }).defaultPrevented) {
862      return;
863    }
864
865    this.heavyAppended = true;
866    this.content.append();
867    this.pswp.dispatch('appendHeavyContent', {
868      slide: this
869    });
870  }
871  /**
872   * Triggered when this slide is active (selected).
873   *
874   * If it's part of opening/closing transition -
875   * activate() will trigger after the transition is ended.
876   */
877
878
879  activate() {
880    this.isActive = true;
881    this.appendHeavy();
882    this.content.activate();
883    this.pswp.dispatch('slideActivate', {
884      slide: this
885    });
886  }
887  /**
888   * Triggered when this slide becomes inactive.
889   *
890   * Slide can become inactive only after it was active.
891   */
892
893
894  deactivate() {
895    this.isActive = false;
896    this.content.deactivate();
897
898    if (this.currZoomLevel !== this.zoomLevels.initial) {
899      // allow filtering
900      this.calculateSize();
901    } // reset zoom level
902
903
904    this.currentResolution = 0;
905    this.zoomAndPanToInitial();
906    this.applyCurrentZoomPan();
907    this.updateContentSize();
908    this.pswp.dispatch('slideDeactivate', {
909      slide: this
910    });
911  }
912  /**
913   * The slide should destroy itself, it will never be used again.
914   * (unbind all events and destroy internal components)
915   */
916
917
918  destroy() {
919    this.content.hasSlide = false;
920    this.content.remove();
921    this.container.remove();
922    this.pswp.dispatch('slideDestroy', {
923      slide: this
924    });
925  }
926
927  resize() {
928    if (this.currZoomLevel === this.zoomLevels.initial || !this.isActive) {
929      // Keep initial zoom level if it was before the resize,
930      // as well as when this slide is not active
931      // Reset position and scale to original state
932      this.calculateSize();
933      this.currentResolution = 0;
934      this.zoomAndPanToInitial();
935      this.applyCurrentZoomPan();
936      this.updateContentSize();
937    } else {
938      // readjust pan position if it's beyond the bounds
939      this.calculateSize();
940      this.bounds.update(this.currZoomLevel);
941      this.panTo(this.pan.x, this.pan.y);
942    }
943  }
944  /**
945   * Apply size to current slide content,
946   * based on the current resolution and scale.
947   *
948   * @param {boolean} [force] if size should be updated even if dimensions weren't changed
949   */
950
951
952  updateContentSize(force) {
953    // Use initial zoom level
954    // if resolution is not defined (user didn't zoom yet)
955    const scaleMultiplier = this.currentResolution || this.zoomLevels.initial;
956
957    if (!scaleMultiplier) {
958      return;
959    }
960
961    const width = Math.round(this.width * scaleMultiplier) || this.pswp.viewportSize.x;
962    const height = Math.round(this.height * scaleMultiplier) || this.pswp.viewportSize.y;
963
964    if (!this.sizeChanged(width, height) && !force) {
965      return;
966    }
967
968    this.content.setDisplayedSize(width, height);
969  }
970  /**
971   * @param {number} width
972   * @param {number} height
973   */
974
975
976  sizeChanged(width, height) {
977    if (width !== this.prevDisplayedWidth || height !== this.prevDisplayedHeight) {
978      this.prevDisplayedWidth = width;
979      this.prevDisplayedHeight = height;
980      return true;
981    }
982
983    return false;
984  }
985  /** @returns {HTMLImageElement | HTMLDivElement | null | undefined} */
986
987
988  getPlaceholderElement() {
989    var _this$content$placeho;
990
991    return (_this$content$placeho = this.content.placeholder) === null || _this$content$placeho === void 0 ? void 0 : _this$content$placeho.element;
992  }
993  /**
994   * Zoom current slide image to...
995   *
996   * @param {number} destZoomLevel Destination zoom level.
997   * @param {Point} [centerPoint]
998   * Transform origin center point, or false if viewport center should be used.
999   * @param {number | false} [transitionDuration] Transition duration, may be set to 0.
1000   * @param {boolean} [ignoreBounds] Minimum and maximum zoom levels will be ignored.
1001   */
1002
1003
1004  zoomTo(destZoomLevel, centerPoint, transitionDuration, ignoreBounds) {
1005    const {
1006      pswp
1007    } = this;
1008
1009    if (!this.isZoomable() || pswp.mainScroll.isShifted()) {
1010      return;
1011    }
1012
1013    pswp.dispatch('beforeZoomTo', {
1014      destZoomLevel,
1015      centerPoint,
1016      transitionDuration
1017    }); // stop all pan and zoom transitions
1018
1019    pswp.animations.stopAllPan(); // if (!centerPoint) {
1020    //   centerPoint = pswp.getViewportCenterPoint();
1021    // }
1022
1023    const prevZoomLevel = this.currZoomLevel;
1024
1025    if (!ignoreBounds) {
1026      destZoomLevel = clamp(destZoomLevel, this.zoomLevels.min, this.zoomLevels.max);
1027    } // if (transitionDuration === undefined) {
1028    //   transitionDuration = this.pswp.options.zoomAnimationDuration;
1029    // }
1030
1031
1032    this.setZoomLevel(destZoomLevel);
1033    this.pan.x = this.calculateZoomToPanOffset('x', centerPoint, prevZoomLevel);
1034    this.pan.y = this.calculateZoomToPanOffset('y', centerPoint, prevZoomLevel);
1035    roundPoint(this.pan);
1036
1037    const finishTransition = () => {
1038      this._setResolution(destZoomLevel);
1039
1040      this.applyCurrentZoomPan();
1041    };
1042
1043    if (!transitionDuration) {
1044      finishTransition();
1045    } else {
1046      pswp.animations.startTransition({
1047        isPan: true,
1048        name: 'zoomTo',
1049        target: this.container,
1050        transform: this.getCurrentTransform(),
1051        onComplete: finishTransition,
1052        duration: transitionDuration,
1053        easing: pswp.options.easing
1054      });
1055    }
1056  }
1057  /**
1058   * @param {Point} [centerPoint]
1059   */
1060
1061
1062  toggleZoom(centerPoint) {
1063    this.zoomTo(this.currZoomLevel === this.zoomLevels.initial ? this.zoomLevels.secondary : this.zoomLevels.initial, centerPoint, this.pswp.options.zoomAnimationDuration);
1064  }
1065  /**
1066   * Updates zoom level property and recalculates new pan bounds,
1067   * unlike zoomTo it does not apply transform (use applyCurrentZoomPan)
1068   *
1069   * @param {number} currZoomLevel
1070   */
1071
1072
1073  setZoomLevel(currZoomLevel) {
1074    this.currZoomLevel = currZoomLevel;
1075    this.bounds.update(this.currZoomLevel);
1076  }
1077  /**
1078   * Get pan position after zoom at a given `point`.
1079   *
1080   * Always call setZoomLevel(newZoomLevel) beforehand to recalculate
1081   * pan bounds according to the new zoom level.
1082   *
1083   * @param {'x' | 'y'} axis
1084   * @param {Point} [point]
1085   * point based on which zoom is performed, usually refers to the current mouse position,
1086   * if false - viewport center will be used.
1087   * @param {number} [prevZoomLevel] Zoom level before new zoom was applied.
1088   * @returns {number}
1089   */
1090
1091
1092  calculateZoomToPanOffset(axis, point, prevZoomLevel) {
1093    const totalPanDistance = this.bounds.max[axis] - this.bounds.min[axis];
1094
1095    if (totalPanDistance === 0) {
1096      return this.bounds.center[axis];
1097    }
1098
1099    if (!point) {
1100      point = this.pswp.getViewportCenterPoint();
1101    }
1102
1103    if (!prevZoomLevel) {
1104      prevZoomLevel = this.zoomLevels.initial;
1105    }
1106
1107    const zoomFactor = this.currZoomLevel / prevZoomLevel;
1108    return this.bounds.correctPan(axis, (this.pan[axis] - point[axis]) * zoomFactor + point[axis]);
1109  }
1110  /**
1111   * Apply pan and keep it within bounds.
1112   *
1113   * @param {number} panX
1114   * @param {number} panY
1115   */
1116
1117
1118  panTo(panX, panY) {
1119    this.pan.x = this.bounds.correctPan('x', panX);
1120    this.pan.y = this.bounds.correctPan('y', panY);
1121    this.applyCurrentZoomPan();
1122  }
1123  /**
1124   * If the slide in the current state can be panned by the user
1125   * @returns {boolean}
1126   */
1127
1128
1129  isPannable() {
1130    return Boolean(this.width) && this.currZoomLevel > this.zoomLevels.fit;
1131  }
1132  /**
1133   * If the slide can be zoomed
1134   * @returns {boolean}
1135   */
1136
1137
1138  isZoomable() {
1139    return Boolean(this.width) && this.content.isZoomable();
1140  }
1141  /**
1142   * Apply transform and scale based on
1143   * the current pan position (this.pan) and zoom level (this.currZoomLevel)
1144   */
1145
1146
1147  applyCurrentZoomPan() {
1148    this._applyZoomTransform(this.pan.x, this.pan.y, this.currZoomLevel);
1149
1150    if (this === this.pswp.currSlide) {
1151      this.pswp.dispatch('zoomPanUpdate', {
1152        slide: this
1153      });
1154    }
1155  }
1156
1157  zoomAndPanToInitial() {
1158    this.currZoomLevel = this.zoomLevels.initial; // pan according to the zoom level
1159
1160    this.bounds.update(this.currZoomLevel);
1161    equalizePoints(this.pan, this.bounds.center);
1162    this.pswp.dispatch('initialZoomPan', {
1163      slide: this
1164    });
1165  }
1166  /**
1167   * Set translate and scale based on current resolution
1168   *
1169   * @param {number} x
1170   * @param {number} y
1171   * @param {number} zoom
1172   * @private
1173   */
1174
1175
1176  _applyZoomTransform(x, y, zoom) {
1177    zoom /= this.currentResolution || this.zoomLevels.initial;
1178    setTransform(this.container, x, y, zoom);
1179  }
1180
1181  calculateSize() {
1182    const {
1183      pswp
1184    } = this;
1185    equalizePoints(this.panAreaSize, getPanAreaSize(pswp.options, pswp.viewportSize, this.data, this.index));
1186    this.zoomLevels.update(this.width, this.height, this.panAreaSize);
1187    pswp.dispatch('calcSlideSize', {
1188      slide: this
1189    });
1190  }
1191  /** @returns {string} */
1192
1193
1194  getCurrentTransform() {
1195    const scale = this.currZoomLevel / (this.currentResolution || this.zoomLevels.initial);
1196    return toTransformString(this.pan.x, this.pan.y, scale);
1197  }
1198  /**
1199   * Set resolution and re-render the image.
1200   *
1201   * For example, if the real image size is 2000x1500,
1202   * and resolution is 0.5 - it will be rendered as 1000x750.
1203   *
1204   * Image with zoom level 2 and resolution 0.5 is
1205   * the same as image with zoom level 1 and resolution 1.
1206   *
1207   * Used to optimize animations and make
1208   * sure that browser renders image in the highest quality.
1209   * Also used by responsive images to load the correct one.
1210   *
1211   * @param {number} newResolution
1212   */
1213
1214
1215  _setResolution(newResolution) {
1216    if (newResolution === this.currentResolution) {
1217      return;
1218    }
1219
1220    this.currentResolution = newResolution;
1221    this.updateContentSize();
1222    this.pswp.dispatch('resolutionChanged');
1223  }
1224
1225}
1226
1227/** @typedef {import('../photoswipe.js').Point} Point */
1228
1229/** @typedef {import('./gestures.js').default} Gestures */
1230
1231const PAN_END_FRICTION = 0.35;
1232const VERTICAL_DRAG_FRICTION = 0.6; // 1 corresponds to the third of viewport height
1233
1234const MIN_RATIO_TO_CLOSE = 0.4; // Minimum speed required to navigate
1235// to next or previous slide
1236
1237const MIN_NEXT_SLIDE_SPEED = 0.5;
1238/**
1239 * @param {number} initialVelocity
1240 * @param {number} decelerationRate
1241 * @returns {number}
1242 */
1243
1244function project(initialVelocity, decelerationRate) {
1245  return initialVelocity * decelerationRate / (1 - decelerationRate);
1246}
1247/**
1248 * Handles single pointer dragging
1249 */
1250
1251
1252class DragHandler {
1253  /**
1254   * @param {Gestures} gestures
1255   */
1256  constructor(gestures) {
1257    this.gestures = gestures;
1258    this.pswp = gestures.pswp;
1259    /** @type {Point} */
1260
1261    this.startPan = {
1262      x: 0,
1263      y: 0
1264    };
1265  }
1266
1267  start() {
1268    if (this.pswp.currSlide) {
1269      equalizePoints(this.startPan, this.pswp.currSlide.pan);
1270    }
1271
1272    this.pswp.animations.stopAll();
1273  }
1274
1275  change() {
1276    const {
1277      p1,
1278      prevP1,
1279      dragAxis
1280    } = this.gestures;
1281    const {
1282      currSlide
1283    } = this.pswp;
1284
1285    if (dragAxis === 'y' && this.pswp.options.closeOnVerticalDrag && currSlide && currSlide.currZoomLevel <= currSlide.zoomLevels.fit && !this.gestures.isMultitouch) {
1286      // Handle vertical drag to close
1287      const panY = currSlide.pan.y + (p1.y - prevP1.y);
1288
1289      if (!this.pswp.dispatch('verticalDrag', {
1290        panY
1291      }).defaultPrevented) {
1292        this._setPanWithFriction('y', panY, VERTICAL_DRAG_FRICTION);
1293
1294        const bgOpacity = 1 - Math.abs(this._getVerticalDragRatio(currSlide.pan.y));
1295        this.pswp.applyBgOpacity(bgOpacity);
1296        currSlide.applyCurrentZoomPan();
1297      }
1298    } else {
1299      const mainScrollChanged = this._panOrMoveMainScroll('x');
1300
1301      if (!mainScrollChanged) {
1302        this._panOrMoveMainScroll('y');
1303
1304        if (currSlide) {
1305          roundPoint(currSlide.pan);
1306          currSlide.applyCurrentZoomPan();
1307        }
1308      }
1309    }
1310  }
1311
1312  end() {
1313    const {
1314      velocity
1315    } = this.gestures;
1316    const {
1317      mainScroll,
1318      currSlide
1319    } = this.pswp;
1320    let indexDiff = 0;
1321    this.pswp.animations.stopAll(); // Handle main scroll if it's shifted
1322
1323    if (mainScroll.isShifted()) {
1324      // Position of the main scroll relative to the viewport
1325      const mainScrollShiftDiff = mainScroll.x - mainScroll.getCurrSlideX(); // Ratio between 0 and 1:
1326      // 0 - slide is not visible at all,
1327      // 0.5 - half of the slide is visible
1328      // 1 - slide is fully visible
1329
1330      const currentSlideVisibilityRatio = mainScrollShiftDiff / this.pswp.viewportSize.x; // Go next slide.
1331      //
1332      // - if velocity and its direction is matched,
1333      //   and we see at least tiny part of the next slide
1334      //
1335      // - or if we see less than 50% of the current slide
1336      //   and velocity is close to 0
1337      //
1338
1339      if (velocity.x < -MIN_NEXT_SLIDE_SPEED && currentSlideVisibilityRatio < 0 || velocity.x < 0.1 && currentSlideVisibilityRatio < -0.5) {
1340        // Go to next slide
1341        indexDiff = 1;
1342        velocity.x = Math.min(velocity.x, 0);
1343      } else if (velocity.x > MIN_NEXT_SLIDE_SPEED && currentSlideVisibilityRatio > 0 || velocity.x > -0.1 && currentSlideVisibilityRatio > 0.5) {
1344        // Go to prev slide
1345        indexDiff = -1;
1346        velocity.x = Math.max(velocity.x, 0);
1347      }
1348
1349      mainScroll.moveIndexBy(indexDiff, true, velocity.x);
1350    } // Restore zoom level
1351
1352
1353    if (currSlide && currSlide.currZoomLevel > currSlide.zoomLevels.max || this.gestures.isMultitouch) {
1354      this.gestures.zoomLevels.correctZoomPan(true);
1355    } else {
1356      // we run two animations instead of one,
1357      // as each axis has own pan boundaries and thus different spring function
1358      // (correctZoomPan does not have this functionality,
1359      //  it animates all properties with single timing function)
1360      this._finishPanGestureForAxis('x');
1361
1362      this._finishPanGestureForAxis('y');
1363    }
1364  }
1365  /**
1366   * @private
1367   * @param {'x' | 'y'} axis
1368   */
1369
1370
1371  _finishPanGestureForAxis(axis) {
1372    const {
1373      velocity
1374    } = this.gestures;
1375    const {
1376      currSlide
1377    } = this.pswp;
1378
1379    if (!currSlide) {
1380      return;
1381    }
1382
1383    const {
1384      pan,
1385      bounds
1386    } = currSlide;
1387    const panPos = pan[axis];
1388    const restoreBgOpacity = this.pswp.bgOpacity < 1 && axis === 'y'; // 0.995 means - scroll view loses 0.5% of its velocity per millisecond
1389    // Increasing this number will reduce travel distance
1390
1391    const decelerationRate = 0.995; // 0.99
1392    // Pan position if there is no bounds
1393
1394    const projectedPosition = panPos + project(velocity[axis], decelerationRate);
1395
1396    if (restoreBgOpacity) {
1397      const vDragRatio = this._getVerticalDragRatio(panPos);
1398
1399      const projectedVDragRatio = this._getVerticalDragRatio(projectedPosition); // If we are above and moving upwards,
1400      // or if we are below and moving downwards
1401
1402
1403      if (vDragRatio < 0 && projectedVDragRatio < -MIN_RATIO_TO_CLOSE || vDragRatio > 0 && projectedVDragRatio > MIN_RATIO_TO_CLOSE) {
1404        this.pswp.close();
1405        return;
1406      }
1407    } // Pan position with corrected bounds
1408
1409
1410    const correctedPanPosition = bounds.correctPan(axis, projectedPosition); // Exit if pan position should not be changed
1411    // or if speed it too low
1412
1413    if (panPos === correctedPanPosition) {
1414      return;
1415    } // Overshoot if the final position is out of pan bounds
1416
1417
1418    const dampingRatio = correctedPanPosition === projectedPosition ? 1 : 0.82;
1419    const initialBgOpacity = this.pswp.bgOpacity;
1420    const totalPanDist = correctedPanPosition - panPos;
1421    this.pswp.animations.startSpring({
1422      name: 'panGesture' + axis,
1423      isPan: true,
1424      start: panPos,
1425      end: correctedPanPosition,
1426      velocity: velocity[axis],
1427      dampingRatio,
1428      onUpdate: pos => {
1429        // Animate opacity of background relative to Y pan position of an image
1430        if (restoreBgOpacity && this.pswp.bgOpacity < 1) {
1431          // 0 - start of animation, 1 - end of animation
1432          const animationProgressRatio = 1 - (correctedPanPosition - pos) / totalPanDist; // We clamp opacity to keep it between 0 and 1.
1433          // As progress ratio can be larger than 1 due to overshoot,
1434          // and we do not want to bounce opacity.
1435
1436          this.pswp.applyBgOpacity(clamp(initialBgOpacity + (1 - initialBgOpacity) * animationProgressRatio, 0, 1));
1437        }
1438
1439        pan[axis] = Math.floor(pos);
1440        currSlide.applyCurrentZoomPan();
1441      }
1442    });
1443  }
1444  /**
1445   * Update position of the main scroll,
1446   * or/and update pan position of the current slide.
1447   *
1448   * Should return true if it changes (or can change) main scroll.
1449   *
1450   * @private
1451   * @param {'x' | 'y'} axis
1452   * @returns {boolean}
1453   */
1454
1455
1456  _panOrMoveMainScroll(axis) {
1457    const {
1458      p1,
1459      dragAxis,
1460      prevP1,
1461      isMultitouch
1462    } = this.gestures;
1463    const {
1464      currSlide,
1465      mainScroll
1466    } = this.pswp;
1467    const delta = p1[axis] - prevP1[axis];
1468    const newMainScrollX = mainScroll.x + delta;
1469
1470    if (!delta || !currSlide) {
1471      return false;
1472    } // Always move main scroll if image can not be panned
1473
1474
1475    if (axis === 'x' && !currSlide.isPannable() && !isMultitouch) {
1476      mainScroll.moveTo(newMainScrollX, true);
1477      return true; // changed main scroll
1478    }
1479
1480    const {
1481      bounds
1482    } = currSlide;
1483    const newPan = currSlide.pan[axis] + delta;
1484
1485    if (this.pswp.options.allowPanToNext && dragAxis === 'x' && axis === 'x' && !isMultitouch) {
1486      const currSlideMainScrollX = mainScroll.getCurrSlideX(); // Position of the main scroll relative to the viewport
1487
1488      const mainScrollShiftDiff = mainScroll.x - currSlideMainScrollX;
1489      const isLeftToRight = delta > 0;
1490      const isRightToLeft = !isLeftToRight;
1491
1492      if (newPan > bounds.min[axis] && isLeftToRight) {
1493        // Panning from left to right, beyond the left edge
1494        // Wether the image was at minimum pan position (or less)
1495        // when this drag gesture started.
1496        // Minimum pan position refers to the left edge of the image.
1497        const wasAtMinPanPosition = bounds.min[axis] <= this.startPan[axis];
1498
1499        if (wasAtMinPanPosition) {
1500          mainScroll.moveTo(newMainScrollX, true);
1501          return true;
1502        } else {
1503          this._setPanWithFriction(axis, newPan); //currSlide.pan[axis] = newPan;
1504
1505        }
1506      } else if (newPan < bounds.max[axis] && isRightToLeft) {
1507        // Paning from right to left, beyond the right edge
1508        // Maximum pan position refers to the right edge of the image.
1509        const wasAtMaxPanPosition = this.startPan[axis] <= bounds.max[axis];
1510
1511        if (wasAtMaxPanPosition) {
1512          mainScroll.moveTo(newMainScrollX, true);
1513          return true;
1514        } else {
1515          this._setPanWithFriction(axis, newPan); //currSlide.pan[axis] = newPan;
1516
1517        }
1518      } else {
1519        // If main scroll is shifted
1520        if (mainScrollShiftDiff !== 0) {
1521          // If main scroll is shifted right
1522          if (mainScrollShiftDiff > 0
1523          /*&& isRightToLeft*/
1524          ) {
1525            mainScroll.moveTo(Math.max(newMainScrollX, currSlideMainScrollX), true);
1526            return true;
1527          } else if (mainScrollShiftDiff < 0
1528          /*&& isLeftToRight*/
1529          ) {
1530            // Main scroll is shifted left (Position is less than 0 comparing to the viewport 0)
1531            mainScroll.moveTo(Math.min(newMainScrollX, currSlideMainScrollX), true);
1532            return true;
1533          }
1534        } else {
1535          // We are within pan bounds, so just pan
1536          this._setPanWithFriction(axis, newPan);
1537        }
1538      }
1539    } else {
1540      if (axis === 'y') {
1541        // Do not pan vertically if main scroll is shifted o
1542        if (!mainScroll.isShifted() && bounds.min.y !== bounds.max.y) {
1543          this._setPanWithFriction(axis, newPan);
1544        }
1545      } else {
1546        this._setPanWithFriction(axis, newPan);
1547      }
1548    }
1549
1550    return false;
1551  } // If we move above - the ratio is negative
1552  // If we move below the ratio is positive
1553
1554  /**
1555   * Relation between pan Y position and third of viewport height.
1556   *
1557   * When we are at initial position (center bounds) - the ratio is 0,
1558   * if position is shifted upwards - the ratio is negative,
1559   * if position is shifted downwards - the ratio is positive.
1560   *
1561   * @private
1562   * @param {number} panY The current pan Y position.
1563   * @returns {number}
1564   */
1565
1566
1567  _getVerticalDragRatio(panY) {
1568    var _this$pswp$currSlide$, _this$pswp$currSlide;
1569
1570    return (panY - ((_this$pswp$currSlide$ = (_this$pswp$currSlide = this.pswp.currSlide) === null || _this$pswp$currSlide === void 0 ? void 0 : _this$pswp$currSlide.bounds.center.y) !== null && _this$pswp$currSlide$ !== void 0 ? _this$pswp$currSlide$ : 0)) / (this.pswp.viewportSize.y / 3);
1571  }
1572  /**
1573   * Set pan position of the current slide.
1574   * Apply friction if the position is beyond the pan bounds,
1575   * or if custom friction is defined.
1576   *
1577   * @private
1578   * @param {'x' | 'y'} axis
1579   * @param {number} potentialPan
1580   * @param {number} [customFriction] (0.1 - 1)
1581   */
1582
1583
1584  _setPanWithFriction(axis, potentialPan, customFriction) {
1585    const {
1586      currSlide
1587    } = this.pswp;
1588
1589    if (!currSlide) {
1590      return;
1591    }
1592
1593    const {
1594      pan,
1595      bounds
1596    } = currSlide;
1597    const correctedPan = bounds.correctPan(axis, potentialPan); // If we are out of pan bounds
1598
1599    if (correctedPan !== potentialPan || customFriction) {
1600      const delta = Math.round(potentialPan - pan[axis]);
1601      pan[axis] += delta * (customFriction || PAN_END_FRICTION);
1602    } else {
1603      pan[axis] = potentialPan;
1604    }
1605  }
1606
1607}
1608
1609/** @typedef {import('../photoswipe.js').Point} Point */
1610
1611/** @typedef {import('./gestures.js').default} Gestures */
1612
1613const UPPER_ZOOM_FRICTION = 0.05;
1614const LOWER_ZOOM_FRICTION = 0.15;
1615/**
1616 * Get center point between two points
1617 *
1618 * @param {Point} p
1619 * @param {Point} p1
1620 * @param {Point} p2
1621 * @returns {Point}
1622 */
1623
1624function getZoomPointsCenter(p, p1, p2) {
1625  p.x = (p1.x + p2.x) / 2;
1626  p.y = (p1.y + p2.y) / 2;
1627  return p;
1628}
1629
1630class ZoomHandler {
1631  /**
1632   * @param {Gestures} gestures
1633   */
1634  constructor(gestures) {
1635    this.gestures = gestures;
1636    /**
1637     * @private
1638     * @type {Point}
1639     */
1640
1641    this._startPan = {
1642      x: 0,
1643      y: 0
1644    };
1645    /**
1646     * @private
1647     * @type {Point}
1648     */
1649
1650    this._startZoomPoint = {
1651      x: 0,
1652      y: 0
1653    };
1654    /**
1655     * @private
1656     * @type {Point}
1657     */
1658
1659    this._zoomPoint = {
1660      x: 0,
1661      y: 0
1662    };
1663    /** @private */
1664
1665    this._wasOverFitZoomLevel = false;
1666    /** @private */
1667
1668    this._startZoomLevel = 1;
1669  }
1670
1671  start() {
1672    const {
1673      currSlide
1674    } = this.gestures.pswp;
1675
1676    if (currSlide) {
1677      this._startZoomLevel = currSlide.currZoomLevel;
1678      equalizePoints(this._startPan, currSlide.pan);
1679    }
1680
1681    this.gestures.pswp.animations.stopAllPan();
1682    this._wasOverFitZoomLevel = false;
1683  }
1684
1685  change() {
1686    const {
1687      p1,
1688      startP1,
1689      p2,
1690      startP2,
1691      pswp
1692    } = this.gestures;
1693    const {
1694      currSlide
1695    } = pswp;
1696
1697    if (!currSlide) {
1698      return;
1699    }
1700
1701    const minZoomLevel = currSlide.zoomLevels.min;
1702    const maxZoomLevel = currSlide.zoomLevels.max;
1703
1704    if (!currSlide.isZoomable() || pswp.mainScroll.isShifted()) {
1705      return;
1706    }
1707
1708    getZoomPointsCenter(this._startZoomPoint, startP1, startP2);
1709    getZoomPointsCenter(this._zoomPoint, p1, p2);
1710
1711    let currZoomLevel = 1 / getDistanceBetween(startP1, startP2) * getDistanceBetween(p1, p2) * this._startZoomLevel; // slightly over the zoom.fit
1712
1713
1714    if (currZoomLevel > currSlide.zoomLevels.initial + currSlide.zoomLevels.initial / 15) {
1715      this._wasOverFitZoomLevel = true;
1716    }
1717
1718    if (currZoomLevel < minZoomLevel) {
1719      if (pswp.options.pinchToClose && !this._wasOverFitZoomLevel && this._startZoomLevel <= currSlide.zoomLevels.initial) {
1720        // fade out background if zooming out
1721        const bgOpacity = 1 - (minZoomLevel - currZoomLevel) / (minZoomLevel / 1.2);
1722
1723        if (!pswp.dispatch('pinchClose', {
1724          bgOpacity
1725        }).defaultPrevented) {
1726          pswp.applyBgOpacity(bgOpacity);
1727        }
1728      } else {
1729        // Apply the friction if zoom level is below the min
1730        currZoomLevel = minZoomLevel - (minZoomLevel - currZoomLevel) * LOWER_ZOOM_FRICTION;
1731      }
1732    } else if (currZoomLevel > maxZoomLevel) {
1733      // Apply the friction if zoom level is above the max
1734      currZoomLevel = maxZoomLevel + (currZoomLevel - maxZoomLevel) * UPPER_ZOOM_FRICTION;
1735    }
1736
1737    currSlide.pan.x = this._calculatePanForZoomLevel('x', currZoomLevel);
1738    currSlide.pan.y = this._calculatePanForZoomLevel('y', currZoomLevel);
1739    currSlide.setZoomLevel(currZoomLevel);
1740    currSlide.applyCurrentZoomPan();
1741  }
1742
1743  end() {
1744    const {
1745      pswp
1746    } = this.gestures;
1747    const {
1748      currSlide
1749    } = pswp;
1750
1751    if ((!currSlide || currSlide.currZoomLevel < currSlide.zoomLevels.initial) && !this._wasOverFitZoomLevel && pswp.options.pinchToClose) {
1752      pswp.close();
1753    } else {
1754      this.correctZoomPan();
1755    }
1756  }
1757  /**
1758   * @private
1759   * @param {'x' | 'y'} axis
1760   * @param {number} currZoomLevel
1761   * @returns {number}
1762   */
1763
1764
1765  _calculatePanForZoomLevel(axis, currZoomLevel) {
1766    const zoomFactor = currZoomLevel / this._startZoomLevel;
1767    return this._zoomPoint[axis] - (this._startZoomPoint[axis] - this._startPan[axis]) * zoomFactor;
1768  }
1769  /**
1770   * Correct currZoomLevel and pan if they are
1771   * beyond minimum or maximum values.
1772   * With animation.
1773   *
1774   * @param {boolean} [ignoreGesture]
1775   * Wether gesture coordinates should be ignored when calculating destination pan position.
1776   */
1777
1778
1779  correctZoomPan(ignoreGesture) {
1780    const {
1781      pswp
1782    } = this.gestures;
1783    const {
1784      currSlide
1785    } = pswp;
1786
1787    if (!(currSlide !== null && currSlide !== void 0 && currSlide.isZoomable())) {
1788      return;
1789    }
1790
1791    if (this._zoomPoint.x === 0) {
1792      ignoreGesture = true;
1793    }
1794
1795    const prevZoomLevel = currSlide.currZoomLevel;
1796    /** @type {number} */
1797
1798    let destinationZoomLevel;
1799    let currZoomLevelNeedsChange = true;
1800
1801    if (prevZoomLevel < currSlide.zoomLevels.initial) {
1802      destinationZoomLevel = currSlide.zoomLevels.initial; // zoom to min
1803    } else if (prevZoomLevel > currSlide.zoomLevels.max) {
1804      destinationZoomLevel = currSlide.zoomLevels.max; // zoom to max
1805    } else {
1806      currZoomLevelNeedsChange = false;
1807      destinationZoomLevel = prevZoomLevel;
1808    }
1809
1810    const initialBgOpacity = pswp.bgOpacity;
1811    const restoreBgOpacity = pswp.bgOpacity < 1;
1812    const initialPan = equalizePoints({
1813      x: 0,
1814      y: 0
1815    }, currSlide.pan);
1816    let destinationPan = equalizePoints({
1817      x: 0,
1818      y: 0
1819    }, initialPan);
1820
1821    if (ignoreGesture) {
1822      this._zoomPoint.x = 0;
1823      this._zoomPoint.y = 0;
1824      this._startZoomPoint.x = 0;
1825      this._startZoomPoint.y = 0;
1826      this._startZoomLevel = prevZoomLevel;
1827      equalizePoints(this._startPan, initialPan);
1828    }
1829
1830    if (currZoomLevelNeedsChange) {
1831      destinationPan = {
1832        x: this._calculatePanForZoomLevel('x', destinationZoomLevel),
1833        y: this._calculatePanForZoomLevel('y', destinationZoomLevel)
1834      };
1835    } // set zoom level, so pan bounds are updated according to it
1836
1837
1838    currSlide.setZoomLevel(destinationZoomLevel);
1839    destinationPan = {
1840      x: currSlide.bounds.correctPan('x', destinationPan.x),
1841      y: currSlide.bounds.correctPan('y', destinationPan.y)
1842    }; // return zoom level and its bounds to initial
1843
1844    currSlide.setZoomLevel(prevZoomLevel);
1845    const panNeedsChange = !pointsEqual(destinationPan, initialPan);
1846
1847    if (!panNeedsChange && !currZoomLevelNeedsChange && !restoreBgOpacity) {
1848      // update resolution after gesture
1849      currSlide._setResolution(destinationZoomLevel);
1850
1851      currSlide.applyCurrentZoomPan(); // nothing to animate
1852
1853      return;
1854    }
1855
1856    pswp.animations.stopAllPan();
1857    pswp.animations.startSpring({
1858      isPan: true,
1859      start: 0,
1860      end: 1000,
1861      velocity: 0,
1862      dampingRatio: 1,
1863      naturalFrequency: 40,
1864      onUpdate: now => {
1865        now /= 1000; // 0 - start, 1 - end
1866
1867        if (panNeedsChange || currZoomLevelNeedsChange) {
1868          if (panNeedsChange) {
1869            currSlide.pan.x = initialPan.x + (destinationPan.x - initialPan.x) * now;
1870            currSlide.pan.y = initialPan.y + (destinationPan.y - initialPan.y) * now;
1871          }
1872
1873          if (currZoomLevelNeedsChange) {
1874            const newZoomLevel = prevZoomLevel + (destinationZoomLevel - prevZoomLevel) * now;
1875            currSlide.setZoomLevel(newZoomLevel);
1876          }
1877
1878          currSlide.applyCurrentZoomPan();
1879        } // Restore background opacity
1880
1881
1882        if (restoreBgOpacity && pswp.bgOpacity < 1) {
1883          // We clamp opacity to keep it between 0 and 1.
1884          // As progress ratio can be larger than 1 due to overshoot,
1885          // and we do not want to bounce opacity.
1886          pswp.applyBgOpacity(clamp(initialBgOpacity + (1 - initialBgOpacity) * now, 0, 1));
1887        }
1888      },
1889      onComplete: () => {
1890        // update resolution after transition ends
1891        currSlide._setResolution(destinationZoomLevel);
1892
1893        currSlide.applyCurrentZoomPan();
1894      }
1895    });
1896  }
1897
1898}
1899
1900/**
1901 * @template {string} T
1902 * @template {string} P
1903 * @typedef {import('../types.js').AddPostfix<T, P>} AddPostfix<T, P>
1904 */
1905
1906/** @typedef {import('./gestures.js').default} Gestures */
1907
1908/** @typedef {import('../photoswipe.js').Point} Point */
1909
1910/** @typedef {'imageClick' | 'bgClick' | 'tap' | 'doubleTap'} Actions */
1911
1912/**
1913 * Whether the tap was performed on the main slide
1914 * (rather than controls or caption).
1915 *
1916 * @param {PointerEvent} event
1917 * @returns {boolean}
1918 */
1919function didTapOnMainContent(event) {
1920  return !!
1921  /** @type {HTMLElement} */
1922  event.target.closest('.pswp__container');
1923}
1924/**
1925 * Tap, double-tap handler.
1926 */
1927
1928
1929class TapHandler {
1930  /**
1931   * @param {Gestures} gestures
1932   */
1933  constructor(gestures) {
1934    this.gestures = gestures;
1935  }
1936  /**
1937   * @param {Point} point
1938   * @param {PointerEvent} originalEvent
1939   */
1940
1941
1942  click(point, originalEvent) {
1943    const targetClassList =
1944    /** @type {HTMLElement} */
1945    originalEvent.target.classList;
1946    const isImageClick = targetClassList.contains('pswp__img');
1947    const isBackgroundClick = targetClassList.contains('pswp__item') || targetClassList.contains('pswp__zoom-wrap');
1948
1949    if (isImageClick) {
1950      this._doClickOrTapAction('imageClick', point, originalEvent);
1951    } else if (isBackgroundClick) {
1952      this._doClickOrTapAction('bgClick', point, originalEvent);
1953    }
1954  }
1955  /**
1956   * @param {Point} point
1957   * @param {PointerEvent} originalEvent
1958   */
1959
1960
1961  tap(point, originalEvent) {
1962    if (didTapOnMainContent(originalEvent)) {
1963      this._doClickOrTapAction('tap', point, originalEvent);
1964    }
1965  }
1966  /**
1967   * @param {Point} point
1968   * @param {PointerEvent} originalEvent
1969   */
1970
1971
1972  doubleTap(point, originalEvent) {
1973    if (didTapOnMainContent(originalEvent)) {
1974      this._doClickOrTapAction('doubleTap', point, originalEvent);
1975    }
1976  }
1977  /**
1978   * @private
1979   * @param {Actions} actionName
1980   * @param {Point} point
1981   * @param {PointerEvent} originalEvent
1982   */
1983
1984
1985  _doClickOrTapAction(actionName, point, originalEvent) {
1986    var _this$gestures$pswp$e;
1987
1988    const {
1989      pswp
1990    } = this.gestures;
1991    const {
1992      currSlide
1993    } = pswp;
1994    const actionFullName =
1995    /** @type {AddPostfix<Actions, 'Action'>} */
1996    actionName + 'Action';
1997    const optionValue = pswp.options[actionFullName];
1998
1999    if (pswp.dispatch(actionFullName, {
2000      point,
2001      originalEvent
2002    }).defaultPrevented) {
2003      return;
2004    }
2005
2006    if (typeof optionValue === 'function') {
2007      optionValue.call(pswp, point, originalEvent);
2008      return;
2009    }
2010
2011    switch (optionValue) {
2012      case 'close':
2013      case 'next':
2014        pswp[optionValue]();
2015        break;
2016
2017      case 'zoom':
2018        currSlide === null || currSlide === void 0 || currSlide.toggleZoom(point);
2019        break;
2020
2021      case 'zoom-or-close':
2022        // by default click zooms current image,
2023        // if it can not be zoomed - gallery will be closed
2024        if (currSlide !== null && currSlide !== void 0 && currSlide.isZoomable() && currSlide.zoomLevels.secondary !== currSlide.zoomLevels.initial) {
2025          currSlide.toggleZoom(point);
2026        } else if (pswp.options.clickToCloseNonZoomable) {
2027          pswp.close();
2028        }
2029
2030        break;
2031
2032      case 'toggle-controls':
2033        (_this$gestures$pswp$e = this.gestures.pswp.element) === null || _this$gestures$pswp$e === void 0 || _this$gestures$pswp$e.classList.toggle('pswp--ui-visible'); // if (_controlsVisible) {
2034        //   _ui.hideControls();
2035        // } else {
2036        //   _ui.showControls();
2037        // }
2038
2039        break;
2040    }
2041  }
2042
2043}
2044
2045/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
2046
2047/** @typedef {import('../photoswipe.js').Point} Point */
2048// How far should user should drag
2049// until we can determine that the gesture is swipe and its direction
2050
2051const AXIS_SWIPE_HYSTERISIS = 10; //const PAN_END_FRICTION = 0.35;
2052
2053const DOUBLE_TAP_DELAY = 300; // ms
2054
2055const MIN_TAP_DISTANCE = 25; // px
2056
2057/**
2058 * Gestures class bind touch, pointer or mouse events
2059 * and emits drag to drag-handler and zoom events zoom-handler.
2060 *
2061 * Drag and zoom events are emited in requestAnimationFrame,
2062 * and only when one of pointers was actually changed.
2063 */
2064
2065class Gestures {
2066  /**
2067   * @param {PhotoSwipe} pswp
2068   */
2069  constructor(pswp) {
2070    this.pswp = pswp;
2071    /** @type {'x' | 'y' | null} */
2072
2073    this.dragAxis = null; // point objects are defined once and reused
2074    // PhotoSwipe keeps track only of two pointers, others are ignored
2075
2076    /** @type {Point} */
2077
2078    this.p1 = {
2079      x: 0,
2080      y: 0
2081    }; // the first pressed pointer
2082
2083    /** @type {Point} */
2084
2085    this.p2 = {
2086      x: 0,
2087      y: 0
2088    }; // the second pressed pointer
2089
2090    /** @type {Point} */
2091
2092    this.prevP1 = {
2093      x: 0,
2094      y: 0
2095    };
2096    /** @type {Point} */
2097
2098    this.prevP2 = {
2099      x: 0,
2100      y: 0
2101    };
2102    /** @type {Point} */
2103
2104    this.startP1 = {
2105      x: 0,
2106      y: 0
2107    };
2108    /** @type {Point} */
2109
2110    this.startP2 = {
2111      x: 0,
2112      y: 0
2113    };
2114    /** @type {Point} */
2115
2116    this.velocity = {
2117      x: 0,
2118      y: 0
2119    };
2120    /** @type {Point}
2121     * @private
2122     */
2123
2124    this._lastStartP1 = {
2125      x: 0,
2126      y: 0
2127    };
2128    /** @type {Point}
2129     * @private
2130     */
2131
2132    this._intervalP1 = {
2133      x: 0,
2134      y: 0
2135    };
2136    /** @private */
2137
2138    this._numActivePoints = 0;
2139    /** @type {Point[]}
2140     * @private
2141     */
2142
2143    this._ongoingPointers = [];
2144    /** @private */
2145
2146    this._touchEventEnabled = 'ontouchstart' in window;
2147    /** @private */
2148
2149    this._pointerEventEnabled = !!window.PointerEvent;
2150    this.supportsTouch = this._touchEventEnabled || this._pointerEventEnabled && navigator.maxTouchPoints > 1;
2151    /** @private */
2152
2153    this._numActivePoints = 0;
2154    /** @private */
2155
2156    this._intervalTime = 0;
2157    /** @private */
2158
2159    this._velocityCalculated = false;
2160    this.isMultitouch = false;
2161    this.isDragging = false;
2162    this.isZooming = false;
2163    /** @type {number | null} */
2164
2165    this.raf = null;
2166    /** @type {NodeJS.Timeout | null}
2167     * @private
2168     */
2169
2170    this._tapTimer = null;
2171
2172    if (!this.supportsTouch) {
2173      // disable pan to next slide for non-touch devices
2174      pswp.options.allowPanToNext = false;
2175    }
2176
2177    this.drag = new DragHandler(this);
2178    this.zoomLevels = new ZoomHandler(this);
2179    this.tapHandler = new TapHandler(this);
2180    pswp.on('bindEvents', () => {
2181      pswp.events.add(pswp.scrollWrap, 'click',
2182      /** @type EventListener */
2183      this._onClick.bind(this));
2184
2185      if (this._pointerEventEnabled) {
2186        this._bindEvents('pointer', 'down', 'up', 'cancel');
2187      } else if (this._touchEventEnabled) {
2188        this._bindEvents('touch', 'start', 'end', 'cancel'); // In previous versions we also bound mouse event here,
2189        // in case device supports both touch and mouse events,
2190        // but newer versions of browsers now support PointerEvent.
2191        // on iOS10 if you bind touchmove/end after touchstart,
2192        // and you don't preventDefault touchstart (which PhotoSwipe does),
2193        // preventDefault will have no effect on touchmove and touchend.
2194        // Unless you bind it previously.
2195
2196
2197        if (pswp.scrollWrap) {
2198          pswp.scrollWrap.ontouchmove = () => {};
2199
2200          pswp.scrollWrap.ontouchend = () => {};
2201        }
2202      } else {
2203        this._bindEvents('mouse', 'down', 'up');
2204      }
2205    });
2206  }
2207  /**
2208   * @private
2209   * @param {'mouse' | 'touch' | 'pointer'} pref
2210   * @param {'down' | 'start'} down
2211   * @param {'up' | 'end'} up
2212   * @param {'cancel'} [cancel]
2213   */
2214
2215
2216  _bindEvents(pref, down, up, cancel) {
2217    const {
2218      pswp
2219    } = this;
2220    const {
2221      events
2222    } = pswp;
2223    const cancelEvent = cancel ? pref + cancel : '';
2224    events.add(pswp.scrollWrap, pref + down,
2225    /** @type EventListener */
2226    this.onPointerDown.bind(this));
2227    events.add(window, pref + 'move',
2228    /** @type EventListener */
2229    this.onPointerMove.bind(this));
2230    events.add(window, pref + up,
2231    /** @type EventListener */
2232    this.onPointerUp.bind(this));
2233
2234    if (cancelEvent) {
2235      events.add(pswp.scrollWrap, cancelEvent,
2236      /** @type EventListener */
2237      this.onPointerUp.bind(this));
2238    }
2239  }
2240  /**
2241   * @param {PointerEvent} e
2242   */
2243
2244
2245  onPointerDown(e) {
2246    // We do not call preventDefault for touch events
2247    // to allow browser to show native dialog on longpress
2248    // (the one that allows to save image or open it in new tab).
2249    //
2250    // Desktop Safari allows to drag images when preventDefault isn't called on mousedown,
2251    // even though preventDefault IS called on mousemove. That's why we preventDefault mousedown.
2252    const isMousePointer = e.type === 'mousedown' || e.pointerType === 'mouse'; // Allow dragging only via left mouse button.
2253    // http://www.quirksmode.org/js/events_properties.html
2254    // https://developer.mozilla.org/en-US/docs/Web/API/event.button
2255
2256    if (isMousePointer && e.button > 0) {
2257      return;
2258    }
2259
2260    const {
2261      pswp
2262    } = this; // if PhotoSwipe is opening or closing
2263
2264    if (!pswp.opener.isOpen) {
2265      e.preventDefault();
2266      return;
2267    }
2268
2269    if (pswp.dispatch('pointerDown', {
2270      originalEvent: e
2271    }).defaultPrevented) {
2272      return;
2273    }
2274
2275    if (isMousePointer) {
2276      pswp.mouseDetected(); // preventDefault mouse event to prevent
2277      // browser image drag feature
2278
2279      this._preventPointerEventBehaviour(e, 'down');
2280    }
2281
2282    pswp.animations.stopAll();
2283
2284    this._updatePoints(e, 'down');
2285
2286    if (this._numActivePoints === 1) {
2287      this.dragAxis = null; // we need to store initial point to determine the main axis,
2288      // drag is activated only after the axis is determined
2289
2290      equalizePoints(this.startP1, this.p1);
2291    }
2292
2293    if (this._numActivePoints > 1) {
2294      // Tap or double tap should not trigger if more than one pointer
2295      this._clearTapTimer();
2296
2297      this.isMultitouch = true;
2298    } else {
2299      this.isMultitouch = false;
2300    }
2301  }
2302  /**
2303   * @param {PointerEvent} e
2304   */
2305
2306
2307  onPointerMove(e) {
2308    this._preventPointerEventBehaviour(e, 'move');
2309
2310    if (!this._numActivePoints) {
2311      return;
2312    }
2313
2314    this._updatePoints(e, 'move');
2315
2316    if (this.pswp.dispatch('pointerMove', {
2317      originalEvent: e
2318    }).defaultPrevented) {
2319      return;
2320    }
2321
2322    if (this._numActivePoints === 1 && !this.isDragging) {
2323      if (!this.dragAxis) {
2324        this._calculateDragDirection();
2325      } // Drag axis was detected, emit drag.start
2326
2327
2328      if (this.dragAxis && !this.isDragging) {
2329        if (this.isZooming) {
2330          this.isZooming = false;
2331          this.zoomLevels.end();
2332        }
2333
2334        this.isDragging = true;
2335
2336        this._clearTapTimer(); // Tap can not trigger after drag
2337        // Adjust starting point
2338
2339
2340        this._updateStartPoints();
2341
2342        this._intervalTime = Date.now(); //this._startTime = this._intervalTime;
2343
2344        this._velocityCalculated = false;
2345        equalizePoints(this._intervalP1, this.p1);
2346        this.velocity.x = 0;
2347        this.velocity.y = 0;
2348        this.drag.start();
2349
2350        this._rafStopLoop();
2351
2352        this._rafRenderLoop();
2353      }
2354    } else if (this._numActivePoints > 1 && !this.isZooming) {
2355      this._finishDrag();
2356
2357      this.isZooming = true; // Adjust starting points
2358
2359      this._updateStartPoints();
2360
2361      this.zoomLevels.start();
2362
2363      this._rafStopLoop();
2364
2365      this._rafRenderLoop();
2366    }
2367  }
2368  /**
2369   * @private
2370   */
2371
2372
2373  _finishDrag() {
2374    if (this.isDragging) {
2375      this.isDragging = false; // Try to calculate velocity,
2376      // if it wasn't calculated yet in drag.change
2377
2378      if (!this._velocityCalculated) {
2379        this._updateVelocity(true);
2380      }
2381
2382      this.drag.end();
2383      this.dragAxis = null;
2384    }
2385  }
2386  /**
2387   * @param {PointerEvent} e
2388   */
2389
2390
2391  onPointerUp(e) {
2392    if (!this._numActivePoints) {
2393      return;
2394    }
2395
2396    this._updatePoints(e, 'up');
2397
2398    if (this.pswp.dispatch('pointerUp', {
2399      originalEvent: e
2400    }).defaultPrevented) {
2401      return;
2402    }
2403
2404    if (this._numActivePoints === 0) {
2405      this._rafStopLoop();
2406
2407      if (this.isDragging) {
2408        this._finishDrag();
2409      } else if (!this.isZooming && !this.isMultitouch) {
2410        //this.zoomLevels.correctZoomPan();
2411        this._finishTap(e);
2412      }
2413    }
2414
2415    if (this._numActivePoints < 2 && this.isZooming) {
2416      this.isZooming = false;
2417      this.zoomLevels.end();
2418
2419      if (this._numActivePoints === 1) {
2420        // Since we have 1 point left, we need to reinitiate drag
2421        this.dragAxis = null;
2422
2423        this._updateStartPoints();
2424      }
2425    }
2426  }
2427  /**
2428   * @private
2429   */
2430
2431
2432  _rafRenderLoop() {
2433    if (this.isDragging || this.isZooming) {
2434      this._updateVelocity();
2435
2436      if (this.isDragging) {
2437        // make sure that pointer moved since the last update
2438        if (!pointsEqual(this.p1, this.prevP1)) {
2439          this.drag.change();
2440        }
2441      } else
2442        /* if (this.isZooming) */
2443        {
2444          if (!pointsEqual(this.p1, this.prevP1) || !pointsEqual(this.p2, this.prevP2)) {
2445            this.zoomLevels.change();
2446          }
2447        }
2448
2449      this._updatePrevPoints();
2450
2451      this.raf = requestAnimationFrame(this._rafRenderLoop.bind(this));
2452    }
2453  }
2454  /**
2455   * Update velocity at 50ms interval
2456   *
2457   * @private
2458   * @param {boolean} [force]
2459   */
2460
2461
2462  _updateVelocity(force) {
2463    const time = Date.now();
2464    const duration = time - this._intervalTime;
2465
2466    if (duration < 50 && !force) {
2467      return;
2468    }
2469
2470    this.velocity.x = this._getVelocity('x', duration);
2471    this.velocity.y = this._getVelocity('y', duration);
2472    this._intervalTime = time;
2473    equalizePoints(this._intervalP1, this.p1);
2474    this._velocityCalculated = true;
2475  }
2476  /**
2477   * @private
2478   * @param {PointerEvent} e
2479   */
2480
2481
2482  _finishTap(e) {
2483    const {
2484      mainScroll
2485    } = this.pswp; // Do not trigger tap events if main scroll is shifted
2486
2487    if (mainScroll.isShifted()) {
2488      // restore main scroll position
2489      // (usually happens if stopped in the middle of animation)
2490      mainScroll.moveIndexBy(0, true);
2491      return;
2492    } // Do not trigger tap for touchcancel or pointercancel
2493
2494
2495    if (e.type.indexOf('cancel') > 0) {
2496      return;
2497    } // Trigger click instead of tap for mouse events
2498
2499
2500    if (e.type === 'mouseup' || e.pointerType === 'mouse') {
2501      this.tapHandler.click(this.startP1, e);
2502      return;
2503    } // Disable delay if there is no doubleTapAction
2504
2505
2506    const tapDelay = this.pswp.options.doubleTapAction ? DOUBLE_TAP_DELAY : 0; // If tapTimer is defined - we tapped recently,
2507    // check if the current tap is close to the previous one,
2508    // if yes - trigger double tap
2509
2510    if (this._tapTimer) {
2511      this._clearTapTimer(); // Check if two taps were more or less on the same place
2512
2513
2514      if (getDistanceBetween(this._lastStartP1, this.startP1) < MIN_TAP_DISTANCE) {
2515        this.tapHandler.doubleTap(this.startP1, e);
2516      }
2517    } else {
2518      equalizePoints(this._lastStartP1, this.startP1);
2519      this._tapTimer = setTimeout(() => {
2520        this.tapHandler.tap(this.startP1, e);
2521
2522        this._clearTapTimer();
2523      }, tapDelay);
2524    }
2525  }
2526  /**
2527   * @private
2528   */
2529
2530
2531  _clearTapTimer() {
2532    if (this._tapTimer) {
2533      clearTimeout(this._tapTimer);
2534      this._tapTimer = null;
2535    }
2536  }
2537  /**
2538   * Get velocity for axis
2539   *
2540   * @private
2541   * @param {'x' | 'y'} axis
2542   * @param {number} duration
2543   * @returns {number}
2544   */
2545
2546
2547  _getVelocity(axis, duration) {
2548    // displacement is like distance, but can be negative.
2549    const displacement = this.p1[axis] - this._intervalP1[axis];
2550
2551    if (Math.abs(displacement) > 1 && duration > 5) {
2552      return displacement / duration;
2553    }
2554
2555    return 0;
2556  }
2557  /**
2558   * @private
2559   */
2560
2561
2562  _rafStopLoop() {
2563    if (this.raf) {
2564      cancelAnimationFrame(this.raf);
2565      this.raf = null;
2566    }
2567  }
2568  /**
2569   * @private
2570   * @param {PointerEvent} e
2571   * @param {'up' | 'down' | 'move'} pointerType Normalized pointer type
2572   */
2573
2574
2575  _preventPointerEventBehaviour(e, pointerType) {
2576    const preventPointerEvent = this.pswp.applyFilters('preventPointerEvent', true, e, pointerType);
2577
2578    if (preventPointerEvent) {
2579      e.preventDefault();
2580    }
2581  }
2582  /**
2583   * Parses and normalizes points from the touch, mouse or pointer event.
2584   * Updates p1 and p2.
2585   *
2586   * @private
2587   * @param {PointerEvent | TouchEvent} e
2588   * @param {'up' | 'down' | 'move'} pointerType Normalized pointer type
2589   */
2590
2591
2592  _updatePoints(e, pointerType) {
2593    if (this._pointerEventEnabled) {
2594      const pointerEvent =
2595      /** @type {PointerEvent} */
2596      e; // Try to find the current pointer in ongoing pointers by its ID
2597
2598      const pointerIndex = this._ongoingPointers.findIndex(ongoingPointer => {
2599        return ongoingPointer.id === pointerEvent.pointerId;
2600      });
2601
2602      if (pointerType === 'up' && pointerIndex > -1) {
2603        // release the pointer - remove it from ongoing
2604        this._ongoingPointers.splice(pointerIndex, 1);
2605      } else if (pointerType === 'down' && pointerIndex === -1) {
2606        // add new pointer
2607        this._ongoingPointers.push(this._convertEventPosToPoint(pointerEvent, {
2608          x: 0,
2609          y: 0
2610        }));
2611      } else if (pointerIndex > -1) {
2612        // update existing pointer
2613        this._convertEventPosToPoint(pointerEvent, this._ongoingPointers[pointerIndex]);
2614      }
2615
2616      this._numActivePoints = this._ongoingPointers.length; // update points that PhotoSwipe uses
2617      // to calculate position and scale
2618
2619      if (this._numActivePoints > 0) {
2620        equalizePoints(this.p1, this._ongoingPointers[0]);
2621      }
2622
2623      if (this._numActivePoints > 1) {
2624        equalizePoints(this.p2, this._ongoingPointers[1]);
2625      }
2626    } else {
2627      const touchEvent =
2628      /** @type {TouchEvent} */
2629      e;
2630      this._numActivePoints = 0;
2631
2632      if (touchEvent.type.indexOf('touch') > -1) {
2633        // Touch Event
2634        // https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent
2635        if (touchEvent.touches && touchEvent.touches.length > 0) {
2636          this._convertEventPosToPoint(touchEvent.touches[0], this.p1);
2637
2638          this._numActivePoints++;
2639
2640          if (touchEvent.touches.length > 1) {
2641            this._convertEventPosToPoint(touchEvent.touches[1], this.p2);
2642
2643            this._numActivePoints++;
2644          }
2645        }
2646      } else {
2647        // Mouse Event
2648        this._convertEventPosToPoint(
2649        /** @type {PointerEvent} */
2650        e, this.p1);
2651
2652        if (pointerType === 'up') {
2653          // clear all points on mouseup
2654          this._numActivePoints = 0;
2655        } else {
2656          this._numActivePoints++;
2657        }
2658      }
2659    }
2660  }
2661  /** update points that were used during previous rAF tick
2662   * @private
2663   */
2664
2665
2666  _updatePrevPoints() {
2667    equalizePoints(this.prevP1, this.p1);
2668    equalizePoints(this.prevP2, this.p2);
2669  }
2670  /** update points at the start of gesture
2671   * @private
2672   */
2673
2674
2675  _updateStartPoints() {
2676    equalizePoints(this.startP1, this.p1);
2677    equalizePoints(this.startP2, this.p2);
2678
2679    this._updatePrevPoints();
2680  }
2681  /** @private */
2682
2683
2684  _calculateDragDirection() {
2685    if (this.pswp.mainScroll.isShifted()) {
2686      // if main scroll position is shifted – direction is always horizontal
2687      this.dragAxis = 'x';
2688    } else {
2689      // calculate delta of the last touchmove tick
2690      const diff = Math.abs(this.p1.x - this.startP1.x) - Math.abs(this.p1.y - this.startP1.y);
2691
2692      if (diff !== 0) {
2693        // check if pointer was shifted horizontally or vertically
2694        const axisToCheck = diff > 0 ? 'x' : 'y';
2695
2696        if (Math.abs(this.p1[axisToCheck] - this.startP1[axisToCheck]) >= AXIS_SWIPE_HYSTERISIS) {
2697          this.dragAxis = axisToCheck;
2698        }
2699      }
2700    }
2701  }
2702  /**
2703   * Converts touch, pointer or mouse event
2704   * to PhotoSwipe point.
2705   *
2706   * @private
2707   * @param {Touch | PointerEvent} e
2708   * @param {Point} p
2709   * @returns {Point}
2710   */
2711
2712
2713  _convertEventPosToPoint(e, p) {
2714    p.x = e.pageX - this.pswp.offset.x;
2715    p.y = e.pageY - this.pswp.offset.y;
2716
2717    if ('pointerId' in e) {
2718      p.id = e.pointerId;
2719    } else if (e.identifier !== undefined) {
2720      p.id = e.identifier;
2721    }
2722
2723    return p;
2724  }
2725  /**
2726   * @private
2727   * @param {PointerEvent} e
2728   */
2729
2730
2731  _onClick(e) {
2732    // Do not allow click event to pass through after drag
2733    if (this.pswp.mainScroll.isShifted()) {
2734      e.preventDefault();
2735      e.stopPropagation();
2736    }
2737  }
2738
2739}
2740
2741/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
2742
2743/** @typedef {import('./slide/slide.js').default} Slide */
2744
2745/** @typedef {{ el: HTMLDivElement; slide?: Slide }} ItemHolder */
2746
2747const MAIN_SCROLL_END_FRICTION = 0.35; // const MIN_SWIPE_TRANSITION_DURATION = 250;
2748// const MAX_SWIPE_TRABSITION_DURATION = 500;
2749// const DEFAULT_SWIPE_TRANSITION_DURATION = 333;
2750
2751/**
2752 * Handles movement of the main scrolling container
2753 * (for example, it repositions when user swipes left or right).
2754 *
2755 * Also stores its state.
2756 */
2757
2758class MainScroll {
2759  /**
2760   * @param {PhotoSwipe} pswp
2761   */
2762  constructor(pswp) {
2763    this.pswp = pswp;
2764    this.x = 0;
2765    this.slideWidth = 0;
2766    /** @private */
2767
2768    this._currPositionIndex = 0;
2769    /** @private */
2770
2771    this._prevPositionIndex = 0;
2772    /** @private */
2773
2774    this._containerShiftIndex = -1;
2775    /** @type {ItemHolder[]} */
2776
2777    this.itemHolders = [];
2778  }
2779  /**
2780   * Position the scroller and slide containers
2781   * according to viewport size.
2782   *
2783   * @param {boolean} [resizeSlides] Whether slides content should resized
2784   */
2785
2786
2787  resize(resizeSlides) {
2788    const {
2789      pswp
2790    } = this;
2791    const newSlideWidth = Math.round(pswp.viewportSize.x + pswp.viewportSize.x * pswp.options.spacing); // Mobile browsers might trigger a resize event during a gesture.
2792    // (due to toolbar appearing or hiding).
2793    // Avoid re-adjusting main scroll position if width wasn't changed
2794
2795    const slideWidthChanged = newSlideWidth !== this.slideWidth;
2796
2797    if (slideWidthChanged) {
2798      this.slideWidth = newSlideWidth;
2799      this.moveTo(this.getCurrSlideX());
2800    }
2801
2802    this.itemHolders.forEach((itemHolder, index) => {
2803      if (slideWidthChanged) {
2804        setTransform(itemHolder.el, (index + this._containerShiftIndex) * this.slideWidth);
2805      }
2806
2807      if (resizeSlides && itemHolder.slide) {
2808        itemHolder.slide.resize();
2809      }
2810    });
2811  }
2812  /**
2813   * Reset X position of the main scroller to zero
2814   */
2815
2816
2817  resetPosition() {
2818    // Position on the main scroller (offset)
2819    // it is independent from slide index
2820    this._currPositionIndex = 0;
2821    this._prevPositionIndex = 0; // This will force recalculation of size on next resize()
2822
2823    this.slideWidth = 0; // _containerShiftIndex*viewportSize will give you amount of transform of the current slide
2824
2825    this._containerShiftIndex = -1;
2826  }
2827  /**
2828   * Create and append array of three items
2829   * that hold data about slides in DOM
2830   */
2831
2832
2833  appendHolders() {
2834    this.itemHolders = []; // append our three slide holders -
2835    // previous, current, and next
2836
2837    for (let i = 0; i < 3; i++) {
2838      const el = createElement('pswp__item', 'div', this.pswp.container);
2839      el.setAttribute('role', 'group');
2840      el.setAttribute('aria-roledescription', 'slide');
2841      el.setAttribute('aria-hidden', 'true'); // hide nearby item holders until initial zoom animation finishes (to avoid extra Paints)
2842
2843      el.style.display = i === 1 ? 'block' : 'none';
2844      this.itemHolders.push({
2845        el //index: -1
2846
2847      });
2848    }
2849  }
2850  /**
2851   * Whether the main scroll can be horizontally swiped to the next or previous slide.
2852   * @returns {boolean}
2853   */
2854
2855
2856  canBeSwiped() {
2857    return this.pswp.getNumItems() > 1;
2858  }
2859  /**
2860   * Move main scroll by X amount of slides.
2861   * For example:
2862   *   `-1` will move to the previous slide,
2863   *    `0` will reset the scroll position of the current slide,
2864   *    `3` will move three slides forward
2865   *
2866   * If loop option is enabled - index will be automatically looped too,
2867   * (for example `-1` will move to the last slide of the gallery).
2868   *
2869   * @param {number} diff
2870   * @param {boolean} [animate]
2871   * @param {number} [velocityX]
2872   * @returns {boolean} whether index was changed or not
2873   */
2874
2875
2876  moveIndexBy(diff, animate, velocityX) {
2877    const {
2878      pswp
2879    } = this;
2880    let newIndex = pswp.potentialIndex + diff;
2881    const numSlides = pswp.getNumItems();
2882
2883    if (pswp.canLoop()) {
2884      newIndex = pswp.getLoopedIndex(newIndex);
2885      const distance = (diff + numSlides) % numSlides;
2886
2887      if (distance <= numSlides / 2) {
2888        // go forward
2889        diff = distance;
2890      } else {
2891        // go backwards
2892        diff = distance - numSlides;
2893      }
2894    } else {
2895      if (newIndex < 0) {
2896        newIndex = 0;
2897      } else if (newIndex >= numSlides) {
2898        newIndex = numSlides - 1;
2899      }
2900
2901      diff = newIndex - pswp.potentialIndex;
2902    }
2903
2904    pswp.potentialIndex = newIndex;
2905    this._currPositionIndex -= diff;
2906    pswp.animations.stopMainScroll();
2907    const destinationX = this.getCurrSlideX();
2908
2909    if (!animate) {
2910      this.moveTo(destinationX);
2911      this.updateCurrItem();
2912    } else {
2913      pswp.animations.startSpring({
2914        isMainScroll: true,
2915        start: this.x,
2916        end: destinationX,
2917        velocity: velocityX || 0,
2918        naturalFrequency: 30,
2919        dampingRatio: 1,
2920        //0.7,
2921        onUpdate: x => {
2922          this.moveTo(x);
2923        },
2924        onComplete: () => {
2925          this.updateCurrItem();
2926          pswp.appendHeavy();
2927        }
2928      });
2929      let currDiff = pswp.potentialIndex - pswp.currIndex;
2930
2931      if (pswp.canLoop()) {
2932        const currDistance = (currDiff + numSlides) % numSlides;
2933
2934        if (currDistance <= numSlides / 2) {
2935          // go forward
2936          currDiff = currDistance;
2937        } else {
2938          // go backwards
2939          currDiff = currDistance - numSlides;
2940        }
2941      } // Force-append new slides during transition
2942      // if difference between slides is more than 1
2943
2944
2945      if (Math.abs(currDiff) > 1) {
2946        this.updateCurrItem();
2947      }
2948    }
2949
2950    return Boolean(diff);
2951  }
2952  /**
2953   * X position of the main scroll for the current slide
2954   * (ignores position during dragging)
2955   * @returns {number}
2956   */
2957
2958
2959  getCurrSlideX() {
2960    return this.slideWidth * this._currPositionIndex;
2961  }
2962  /**
2963   * Whether scroll position is shifted.
2964   * For example, it will return true if the scroll is being dragged or animated.
2965   * @returns {boolean}
2966   */
2967
2968
2969  isShifted() {
2970    return this.x !== this.getCurrSlideX();
2971  }
2972  /**
2973   * Update slides X positions and set their content
2974   */
2975
2976
2977  updateCurrItem() {
2978    var _this$itemHolders$;
2979
2980    const {
2981      pswp
2982    } = this;
2983    const positionDifference = this._prevPositionIndex - this._currPositionIndex;
2984
2985    if (!positionDifference) {
2986      return;
2987    }
2988
2989    this._prevPositionIndex = this._currPositionIndex;
2990    pswp.currIndex = pswp.potentialIndex;
2991    let diffAbs = Math.abs(positionDifference);
2992    /** @type {ItemHolder | undefined} */
2993
2994    let tempHolder;
2995
2996    if (diffAbs >= 3) {
2997      this._containerShiftIndex += positionDifference + (positionDifference > 0 ? -3 : 3);
2998      diffAbs = 3; // If slides are changed by 3 screens or more - clean up previous slides
2999
3000      this.itemHolders.forEach(itemHolder => {
3001        var _itemHolder$slide;
3002
3003        (_itemHolder$slide = itemHolder.slide) === null || _itemHolder$slide === void 0 || _itemHolder$slide.destroy();
3004        itemHolder.slide = undefined;
3005      });
3006    }
3007
3008    for (let i = 0; i < diffAbs; i++) {
3009      if (positionDifference > 0) {
3010        tempHolder = this.itemHolders.shift();
3011
3012        if (tempHolder) {
3013          this.itemHolders[2] = tempHolder; // move first to last
3014
3015          this._containerShiftIndex++;
3016          setTransform(tempHolder.el, (this._containerShiftIndex + 2) * this.slideWidth);
3017          pswp.setContent(tempHolder, pswp.currIndex - diffAbs + i + 2);
3018        }
3019      } else {
3020        tempHolder = this.itemHolders.pop();
3021
3022        if (tempHolder) {
3023          this.itemHolders.unshift(tempHolder); // move last to first
3024
3025          this._containerShiftIndex--;
3026          setTransform(tempHolder.el, this._containerShiftIndex * this.slideWidth);
3027          pswp.setContent(tempHolder, pswp.currIndex + diffAbs - i - 2);
3028        }
3029      }
3030    } // Reset transfrom every 50ish navigations in one direction.
3031    //
3032    // Otherwise transform will keep growing indefinitely,
3033    // which might cause issues as browsers have a maximum transform limit.
3034    // I wasn't able to reach it, but just to be safe.
3035    // This should not cause noticable lag.
3036
3037
3038    if (Math.abs(this._containerShiftIndex) > 50 && !this.isShifted()) {
3039      this.resetPosition();
3040      this.resize();
3041    } // Pan transition might be running (and consntantly updating pan position)
3042
3043
3044    pswp.animations.stopAllPan();
3045    this.itemHolders.forEach((itemHolder, i) => {
3046      if (itemHolder.slide) {
3047        // Slide in the 2nd holder is always active
3048        itemHolder.slide.setIsActive(i === 1);
3049      }
3050    });
3051    pswp.currSlide = (_this$itemHolders$ = this.itemHolders[1]) === null || _this$itemHolders$ === void 0 ? void 0 : _this$itemHolders$.slide;
3052    pswp.contentLoader.updateLazy(positionDifference);
3053
3054    if (pswp.currSlide) {
3055      pswp.currSlide.applyCurrentZoomPan();
3056    }
3057
3058    pswp.dispatch('change');
3059  }
3060  /**
3061   * Move the X position of the main scroll container
3062   *
3063   * @param {number} x
3064   * @param {boolean} [dragging]
3065   */
3066
3067
3068  moveTo(x, dragging) {
3069    if (!this.pswp.canLoop() && dragging) {
3070      // Apply friction
3071      let newSlideIndexOffset = (this.slideWidth * this._currPositionIndex - x) / this.slideWidth;
3072      newSlideIndexOffset += this.pswp.currIndex;
3073      const delta = Math.round(x - this.x);
3074
3075      if (newSlideIndexOffset < 0 && delta > 0 || newSlideIndexOffset >= this.pswp.getNumItems() - 1 && delta < 0) {
3076        x = this.x + delta * MAIN_SCROLL_END_FRICTION;
3077      }
3078    }
3079
3080    this.x = x;
3081
3082    if (this.pswp.container) {
3083      setTransform(this.pswp.container, x);
3084    }
3085
3086    this.pswp.dispatch('moveMainScroll', {
3087      x,
3088      dragging: dragging !== null && dragging !== void 0 ? dragging : false
3089    });
3090  }
3091
3092}
3093
3094/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
3095
3096/**
3097 * @template T
3098 * @typedef {import('./types.js').Methods<T>} Methods<T>
3099 */
3100
3101const KeyboardKeyCodesMap = {
3102  Escape: 27,
3103  z: 90,
3104  ArrowLeft: 37,
3105  ArrowUp: 38,
3106  ArrowRight: 39,
3107  ArrowDown: 40,
3108  Tab: 9
3109};
3110/**
3111 * @template {keyof KeyboardKeyCodesMap} T
3112 * @param {T} key
3113 * @param {boolean} isKeySupported
3114 * @returns {T | number | undefined}
3115 */
3116
3117const getKeyboardEventKey = (key, isKeySupported) => {
3118  return isKeySupported ? key : KeyboardKeyCodesMap[key];
3119};
3120/**
3121 * - Manages keyboard shortcuts.
3122 * - Helps trap focus within photoswipe.
3123 */
3124
3125
3126class Keyboard {
3127  /**
3128   * @param {PhotoSwipe} pswp
3129   */
3130  constructor(pswp) {
3131    this.pswp = pswp;
3132    /** @private */
3133
3134    this._wasFocused = false;
3135    pswp.on('bindEvents', () => {
3136      if (pswp.options.trapFocus) {
3137        // Dialog was likely opened by keyboard if initial point is not defined
3138        if (!pswp.options.initialPointerPos) {
3139          // focus causes layout,
3140          // which causes lag during the animation,
3141          // that's why we delay it until the opener transition ends
3142          this._focusRoot();
3143        }
3144
3145        pswp.events.add(document, 'focusin',
3146        /** @type EventListener */
3147        this._onFocusIn.bind(this));
3148      }
3149
3150      pswp.events.add(document, 'keydown',
3151      /** @type EventListener */
3152      this._onKeyDown.bind(this));
3153    });
3154    const lastActiveElement =
3155    /** @type {HTMLElement} */
3156    document.activeElement;
3157    pswp.on('destroy', () => {
3158      if (pswp.options.returnFocus && lastActiveElement && this._wasFocused) {
3159        lastActiveElement.focus();
3160      }
3161    });
3162  }
3163  /** @private */
3164
3165
3166  _focusRoot() {
3167    if (!this._wasFocused && this.pswp.element) {
3168      this.pswp.element.focus();
3169      this._wasFocused = true;
3170    }
3171  }
3172  /**
3173   * @private
3174   * @param {KeyboardEvent} e
3175   */
3176
3177
3178  _onKeyDown(e) {
3179    const {
3180      pswp
3181    } = this;
3182
3183    if (pswp.dispatch('keydown', {
3184      originalEvent: e
3185    }).defaultPrevented) {
3186      return;
3187    }
3188
3189    if (specialKeyUsed(e)) {
3190      // don't do anything if special key pressed
3191      // to prevent from overriding default browser actions
3192      // for example, in Chrome on Mac cmd+arrow-left returns to previous page
3193      return;
3194    }
3195    /** @type {Methods<PhotoSwipe> | undefined} */
3196
3197
3198    let keydownAction;
3199    /** @type {'x' | 'y' | undefined} */
3200
3201    let axis;
3202    let isForward = false;
3203    const isKeySupported = ('key' in e);
3204
3205    switch (isKeySupported ? e.key : e.keyCode) {
3206      case getKeyboardEventKey('Escape', isKeySupported):
3207        if (pswp.options.escKey) {
3208          keydownAction = 'close';
3209        }
3210
3211        break;
3212
3213      case getKeyboardEventKey('z', isKeySupported):
3214        keydownAction = 'toggleZoom';
3215        break;
3216
3217      case getKeyboardEventKey('ArrowLeft', isKeySupported):
3218        axis = 'x';
3219        break;
3220
3221      case getKeyboardEventKey('ArrowUp', isKeySupported):
3222        axis = 'y';
3223        break;
3224
3225      case getKeyboardEventKey('ArrowRight', isKeySupported):
3226        axis = 'x';
3227        isForward = true;
3228        break;
3229
3230      case getKeyboardEventKey('ArrowDown', isKeySupported):
3231        isForward = true;
3232        axis = 'y';
3233        break;
3234
3235      case getKeyboardEventKey('Tab', isKeySupported):
3236        this._focusRoot();
3237
3238        break;
3239    } // if left/right/top/bottom key
3240
3241
3242    if (axis) {
3243      // prevent page scroll
3244      e.preventDefault();
3245      const {
3246        currSlide
3247      } = pswp;
3248
3249      if (pswp.options.arrowKeys && axis === 'x' && pswp.getNumItems() > 1) {
3250        keydownAction = isForward ? 'next' : 'prev';
3251      } else if (currSlide && currSlide.currZoomLevel > currSlide.zoomLevels.fit) {
3252        // up/down arrow keys pan the image vertically
3253        // left/right arrow keys pan horizontally.
3254        // Unless there is only one image,
3255        // or arrowKeys option is disabled
3256        currSlide.pan[axis] += isForward ? -80 : 80;
3257        currSlide.panTo(currSlide.pan.x, currSlide.pan.y);
3258      }
3259    }
3260
3261    if (keydownAction) {
3262      e.preventDefault(); // @ts-ignore
3263
3264      pswp[keydownAction]();
3265    }
3266  }
3267  /**
3268   * Trap focus inside photoswipe
3269   *
3270   * @private
3271   * @param {FocusEvent} e
3272   */
3273
3274
3275  _onFocusIn(e) {
3276    const {
3277      template
3278    } = this.pswp;
3279
3280    if (template && document !== e.target && template !== e.target && !template.contains(
3281    /** @type {Node} */
3282    e.target)) {
3283      // focus root element
3284      template.focus();
3285    }
3286  }
3287
3288}
3289
3290const DEFAULT_EASING = 'cubic-bezier(.4,0,.22,1)';
3291/** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */
3292
3293/** @typedef {Object} DefaultCssAnimationProps
3294 *
3295 * @prop {HTMLElement} target
3296 * @prop {number} [duration]
3297 * @prop {string} [easing]
3298 * @prop {string} [transform]
3299 * @prop {string} [opacity]
3300 * */
3301
3302/** @typedef {SharedAnimationProps & DefaultCssAnimationProps} CssAnimationProps */
3303
3304/**
3305 * Runs CSS transition.
3306 */
3307
3308class CSSAnimation {
3309  /**
3310   * onComplete can be unpredictable, be careful about current state
3311   *
3312   * @param {CssAnimationProps} props
3313   */
3314  constructor(props) {
3315    var _props$prop;
3316
3317    this.props = props;
3318    const {
3319      target,
3320      onComplete,
3321      transform,
3322      onFinish = () => {},
3323      duration = 333,
3324      easing = DEFAULT_EASING
3325    } = props;
3326    this.onFinish = onFinish; // support only transform and opacity
3327
3328    const prop = transform ? 'transform' : 'opacity';
3329    const propValue = (_props$prop = props[prop]) !== null && _props$prop !== void 0 ? _props$prop : '';
3330    /** @private */
3331
3332    this._target = target;
3333    /** @private */
3334
3335    this._onComplete = onComplete;
3336    /** @private */
3337
3338    this._finished = false;
3339    /** @private */
3340
3341    this._onTransitionEnd = this._onTransitionEnd.bind(this); // Using timeout hack to make sure that animation
3342    // starts even if the animated property was changed recently,
3343    // otherwise transitionend might not fire or transition won't start.
3344    // https://drafts.csswg.org/css-transitions/#starting
3345    //
3346    // ¯\_(ツ)_/¯
3347
3348    /** @private */
3349
3350    this._helperTimeout = setTimeout(() => {
3351      setTransitionStyle(target, prop, duration, easing);
3352      this._helperTimeout = setTimeout(() => {
3353        target.addEventListener('transitionend', this._onTransitionEnd, false);
3354        target.addEventListener('transitioncancel', this._onTransitionEnd, false); // Safari occasionally does not emit transitionend event
3355        // if element property was modified during the transition,
3356        // which may be caused by resize or third party component,
3357        // using timeout as a safety fallback
3358
3359        this._helperTimeout = setTimeout(() => {
3360          this._finalizeAnimation();
3361        }, duration + 500);
3362        target.style[prop] = propValue;
3363      }, 30); // Do not reduce this number
3364    }, 0);
3365  }
3366  /**
3367   * @private
3368   * @param {TransitionEvent} e
3369   */
3370
3371
3372  _onTransitionEnd(e) {
3373    if (e.target === this._target) {
3374      this._finalizeAnimation();
3375    }
3376  }
3377  /**
3378   * @private
3379   */
3380
3381
3382  _finalizeAnimation() {
3383    if (!this._finished) {
3384      this._finished = true;
3385      this.onFinish();
3386
3387      if (this._onComplete) {
3388        this._onComplete();
3389      }
3390    }
3391  } // Destroy is called automatically onFinish
3392
3393
3394  destroy() {
3395    if (this._helperTimeout) {
3396      clearTimeout(this._helperTimeout);
3397    }
3398
3399    removeTransitionStyle(this._target);
3400
3401    this._target.removeEventListener('transitionend', this._onTransitionEnd, false);
3402
3403    this._target.removeEventListener('transitioncancel', this._onTransitionEnd, false);
3404
3405    if (!this._finished) {
3406      this._finalizeAnimation();
3407    }
3408  }
3409
3410}
3411
3412const DEFAULT_NATURAL_FREQUENCY = 12;
3413const DEFAULT_DAMPING_RATIO = 0.75;
3414/**
3415 * Spring easing helper
3416 */
3417
3418class SpringEaser {
3419  /**
3420   * @param {number} initialVelocity Initial velocity, px per ms.
3421   *
3422   * @param {number} [dampingRatio]
3423   * Determines how bouncy animation will be.
3424   * From 0 to 1, 0 - always overshoot, 1 - do not overshoot.
3425   * "overshoot" refers to part of animation that
3426   * goes beyond the final value.
3427   *
3428   * @param {number} [naturalFrequency]
3429   * Determines how fast animation will slow down.
3430   * The higher value - the stiffer the transition will be,
3431   * and the faster it will slow down.
3432   * Recommended value from 10 to 50
3433   */
3434  constructor(initialVelocity, dampingRatio, naturalFrequency) {
3435    this.velocity = initialVelocity * 1000; // convert to "pixels per second"
3436    // https://en.wikipedia.org/wiki/Damping_ratio
3437
3438    this._dampingRatio = dampingRatio || DEFAULT_DAMPING_RATIO; // https://en.wikipedia.org/wiki/Natural_frequency
3439
3440    this._naturalFrequency = naturalFrequency || DEFAULT_NATURAL_FREQUENCY;
3441    this._dampedFrequency = this._naturalFrequency;
3442
3443    if (this._dampingRatio < 1) {
3444      this._dampedFrequency *= Math.sqrt(1 - this._dampingRatio * this._dampingRatio);
3445    }
3446  }
3447  /**
3448   * @param {number} deltaPosition Difference between current and end position of the animation
3449   * @param {number} deltaTime Frame duration in milliseconds
3450   *
3451   * @returns {number} Displacement, relative to the end position.
3452   */
3453
3454
3455  easeFrame(deltaPosition, deltaTime) {
3456    // Inspired by Apple Webkit and Android spring function implementation
3457    // https://en.wikipedia.org/wiki/Oscillation
3458    // https://en.wikipedia.org/wiki/Damping_ratio
3459    // we ignore mass (assume that it's 1kg)
3460    let displacement = 0;
3461    let coeff;
3462    deltaTime /= 1000;
3463    const naturalDumpingPow = Math.E ** (-this._dampingRatio * this._naturalFrequency * deltaTime);
3464
3465    if (this._dampingRatio === 1) {
3466      coeff = this.velocity + this._naturalFrequency * deltaPosition;
3467      displacement = (deltaPosition + coeff * deltaTime) * naturalDumpingPow;
3468      this.velocity = displacement * -this._naturalFrequency + coeff * naturalDumpingPow;
3469    } else if (this._dampingRatio < 1) {
3470      coeff = 1 / this._dampedFrequency * (this._dampingRatio * this._naturalFrequency * deltaPosition + this.velocity);
3471      const dumpedFCos = Math.cos(this._dampedFrequency * deltaTime);
3472      const dumpedFSin = Math.sin(this._dampedFrequency * deltaTime);
3473      displacement = naturalDumpingPow * (deltaPosition * dumpedFCos + coeff * dumpedFSin);
3474      this.velocity = displacement * -this._naturalFrequency * this._dampingRatio + naturalDumpingPow * (-this._dampedFrequency * deltaPosition * dumpedFSin + this._dampedFrequency * coeff * dumpedFCos);
3475    } // Overdamped (>1) damping ratio is not supported
3476
3477
3478    return displacement;
3479  }
3480
3481}
3482
3483/** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */
3484
3485/**
3486 * @typedef {Object} DefaultSpringAnimationProps
3487 *
3488 * @prop {number} start
3489 * @prop {number} end
3490 * @prop {number} velocity
3491 * @prop {number} [dampingRatio]
3492 * @prop {number} [naturalFrequency]
3493 * @prop {(end: number) => void} onUpdate
3494 */
3495
3496/** @typedef {SharedAnimationProps & DefaultSpringAnimationProps} SpringAnimationProps */
3497
3498class SpringAnimation {
3499  /**
3500   * @param {SpringAnimationProps} props
3501   */
3502  constructor(props) {
3503    this.props = props;
3504    this._raf = 0;
3505    const {
3506      start,
3507      end,
3508      velocity,
3509      onUpdate,
3510      onComplete,
3511      onFinish = () => {},
3512      dampingRatio,
3513      naturalFrequency
3514    } = props;
3515    this.onFinish = onFinish;
3516    const easer = new SpringEaser(velocity, dampingRatio, naturalFrequency);
3517    let prevTime = Date.now();
3518    let deltaPosition = start - end;
3519
3520    const animationLoop = () => {
3521      if (this._raf) {
3522        deltaPosition = easer.easeFrame(deltaPosition, Date.now() - prevTime); // Stop the animation if velocity is low and position is close to end
3523
3524        if (Math.abs(deltaPosition) < 1 && Math.abs(easer.velocity) < 50) {
3525          // Finalize the animation
3526          onUpdate(end);
3527
3528          if (onComplete) {
3529            onComplete();
3530          }
3531
3532          this.onFinish();
3533        } else {
3534          prevTime = Date.now();
3535          onUpdate(deltaPosition + end);
3536          this._raf = requestAnimationFrame(animationLoop);
3537        }
3538      }
3539    };
3540
3541    this._raf = requestAnimationFrame(animationLoop);
3542  } // Destroy is called automatically onFinish
3543
3544
3545  destroy() {
3546    if (this._raf >= 0) {
3547      cancelAnimationFrame(this._raf);
3548    }
3549
3550    this._raf = 0;
3551  }
3552
3553}
3554
3555/** @typedef {import('./css-animation.js').CssAnimationProps} CssAnimationProps */
3556
3557/** @typedef {import('./spring-animation.js').SpringAnimationProps} SpringAnimationProps */
3558
3559/** @typedef {Object} SharedAnimationProps
3560 * @prop {string} [name]
3561 * @prop {boolean} [isPan]
3562 * @prop {boolean} [isMainScroll]
3563 * @prop {VoidFunction} [onComplete]
3564 * @prop {VoidFunction} [onFinish]
3565 */
3566
3567/** @typedef {SpringAnimation | CSSAnimation} Animation */
3568
3569/** @typedef {SpringAnimationProps | CssAnimationProps} AnimationProps */
3570
3571/**
3572 * Manages animations
3573 */
3574
3575class Animations {
3576  constructor() {
3577    /** @type {Animation[]} */
3578    this.activeAnimations = [];
3579  }
3580  /**
3581   * @param {SpringAnimationProps} props
3582   */
3583
3584
3585  startSpring(props) {
3586    this._start(props, true);
3587  }
3588  /**
3589   * @param {CssAnimationProps} props
3590   */
3591
3592
3593  startTransition(props) {
3594    this._start(props);
3595  }
3596  /**
3597   * @private
3598   * @param {AnimationProps} props
3599   * @param {boolean} [isSpring]
3600   * @returns {Animation}
3601   */
3602
3603
3604  _start(props, isSpring) {
3605    const animation = isSpring ? new SpringAnimation(
3606    /** @type SpringAnimationProps */
3607    props) : new CSSAnimation(
3608    /** @type CssAnimationProps */
3609    props);
3610    this.activeAnimations.push(animation);
3611
3612    animation.onFinish = () => this.stop(animation);
3613
3614    return animation;
3615  }
3616  /**
3617   * @param {Animation} animation
3618   */
3619
3620
3621  stop(animation) {
3622    animation.destroy();
3623    const index = this.activeAnimations.indexOf(animation);
3624
3625    if (index > -1) {
3626      this.activeAnimations.splice(index, 1);
3627    }
3628  }
3629
3630  stopAll() {
3631    // _stopAllAnimations
3632    this.activeAnimations.forEach(animation => {
3633      animation.destroy();
3634    });
3635    this.activeAnimations = [];
3636  }
3637  /**
3638   * Stop all pan or zoom transitions
3639   */
3640
3641
3642  stopAllPan() {
3643    this.activeAnimations = this.activeAnimations.filter(animation => {
3644      if (animation.props.isPan) {
3645        animation.destroy();
3646        return false;
3647      }
3648
3649      return true;
3650    });
3651  }
3652
3653  stopMainScroll() {
3654    this.activeAnimations = this.activeAnimations.filter(animation => {
3655      if (animation.props.isMainScroll) {
3656        animation.destroy();
3657        return false;
3658      }
3659
3660      return true;
3661    });
3662  }
3663  /**
3664   * Returns true if main scroll transition is running
3665   */
3666  // isMainScrollRunning() {
3667  //   return this.activeAnimations.some((animation) => {
3668  //     return animation.props.isMainScroll;
3669  //   });
3670  // }
3671
3672  /**
3673   * Returns true if any pan or zoom transition is running
3674   */
3675
3676
3677  isPanRunning() {
3678    return this.activeAnimations.some(animation => {
3679      return animation.props.isPan;
3680    });
3681  }
3682
3683}
3684
3685/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
3686
3687/**
3688 * Handles scroll wheel.
3689 * Can pan and zoom current slide image.
3690 */
3691class ScrollWheel {
3692  /**
3693   * @param {PhotoSwipe} pswp
3694   */
3695  constructor(pswp) {
3696    this.pswp = pswp;
3697    pswp.events.add(pswp.element, 'wheel',
3698    /** @type EventListener */
3699    this._onWheel.bind(this));
3700  }
3701  /**
3702   * @private
3703   * @param {WheelEvent} e
3704   */
3705
3706
3707  _onWheel(e) {
3708    e.preventDefault();
3709    const {
3710      currSlide
3711    } = this.pswp;
3712    let {
3713      deltaX,
3714      deltaY
3715    } = e;
3716
3717    if (!currSlide) {
3718      return;
3719    }
3720
3721    if (this.pswp.dispatch('wheel', {
3722      originalEvent: e
3723    }).defaultPrevented) {
3724      return;
3725    }
3726
3727    if (e.ctrlKey || this.pswp.options.wheelToZoom) {
3728      // zoom
3729      if (currSlide.isZoomable()) {
3730        let zoomFactor = -deltaY;
3731
3732        if (e.deltaMode === 1
3733        /* DOM_DELTA_LINE */
3734        ) {
3735          zoomFactor *= 0.05;
3736        } else {
3737          zoomFactor *= e.deltaMode ? 1 : 0.002;
3738        }
3739
3740        zoomFactor = 2 ** zoomFactor;
3741        const destZoomLevel = currSlide.currZoomLevel * zoomFactor;
3742        currSlide.zoomTo(destZoomLevel, {
3743          x: e.clientX,
3744          y: e.clientY
3745        });
3746      }
3747    } else {
3748      // pan
3749      if (currSlide.isPannable()) {
3750        if (e.deltaMode === 1
3751        /* DOM_DELTA_LINE */
3752        ) {
3753          // 18 - average line height
3754          deltaX *= 18;
3755          deltaY *= 18;
3756        }
3757
3758        currSlide.panTo(currSlide.pan.x - deltaX, currSlide.pan.y - deltaY);
3759      }
3760    }
3761  }
3762
3763}
3764
3765/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
3766
3767/**
3768 * @template T
3769 * @typedef {import('../types.js').Methods<T>} Methods<T>
3770 */
3771
3772/**
3773 * @typedef {Object} UIElementMarkupProps
3774 * @prop {boolean} [isCustomSVG]
3775 * @prop {string} inner
3776 * @prop {string} [outlineID]
3777 * @prop {number | string} [size]
3778 */
3779
3780/**
3781 * @typedef {Object} UIElementData
3782 * @prop {DefaultUIElements | string} [name]
3783 * @prop {string} [className]
3784 * @prop {UIElementMarkup} [html]
3785 * @prop {boolean} [isButton]
3786 * @prop {keyof HTMLElementTagNameMap} [tagName]
3787 * @prop {string} [title]
3788 * @prop {string} [ariaLabel]
3789 * @prop {(element: HTMLElement, pswp: PhotoSwipe) => void} [onInit]
3790 * @prop {Methods<PhotoSwipe> | ((e: MouseEvent, element: HTMLElement, pswp: PhotoSwipe) => void)} [onClick]
3791 * @prop {'bar' | 'wrapper' | 'root'} [appendTo]
3792 * @prop {number} [order]
3793 */
3794
3795/** @typedef {'arrowPrev' | 'arrowNext' | 'close' | 'zoom' | 'counter'} DefaultUIElements */
3796
3797/** @typedef {string | UIElementMarkupProps} UIElementMarkup */
3798
3799/**
3800 * @param {UIElementMarkup} [htmlData]
3801 * @returns {string}
3802 */
3803
3804function addElementHTML(htmlData) {
3805  if (typeof htmlData === 'string') {
3806    // Allow developers to provide full svg,
3807    // For example:
3808    // <svg viewBox="0 0 32 32" width="32" height="32" aria-hidden="true" class="pswp__icn">
3809    //   <path d="..." />
3810    //   <circle ... />
3811    // </svg>
3812    // Can also be any HTML string.
3813    return htmlData;
3814  }
3815
3816  if (!htmlData || !htmlData.isCustomSVG) {
3817    return '';
3818  }
3819
3820  const svgData = htmlData;
3821  let out = '<svg aria-hidden="true" class="pswp__icn" viewBox="0 0 %d %d" width="%d" height="%d">'; // replace all %d with size
3822
3823  out = out.split('%d').join(
3824  /** @type {string} */
3825  svgData.size || 32); // Icons may contain outline/shadow,
3826  // to make it we "clone" base icon shape and add border to it.
3827  // Icon itself and border are styled via CSS.
3828  //
3829  // Property shadowID defines ID of element that should be cloned.
3830
3831  if (svgData.outlineID) {
3832    out += '<use class="pswp__icn-shadow" xlink:href="#' + svgData.outlineID + '"/>';
3833  }
3834
3835  out += svgData.inner;
3836  out += '</svg>';
3837  return out;
3838}
3839
3840class UIElement {
3841  /**
3842   * @param {PhotoSwipe} pswp
3843   * @param {UIElementData} data
3844   */
3845  constructor(pswp, data) {
3846    var _container;
3847
3848    const name = data.name || data.className;
3849    let elementHTML = data.html; // @ts-expect-error lookup only by `data.name` maybe?
3850
3851    if (pswp.options[name] === false) {
3852      // exit if element is disabled from options
3853      return;
3854    } // Allow to override SVG icons from options
3855    // @ts-expect-error lookup only by `data.name` maybe?
3856
3857
3858    if (typeof pswp.options[name + 'SVG'] === 'string') {
3859      // arrowPrevSVG
3860      // arrowNextSVG
3861      // closeSVG
3862      // zoomSVG
3863      // @ts-expect-error lookup only by `data.name` maybe?
3864      elementHTML = pswp.options[name + 'SVG'];
3865    }
3866
3867    pswp.dispatch('uiElementCreate', {
3868      data
3869    });
3870    let className = '';
3871
3872    if (data.isButton) {
3873      className += 'pswp__button ';
3874      className += data.className || `pswp__button--${data.name}`;
3875    } else {
3876      className += data.className || `pswp__${data.name}`;
3877    }
3878
3879    let tagName = data.isButton ? data.tagName || 'button' : data.tagName || 'div';
3880    tagName =
3881    /** @type {keyof HTMLElementTagNameMap} */
3882    tagName.toLowerCase();
3883    /** @type {HTMLElement} */
3884
3885    const element = createElement(className, tagName);
3886
3887    if (data.isButton) {
3888      if (tagName === 'button') {
3889        /** @type {HTMLButtonElement} */
3890        element.type = 'button';
3891      }
3892
3893      let {
3894        title
3895      } = data;
3896      const {
3897        ariaLabel
3898      } = data; // @ts-expect-error lookup only by `data.name` maybe?
3899
3900      if (typeof pswp.options[name + 'Title'] === 'string') {
3901        // @ts-expect-error lookup only by `data.name` maybe?
3902        title = pswp.options[name + 'Title'];
3903      }
3904
3905      if (title) {
3906        element.title = title;
3907      }
3908
3909      const ariaText = ariaLabel || title;
3910
3911      if (ariaText) {
3912        element.setAttribute('aria-label', ariaText);
3913      }
3914    }
3915
3916    element.innerHTML = addElementHTML(elementHTML);
3917
3918    if (data.onInit) {
3919      data.onInit(element, pswp);
3920    }
3921
3922    if (data.onClick) {
3923      element.onclick = e => {
3924        if (typeof data.onClick === 'string') {
3925          // @ts-ignore
3926          pswp[data.onClick]();
3927        } else if (typeof data.onClick === 'function') {
3928          data.onClick(e, element, pswp);
3929        }
3930      };
3931    } // Top bar is default position
3932
3933
3934    const appendTo = data.appendTo || 'bar';
3935    /** @type {HTMLElement | undefined} root element by default */
3936
3937    let container = pswp.element;
3938
3939    if (appendTo === 'bar') {
3940      if (!pswp.topBar) {
3941        pswp.topBar = createElement('pswp__top-bar pswp__hide-on-close', 'div', pswp.scrollWrap);
3942      }
3943
3944      container = pswp.topBar;
3945    } else {
3946      // element outside of top bar gets a secondary class
3947      // that makes element fade out on close
3948      element.classList.add('pswp__hide-on-close');
3949
3950      if (appendTo === 'wrapper') {
3951        container = pswp.scrollWrap;
3952      }
3953    }
3954
3955    (_container = container) === null || _container === void 0 || _container.appendChild(pswp.applyFilters('uiElement', element, data));
3956  }
3957
3958}
3959
3960/*
3961  Backward and forward arrow buttons
3962 */
3963
3964/** @typedef {import('./ui-element.js').UIElementData} UIElementData */
3965
3966/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
3967
3968/**
3969 *
3970 * @param {HTMLElement} element
3971 * @param {PhotoSwipe} pswp
3972 * @param {boolean} [isNextButton]
3973 */
3974function initArrowButton(element, pswp, isNextButton) {
3975  element.classList.add('pswp__button--arrow'); // TODO: this should point to a unique id for this instance
3976
3977  element.setAttribute('aria-controls', 'pswp__items');
3978  pswp.on('change', () => {
3979    if (!pswp.options.loop) {
3980      if (isNextButton) {
3981        /** @type {HTMLButtonElement} */
3982        element.disabled = !(pswp.currIndex < pswp.getNumItems() - 1);
3983      } else {
3984        /** @type {HTMLButtonElement} */
3985        element.disabled = !(pswp.currIndex > 0);
3986      }
3987    }
3988  });
3989}
3990/** @type {UIElementData} */
3991
3992
3993const arrowPrev = {
3994  name: 'arrowPrev',
3995  className: 'pswp__button--arrow--prev',
3996  title: 'Previous',
3997  order: 10,
3998  isButton: true,
3999  appendTo: 'wrapper',
4000  html: {
4001    isCustomSVG: true,
4002    size: 60,
4003    inner: '<path d="M29 43l-3 3-16-16 16-16 3 3-13 13 13 13z" id="pswp__icn-arrow"/>',
4004    outlineID: 'pswp__icn-arrow'
4005  },
4006  onClick: 'prev',
4007  onInit: initArrowButton
4008};
4009/** @type {UIElementData} */
4010
4011const arrowNext = {
4012  name: 'arrowNext',
4013  className: 'pswp__button--arrow--next',
4014  title: 'Next',
4015  order: 11,
4016  isButton: true,
4017  appendTo: 'wrapper',
4018  html: {
4019    isCustomSVG: true,
4020    size: 60,
4021    inner: '<use xlink:href="#pswp__icn-arrow"/>',
4022    outlineID: 'pswp__icn-arrow'
4023  },
4024  onClick: 'next',
4025  onInit: (el, pswp) => {
4026    initArrowButton(el, pswp, true);
4027  }
4028};
4029
4030/** @type {import('./ui-element.js').UIElementData} UIElementData */
4031const closeButton = {
4032  name: 'close',
4033  title: 'Close',
4034  order: 20,
4035  isButton: true,
4036  html: {
4037    isCustomSVG: true,
4038    inner: '<path d="M24 10l-2-2-6 6-6-6-2 2 6 6-6 6 2 2 6-6 6 6 2-2-6-6z" id="pswp__icn-close"/>',
4039    outlineID: 'pswp__icn-close'
4040  },
4041  onClick: 'close'
4042};
4043
4044/** @type {import('./ui-element.js').UIElementData} UIElementData */
4045const zoomButton = {
4046  name: 'zoom',
4047  title: 'Zoom',
4048  order: 10,
4049  isButton: true,
4050  html: {
4051    isCustomSVG: true,
4052    // eslint-disable-next-line max-len
4053    inner: '<path d="M17.426 19.926a6 6 0 1 1 1.5-1.5L23 22.5 21.5 24l-4.074-4.074z" id="pswp__icn-zoom"/>' + '<path fill="currentColor" class="pswp__zoom-icn-bar-h" d="M11 16v-2h6v2z"/>' + '<path fill="currentColor" class="pswp__zoom-icn-bar-v" d="M13 12h2v6h-2z"/>',
4054    outlineID: 'pswp__icn-zoom'
4055  },
4056  onClick: 'toggleZoom'
4057};
4058
4059/** @type {import('./ui-element.js').UIElementData} UIElementData */
4060const loadingIndicator = {
4061  name: 'preloader',
4062  appendTo: 'bar',
4063  order: 7,
4064  html: {
4065    isCustomSVG: true,
4066    // eslint-disable-next-line max-len
4067    inner: '<path fill-rule="evenodd" clip-rule="evenodd" d="M21.2 16a5.2 5.2 0 1 1-5.2-5.2V8a8 8 0 1 0 8 8h-2.8Z" id="pswp__icn-loading"/>',
4068    outlineID: 'pswp__icn-loading'
4069  },
4070  onInit: (indicatorElement, pswp) => {
4071    /** @type {boolean | undefined} */
4072    let isVisible;
4073    /** @type {NodeJS.Timeout | null} */
4074
4075    let delayTimeout = null;
4076    /**
4077     * @param {string} className
4078     * @param {boolean} add
4079     */
4080
4081    const toggleIndicatorClass = (className, add) => {
4082      indicatorElement.classList.toggle('pswp__preloader--' + className, add);
4083    };
4084    /**
4085     * @param {boolean} visible
4086     */
4087
4088
4089    const setIndicatorVisibility = visible => {
4090      if (isVisible !== visible) {
4091        isVisible = visible;
4092        toggleIndicatorClass('active', visible);
4093      }
4094    };
4095
4096    const updatePreloaderVisibility = () => {
4097      var _pswp$currSlide;
4098
4099      if (!((_pswp$currSlide = pswp.currSlide) !== null && _pswp$currSlide !== void 0 && _pswp$currSlide.content.isLoading())) {
4100        setIndicatorVisibility(false);
4101
4102        if (delayTimeout) {
4103          clearTimeout(delayTimeout);
4104          delayTimeout = null;
4105        }
4106
4107        return;
4108      }
4109
4110      if (!delayTimeout) {
4111        // display loading indicator with delay
4112        delayTimeout = setTimeout(() => {
4113          var _pswp$currSlide2;
4114
4115          setIndicatorVisibility(Boolean((_pswp$currSlide2 = pswp.currSlide) === null || _pswp$currSlide2 === void 0 ? void 0 : _pswp$currSlide2.content.isLoading()));
4116          delayTimeout = null;
4117        }, pswp.options.preloaderDelay);
4118      }
4119    };
4120
4121    pswp.on('change', updatePreloaderVisibility);
4122    pswp.on('loadComplete', e => {
4123      if (pswp.currSlide === e.slide) {
4124        updatePreloaderVisibility();
4125      }
4126    }); // expose the method
4127
4128    if (pswp.ui) {
4129      pswp.ui.updatePreloaderVisibility = updatePreloaderVisibility;
4130    }
4131  }
4132};
4133
4134/** @type {import('./ui-element.js').UIElementData} UIElementData */
4135const counterIndicator = {
4136  name: 'counter',
4137  order: 5,
4138  onInit: (counterElement, pswp) => {
4139    pswp.on('change', () => {
4140      counterElement.innerText = pswp.currIndex + 1 + pswp.options.indexIndicatorSep + pswp.getNumItems();
4141    });
4142  }
4143};
4144
4145/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
4146
4147/** @typedef {import('./ui-element.js').UIElementData} UIElementData */
4148
4149/**
4150 * Set special class on element when image is zoomed.
4151 *
4152 * By default, it is used to adjust
4153 * zoom icon and zoom cursor via CSS.
4154 *
4155 * @param {HTMLElement} el
4156 * @param {boolean} isZoomedIn
4157 */
4158
4159function setZoomedIn(el, isZoomedIn) {
4160  el.classList.toggle('pswp--zoomed-in', isZoomedIn);
4161}
4162
4163class UI {
4164  /**
4165   * @param {PhotoSwipe} pswp
4166   */
4167  constructor(pswp) {
4168    this.pswp = pswp;
4169    this.isRegistered = false;
4170    /** @type {UIElementData[]} */
4171
4172    this.uiElementsData = [];
4173    /** @type {(UIElement | UIElementData)[]} */
4174
4175    this.items = [];
4176    /** @type {() => void} */
4177
4178    this.updatePreloaderVisibility = () => {};
4179    /**
4180     * @private
4181     * @type {number | undefined}
4182     */
4183
4184
4185    this._lastUpdatedZoomLevel = undefined;
4186  }
4187
4188  init() {
4189    const {
4190      pswp
4191    } = this;
4192    this.isRegistered = false;
4193    this.uiElementsData = [closeButton, arrowPrev, arrowNext, zoomButton, loadingIndicator, counterIndicator];
4194    pswp.dispatch('uiRegister'); // sort by order
4195
4196    this.uiElementsData.sort((a, b) => {
4197      // default order is 0
4198      return (a.order || 0) - (b.order || 0);
4199    });
4200    this.items = [];
4201    this.isRegistered = true;
4202    this.uiElementsData.forEach(uiElementData => {
4203      this.registerElement(uiElementData);
4204    });
4205    pswp.on('change', () => {
4206      var _pswp$element;
4207
4208      (_pswp$element = pswp.element) === null || _pswp$element === void 0 || _pswp$element.classList.toggle('pswp--one-slide', pswp.getNumItems() === 1);
4209    });
4210    pswp.on('zoomPanUpdate', () => this._onZoomPanUpdate());
4211  }
4212  /**
4213   * @param {UIElementData} elementData
4214   */
4215
4216
4217  registerElement(elementData) {
4218    if (this.isRegistered) {
4219      this.items.push(new UIElement(this.pswp, elementData));
4220    } else {
4221      this.uiElementsData.push(elementData);
4222    }
4223  }
4224  /**
4225   * Fired each time zoom or pan position is changed.
4226   * Update classes that control visibility of zoom button and cursor icon.
4227   *
4228   * @private
4229   */
4230
4231
4232  _onZoomPanUpdate() {
4233    const {
4234      template,
4235      currSlide,
4236      options
4237    } = this.pswp;
4238
4239    if (this.pswp.opener.isClosing || !template || !currSlide) {
4240      return;
4241    }
4242
4243    let {
4244      currZoomLevel
4245    } = currSlide; // if not open yet - check against initial zoom level
4246
4247    if (!this.pswp.opener.isOpen) {
4248      currZoomLevel = currSlide.zoomLevels.initial;
4249    }
4250
4251    if (currZoomLevel === this._lastUpdatedZoomLevel) {
4252      return;
4253    }
4254
4255    this._lastUpdatedZoomLevel = currZoomLevel;
4256    const currZoomLevelDiff = currSlide.zoomLevels.initial - currSlide.zoomLevels.secondary; // Initial and secondary zoom levels are almost equal
4257
4258    if (Math.abs(currZoomLevelDiff) < 0.01 || !currSlide.isZoomable()) {
4259      // disable zoom
4260      setZoomedIn(template, false);
4261      template.classList.remove('pswp--zoom-allowed');
4262      return;
4263    }
4264
4265    template.classList.add('pswp--zoom-allowed');
4266    const potentialZoomLevel = currZoomLevel === currSlide.zoomLevels.initial ? currSlide.zoomLevels.secondary : currSlide.zoomLevels.initial;
4267    setZoomedIn(template, potentialZoomLevel <= currZoomLevel);
4268
4269    if (options.imageClickAction === 'zoom' || options.imageClickAction === 'zoom-or-close') {
4270      template.classList.add('pswp--click-to-zoom');
4271    }
4272  }
4273
4274}
4275
4276/** @typedef {import('./slide.js').SlideData} SlideData */
4277
4278/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
4279
4280/** @typedef {{ x: number; y: number; w: number; innerRect?: { w: number; h: number; x: number; y: number } }} Bounds */
4281
4282/**
4283 * @param {HTMLElement} el
4284 * @returns Bounds
4285 */
4286function getBoundsByElement(el) {
4287  const thumbAreaRect = el.getBoundingClientRect();
4288  return {
4289    x: thumbAreaRect.left,
4290    y: thumbAreaRect.top,
4291    w: thumbAreaRect.width
4292  };
4293}
4294/**
4295 * @param {HTMLElement} el
4296 * @param {number} imageWidth
4297 * @param {number} imageHeight
4298 * @returns Bounds
4299 */
4300
4301
4302function getCroppedBoundsByElement(el, imageWidth, imageHeight) {
4303  const thumbAreaRect = el.getBoundingClientRect(); // fill image into the area
4304  // (do they same as object-fit:cover does to retrieve coordinates)
4305
4306  const hRatio = thumbAreaRect.width / imageWidth;
4307  const vRatio = thumbAreaRect.height / imageHeight;
4308  const fillZoomLevel = hRatio > vRatio ? hRatio : vRatio;
4309  const offsetX = (thumbAreaRect.width - imageWidth * fillZoomLevel) / 2;
4310  const offsetY = (thumbAreaRect.height - imageHeight * fillZoomLevel) / 2;
4311  /**
4312   * Coordinates of the image,
4313   * as if it was not cropped,
4314   * height is calculated automatically
4315   *
4316   * @type {Bounds}
4317   */
4318
4319  const bounds = {
4320    x: thumbAreaRect.left + offsetX,
4321    y: thumbAreaRect.top + offsetY,
4322    w: imageWidth * fillZoomLevel
4323  }; // Coordinates of inner crop area
4324  // relative to the image
4325
4326  bounds.innerRect = {
4327    w: thumbAreaRect.width,
4328    h: thumbAreaRect.height,
4329    x: offsetX,
4330    y: offsetY
4331  };
4332  return bounds;
4333}
4334/**
4335 * Get dimensions of thumbnail image
4336 * (click on which opens photoswipe or closes photoswipe to)
4337 *
4338 * @param {number} index
4339 * @param {SlideData} itemData
4340 * @param {PhotoSwipe} instance PhotoSwipe instance
4341 * @returns {Bounds | undefined}
4342 */
4343
4344
4345function getThumbBounds(index, itemData, instance) {
4346  // legacy event, before filters were introduced
4347  const event = instance.dispatch('thumbBounds', {
4348    index,
4349    itemData,
4350    instance
4351  }); // @ts-expect-error
4352
4353  if (event.thumbBounds) {
4354    // @ts-expect-error
4355    return event.thumbBounds;
4356  }
4357
4358  const {
4359    element
4360  } = itemData;
4361  /** @type {Bounds | undefined} */
4362
4363  let thumbBounds;
4364  /** @type {HTMLElement | null | undefined} */
4365
4366  let thumbnail;
4367
4368  if (element && instance.options.thumbSelector !== false) {
4369    const thumbSelector = instance.options.thumbSelector || 'img';
4370    thumbnail = element.matches(thumbSelector) ? element :
4371    /** @type {HTMLElement | null} */
4372    element.querySelector(thumbSelector);
4373  }
4374
4375  thumbnail = instance.applyFilters('thumbEl', thumbnail, itemData, index);
4376
4377  if (thumbnail) {
4378    if (!itemData.thumbCropped) {
4379      thumbBounds = getBoundsByElement(thumbnail);
4380    } else {
4381      thumbBounds = getCroppedBoundsByElement(thumbnail, itemData.width || itemData.w || 0, itemData.height || itemData.h || 0);
4382    }
4383  }
4384
4385  return instance.applyFilters('thumbBounds', thumbBounds, itemData, index);
4386}
4387
4388/** @typedef {import('../lightbox/lightbox.js').default} PhotoSwipeLightbox */
4389
4390/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
4391
4392/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
4393
4394/** @typedef {import('../photoswipe.js').DataSource} DataSource */
4395
4396/** @typedef {import('../ui/ui-element.js').UIElementData} UIElementData */
4397
4398/** @typedef {import('../slide/content.js').default} ContentDefault */
4399
4400/** @typedef {import('../slide/slide.js').default} Slide */
4401
4402/** @typedef {import('../slide/slide.js').SlideData} SlideData */
4403
4404/** @typedef {import('../slide/zoom-level.js').default} ZoomLevel */
4405
4406/** @typedef {import('../slide/get-thumb-bounds.js').Bounds} Bounds */
4407
4408/**
4409 * Allow adding an arbitrary props to the Content
4410 * https://photoswipe.com/custom-content/#using-webp-image-format
4411 * @typedef {ContentDefault & Record<string, any>} Content
4412 */
4413
4414/** @typedef {{ x?: number; y?: number }} Point */
4415
4416/**
4417 * @typedef {Object} PhotoSwipeEventsMap https://photoswipe.com/events/
4418 *
4419 *
4420 * https://photoswipe.com/adding-ui-elements/
4421 *
4422 * @prop {undefined} uiRegister
4423 * @prop {{ data: UIElementData }} uiElementCreate
4424 *
4425 *
4426 * https://photoswipe.com/events/#initialization-events
4427 *
4428 * @prop {undefined} beforeOpen
4429 * @prop {undefined} firstUpdate
4430 * @prop {undefined} initialLayout
4431 * @prop {undefined} change
4432 * @prop {undefined} afterInit
4433 * @prop {undefined} bindEvents
4434 *
4435 *
4436 * https://photoswipe.com/events/#opening-or-closing-transition-events
4437 *
4438 * @prop {undefined} openingAnimationStart
4439 * @prop {undefined} openingAnimationEnd
4440 * @prop {undefined} closingAnimationStart
4441 * @prop {undefined} closingAnimationEnd
4442 *
4443 *
4444 * https://photoswipe.com/events/#closing-events
4445 *
4446 * @prop {undefined} close
4447 * @prop {undefined} destroy
4448 *
4449 *
4450 * https://photoswipe.com/events/#pointer-and-gesture-events
4451 *
4452 * @prop {{ originalEvent: PointerEvent }} pointerDown
4453 * @prop {{ originalEvent: PointerEvent }} pointerMove
4454 * @prop {{ originalEvent: PointerEvent }} pointerUp
4455 * @prop {{ bgOpacity: number }} pinchClose can be default prevented
4456 * @prop {{ panY: number }} verticalDrag can be default prevented
4457 *
4458 *
4459 * https://photoswipe.com/events/#slide-content-events
4460 *
4461 * @prop {{ content: Content }} contentInit
4462 * @prop {{ content: Content; isLazy: boolean }} contentLoad can be default prevented
4463 * @prop {{ content: Content; isLazy: boolean }} contentLoadImage can be default prevented
4464 * @prop {{ content: Content; slide: Slide; isError?: boolean }} loadComplete
4465 * @prop {{ content: Content; slide: Slide }} loadError
4466 * @prop {{ content: Content; width: number; height: number }} contentResize can be default prevented
4467 * @prop {{ content: Content; width: number; height: number; slide: Slide }} imageSizeChange
4468 * @prop {{ content: Content }} contentLazyLoad can be default prevented
4469 * @prop {{ content: Content }} contentAppend can be default prevented
4470 * @prop {{ content: Content }} contentActivate can be default prevented
4471 * @prop {{ content: Content }} contentDeactivate can be default prevented
4472 * @prop {{ content: Content }} contentRemove can be default prevented
4473 * @prop {{ content: Content }} contentDestroy can be default prevented
4474 *
4475 *
4476 * undocumented
4477 *
4478 * @prop {{ point: Point; originalEvent: PointerEvent }} imageClickAction can be default prevented
4479 * @prop {{ point: Point; originalEvent: PointerEvent }} bgClickAction can be default prevented
4480 * @prop {{ point: Point; originalEvent: PointerEvent }} tapAction can be default prevented
4481 * @prop {{ point: Point; originalEvent: PointerEvent }} doubleTapAction can be default prevented
4482 *
4483 * @prop {{ originalEvent: KeyboardEvent }} keydown can be default prevented
4484 * @prop {{ x: number; dragging: boolean }} moveMainScroll
4485 * @prop {{ slide: Slide }} firstZoomPan
4486 * @prop {{ slide: Slide | undefined, data: SlideData, index: number }} gettingData
4487 * @prop {undefined} beforeResize
4488 * @prop {undefined} resize
4489 * @prop {undefined} viewportSize
4490 * @prop {undefined} updateScrollOffset
4491 * @prop {{ slide: Slide }} slideInit
4492 * @prop {{ slide: Slide }} afterSetContent
4493 * @prop {{ slide: Slide }} slideLoad
4494 * @prop {{ slide: Slide }} appendHeavy can be default prevented
4495 * @prop {{ slide: Slide }} appendHeavyContent
4496 * @prop {{ slide: Slide }} slideActivate
4497 * @prop {{ slide: Slide }} slideDeactivate
4498 * @prop {{ slide: Slide }} slideDestroy
4499 * @prop {{ destZoomLevel: number, centerPoint: Point | undefined, transitionDuration: number | false | undefined }} beforeZoomTo
4500 * @prop {{ slide: Slide }} zoomPanUpdate
4501 * @prop {{ slide: Slide }} initialZoomPan
4502 * @prop {{ slide: Slide }} calcSlideSize
4503 * @prop {undefined} resolutionChanged
4504 * @prop {{ originalEvent: WheelEvent }} wheel can be default prevented
4505 * @prop {{ content: Content }} contentAppendImage can be default prevented
4506 * @prop {{ index: number; itemData: SlideData }} lazyLoadSlide can be default prevented
4507 * @prop {undefined} lazyLoad
4508 * @prop {{ slide: Slide }} calcBounds
4509 * @prop {{ zoomLevels: ZoomLevel, slideData: SlideData }} zoomLevelsUpdate
4510 *
4511 *
4512 * legacy
4513 *
4514 * @prop {undefined} init
4515 * @prop {undefined} initialZoomIn
4516 * @prop {undefined} initialZoomOut
4517 * @prop {undefined} initialZoomInEnd
4518 * @prop {undefined} initialZoomOutEnd
4519 * @prop {{ dataSource: DataSource | undefined, numItems: number }} numItems
4520 * @prop {{ itemData: SlideData; index: number }} itemData
4521 * @prop {{ index: number, itemData: SlideData, instance: PhotoSwipe }} thumbBounds
4522 */
4523
4524/**
4525 * @typedef {Object} PhotoSwipeFiltersMap https://photoswipe.com/filters/
4526 *
4527 * @prop {(numItems: number, dataSource: DataSource | undefined) => number} numItems
4528 * Modify the total amount of slides. Example on Data sources page.
4529 * https://photoswipe.com/filters/#numitems
4530 *
4531 * @prop {(itemData: SlideData, index: number) => SlideData} itemData
4532 * Modify slide item data. Example on Data sources page.
4533 * https://photoswipe.com/filters/#itemdata
4534 *
4535 * @prop {(itemData: SlideData, element: HTMLElement, linkEl: HTMLAnchorElement) => SlideData} domItemData
4536 * Modify item data when it's parsed from DOM element. Example on Data sources page.
4537 * https://photoswipe.com/filters/#domitemdata
4538 *
4539 * @prop {(clickedIndex: number, e: MouseEvent, instance: PhotoSwipeLightbox) => number} clickedIndex
4540 * Modify clicked gallery item index.
4541 * https://photoswipe.com/filters/#clickedindex
4542 *
4543 * @prop {(placeholderSrc: string | false, content: Content) => string | false} placeholderSrc
4544 * Modify placeholder image source.
4545 * https://photoswipe.com/filters/#placeholdersrc
4546 *
4547 * @prop {(isContentLoading: boolean, content: Content) => boolean} isContentLoading
4548 * Modify if the content is currently loading.
4549 * https://photoswipe.com/filters/#iscontentloading
4550 *
4551 * @prop {(isContentZoomable: boolean, content: Content) => boolean} isContentZoomable
4552 * Modify if the content can be zoomed.
4553 * https://photoswipe.com/filters/#iscontentzoomable
4554 *
4555 * @prop {(useContentPlaceholder: boolean, content: Content) => boolean} useContentPlaceholder
4556 * Modify if the placeholder should be used for the content.
4557 * https://photoswipe.com/filters/#usecontentplaceholder
4558 *
4559 * @prop {(isKeepingPlaceholder: boolean, content: Content) => boolean} isKeepingPlaceholder
4560 * Modify if the placeholder should be kept after the content is loaded.
4561 * https://photoswipe.com/filters/#iskeepingplaceholder
4562 *
4563 *
4564 * @prop {(contentErrorElement: HTMLElement, content: Content) => HTMLElement} contentErrorElement
4565 * Modify an element when the content has error state (for example, if image cannot be loaded).
4566 * https://photoswipe.com/filters/#contenterrorelement
4567 *
4568 * @prop {(element: HTMLElement, data: UIElementData) => HTMLElement} uiElement
4569 * Modify a UI element that's being created.
4570 * https://photoswipe.com/filters/#uielement
4571 *
4572 * @prop {(thumbnail: HTMLElement | null | undefined, itemData: SlideData, index: number) => HTMLElement} thumbEl
4573 * Modify the thumbnail element from which opening zoom animation starts or ends.
4574 * https://photoswipe.com/filters/#thumbel
4575 *
4576 * @prop {(thumbBounds: Bounds | undefined, itemData: SlideData, index: number) => Bounds} thumbBounds
4577 * Modify the thumbnail bounds from which opening zoom animation starts or ends.
4578 * https://photoswipe.com/filters/#thumbbounds
4579 *
4580 * @prop {(srcsetSizesWidth: number, content: Content) => number} srcsetSizesWidth
4581 *
4582 * @prop {(preventPointerEvent: boolean, event: PointerEvent, pointerType: string) => boolean} preventPointerEvent
4583 *
4584 */
4585
4586/**
4587 * @template {keyof PhotoSwipeFiltersMap} T
4588 * @typedef {{ fn: PhotoSwipeFiltersMap[T], priority: number }} Filter
4589 */
4590
4591/**
4592 * @template {keyof PhotoSwipeEventsMap} T
4593 * @typedef {PhotoSwipeEventsMap[T] extends undefined ? PhotoSwipeEvent<T> : PhotoSwipeEvent<T> & PhotoSwipeEventsMap[T]} AugmentedEvent
4594 */
4595
4596/**
4597 * @template {keyof PhotoSwipeEventsMap} T
4598 * @typedef {(event: AugmentedEvent<T>) => void} EventCallback
4599 */
4600
4601/**
4602 * Base PhotoSwipe event object
4603 *
4604 * @template {keyof PhotoSwipeEventsMap} T
4605 */
4606class PhotoSwipeEvent {
4607  /**
4608   * @param {T} type
4609   * @param {PhotoSwipeEventsMap[T]} [details]
4610   */
4611  constructor(type, details) {
4612    this.type = type;
4613    this.defaultPrevented = false;
vendor: 19,004 bytes, lines 4614-5370
4614
4615    if (details) {
4616      Object.assign(this, details);
4617    }
4618  }
4619
4620  preventDefault() {
4621    this.defaultPrevented = true;
4622  }
4623
4624}
4625/**
4626 * PhotoSwipe base class that can listen and dispatch for events.
4627 * Shared by PhotoSwipe Core and PhotoSwipe Lightbox, extended by base.js
4628 */
4629
4630
4631class Eventable {
4632  constructor() {
4633    /**
4634     * @type {{ [T in keyof PhotoSwipeEventsMap]?: ((event: AugmentedEvent<T>) => void)[] }}
4635     */
4636    this._listeners = {};
4637    /**
4638     * @type {{ [T in keyof PhotoSwipeFiltersMap]?: Filter<T>[] }}
4639     */
4640
4641    this._filters = {};
4642    /** @type {PhotoSwipe | undefined} */
4643
4644    this.pswp = undefined;
4645    /** @type {PhotoSwipeOptions | undefined} */
4646
4647    this.options = undefined;
4648  }
4649  /**
4650   * @template {keyof PhotoSwipeFiltersMap} T
4651   * @param {T} name
4652   * @param {PhotoSwipeFiltersMap[T]} fn
4653   * @param {number} priority
4654   */
4655
4656
4657  addFilter(name, fn, priority = 100) {
4658    var _this$_filters$name, _this$_filters$name2, _this$pswp;
4659
4660    if (!this._filters[name]) {
4661      this._filters[name] = [];
4662    }
4663
4664    (_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.push({
4665      fn,
4666      priority
4667    });
4668    (_this$_filters$name2 = this._filters[name]) === null || _this$_filters$name2 === void 0 || _this$_filters$name2.sort((f1, f2) => f1.priority - f2.priority);
4669    (_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.addFilter(name, fn, priority);
4670  }
4671  /**
4672   * @template {keyof PhotoSwipeFiltersMap} T
4673   * @param {T} name
4674   * @param {PhotoSwipeFiltersMap[T]} fn
4675   */
4676
4677
4678  removeFilter(name, fn) {
4679    if (this._filters[name]) {
4680      // @ts-expect-error
4681      this._filters[name] = this._filters[name].filter(filter => filter.fn !== fn);
4682    }
4683
4684    if (this.pswp) {
4685      this.pswp.removeFilter(name, fn);
4686    }
4687  }
4688  /**
4689   * @template {keyof PhotoSwipeFiltersMap} T
4690   * @param {T} name
4691   * @param {Parameters<PhotoSwipeFiltersMap[T]>} args
4692   * @returns {Parameters<PhotoSwipeFiltersMap[T]>[0]}
4693   */
4694
4695
4696  applyFilters(name, ...args) {
4697    var _this$_filters$name3;
4698
4699    (_this$_filters$name3 = this._filters[name]) === null || _this$_filters$name3 === void 0 || _this$_filters$name3.forEach(filter => {
4700      // @ts-expect-error
4701      args[0] = filter.fn.apply(this, args);
4702    });
4703    return args[0];
4704  }
4705  /**
4706   * @template {keyof PhotoSwipeEventsMap} T
4707   * @param {T} name
4708   * @param {EventCallback<T>} fn
4709   */
4710
4711
4712  on(name, fn) {
4713    var _this$_listeners$name, _this$pswp2;
4714
4715    if (!this._listeners[name]) {
4716      this._listeners[name] = [];
4717    }
4718
4719    (_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.push(fn); // When binding events to lightbox,
4720    // also bind events to PhotoSwipe Core,
4721    // if it's open.
4722
4723    (_this$pswp2 = this.pswp) === null || _this$pswp2 === void 0 || _this$pswp2.on(name, fn);
4724  }
4725  /**
4726   * @template {keyof PhotoSwipeEventsMap} T
4727   * @param {T} name
4728   * @param {EventCallback<T>} fn
4729   */
4730
4731
4732  off(name, fn) {
4733    var _this$pswp3;
4734
4735    if (this._listeners[name]) {
4736      // @ts-expect-error
4737      this._listeners[name] = this._listeners[name].filter(listener => fn !== listener);
4738    }
4739
4740    (_this$pswp3 = this.pswp) === null || _this$pswp3 === void 0 || _this$pswp3.off(name, fn);
4741  }
4742  /**
4743   * @template {keyof PhotoSwipeEventsMap} T
4744   * @param {T} name
4745   * @param {PhotoSwipeEventsMap[T]} [details]
4746   * @returns {AugmentedEvent<T>}
4747   */
4748
4749
4750  dispatch(name, details) {
4751    var _this$_listeners$name2;
4752
4753    if (this.pswp) {
4754      return this.pswp.dispatch(name, details);
4755    }
4756
4757    const event =
4758    /** @type {AugmentedEvent<T>} */
4759    new PhotoSwipeEvent(name, details);
4760    (_this$_listeners$name2 = this._listeners[name]) === null || _this$_listeners$name2 === void 0 || _this$_listeners$name2.forEach(listener => {
4761      listener.call(this, event);
4762    });
4763    return event;
4764  }
4765
4766}
4767
4768class Placeholder {
4769  /**
4770   * @param {string | false} imageSrc
4771   * @param {HTMLElement} container
4772   */
4773  constructor(imageSrc, container) {
4774    // Create placeholder
4775    // (stretched thumbnail or simple div behind the main image)
4776
4777    /** @type {HTMLImageElement | HTMLDivElement | null} */
4778    this.element = createElement('pswp__img pswp__img--placeholder', imageSrc ? 'img' : 'div', container);
4779
4780    if (imageSrc) {
4781      const imgEl =
4782      /** @type {HTMLImageElement} */
4783      this.element;
4784      imgEl.decoding = 'async';
4785      imgEl.alt = '';
4786      imgEl.src = imageSrc;
4787      imgEl.setAttribute('role', 'presentation');
4788    }
4789
4790    this.element.setAttribute('aria-hidden', 'true');
4791  }
4792  /**
4793   * @param {number} width
4794   * @param {number} height
4795   */
4796
4797
4798  setDisplayedSize(width, height) {
4799    if (!this.element) {
4800      return;
4801    }
4802
4803    if (this.element.tagName === 'IMG') {
4804      // Use transform scale() to modify img placeholder size
4805      // (instead of changing width/height directly).
4806      // This helps with performance, specifically in iOS15 Safari.
4807      setWidthHeight(this.element, 250, 'auto');
4808      this.element.style.transformOrigin = '0 0';
4809      this.element.style.transform = toTransformString(0, 0, width / 250);
4810    } else {
4811      setWidthHeight(this.element, width, height);
4812    }
4813  }
4814
4815  destroy() {
4816    var _this$element;
4817
4818    if ((_this$element = this.element) !== null && _this$element !== void 0 && _this$element.parentNode) {
4819      this.element.remove();
4820    }
4821
4822    this.element = null;
4823  }
4824
4825}
4826
4827/** @typedef {import('./slide.js').default} Slide */
4828
4829/** @typedef {import('./slide.js').SlideData} SlideData */
4830
4831/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
4832
4833/** @typedef {import('../util/util.js').LoadState} LoadState */
4834
4835class Content {
4836  /**
4837   * @param {SlideData} itemData Slide data
4838   * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
4839   * @param {number} index
4840   */
4841  constructor(itemData, instance, index) {
4842    this.instance = instance;
4843    this.data = itemData;
4844    this.index = index;
4845    /** @type {HTMLImageElement | HTMLDivElement | undefined} */
4846
4847    this.element = undefined;
4848    /** @type {Placeholder | undefined} */
4849
4850    this.placeholder = undefined;
4851    /** @type {Slide | undefined} */
4852
4853    this.slide = undefined;
4854    this.displayedImageWidth = 0;
4855    this.displayedImageHeight = 0;
4856    this.width = Number(this.data.w) || Number(this.data.width) || 0;
4857    this.height = Number(this.data.h) || Number(this.data.height) || 0;
4858    this.isAttached = false;
4859    this.hasSlide = false;
4860    this.isDecoding = false;
4861    /** @type {LoadState} */
4862
4863    this.state = LOAD_STATE.IDLE;
4864
4865    if (this.data.type) {
4866      this.type = this.data.type;
4867    } else if (this.data.src) {
4868      this.type = 'image';
4869    } else {
4870      this.type = 'html';
4871    }
4872
4873    this.instance.dispatch('contentInit', {
4874      content: this
4875    });
4876  }
4877
4878  removePlaceholder() {
4879    if (this.placeholder && !this.keepPlaceholder()) {
4880      // With delay, as image might be loaded, but not rendered
4881      setTimeout(() => {
4882        if (this.placeholder) {
4883          this.placeholder.destroy();
4884          this.placeholder = undefined;
4885        }
4886      }, 1000);
4887    }
4888  }
4889  /**
4890   * Preload content
4891   *
4892   * @param {boolean} isLazy
4893   * @param {boolean} [reload]
4894   */
4895
4896
4897  load(isLazy, reload) {
4898    if (this.slide && this.usePlaceholder()) {
4899      if (!this.placeholder) {
4900        const placeholderSrc = this.instance.applyFilters('placeholderSrc', // use  image-based placeholder only for the first slide,
4901        // as rendering (even small stretched thumbnail) is an expensive operation
4902        this.data.msrc && this.slide.isFirstSlide ? this.data.msrc : false, this);
4903        this.placeholder = new Placeholder(placeholderSrc, this.slide.container);
4904      } else {
4905        const placeholderEl = this.placeholder.element; // Add placeholder to DOM if it was already created
4906
4907        if (placeholderEl && !placeholderEl.parentElement) {
4908          this.slide.container.prepend(placeholderEl);
4909        }
4910      }
4911    }
4912
4913    if (this.element && !reload) {
4914      return;
4915    }
4916
4917    if (this.instance.dispatch('contentLoad', {
4918      content: this,
4919      isLazy
4920    }).defaultPrevented) {
4921      return;
4922    }
4923
4924    if (this.isImageContent()) {
4925      this.element = createElement('pswp__img', 'img'); // Start loading only after width is defined, as sizes might depend on it.
4926      // Due to Safari feature, we must define sizes before srcset.
4927
4928      if (this.displayedImageWidth) {
4929        this.loadImage(isLazy);
4930      }
4931    } else {
4932      this.element = createElement('pswp__content', 'div');
4933      this.element.innerHTML = this.data.html || '';
4934    }
4935
4936    if (reload && this.slide) {
4937      this.slide.updateContentSize(true);
4938    }
4939  }
4940  /**
4941   * Preload image
4942   *
4943   * @param {boolean} isLazy
4944   */
4945
4946
4947  loadImage(isLazy) {
4948    var _this$data$src, _this$data$alt;
4949
4950    if (!this.isImageContent() || !this.element || this.instance.dispatch('contentLoadImage', {
4951      content: this,
4952      isLazy
4953    }).defaultPrevented) {
4954      return;
4955    }
4956
4957    const imageElement =
4958    /** @type HTMLImageElement */
4959    this.element;
4960    this.updateSrcsetSizes();
4961
4962    if (this.data.srcset) {
4963      imageElement.srcset = this.data.srcset;
4964    }
4965
4966    imageElement.src = (_this$data$src = this.data.src) !== null && _this$data$src !== void 0 ? _this$data$src : '';
4967    imageElement.alt = (_this$data$alt = this.data.alt) !== null && _this$data$alt !== void 0 ? _this$data$alt : '';
4968    this.state = LOAD_STATE.LOADING;
4969
4970    if (imageElement.complete) {
4971      this.onLoaded();
4972    } else {
4973      imageElement.onload = () => {
4974        this.onLoaded();
4975      };
4976
4977      imageElement.onerror = () => {
4978        this.onError();
4979      };
4980    }
4981  }
4982  /**
4983   * Assign slide to content
4984   *
4985   * @param {Slide} slide
4986   */
4987
4988
4989  setSlide(slide) {
4990    this.slide = slide;
4991    this.hasSlide = true;
4992    this.instance = slide.pswp; // todo: do we need to unset slide?
4993  }
4994  /**
4995   * Content load success handler
4996   */
4997
4998
4999  onLoaded() {
5000    this.state = LOAD_STATE.LOADED;
5001
5002    if (this.slide && this.element) {
5003      this.instance.dispatch('loadComplete', {
5004        slide: this.slide,
5005        content: this
5006      }); // if content is reloaded
5007
5008      if (this.slide.isActive && this.slide.heavyAppended && !this.element.parentNode) {
5009        this.append();
5010        this.slide.updateContentSize(true);
5011      }
5012
5013      if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
5014        this.removePlaceholder();
5015      }
5016    }
5017  }
5018  /**
5019   * Content load error handler
5020   */
5021
5022
5023  onError() {
5024    this.state = LOAD_STATE.ERROR;
5025
5026    if (this.slide) {
5027      this.displayError();
5028      this.instance.dispatch('loadComplete', {
5029        slide: this.slide,
5030        isError: true,
5031        content: this
5032      });
5033      this.instance.dispatch('loadError', {
5034        slide: this.slide,
5035        content: this
5036      });
5037    }
5038  }
5039  /**
5040   * @returns {Boolean} If the content is currently loading
5041   */
5042
5043
5044  isLoading() {
5045    return this.instance.applyFilters('isContentLoading', this.state === LOAD_STATE.LOADING, this);
5046  }
5047  /**
5048   * @returns {Boolean} If the content is in error state
5049   */
5050
5051
5052  isError() {
5053    return this.state === LOAD_STATE.ERROR;
5054  }
5055  /**
5056   * @returns {boolean} If the content is image
5057   */
5058
5059
5060  isImageContent() {
5061    return this.type === 'image';
5062  }
5063  /**
5064   * Update content size
5065   *
5066   * @param {Number} width
5067   * @param {Number} height
5068   */
5069
5070
5071  setDisplayedSize(width, height) {
5072    if (!this.element) {
5073      return;
5074    }
5075
5076    if (this.placeholder) {
5077      this.placeholder.setDisplayedSize(width, height);
5078    }
5079
5080    if (this.instance.dispatch('contentResize', {
5081      content: this,
5082      width,
5083      height
5084    }).defaultPrevented) {
5085      return;
5086    }
5087
5088    setWidthHeight(this.element, width, height);
5089
5090    if (this.isImageContent() && !this.isError()) {
5091      const isInitialSizeUpdate = !this.displayedImageWidth && width;
5092      this.displayedImageWidth = width;
5093      this.displayedImageHeight = height;
5094
5095      if (isInitialSizeUpdate) {
5096        this.loadImage(false);
5097      } else {
5098        this.updateSrcsetSizes();
5099      }
5100
5101      if (this.slide) {
5102        this.instance.dispatch('imageSizeChange', {
5103          slide: this.slide,
5104          width,
5105          height,
5106          content: this
5107        });
5108      }
5109    }
5110  }
5111  /**
5112   * @returns {boolean} If the content can be zoomed
5113   */
5114
5115
5116  isZoomable() {
5117    return this.instance.applyFilters('isContentZoomable', this.isImageContent() && this.state !== LOAD_STATE.ERROR, this);
5118  }
5119  /**
5120   * Update image srcset sizes attribute based on width and height
5121   */
5122
5123
5124  updateSrcsetSizes() {
5125    // Handle srcset sizes attribute.
5126    //
5127    // Never lower quality, if it was increased previously.
5128    // Chrome does this automatically, Firefox and Safari do not,
5129    // so we store largest used size in dataset.
5130    if (!this.isImageContent() || !this.element || !this.data.srcset) {
5131      return;
5132    }
5133
5134    const image =
5135    /** @type HTMLImageElement */
5136    this.element;
5137    const sizesWidth = this.instance.applyFilters('srcsetSizesWidth', this.displayedImageWidth, this);
5138
5139    if (!image.dataset.largestUsedSize || sizesWidth > parseInt(image.dataset.largestUsedSize, 10)) {
5140      image.sizes = sizesWidth + 'px';
5141      image.dataset.largestUsedSize = String(sizesWidth);
5142    }
5143  }
5144  /**
5145   * @returns {boolean} If content should use a placeholder (from msrc by default)
5146   */
5147
5148
5149  usePlaceholder() {
5150    return this.instance.applyFilters('useContentPlaceholder', this.isImageContent(), this);
5151  }
5152  /**
5153   * Preload content with lazy-loading param
5154   */
5155
5156
5157  lazyLoad() {
5158    if (this.instance.dispatch('contentLazyLoad', {
5159      content: this
5160    }).defaultPrevented) {
5161      return;
5162    }
5163
5164    this.load(true);
5165  }
5166  /**
5167   * @returns {boolean} If placeholder should be kept after content is loaded
5168   */
5169
5170
5171  keepPlaceholder() {
5172    return this.instance.applyFilters('isKeepingPlaceholder', this.isLoading(), this);
5173  }
5174  /**
5175   * Destroy the content
5176   */
5177
5178
5179  destroy() {
5180    this.hasSlide = false;
5181    this.slide = undefined;
5182
5183    if (this.instance.dispatch('contentDestroy', {
5184      content: this
5185    }).defaultPrevented) {
5186      return;
5187    }
5188
5189    this.remove();
5190
5191    if (this.placeholder) {
5192      this.placeholder.destroy();
5193      this.placeholder = undefined;
5194    }
5195
5196    if (this.isImageContent() && this.element) {
5197      this.element.onload = null;
5198      this.element.onerror = null;
5199      this.element = undefined;
5200    }
5201  }
5202  /**
5203   * Display error message
5204   */
5205
5206
5207  displayError() {
5208    if (this.slide) {
5209      var _this$instance$option, _this$instance$option2;
5210
5211      let errorMsgEl = createElement('pswp__error-msg', 'div');
5212      errorMsgEl.innerText = (_this$instance$option = (_this$instance$option2 = this.instance.options) === null || _this$instance$option2 === void 0 ? void 0 : _this$instance$option2.errorMsg) !== null && _this$instance$option !== void 0 ? _this$instance$option : '';
5213      errorMsgEl =
5214      /** @type {HTMLDivElement} */
5215      this.instance.applyFilters('contentErrorElement', errorMsgEl, this);
5216      this.element = createElement('pswp__content pswp__error-msg-container', 'div');
5217      this.element.appendChild(errorMsgEl);
5218      this.slide.container.innerText = '';
5219      this.slide.container.appendChild(this.element);
5220      this.slide.updateContentSize(true);
5221      this.removePlaceholder();
5222    }
5223  }
5224  /**
5225   * Append the content
5226   */
5227
5228
5229  append() {
5230    if (this.isAttached || !this.element) {
5231      return;
5232    }
5233
5234    this.isAttached = true;
5235
5236    if (this.state === LOAD_STATE.ERROR) {
5237      this.displayError();
5238      return;
5239    }
5240
5241    if (this.instance.dispatch('contentAppend', {
5242      content: this
5243    }).defaultPrevented) {
5244      return;
5245    }
5246
5247    const supportsDecode = ('decode' in this.element);
5248
5249    if (this.isImageContent()) {
5250      // Use decode() on nearby slides
5251      //
5252      // Nearby slide images are in DOM and not hidden via display:none.
5253      // However, they are placed offscreen (to the left and right side).
5254      //
5255      // Some browsers do not composite the image until it's actually visible,
5256      // using decode() helps.
5257      //
5258      // You might ask "why dont you just decode() and then append all images",
5259      // that's because I want to show image before it's fully loaded,
5260      // as browser can render parts of image while it is loading.
5261      // We do not do this in Safari due to partial loading bug.
5262      if (supportsDecode && this.slide && (!this.slide.isActive || isSafari())) {
5263        this.isDecoding = true; // purposefully using finally instead of then,
5264        // as if srcset sizes changes dynamically - it may cause decode error
5265
5266        /** @type {HTMLImageElement} */
5267
5268        this.element.decode().catch(() => {}).finally(() => {
5269          this.isDecoding = false;
5270          this.appendImage();
5271        });
5272      } else {
5273        this.appendImage();
5274      }
5275    } else if (this.slide && !this.element.parentNode) {
5276      this.slide.container.appendChild(this.element);
5277    }
5278  }
5279  /**
5280   * Activate the slide,
5281   * active slide is generally the current one,
5282   * meaning the user can see it.
5283   */
5284
5285
5286  activate() {
5287    if (this.instance.dispatch('contentActivate', {
5288      content: this
5289    }).defaultPrevented || !this.slide) {
5290      return;
5291    }
5292
5293    if (this.isImageContent() && this.isDecoding && !isSafari()) {
5294      // add image to slide when it becomes active,
5295      // even if it's not finished decoding
5296      this.appendImage();
5297    } else if (this.isError()) {
5298      this.load(false, true); // try to reload
5299    }
5300
5301    if (this.slide.holderElement) {
5302      this.slide.holderElement.setAttribute('aria-hidden', 'false');
5303    }
5304  }
5305  /**
5306   * Deactivate the content
5307   */
5308
5309
5310  deactivate() {
5311    this.instance.dispatch('contentDeactivate', {
5312      content: this
5313    });
5314
5315    if (this.slide && this.slide.holderElement) {
5316      this.slide.holderElement.setAttribute('aria-hidden', 'true');
5317    }
5318  }
5319  /**
5320   * Remove the content from DOM
5321   */
5322
5323
5324  remove() {
5325    this.isAttached = false;
5326
5327    if (this.instance.dispatch('contentRemove', {
5328      content: this
5329    }).defaultPrevented) {
5330      return;
5331    }
5332
5333    if (this.element && this.element.parentNode) {
5334      this.element.remove();
5335    }
5336
5337    if (this.placeholder && this.placeholder.element) {
5338      this.placeholder.element.remove();
5339    }
5340  }
5341  /**
5342   * Append the image content to slide container
5343   */
5344
5345
5346  appendImage() {
5347    if (!this.isAttached) {
5348      return;
5349    }
5350
5351    if (this.instance.dispatch('contentAppendImage', {
5352      content: this
5353    }).defaultPrevented) {
5354      return;
5355    } // ensure that element exists and is not already appended
5356
5357
5358    if (this.slide && this.element && !this.element.parentNode) {
5359      this.slide.container.appendChild(this.element);
5360    }
5361
5362    if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
5363      this.removePlaceholder();
5364    }
5365  }
5366
5367}
5368
5369/** @typedef {import('./content.js').default} Content */
5370
vendor: 10,948 bytes, lines 5371-5774
5371/** @typedef {import('./slide.js').default} Slide */
5372
5373/** @typedef {import('./slide.js').SlideData} SlideData */
5374
5375/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
5376
5377/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
5378
5379const MIN_SLIDES_TO_CACHE = 5;
5380/**
5381 * Lazy-load an image
5382 * This function is used both by Lightbox and PhotoSwipe core,
5383 * thus it can be called before dialog is opened.
5384 *
5385 * @param {SlideData} itemData Data about the slide
5386 * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
5387 * @param {number} index
5388 * @returns {Content} Image that is being decoded or false.
5389 */
5390
5391function lazyLoadData(itemData, instance, index) {
5392  const content = instance.createContentFromData(itemData, index);
5393  /** @type {ZoomLevel | undefined} */
5394
5395  let zoomLevel;
5396  const {
5397    options
5398  } = instance; // We need to know dimensions of the image to preload it,
5399  // as it might use srcset, and we need to define sizes
5400
5401  if (options) {
5402    zoomLevel = new ZoomLevel(options, itemData, -1);
5403    let viewportSize;
5404
5405    if (instance.pswp) {
5406      viewportSize = instance.pswp.viewportSize;
5407    } else {
5408      viewportSize = getViewportSize(options, instance);
5409    }
5410
5411    const panAreaSize = getPanAreaSize(options, viewportSize, itemData, index);
5412    zoomLevel.update(content.width, content.height, panAreaSize);
5413  }
5414
5415  content.lazyLoad();
5416
5417  if (zoomLevel) {
5418    content.setDisplayedSize(Math.ceil(content.width * zoomLevel.initial), Math.ceil(content.height * zoomLevel.initial));
5419  }
5420
5421  return content;
5422}
5423/**
5424 * Lazy-loads specific slide.
5425 * This function is used both by Lightbox and PhotoSwipe core,
5426 * thus it can be called before dialog is opened.
5427 *
5428 * By default, it loads image based on viewport size and initial zoom level.
5429 *
5430 * @param {number} index Slide index
5431 * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox eventable instance
5432 * @returns {Content | undefined}
5433 */
5434
5435function lazyLoadSlide(index, instance) {
5436  const itemData = instance.getItemData(index);
5437
5438  if (instance.dispatch('lazyLoadSlide', {
5439    index,
5440    itemData
5441  }).defaultPrevented) {
5442    return;
5443  }
5444
5445  return lazyLoadData(itemData, instance, index);
5446}
5447
5448class ContentLoader {
5449  /**
5450   * @param {PhotoSwipe} pswp
5451   */
5452  constructor(pswp) {
5453    this.pswp = pswp; // Total amount of cached images
5454
5455    this.limit = Math.max(pswp.options.preload[0] + pswp.options.preload[1] + 1, MIN_SLIDES_TO_CACHE);
5456    /** @type {Content[]} */
5457
5458    this._cachedItems = [];
5459  }
5460  /**
5461   * Lazy load nearby slides based on `preload` option.
5462   *
5463   * @param {number} [diff] Difference between slide indexes that was changed recently, or 0.
5464   */
5465
5466
5467  updateLazy(diff) {
5468    const {
5469      pswp
5470    } = this;
5471
5472    if (pswp.dispatch('lazyLoad').defaultPrevented) {
5473      return;
5474    }
5475
5476    const {
5477      preload
5478    } = pswp.options;
5479    const isForward = diff === undefined ? true : diff >= 0;
5480    let i; // preload[1] - num items to preload in forward direction
5481
5482    for (i = 0; i <= preload[1]; i++) {
5483      this.loadSlideByIndex(pswp.currIndex + (isForward ? i : -i));
5484    } // preload[0] - num items to preload in backward direction
5485
5486
5487    for (i = 1; i <= preload[0]; i++) {
5488      this.loadSlideByIndex(pswp.currIndex + (isForward ? -i : i));
5489    }
5490  }
5491  /**
5492   * @param {number} initialIndex
5493   */
5494
5495
5496  loadSlideByIndex(initialIndex) {
5497    const index = this.pswp.getLoopedIndex(initialIndex); // try to get cached content
5498
5499    let content = this.getContentByIndex(index);
5500
5501    if (!content) {
5502      // no cached content, so try to load from scratch:
5503      content = lazyLoadSlide(index, this.pswp); // if content can be loaded, add it to cache:
5504
5505      if (content) {
5506        this.addToCache(content);
5507      }
5508    }
5509  }
5510  /**
5511   * @param {Slide} slide
5512   * @returns {Content}
5513   */
5514
5515
5516  getContentBySlide(slide) {
5517    let content = this.getContentByIndex(slide.index);
5518
5519    if (!content) {
5520      // create content if not found in cache
5521      content = this.pswp.createContentFromData(slide.data, slide.index);
5522      this.addToCache(content);
5523    } // assign slide to content
5524
5525
5526    content.setSlide(slide);
5527    return content;
5528  }
5529  /**
5530   * @param {Content} content
5531   */
5532
5533
5534  addToCache(content) {
5535    // move to the end of array
5536    this.removeByIndex(content.index);
5537
5538    this._cachedItems.push(content);
5539
5540    if (this._cachedItems.length > this.limit) {
5541      // Destroy the first content that's not attached
5542      const indexToRemove = this._cachedItems.findIndex(item => {
5543        return !item.isAttached && !item.hasSlide;
5544      });
5545
5546      if (indexToRemove !== -1) {
5547        const removedItem = this._cachedItems.splice(indexToRemove, 1)[0];
5548
5549        removedItem.destroy();
5550      }
5551    }
5552  }
5553  /**
5554   * Removes an image from cache, does not destroy() it, just removes.
5555   *
5556   * @param {number} index
5557   */
5558
5559
5560  removeByIndex(index) {
5561    const indexToRemove = this._cachedItems.findIndex(item => item.index === index);
5562
5563    if (indexToRemove !== -1) {
5564      this._cachedItems.splice(indexToRemove, 1);
5565    }
5566  }
5567  /**
5568   * @param {number} index
5569   * @returns {Content | undefined}
5570   */
5571
5572
5573  getContentByIndex(index) {
5574    return this._cachedItems.find(content => content.index === index);
5575  }
5576
5577  destroy() {
5578    this._cachedItems.forEach(content => content.destroy());
5579
5580    this._cachedItems = [];
5581  }
5582
5583}
5584
5585/** @typedef {import("../photoswipe.js").default} PhotoSwipe */
5586
5587/** @typedef {import("../slide/slide.js").SlideData} SlideData */
5588
5589/**
5590 * PhotoSwipe base class that can retrieve data about every slide.
5591 * Shared by PhotoSwipe Core and PhotoSwipe Lightbox
5592 */
5593
5594class PhotoSwipeBase extends Eventable {
5595  /**
5596   * Get total number of slides
5597   *
5598   * @returns {number}
5599   */
5600  getNumItems() {
5601    var _this$options;
5602
5603    let numItems = 0;
5604    const dataSource = (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.dataSource;
5605
5606    if (dataSource && 'length' in dataSource) {
5607      // may be an array or just object with length property
5608      numItems = dataSource.length;
5609    } else if (dataSource && 'gallery' in dataSource) {
5610      // query DOM elements
5611      if (!dataSource.items) {
5612        dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
5613      }
5614
5615      if (dataSource.items) {
5616        numItems = dataSource.items.length;
5617      }
5618    } // legacy event, before filters were introduced
5619
5620
5621    const event = this.dispatch('numItems', {
5622      dataSource,
5623      numItems
5624    });
5625    return this.applyFilters('numItems', event.numItems, dataSource);
5626  }
5627  /**
5628   * @param {SlideData} slideData
5629   * @param {number} index
5630   * @returns {Content}
5631   */
5632
5633
5634  createContentFromData(slideData, index) {
5635    return new Content(slideData, this, index);
5636  }
5637  /**
5638   * Get item data by index.
5639   *
5640   * "item data" should contain normalized information that PhotoSwipe needs to generate a slide.
5641   * For example, it may contain properties like
5642   * `src`, `srcset`, `w`, `h`, which will be used to generate a slide with image.
5643   *
5644   * @param {number} index
5645   * @returns {SlideData}
5646   */
5647
5648
5649  getItemData(index) {
5650    var _this$options2;
5651
5652    const dataSource = (_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.dataSource;
5653    /** @type {SlideData | HTMLElement} */
5654
5655    let dataSourceItem = {};
5656
5657    if (Array.isArray(dataSource)) {
5658      // Datasource is an array of elements
5659      dataSourceItem = dataSource[index];
5660    } else if (dataSource && 'gallery' in dataSource) {
5661      // dataSource has gallery property,
5662      // thus it was created by Lightbox, based on
5663      // gallery and children options
5664      // query DOM elements
5665      if (!dataSource.items) {
5666        dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
5667      }
5668
5669      dataSourceItem = dataSource.items[index];
5670    }
5671
5672    let itemData = dataSourceItem;
5673
5674    if (itemData instanceof Element) {
5675      itemData = this._domElementToItemData(itemData);
5676    } // Dispatching the itemData event,
5677    // it's a legacy verion before filters were introduced
5678
5679
5680    const event = this.dispatch('itemData', {
5681      itemData: itemData || {},
5682      index
5683    });
5684    return this.applyFilters('itemData', event.itemData, index);
5685  }
5686  /**
5687   * Get array of gallery DOM elements,
5688   * based on childSelector and gallery element.
5689   *
5690   * @param {HTMLElement} galleryElement
5691   * @returns {HTMLElement[]}
5692   */
5693
5694
5695  _getGalleryDOMElements(galleryElement) {
5696    var _this$options3, _this$options4;
5697
5698    if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.children || (_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.childSelector) {
5699      return getElementsFromOption(this.options.children, this.options.childSelector, galleryElement) || [];
5700    }
5701
5702    return [galleryElement];
5703  }
5704  /**
5705   * Converts DOM element to item data object.
5706   *
5707   * @param {HTMLElement} element DOM element
5708   * @returns {SlideData}
5709   */
5710
5711
5712  _domElementToItemData(element) {
5713    /** @type {SlideData} */
5714    const itemData = {
5715      element
5716    };
5717    const linkEl =
5718    /** @type {HTMLAnchorElement} */
5719    element.tagName === 'A' ? element : element.querySelector('a');
5720
5721    if (linkEl) {
5722      // src comes from data-pswp-src attribute,
5723      // if it's empty link href is used
5724      itemData.src = linkEl.dataset.pswpSrc || linkEl.href;
5725
5726      if (linkEl.dataset.pswpSrcset) {
5727        itemData.srcset = linkEl.dataset.pswpSrcset;
5728      }
5729
5730      itemData.width = linkEl.dataset.pswpWidth ? parseInt(linkEl.dataset.pswpWidth, 10) : 0;
5731      itemData.height = linkEl.dataset.pswpHeight ? parseInt(linkEl.dataset.pswpHeight, 10) : 0; // support legacy w & h properties
5732
5733      itemData.w = itemData.width;
5734      itemData.h = itemData.height;
5735
5736      if (linkEl.dataset.pswpType) {
5737        itemData.type = linkEl.dataset.pswpType;
5738      }
5739
5740      const thumbnailEl = element.querySelector('img');
5741
5742      if (thumbnailEl) {
5743        var _thumbnailEl$getAttri;
5744
5745        // msrc is URL to placeholder image that's displayed before large image is loaded
5746        // by default it's displayed only for the first slide
5747        itemData.msrc = thumbnailEl.currentSrc || thumbnailEl.src;
5748        itemData.alt = (_thumbnailEl$getAttri = thumbnailEl.getAttribute('alt')) !== null && _thumbnailEl$getAttri !== void 0 ? _thumbnailEl$getAttri : '';
5749      }
5750
5751      if (linkEl.dataset.pswpCropped || linkEl.dataset.cropped) {
5752        itemData.thumbCropped = true;
5753      }
5754    }
5755
5756    return this.applyFilters('domItemData', itemData, element, linkEl);
5757  }
5758  /**
5759   * Lazy-load by slide data
5760   *
5761   * @param {SlideData} itemData Data about the slide
5762   * @param {number} index
5763   * @returns {Content} Image that is being decoded or false.
5764   */
5765
5766
5767  lazyLoadData(itemData, index) {
5768    return lazyLoadData(itemData, this, index);
5769  }
5770
5771}
5772
5773/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
5774
5775/** @typedef {import('./slide/get-thumb-bounds.js').Bounds} Bounds */
5776
5777/** @typedef {import('./util/animations.js').AnimationProps} AnimationProps */
5778// some browsers do not paint
5779// elements which opacity is set to 0,
5780// since we need to pre-render elements for the animation -
5781// we set it to the minimum amount
5782
5783const MIN_OPACITY = 0.003;
5784/**
5785 * Manages opening and closing transitions of the PhotoSwipe.
5786 *
5787 * It can perform zoom, fade or no transition.
5788 */
5789
5790class Opener {
5791  /**
5792   * @param {PhotoSwipe} pswp
5793   */
5794  constructor(pswp) {
5795    this.pswp = pswp;
5796    this.isClosed = true;
5797    this.isOpen = false;
5798    this.isClosing = false;
5799    this.isOpening = false;
5800    /**
5801     * @private
5802     * @type {number | false | undefined}
5803     */
5804
5805    this._duration = undefined;
5806    /** @private */
5807
5808    this._useAnimation = false;
5809    /** @private */
5810
5811    this._croppedZoom = false;
5812    /** @private */
5813
5814    this._animateRootOpacity = false;
5815    /** @private */
5816
5817    this._animateBgOpacity = false;
5818    /**
5819     * @private
5820     * @type { HTMLDivElement | HTMLImageElement | null | undefined }
5821     */
5822
5823    this._placeholder = undefined;
5824    /**
5825     * @private
5826     * @type { HTMLDivElement | undefined }
5827     */
5828
5829    this._opacityElement = undefined;
5830    /**
5831     * @private
5832     * @type { HTMLDivElement | undefined }
5833     */
5834
5835    this._cropContainer1 = undefined;
5836    /**
5837     * @private
5838     * @type { HTMLElement | null | undefined }
5839     */
5840
5841    this._cropContainer2 = undefined;
5842    /**
5843     * @private
5844     * @type {Bounds | undefined}
vendor: 9,634 bytes, lines 5844-6178
5844
5845     */
5846
5847    this._thumbBounds = undefined;
5848    this._prepareOpen = this._prepareOpen.bind(this); // Override initial zoom and pan position
5849
5850    pswp.on('firstZoomPan', this._prepareOpen);
5851  }
5852
5853  open() {
5854    this._prepareOpen();
5855
5856    this._start();
5857  }
5858
5859  close() {
5860    if (this.isClosed || this.isClosing || this.isOpening) {
5861      // if we close during opening animation
5862      // for now do nothing,
5863      // browsers aren't good at changing the direction of the CSS transition
5864      return;
5865    }
5866
5867    const slide = this.pswp.currSlide;
5868    this.isOpen = false;
5869    this.isOpening = false;
5870    this.isClosing = true;
5871    this._duration = this.pswp.options.hideAnimationDuration;
5872
5873    if (slide && slide.currZoomLevel * slide.width >= this.pswp.options.maxWidthToAnimate) {
5874      this._duration = 0;
5875    }
5876
5877    this._applyStartProps();
5878
5879    setTimeout(() => {
5880      this._start();
5881    }, this._croppedZoom ? 30 : 0);
5882  }
5883  /** @private */
5884
5885
5886  _prepareOpen() {
5887    this.pswp.off('firstZoomPan', this._prepareOpen);
5888
5889    if (!this.isOpening) {
5890      const slide = this.pswp.currSlide;
5891      this.isOpening = true;
5892      this.isClosing = false;
5893      this._duration = this.pswp.options.showAnimationDuration;
5894
5895      if (slide && slide.zoomLevels.initial * slide.width >= this.pswp.options.maxWidthToAnimate) {
5896        this._duration = 0;
5897      }
5898
5899      this._applyStartProps();
5900    }
5901  }
5902  /** @private */
5903
5904
5905  _applyStartProps() {
5906    const {
5907      pswp
5908    } = this;
5909    const slide = this.pswp.currSlide;
5910    const {
5911      options
5912    } = pswp;
5913
5914    if (options.showHideAnimationType === 'fade') {
5915      options.showHideOpacity = true;
5916      this._thumbBounds = undefined;
5917    } else if (options.showHideAnimationType === 'none') {
5918      options.showHideOpacity = false;
5919      this._duration = 0;
5920      this._thumbBounds = undefined;
5921    } else if (this.isOpening && pswp._initialThumbBounds) {
5922      // Use initial bounds if defined
5923      this._thumbBounds = pswp._initialThumbBounds;
5924    } else {
5925      this._thumbBounds = this.pswp.getThumbBounds();
5926    }
5927
5928    this._placeholder = slide === null || slide === void 0 ? void 0 : slide.getPlaceholderElement();
5929    pswp.animations.stopAll(); // Discard animations when duration is less than 50ms
5930
5931    this._useAnimation = Boolean(this._duration && this._duration > 50);
5932    this._animateZoom = Boolean(this._thumbBounds) && (slide === null || slide === void 0 ? void 0 : slide.content.usePlaceholder()) && (!this.isClosing || !pswp.mainScroll.isShifted());
5933
5934    if (!this._animateZoom) {
5935      this._animateRootOpacity = true;
5936
5937      if (this.isOpening && slide) {
5938        slide.zoomAndPanToInitial();
5939        slide.applyCurrentZoomPan();
5940      }
5941    } else {
5942      var _options$showHideOpac;
5943
5944      this._animateRootOpacity = (_options$showHideOpac = options.showHideOpacity) !== null && _options$showHideOpac !== void 0 ? _options$showHideOpac : false;
5945    }
5946
5947    this._animateBgOpacity = !this._animateRootOpacity && this.pswp.options.bgOpacity > MIN_OPACITY;
5948    this._opacityElement = this._animateRootOpacity ? pswp.element : pswp.bg;
5949
5950    if (!this._useAnimation) {
5951      this._duration = 0;
5952      this._animateZoom = false;
5953      this._animateBgOpacity = false;
5954      this._animateRootOpacity = true;
5955
5956      if (this.isOpening) {
5957        if (pswp.element) {
5958          pswp.element.style.opacity = String(MIN_OPACITY);
5959        }
5960
5961        pswp.applyBgOpacity(1);
5962      }
5963
5964      return;
5965    }
5966
5967    if (this._animateZoom && this._thumbBounds && this._thumbBounds.innerRect) {
5968      var _this$pswp$currSlide;
5969
5970      // Properties are used when animation from cropped thumbnail
5971      this._croppedZoom = true;
5972      this._cropContainer1 = this.pswp.container;
5973      this._cropContainer2 = (_this$pswp$currSlide = this.pswp.currSlide) === null || _this$pswp$currSlide === void 0 ? void 0 : _this$pswp$currSlide.holderElement;
5974
5975      if (pswp.container) {
5976        pswp.container.style.overflow = 'hidden';
5977        pswp.container.style.width = pswp.viewportSize.x + 'px';
5978      }
5979    } else {
5980      this._croppedZoom = false;
5981    }
5982
5983    if (this.isOpening) {
5984      // Apply styles before opening transition
5985      if (this._animateRootOpacity) {
5986        if (pswp.element) {
5987          pswp.element.style.opacity = String(MIN_OPACITY);
5988        }
5989
5990        pswp.applyBgOpacity(1);
5991      } else {
5992        if (this._animateBgOpacity && pswp.bg) {
5993          pswp.bg.style.opacity = String(MIN_OPACITY);
5994        }
5995
5996        if (pswp.element) {
5997          pswp.element.style.opacity = '1';
5998        }
5999      }
6000
6001      if (this._animateZoom) {
6002        this._setClosedStateZoomPan();
6003
6004        if (this._placeholder) {
6005          // tell browser that we plan to animate the placeholder
6006          this._placeholder.style.willChange = 'transform'; // hide placeholder to allow hiding of
6007          // elements that overlap it (such as icons over the thumbnail)
6008
6009          this._placeholder.style.opacity = String(MIN_OPACITY);
6010        }
6011      }
6012    } else if (this.isClosing) {
6013      // hide nearby slides to make sure that
6014      // they are not painted during the transition
6015      if (pswp.mainScroll.itemHolders[0]) {
6016        pswp.mainScroll.itemHolders[0].el.style.display = 'none';
6017      }
6018
6019      if (pswp.mainScroll.itemHolders[2]) {
6020        pswp.mainScroll.itemHolders[2].el.style.display = 'none';
6021      }
6022
6023      if (this._croppedZoom) {
6024        if (pswp.mainScroll.x !== 0) {
6025          // shift the main scroller to zero position
6026          pswp.mainScroll.resetPosition();
6027          pswp.mainScroll.resize();
6028        }
6029      }
6030    }
6031  }
6032  /** @private */
6033
6034
6035  _start() {
6036    if (this.isOpening && this._useAnimation && this._placeholder && this._placeholder.tagName === 'IMG') {
6037      // To ensure smooth animation
6038      // we wait till the current slide image placeholder is decoded,
6039      // but no longer than 250ms,
6040      // and no shorter than 50ms
6041      // (just using requestanimationframe is not enough in Firefox,
6042      // for some reason)
6043      new Promise(resolve => {
6044        let decoded = false;
6045        let isDelaying = true;
6046        decodeImage(
6047        /** @type {HTMLImageElement} */
6048        this._placeholder).finally(() => {
6049          decoded = true;
6050
6051          if (!isDelaying) {
6052            resolve(true);
6053          }
6054        });
6055        setTimeout(() => {
6056          isDelaying = false;
6057
6058          if (decoded) {
6059            resolve(true);
6060          }
6061        }, 50);
6062        setTimeout(resolve, 250);
6063      }).finally(() => this._initiate());
6064    } else {
6065      this._initiate();
6066    }
6067  }
6068  /** @private */
6069
6070
6071  _initiate() {
6072    var _this$pswp$element, _this$pswp$element2;
6073
6074    (_this$pswp$element = this.pswp.element) === null || _this$pswp$element === void 0 || _this$pswp$element.style.setProperty('--pswp-transition-duration', this._duration + 'ms');
6075    this.pswp.dispatch(this.isOpening ? 'openingAnimationStart' : 'closingAnimationStart'); // legacy event
6076
6077    this.pswp.dispatch(
6078    /** @type {'initialZoomIn' | 'initialZoomOut'} */
6079    'initialZoom' + (this.isOpening ? 'In' : 'Out'));
6080    (_this$pswp$element2 = this.pswp.element) === null || _this$pswp$element2 === void 0 || _this$pswp$element2.classList.toggle('pswp--ui-visible', this.isOpening);
6081
6082    if (this.isOpening) {
6083      if (this._placeholder) {
6084        // unhide the placeholder
6085        this._placeholder.style.opacity = '1';
6086      }
6087
6088      this._animateToOpenState();
6089    } else if (this.isClosing) {
6090      this._animateToClosedState();
6091    }
6092
6093    if (!this._useAnimation) {
6094      this._onAnimationComplete();
6095    }
6096  }
6097  /** @private */
6098
6099
6100  _onAnimationComplete() {
6101    const {
6102      pswp
6103    } = this;
6104    this.isOpen = this.isOpening;
6105    this.isClosed = this.isClosing;
6106    this.isOpening = false;
6107    this.isClosing = false;
6108    pswp.dispatch(this.isOpen ? 'openingAnimationEnd' : 'closingAnimationEnd'); // legacy event
6109
6110    pswp.dispatch(
6111    /** @type {'initialZoomInEnd' | 'initialZoomOutEnd'} */
6112    'initialZoom' + (this.isOpen ? 'InEnd' : 'OutEnd'));
6113
6114    if (this.isClosed) {
6115      pswp.destroy();
6116    } else if (this.isOpen) {
6117      var _pswp$currSlide;
6118
6119      if (this._animateZoom && pswp.container) {
6120        pswp.container.style.overflow = 'visible';
6121        pswp.container.style.width = '100%';
6122      }
6123
6124      (_pswp$currSlide = pswp.currSlide) === null || _pswp$currSlide === void 0 || _pswp$currSlide.applyCurrentZoomPan();
6125    }
6126  }
6127  /** @private */
6128
6129
6130  _animateToOpenState() {
6131    const {
6132      pswp
6133    } = this;
6134
6135    if (this._animateZoom) {
6136      if (this._croppedZoom && this._cropContainer1 && this._cropContainer2) {
6137        this._animateTo(this._cropContainer1, 'transform', 'translate3d(0,0,0)');
6138
6139        this._animateTo(this._cropContainer2, 'transform', 'none');
6140      }
6141
6142      if (pswp.currSlide) {
6143        pswp.currSlide.zoomAndPanToInitial();
6144
6145        this._animateTo(pswp.currSlide.container, 'transform', pswp.currSlide.getCurrentTransform());
6146      }
6147    }
6148
6149    if (this._animateBgOpacity && pswp.bg) {
6150      this._animateTo(pswp.bg, 'opacity', String(pswp.options.bgOpacity));
6151    }
6152
6153    if (this._animateRootOpacity && pswp.element) {
6154      this._animateTo(pswp.element, 'opacity', '1');
6155    }
6156  }
6157  /** @private */
6158
6159
6160  _animateToClosedState() {
6161    const {
6162      pswp
6163    } = this;
6164
6165    if (this._animateZoom) {
6166      this._setClosedStateZoomPan(true);
6167    } // do not animate opacity if it's already at 0
6168
6169
6170    if (this._animateBgOpacity && pswp.bgOpacity > 0.01 && pswp.bg) {
6171      this._animateTo(pswp.bg, 'opacity', '0');
6172    }
6173
6174    if (this._animateRootOpacity && pswp.element) {
6175      this._animateTo(pswp.element, 'opacity', '0');
6176    }
6177  }
6178  /**
vendor: 3,250 bytes, lines 6178-6293
6178
6179   * @private
6180   * @param {boolean} [animate]
6181   */
6182
6183
6184  _setClosedStateZoomPan(animate) {
6185    if (!this._thumbBounds) return;
6186    const {
6187      pswp
6188    } = this;
6189    const {
6190      innerRect
6191    } = this._thumbBounds;
6192    const {
6193      currSlide,
6194      viewportSize
6195    } = pswp;
6196
6197    if (this._croppedZoom && innerRect && this._cropContainer1 && this._cropContainer2) {
6198      const containerOnePanX = -viewportSize.x + (this._thumbBounds.x - innerRect.x) + innerRect.w;
6199      const containerOnePanY = -viewportSize.y + (this._thumbBounds.y - innerRect.y) + innerRect.h;
6200      const containerTwoPanX = viewportSize.x - innerRect.w;
6201      const containerTwoPanY = viewportSize.y - innerRect.h;
6202
6203      if (animate) {
6204        this._animateTo(this._cropContainer1, 'transform', toTransformString(containerOnePanX, containerOnePanY));
6205
6206        this._animateTo(this._cropContainer2, 'transform', toTransformString(containerTwoPanX, containerTwoPanY));
6207      } else {
6208        setTransform(this._cropContainer1, containerOnePanX, containerOnePanY);
6209        setTransform(this._cropContainer2, containerTwoPanX, containerTwoPanY);
6210      }
6211    }
6212
6213    if (currSlide) {
6214      equalizePoints(currSlide.pan, innerRect || this._thumbBounds);
6215      currSlide.currZoomLevel = this._thumbBounds.w / currSlide.width;
6216
6217      if (animate) {
6218        this._animateTo(currSlide.container, 'transform', currSlide.getCurrentTransform());
6219      } else {
6220        currSlide.applyCurrentZoomPan();
6221      }
6222    }
6223  }
6224  /**
6225   * @private
6226   * @param {HTMLElement} target
6227   * @param {'transform' | 'opacity'} prop
6228   * @param {string} propValue
6229   */
6230
6231
6232  _animateTo(target, prop, propValue) {
6233    if (!this._duration) {
6234      target.style[prop] = propValue;
6235      return;
6236    }
6237
6238    const {
6239      animations
6240    } = this.pswp;
6241    /** @type {AnimationProps} */
6242
6243    const animProps = {
6244      duration: this._duration,
6245      easing: this.pswp.options.easing,
6246      onComplete: () => {
6247        if (!animations.activeAnimations.length) {
6248          this._onAnimationComplete();
6249        }
6250      },
6251      target
6252    };
6253    animProps[prop] = propValue;
6254    animations.startTransition(animProps);
6255  }
6256
6257}
6258
6259/**
6260 * @template T
6261 * @typedef {import('./types.js').Type<T>} Type<T>
6262 */
6263
6264/** @typedef {import('./slide/slide.js').SlideData} SlideData */
6265
6266/** @typedef {import('./slide/zoom-level.js').ZoomLevelOption} ZoomLevelOption */
6267
6268/** @typedef {import('./ui/ui-element.js').UIElementData} UIElementData */
6269
6270/** @typedef {import('./main-scroll.js').ItemHolder} ItemHolder */
6271
6272/** @typedef {import('./core/eventable.js').PhotoSwipeEventsMap} PhotoSwipeEventsMap */
6273
6274/** @typedef {import('./core/eventable.js').PhotoSwipeFiltersMap} PhotoSwipeFiltersMap */
6275
6276/** @typedef {import('./slide/get-thumb-bounds').Bounds} Bounds */
6277
6278/**
6279 * @template {keyof PhotoSwipeEventsMap} T
6280 * @typedef {import('./core/eventable.js').EventCallback<T>} EventCallback<T>
6281 */
6282
6283/**
6284 * @template {keyof PhotoSwipeEventsMap} T
6285 * @typedef {import('./core/eventable.js').AugmentedEvent<T>} AugmentedEvent<T>
6286 */
6287
6288/** @typedef {{ x: number; y: number; id?: string | number }} Point */
6289
6290/** @typedef {{ top: number; bottom: number; left: number; right: number }} Padding */
6291
6292/** @typedef {SlideData[]} DataSourceArray */
6293
6294/** @typedef {{ gallery: HTMLElement; items?: HTMLElement[] }} DataSourceObject */
6295
6296/** @typedef {DataSourceArray | DataSourceObject} DataSource */
6297
6298/** @typedef {(point: Point, originalEvent: PointerEvent) => void} ActionFn */
6299
6300/** @typedef {'close' | 'next' | 'zoom' | 'zoom-or-close' | 'toggle-controls'} ActionType */
6301
6302/** @typedef {Type<PhotoSwipe> | { default: Type<PhotoSwipe> }} PhotoSwipeModule */
6303
6304/** @typedef {PhotoSwipeModule | Promise<PhotoSwipeModule> | (() => Promise<PhotoSwipeModule>)} PhotoSwipeModuleOption */
6305
6306/**
6307 * @typedef {string | NodeListOf<HTMLElement> | HTMLElement[] | HTMLElement} ElementProvider
6308 */
6309
6310/** @typedef {Partial<PreparedPhotoSwipeOptions>} PhotoSwipeOptions https://photoswipe.com/options/ */
6311
6312/**
6313 * @typedef {Object} PreparedPhotoSwipeOptions
6314 *
6315 * @prop {DataSource} [dataSource]
6316 * Pass an array of any items via dataSource option. Its length will determine amount of slides
6317 * (which may be modified further from numItems event).
6318 *
6319 * Each item should contain data that you need to generate slide
6320 * (for image slide it would be src (image URL), width (image width), height, srcset, alt).
6321 *
6322 * If these properties are not present in your initial array, you may "pre-parse" each item from itemData filter.
6323 *
6324 * @prop {number} bgOpacity
6325 * Background backdrop opacity, always define it via this option and not via CSS rgba color.
6326 *
6327 * @prop {number} spacing
6328 * Spacing between slides. Defined as ratio relative to the viewport width (0.1 = 10% of viewport).
6329 *
6330 * @prop {boolean} allowPanToNext
6331 * Allow swipe navigation to the next slide when the current slide is zoomed. Does not apply to mouse events.
6332 *
6333 * @prop {boolean} loop
6334 * If set to true you'll be able to swipe from the last to the first image.
6335 * Option is always false when there are less than 3 slides.
6336 *
6337 * @prop {boolean} [wheelToZoom]
6338 * By default PhotoSwipe zooms image with ctrl-wheel, if you enable this option - image will zoom just via wheel.
6339 *
6340 * @prop {boolean} pinchToClose
6341 * Pinch touch gesture to close the gallery.
6342 *
6343 * @prop {boolean} closeOnVerticalDrag
6344 * Vertical drag gesture to close the PhotoSwipe.
6345 *
6346 * @prop {Padding} [padding]
6347 * Slide area padding (in pixels).
6348 *
6349 * @prop {(viewportSize: Point, itemData: SlideData, index: number) => Padding} [paddingFn]
6350 * The option is checked frequently, so make sure it's performant. Overrides padding option if defined. For example:
6351 *
6352 * @prop {number | false} hideAnimationDuration
6353 * Transition duration in milliseconds, can be 0.
6354 *
6355 * @prop {number | false} showAnimationDuration
6356 * Transition duration in milliseconds, can be 0.
6357 *
6358 * @prop {number | false} zoomAnimationDuration
6359 * Transition duration in milliseconds, can be 0.
6360 *
6361 * @prop {string} easing
6362 * String, 'cubic-bezier(.4,0,.22,1)'. CSS easing function for open/close/zoom transitions.
6363 *
6364 * @prop {boolean} escKey
6365 * Esc key to close.
6366 *
6367 * @prop {boolean} arrowKeys
6368 * Left/right arrow keys for navigation.
6369 *
6370 * @prop {boolean} trapFocus
6371 * Trap focus within PhotoSwipe element while it's open.
6372 *
6373 * @prop {boolean} returnFocus
6374 * Restore focus the last active element after PhotoSwipe is closed.
6375 *
6376 * @prop {boolean} clickToCloseNonZoomable
6377 * If image is not zoomable (for example, smaller than viewport) it can be closed by clicking on it.
6378 *
6379 * @prop {ActionType | ActionFn | false} imageClickAction
6380 * Refer to click and tap actions page.
6381 *
6382 * @prop {ActionType | ActionFn | false} bgClickAction
6383 * Refer to click and tap actions page.
6384 *
6385 * @prop {ActionType | ActionFn | false} tapAction
6386 * Refer to click and tap actions page.
6387 *
6388 * @prop {ActionType | ActionFn | false} doubleTapAction
6389 * Refer to click and tap actions page.
6390 *
6391 * @prop {number} preloaderDelay
6392 * Delay before the loading indicator will be displayed,
6393 * if image is loaded during it - the indicator will not be displayed at all. Can be zero.
6394 *
6395 * @prop {string} indexIndicatorSep
6396 * Used for slide count indicator ("1 of 10 ").
6397 *
6398 * @prop {(options: PhotoSwipeOptions, pswp: PhotoSwipeBase) => Point} [getViewportSizeFn]
6399 * A function that should return slide viewport width and height, in format {x: 100, y: 100}.
6400 *
6401 * @prop {string} errorMsg
6402 * Message to display when the image wasn't able to load. If you need to display HTML - use contentErrorElement filter.
6403 *
6404 * @prop {[number, number]} preload
6405 * Lazy loading of nearby slides based on direction of movement. Should be an array with two integers,
6406 * first one - number of items to preload before the current image, second one - after the current image.
6407 * Two nearby images are always loaded.
6408 *
6409 * @prop {string} [mainClass]
6410 * Class that will be added to the root element of PhotoSwipe, may contain multiple separated by space.
6411 * Example on Styling page.
6412 *
6413 * @prop {HTMLElement} [appendToEl]
6414 * Element to which PhotoSwipe dialog will be appended when it opens.
6415 *
6416 * @prop {number} maxWidthToAnimate
6417 * Maximum width of image to animate, if initial rendered image width
6418 * is larger than this value - the opening/closing transition will be automatically disabled.
6419 *
6420 * @prop {string} [closeTitle]
6421 * Translating
6422 *
6423 * @prop {string} [zoomTitle]
6424 * Translating
6425 *
6426 * @prop {string} [arrowPrevTitle]
6427 * Translating
6428 *
6429 * @prop {string} [arrowNextTitle]
6430 * Translating
6431 *
6432 * @prop {'zoom' | 'fade' | 'none'} [showHideAnimationType]
6433 * To adjust opening or closing transition type use lightbox option `showHideAnimationType` (`String`).
6434 * It supports three values - `zoom` (default), `fade` (default if there is no thumbnail) and `none`.
6435 *
6436 * Animations are automatically disabled if user `(prefers-reduced-motion: reduce)`.
6437 *
6438 * @prop {number} index
6439 * Defines start slide index.
6440 *
6441 * @prop {(e: MouseEvent) => number} [getClickedIndexFn]
6442 *
6443 * @prop {boolean} [arrowPrev]
6444 * @prop {boolean} [arrowNext]
6445 * @prop {boolean} [zoom]
6446 * @prop {boolean} [close]
6447 * @prop {boolean} [counter]
6448 *
6449 * @prop {string} [arrowPrevSVG]
6450 * @prop {string} [arrowNextSVG]
6451 * @prop {string} [zoomSVG]
6452 * @prop {string} [closeSVG]
6453 * @prop {string} [counterSVG]
6454 *
6455 * @prop {string} [arrowPrevTitle]
6456 * @prop {string} [arrowNextTitle]
6457 * @prop {string} [zoomTitle]
6458 * @prop {string} [closeTitle]
6459 * @prop {string} [counterTitle]
6460 *
6461 * @prop {ZoomLevelOption} [initialZoomLevel]
6462 * @prop {ZoomLevelOption} [secondaryZoomLevel]
6463 * @prop {ZoomLevelOption} [maxZoomLevel]
6464 *
6465 * @prop {boolean} [mouseMovePan]
6466 * @prop {Point | null} [initialPointerPos]
6467 * @prop {boolean} [showHideOpacity]
6468 *
6469 * @prop {PhotoSwipeModuleOption} [pswpModule]
6470 * @prop {() => Promise<any>} [openPromise]
6471 * @prop {boolean} [preloadFirstSlide]
6472 * @prop {ElementProvider} [gallery]
6473 * @prop {string} [gallerySelector]
6474 * @prop {ElementProvider} [children]
6475 * @prop {string} [childSelector]
6476 * @prop {string | false} [thumbSelector]
6477 */
6478
6479/** @type {PreparedPhotoSwipeOptions} */
6480
6481const defaultOptions = {
6482  allowPanToNext: true,
6483  spacing: 0.1,
6484  loop: true,
6485  pinchToClose: true,
6486  closeOnVerticalDrag: true,
6487  hideAnimationDuration: 333,
6488  showAnimationDuration: 333,
6489  zoomAnimationDuration: 333,
6490  escKey: true,
6491  arrowKeys: true,
6492  trapFocus: true,
6493  returnFocus: true,
6494  maxWidthToAnimate: 4000,
6495  clickToCloseNonZoomable: true,
6496  imageClickAction: 'zoom-or-close',
6497  bgClickAction: 'close',
vendor: 14,421 bytes, lines 6498-7081
6498  tapAction: 'toggle-controls',
6499  doubleTapAction: 'zoom',
6500  indexIndicatorSep: ' / ',
6501  preloaderDelay: 2000,
6502  bgOpacity: 0.8,
6503  index: 0,
6504  errorMsg: 'The image cannot be loaded',
6505  preload: [1, 2],
6506  easing: 'cubic-bezier(.4,0,.22,1)'
6507};
6508/**
6509 * PhotoSwipe Core
6510 */
6511
6512class PhotoSwipe extends PhotoSwipeBase {
6513  /**
6514   * @param {PhotoSwipeOptions} [options]
6515   */
6516  constructor(options) {
6517    super();
6518    this.options = this._prepareOptions(options || {});
6519    /**
6520     * offset of viewport relative to document
6521     *
6522     * @type {Point}
6523     */
6524
6525    this.offset = {
6526      x: 0,
6527      y: 0
6528    };
6529    /**
6530     * @type {Point}
6531     * @private
6532     */
6533
6534    this._prevViewportSize = {
6535      x: 0,
6536      y: 0
6537    };
6538    /**
6539     * Size of scrollable PhotoSwipe viewport
6540     *
6541     * @type {Point}
6542     */
6543
6544    this.viewportSize = {
6545      x: 0,
6546      y: 0
6547    };
6548    /**
6549     * background (backdrop) opacity
6550     */
6551
6552    this.bgOpacity = 1;
6553    this.currIndex = 0;
6554    this.potentialIndex = 0;
6555    this.isOpen = false;
6556    this.isDestroying = false;
6557    this.hasMouse = false;
6558    /**
6559     * @private
6560     * @type {SlideData}
6561     */
6562
6563    this._initialItemData = {};
6564    /** @type {Bounds | undefined} */
6565
6566    this._initialThumbBounds = undefined;
6567    /** @type {HTMLDivElement | undefined} */
6568
6569    this.topBar = undefined;
6570    /** @type {HTMLDivElement | undefined} */
6571
6572    this.element = undefined;
6573    /** @type {HTMLDivElement | undefined} */
6574
6575    this.template = undefined;
6576    /** @type {HTMLDivElement | undefined} */
6577
6578    this.container = undefined;
6579    /** @type {HTMLElement | undefined} */
6580
6581    this.scrollWrap = undefined;
6582    /** @type {Slide | undefined} */
6583
6584    this.currSlide = undefined;
6585    this.events = new DOMEvents();
6586    this.animations = new Animations();
6587    this.mainScroll = new MainScroll(this);
6588    this.gestures = new Gestures(this);
6589    this.opener = new Opener(this);
6590    this.keyboard = new Keyboard(this);
6591    this.contentLoader = new ContentLoader(this);
6592  }
6593  /** @returns {boolean} */
6594
6595
6596  init() {
6597    if (this.isOpen || this.isDestroying) {
6598      return false;
6599    }
6600
6601    this.isOpen = true;
6602    this.dispatch('init'); // legacy
6603
6604    this.dispatch('beforeOpen');
6605
6606    this._createMainStructure(); // add classes to the root element of PhotoSwipe
6607
6608
6609    let rootClasses = 'pswp--open';
6610
6611    if (this.gestures.supportsTouch) {
6612      rootClasses += ' pswp--touch';
6613    }
6614
6615    if (this.options.mainClass) {
6616      rootClasses += ' ' + this.options.mainClass;
6617    }
6618
6619    if (this.element) {
6620      this.element.className += ' ' + rootClasses;
6621    }
6622
6623    this.currIndex = this.options.index || 0;
6624    this.potentialIndex = this.currIndex;
6625    this.dispatch('firstUpdate'); // starting index can be modified here
6626    // initialize scroll wheel handler to block the scroll
6627
6628    this.scrollWheel = new ScrollWheel(this); // sanitize index
6629
6630    if (Number.isNaN(this.currIndex) || this.currIndex < 0 || this.currIndex >= this.getNumItems()) {
6631      this.currIndex = 0;
6632    }
6633
6634    if (!this.gestures.supportsTouch) {
6635      // enable mouse features if no touch support detected
6636      this.mouseDetected();
6637    } // causes forced synchronous layout
6638
6639
6640    this.updateSize();
6641    this.offset.y = window.pageYOffset;
6642    this._initialItemData = this.getItemData(this.currIndex);
6643    this.dispatch('gettingData', {
6644      index: this.currIndex,
6645      data: this._initialItemData,
6646      slide: undefined
6647    }); // *Layout* - calculate size and position of elements here
6648
6649    this._initialThumbBounds = this.getThumbBounds();
6650    this.dispatch('initialLayout');
6651    this.on('openingAnimationEnd', () => {
6652      const {
6653        itemHolders
6654      } = this.mainScroll; // Add content to the previous and next slide
6655
6656      if (itemHolders[0]) {
6657        itemHolders[0].el.style.display = 'block';
6658        this.setContent(itemHolders[0], this.currIndex - 1);
6659      }
6660
6661      if (itemHolders[2]) {
6662        itemHolders[2].el.style.display = 'block';
6663        this.setContent(itemHolders[2], this.currIndex + 1);
6664      }
6665
6666      this.appendHeavy();
6667      this.contentLoader.updateLazy();
6668      this.events.add(window, 'resize', this._handlePageResize.bind(this));
6669      this.events.add(window, 'scroll', this._updatePageScrollOffset.bind(this));
6670      this.dispatch('bindEvents');
6671    }); // set content for center slide (first time)
6672
6673    if (this.mainScroll.itemHolders[1]) {
6674      this.setContent(this.mainScroll.itemHolders[1], this.currIndex);
6675    }
6676
6677    this.dispatch('change');
6678    this.opener.open();
6679    this.dispatch('afterInit');
6680    return true;
6681  }
6682  /**
6683   * Get looped slide index
6684   * (for example, -1 will return the last slide)
6685   *
6686   * @param {number} index
6687   * @returns {number}
6688   */
6689
6690
6691  getLoopedIndex(index) {
6692    const numSlides = this.getNumItems();
6693
6694    if (this.options.loop) {
6695      if (index > numSlides - 1) {
6696        index -= numSlides;
6697      }
6698
6699      if (index < 0) {
6700        index += numSlides;
6701      }
6702    }
6703
6704    return clamp(index, 0, numSlides - 1);
6705  }
6706
6707  appendHeavy() {
6708    this.mainScroll.itemHolders.forEach(itemHolder => {
6709      var _itemHolder$slide;
6710
6711      (_itemHolder$slide = itemHolder.slide) === null || _itemHolder$slide === void 0 || _itemHolder$slide.appendHeavy();
6712    });
6713  }
6714  /**
6715   * Change the slide
6716   * @param {number} index New index
6717   */
6718
6719
6720  goTo(index) {
6721    this.mainScroll.moveIndexBy(this.getLoopedIndex(index) - this.potentialIndex);
6722  }
6723  /**
6724   * Go to the next slide.
6725   */
6726
6727
6728  next() {
6729    this.goTo(this.potentialIndex + 1);
6730  }
6731  /**
6732   * Go to the previous slide.
6733   */
6734
6735
6736  prev() {
6737    this.goTo(this.potentialIndex - 1);
6738  }
6739  /**
6740   * @see slide/slide.js zoomTo
6741   *
6742   * @param {Parameters<Slide['zoomTo']>} args
6743   */
6744
6745
6746  zoomTo(...args) {
6747    var _this$currSlide;
6748
6749    (_this$currSlide = this.currSlide) === null || _this$currSlide === void 0 || _this$currSlide.zoomTo(...args);
6750  }
6751  /**
6752   * @see slide/slide.js toggleZoom
6753   */
6754
6755
6756  toggleZoom() {
6757    var _this$currSlide2;
6758
6759    (_this$currSlide2 = this.currSlide) === null || _this$currSlide2 === void 0 || _this$currSlide2.toggleZoom();
6760  }
6761  /**
6762   * Close the gallery.
6763   * After closing transition ends - destroy it
6764   */
6765
6766
6767  close() {
6768    if (!this.opener.isOpen || this.isDestroying) {
6769      return;
6770    }
6771
6772    this.isDestroying = true;
6773    this.dispatch('close');
6774    this.events.removeAll();
6775    this.opener.close();
6776  }
6777  /**
6778   * Destroys the gallery:
6779   * - instantly closes the gallery
6780   * - unbinds events,
6781   * - cleans intervals and timeouts
6782   * - removes elements from DOM
6783   */
6784
6785
6786  destroy() {
6787    var _this$element;
6788
6789    if (!this.isDestroying) {
6790      this.options.showHideAnimationType = 'none';
6791      this.close();
6792      return;
6793    }
6794
6795    this.dispatch('destroy');
6796    this._listeners = {};
6797
6798    if (this.scrollWrap) {
6799      this.scrollWrap.ontouchmove = null;
6800      this.scrollWrap.ontouchend = null;
6801    }
6802
6803    (_this$element = this.element) === null || _this$element === void 0 || _this$element.remove();
6804    this.mainScroll.itemHolders.forEach(itemHolder => {
6805      var _itemHolder$slide2;
6806
6807      (_itemHolder$slide2 = itemHolder.slide) === null || _itemHolder$slide2 === void 0 || _itemHolder$slide2.destroy();
6808    });
6809    this.contentLoader.destroy();
6810    this.events.removeAll();
6811  }
6812  /**
6813   * Refresh/reload content of a slide by its index
6814   *
6815   * @param {number} slideIndex
6816   */
6817
6818
6819  refreshSlideContent(slideIndex) {
6820    this.contentLoader.removeByIndex(slideIndex);
6821    this.mainScroll.itemHolders.forEach((itemHolder, i) => {
6822      var _this$currSlide$index, _this$currSlide3;
6823
6824      let potentialHolderIndex = ((_this$currSlide$index = (_this$currSlide3 = this.currSlide) === null || _this$currSlide3 === void 0 ? void 0 : _this$currSlide3.index) !== null && _this$currSlide$index !== void 0 ? _this$currSlide$index : 0) - 1 + i;
6825
6826      if (this.canLoop()) {
6827        potentialHolderIndex = this.getLoopedIndex(potentialHolderIndex);
6828      }
6829
6830      if (potentialHolderIndex === slideIndex) {
6831        // set the new slide content
6832        this.setContent(itemHolder, slideIndex, true); // activate the new slide if it's current
6833
6834        if (i === 1) {
6835          var _itemHolder$slide3;
6836
6837          this.currSlide = itemHolder.slide;
6838          (_itemHolder$slide3 = itemHolder.slide) === null || _itemHolder$slide3 === void 0 || _itemHolder$slide3.setIsActive(true);
6839        }
6840      }
6841    });
6842    this.dispatch('change');
6843  }
6844  /**
6845   * Set slide content
6846   *
6847   * @param {ItemHolder} holder mainScroll.itemHolders array item
6848   * @param {number} index Slide index
6849   * @param {boolean} [force] If content should be set even if index wasn't changed
6850   */
6851
6852
6853  setContent(holder, index, force) {
6854    if (this.canLoop()) {
6855      index = this.getLoopedIndex(index);
6856    }
6857
6858    if (holder.slide) {
6859      if (holder.slide.index === index && !force) {
6860        // exit if holder already contains this slide
6861        // this could be common when just three slides are used
6862        return;
6863      } // destroy previous slide
6864
6865
6866      holder.slide.destroy();
6867      holder.slide = undefined;
6868    } // exit if no loop and index is out of bounds
6869
6870
6871    if (!this.canLoop() && (index < 0 || index >= this.getNumItems())) {
6872      return;
6873    }
6874
6875    const itemData = this.getItemData(index);
6876    holder.slide = new Slide(itemData, index, this); // set current slide
6877
6878    if (index === this.currIndex) {
6879      this.currSlide = holder.slide;
6880    }
6881
6882    holder.slide.append(holder.el);
6883  }
6884  /** @returns {Point} */
6885
6886
6887  getViewportCenterPoint() {
6888    return {
6889      x: this.viewportSize.x / 2,
6890      y: this.viewportSize.y / 2
6891    };
6892  }
6893  /**
6894   * Update size of all elements.
6895   * Executed on init and on page resize.
6896   *
6897   * @param {boolean} [force] Update size even if size of viewport was not changed.
6898   */
6899
6900
6901  updateSize(force) {
6902    // let item;
6903    // let itemIndex;
6904    if (this.isDestroying) {
6905      // exit if PhotoSwipe is closed or closing
6906      // (to avoid errors, as resize event might be delayed)
6907      return;
6908    } //const newWidth = this.scrollWrap.clientWidth;
6909    //const newHeight = this.scrollWrap.clientHeight;
6910
6911
6912    const newViewportSize = getViewportSize(this.options, this);
6913
6914    if (!force && pointsEqual(newViewportSize, this._prevViewportSize)) {
6915      // Exit if dimensions were not changed
6916      return;
6917    } //this._prevViewportSize.x = newWidth;
6918    //this._prevViewportSize.y = newHeight;
6919
6920
6921    equalizePoints(this._prevViewportSize, newViewportSize);
6922    this.dispatch('beforeResize');
6923    equalizePoints(this.viewportSize, this._prevViewportSize);
6924
6925    this._updatePageScrollOffset();
6926
6927    this.dispatch('viewportSize'); // Resize slides only after opener animation is finished
6928    // and don't re-calculate size on inital size update
6929
6930    this.mainScroll.resize(this.opener.isOpen);
6931
6932    if (!this.hasMouse && window.matchMedia('(any-hover: hover)').matches) {
6933      this.mouseDetected();
6934    }
6935
6936    this.dispatch('resize');
6937  }
6938  /**
6939   * @param {number} opacity
6940   */
6941
6942
6943  applyBgOpacity(opacity) {
6944    this.bgOpacity = Math.max(opacity, 0);
6945
6946    if (this.bg) {
6947      this.bg.style.opacity = String(this.bgOpacity * this.options.bgOpacity);
6948    }
6949  }
6950  /**
6951   * Whether mouse is detected
6952   */
6953
6954
6955  mouseDetected() {
6956    if (!this.hasMouse) {
6957      var _this$element2;
6958
6959      this.hasMouse = true;
6960      (_this$element2 = this.element) === null || _this$element2 === void 0 || _this$element2.classList.add('pswp--has_mouse');
6961    }
6962  }
6963  /**
6964   * Page resize event handler
6965   *
6966   * @private
6967   */
6968
6969
6970  _handlePageResize() {
6971    this.updateSize(); // In iOS webview, if element size depends on document size,
6972    // it'll be measured incorrectly in resize event
6973    //
6974    // https://bugs.webkit.org/show_bug.cgi?id=170595
6975    // https://hackernoon.com/onresize-event-broken-in-mobile-safari-d8469027bf4d
6976
6977    if (/iPhone|iPad|iPod/i.test(window.navigator.userAgent)) {
6978      setTimeout(() => {
6979        this.updateSize();
6980      }, 500);
6981    }
6982  }
6983  /**
6984   * Page scroll offset is used
6985   * to get correct coordinates
6986   * relative to PhotoSwipe viewport.
6987   *
6988   * @private
6989   */
6990
6991
6992  _updatePageScrollOffset() {
6993    this.setScrollOffset(0, window.pageYOffset);
6994  }
6995  /**
6996   * @param {number} x
6997   * @param {number} y
6998   */
6999
7000
7001  setScrollOffset(x, y) {
7002    this.offset.x = x;
7003    this.offset.y = y;
7004    this.dispatch('updateScrollOffset');
7005  }
7006  /**
7007   * Create main HTML structure of PhotoSwipe,
7008   * and add it to DOM
7009   *
7010   * @private
7011   */
7012
7013
7014  _createMainStructure() {
7015    // root DOM element of PhotoSwipe (.pswp)
7016    this.element = createElement('pswp', 'div');
7017    this.element.setAttribute('tabindex', '-1');
7018    this.element.setAttribute('role', 'dialog'); // template is legacy prop
7019
7020    this.template = this.element; // Background is added as a separate element,
7021    // as animating opacity is faster than animating rgba()
7022
7023    this.bg = createElement('pswp__bg', 'div', this.element);
7024    this.scrollWrap = createElement('pswp__scroll-wrap', 'section', this.element);
7025    this.container = createElement('pswp__container', 'div', this.scrollWrap); // aria pattern: carousel
7026
7027    this.scrollWrap.setAttribute('aria-roledescription', 'carousel');
7028    this.container.setAttribute('aria-live', 'off');
7029    this.container.setAttribute('id', 'pswp__items');
7030    this.mainScroll.appendHolders();
7031    this.ui = new UI(this);
7032    this.ui.init(); // append to DOM
7033
7034    (this.options.appendToEl || document.body).appendChild(this.element);
7035  }
7036  /**
7037   * Get position and dimensions of small thumbnail
7038   *   {x:,y:,w:}
7039   *
7040   * Height is optional (calculated based on the large image)
7041   *
7042   * @returns {Bounds | undefined}
7043   */
7044
7045
7046  getThumbBounds() {
7047    return getThumbBounds(this.currIndex, this.currSlide ? this.currSlide.data : this._initialItemData, this);
7048  }
7049  /**
7050   * If the PhotoSwipe can have continuous loop
7051   * @returns Boolean
7052   */
7053
7054
7055  canLoop() {
7056    return this.options.loop && this.getNumItems() > 2;
7057  }
7058  /**
7059   * @private
7060   * @param {PhotoSwipeOptions} options
7061   * @returns {PreparedPhotoSwipeOptions}
7062   */
7063
7064
7065  _prepareOptions(options) {
7066    if (window.matchMedia('(prefers-reduced-motion), (update: slow)').matches) {
7067      options.showHideAnimationType = 'none';
7068      options.zoomAnimationDuration = 0;
7069    }
7070    /** @type {PreparedPhotoSwipeOptions} */
7071
7072
7073    return { ...defaultOptions,
7074      ...options
7075    };
7076  }
7077
7078}
7079
7080export { PhotoSwipe as default };
7081//# sourceMappingURL=photoswipe.esm.js.map

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.