photoswipe
Version:
JavaScript gallery
1,379 lines (1,173 loc) • 36.2 kB
JavaScript
/*!
* PhotoSwipe Lightbox 5.2.1 - https://photoswipe.com
* (c) 2022 Dmytro 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;
}
/**
* Get transform string
*
* @param {Number} x
* @param {Number|null} y
* @param {Number|null} scale
*/
function toTransformString(x, y, scale) {
let propValue = 'translate3d('
+ x + 'px,' + (y || 0) + 'px'
+ ',0)';
if (scale !== undefined) {
propValue += ' scale3d('
+ scale + ',' + scale
+ ',1)';
}
return propValue;
}
/**
* 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;
}
/**
* @param {*} v
* @returns Boolean
*/
function isClass(fn) {
return typeof fn === 'function' && /^\s*class\s+/.test(fn.toString());
}
/**
* 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 Placeholder {
/**
* @param {String|false} imageSrc
* @param {Element} container
*/
constructor(imageSrc, container) {
// Create placeholder
// (stretched thumbnail or simple div behind the main image)
this.element = createElement(
'pswp__img pswp__img--placeholder',
imageSrc ? 'img' : '',
container
);
if (imageSrc) {
this.element.decoding = 'async';
this.element.alt = '';
this.element.src = imageSrc;
this.element.setAttribute('role', 'presentation');
}
this.element.setAttribute('aria-hiden', 'true');
}
setDisplayedSize(width, height) {
if (!this.element) {
return;
}
if (this.element.tagName === 'IMG') {
// Use transform scale() to modify img placeholder size
// (instead of changing width/height directly).
// This helps with performance, specifically in iOS15 Safari.
setWidthHeight(this.element, 250, 'auto');
this.element.style.transformOrigin = '0 0';
this.element.style.transform = toTransformString(0, 0, width / 250);
} else {
setWidthHeight(this.element, width, height);
}
}
destroy() {
if (this.element.parentNode) {
this.element.remove();
}
this.element = null;
}
}
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, index) {
this.instance = instance;
this.data = itemData;
this.index = index;
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.hasSlide = false;
this.state = LOAD_STATE.IDLE;
if (this.data.type) {
this.type = this.data.type;
} else if (this.data.src) {
this.type = 'image';
} else {
this.type = 'html';
}
this.instance.dispatch('contentInit', { content: this });
}
removePlaceholder() {
if (this.placeholder && !this.keepPlaceholder()) {
// With delay, as image might be loaded, but not rendered
setTimeout(() => {
if (this.placeholder) {
this.placeholder.destroy();
this.placeholder = null;
}
}, 500);
}
}
/**
* Preload content
*
* @param {Boolean} isLazy
*/
load(isLazy, reload) {
if (!this.placeholder && this.slide && this.usePlaceholder()) {
// use -based placeholder only for the first slide,
// as rendering (even small stretched thumbnail) is an expensive operation
const placeholderSrc = this.instance.applyFilters(
'placeholderSrc',
(this.data.msrc && this.slide.isFirstSlide) ? this.data.msrc : false,
this
);
this.placeholder = new Placeholder(
placeholderSrc,
this.slide.container
);
}
if (this.element && !reload) {
return;
}
if (this.instance.dispatch('contentLoad', { content: this, isLazy }).defaultPrevented) {
return;
}
if (this.isImageContent()) {
this.loadImage(isLazy);
} else {
this.element = createElement('pswp__content');
this.element.innerHTML = this.data.html || '';
}
if (reload && this.slide) {
this.slide.updateContentSize(true);
}
}
/**
* Preload image
*
* @param {Boolean} isLazy
*/
loadImage(isLazy) {
this.element = createElement('pswp__img', 'img');
if (this.instance.dispatch('contentLoadImage', { content: this, isLazy }).defaultPrevented) {
return;
}
if (this.data.srcset) {
this.element.srcset = this.data.srcset;
}
this.element.src = this.data.src;
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();
};
}
}
/**
* Assign slide to content
*
* @param {Slide} slide
*/
setSlide(slide) {
this.slide = slide;
this.hasSlide = true;
this.instance = slide.pswp;
// todo: do we need to unset slide?
}
/**
* Content load success handler
*/
onLoaded() {
this.state = LOAD_STATE.LOADED;
if (this.slide) {
this.instance.dispatch('loadComplete', { slide: this.slide, content: this });
// if content is reloaded
if (this.slide.isActive
&& this.slide.heavyAppended
&& !this.element.parentNode) {
this.slide.container.innerHTML = '';
this.append();
this.slide.updateContentSize(true);
}
}
}
/**
* Content load error handler
*/
onError() {
this.state = LOAD_STATE.ERROR;
if (this.slide) {
this.displayError();
this.instance.dispatch('loadComplete', { slide: this.slide, isError: true, content: this });
this.instance.dispatch('loadError', { slide: this.slide, content: this });
}
}
/**
* @returns {Boolean} If the content is currently loading
*/
isLoading() {
return this.instance.applyFilters(
'isContentLoading',
this.state === LOAD_STATE.LOADING,
this
);
}
isError() {
return this.state === LOAD_STATE.ERROR;
}
/**
* @returns {Boolean} If the content is image
*/
isImageContent() {
return this.type === 'image';
}
/**
* Update content size
*
* @param {Number} width
* @param {Number} height
*/
setDisplayedSize(width, height) {
if (!this.element) {
return;
}
if (this.placeholder) {
this.placeholder.setDisplayedSize(width, height);
}
if (this.instance.dispatch('contentResize', { content: this, width, height }).defaultPrevented) {
return;
}
setWidthHeight(this.element, width, height);
if (this.isImageContent() && !this.isError()) {
const image = this.element;
// 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.instance.dispatch('imageSizeChange', { slide: this.slide, width, height, content: this });
}
}
}
/**
* @returns {Boolean} If the content can be zoomed
*/
isZoomable() {
return this.instance.applyFilters(
'isContentZoomable',
this.isImageContent() && (this.state !== LOAD_STATE.ERROR),
this
);
}
/**
* @returns {Boolean} If content should use a placeholder (from msrc by default)
*/
usePlaceholder() {
return this.instance.applyFilters(
'useContentPlaceholder',
this.isImageContent(),
this
);
}
/**
* Preload content with lazy-loading param
*
* @param {Boolean} isLazy
*/
lazyLoad() {
if (this.instance.dispatch('contentLazyLoad', { content: this }).defaultPrevented) {
return;
}
this.load(true);
}
/**
* @returns {Boolean} If placeholder should be kept after content is loaded
*/
keepPlaceholder() {
return this.instance.applyFilters(
'isKeepingPlaceholder',
this.isLoading(),
this
);
}
/**
* Destroy the content
*/
destroy() {
this.hasSlide = false;
this.slide = null;
if (this.instance.dispatch('contentDestroy', { content: this }).defaultPrevented) {
return;
}
this.remove();
if (this.isImageContent() && this.element) {
this.element.onload = null;
this.element.onerror = null;
this.element = null;
}
}
/**
* Display error message
*/
displayError() {
if (this.slide) {
let errorMsgEl = createElement('pswp__error-msg');
errorMsgEl.innerText = this.instance.options.errorMsg;
errorMsgEl = this.instance.applyFilters(
'contentErrorElement',
errorMsgEl,
this
);
this.element = createElement('pswp__content pswp__error-msg-container');
this.element.appendChild(errorMsgEl);
this.slide.container.innerHTML = '';
this.slide.container.appendChild(this.element);
this.slide.updateContentSize(true);
this.removePlaceholder();
}
}
/**
* Append the content
*/
append() {
this.isAttached = true;
if (this.state === LOAD_STATE.ERROR) {
this.displayError();
return;
}
if (this.instance.dispatch('contentAppend', { content: this }).defaultPrevented) {
return;
}
if (this.isImageContent()) {
// 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(() => {
// element might change
if (this.element && this.element.tagName === 'IMG') {
this.element.decode().then(() => {
this.isDecoding = false;
requestAnimationFrame(() => {
this.appendImage();
});
}).catch(() => {
this.isDecoding = false;
});
}
});
} else {
if (this.placeholder
&& (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR)) {
this.removePlaceholder();
}
this.appendImage();
}
} else if (this.element && !this.element.parentNode) {
this.slide.container.appendChild(this.element);
}
}
/**
* Activate the slide,
* active slide is generally the current one,
* meaning the user can see it.
*/
activate() {
if (this.instance.dispatch('contentActivate', { content: this }).defaultPrevented) {
return;
}
if (this.slide) {
if (this.isImageContent() && this.isDecoding) {
// add image to slide when it becomes active,
// even if it's not finished decoding
this.appendImage();
} else if (this.isError()) {
this.load(false, true); // try to reload
}
}
}
/**
* Deactivate the content
*/
deactivate() {
this.instance.dispatch('contentDeactivate', { content: this });
}
/**
* Remove the content from DOM
*/
remove() {
this.isAttached = false;
if (this.instance.dispatch('contentRemove', { content: this }).defaultPrevented) {
return;
}
if (this.element && this.element.parentNode) {
this.element.remove();
}
}
/**
* Append the image content to slide container
*/
appendImage() {
if (!this.isAttached) {
return;
}
if (this.instance.dispatch('contentAppendImage', { content: this }).defaultPrevented) {
return;
}
// ensure that element exists and is not already appended
if (this.slide && this.element && !this.element.parentNode) {
this.slide.container.appendChild(this.element);
if (this.placeholder
&& (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR)) {
this.removePlaceholder();
}
}
}
}
/**
* PhotoSwipe base class that can retrieve data about every slide.
* Shared by PhotoSwipe Core and PhotoSwipe Lightbox
*/
class PhotoSwipeBase extends Eventable {
/**
* 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);
}
createContentFromData(slideData, index) {
return new Content(slideData, this, index);
}
/**
* 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;
if (linkEl.dataset.pswpSrcset) {
itemData.srcset = linkEl.dataset.pswpSrcset;
}
itemData.width = parseInt(linkEl.dataset.pswpWidth, 10);
itemData.height = parseInt(linkEl.dataset.pswpHeight, 10);
// support legacy w & h properties
itemData.w = itemData.width;
itemData.h = itemData.height;
if (linkEl.dataset.pswpType) {
itemData.type = linkEl.dataset.pswpType;
}
const thumbnailEl = element.querySelector('img');
if (thumbnailEl) {
// msrc is URL to placeholder image that's displayed before large image is loaded
// by default it's displayed only for the first slide
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, itemData, index) => {
* 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 }
* @param {Object} itemData Data about the slide
* @param {Integer} index Slide index
* @returns {Number}
*/
function parsePaddingOption(prop, options, viewportSize, itemData, index) {
let paddingValue;
if (options.paddingFn) {
paddingValue = options.paddingFn(viewportSize, itemData, index)[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, itemData, index) {
return {
x: viewportSize.x
- parsePaddingOption('left', options, viewportSize, itemData, index)
- parsePaddingOption('right', options, viewportSize, itemData, index),
y: viewportSize.y
- parsePaddingOption('top', options, viewportSize, itemData, index)
- parsePaddingOption('bottom', options, viewportSize, itemData, index)
};
}
/**
* 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);
}
}
/**
* 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, index);
if (!content || !content.lazyLoad) {
return;
}
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, itemData, index);
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;
}
let clickedIndex = this.getClickedIndex(e);
clickedIndex = this.applyFilters('clickedIndex', clickedIndex, e, this);
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) {
// legacy option
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 = [];
const pswpModuleType = typeof options.pswpModule;
if (isClass(options.pswpModule)) {
promiseArray.push(options.pswpModule);
} else if (pswpModuleType === 'string') {
throw new Error('pswpModule as string is no longer supported');
} else if (pswpModuleType === 'function') {
promiseArray.push(options.pswpModule());
} else {
throw new Error('pswpModule is not valid');
}
// 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(this.options) // eslint-disable-line
: new module(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);
});
});
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.destroy();
}
this.shouldOpen = false;
this._listeners = null;
getElementsFromOption(this.options.gallery, this.options.gallerySelector)
.forEach((galleryElement) => {
galleryElement.removeEventListener('click', this.onThumbnailsClick, false);
});
}
}
export default PhotoSwipeLightbox;
//# sourceMappingURL=photoswipe-lightbox.esm.js.map