You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
807 lines
22 KiB
807 lines
22 KiB
import {
|
|
createElement,
|
|
equalizePoints,
|
|
pointsEqual,
|
|
clamp,
|
|
} from './util/util.js';
|
|
|
|
import DOMEvents from './util/dom-events.js';
|
|
import Slide from './slide/slide.js';
|
|
import Gestures from './gestures/gestures.js';
|
|
import MainScroll from './main-scroll.js';
|
|
|
|
import Keyboard from './keyboard.js';
|
|
import Animations from './util/animations.js';
|
|
import ScrollWheel from './scroll-wheel.js';
|
|
import UI from './ui/ui.js';
|
|
import { getViewportSize } from './util/viewport-size.js';
|
|
import { getThumbBounds } from './slide/get-thumb-bounds.js';
|
|
import PhotoSwipeBase from './core/base.js';
|
|
import Opener from './opener.js';
|
|
import ContentLoader from './slide/loader.js';
|
|
|
|
/**
|
|
* @template T
|
|
* @typedef {import('./types.js').Type<T>} Type<T>
|
|
*/
|
|
|
|
/** @typedef {import('./slide/slide.js').SlideData} SlideData */
|
|
/** @typedef {import('./slide/zoom-level.js').ZoomLevelOption} ZoomLevelOption */
|
|
/** @typedef {import('./ui/ui-element.js').UIElementData} UIElementData */
|
|
/** @typedef {import('./main-scroll.js').ItemHolder} ItemHolder */
|
|
/** @typedef {import('./core/eventable.js').PhotoSwipeEventsMap} PhotoSwipeEventsMap */
|
|
/** @typedef {import('./core/eventable.js').PhotoSwipeFiltersMap} PhotoSwipeFiltersMap */
|
|
/** @typedef {import('./slide/get-thumb-bounds').Bounds} Bounds */
|
|
/**
|
|
* @template {keyof PhotoSwipeEventsMap} T
|
|
* @typedef {import('./core/eventable.js').EventCallback<T>} EventCallback<T>
|
|
*/
|
|
/**
|
|
* @template {keyof PhotoSwipeEventsMap} T
|
|
* @typedef {import('./core/eventable.js').AugmentedEvent<T>} AugmentedEvent<T>
|
|
*/
|
|
|
|
/** @typedef {{ x: number; y: number; id?: string | number }} Point */
|
|
/** @typedef {{ top: number; bottom: number; left: number; right: number }} Padding */
|
|
/** @typedef {SlideData[]} DataSourceArray */
|
|
/** @typedef {{ gallery: HTMLElement; items?: HTMLElement[] }} DataSourceObject */
|
|
/** @typedef {DataSourceArray | DataSourceObject} DataSource */
|
|
/** @typedef {(point: Point, originalEvent: PointerEvent) => void} ActionFn */
|
|
/** @typedef {'close' | 'next' | 'zoom' | 'zoom-or-close' | 'toggle-controls'} ActionType */
|
|
/** @typedef {Type<PhotoSwipe> | { default: Type<PhotoSwipe> }} PhotoSwipeModule */
|
|
/** @typedef {PhotoSwipeModule | Promise<PhotoSwipeModule> | (() => Promise<PhotoSwipeModule>)} PhotoSwipeModuleOption */
|
|
|
|
/**
|
|
* @typedef {string | NodeListOf<HTMLElement> | HTMLElement[] | HTMLElement} ElementProvider
|
|
*/
|
|
|
|
/** @typedef {Partial<PreparedPhotoSwipeOptions>} PhotoSwipeOptions https://photoswipe.com/options/ */
|
|
/**
|
|
* @typedef {Object} PreparedPhotoSwipeOptions
|
|
*
|
|
* @prop {DataSource} [dataSource]
|
|
* Pass an array of any items via dataSource option. Its length will determine amount of slides
|
|
* (which may be modified further from numItems event).
|
|
*
|
|
* Each item should contain data that you need to generate slide
|
|
* (for image slide it would be src (image URL), width (image width), height, srcset, alt).
|
|
*
|
|
* If these properties are not present in your initial array, you may "pre-parse" each item from itemData filter.
|
|
*
|
|
* @prop {number} bgOpacity
|
|
* Background backdrop opacity, always define it via this option and not via CSS rgba color.
|
|
*
|
|
* @prop {number} spacing
|
|
* Spacing between slides. Defined as ratio relative to the viewport width (0.1 = 10% of viewport).
|
|
*
|
|
* @prop {boolean} allowPanToNext
|
|
* Allow swipe navigation to the next slide when the current slide is zoomed. Does not apply to mouse events.
|
|
*
|
|
* @prop {boolean} loop
|
|
* If set to true you'll be able to swipe from the last to the first image.
|
|
* Option is always false when there are less than 3 slides.
|
|
*
|
|
* @prop {boolean} [wheelToZoom]
|
|
* By default PhotoSwipe zooms image with ctrl-wheel, if you enable this option - image will zoom just via wheel.
|
|
*
|
|
* @prop {boolean} pinchToClose
|
|
* Pinch touch gesture to close the gallery.
|
|
*
|
|
* @prop {boolean} closeOnVerticalDrag
|
|
* Vertical drag gesture to close the PhotoSwipe.
|
|
*
|
|
* @prop {Padding} [padding]
|
|
* Slide area padding (in pixels).
|
|
*
|
|
* @prop {(viewportSize: Point, itemData: SlideData, index: number) => Padding} [paddingFn]
|
|
* The option is checked frequently, so make sure it's performant. Overrides padding option if defined. For example:
|
|
*
|
|
* @prop {number | false} hideAnimationDuration
|
|
* Transition duration in milliseconds, can be 0.
|
|
*
|
|
* @prop {number | false} showAnimationDuration
|
|
* Transition duration in milliseconds, can be 0.
|
|
*
|
|
* @prop {number | false} zoomAnimationDuration
|
|
* Transition duration in milliseconds, can be 0.
|
|
*
|
|
* @prop {string} easing
|
|
* String, 'cubic-bezier(.4,0,.22,1)'. CSS easing function for open/close/zoom transitions.
|
|
*
|
|
* @prop {boolean} escKey
|
|
* Esc key to close.
|
|
*
|
|
* @prop {boolean} arrowKeys
|
|
* Left/right arrow keys for navigation.
|
|
*
|
|
* @prop {boolean} trapFocus
|
|
* Trap focus within PhotoSwipe element while it's open.
|
|
*
|
|
* @prop {boolean} returnFocus
|
|
* Restore focus the last active element after PhotoSwipe is closed.
|
|
*
|
|
* @prop {boolean} clickToCloseNonZoomable
|
|
* If image is not zoomable (for example, smaller than viewport) it can be closed by clicking on it.
|
|
*
|
|
* @prop {ActionType | ActionFn | false} imageClickAction
|
|
* Refer to click and tap actions page.
|
|
*
|
|
* @prop {ActionType | ActionFn | false} bgClickAction
|
|
* Refer to click and tap actions page.
|
|
*
|
|
* @prop {ActionType | ActionFn | false} tapAction
|
|
* Refer to click and tap actions page.
|
|
*
|
|
* @prop {ActionType | ActionFn | false} doubleTapAction
|
|
* Refer to click and tap actions page.
|
|
*
|
|
* @prop {number} preloaderDelay
|
|
* Delay before the loading indicator will be displayed,
|
|
* if image is loaded during it - the indicator will not be displayed at all. Can be zero.
|
|
*
|
|
* @prop {string} indexIndicatorSep
|
|
* Used for slide count indicator ("1 of 10 ").
|
|
*
|
|
* @prop {(options: PhotoSwipeOptions, pswp: PhotoSwipeBase) => Point} [getViewportSizeFn]
|
|
* A function that should return slide viewport width and height, in format {x: 100, y: 100}.
|
|
*
|
|
* @prop {string} errorMsg
|
|
* Message to display when the image wasn't able to load. If you need to display HTML - use contentErrorElement filter.
|
|
*
|
|
* @prop {[number, number]} preload
|
|
* Lazy loading of nearby slides based on direction of movement. Should be an array with two integers,
|
|
* first one - number of items to preload before the current image, second one - after the current image.
|
|
* Two nearby images are always loaded.
|
|
*
|
|
* @prop {string} [mainClass]
|
|
* Class that will be added to the root element of PhotoSwipe, may contain multiple separated by space.
|
|
* Example on Styling page.
|
|
*
|
|
* @prop {HTMLElement} [appendToEl]
|
|
* Element to which PhotoSwipe dialog will be appended when it opens.
|
|
*
|
|
* @prop {number} maxWidthToAnimate
|
|
* Maximum width of image to animate, if initial rendered image width
|
|
* is larger than this value - the opening/closing transition will be automatically disabled.
|
|
*
|
|
* @prop {string} [closeTitle]
|
|
* Translating
|
|
*
|
|
* @prop {string} [zoomTitle]
|
|
* Translating
|
|
*
|
|
* @prop {string} [arrowPrevTitle]
|
|
* Translating
|
|
*
|
|
* @prop {string} [arrowNextTitle]
|
|
* Translating
|
|
*
|
|
* @prop {'zoom' | 'fade' | 'none'} [showHideAnimationType]
|
|
* To adjust opening or closing transition type use lightbox option `showHideAnimationType` (`String`).
|
|
* It supports three values - `zoom` (default), `fade` (default if there is no thumbnail) and `none`.
|
|
*
|
|
* Animations are automatically disabled if user `(prefers-reduced-motion: reduce)`.
|
|
*
|
|
* @prop {number} index
|
|
* Defines start slide index.
|
|
*
|
|
* @prop {(e: MouseEvent) => number} [getClickedIndexFn]
|
|
*
|
|
* @prop {boolean} [arrowPrev]
|
|
* @prop {boolean} [arrowNext]
|
|
* @prop {boolean} [zoom]
|
|
* @prop {boolean} [close]
|
|
* @prop {boolean} [counter]
|
|
*
|
|
* @prop {string} [arrowPrevSVG]
|
|
* @prop {string} [arrowNextSVG]
|
|
* @prop {string} [zoomSVG]
|
|
* @prop {string} [closeSVG]
|
|
* @prop {string} [counterSVG]
|
|
*
|
|
* @prop {string} [arrowPrevTitle]
|
|
* @prop {string} [arrowNextTitle]
|
|
* @prop {string} [zoomTitle]
|
|
* @prop {string} [closeTitle]
|
|
* @prop {string} [counterTitle]
|
|
*
|
|
* @prop {ZoomLevelOption} [initialZoomLevel]
|
|
* @prop {ZoomLevelOption} [secondaryZoomLevel]
|
|
* @prop {ZoomLevelOption} [maxZoomLevel]
|
|
*
|
|
* @prop {boolean} [mouseMovePan]
|
|
* @prop {Point | null} [initialPointerPos]
|
|
* @prop {boolean} [showHideOpacity]
|
|
*
|
|
* @prop {PhotoSwipeModuleOption} [pswpModule]
|
|
* @prop {() => Promise<any>} [openPromise]
|
|
* @prop {boolean} [preloadFirstSlide]
|
|
* @prop {ElementProvider} [gallery]
|
|
* @prop {string} [gallerySelector]
|
|
* @prop {ElementProvider} [children]
|
|
* @prop {string} [childSelector]
|
|
* @prop {string | false} [thumbSelector]
|
|
*/
|
|
|
|
/** @type {PreparedPhotoSwipeOptions} */
|
|
const defaultOptions = {
|
|
allowPanToNext: true,
|
|
spacing: 0.1,
|
|
loop: true,
|
|
pinchToClose: true,
|
|
closeOnVerticalDrag: true,
|
|
hideAnimationDuration: 333,
|
|
showAnimationDuration: 333,
|
|
zoomAnimationDuration: 333,
|
|
escKey: true,
|
|
arrowKeys: true,
|
|
trapFocus: true,
|
|
returnFocus: true,
|
|
maxWidthToAnimate: 4000,
|
|
clickToCloseNonZoomable: true,
|
|
imageClickAction: 'zoom-or-close',
|
|
bgClickAction: 'close',
|
|
tapAction: 'toggle-controls',
|
|
doubleTapAction: 'zoom',
|
|
indexIndicatorSep: ' / ',
|
|
preloaderDelay: 2000,
|
|
bgOpacity: 0.8,
|
|
|
|
index: 0,
|
|
errorMsg: 'The image cannot be loaded',
|
|
preload: [1, 2],
|
|
easing: 'cubic-bezier(.4,0,.22,1)'
|
|
};
|
|
|
|
/**
|
|
* PhotoSwipe Core
|
|
*/
|
|
class PhotoSwipe extends PhotoSwipeBase {
|
|
/**
|
|
* @param {PhotoSwipeOptions} [options]
|
|
*/
|
|
constructor(options) {
|
|
super();
|
|
|
|
this.options = this._prepareOptions(options || {});
|
|
|
|
/**
|
|
* offset of viewport relative to document
|
|
*
|
|
* @type {Point}
|
|
*/
|
|
this.offset = { x: 0, y: 0 };
|
|
|
|
/**
|
|
* @type {Point}
|
|
* @private
|
|
*/
|
|
this._prevViewportSize = { x: 0, y: 0 };
|
|
|
|
/**
|
|
* Size of scrollable PhotoSwipe viewport
|
|
*
|
|
* @type {Point}
|
|
*/
|
|
this.viewportSize = { x: 0, y: 0 };
|
|
|
|
/**
|
|
* background (backdrop) opacity
|
|
*/
|
|
this.bgOpacity = 1;
|
|
this.currIndex = 0;
|
|
this.potentialIndex = 0;
|
|
this.isOpen = false;
|
|
this.isDestroying = false;
|
|
this.hasMouse = false;
|
|
|
|
/**
|
|
* @private
|
|
* @type {SlideData}
|
|
*/
|
|
this._initialItemData = {};
|
|
/** @type {Bounds | undefined} */
|
|
this._initialThumbBounds = undefined;
|
|
|
|
/** @type {HTMLDivElement | undefined} */
|
|
this.topBar = undefined;
|
|
/** @type {HTMLDivElement | undefined} */
|
|
this.element = undefined;
|
|
/** @type {HTMLDivElement | undefined} */
|
|
this.template = undefined;
|
|
/** @type {HTMLDivElement | undefined} */
|
|
this.container = undefined;
|
|
/** @type {HTMLElement | undefined} */
|
|
this.scrollWrap = undefined;
|
|
/** @type {Slide | undefined} */
|
|
this.currSlide = undefined;
|
|
|
|
this.events = new DOMEvents();
|
|
this.animations = new Animations();
|
|
this.mainScroll = new MainScroll(this);
|
|
this.gestures = new Gestures(this);
|
|
this.opener = new Opener(this);
|
|
this.keyboard = new Keyboard(this);
|
|
this.contentLoader = new ContentLoader(this);
|
|
}
|
|
|
|
/** @returns {boolean} */
|
|
init() {
|
|
if (this.isOpen || this.isDestroying) {
|
|
return false;
|
|
}
|
|
|
|
this.isOpen = true;
|
|
this.dispatch('init'); // legacy
|
|
this.dispatch('beforeOpen');
|
|
|
|
this._createMainStructure();
|
|
|
|
// add classes to the root element of PhotoSwipe
|
|
let rootClasses = 'pswp--open';
|
|
if (this.gestures.supportsTouch) {
|
|
rootClasses += ' pswp--touch';
|
|
}
|
|
if (this.options.mainClass) {
|
|
rootClasses += ' ' + this.options.mainClass;
|
|
}
|
|
if (this.element) {
|
|
this.element.className += ' ' + rootClasses;
|
|
}
|
|
|
|
this.currIndex = this.options.index || 0;
|
|
this.potentialIndex = this.currIndex;
|
|
this.dispatch('firstUpdate'); // starting index can be modified here
|
|
|
|
// initialize scroll wheel handler to block the scroll
|
|
this.scrollWheel = new ScrollWheel(this);
|
|
|
|
// sanitize index
|
|
if (Number.isNaN(this.currIndex)
|
|
|| this.currIndex < 0
|
|
|| this.currIndex >= this.getNumItems()) {
|
|
this.currIndex = 0;
|
|
}
|
|
|
|
if (!this.gestures.supportsTouch) {
|
|
// enable mouse features if no touch support detected
|
|
this.mouseDetected();
|
|
}
|
|
|
|
// causes forced synchronous layout
|
|
this.updateSize();
|
|
|
|
this.offset.y = window.pageYOffset;
|
|
|
|
this._initialItemData = this.getItemData(this.currIndex);
|
|
this.dispatch('gettingData', {
|
|
index: this.currIndex,
|
|
data: this._initialItemData,
|
|
slide: undefined
|
|
});
|
|
|
|
// *Layout* - calculate size and position of elements here
|
|
this._initialThumbBounds = this.getThumbBounds();
|
|
this.dispatch('initialLayout');
|
|
|
|
this.on('openingAnimationEnd', () => {
|
|
const { itemHolders } = this.mainScroll;
|
|
|
|
// Add content to the previous and next slide
|
|
if (itemHolders[0]) {
|
|
itemHolders[0].el.style.display = 'block';
|
|
this.setContent(itemHolders[0], this.currIndex - 1);
|
|
}
|
|
if (itemHolders[2]) {
|
|
itemHolders[2].el.style.display = 'block';
|
|
this.setContent(itemHolders[2], this.currIndex + 1);
|
|
}
|
|
|
|
this.appendHeavy();
|
|
|
|
this.contentLoader.updateLazy();
|
|
|
|
this.events.add(window, 'resize', this._handlePageResize.bind(this));
|
|
this.events.add(window, 'scroll', this._updatePageScrollOffset.bind(this));
|
|
this.dispatch('bindEvents');
|
|
});
|
|
|
|
// set content for center slide (first time)
|
|
if (this.mainScroll.itemHolders[1]) {
|
|
this.setContent(this.mainScroll.itemHolders[1], this.currIndex);
|
|
}
|
|
this.dispatch('change');
|
|
|
|
this.opener.open();
|
|
|
|
this.dispatch('afterInit');
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get looped slide index
|
|
* (for example, -1 will return the last slide)
|
|
*
|
|
* @param {number} index
|
|
* @returns {number}
|
|
*/
|
|
getLoopedIndex(index) {
|
|
const numSlides = this.getNumItems();
|
|
|
|
if (this.options.loop) {
|
|
if (index > numSlides - 1) {
|
|
index -= numSlides;
|
|
}
|
|
|
|
if (index < 0) {
|
|
index += numSlides;
|
|
}
|
|
}
|
|
|
|
return clamp(index, 0, numSlides - 1);
|
|
}
|
|
|
|
appendHeavy() {
|
|
this.mainScroll.itemHolders.forEach((itemHolder) => {
|
|
itemHolder.slide?.appendHeavy();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Change the slide
|
|
* @param {number} index New index
|
|
*/
|
|
goTo(index) {
|
|
this.mainScroll.moveIndexBy(
|
|
this.getLoopedIndex(index) - this.potentialIndex
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Go to the next slide.
|
|
*/
|
|
next() {
|
|
this.goTo(this.potentialIndex + 1);
|
|
}
|
|
|
|
/**
|
|
* Go to the previous slide.
|
|
*/
|
|
prev() {
|
|
this.goTo(this.potentialIndex - 1);
|
|
}
|
|
|
|
/**
|
|
* @see slide/slide.js zoomTo
|
|
*
|
|
* @param {Parameters<Slide['zoomTo']>} args
|
|
*/
|
|
zoomTo(...args) {
|
|
this.currSlide?.zoomTo(...args);
|
|
}
|
|
|
|
/**
|
|
* @see slide/slide.js toggleZoom
|
|
*/
|
|
toggleZoom() {
|
|
this.currSlide?.toggleZoom();
|
|
}
|
|
|
|
/**
|
|
* Close the gallery.
|
|
* After closing transition ends - destroy it
|
|
*/
|
|
close() {
|
|
if (!this.opener.isOpen || this.isDestroying) {
|
|
return;
|
|
}
|
|
|
|
this.isDestroying = true;
|
|
|
|
this.dispatch('close');
|
|
|
|
this.events.removeAll();
|
|
this.opener.close();
|
|
}
|
|
|
|
/**
|
|
* Destroys the gallery:
|
|
* - instantly closes the gallery
|
|
* - unbinds events,
|
|
* - cleans intervals and timeouts
|
|
* - removes elements from DOM
|
|
*/
|
|
destroy() {
|
|
if (!this.isDestroying) {
|
|
this.options.showHideAnimationType = 'none';
|
|
this.close();
|
|
return;
|
|
}
|
|
|
|
this.dispatch('destroy');
|
|
|
|
this._listeners = {};
|
|
|
|
if (this.scrollWrap) {
|
|
this.scrollWrap.ontouchmove = null;
|
|
this.scrollWrap.ontouchend = null;
|
|
}
|
|
|
|
this.element?.remove();
|
|
|
|
this.mainScroll.itemHolders.forEach((itemHolder) => {
|
|
itemHolder.slide?.destroy();
|
|
});
|
|
|
|
this.contentLoader.destroy();
|
|
this.events.removeAll();
|
|
}
|
|
|
|
/**
|
|
* Refresh/reload content of a slide by its index
|
|
*
|
|
* @param {number} slideIndex
|
|
*/
|
|
refreshSlideContent(slideIndex) {
|
|
this.contentLoader.removeByIndex(slideIndex);
|
|
this.mainScroll.itemHolders.forEach((itemHolder, i) => {
|
|
let potentialHolderIndex = (this.currSlide?.index ?? 0) - 1 + i;
|
|
if (this.canLoop()) {
|
|
potentialHolderIndex = this.getLoopedIndex(potentialHolderIndex);
|
|
}
|
|
if (potentialHolderIndex === slideIndex) {
|
|
// set the new slide content
|
|
this.setContent(itemHolder, slideIndex, true);
|
|
|
|
// activate the new slide if it's current
|
|
if (i === 1) {
|
|
this.currSlide = itemHolder.slide;
|
|
itemHolder.slide?.setIsActive(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
this.dispatch('change');
|
|
}
|
|
|
|
|
|
/**
|
|
* Set slide content
|
|
*
|
|
* @param {ItemHolder} holder mainScroll.itemHolders array item
|
|
* @param {number} index Slide index
|
|
* @param {boolean} [force] If content should be set even if index wasn't changed
|
|
*/
|
|
setContent(holder, index, force) {
|
|
if (this.canLoop()) {
|
|
index = this.getLoopedIndex(index);
|
|
}
|
|
|
|
if (holder.slide) {
|
|
if (holder.slide.index === index && !force) {
|
|
// exit if holder already contains this slide
|
|
// this could be common when just three slides are used
|
|
return;
|
|
}
|
|
|
|
// destroy previous slide
|
|
holder.slide.destroy();
|
|
holder.slide = undefined;
|
|
}
|
|
|
|
// exit if no loop and index is out of bounds
|
|
if (!this.canLoop() && (index < 0 || index >= this.getNumItems())) {
|
|
return;
|
|
}
|
|
|
|
const itemData = this.getItemData(index);
|
|
holder.slide = new Slide(itemData, index, this);
|
|
|
|
// set current slide
|
|
if (index === this.currIndex) {
|
|
this.currSlide = holder.slide;
|
|
}
|
|
|
|
holder.slide.append(holder.el);
|
|
}
|
|
|
|
/** @returns {Point} */
|
|
getViewportCenterPoint() {
|
|
return {
|
|
x: this.viewportSize.x / 2,
|
|
y: this.viewportSize.y / 2
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Update size of all elements.
|
|
* Executed on init and on page resize.
|
|
*
|
|
* @param {boolean} [force] Update size even if size of viewport was not changed.
|
|
*/
|
|
updateSize(force) {
|
|
// let item;
|
|
// let itemIndex;
|
|
|
|
if (this.isDestroying) {
|
|
// exit if PhotoSwipe is closed or closing
|
|
// (to avoid errors, as resize event might be delayed)
|
|
return;
|
|
}
|
|
|
|
//const newWidth = this.scrollWrap.clientWidth;
|
|
//const newHeight = this.scrollWrap.clientHeight;
|
|
|
|
const newViewportSize = getViewportSize(this.options, this);
|
|
|
|
if (!force && pointsEqual(newViewportSize, this._prevViewportSize)) {
|
|
// Exit if dimensions were not changed
|
|
return;
|
|
}
|
|
|
|
//this._prevViewportSize.x = newWidth;
|
|
//this._prevViewportSize.y = newHeight;
|
|
equalizePoints(this._prevViewportSize, newViewportSize);
|
|
|
|
this.dispatch('beforeResize');
|
|
|
|
equalizePoints(this.viewportSize, this._prevViewportSize);
|
|
|
|
this._updatePageScrollOffset();
|
|
|
|
this.dispatch('viewportSize');
|
|
|
|
// Resize slides only after opener animation is finished
|
|
// and don't re-calculate size on inital size update
|
|
this.mainScroll.resize(this.opener.isOpen);
|
|
|
|
if (!this.hasMouse && window.matchMedia('(any-hover: hover)').matches) {
|
|
this.mouseDetected();
|
|
}
|
|
|
|
this.dispatch('resize');
|
|
}
|
|
|
|
/**
|
|
* @param {number} opacity
|
|
*/
|
|
applyBgOpacity(opacity) {
|
|
this.bgOpacity = Math.max(opacity, 0);
|
|
if (this.bg) {
|
|
this.bg.style.opacity = String(this.bgOpacity * this.options.bgOpacity);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether mouse is detected
|
|
*/
|
|
mouseDetected() {
|
|
if (!this.hasMouse) {
|
|
this.hasMouse = true;
|
|
this.element?.classList.add('pswp--has_mouse');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Page resize event handler
|
|
*
|
|
* @private
|
|
*/
|
|
_handlePageResize() {
|
|
this.updateSize();
|
|
|
|
// In iOS webview, if element size depends on document size,
|
|
// it'll be measured incorrectly in resize event
|
|
//
|
|
// https://bugs.webkit.org/show_bug.cgi?id=170595
|
|
// https://hackernoon.com/onresize-event-broken-in-mobile-safari-d8469027bf4d
|
|
if (/iPhone|iPad|iPod/i.test(window.navigator.userAgent)) {
|
|
setTimeout(() => {
|
|
this.updateSize();
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Page scroll offset is used
|
|
* to get correct coordinates
|
|
* relative to PhotoSwipe viewport.
|
|
*
|
|
* @private
|
|
*/
|
|
_updatePageScrollOffset() {
|
|
this.setScrollOffset(0, window.pageYOffset);
|
|
}
|
|
|
|
/**
|
|
* @param {number} x
|
|
* @param {number} y
|
|
*/
|
|
setScrollOffset(x, y) {
|
|
this.offset.x = x;
|
|
this.offset.y = y;
|
|
this.dispatch('updateScrollOffset');
|
|
}
|
|
|
|
/**
|
|
* Create main HTML structure of PhotoSwipe,
|
|
* and add it to DOM
|
|
*
|
|
* @private
|
|
*/
|
|
_createMainStructure() {
|
|
// root DOM element of PhotoSwipe (.pswp)
|
|
this.element = createElement('pswp', 'div');
|
|
this.element.setAttribute('tabindex', '-1');
|
|
this.element.setAttribute('role', 'dialog');
|
|
|
|
// template is legacy prop
|
|
this.template = this.element;
|
|
|
|
// Background is added as a separate element,
|
|
// as animating opacity is faster than animating rgba()
|
|
this.bg = createElement('pswp__bg', 'div', this.element);
|
|
this.scrollWrap = createElement('pswp__scroll-wrap', 'section', this.element);
|
|
this.container = createElement('pswp__container', 'div', this.scrollWrap);
|
|
|
|
// aria pattern: carousel
|
|
this.scrollWrap.setAttribute('aria-roledescription', 'carousel');
|
|
this.container.setAttribute('aria-live', 'off');
|
|
this.container.setAttribute('id', 'pswp__items');
|
|
|
|
this.mainScroll.appendHolders();
|
|
|
|
this.ui = new UI(this);
|
|
this.ui.init();
|
|
|
|
// append to DOM
|
|
(this.options.appendToEl || document.body).appendChild(this.element);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get position and dimensions of small thumbnail
|
|
* {x:,y:,w:}
|
|
*
|
|
* Height is optional (calculated based on the large image)
|
|
*
|
|
* @returns {Bounds | undefined}
|
|
*/
|
|
getThumbBounds() {
|
|
return getThumbBounds(
|
|
this.currIndex,
|
|
this.currSlide ? this.currSlide.data : this._initialItemData,
|
|
this
|
|
);
|
|
}
|
|
|
|
/**
|
|
* If the PhotoSwipe can have continuous loop
|
|
* @returns Boolean
|
|
*/
|
|
canLoop() {
|
|
return (this.options.loop && this.getNumItems() > 2);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {PhotoSwipeOptions} options
|
|
* @returns {PreparedPhotoSwipeOptions}
|
|
*/
|
|
_prepareOptions(options) {
|
|
if (window.matchMedia('(prefers-reduced-motion), (update: slow)').matches) {
|
|
options.showHideAnimationType = 'none';
|
|
options.zoomAnimationDuration = 0;
|
|
}
|
|
|
|
/** @type {PreparedPhotoSwipeOptions} */
|
|
return {
|
|
...defaultOptions,
|
|
...options
|
|
};
|
|
}
|
|
}
|
|
|
|
export default PhotoSwipe;
|