1155 lines
28 KiB
JavaScript
1155 lines
28 KiB
JavaScript
/*!
|
|
* PhotoSwipe Lightbox 5.1.8 - https://photoswipe.com
|
|
* (c) 2022 Dmitry Semenov
|
|
*/
|
|
/**
|
|
* Creates element and optionally appends it to another.
|
|
*
|
|
* @param {String} className
|
|
* @param {String|NULL} tagName
|
|
* @param {Element|NULL} appendToEl
|
|
*/
|
|
function createElement(className, tagName, appendToEl) {
|
|
const el = document.createElement(tagName || 'div');
|
|
if (className) {
|
|
el.className = className;
|
|
}
|
|
if (appendToEl) {
|
|
appendToEl.appendChild(el);
|
|
}
|
|
return el;
|
|
}
|
|
|
|
/**
|
|
* Apply width and height CSS properties to element
|
|
*/
|
|
function setWidthHeight(el, w, h) {
|
|
el.style.width = (typeof w === 'number') ? (w + 'px') : w;
|
|
el.style.height = (typeof h === 'number') ? (h + 'px') : h;
|
|
}
|
|
|
|
const LOAD_STATE = {
|
|
IDLE: 'idle',
|
|
LOADING: 'loading',
|
|
LOADED: 'loaded',
|
|
ERROR: 'error',
|
|
};
|
|
|
|
|
|
/**
|
|
* Check if click or keydown event was dispatched
|
|
* with a special key or via mouse wheel.
|
|
*
|
|
* @param {Event} e
|
|
*/
|
|
function specialKeyUsed(e) {
|
|
if (e.which === 2 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse `gallery` or `children` options.
|
|
*
|
|
* @param {Element|NodeList|String} option
|
|
* @param {String|null} legacySelector
|
|
* @param {Element|null} parent
|
|
* @returns Element[]
|
|
*/
|
|
function getElementsFromOption(option, legacySelector, parent = document) {
|
|
let elements = [];
|
|
|
|
if (option instanceof Element) {
|
|
elements = [option];
|
|
} else if (option instanceof NodeList || Array.isArray(option)) {
|
|
elements = Array.from(option);
|
|
} else {
|
|
const selector = typeof option === 'string' ? option : legacySelector;
|
|
if (selector) {
|
|
elements = Array.from(parent.querySelectorAll(selector));
|
|
}
|
|
}
|
|
|
|
return elements;
|
|
}
|
|
|
|
function dynamicImportModule(module) {
|
|
return typeof module === 'string' ? import(/* webpackIgnore: true */ module) : module;
|
|
}
|
|
|
|
/**
|
|
* Base PhotoSwipe event object
|
|
*/
|
|
class PhotoSwipeEvent {
|
|
constructor(type, details) {
|
|
this.type = type;
|
|
if (details) {
|
|
Object.assign(this, details);
|
|
}
|
|
}
|
|
|
|
preventDefault() {
|
|
this.defaultPrevented = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PhotoSwipe base class that can listen and dispatch for events.
|
|
* Shared by PhotoSwipe Core and PhotoSwipe Lightbox, extended by base.js
|
|
*/
|
|
class Eventable {
|
|
constructor() {
|
|
this._listeners = {};
|
|
this._filters = {};
|
|
}
|
|
|
|
addFilter(name, fn, priority = 100) {
|
|
if (!this._filters[name]) {
|
|
this._filters[name] = [];
|
|
}
|
|
|
|
this._filters[name].push({ fn, priority });
|
|
this._filters[name].sort((f1, f2) => f1.priority - f2.priority);
|
|
|
|
if (this.pswp) {
|
|
this.pswp.addFilter(name, fn, priority);
|
|
}
|
|
}
|
|
|
|
removeFilter(name, fn) {
|
|
if (this._filters[name]) {
|
|
this._filters[name] = this._filters[name].filter(filter => (filter.fn !== fn));
|
|
}
|
|
|
|
if (this.pswp) {
|
|
this.pswp.removeFilter(name, fn);
|
|
}
|
|
}
|
|
|
|
applyFilters(name, ...args) {
|
|
if (this._filters[name]) {
|
|
this._filters[name].forEach((filter) => {
|
|
args[0] = filter.fn.apply(this, args);
|
|
});
|
|
}
|
|
return args[0];
|
|
}
|
|
|
|
on(name, fn) {
|
|
if (!this._listeners[name]) {
|
|
this._listeners[name] = [];
|
|
}
|
|
this._listeners[name].push(fn);
|
|
|
|
// When binding events to lightbox,
|
|
// also bind events to PhotoSwipe Core,
|
|
// if it's open.
|
|
if (this.pswp) {
|
|
this.pswp.on(name, fn);
|
|
}
|
|
}
|
|
|
|
off(name, fn) {
|
|
if (this._listeners[name]) {
|
|
this._listeners[name] = this._listeners[name].filter(listener => (fn !== listener));
|
|
}
|
|
|
|
if (this.pswp) {
|
|
this.pswp.off(name, fn);
|
|
}
|
|
}
|
|
|
|
dispatch(name, details) {
|
|
if (this.pswp) {
|
|
return this.pswp.dispatch(name, details);
|
|
}
|
|
|
|
const event = new PhotoSwipeEvent(name, details);
|
|
|
|
if (!this._listeners) {
|
|
return event;
|
|
}
|
|
|
|
if (this._listeners[name]) {
|
|
this._listeners[name].forEach((listener) => {
|
|
listener.call(this, event);
|
|
});
|
|
}
|
|
|
|
return event;
|
|
}
|
|
}
|
|
|
|
class Content {
|
|
/**
|
|
* @param {Object} itemData Slide data
|
|
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
|
|
* @param {Slide|undefined} slide Slide that requested the image,
|
|
* can be undefined if image was requested by something else
|
|
* (for example by lazy-loader)
|
|
*/
|
|
constructor(itemData, instance) {
|
|
this.options = instance.options;
|
|
this.instance = instance;
|
|
this.data = itemData;
|
|
|
|
this.width = Number(this.data.w) || Number(this.data.width) || 0;
|
|
this.height = Number(this.data.h) || Number(this.data.height) || 0;
|
|
|
|
this.isAttached = false;
|
|
this.state = LOAD_STATE.IDLE;
|
|
}
|
|
|
|
setSlide(slide) {
|
|
this.slide = slide;
|
|
this.pswp = slide.pswp;
|
|
}
|
|
|
|
/**
|
|
* Load the content
|
|
*
|
|
* @param {Boolean} isLazy If method is executed by lazy-loader
|
|
*/
|
|
load(/* isLazy */) {
|
|
if (!this.element) {
|
|
this.element = createElement('pswp__content');
|
|
this.element.style.position = 'absolute';
|
|
this.element.style.left = 0;
|
|
this.element.style.top = 0;
|
|
this.element.innerHTML = this.data.html || '';
|
|
}
|
|
}
|
|
|
|
isZoomable() {
|
|
return false;
|
|
}
|
|
|
|
usePlaceholder() {
|
|
return false;
|
|
}
|
|
|
|
activate() {
|
|
|
|
}
|
|
|
|
deactivate() {
|
|
|
|
}
|
|
|
|
setDisplayedSize(width, height) {
|
|
if (this.element) {
|
|
setWidthHeight(this.element, width, height);
|
|
}
|
|
}
|
|
|
|
onLoaded() {
|
|
this.state = LOAD_STATE.LOADED;
|
|
|
|
if (this.slide) {
|
|
this.pswp.dispatch('loadComplete', { slide: this.slide });
|
|
}
|
|
}
|
|
|
|
isLoading() {
|
|
return (this.state === LOAD_STATE.LOADING);
|
|
}
|
|
|
|
// If the placeholder should be kept in DOM
|
|
keepPlaceholder() {
|
|
return this.isLoading();
|
|
}
|
|
|
|
onError() {
|
|
this.state = LOAD_STATE.ERROR;
|
|
|
|
if (this.slide) {
|
|
this.pswp.dispatch('loadComplete', { slide: this.slide, isError: true });
|
|
this.pswp.dispatch('loadError', { slide: this.slide });
|
|
}
|
|
}
|
|
|
|
getErrorElement() {
|
|
return false;
|
|
}
|
|
|
|
|
|
remove() {
|
|
this.isAttached = false;
|
|
if (this.element && this.element.parentNode) {
|
|
this.element.remove();
|
|
}
|
|
}
|
|
|
|
appendTo(container) {
|
|
this.isAttached = true;
|
|
if (this.element && !this.element.parentNode) {
|
|
container.appendChild(this.element);
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
|
|
}
|
|
}
|
|
|
|
class ImageContent extends Content {
|
|
load(/* isLazy */) {
|
|
if (this.element) {
|
|
return;
|
|
}
|
|
|
|
const imageSrc = this.data.src;
|
|
|
|
if (!imageSrc) {
|
|
return;
|
|
}
|
|
|
|
this.element = createElement('pswp__img', 'img');
|
|
|
|
if (this.data.srcset) {
|
|
this.element.srcset = this.data.srcset;
|
|
}
|
|
|
|
this.element.src = imageSrc;
|
|
|
|
this.element.alt = this.data.alt || '';
|
|
|
|
this.state = LOAD_STATE.LOADING;
|
|
|
|
if (this.element.complete) {
|
|
this.onLoaded();
|
|
} else {
|
|
this.element.onload = () => {
|
|
this.onLoaded();
|
|
};
|
|
|
|
this.element.onerror = () => {
|
|
this.onError();
|
|
};
|
|
}
|
|
}
|
|
|
|
setDisplayedSize(width, height) {
|
|
const image = this.element;
|
|
if (image) {
|
|
setWidthHeight(image, width, 'auto');
|
|
|
|
// Handle srcset sizes attribute.
|
|
//
|
|
// Never lower quality, if it was increased previously.
|
|
// Chrome does this automatically, Firefox and Safari do not,
|
|
// so we store largest used size in dataset.
|
|
if (image.srcset
|
|
&& (!image.dataset.largestUsedSize || width > image.dataset.largestUsedSize)) {
|
|
image.sizes = width + 'px';
|
|
image.dataset.largestUsedSize = width;
|
|
}
|
|
|
|
if (this.slide) {
|
|
this.pswp.dispatch('imageSizeChange', { slide: this.slide, width, height });
|
|
}
|
|
}
|
|
}
|
|
|
|
isZoomable() {
|
|
return (this.state !== LOAD_STATE.ERROR);
|
|
}
|
|
|
|
usePlaceholder() {
|
|
return true;
|
|
}
|
|
|
|
lazyLoad() {
|
|
this.load();
|
|
}
|
|
|
|
destroy() {
|
|
if (this.element) {
|
|
this.element.onload = null;
|
|
this.element.onerror = null;
|
|
this.element = null;
|
|
}
|
|
}
|
|
|
|
appendTo(container) {
|
|
this.isAttached = true;
|
|
|
|
// Use decode() on nearby slides
|
|
//
|
|
// Nearby slide images are in DOM and not hidden via display:none.
|
|
// However, they are placed offscreen (to the left and right side).
|
|
//
|
|
// Some browsers do not composite the image until it's actually visible,
|
|
// using decode() helps.
|
|
//
|
|
// You might ask "why dont you just decode() and then append all images",
|
|
// that's because I want to show image before it's fully loaded,
|
|
// as browser can render parts of image while it is loading.
|
|
if (this.slide && !this.slide.isActive && ('decode' in this.element)) {
|
|
this.isDecoding = true;
|
|
// Make sure that we start decoding on the next frame
|
|
requestAnimationFrame(() => {
|
|
if (this.element) {
|
|
this.element.decode().then(() => {
|
|
this.isDecoding = false;
|
|
requestAnimationFrame(() => {
|
|
this.appendImageTo(container);
|
|
});
|
|
}).catch(() => {});
|
|
}
|
|
});
|
|
} else {
|
|
this.appendImageTo(container);
|
|
}
|
|
}
|
|
|
|
activate() {
|
|
if (this.slide && this.slide.container && this.isDecoding) {
|
|
// add image to slide when it becomes active,
|
|
// even if it's not finished decoding
|
|
this.appendImageTo(this.slide.container);
|
|
}
|
|
}
|
|
|
|
getErrorElement() {
|
|
const el = createElement('pswp__error-msg-container');
|
|
el.innerHTML = this.options.errorMsg;
|
|
const linkEl = el.querySelector('a');
|
|
if (linkEl) {
|
|
linkEl.href = this.data.src;
|
|
}
|
|
return el;
|
|
}
|
|
|
|
appendImageTo(container) {
|
|
// ensure that element exists and is not already appended
|
|
if (this.element && !this.element.parentNode && this.isAttached) {
|
|
container.appendChild(this.element);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PhotoSwipe base class that can retrieve data about every slide.
|
|
* Shared by PhotoSwipe Core and PhotoSwipe Lightbox
|
|
*/
|
|
|
|
class PhotoSwipeBase extends Eventable {
|
|
constructor() {
|
|
super();
|
|
this.contentTypes = {
|
|
image: ImageContent,
|
|
html: Content
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get total number of slides
|
|
*/
|
|
getNumItems() {
|
|
let numItems;
|
|
const { dataSource } = this.options;
|
|
if (!dataSource) {
|
|
numItems = 0;
|
|
} else if (dataSource.length) {
|
|
// may be an array or just object with length property
|
|
numItems = dataSource.length;
|
|
} else if (dataSource.gallery) {
|
|
// query DOM elements
|
|
if (!dataSource.items) {
|
|
dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
|
|
}
|
|
|
|
if (dataSource.items) {
|
|
numItems = dataSource.items.length;
|
|
}
|
|
}
|
|
|
|
// legacy event, before filters were introduced
|
|
const event = this.dispatch('numItems', {
|
|
dataSource,
|
|
numItems
|
|
});
|
|
return this.applyFilters('numItems', event.numItems, dataSource);
|
|
}
|
|
|
|
/**
|
|
* Add or set slide content type
|
|
*
|
|
* @param {String} type
|
|
* @param {Class} ContentClass
|
|
*/
|
|
addContentType(type, ContentClass) {
|
|
this.contentTypes[type] = ContentClass;
|
|
}
|
|
|
|
/**
|
|
* Get slide content class based on its data
|
|
*
|
|
* @param {Object} slideData
|
|
* @param {Integer} slideIndex
|
|
* @returns Class
|
|
*/
|
|
getContentClass(slideData) {
|
|
if (slideData.type) {
|
|
return this.contentTypes[slideData.type];
|
|
} else if (slideData.src) {
|
|
return this.contentTypes.image;
|
|
} else if (slideData.html) {
|
|
return this.contentTypes.html;
|
|
}
|
|
}
|
|
|
|
createContentFromData(slideData) {
|
|
const ContentClass = this.getContentClass(slideData);
|
|
if (!ContentClass) {
|
|
return false;
|
|
}
|
|
const content = new ContentClass(slideData, this);
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* Get item data by index.
|
|
*
|
|
* "item data" should contain normalized information that PhotoSwipe needs to generate a slide.
|
|
* For example, it may contain properties like
|
|
* `src`, `srcset`, `w`, `h`, which will be used to generate a slide with image.
|
|
*
|
|
* @param {Integer} index
|
|
*/
|
|
getItemData(index) {
|
|
const { dataSource } = this.options;
|
|
let dataSourceItem;
|
|
if (Array.isArray(dataSource)) {
|
|
// Datasource is an array of elements
|
|
dataSourceItem = dataSource[index];
|
|
} else if (dataSource && dataSource.gallery) {
|
|
// dataSource has gallery property,
|
|
// thus it was created by Lightbox, based on
|
|
// gallerySelecor and childSelector options
|
|
|
|
// query DOM elements
|
|
if (!dataSource.items) {
|
|
dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
|
|
}
|
|
|
|
dataSourceItem = dataSource.items[index];
|
|
}
|
|
|
|
let itemData = dataSourceItem;
|
|
|
|
if (itemData instanceof Element) {
|
|
itemData = this._domElementToItemData(itemData);
|
|
}
|
|
|
|
// Dispatching the itemData event,
|
|
// it's a legacy verion before filters were introduced
|
|
const event = this.dispatch('itemData', {
|
|
itemData: itemData || {},
|
|
index
|
|
});
|
|
|
|
return this.applyFilters('itemData', event.itemData, index);
|
|
}
|
|
|
|
/**
|
|
* Get array of gallery DOM elements,
|
|
* based on childSelector and gallery element.
|
|
*
|
|
* @param {Element} galleryElement
|
|
*/
|
|
_getGalleryDOMElements(galleryElement) {
|
|
if (this.options.children || this.options.childSelector) {
|
|
return getElementsFromOption(
|
|
this.options.children,
|
|
this.options.childSelector,
|
|
galleryElement
|
|
) || [];
|
|
}
|
|
|
|
return [galleryElement];
|
|
}
|
|
|
|
/**
|
|
* Converts DOM element to item data object.
|
|
*
|
|
* @param {Element} element DOM element
|
|
*/
|
|
// eslint-disable-next-line class-methods-use-this
|
|
_domElementToItemData(element) {
|
|
const itemData = {
|
|
element
|
|
};
|
|
|
|
const linkEl = element.tagName === 'A' ? element : element.querySelector('a');
|
|
|
|
if (linkEl) {
|
|
// src comes from data-pswp-src attribute,
|
|
// if it's empty link href is used
|
|
itemData.src = linkEl.dataset.pswpSrc || linkEl.href;
|
|
|
|
itemData.srcset = linkEl.dataset.pswpSrcset;
|
|
|
|
itemData.w = parseInt(linkEl.dataset.pswpWidth, 10);
|
|
itemData.h = parseInt(linkEl.dataset.pswpHeight, 10);
|
|
|
|
if (linkEl.dataset.pswpType) {
|
|
itemData.type = linkEl.dataset.pswpType;
|
|
}
|
|
|
|
const thumbnailEl = element.querySelector('img');
|
|
|
|
if (thumbnailEl) {
|
|
// define msrc only if it's the first slide,
|
|
// as rendering (even small stretched thumbnail) is an expensive operation
|
|
itemData.msrc = thumbnailEl.currentSrc || thumbnailEl.src;
|
|
itemData.alt = thumbnailEl.getAttribute('alt');
|
|
}
|
|
|
|
if (linkEl.dataset.pswpCropped || linkEl.dataset.cropped) {
|
|
itemData.thumbCropped = true;
|
|
}
|
|
}
|
|
|
|
this.applyFilters('domItemData', itemData, element, linkEl);
|
|
|
|
return itemData;
|
|
}
|
|
}
|
|
|
|
function getViewportSize(options, pswp) {
|
|
if (options.getViewportSizeFn) {
|
|
const newViewportSize = options.getViewportSizeFn(options, pswp);
|
|
if (newViewportSize) {
|
|
return newViewportSize;
|
|
}
|
|
}
|
|
|
|
return {
|
|
x: document.documentElement.clientWidth,
|
|
|
|
// TODO: height on mobile is very incosistent due to toolbar
|
|
// find a way to improve this
|
|
//
|
|
// document.documentElement.clientHeight - doesn't seem to work well
|
|
y: window.innerHeight
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parses padding option.
|
|
* Supported formats:
|
|
*
|
|
* // Object
|
|
* padding: {
|
|
* top: 0,
|
|
* bottom: 0,
|
|
* left: 0,
|
|
* right: 0
|
|
* }
|
|
*
|
|
* // A function that returns the object
|
|
* paddingFn: (viewportSize) => {
|
|
* return {
|
|
* top: 0,
|
|
* bottom: 0,
|
|
* left: 0,
|
|
* right: 0
|
|
* };
|
|
* }
|
|
*
|
|
* // Legacy variant
|
|
* paddingLeft: 0,
|
|
* paddingRight: 0,
|
|
* paddingTop: 0,
|
|
* paddingBottom: 0,
|
|
*
|
|
* @param {String} prop 'left', 'top', 'bottom', 'right'
|
|
* @param {Object} options PhotoSwipe options
|
|
* @param {Object} viewportSize PhotoSwipe viewport size, for example: { x:800, y:600 }
|
|
* @returns {Number}
|
|
*/
|
|
function parsePaddingOption(prop, options, viewportSize) {
|
|
let paddingValue;
|
|
|
|
if (options.paddingFn) {
|
|
paddingValue = options.paddingFn(viewportSize)[prop];
|
|
} else if (options.padding) {
|
|
paddingValue = options.padding[prop];
|
|
} else {
|
|
const legacyPropName = 'padding' + prop[0].toUpperCase() + prop.slice(1);
|
|
if (options[legacyPropName]) {
|
|
paddingValue = options[legacyPropName];
|
|
}
|
|
}
|
|
|
|
return paddingValue || 0;
|
|
}
|
|
|
|
|
|
function getPanAreaSize(options, viewportSize/*, pswp*/) {
|
|
return {
|
|
x: viewportSize.x
|
|
- parsePaddingOption('left', options, viewportSize)
|
|
- parsePaddingOption('right', options, viewportSize),
|
|
y: viewportSize.y
|
|
- parsePaddingOption('top', options, viewportSize)
|
|
- parsePaddingOption('bottom', options, viewportSize)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculates zoom levels for specific slide.
|
|
* Depends on viewport size and image size.
|
|
*/
|
|
|
|
const MAX_IMAGE_WIDTH = 4000;
|
|
|
|
class ZoomLevel {
|
|
/**
|
|
* @param {Object} options PhotoSwipe options
|
|
* @param {Object} itemData Slide data
|
|
* @param {Integer} index Slide index
|
|
* @param {PhotoSwipe|undefined} pswp PhotoSwipe instance, can be undefined if not initialized yet
|
|
*/
|
|
constructor(options, itemData, index, pswp) {
|
|
this.pswp = pswp;
|
|
this.options = options;
|
|
this.itemData = itemData;
|
|
this.index = index;
|
|
}
|
|
|
|
/**
|
|
* Calculate initial, secondary and maximum zoom level for the specified slide.
|
|
*
|
|
* It should be called when either image or viewport size changes.
|
|
*
|
|
* @param {Slide} slide
|
|
*/
|
|
update(maxWidth, maxHeight, panAreaSize) {
|
|
this.elementSize = {
|
|
x: maxWidth,
|
|
y: maxHeight
|
|
};
|
|
|
|
this.panAreaSize = panAreaSize;
|
|
|
|
const hRatio = this.panAreaSize.x / this.elementSize.x;
|
|
const vRatio = this.panAreaSize.y / this.elementSize.y;
|
|
|
|
this.fit = Math.min(1, hRatio < vRatio ? hRatio : vRatio);
|
|
this.fill = Math.min(1, hRatio > vRatio ? hRatio : vRatio);
|
|
|
|
// zoom.vFill defines zoom level of the image
|
|
// when it has 100% of viewport vertical space (height)
|
|
this.vFill = Math.min(1, vRatio);
|
|
|
|
this.initial = this._getInitial();
|
|
this.secondary = this._getSecondary();
|
|
this.max = Math.max(
|
|
this.initial,
|
|
this.secondary,
|
|
this._getMax()
|
|
);
|
|
|
|
this.min = Math.min(
|
|
this.fit,
|
|
this.initial,
|
|
this.secondary
|
|
);
|
|
|
|
if (this.pswp) {
|
|
this.pswp.dispatch('zoomLevelsUpdate', { zoomLevels: this, slideData: this.itemData });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses user-defined zoom option.
|
|
*
|
|
* @param {Mixed} optionPrefix Zoom level option prefix (initial, secondary, max)
|
|
*/
|
|
_parseZoomLevelOption(optionPrefix) {
|
|
// zoom.initial
|
|
// zoom.secondary
|
|
// zoom.max
|
|
const optionValue = this.options[optionPrefix + 'ZoomLevel'];
|
|
|
|
if (!optionValue) {
|
|
return;
|
|
}
|
|
|
|
if (typeof optionValue === 'function') {
|
|
return optionValue(this);
|
|
}
|
|
|
|
if (optionValue === 'fill') {
|
|
return this.fill;
|
|
}
|
|
|
|
if (optionValue === 'fit') {
|
|
return this.fit;
|
|
}
|
|
|
|
return Number(optionValue);
|
|
}
|
|
|
|
/**
|
|
* Get zoom level to which image will be zoomed after double-tap gesture,
|
|
* or when user clicks on zoom icon,
|
|
* or mouse-click on image itself.
|
|
* If you return 1 image will be zoomed to its original size.
|
|
*
|
|
* @return {Number}
|
|
*/
|
|
_getSecondary() {
|
|
let currZoomLevel = this._parseZoomLevelOption('secondary');
|
|
|
|
if (currZoomLevel) {
|
|
return currZoomLevel;
|
|
}
|
|
|
|
// 3x of "fit" state, but not larger than original
|
|
currZoomLevel = Math.min(1, this.fit * 3);
|
|
|
|
if (currZoomLevel * this.elementSize.x > MAX_IMAGE_WIDTH) {
|
|
currZoomLevel = MAX_IMAGE_WIDTH / this.elementSize.x;
|
|
}
|
|
|
|
return currZoomLevel;
|
|
}
|
|
|
|
/**
|
|
* Get initial image zoom level.
|
|
*
|
|
* @return {Number}
|
|
*/
|
|
_getInitial() {
|
|
return this._parseZoomLevelOption('initial') || this.fit;
|
|
}
|
|
|
|
/**
|
|
* Maximum zoom level when user zooms
|
|
* via zoom/pinch gesture,
|
|
* via cmd/ctrl-wheel or via trackpad.
|
|
*
|
|
* @return {Number}
|
|
*/
|
|
_getMax() {
|
|
const currZoomLevel = this._parseZoomLevelOption('max');
|
|
|
|
if (currZoomLevel) {
|
|
return currZoomLevel;
|
|
}
|
|
|
|
// max zoom level is x4 from "fit state",
|
|
// used for zoom gesture and ctrl/trackpad zoom
|
|
return Math.max(1, this.fit * 4);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns cache key by slide index and data
|
|
*
|
|
* @param {Object} itemData
|
|
* @param {Integer} index
|
|
* @returns {String}
|
|
*/
|
|
function getKey(itemData, index) {
|
|
if (itemData && itemData.src) {
|
|
return itemData.src + '_' + index;
|
|
}
|
|
return index;
|
|
}
|
|
|
|
|
|
/**
|
|
* Lazy-load an image
|
|
* This function is used both by Lightbox and PhotoSwipe core,
|
|
* thus it can be called before dialog is opened.
|
|
*
|
|
* @param {Object} itemData Data about the slide
|
|
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox
|
|
* @param {Integer} index
|
|
* @returns {Object|Boolean} Image that is being decoded or false.
|
|
*/
|
|
function lazyLoadData(itemData, instance, index) {
|
|
// src/slide/content/content.js
|
|
const content = instance.createContentFromData(itemData);
|
|
|
|
if (!content || !content.lazyLoad) {
|
|
return;
|
|
}
|
|
|
|
content.key = getKey(itemData, index);
|
|
|
|
const { options } = instance;
|
|
|
|
// We need to know dimensions of the image to preload it,
|
|
// as it might use srcset and we need to define sizes
|
|
const viewportSize = instance.viewportSize || getViewportSize(options);
|
|
const panAreaSize = getPanAreaSize(options, viewportSize);
|
|
|
|
const zoomLevel = new ZoomLevel(options, itemData, -1);
|
|
zoomLevel.update(content.width, content.height, panAreaSize);
|
|
|
|
content.lazyLoad();
|
|
content.setDisplayedSize(
|
|
Math.ceil(content.width * zoomLevel.initial),
|
|
Math.ceil(content.height * zoomLevel.initial)
|
|
);
|
|
|
|
return content;
|
|
}
|
|
|
|
|
|
/**
|
|
* Lazy-loads specific slide.
|
|
* This function is used both by Lightbox and PhotoSwipe core,
|
|
* thus it can be called before dialog is opened.
|
|
*
|
|
* By default it loads image based on viewport size and initial zoom level.
|
|
*
|
|
* @param {Integer} index Slide index
|
|
* @param {Object} instance PhotoSwipe or PhotoSwipeLightbox eventable instance
|
|
*/
|
|
function lazyLoadSlide(index, instance) {
|
|
const itemData = instance.getItemData(index);
|
|
|
|
if (instance.dispatch('lazyLoadSlide', { index, itemData }).defaultPrevented) {
|
|
return;
|
|
}
|
|
|
|
return lazyLoadData(itemData, instance, index);
|
|
}
|
|
|
|
/**
|
|
* PhotoSwipe lightbox
|
|
*
|
|
* - If user has unsupported browser it falls back to default browser action (just opens URL)
|
|
* - Binds click event to links that should open PhotoSwipe
|
|
* - parses DOM strcture for PhotoSwipe (retrieves large image URLs and sizes)
|
|
* - Initializes PhotoSwipe
|
|
*
|
|
*
|
|
* Loader options use the same object as PhotoSwipe, and supports such options:
|
|
*
|
|
* gallery - Element | Element[] | NodeList | string selector for the gallery element
|
|
* children - Element | Element[] | NodeList | string selector for the gallery children
|
|
*
|
|
*/
|
|
|
|
class PhotoSwipeLightbox extends PhotoSwipeBase {
|
|
constructor(options) {
|
|
super();
|
|
this.options = options || {};
|
|
this._uid = 0;
|
|
}
|
|
|
|
init() {
|
|
this.onThumbnailsClick = this.onThumbnailsClick.bind(this);
|
|
|
|
// Bind click events to each gallery
|
|
getElementsFromOption(this.options.gallery, this.options.gallerySelector)
|
|
.forEach((galleryElement) => {
|
|
galleryElement.addEventListener('click', this.onThumbnailsClick, false);
|
|
});
|
|
}
|
|
|
|
onThumbnailsClick(e) {
|
|
// Exit and allow default browser action if:
|
|
if (specialKeyUsed(e) // ... if clicked with a special key (ctrl/cmd...)
|
|
|| window.pswp // ... if PhotoSwipe is already open
|
|
|| window.navigator.onLine === false) { // ... if offline
|
|
return;
|
|
}
|
|
|
|
// If both clientX and clientY are 0 or not defined,
|
|
// the event is likely triggered by keyboard,
|
|
// so we do not pass the initialPoint
|
|
//
|
|
// Note that some screen readers emulate the mouse position,
|
|
// so it's not ideal way to detect them.
|
|
//
|
|
let initialPoint = { x: e.clientX, y: e.clientY };
|
|
|
|
if (!initialPoint.x && !initialPoint.y) {
|
|
initialPoint = null;
|
|
}
|
|
|
|
const clickedIndex = this.getClickedIndex(e);
|
|
const dataSource = {
|
|
gallery: e.currentTarget
|
|
};
|
|
|
|
if (clickedIndex >= 0) {
|
|
e.preventDefault();
|
|
this.loadAndOpen(clickedIndex, dataSource, initialPoint);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get index of gallery item that was clicked.
|
|
*
|
|
* @param {Event} e click event
|
|
*/
|
|
getClickedIndex(e) {
|
|
if (this.options.getClickedIndexFn) {
|
|
return this.options.getClickedIndexFn.call(this, e);
|
|
}
|
|
|
|
const clickedTarget = e.target;
|
|
const childElements = getElementsFromOption(
|
|
this.options.children,
|
|
this.options.childSelector,
|
|
e.currentTarget
|
|
);
|
|
const clickedChildIndex = childElements.findIndex(
|
|
child => child === clickedTarget || child.contains(clickedTarget)
|
|
);
|
|
|
|
if (clickedChildIndex !== -1) {
|
|
return clickedChildIndex;
|
|
} else if (this.options.children || this.options.childSelector) {
|
|
// click wasn't on a child element
|
|
return -1;
|
|
}
|
|
|
|
// There is only one item (which is the gallery)
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Load and open PhotoSwipe
|
|
*
|
|
* @param {Integer} index
|
|
* @param {Array|Object|null} dataSource
|
|
* @param {Point|null} initialPoint
|
|
*/
|
|
loadAndOpen(index, dataSource, initialPoint) {
|
|
// Check if the gallery is already open
|
|
if (window.pswp) {
|
|
return false;
|
|
}
|
|
|
|
// set initial index
|
|
this.options.index = index;
|
|
|
|
// define options for PhotoSwipe constructor
|
|
this.options.initialPointerPos = initialPoint;
|
|
|
|
this.shouldOpen = true;
|
|
this.preload(index, dataSource);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Load the main module and the slide content by index
|
|
*
|
|
* @param {Integer} index
|
|
*/
|
|
preload(index, dataSource) {
|
|
const { options } = this;
|
|
|
|
if (dataSource) {
|
|
options.dataSource = dataSource;
|
|
}
|
|
|
|
// Add the main module
|
|
const promiseArray = [dynamicImportModule(options.pswpModule)];
|
|
|
|
// Add custom-defined promise, if any
|
|
if (typeof options.openPromise === 'function') {
|
|
// allow developers to perform some task before opening
|
|
promiseArray.push(options.openPromise());
|
|
}
|
|
|
|
if (options.preloadFirstSlide !== false && index >= 0) {
|
|
this._preloadedContent = lazyLoadSlide(index, this);
|
|
}
|
|
|
|
// Wait till all promises resolve and open PhotoSwipe
|
|
const uid = ++this._uid;
|
|
Promise.all(promiseArray).then((iterableModules) => {
|
|
if (this.shouldOpen) {
|
|
const mainModule = iterableModules[0];
|
|
this._openPhotoswipe(mainModule, uid);
|
|
}
|
|
});
|
|
}
|
|
|
|
_openPhotoswipe(module, uid) {
|
|
// Cancel opening if UID doesn't match the current one
|
|
// (if user clicked on another gallery item before current was loaded).
|
|
//
|
|
// Or if shouldOpen flag is set to false
|
|
// (developer may modify it via public API)
|
|
if (uid !== this._uid && this.shouldOpen) {
|
|
return;
|
|
}
|
|
|
|
this.shouldOpen = false;
|
|
|
|
// PhotoSwipe is already open
|
|
if (window.pswp) {
|
|
return;
|
|
}
|
|
|
|
// Pass data to PhotoSwipe and open init
|
|
const pswp = typeof module === 'object'
|
|
? new module.default(null, this.options) // eslint-disable-line
|
|
: new module(null, this.options); // eslint-disable-line
|
|
|
|
this.pswp = pswp;
|
|
window.pswp = pswp;
|
|
|
|
// map listeners from Lightbox to PhotoSwipe Core
|
|
Object.keys(this._listeners).forEach((name) => {
|
|
this._listeners[name].forEach((fn) => {
|
|
pswp.on(name, fn);
|
|
});
|
|
});
|
|
|
|
// same with filters
|
|
Object.keys(this._filters).forEach((name) => {
|
|
this._filters[name].forEach((filter) => {
|
|
pswp.addFilter(name, filter.fn, filter.priority);
|
|
});
|
|
});
|
|
|
|
// same with content types
|
|
pswp.contentTypes = { ...this.contentTypes };
|
|
|
|
if (this._preloadedContent) {
|
|
pswp.contentLoader.addToCache(this._preloadedContent);
|
|
this._preloadedContent = null;
|
|
}
|
|
|
|
pswp.on('destroy', () => {
|
|
// clean up public variables
|
|
this.pswp = null;
|
|
window.pswp = null;
|
|
});
|
|
|
|
pswp.init();
|
|
}
|
|
|
|
destroy() {
|
|
if (this.pswp) {
|
|
this.pswp.close();
|
|
}
|
|
|
|
this.shouldOpen = false;
|
|
this._listeners = null;
|
|
|
|
getElementsFromOption(this.options.gallery, this.options.gallerySelector)
|
|
.forEach((galleryElement) => {
|
|
galleryElement.removeEventListener('click', this.onThumbnailsClick, false);
|
|
});
|
|
}
|
|
}
|
|
|
|
export default PhotoSwipeLightbox;
|
|
export { Content, ImageContent };
|
|
//# sourceMappingURL=photoswipe-lightbox.esm.js.map
|