UNPKG

photoswipe

Version:
298 lines (256 loc) 8.87 kB
import { specialKeyUsed, getElementsFromOption, isPswpClass } from '../util/util.js'; import PhotoSwipeBase from '../core/base.js'; import { lazyLoadSlide } from '../slide/loader.js'; /** * @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) { 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 {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) => { 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) => { 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() { this.pswp?.destroy(); this.shouldOpen = false; this._listeners = {}; getElementsFromOption(this.options.gallery, this.options.gallerySelector) .forEach((galleryElement) => { galleryElement.removeEventListener('click', this.onThumbnailsClick, false); }); } } export default PhotoSwipeLightbox;