2076 lines
55 KiB
JavaScript
2076 lines
55 KiB
JavaScript
/*!
|
|
* PhotoSwipe Lightbox 5.4.4 - https://photoswipe.com
|
|
* (c) 2024 Dmytro Semenov
|
|
*/
|
|
/** @typedef {import('../photoswipe.js').Point} Point */
|
|
|
|
/**
|
|
* @template {keyof HTMLElementTagNameMap} T
|
|
* @param {string} className
|
|
* @param {T} tagName
|
|
* @param {Node} [appendToEl]
|
|
* @returns {HTMLElementTagNameMap[T]}
|
|
*/
|
|
function createElement(className, tagName, appendToEl) {
|
|
const el = document.createElement(tagName);
|
|
|
|
if (className) {
|
|
el.className = className;
|
|
}
|
|
|
|
if (appendToEl) {
|
|
appendToEl.appendChild(el);
|
|
}
|
|
|
|
return el;
|
|
}
|
|
/**
|
|
* Get transform string
|
|
*
|
|
* @param {number} x
|
|
* @param {number} [y]
|
|
* @param {number} [scale]
|
|
* @returns {string}
|
|
*/
|
|
|
|
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
|
|
*
|
|
* @param {HTMLElement} el
|
|
* @param {string | number} w
|
|
* @param {string | number} h
|
|
*/
|
|
|
|
function setWidthHeight(el, w, h) {
|
|
el.style.width = typeof w === "number" ? `${w}px` : w;
|
|
el.style.height = typeof h === "number" ? `${h}px` : h;
|
|
}
|
|
/** @typedef {LOAD_STATE[keyof LOAD_STATE]} LoadState */
|
|
|
|
/** @type {{ IDLE: 'idle'; LOADING: 'loading'; LOADED: 'loaded'; ERROR: 'error' }} */
|
|
|
|
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 {MouseEvent | KeyboardEvent} e
|
|
* @returns {boolean}
|
|
*/
|
|
|
|
function specialKeyUsed(e) {
|
|
return (
|
|
("button" in e && e.button === 1) ||
|
|
e.ctrlKey ||
|
|
e.metaKey ||
|
|
e.altKey ||
|
|
e.shiftKey
|
|
);
|
|
}
|
|
/**
|
|
* Parse `gallery` or `children` options.
|
|
*
|
|
* @param {import('../photoswipe.js').ElementProvider} [option]
|
|
* @param {string} [legacySelector]
|
|
* @param {HTMLElement | Document} [parent]
|
|
* @returns HTMLElement[]
|
|
*/
|
|
|
|
function getElementsFromOption(option, legacySelector, parent = document) {
|
|
/** @type {HTMLElement[]} */
|
|
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;
|
|
}
|
|
/**
|
|
* Check if variable is PhotoSwipe class
|
|
*
|
|
* @param {any} fn
|
|
* @returns {boolean}
|
|
*/
|
|
|
|
function isPswpClass(fn) {
|
|
return typeof fn === "function" && fn.prototype && fn.prototype.goTo;
|
|
}
|
|
/**
|
|
* Check if browser is Safari
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
|
|
function isSafari() {
|
|
return !!(navigator.vendor && navigator.vendor.match(/apple/i));
|
|
}
|
|
|
|
/** @typedef {import('../lightbox/lightbox.js').default} PhotoSwipeLightbox */
|
|
|
|
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
|
|
|
|
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
|
|
|
|
/** @typedef {import('../photoswipe.js').DataSource} DataSource */
|
|
|
|
/** @typedef {import('../ui/ui-element.js').UIElementData} UIElementData */
|
|
|
|
/** @typedef {import('../slide/content.js').default} ContentDefault */
|
|
|
|
/** @typedef {import('../slide/slide.js').default} Slide */
|
|
|
|
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
|
|
|
|
/** @typedef {import('../slide/zoom-level.js').default} ZoomLevel */
|
|
|
|
/** @typedef {import('../slide/get-thumb-bounds.js').Bounds} Bounds */
|
|
|
|
/**
|
|
* Allow adding an arbitrary props to the Content
|
|
* https://photoswipe.com/custom-content/#using-webp-image-format
|
|
* @typedef {ContentDefault & Record<string, any>} Content
|
|
*/
|
|
|
|
/** @typedef {{ x?: number; y?: number }} Point */
|
|
|
|
/**
|
|
* @typedef {Object} PhotoSwipeEventsMap https://photoswipe.com/events/
|
|
*
|
|
*
|
|
* https://photoswipe.com/adding-ui-elements/
|
|
*
|
|
* @prop {undefined} uiRegister
|
|
* @prop {{ data: UIElementData }} uiElementCreate
|
|
*
|
|
*
|
|
* https://photoswipe.com/events/#initialization-events
|
|
*
|
|
* @prop {undefined} beforeOpen
|
|
* @prop {undefined} firstUpdate
|
|
* @prop {undefined} initialLayout
|
|
* @prop {undefined} change
|
|
* @prop {undefined} afterInit
|
|
* @prop {undefined} bindEvents
|
|
*
|
|
*
|
|
* https://photoswipe.com/events/#opening-or-closing-transition-events
|
|
*
|
|
* @prop {undefined} openingAnimationStart
|
|
* @prop {undefined} openingAnimationEnd
|
|
* @prop {undefined} closingAnimationStart
|
|
* @prop {undefined} closingAnimationEnd
|
|
*
|
|
*
|
|
* https://photoswipe.com/events/#closing-events
|
|
*
|
|
* @prop {undefined} close
|
|
* @prop {undefined} destroy
|
|
*
|
|
*
|
|
* https://photoswipe.com/events/#pointer-and-gesture-events
|
|
*
|
|
* @prop {{ originalEvent: PointerEvent }} pointerDown
|
|
* @prop {{ originalEvent: PointerEvent }} pointerMove
|
|
* @prop {{ originalEvent: PointerEvent }} pointerUp
|
|
* @prop {{ bgOpacity: number }} pinchClose can be default prevented
|
|
* @prop {{ panY: number }} verticalDrag can be default prevented
|
|
*
|
|
*
|
|
* https://photoswipe.com/events/#slide-content-events
|
|
*
|
|
* @prop {{ content: Content }} contentInit
|
|
* @prop {{ content: Content; isLazy: boolean }} contentLoad can be default prevented
|
|
* @prop {{ content: Content; isLazy: boolean }} contentLoadImage can be default prevented
|
|
* @prop {{ content: Content; slide: Slide; isError?: boolean }} loadComplete
|
|
* @prop {{ content: Content; slide: Slide }} loadError
|
|
* @prop {{ content: Content; width: number; height: number }} contentResize can be default prevented
|
|
* @prop {{ content: Content; width: number; height: number; slide: Slide }} imageSizeChange
|
|
* @prop {{ content: Content }} contentLazyLoad can be default prevented
|
|
* @prop {{ content: Content }} contentAppend can be default prevented
|
|
* @prop {{ content: Content }} contentActivate can be default prevented
|
|
* @prop {{ content: Content }} contentDeactivate can be default prevented
|
|
* @prop {{ content: Content }} contentRemove can be default prevented
|
|
* @prop {{ content: Content }} contentDestroy can be default prevented
|
|
*
|
|
*
|
|
* undocumented
|
|
*
|
|
* @prop {{ point: Point; originalEvent: PointerEvent }} imageClickAction can be default prevented
|
|
* @prop {{ point: Point; originalEvent: PointerEvent }} bgClickAction can be default prevented
|
|
* @prop {{ point: Point; originalEvent: PointerEvent }} tapAction can be default prevented
|
|
* @prop {{ point: Point; originalEvent: PointerEvent }} doubleTapAction can be default prevented
|
|
*
|
|
* @prop {{ originalEvent: KeyboardEvent }} keydown can be default prevented
|
|
* @prop {{ x: number; dragging: boolean }} moveMainScroll
|
|
* @prop {{ slide: Slide }} firstZoomPan
|
|
* @prop {{ slide: Slide | undefined, data: SlideData, index: number }} gettingData
|
|
* @prop {undefined} beforeResize
|
|
* @prop {undefined} resize
|
|
* @prop {undefined} viewportSize
|
|
* @prop {undefined} updateScrollOffset
|
|
* @prop {{ slide: Slide }} slideInit
|
|
* @prop {{ slide: Slide }} afterSetContent
|
|
* @prop {{ slide: Slide }} slideLoad
|
|
* @prop {{ slide: Slide }} appendHeavy can be default prevented
|
|
* @prop {{ slide: Slide }} appendHeavyContent
|
|
* @prop {{ slide: Slide }} slideActivate
|
|
* @prop {{ slide: Slide }} slideDeactivate
|
|
* @prop {{ slide: Slide }} slideDestroy
|
|
* @prop {{ destZoomLevel: number, centerPoint: Point | undefined, transitionDuration: number | false | undefined }} beforeZoomTo
|
|
* @prop {{ slide: Slide }} zoomPanUpdate
|
|
* @prop {{ slide: Slide }} initialZoomPan
|
|
* @prop {{ slide: Slide }} calcSlideSize
|
|
* @prop {undefined} resolutionChanged
|
|
* @prop {{ originalEvent: WheelEvent }} wheel can be default prevented
|
|
* @prop {{ content: Content }} contentAppendImage can be default prevented
|
|
* @prop {{ index: number; itemData: SlideData }} lazyLoadSlide can be default prevented
|
|
* @prop {undefined} lazyLoad
|
|
* @prop {{ slide: Slide }} calcBounds
|
|
* @prop {{ zoomLevels: ZoomLevel, slideData: SlideData }} zoomLevelsUpdate
|
|
*
|
|
*
|
|
* legacy
|
|
*
|
|
* @prop {undefined} init
|
|
* @prop {undefined} initialZoomIn
|
|
* @prop {undefined} initialZoomOut
|
|
* @prop {undefined} initialZoomInEnd
|
|
* @prop {undefined} initialZoomOutEnd
|
|
* @prop {{ dataSource: DataSource | undefined, numItems: number }} numItems
|
|
* @prop {{ itemData: SlideData; index: number }} itemData
|
|
* @prop {{ index: number, itemData: SlideData, instance: PhotoSwipe }} thumbBounds
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} PhotoSwipeFiltersMap https://photoswipe.com/filters/
|
|
*
|
|
* @prop {(numItems: number, dataSource: DataSource | undefined) => number} numItems
|
|
* Modify the total amount of slides. Example on Data sources page.
|
|
* https://photoswipe.com/filters/#numitems
|
|
*
|
|
* @prop {(itemData: SlideData, index: number) => SlideData} itemData
|
|
* Modify slide item data. Example on Data sources page.
|
|
* https://photoswipe.com/filters/#itemdata
|
|
*
|
|
* @prop {(itemData: SlideData, element: HTMLElement, linkEl: HTMLAnchorElement) => SlideData} domItemData
|
|
* Modify item data when it's parsed from DOM element. Example on Data sources page.
|
|
* https://photoswipe.com/filters/#domitemdata
|
|
*
|
|
* @prop {(clickedIndex: number, e: MouseEvent, instance: PhotoSwipeLightbox) => number} clickedIndex
|
|
* Modify clicked gallery item index.
|
|
* https://photoswipe.com/filters/#clickedindex
|
|
*
|
|
* @prop {(placeholderSrc: string | false, content: Content) => string | false} placeholderSrc
|
|
* Modify placeholder image source.
|
|
* https://photoswipe.com/filters/#placeholdersrc
|
|
*
|
|
* @prop {(isContentLoading: boolean, content: Content) => boolean} isContentLoading
|
|
* Modify if the content is currently loading.
|
|
* https://photoswipe.com/filters/#iscontentloading
|
|
*
|
|
* @prop {(isContentZoomable: boolean, content: Content) => boolean} isContentZoomable
|
|
* Modify if the content can be zoomed.
|
|
* https://photoswipe.com/filters/#iscontentzoomable
|
|
*
|
|
* @prop {(useContentPlaceholder: boolean, content: Content) => boolean} useContentPlaceholder
|
|
* Modify if the placeholder should be used for the content.
|
|
* https://photoswipe.com/filters/#usecontentplaceholder
|
|
*
|
|
* @prop {(isKeepingPlaceholder: boolean, content: Content) => boolean} isKeepingPlaceholder
|
|
* Modify if the placeholder should be kept after the content is loaded.
|
|
* https://photoswipe.com/filters/#iskeepingplaceholder
|
|
*
|
|
*
|
|
* @prop {(contentErrorElement: HTMLElement, content: Content) => HTMLElement} contentErrorElement
|
|
* Modify an element when the content has error state (for example, if image cannot be loaded).
|
|
* https://photoswipe.com/filters/#contenterrorelement
|
|
*
|
|
* @prop {(element: HTMLElement, data: UIElementData) => HTMLElement} uiElement
|
|
* Modify a UI element that's being created.
|
|
* https://photoswipe.com/filters/#uielement
|
|
*
|
|
* @prop {(thumbnail: HTMLElement | null | undefined, itemData: SlideData, index: number) => HTMLElement} thumbEl
|
|
* Modify the thumbnail element from which opening zoom animation starts or ends.
|
|
* https://photoswipe.com/filters/#thumbel
|
|
*
|
|
* @prop {(thumbBounds: Bounds | undefined, itemData: SlideData, index: number) => Bounds} thumbBounds
|
|
* Modify the thumbnail bounds from which opening zoom animation starts or ends.
|
|
* https://photoswipe.com/filters/#thumbbounds
|
|
*
|
|
* @prop {(srcsetSizesWidth: number, content: Content) => number} srcsetSizesWidth
|
|
*
|
|
* @prop {(preventPointerEvent: boolean, event: PointerEvent, pointerType: string) => boolean} preventPointerEvent
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* @template {keyof PhotoSwipeFiltersMap} T
|
|
* @typedef {{ fn: PhotoSwipeFiltersMap[T], priority: number }} Filter
|
|
*/
|
|
|
|
/**
|
|
* @template {keyof PhotoSwipeEventsMap} T
|
|
* @typedef {PhotoSwipeEventsMap[T] extends undefined ? PhotoSwipeEvent<T> : PhotoSwipeEvent<T> & PhotoSwipeEventsMap[T]} AugmentedEvent
|
|
*/
|
|
|
|
/**
|
|
* @template {keyof PhotoSwipeEventsMap} T
|
|
* @typedef {(event: AugmentedEvent<T>) => void} EventCallback
|
|
*/
|
|
|
|
/**
|
|
* Base PhotoSwipe event object
|
|
*
|
|
* @template {keyof PhotoSwipeEventsMap} T
|
|
*/
|
|
class PhotoSwipeEvent {
|
|
/**
|
|
* @param {T} type
|
|
* @param {PhotoSwipeEventsMap[T]} [details]
|
|
*/
|
|
constructor(type, details) {
|
|
this.type = type;
|
|
this.defaultPrevented = false;
|
|
|
|
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() {
|
|
/**
|
|
* @type {{ [T in keyof PhotoSwipeEventsMap]?: ((event: AugmentedEvent<T>) => void)[] }}
|
|
*/
|
|
this._listeners = {};
|
|
/**
|
|
* @type {{ [T in keyof PhotoSwipeFiltersMap]?: Filter<T>[] }}
|
|
*/
|
|
|
|
this._filters = {};
|
|
/** @type {PhotoSwipe | undefined} */
|
|
|
|
this.pswp = undefined;
|
|
/** @type {PhotoSwipeOptions | undefined} */
|
|
|
|
this.options = undefined;
|
|
}
|
|
/**
|
|
* @template {keyof PhotoSwipeFiltersMap} T
|
|
* @param {T} name
|
|
* @param {PhotoSwipeFiltersMap[T]} fn
|
|
* @param {number} priority
|
|
*/
|
|
|
|
addFilter(name, fn, priority = 100) {
|
|
var _this$_filters$name, _this$_filters$name2, _this$pswp;
|
|
|
|
if (!this._filters[name]) {
|
|
this._filters[name] = [];
|
|
}
|
|
|
|
(_this$_filters$name = this._filters[name]) === null ||
|
|
_this$_filters$name === void 0 ||
|
|
_this$_filters$name.push({
|
|
fn,
|
|
priority,
|
|
});
|
|
(_this$_filters$name2 = this._filters[name]) === null ||
|
|
_this$_filters$name2 === void 0 ||
|
|
_this$_filters$name2.sort((f1, f2) => f1.priority - f2.priority);
|
|
(_this$pswp = this.pswp) === null ||
|
|
_this$pswp === void 0 ||
|
|
_this$pswp.addFilter(name, fn, priority);
|
|
}
|
|
/**
|
|
* @template {keyof PhotoSwipeFiltersMap} T
|
|
* @param {T} name
|
|
* @param {PhotoSwipeFiltersMap[T]} fn
|
|
*/
|
|
|
|
removeFilter(name, fn) {
|
|
if (this._filters[name]) {
|
|
// @ts-expect-error
|
|
this._filters[name] = this._filters[name].filter(
|
|
(filter) => filter.fn !== fn,
|
|
);
|
|
}
|
|
|
|
if (this.pswp) {
|
|
this.pswp.removeFilter(name, fn);
|
|
}
|
|
}
|
|
/**
|
|
* @template {keyof PhotoSwipeFiltersMap} T
|
|
* @param {T} name
|
|
* @param {Parameters<PhotoSwipeFiltersMap[T]>} args
|
|
* @returns {Parameters<PhotoSwipeFiltersMap[T]>[0]}
|
|
*/
|
|
|
|
applyFilters(name, ...args) {
|
|
var _this$_filters$name3;
|
|
|
|
(_this$_filters$name3 = this._filters[name]) === null ||
|
|
_this$_filters$name3 === void 0 ||
|
|
_this$_filters$name3.forEach((filter) => {
|
|
// @ts-expect-error
|
|
args[0] = filter.fn.apply(this, args);
|
|
});
|
|
return args[0];
|
|
}
|
|
/**
|
|
* @template {keyof PhotoSwipeEventsMap} T
|
|
* @param {T} name
|
|
* @param {EventCallback<T>} fn
|
|
*/
|
|
|
|
on(name, fn) {
|
|
var _this$_listeners$name, _this$pswp2;
|
|
|
|
if (!this._listeners[name]) {
|
|
this._listeners[name] = [];
|
|
}
|
|
|
|
(_this$_listeners$name = this._listeners[name]) === null ||
|
|
_this$_listeners$name === void 0 ||
|
|
_this$_listeners$name.push(fn); // When binding events to lightbox,
|
|
// also bind events to PhotoSwipe Core,
|
|
// if it's open.
|
|
|
|
(_this$pswp2 = this.pswp) === null ||
|
|
_this$pswp2 === void 0 ||
|
|
_this$pswp2.on(name, fn);
|
|
}
|
|
/**
|
|
* @template {keyof PhotoSwipeEventsMap} T
|
|
* @param {T} name
|
|
* @param {EventCallback<T>} fn
|
|
*/
|
|
|
|
off(name, fn) {
|
|
var _this$pswp3;
|
|
|
|
if (this._listeners[name]) {
|
|
// @ts-expect-error
|
|
this._listeners[name] = this._listeners[name].filter(
|
|
(listener) => fn !== listener,
|
|
);
|
|
}
|
|
|
|
(_this$pswp3 = this.pswp) === null ||
|
|
_this$pswp3 === void 0 ||
|
|
_this$pswp3.off(name, fn);
|
|
}
|
|
/**
|
|
* @template {keyof PhotoSwipeEventsMap} T
|
|
* @param {T} name
|
|
* @param {PhotoSwipeEventsMap[T]} [details]
|
|
* @returns {AugmentedEvent<T>}
|
|
*/
|
|
|
|
dispatch(name, details) {
|
|
var _this$_listeners$name2;
|
|
|
|
if (this.pswp) {
|
|
return this.pswp.dispatch(name, details);
|
|
}
|
|
|
|
const event =
|
|
/** @type {AugmentedEvent<T>} */
|
|
new PhotoSwipeEvent(name, details);
|
|
(_this$_listeners$name2 = this._listeners[name]) === null ||
|
|
_this$_listeners$name2 === void 0 ||
|
|
_this$_listeners$name2.forEach((listener) => {
|
|
listener.call(this, event);
|
|
});
|
|
return event;
|
|
}
|
|
}
|
|
|
|
class Placeholder {
|
|
/**
|
|
* @param {string | false} imageSrc
|
|
* @param {HTMLElement} container
|
|
*/
|
|
constructor(imageSrc, container) {
|
|
// Create placeholder
|
|
// (stretched thumbnail or simple div behind the main image)
|
|
|
|
/** @type {HTMLImageElement | HTMLDivElement | null} */
|
|
this.element = createElement(
|
|
"pswp__img pswp__img--placeholder",
|
|
imageSrc ? "img" : "div",
|
|
container,
|
|
);
|
|
|
|
if (imageSrc) {
|
|
const imgEl =
|
|
/** @type {HTMLImageElement} */
|
|
this.element;
|
|
imgEl.decoding = "async";
|
|
imgEl.alt = "";
|
|
imgEl.src = imageSrc;
|
|
imgEl.setAttribute("role", "presentation");
|
|
}
|
|
|
|
this.element.setAttribute("aria-hidden", "true");
|
|
}
|
|
/**
|
|
* @param {number} width
|
|
* @param {number} height
|
|
*/
|
|
|
|
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() {
|
|
var _this$element;
|
|
|
|
if (
|
|
(_this$element = this.element) !== null &&
|
|
_this$element !== void 0 &&
|
|
_this$element.parentNode
|
|
) {
|
|
this.element.remove();
|
|
}
|
|
|
|
this.element = null;
|
|
}
|
|
}
|
|
|
|
/** @typedef {import('./slide.js').default} Slide */
|
|
|
|
/** @typedef {import('./slide.js').SlideData} SlideData */
|
|
|
|
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
|
|
|
|
/** @typedef {import('../util/util.js').LoadState} LoadState */
|
|
|
|
class Content {
|
|
/**
|
|
* @param {SlideData} itemData Slide data
|
|
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
|
|
* @param {number} index
|
|
*/
|
|
constructor(itemData, instance, index) {
|
|
this.instance = instance;
|
|
this.data = itemData;
|
|
this.index = index;
|
|
/** @type {HTMLImageElement | HTMLDivElement | undefined} */
|
|
|
|
this.element = undefined;
|
|
/** @type {Placeholder | undefined} */
|
|
|
|
this.placeholder = undefined;
|
|
/** @type {Slide | undefined} */
|
|
|
|
this.slide = undefined;
|
|
this.displayedImageWidth = 0;
|
|
this.displayedImageHeight = 0;
|
|
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.isDecoding = false;
|
|
/** @type {LoadState} */
|
|
|
|
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 = undefined;
|
|
}
|
|
}, 1000);
|
|
}
|
|
}
|
|
/**
|
|
* Preload content
|
|
*
|
|
* @param {boolean} isLazy
|
|
* @param {boolean} [reload]
|
|
*/
|
|
|
|
load(isLazy, reload) {
|
|
if (this.slide && this.usePlaceholder()) {
|
|
if (!this.placeholder) {
|
|
const placeholderSrc = this.instance.applyFilters(
|
|
"placeholderSrc", // use image-based placeholder only for the first slide,
|
|
// as rendering (even small stretched thumbnail) is an expensive operation
|
|
this.data.msrc && this.slide.isFirstSlide ? this.data.msrc : false,
|
|
this,
|
|
);
|
|
this.placeholder = new Placeholder(
|
|
placeholderSrc,
|
|
this.slide.container,
|
|
);
|
|
} else {
|
|
const placeholderEl = this.placeholder.element; // Add placeholder to DOM if it was already created
|
|
|
|
if (placeholderEl && !placeholderEl.parentElement) {
|
|
this.slide.container.prepend(placeholderEl);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.element && !reload) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
this.instance.dispatch("contentLoad", {
|
|
content: this,
|
|
isLazy,
|
|
}).defaultPrevented
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (this.isImageContent()) {
|
|
this.element = createElement("pswp__img", "img"); // Start loading only after width is defined, as sizes might depend on it.
|
|
// Due to Safari feature, we must define sizes before srcset.
|
|
|
|
if (this.displayedImageWidth) {
|
|
this.loadImage(isLazy);
|
|
}
|
|
} else {
|
|
this.element = createElement("pswp__content", "div");
|
|
this.element.innerHTML = this.data.html || "";
|
|
}
|
|
|
|
if (reload && this.slide) {
|
|
this.slide.updateContentSize(true);
|
|
}
|
|
}
|
|
/**
|
|
* Preload image
|
|
*
|
|
* @param {boolean} isLazy
|
|
*/
|
|
|
|
loadImage(isLazy) {
|
|
var _this$data$src, _this$data$alt;
|
|
|
|
if (
|
|
!this.isImageContent() ||
|
|
!this.element ||
|
|
this.instance.dispatch("contentLoadImage", {
|
|
content: this,
|
|
isLazy,
|
|
}).defaultPrevented
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const imageElement =
|
|
/** @type HTMLImageElement */
|
|
this.element;
|
|
this.updateSrcsetSizes();
|
|
|
|
if (this.data.srcset) {
|
|
imageElement.srcset = this.data.srcset;
|
|
}
|
|
|
|
imageElement.src =
|
|
(_this$data$src = this.data.src) !== null && _this$data$src !== void 0
|
|
? _this$data$src
|
|
: "";
|
|
imageElement.alt =
|
|
(_this$data$alt = this.data.alt) !== null && _this$data$alt !== void 0
|
|
? _this$data$alt
|
|
: "";
|
|
this.state = LOAD_STATE.LOADING;
|
|
|
|
if (imageElement.complete) {
|
|
this.onLoaded();
|
|
} else {
|
|
imageElement.onload = () => {
|
|
this.onLoaded();
|
|
};
|
|
|
|
imageElement.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.element) {
|
|
this.instance.dispatch("loadComplete", {
|
|
slide: this.slide,
|
|
content: this,
|
|
}); // if content is reloaded
|
|
|
|
if (
|
|
this.slide.isActive &&
|
|
this.slide.heavyAppended &&
|
|
!this.element.parentNode
|
|
) {
|
|
this.append();
|
|
this.slide.updateContentSize(true);
|
|
}
|
|
|
|
if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
|
|
this.removePlaceholder();
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* 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,
|
|
);
|
|
}
|
|
/**
|
|
* @returns {Boolean} If the content is in error state
|
|
*/
|
|
|
|
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 isInitialSizeUpdate = !this.displayedImageWidth && width;
|
|
this.displayedImageWidth = width;
|
|
this.displayedImageHeight = height;
|
|
|
|
if (isInitialSizeUpdate) {
|
|
this.loadImage(false);
|
|
} else {
|
|
this.updateSrcsetSizes();
|
|
}
|
|
|
|
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,
|
|
);
|
|
}
|
|
/**
|
|
* Update image srcset sizes attribute based on width and height
|
|
*/
|
|
|
|
updateSrcsetSizes() {
|
|
// 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 (!this.isImageContent() || !this.element || !this.data.srcset) {
|
|
return;
|
|
}
|
|
|
|
const image =
|
|
/** @type HTMLImageElement */
|
|
this.element;
|
|
const sizesWidth = this.instance.applyFilters(
|
|
"srcsetSizesWidth",
|
|
this.displayedImageWidth,
|
|
this,
|
|
);
|
|
|
|
if (
|
|
!image.dataset.largestUsedSize ||
|
|
sizesWidth > parseInt(image.dataset.largestUsedSize, 10)
|
|
) {
|
|
image.sizes = sizesWidth + "px";
|
|
image.dataset.largestUsedSize = String(sizesWidth);
|
|
}
|
|
}
|
|
/**
|
|
* @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
|
|
*/
|
|
|
|
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 = undefined;
|
|
|
|
if (
|
|
this.instance.dispatch("contentDestroy", {
|
|
content: this,
|
|
}).defaultPrevented
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.remove();
|
|
|
|
if (this.placeholder) {
|
|
this.placeholder.destroy();
|
|
this.placeholder = undefined;
|
|
}
|
|
|
|
if (this.isImageContent() && this.element) {
|
|
this.element.onload = null;
|
|
this.element.onerror = null;
|
|
this.element = undefined;
|
|
}
|
|
}
|
|
/**
|
|
* Display error message
|
|
*/
|
|
|
|
displayError() {
|
|
if (this.slide) {
|
|
var _this$instance$option, _this$instance$option2;
|
|
|
|
let errorMsgEl = createElement("pswp__error-msg", "div");
|
|
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
|
|
: "";
|
|
errorMsgEl =
|
|
/** @type {HTMLDivElement} */
|
|
this.instance.applyFilters("contentErrorElement", errorMsgEl, this);
|
|
this.element = createElement(
|
|
"pswp__content pswp__error-msg-container",
|
|
"div",
|
|
);
|
|
this.element.appendChild(errorMsgEl);
|
|
this.slide.container.innerText = "";
|
|
this.slide.container.appendChild(this.element);
|
|
this.slide.updateContentSize(true);
|
|
this.removePlaceholder();
|
|
}
|
|
}
|
|
/**
|
|
* Append the content
|
|
*/
|
|
|
|
append() {
|
|
if (this.isAttached || !this.element) {
|
|
return;
|
|
}
|
|
|
|
this.isAttached = true;
|
|
|
|
if (this.state === LOAD_STATE.ERROR) {
|
|
this.displayError();
|
|
return;
|
|
}
|
|
|
|
if (
|
|
this.instance.dispatch("contentAppend", {
|
|
content: this,
|
|
}).defaultPrevented
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const supportsDecode = "decode" in this.element;
|
|
|
|
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.
|
|
// We do not do this in Safari due to partial loading bug.
|
|
if (
|
|
supportsDecode &&
|
|
this.slide &&
|
|
(!this.slide.isActive || isSafari())
|
|
) {
|
|
this.isDecoding = true; // purposefully using finally instead of then,
|
|
// as if srcset sizes changes dynamically - it may cause decode error
|
|
|
|
/** @type {HTMLImageElement} */
|
|
|
|
this.element
|
|
.decode()
|
|
.catch(() => {})
|
|
.finally(() => {
|
|
this.isDecoding = false;
|
|
this.appendImage();
|
|
});
|
|
} else {
|
|
this.appendImage();
|
|
}
|
|
} else if (this.slide && !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 ||
|
|
!this.slide
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (this.isImageContent() && this.isDecoding && !isSafari()) {
|
|
// 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
|
|
}
|
|
|
|
if (this.slide.holderElement) {
|
|
this.slide.holderElement.setAttribute("aria-hidden", "false");
|
|
}
|
|
}
|
|
/**
|
|
* Deactivate the content
|
|
*/
|
|
|
|
deactivate() {
|
|
this.instance.dispatch("contentDeactivate", {
|
|
content: this,
|
|
});
|
|
|
|
if (this.slide && this.slide.holderElement) {
|
|
this.slide.holderElement.setAttribute("aria-hidden", "true");
|
|
}
|
|
}
|
|
/**
|
|
* 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();
|
|
}
|
|
|
|
if (this.placeholder && this.placeholder.element) {
|
|
this.placeholder.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.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
|
|
this.removePlaceholder();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
|
|
|
|
/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
|
|
|
|
/** @typedef {import('../photoswipe.js').Point} Point */
|
|
|
|
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
|
|
|
|
/**
|
|
* @param {PhotoSwipeOptions} options
|
|
* @param {PhotoSwipeBase} pswp
|
|
* @returns {Point}
|
|
*/
|
|
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 {'left' | 'top' | 'bottom' | 'right'} prop
|
|
* @param {PhotoSwipeOptions} options PhotoSwipe options
|
|
* @param {Point} viewportSize PhotoSwipe viewport size, for example: { x:800, y:600 }
|
|
* @param {SlideData} itemData Data about the slide
|
|
* @param {number} index Slide index
|
|
* @returns {number}
|
|
*/
|
|
|
|
function parsePaddingOption(prop, options, viewportSize, itemData, index) {
|
|
let paddingValue = 0;
|
|
|
|
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); // @ts-expect-error
|
|
|
|
if (options[legacyPropName]) {
|
|
// @ts-expect-error
|
|
paddingValue = options[legacyPropName];
|
|
}
|
|
}
|
|
|
|
return Number(paddingValue) || 0;
|
|
}
|
|
/**
|
|
* @param {PhotoSwipeOptions} options
|
|
* @param {Point} viewportSize
|
|
* @param {SlideData} itemData
|
|
* @param {number} index
|
|
* @returns {Point}
|
|
*/
|
|
|
|
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),
|
|
};
|
|
}
|
|
|
|
const MAX_IMAGE_WIDTH = 4000;
|
|
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
|
|
|
|
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
|
|
|
|
/** @typedef {import('../photoswipe.js').Point} Point */
|
|
|
|
/** @typedef {import('../slide/slide.js').SlideData} SlideData */
|
|
|
|
/** @typedef {'fit' | 'fill' | number | ((zoomLevelObject: ZoomLevel) => number)} ZoomLevelOption */
|
|
|
|
/**
|
|
* Calculates zoom levels for specific slide.
|
|
* Depends on viewport size and image size.
|
|
*/
|
|
|
|
class ZoomLevel {
|
|
/**
|
|
* @param {PhotoSwipeOptions} options PhotoSwipe options
|
|
* @param {SlideData} itemData Slide data
|
|
* @param {number} index Slide index
|
|
* @param {PhotoSwipe} [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;
|
|
/** @type { Point | null } */
|
|
|
|
this.panAreaSize = null;
|
|
/** @type { Point | null } */
|
|
|
|
this.elementSize = null;
|
|
this.fit = 1;
|
|
this.fill = 1;
|
|
this.vFill = 1;
|
|
this.initial = 1;
|
|
this.secondary = 1;
|
|
this.max = 1;
|
|
this.min = 1;
|
|
}
|
|
/**
|
|
* Calculate initial, secondary and maximum zoom level for the specified slide.
|
|
*
|
|
* It should be called when either image or viewport size changes.
|
|
*
|
|
* @param {number} maxWidth
|
|
* @param {number} maxHeight
|
|
* @param {Point} panAreaSize
|
|
*/
|
|
|
|
update(maxWidth, maxHeight, panAreaSize) {
|
|
/** @type {Point} */
|
|
const elementSize = {
|
|
x: maxWidth,
|
|
y: maxHeight,
|
|
};
|
|
this.elementSize = elementSize;
|
|
this.panAreaSize = panAreaSize;
|
|
const hRatio = panAreaSize.x / elementSize.x;
|
|
const vRatio = panAreaSize.y / 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.
|
|
*
|
|
* @private
|
|
* @param {'initial' | 'secondary' | 'max'} optionPrefix Zoom level option prefix (initial, secondary, max)
|
|
* @returns { number | undefined }
|
|
*/
|
|
|
|
_parseZoomLevelOption(optionPrefix) {
|
|
const optionName =
|
|
/** @type {'initialZoomLevel' | 'secondaryZoomLevel' | 'maxZoomLevel'} */
|
|
optionPrefix + "ZoomLevel";
|
|
const optionValue = this.options[optionName];
|
|
|
|
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.
|
|
*
|
|
* @private
|
|
* @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 (
|
|
this.elementSize &&
|
|
currZoomLevel * this.elementSize.x > MAX_IMAGE_WIDTH
|
|
) {
|
|
currZoomLevel = MAX_IMAGE_WIDTH / this.elementSize.x;
|
|
}
|
|
|
|
return currZoomLevel;
|
|
}
|
|
/**
|
|
* Get initial image zoom level.
|
|
*
|
|
* @private
|
|
* @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.
|
|
*
|
|
* @private
|
|
* @return {number}
|
|
*/
|
|
|
|
_getMax() {
|
|
// max zoom level is x4 from "fit state",
|
|
// used for zoom gesture and ctrl/trackpad zoom
|
|
return this._parseZoomLevelOption("max") || 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 {SlideData} itemData Data about the slide
|
|
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
|
|
* @param {number} index
|
|
* @returns {Content} Image that is being decoded or false.
|
|
*/
|
|
|
|
function lazyLoadData(itemData, instance, index) {
|
|
const content = instance.createContentFromData(itemData, index);
|
|
/** @type {ZoomLevel | undefined} */
|
|
|
|
let zoomLevel;
|
|
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
|
|
|
|
if (options) {
|
|
zoomLevel = new ZoomLevel(options, itemData, -1);
|
|
let viewportSize;
|
|
|
|
if (instance.pswp) {
|
|
viewportSize = instance.pswp.viewportSize;
|
|
} else {
|
|
viewportSize = getViewportSize(options, instance);
|
|
}
|
|
|
|
const panAreaSize = getPanAreaSize(options, viewportSize, itemData, index);
|
|
zoomLevel.update(content.width, content.height, panAreaSize);
|
|
}
|
|
|
|
content.lazyLoad();
|
|
|
|
if (zoomLevel) {
|
|
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 {number} index Slide index
|
|
* @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox eventable instance
|
|
* @returns {Content | undefined}
|
|
*/
|
|
|
|
function lazyLoadSlide(index, instance) {
|
|
const itemData = instance.getItemData(index);
|
|
|
|
if (
|
|
instance.dispatch("lazyLoadSlide", {
|
|
index,
|
|
itemData,
|
|
}).defaultPrevented
|
|
) {
|
|
return;
|
|
}
|
|
|
|
return lazyLoadData(itemData, instance, index);
|
|
}
|
|
|
|
/** @typedef {import("../photoswipe.js").default} PhotoSwipe */
|
|
|
|
/** @typedef {import("../slide/slide.js").SlideData} SlideData */
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
getNumItems() {
|
|
var _this$options;
|
|
|
|
let numItems = 0;
|
|
const dataSource =
|
|
(_this$options = this.options) === null || _this$options === void 0
|
|
? void 0
|
|
: _this$options.dataSource;
|
|
|
|
if (dataSource && "length" in dataSource) {
|
|
// may be an array or just object with length property
|
|
numItems = dataSource.length;
|
|
} else if (dataSource && "gallery" in dataSource) {
|
|
// 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);
|
|
}
|
|
/**
|
|
* @param {SlideData} slideData
|
|
* @param {number} index
|
|
* @returns {Content}
|
|
*/
|
|
|
|
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 {number} index
|
|
* @returns {SlideData}
|
|
*/
|
|
|
|
getItemData(index) {
|
|
var _this$options2;
|
|
|
|
const dataSource =
|
|
(_this$options2 = this.options) === null || _this$options2 === void 0
|
|
? void 0
|
|
: _this$options2.dataSource;
|
|
/** @type {SlideData | HTMLElement} */
|
|
|
|
let dataSourceItem = {};
|
|
|
|
if (Array.isArray(dataSource)) {
|
|
// Datasource is an array of elements
|
|
dataSourceItem = dataSource[index];
|
|
} else if (dataSource && "gallery" in dataSource) {
|
|
// dataSource has gallery property,
|
|
// thus it was created by Lightbox, based on
|
|
// gallery and children 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 {HTMLElement} galleryElement
|
|
* @returns {HTMLElement[]}
|
|
*/
|
|
|
|
_getGalleryDOMElements(galleryElement) {
|
|
var _this$options3, _this$options4;
|
|
|
|
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)
|
|
) {
|
|
return (
|
|
getElementsFromOption(
|
|
this.options.children,
|
|
this.options.childSelector,
|
|
galleryElement,
|
|
) || []
|
|
);
|
|
}
|
|
|
|
return [galleryElement];
|
|
}
|
|
/**
|
|
* Converts DOM element to item data object.
|
|
*
|
|
* @param {HTMLElement} element DOM element
|
|
* @returns {SlideData}
|
|
*/
|
|
|
|
_domElementToItemData(element) {
|
|
/** @type {SlideData} */
|
|
const itemData = {
|
|
element,
|
|
};
|
|
const linkEl =
|
|
/** @type {HTMLAnchorElement} */
|
|
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 = linkEl.dataset.pswpWidth
|
|
? parseInt(linkEl.dataset.pswpWidth, 10)
|
|
: 0;
|
|
itemData.height = linkEl.dataset.pswpHeight
|
|
? parseInt(linkEl.dataset.pswpHeight, 10)
|
|
: 0; // 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) {
|
|
var _thumbnailEl$getAttri;
|
|
|
|
// 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$getAttri = thumbnailEl.getAttribute("alt")) !== null &&
|
|
_thumbnailEl$getAttri !== void 0
|
|
? _thumbnailEl$getAttri
|
|
: "";
|
|
}
|
|
|
|
if (linkEl.dataset.pswpCropped || linkEl.dataset.cropped) {
|
|
itemData.thumbCropped = true;
|
|
}
|
|
}
|
|
|
|
return this.applyFilters("domItemData", itemData, element, linkEl);
|
|
}
|
|
/**
|
|
* Lazy-load by slide data
|
|
*
|
|
* @param {SlideData} itemData Data about the slide
|
|
* @param {number} index
|
|
* @returns {Content} Image that is being decoded or false.
|
|
*/
|
|
|
|
lazyLoadData(itemData, index) {
|
|
return lazyLoadData(itemData, this, index);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @template T
|
|
* @typedef {import('../types.js').Type<T>} Type<T>
|
|
*/
|
|
|
|
/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
|
|
|
|
/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
|
|
|
|
/** @typedef {import('../photoswipe.js').DataSource} DataSource */
|
|
|
|
/** @typedef {import('../photoswipe.js').Point} Point */
|
|
|
|
/** @typedef {import('../slide/content.js').default} Content */
|
|
|
|
/** @typedef {import('../core/eventable.js').PhotoSwipeEventsMap} PhotoSwipeEventsMap */
|
|
|
|
/** @typedef {import('../core/eventable.js').PhotoSwipeFiltersMap} PhotoSwipeFiltersMap */
|
|
|
|
/**
|
|
* @template {keyof PhotoSwipeEventsMap} T
|
|
* @typedef {import('../core/eventable.js').EventCallback<T>} EventCallback<T>
|
|
*/
|
|
|
|
/**
|
|
* 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 {
|
|
/**
|
|
* @param {PhotoSwipeOptions} [options]
|
|
*/
|
|
constructor(options) {
|
|
super();
|
|
/** @type {PhotoSwipeOptions} */
|
|
|
|
this.options = options || {};
|
|
this._uid = 0;
|
|
this.shouldOpen = false;
|
|
/**
|
|
* @private
|
|
* @type {Content | undefined}
|
|
*/
|
|
|
|
this._preloadedContent = undefined;
|
|
this.onThumbnailsClick = this.onThumbnailsClick.bind(this);
|
|
}
|
|
/**
|
|
* Initialize lightbox, should be called only once.
|
|
* It's not included in the main constructor, so you may bind events before it.
|
|
*/
|
|
|
|
init() {
|
|
// Bind click events to each gallery
|
|
getElementsFromOption(
|
|
this.options.gallery,
|
|
this.options.gallerySelector,
|
|
).forEach((galleryElement) => {
|
|
galleryElement.addEventListener("click", this.onThumbnailsClick, false);
|
|
});
|
|
}
|
|
/**
|
|
* @param {MouseEvent} e
|
|
*/
|
|
|
|
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
|
|
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 the ideal way to detect them.
|
|
//
|
|
|
|
/** @type {Point | null} */
|
|
|
|
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);
|
|
/** @type {DataSource} */
|
|
|
|
const dataSource = {
|
|
gallery:
|
|
/** @type {HTMLElement} */
|
|
e.currentTarget,
|
|
};
|
|
|
|
if (clickedIndex >= 0) {
|
|
e.preventDefault();
|
|
this.loadAndOpen(clickedIndex, dataSource, initialPoint);
|
|
}
|
|
}
|
|
/**
|
|
* Get index of gallery item that was clicked.
|
|
*
|
|
* @param {MouseEvent} e click event
|
|
* @returns {number}
|
|
*/
|
|
|
|
getClickedIndex(e) {
|
|
// legacy option
|
|
if (this.options.getClickedIndexFn) {
|
|
return this.options.getClickedIndexFn.call(this, e);
|
|
}
|
|
|
|
const clickedTarget =
|
|
/** @type {HTMLElement} */
|
|
e.target;
|
|
const childElements = getElementsFromOption(
|
|
this.options.children,
|
|
this.options.childSelector,
|
|
/** @type {HTMLElement} */
|
|
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 {number} index
|
|
* @param {DataSource} [dataSource]
|
|
* @param {Point | null} [initialPoint]
|
|
* @returns {boolean}
|
|
*/
|
|
|
|
loadAndOpen(index, dataSource, initialPoint) {
|
|
// Check if the gallery is already open
|
|
if (window.pswp || !this.options) {
|
|
return false;
|
|
} // Use the first gallery element if dataSource is not provided
|
|
|
|
if (!dataSource && this.options.gallery && this.options.children) {
|
|
const galleryElements = getElementsFromOption(this.options.gallery);
|
|
|
|
if (galleryElements[0]) {
|
|
dataSource = {
|
|
gallery: galleryElements[0],
|
|
};
|
|
}
|
|
} // 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 {number} index
|
|
* @param {DataSource} [dataSource]
|
|
*/
|
|
|
|
preload(index, dataSource) {
|
|
const { options } = this;
|
|
|
|
if (dataSource) {
|
|
options.dataSource = dataSource;
|
|
} // Add the main module
|
|
|
|
/** @type {Promise<Type<PhotoSwipe>>[]} */
|
|
|
|
const promiseArray = [];
|
|
const pswpModuleType = typeof options.pswpModule;
|
|
|
|
if (isPswpClass(options.pswpModule)) {
|
|
promiseArray.push(
|
|
Promise.resolve(
|
|
/** @type {Type<PhotoSwipe>} */
|
|
options.pswpModule,
|
|
),
|
|
);
|
|
} else if (pswpModuleType === "string") {
|
|
throw new Error("pswpModule as string is no longer supported");
|
|
} else if (pswpModuleType === "function") {
|
|
promiseArray.push(
|
|
/** @type {() => Promise<Type<PhotoSwipe>>} */
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* @private
|
|
* @param {Type<PhotoSwipe> | { default: Type<PhotoSwipe> }} module
|
|
* @param {number} 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
|
|
*
|
|
* @type {PhotoSwipe}
|
|
*/
|
|
|
|
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
|
|
|
|
/** @type {(keyof PhotoSwipeEventsMap)[]} */
|
|
|
|
Object.keys(this._listeners).forEach((name) => {
|
|
var _this$_listeners$name;
|
|
|
|
(_this$_listeners$name = this._listeners[name]) === null ||
|
|
_this$_listeners$name === void 0 ||
|
|
_this$_listeners$name.forEach((fn) => {
|
|
pswp.on(
|
|
name,
|
|
/** @type {EventCallback<typeof name>} */
|
|
fn,
|
|
);
|
|
});
|
|
}); // same with filters
|
|
|
|
/** @type {(keyof PhotoSwipeFiltersMap)[]} */
|
|
|
|
Object.keys(this._filters).forEach((name) => {
|
|
var _this$_filters$name;
|
|
|
|
(_this$_filters$name = this._filters[name]) === null ||
|
|
_this$_filters$name === void 0 ||
|
|
_this$_filters$name.forEach((filter) => {
|
|
pswp.addFilter(name, filter.fn, filter.priority);
|
|
});
|
|
});
|
|
|
|
if (this._preloadedContent) {
|
|
pswp.contentLoader.addToCache(this._preloadedContent);
|
|
this._preloadedContent = undefined;
|
|
}
|
|
|
|
pswp.on("destroy", () => {
|
|
// clean up public variables
|
|
this.pswp = undefined;
|
|
delete window.pswp;
|
|
});
|
|
pswp.init();
|
|
}
|
|
/**
|
|
* Unbinds all events, closes PhotoSwipe if it's open.
|
|
*/
|
|
|
|
destroy() {
|
|
var _this$pswp;
|
|
|
|
(_this$pswp = this.pswp) === null ||
|
|
_this$pswp === void 0 ||
|
|
_this$pswp.destroy();
|
|
this.shouldOpen = false;
|
|
this._listeners = {};
|
|
getElementsFromOption(
|
|
this.options.gallery,
|
|
this.options.gallerySelector,
|
|
).forEach((galleryElement) => {
|
|
galleryElement.removeEventListener(
|
|
"click",
|
|
this.onThumbnailsClick,
|
|
false,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
export { PhotoSwipeLightbox as default };
|
|
//# sourceMappingURL=photoswipe-lightbox.esm.js.map
|