diff --git a/src/components/node/NodeImageSlideBlock/index.tsx b/src/components/node/NodeImageSlideBlock/index.tsx new file mode 100644 index 00000000..c2efff13 --- /dev/null +++ b/src/components/node/NodeImageSlideBlock/index.tsx @@ -0,0 +1,150 @@ +import React, { + FC, + useMemo, + useState, + useEffect, + RefObject, + LegacyRef, + useRef, + useCallback, + MouseEventHandler, +} from 'react'; +import { ImageSwitcher } from '../ImageSwitcher'; +import * as styles from './styles.scss'; +import { INode } from '~/redux/types'; +import classNames from 'classnames'; +import { getImageSize } from '~/utils/dom'; +import { UPLOAD_TYPES } from '~/redux/uploads/constants'; + +interface IProps { + is_loading: boolean; + node: INode; + layout: {}; + updateLayout: () => void; +} + +const NodeImageSlideBlock: FC<IProps> = ({ node, is_loading, updateLayout }) => { + const [is_animated, setIsAnimated] = useState(false); + const [current, setCurrent] = useState(0); + const [height, setHeight] = useState(320); + const [loaded, setLoaded] = useState<Record<number, boolean>>({}); + const refs = useRef<Record<number, HTMLDivElement>>({}); + const [heights, setHeights] = useState({}); + + const [initial_offset, setInitialOffset] = useState(0); + const [initial_x, setInitialX] = useState(0); + const [offset, setOffset] = useState(0); + const [is_dragging, setIsDragging] = useState(false); + const slide = useRef(); + + const images = useMemo( + () => + (node && node.files && node.files.filter(({ type }) => type === UPLOAD_TYPES.IMAGE)) || [], + [node] + ); + + // console.log({ heights }); + + const updateSizes = useCallback(() => { + const values = Object.keys(refs.current).map(key => { + const ref = refs.current[key]; + if (!ref || !ref.getBoundingClientRect) return 0; + return ref.getBoundingClientRect().height; + }); + }, [refs]); + + const setRef = useCallback( + index => el => { + refs.current[index] = el; + }, + [refs, heights, setHeights] + ); + + const onImageLoad = useCallback(index => () => setLoaded({ ...loaded, [index]: true }), [ + setLoaded, + loaded, + ]); + + // update outside hooks + useEffect(() => updateLayout(), [loaded]); + + useEffect(() => { + updateSizes(); + // + // if (!refs || !refs.current[current] || !loaded[current]) return setHeight(320); + // + // const el = refs.current[current]; + // + // const element_height = el.getBoundingClientRect && el.getBoundingClientRect().height; + // + // setHeight(element_height); + }, [refs, current, loaded]); + + // useEffect(() => { + // const timer = setTimeout(() => setIsAnimated(true), 250); + // + // return () => clearTimeout(timer); + // }, []); + + const stopDragging = useCallback(() => { + window.removeEventListener('mouseup', stopDragging); + setIsDragging(false); + }, [setIsDragging]); + + const startDragging: MouseEventHandler<HTMLDivElement> = useCallback( + event => { + window.addEventListener('mouseup', stopDragging); + setIsDragging(true); + setInitialX(event.clientX); + setInitialOffset(offset); + }, + [setIsDragging, stopDragging, setInitialX, offset, setInitialOffset] + ); + + const onDrag = useCallback( + event => { + if (!is_dragging) return; + + setOffset(initial_offset + event.clientX - initial_x); + }, + [is_dragging, initial_x, setOffset, initial_offset] + ); + + return ( + <div className={classNames(styles.wrap, { is_loading, is_animated })}> + <div + className={styles.image_container} + style={{ + height, + transform: `translate(${offset}px, 0)`, + width: `${images.length * 100}%`, + }} + onMouseDown={startDragging} + onMouseMove={onDrag} + ref={slide} + > + {(is_loading || !loaded[0] || !images.length) && <div className={styles.placeholder} />} + + {images.map((file, index) => ( + <div + className={classNames(styles.image_wrap, { + is_active: index === current && loaded[index], + })} + ref={setRef(index)} + key={file.id} + > + <img + className={styles.image} + src={getImageSize(file, 'node')} + alt="" + key={file.id} + onLoad={onImageLoad(index)} + /> + </div> + ))} + </div> + </div> + ); +}; + +export { NodeImageSlideBlock }; diff --git a/src/components/node/NodeImageSlideBlock/styles.scss b/src/components/node/NodeImageSlideBlock/styles.scss new file mode 100644 index 00000000..c36ee620 --- /dev/null +++ b/src/components/node/NodeImageSlideBlock/styles.scss @@ -0,0 +1,56 @@ +.wrap { + overflow: hidden; + position: relative; + min-width: 0; + width: 100%; + + &:global(.is_animated) { + .image_container { + transition: height 0.5s; + } + + .image_wrap { + transition: opacity 0.5s; + } + } +} + +.image_container { + background: $node_image_bg; + border-radius: $panel_radius 0 0 $panel_radius; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + user-select: none; + + .image { + max-height: 960px; + max-width: 100%; + opacity: 1; + border-radius: $radius $radius 0 0; + } +} + +.image_wrap { + width: 100%; + // top: 0; + // left: 0; + // opacity: 0; + pointer-events: none; + touch-action: none; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + + &:global(.is_active) { + opacity: 1; + } +} + +.placeholder { + background: red; + height: 320px; +} diff --git a/src/redux/node/constants.ts b/src/redux/node/constants.ts index 5f34723a..64bd54c8 100644 --- a/src/redux/node/constants.ts +++ b/src/redux/node/constants.ts @@ -1,6 +1,6 @@ import { FC } from 'react'; import { IBlock, INode, ValueOf, IComment } from '../types'; -import { NodeImageBlock } from '~/components/node/NodeImageBlock'; +import { NodeImageSlideBlock } from '~/components/node/NodeImageSlideBlock'; import { ImageEditor } from '~/components/editors/ImageEditor'; import { TextEditor } from '~/components/editors/TextEditor'; import { DIALOGS } from '../modal/constants'; @@ -63,7 +63,7 @@ type INodeComponents = Record< >; export const NODE_COMPONENTS: INodeComponents = { - [NODE_TYPES.IMAGE]: NodeImageBlock, + [NODE_TYPES.IMAGE]: NodeImageSlideBlock, }; export const EMPTY_COMMENT: IComment = {