From 19aeb8a3344c15d534a77281ca90056ef2d1dbd1 Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Mon, 20 Apr 2020 18:04:09 +0700 Subject: [PATCH] photoswipe element --- package.json | 1 + .../node/NodeAudioImageBlock/index.tsx | 1 + src/components/node/NodeImageBlock/index.tsx | 58 ++++----- .../node/NodeImageSlideBlock/index.tsx | 44 ++++--- src/constants/dialogs.ts | 2 + src/containers/dialogs/PhotoSwipe/index.tsx | 121 ++++++++++++++++++ src/containers/dialogs/PhotoSwipe/styles.scss | 7 + src/containers/node/NodeLayout/index.tsx | 15 ++- src/redux/modal/actions.ts | 12 ++ src/redux/modal/constants.ts | 11 +- src/redux/modal/handlers.ts | 3 + src/redux/modal/reducer.ts | 10 +- src/redux/modal/sagas.ts | 17 +++ src/redux/node/constants.ts | 9 +- webpack.config.js | 2 +- yarn.lock | 5 + 16 files changed, 260 insertions(+), 58 deletions(-) create mode 100644 src/containers/dialogs/PhotoSwipe/index.tsx create mode 100644 src/containers/dialogs/PhotoSwipe/styles.scss diff --git a/package.json b/package.json index a7010af3..ed23e0ea 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "less-middleware": "~2.2.1", "lodash": "^4.17.10", "node-sass": "^4.11.0", + "photoswipe": "^4.1.3", "raleway-cyrillic": "^4.0.2", "ramda": "^0.26.1", "react": "16.13.0", diff --git a/src/components/node/NodeAudioImageBlock/index.tsx b/src/components/node/NodeAudioImageBlock/index.tsx index 95eb3fde..a00f4b1a 100644 --- a/src/components/node/NodeAudioImageBlock/index.tsx +++ b/src/components/node/NodeAudioImageBlock/index.tsx @@ -8,6 +8,7 @@ import { PRESETS } from '~/constants/urls'; interface IProps { node: INode; + modalShowPhotoswipe: any; } const NodeAudioImageBlock: FC = ({ node }) => { diff --git a/src/components/node/NodeImageBlock/index.tsx b/src/components/node/NodeImageBlock/index.tsx index 0ccd33d1..ef2f9f85 100644 --- a/src/components/node/NodeImageBlock/index.tsx +++ b/src/components/node/NodeImageBlock/index.tsx @@ -1,27 +1,22 @@ -import React, { - FC, - useMemo, - useState, - useEffect, - useRef, - useCallback -} from "react"; -import { ImageSwitcher } from "../ImageSwitcher"; -import * as styles from "./styles.scss"; -import { INode } from "~/redux/types"; -import classNames from "classnames"; -import { getURL } from "~/utils/dom"; -import { UPLOAD_TYPES } from "~/redux/uploads/constants"; -import { PRESETS } from "~/constants/urls"; +import React, { FC, useMemo, useState, useEffect, useRef, useCallback } from 'react'; +import { ImageSwitcher } from '../ImageSwitcher'; +import * as styles from './styles.scss'; +import { INode } from '~/redux/types'; +import classNames from 'classnames'; +import { getURL } from '~/utils/dom'; +import { UPLOAD_TYPES } from '~/redux/uploads/constants'; +import { PRESETS } from '~/constants/urls'; +import * as MODAL_ACTIONS from '~/redux/modal/actions'; interface IProps { is_loading: boolean; node: INode; layout: {}; updateLayout: () => void; + modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe; } -const NodeImageBlock: FC = ({ node, is_loading, updateLayout }) => { +const NodeImageBlock: FC = ({ node, is_loading, updateLayout, modalShowPhotoswipe }) => { const [is_animated, setIsAnimated] = useState(false); const [current, setCurrent] = useState(0); const [height, setHeight] = useState(320); @@ -30,36 +25,35 @@ const NodeImageBlock: FC = ({ node, is_loading, updateLayout }) => { const images = useMemo( () => - (node && - node.files && - node.files.filter(({ type }) => type === UPLOAD_TYPES.IMAGE)) || - [], + (node && node.files && node.files.filter(({ type }) => type === UPLOAD_TYPES.IMAGE)) || [], [node] ); const setRef = useCallback(index => el => (refs.current[index] = el), [refs]); - const onImageLoad = useCallback( - index => () => setLoaded({ ...loaded, [index]: true }), - [setLoaded, loaded] + const onImageLoad = useCallback(index => () => setLoaded({ ...loaded, [index]: true }), [ + setLoaded, + loaded, + ]); + + const onOpenPhotoSwipe = useCallback( + (index: number) => () => modalShowPhotoswipe(images, index), + [modalShowPhotoswipe, images] ); useEffect(() => updateLayout(), [loaded]); useEffect(() => { - if (!refs || !refs.current[current] || !loaded[current]) - return setHeight(320); + if (!refs || !refs.current[current] || !loaded[current]) return setHeight(320); const el = refs.current[current]; - const element_height = - el.getBoundingClientRect && el.getBoundingClientRect().height; + const element_height = el.getBoundingClientRect && el.getBoundingClientRect().height; setHeight(element_height); }, [refs, current, loaded]); useEffect(() => { const timer = setTimeout(() => setIsAnimated(true), 250); - return () => clearTimeout(timer); }, []); @@ -74,21 +68,19 @@ const NodeImageBlock: FC = ({ node, is_loading, updateLayout }) => { />
- {(is_loading || !loaded[0] || !images.length) && ( -
- )} + {(is_loading || !loaded[0] || !images.length) &&
} {images.map((file, index) => (
void; + modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe; } const getX = event => (event.touches ? event.touches[0].clientX : event.clientX); -const NodeImageSlideBlock: FC = ({ node, is_loading, updateLayout }) => { +const NodeImageSlideBlock: FC = ({ + node, + is_loading, + updateLayout, + modalShowPhotoswipe, +}) => { const [current, setCurrent] = useState(0); const [height, setHeight] = useState(320); const [max_height, setMaxHeight] = useState(960); @@ -39,6 +38,8 @@ const NodeImageSlideBlock: FC = ({ node, is_loading, updateLayout }) => const [initial_x, setInitialX] = useState(0); const [offset, setOffset] = useState(0); const [is_dragging, setIsDragging] = useState(false); + const [drag_start_time, setDragStartTime] = useState(0); + const slide = useRef(); const wrap = useRef(); @@ -166,12 +167,25 @@ const NodeImageSlideBlock: FC = ({ node, is_loading, updateLayout }) => normalizeOffset(); }, [wrap, setMaxHeight, normalizeOffset]); - const stopDragging = useCallback(() => { - if (!is_dragging) return; + const onOpenPhotoSwipe = useCallback(() => modalShowPhotoswipe(images, current), [ + modalShowPhotoswipe, + images, + current, + ]); - setIsDragging(false); - normalizeOffset(); - }, [setIsDragging, is_dragging, normalizeOffset]); + const stopDragging = useCallback( + event => { + if (!is_dragging) return; + + setIsDragging(false); + normalizeOffset(); + + if (initial_x - event.clientX < 10) { + onOpenPhotoSwipe(); + } + }, + [setIsDragging, is_dragging, normalizeOffset, onOpenPhotoSwipe] + ); const startDragging = useCallback( event => { diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index 0fd36b11..82b52a1e 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -10,6 +10,7 @@ import { ProfileDialog } from '~/containers/dialogs/ProfileDialog'; import { RestoreRequestDialog } from '~/containers/dialogs/RestoreRequestDialog'; import { RestorePasswordDialog } from '~/containers/dialogs/RestorePasswordDialog'; import { DIALOGS } from '~/redux/modal/constants'; +import { PhotoSwipe } from '~/containers/dialogs/PhotoSwipe'; export const DIALOG_CONTENT = { [DIALOGS.EDITOR_IMAGE]: EditorDialogImage, @@ -22,6 +23,7 @@ export const DIALOG_CONTENT = { [DIALOGS.PROFILE]: ProfileDialog, [DIALOGS.RESTORE_REQUEST]: RestoreRequestDialog, [DIALOGS.RESTORE_PASSWORD]: RestorePasswordDialog, + [DIALOGS.PHOTOSWIPE]: PhotoSwipe, }; export const NODE_EDITOR_DIALOGS = { diff --git a/src/containers/dialogs/PhotoSwipe/index.tsx b/src/containers/dialogs/PhotoSwipe/index.tsx new file mode 100644 index 00000000..1a27d896 --- /dev/null +++ b/src/containers/dialogs/PhotoSwipe/index.tsx @@ -0,0 +1,121 @@ +import React, { FC, useRef, useEffect, useMemo, useCallback } from 'react'; +import { connect } from 'react-redux'; + +import PhotoSwipeJs from 'photoswipe/dist/photoswipe.js'; +import PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default.js'; +import 'photoswipe/dist/photoswipe.css'; +import 'photoswipe/dist/default-skin/default-skin.css'; +import { IState } from '~/redux/store'; +import { selectModal } from '~/redux/modal/selectors'; +import { getURL } from '~/utils/dom'; +import { PRESETS } from '~/constants/urls'; +import * as MODAL_ACTIONS from '~/redux/modal/actions'; +import styles from './styles.scss'; +import classNames from 'classnames'; + +const mapStateToProps = (state: IState) => ({ + photoswipe: selectModal(state).photoswipe, +}); + +const mapDispatchToProps = { + modalSetShown: MODAL_ACTIONS.modalSetShown, +}; + +type Props = ReturnType & typeof mapDispatchToProps & {}; + +const PhotoSwipeUnconnected: FC = ({ photoswipe, modalSetShown }) => { + let ref = useRef(null); + + const items = useMemo( + () => + photoswipe.images.map(image => ({ + src: getURL(image, window.innerWidth < 768 ? PRESETS[900] : ''), + })), + [photoswipe.images] + ); + + const closeModal = useCallback(() => modalSetShown(false), [modalSetShown]); + + useEffect(() => { + new Promise(async resolve => { + const images = await Promise.all( + items.map( + item => + new Promise(resolveImage => { + const img = new Image(); + + img.onload = () => { + resolveImage({ + src: item.src, + h: img.naturalHeight, + w: img.naturalWidth, + }); + }; + + img.onerror = () => { + resolveImage({}); + }; + + img.src = item.src; + }) + ) + ); + + resolve(images); + }).then(images => { + const ps = new PhotoSwipeJs(ref.current, PhotoSwipeUI_Default, images, { + index: photoswipe.index || 0, + closeOnScroll: false, + history: false, + }); + + ps.init(); + ps.listen('destroy', closeModal); + ps.listen('close', closeModal); + }); + }, [items, photoswipe.index]); + + return ( +