2022-02-02 13:44:33 +01:00
|
|
|
// @flow
|
2022-02-02 13:48:24 +01:00
|
|
|
import 'scss/component/_swipeable-drawer.scss';
|
|
|
|
|
2022-02-02 13:44:33 +01:00
|
|
|
// $FlowFixMe
|
|
|
|
import { Global } from '@emotion/react';
|
|
|
|
// $FlowFixMe
|
|
|
|
import { grey } from '@mui/material/colors';
|
2022-02-02 13:45:16 +01:00
|
|
|
|
2022-02-23 22:13:22 +01:00
|
|
|
import { HEADER_HEIGHT_MOBILE } from 'component/fileRenderFloating/view';
|
|
|
|
import { PRIMARY_PLAYER_WRAPPER_CLASS, PRIMARY_IMAGE_WRAPPER_CLASS } from 'page/file/view';
|
2022-04-04 14:13:15 +02:00
|
|
|
import { getMaxLandscapeHeight } from 'component/fileRenderFloating/helper-functions';
|
2022-05-18 18:15:12 +02:00
|
|
|
import Drawer from '@mui/material/Drawer';
|
2022-02-02 13:44:33 +01:00
|
|
|
import * as ICONS from 'constants/icons';
|
|
|
|
import * as React from 'react';
|
|
|
|
import Button from 'component/button';
|
2022-02-07 14:04:25 +01:00
|
|
|
import classnames from 'classnames';
|
2022-02-02 13:44:33 +01:00
|
|
|
|
|
|
|
const DRAWER_PULLER_HEIGHT = 42;
|
2022-05-18 18:15:12 +02:00
|
|
|
const TRANSITION_MS = 225;
|
|
|
|
const TRANSITION_STR = `${TRANSITION_MS}ms cubic-bezier(0, 0, 0.2, 1) 0ms`;
|
2022-02-02 13:44:33 +01:00
|
|
|
|
|
|
|
type Props = {
|
|
|
|
children: Node,
|
2022-02-02 13:45:16 +01:00
|
|
|
title: any,
|
2022-02-07 14:04:25 +01:00
|
|
|
hasSubtitle?: boolean,
|
2022-02-02 13:45:16 +01:00
|
|
|
actions?: any,
|
2022-04-04 14:13:15 +02:00
|
|
|
// -- redux --
|
|
|
|
open: boolean,
|
|
|
|
theme: string,
|
2022-02-02 13:44:33 +01:00
|
|
|
toggleDrawer: () => void,
|
|
|
|
};
|
|
|
|
|
|
|
|
export default function SwipeableDrawer(props: Props) {
|
2022-04-04 14:13:15 +02:00
|
|
|
const { title, hasSubtitle, children, open, theme, actions, toggleDrawer } = props;
|
2022-02-02 13:44:33 +01:00
|
|
|
|
2022-05-18 18:15:12 +02:00
|
|
|
const drawerRoot = React.useRef();
|
|
|
|
const backdropRef = React.useRef();
|
|
|
|
const paperRef = React.useRef();
|
2022-02-02 13:44:33 +01:00
|
|
|
|
2022-05-18 18:15:12 +02:00
|
|
|
const pausedByDrawer = React.useRef(false);
|
|
|
|
const touchPos = React.useRef();
|
|
|
|
const openPrev = React.useRef(open);
|
|
|
|
|
|
|
|
const [playerHeight, setPlayerHeight] = React.useState(getMaxLandscapeHeight());
|
|
|
|
|
|
|
|
function handleTouchMove(e) {
|
|
|
|
const touchPosY = e.touches[0].clientY;
|
|
|
|
touchPos.current = touchPosY;
|
|
|
|
const draggingBelowHeader = touchPosY > HEADER_HEIGHT_MOBILE;
|
|
|
|
|
|
|
|
if (draggingBelowHeader) {
|
|
|
|
const root = drawerRoot.current;
|
|
|
|
if (root) {
|
|
|
|
root.setAttribute('style', `transform: none !important`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (paperRef.current) {
|
|
|
|
paperRef.current.setAttribute('style', `transform: translateY(${touchPosY}px) !important`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// makes the backdrop lighter/darker based on how high/low the drawer is
|
|
|
|
const backdrop = backdropRef.current;
|
|
|
|
if (backdrop) {
|
|
|
|
const isDraggingAboveVideo = touchPosY < playerHeight + HEADER_HEIGHT_MOBILE;
|
|
|
|
let backdropTop = HEADER_HEIGHT_MOBILE + playerHeight;
|
|
|
|
// $FlowFixMe
|
|
|
|
let backdropHeight = document.documentElement.getBoundingClientRect().height - backdropTop;
|
|
|
|
let opacity = ((touchPosY - HEADER_HEIGHT_MOBILE) / backdropHeight) * -1 + 1;
|
|
|
|
|
|
|
|
// increase the backdrop height so it also covers the video when pulling the drawer up
|
|
|
|
if (isDraggingAboveVideo) {
|
|
|
|
backdropTop = HEADER_HEIGHT_MOBILE;
|
|
|
|
backdropHeight = playerHeight;
|
|
|
|
opacity = ((touchPosY - HEADER_HEIGHT_MOBILE) / backdropHeight) * -1 + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
backdrop.setAttribute('style', `top: ${backdropTop}px; opacity: ${opacity}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleTouchEnd() {
|
|
|
|
// set by touchMove
|
|
|
|
if (!touchPos.current) return;
|
|
|
|
|
|
|
|
const root = drawerRoot.current;
|
|
|
|
|
|
|
|
if (root) {
|
|
|
|
const middleOfVideo = HEADER_HEIGHT_MOBILE + playerHeight / 2;
|
|
|
|
const drawerMovedFullscreen = touchPos.current < middleOfVideo;
|
|
|
|
// $FlowFixMe
|
|
|
|
const restOfPage = document.documentElement.clientHeight - playerHeight - HEADER_HEIGHT_MOBILE;
|
|
|
|
const draggedBeforeCloseLimit = touchPos.current - playerHeight - HEADER_HEIGHT_MOBILE < restOfPage * 0.2;
|
|
|
|
const backdrop = backdropRef.current;
|
|
|
|
|
|
|
|
if (draggedBeforeCloseLimit) {
|
|
|
|
const minDrawerHeight = HEADER_HEIGHT_MOBILE + playerHeight;
|
|
|
|
const positionToStop = drawerMovedFullscreen ? HEADER_HEIGHT_MOBILE : minDrawerHeight;
|
|
|
|
|
|
|
|
if (paperRef.current) {
|
|
|
|
paperRef.current.setAttribute('style', `transform: none !important; transition: transform ${TRANSITION_STR}`);
|
|
|
|
}
|
|
|
|
root.setAttribute(
|
|
|
|
'style',
|
|
|
|
`transform: translateY(${positionToStop}px) !important; transition: transform ${TRANSITION_STR}`
|
|
|
|
);
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
root.style.height = `calc(100% - ${positionToStop}px)`;
|
|
|
|
}, TRANSITION_MS);
|
|
|
|
|
|
|
|
if (backdrop) {
|
|
|
|
backdrop.setAttribute('style', 'opacity: 0');
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
backdrop.setAttribute('style', `transition: opacity ${TRANSITION_STR}; opacity: 1`);
|
|
|
|
}, TRANSITION_MS);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pause video if drawer made fullscreen (above the player)
|
|
|
|
const playerElement = document.querySelector('.content__viewer--inline');
|
|
|
|
const videoParent = playerElement && playerElement.querySelector('.video-js');
|
|
|
|
const isLivestream = videoParent && videoParent.classList.contains('livestreamPlayer');
|
|
|
|
const videoNode = videoParent && videoParent.querySelector('.vjs-tech');
|
|
|
|
// $FlowFixMe
|
|
|
|
const isPlaying = videoNode && !videoNode.paused;
|
|
|
|
|
|
|
|
if (videoNode && !isLivestream && isPlaying && drawerMovedFullscreen) {
|
|
|
|
// $FlowFixMe
|
|
|
|
videoNode.pause();
|
|
|
|
pausedByDrawer.current = true;
|
|
|
|
} else {
|
|
|
|
handleUnpausePlayer();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
handleCloseDrawer();
|
|
|
|
|
|
|
|
if (backdrop) {
|
|
|
|
backdrop.setAttribute('style', 'opacity: 0');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// clear if not being touched anymore
|
|
|
|
touchPos.current = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleUnpausePlayer() {
|
|
|
|
// Unpause on close and was paused by the drawer
|
|
|
|
const videoParent = document.querySelector('.video-js');
|
|
|
|
const videoNode = videoParent && videoParent.querySelector('.vjs-tech');
|
|
|
|
|
|
|
|
if (videoNode && pausedByDrawer.current) {
|
|
|
|
// $FlowFixMe
|
|
|
|
videoNode.play();
|
|
|
|
pausedByDrawer.current = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleCloseDrawer() {
|
|
|
|
handleUnpausePlayer();
|
|
|
|
toggleDrawer();
|
|
|
|
}
|
2022-02-23 22:13:22 +01:00
|
|
|
|
|
|
|
const handleResize = React.useCallback(() => {
|
|
|
|
const element =
|
|
|
|
document.querySelector(`.${PRIMARY_IMAGE_WRAPPER_CLASS}`) ||
|
|
|
|
document.querySelector(`.${PRIMARY_PLAYER_WRAPPER_CLASS}`);
|
|
|
|
|
|
|
|
if (!element) return;
|
|
|
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
2022-05-18 18:15:12 +02:00
|
|
|
setPlayerHeight(rect.height);
|
2022-02-23 22:13:22 +01:00
|
|
|
}, []);
|
2022-02-02 13:44:33 +01:00
|
|
|
|
|
|
|
React.useEffect(() => {
|
2022-02-23 22:13:22 +01:00
|
|
|
// Drawer will follow the cover image on resize, so it's always visible
|
2022-05-18 18:15:12 +02:00
|
|
|
handleResize();
|
2022-02-02 13:44:33 +01:00
|
|
|
|
2022-05-18 18:15:12 +02:00
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
return () => window.removeEventListener('resize', handleResize);
|
|
|
|
}, [handleResize]);
|
2022-02-02 13:44:33 +01:00
|
|
|
|
2022-02-02 13:49:02 +01:00
|
|
|
// Reset scroll position when opening: avoid broken position where
|
|
|
|
// the drawer is lower than the video
|
|
|
|
React.useEffect(() => {
|
|
|
|
if (open) {
|
|
|
|
const htmlEl = document.querySelector('html');
|
|
|
|
if (htmlEl) htmlEl.scrollTop = 0;
|
|
|
|
}
|
|
|
|
}, [open]);
|
2022-02-02 13:44:33 +01:00
|
|
|
|
2022-05-18 18:15:12 +02:00
|
|
|
React.useEffect(() => {
|
|
|
|
return () => {
|
|
|
|
if (openPrev.current) {
|
|
|
|
handleCloseDrawer();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// close drawer on unmount
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const drawerElemRef = React.useCallback(
|
|
|
|
(node) => {
|
|
|
|
if (node) {
|
|
|
|
const isFullscreenDrawer = node.style.transform.includes(`translateY(${HEADER_HEIGHT_MOBILE}px)`);
|
|
|
|
const openStateChanged = openPrev.current !== open; // so didn't run because of window resize
|
|
|
|
|
|
|
|
if (!isFullscreenDrawer || openStateChanged) {
|
|
|
|
node.setAttribute(
|
|
|
|
'style',
|
|
|
|
`transform: translateY(${HEADER_HEIGHT_MOBILE + playerHeight}px); height: calc(100% - ${
|
|
|
|
HEADER_HEIGHT_MOBILE + playerHeight
|
|
|
|
}px);`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
drawerRoot.current = node;
|
|
|
|
openPrev.current = open;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[open, playerHeight]
|
|
|
|
);
|
|
|
|
|
2022-02-02 13:44:33 +01:00
|
|
|
return (
|
|
|
|
<>
|
2022-05-18 18:15:12 +02:00
|
|
|
<DrawerGlobalStyles open={open} />
|
2022-02-02 13:44:33 +01:00
|
|
|
|
2022-05-18 18:15:12 +02:00
|
|
|
<Drawer
|
|
|
|
ref={drawerElemRef}
|
2022-02-02 13:44:33 +01:00
|
|
|
anchor="bottom"
|
|
|
|
open={open}
|
|
|
|
disableEnforceFocus
|
2022-05-18 18:15:12 +02:00
|
|
|
ModalProps={{ keepMounted: true, sx: { zIndex: '2' } }}
|
|
|
|
BackdropProps={{ ref: backdropRef, open, sx: { backgroundColor: 'black' } }}
|
|
|
|
PaperProps={{ ref: paperRef, sx: { height: `calc(100% - ${DRAWER_PULLER_HEIGHT}px)` } }}
|
2022-02-02 13:44:33 +01:00
|
|
|
>
|
2022-02-02 13:45:16 +01:00
|
|
|
{open && (
|
2022-02-02 13:44:33 +01:00
|
|
|
<div className="swipeable-drawer__header" style={{ top: -DRAWER_PULLER_HEIGHT }}>
|
2022-02-02 13:48:24 +01:00
|
|
|
<Puller theme={theme} />
|
2022-05-18 18:15:12 +02:00
|
|
|
<HeaderContents
|
|
|
|
title={title}
|
|
|
|
hasSubtitle={hasSubtitle}
|
|
|
|
actions={actions}
|
|
|
|
handleClose={handleCloseDrawer}
|
|
|
|
onTouchMove={handleTouchMove}
|
|
|
|
onTouchEnd={handleTouchEnd}
|
|
|
|
/>
|
2022-02-02 13:44:33 +01:00
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{children}
|
2022-05-18 18:15:12 +02:00
|
|
|
</Drawer>
|
2022-02-02 13:44:33 +01:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
2022-02-02 13:48:24 +01:00
|
|
|
|
2022-02-02 13:49:02 +01:00
|
|
|
type GlobalStylesProps = {
|
2022-04-04 14:13:15 +02:00
|
|
|
open: boolean,
|
2022-02-02 13:49:02 +01:00
|
|
|
};
|
|
|
|
|
2022-04-04 14:13:15 +02:00
|
|
|
const DrawerGlobalStyles = (props: GlobalStylesProps) => {
|
2022-05-18 18:15:12 +02:00
|
|
|
const { open } = props;
|
2022-02-02 13:49:02 +01:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Global
|
|
|
|
styles={{
|
|
|
|
'.main-wrapper__inner--filepage': {
|
|
|
|
overflow: open ? 'hidden' : 'unset',
|
2022-05-18 18:15:12 +02:00
|
|
|
maxHeight: open ? '100%' : 'unset',
|
2022-02-02 13:49:02 +01:00
|
|
|
},
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2022-02-02 13:48:24 +01:00
|
|
|
type PullerProps = {
|
|
|
|
theme: string,
|
|
|
|
};
|
|
|
|
|
2022-04-04 14:13:15 +02:00
|
|
|
const Puller = (props: PullerProps) => {
|
|
|
|
const { theme } = props;
|
2022-02-02 13:48:24 +01:00
|
|
|
|
|
|
|
return (
|
|
|
|
<span className="swipeable-drawer__puller" style={{ backgroundColor: theme === 'light' ? grey[300] : grey[800] }} />
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
type HeaderProps = {
|
|
|
|
title: any,
|
2022-02-07 14:04:25 +01:00
|
|
|
hasSubtitle?: boolean,
|
2022-02-02 13:48:24 +01:00
|
|
|
actions?: any,
|
2022-05-18 18:15:12 +02:00
|
|
|
handleClose: () => void,
|
2022-02-02 13:48:24 +01:00
|
|
|
};
|
|
|
|
|
2022-04-04 14:13:15 +02:00
|
|
|
const HeaderContents = (props: HeaderProps) => {
|
2022-05-18 18:15:12 +02:00
|
|
|
const { title, hasSubtitle, actions, handleClose, ...divProps } = props;
|
2022-02-02 13:48:24 +01:00
|
|
|
|
|
|
|
return (
|
2022-02-07 14:04:25 +01:00
|
|
|
<div
|
|
|
|
className={classnames('swipeable-drawer__header-content', {
|
|
|
|
'swipeable-drawer__header--with-subtitle': hasSubtitle,
|
|
|
|
})}
|
2022-05-18 18:15:12 +02:00
|
|
|
{...divProps}
|
2022-02-07 14:04:25 +01:00
|
|
|
>
|
2022-02-02 13:48:24 +01:00
|
|
|
{title}
|
|
|
|
|
|
|
|
<div className="swipeable-drawer__header-actions">
|
|
|
|
{actions}
|
|
|
|
|
2022-05-18 18:15:12 +02:00
|
|
|
<Button icon={ICONS.REMOVE} iconSize={16} onClick={handleClose} />
|
2022-02-02 13:48:24 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|