UNPKG

react-photoswipe-gallery

Version:
344 lines 12 kB
import PhotoSwipe from 'photoswipe'; import React, { useRef, useCallback, useEffect, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import PropTypes from 'prop-types'; import objectToHash from "./helpers/object-to-hash.js"; import hashToObject from "./helpers/hash-to-object.js"; import getHashWithoutGidAndPid from "./helpers/get-hash-without-gid-and-pid.js"; import getHashValue from "./helpers/get-hash-value.js"; import getBaseUrl from "./helpers/get-base-url.js"; import hashIncludesNavigationQueryParams from "./helpers/hash-includes-navigation-query-params.js"; import getInitialActiveSlideIndex from "./helpers/get-initial-active-slide-index.js"; import { Context } from "./context.js"; import PhotoSwipeLightboxStub from "./lightbox-stub.js"; import getSlidesAndIndexFromDataSource from "./helpers/get-slides-and-index-from-data-source.js"; import getSlidesAndIndexFromItemsRefs from "./helpers/get-slides-and-index-from-items-refs.js"; /** * This variable stores the PhotoSwipe instance object * It aims to check whether does the PhotoSwipe opened at the moment * (analog of window.pswp in 'photoswipe/lightbox') */ let pswp = null; /** * Gallery component providing photoswipe context */ export const Gallery = ({ children, dataSource, options, plugins, uiElements, id: galleryUID, onBeforeOpen, onOpen, withCaption, withDownloadButton }) => { const [contentPortal, setContentPortal] = useState(null); const items = useRef(new Map()); /** * Store PID from hash if there are no items yet, * but we need to open photoswipe if items appear in the next render */ const openWhenReadyPid = useRef(null); const open = useCallback((targetRef, targetId, itemIndex, e) => { // only one photoswipe instance could be opened at once // so if photoswipe is already open, function should do nothing if (pswp) { return; } const { slides, index } = dataSource ? getSlidesAndIndexFromDataSource(dataSource, items, targetRef, targetId, itemIndex) : getSlidesAndIndexFromItemsRefs( // eslint-disable-next-line prettier/prettier items, targetRef, targetId, itemIndex); const initialPoint = e && e.clientX !== undefined && e.clientY !== undefined ? { x: e.clientX, y: e.clientY } : null; const instance = new PhotoSwipe(Object.assign({ dataSource: slides, index: getInitialActiveSlideIndex(index, targetId), initialPointerPos: initialPoint }, options || {})); pswp = instance; instance.on('contentActivate', ({ content: slideContent }) => { if (slideContent.data.content) { setContentPortal(createPortal(slideContent.data.content, slideContent.element)); } else { setContentPortal(null); } }); instance.on('close', () => { setContentPortal(null); }); if (withDownloadButton) { instance.on('uiRegister', () => { var _a; (_a = instance.ui) === null || _a === void 0 ? void 0 : _a.registerElement({ name: 'download-button', ariaLabel: 'Download', order: 8, isButton: true, tagName: 'a', appendTo: 'bar', html: { isCustomSVG: true, inner: '<path d="M20.5 14.3 17.1 18V10h-2.2v7.9l-3.4-3.6L10 16l6 6.1 6-6.1ZM23 23H9v2h14Z" id="pswp__icn-download"/>', outlineID: 'pswp__icn-download' }, // can't test onInit callback correctly onInit: /* istanbul ignore next */(el, pswpInstance) => { el.setAttribute('download', ''); el.setAttribute('target', '_blank'); el.setAttribute('rel', 'noopener'); instance.on('change', () => { var _a; if (!((_a = pswpInstance.currSlide) === null || _a === void 0 ? void 0 : _a.data.src)) { return; } const downloadButton = el; downloadButton.href = pswpInstance.currSlide.data.src; }); } }); }); } if (withCaption) { instance.on('uiRegister', () => { var _a; (_a = instance.ui) === null || _a === void 0 ? void 0 : _a.registerElement({ name: 'default-caption', order: 9, isButton: false, appendTo: 'root', // can't test onInit callback correctly onInit: /* istanbul ignore next */(el, pswpInstance) => { /* eslint-disable no-param-reassign */ el.style.position = 'absolute'; el.style.bottom = '15px'; el.style.left = '0'; el.style.right = '0'; el.style.padding = '0 20px'; el.style.color = 'var(--pswp-icon-color)'; el.style.textAlign = 'center'; el.style.fontSize = '14px'; el.style.lineHeight = '1.5'; el.style.textShadow = '1px 1px 3px var(--pswp-icon-color-secondary)'; /* eslint-enable no-param-reassign */ instance.on('change', () => { if (!pswpInstance.currSlide) { return; } const { caption, alt } = pswpInstance.currSlide.data; // eslint-disable-next-line no-param-reassign el.innerHTML = caption || alt || ''; }); } }); }); } if (Array.isArray(uiElements)) { uiElements.forEach(uiElement => { instance.on('uiRegister', () => { var _a; (_a = instance.ui) === null || _a === void 0 ? void 0 : _a.registerElement(uiElement); }); }); } if (typeof plugins === 'function') { plugins(new PhotoSwipeLightboxStub(instance)); } if (typeof onBeforeOpen === 'function') { onBeforeOpen(instance); } const getHistoryState = () => { return { gallery: { galleryUID } }; }; instance.on('beforeOpen', () => { var _a; if (galleryUID === undefined) { return; } const hashIncludesGidAndPid = hashIncludesNavigationQueryParams(getHashValue()); // was openned by react-photoswipe-gallery's open() method call (click on thumbnail, for example) // we need to create new history record to store hash navigation state if (!hashIncludesGidAndPid) { window.history.pushState(getHistoryState(), document.title); return; } const hasGalleryStateInHistory = Boolean((_a = window.history.state) === null || _a === void 0 ? void 0 : _a.gallery); // was openned by history.forward() // we do not need to create new history record for hash navigation // because we already have one if (hasGalleryStateInHistory) { return; } // was openned by link with gid and pid const baseUrl = getBaseUrl(); const currentHash = getHashValue(); const hashWithoutGidAndPid = getHashWithoutGidAndPid(currentHash); const urlWithoutOpenedSlide = `${baseUrl}${hashWithoutGidAndPid ? `#${hashWithoutGidAndPid}` : ''}`; const urlWithOpenedSlide = `${baseUrl}#${currentHash}`; // firstly, we need to modify current history record - set url without gid and pid // we will return to this state after photoswipe closing window.history.replaceState(window.history.state, document.title, urlWithoutOpenedSlide); // then we need to create new history record to store hash navigation state window.history.pushState(getHistoryState(), document.title, urlWithOpenedSlide); }); instance.on('change', () => { var _a; if (galleryUID === undefined) { return; } const pid = ((_a = instance.currSlide) === null || _a === void 0 ? void 0 : _a.data.pid) || instance.currIndex + 1; const baseUrl = getBaseUrl(); const baseHash = getHashWithoutGidAndPid(getHashValue()); const gidAndPidHash = objectToHash({ gid: galleryUID, pid }); const urlWithOpenedSlide = `${baseUrl}#${baseHash}&${gidAndPidHash}`; // updates in current history record hash value with actual pid window.history.replaceState(getHistoryState(), document.title, urlWithOpenedSlide); }); const closeGalleryOnHistoryPopState = () => { if (galleryUID === undefined) { return; } if (pswp !== null) { pswp.close(); } }; window.addEventListener('popstate', closeGalleryOnHistoryPopState); instance.on('destroy', () => { if (galleryUID !== undefined) { window.removeEventListener('popstate', closeGalleryOnHistoryPopState); // if hash includes gid and pid => this destroy was called with ordinary instance.close() call // if not => destroy was called by history.back (browser's back button) => history has been already returned to previous state if (hashIncludesNavigationQueryParams(getHashValue())) { window.history.back(); } } pswp = null; }); instance.init(); if (typeof onOpen === 'function') { onOpen(instance); } }, [options, plugins, uiElements, galleryUID, onBeforeOpen, onOpen, withCaption, withDownloadButton]); useEffect(() => { return () => { if (pswp) { pswp.close(); } }; }, []); const openGalleryBasedOnUrlHash = useCallback(() => { if (galleryUID === undefined) { return; } if (pswp !== null) { return; } const hash = getHashValue(); if (hash.length < 5) { return; } const params = hashToObject(hash); const { pid, gid } = params; if (!pid || !gid) { return; } if (items.current.size === 0) { // no items currently, save PID from hash for future use openWhenReadyPid.current = pid; return; } if (pid && gid === String(galleryUID)) { open(null, pid); } }, [open, galleryUID]); useEffect(() => { openGalleryBasedOnUrlHash(); // needed for case when gallery was firstly opened, then was closed and user clicked on browser's forward button window.addEventListener('popstate', openGalleryBasedOnUrlHash); return () => { window.removeEventListener('popstate', openGalleryBasedOnUrlHash); }; }, [openGalleryBasedOnUrlHash]); const remove = useCallback(ref => { items.current.delete(ref); }, []); const set = useCallback((ref, data) => { items.current.set(ref, data); if (openWhenReadyPid.current === null) { return; } const { id } = data; if (id === openWhenReadyPid.current) { // user provided `id` prop of Item component open(ref); openWhenReadyPid.current = null; return; } if (!id) { // in this case we using index of item as PID const index = parseInt(openWhenReadyPid.current, 10) - 1; const refToOpen = Array.from(items.current.keys())[index]; if (refToOpen) { open(refToOpen); openWhenReadyPid.current = null; } } }, [open]); const isRefRegistered = useCallback(ref => { return items.current.has(ref); }, []); const openAt = useCallback(index => { open(null, null, index); }, [open]); const close = useCallback(() => { if (pswp) { pswp.close(); } }, []); const contextValue = useMemo(() => ({ remove, set, handleClick: open, open: openAt, close, isRefRegistered }), [remove, set, open, openAt, close, isRefRegistered]); return React.createElement(Context.Provider, { value: contextValue }, children, contentPortal); }; Gallery.propTypes = { children: PropTypes.any, options: PropTypes.object, plugins: PropTypes.func, uiElements: PropTypes.array, id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), onBeforeOpen: PropTypes.func, onOpen: PropTypes.func, withCaption: PropTypes.bool, withDownloadButton: PropTypes.bool, dataSource: PropTypes.array };