/*! * PhotoSwipe Lightbox 5.2.4 - 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; } /** * Check if variable is PhotoSwipe class * * @param {*} fn * @returns Boolean */ function isPswpClass(fn) { return typeof fn === 'function' && fn.prototype && fn.prototype.goTo; } /** * 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 (isPswpClass(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