2020-04-06 22:39:49 +02:00
|
|
|
import classnames from 'classnames';
|
2023-02-24 19:07:16 +01:00
|
|
|
import PropTypes from 'prop-types';
|
2023-03-10 04:30:09 +01:00
|
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
2021-06-15 16:56:33 +02:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2022-11-16 20:19:52 +01:00
|
|
|
import SwiperCore, {
|
|
|
|
|
A11y,
|
|
|
|
|
Controller,
|
2023-02-24 19:07:16 +01:00
|
|
|
Navigation,
|
2022-11-16 20:19:52 +01:00
|
|
|
Pagination,
|
|
|
|
|
Scrollbar,
|
|
|
|
|
} from 'swiper';
|
2023-02-24 19:07:16 +01:00
|
|
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
2020-04-06 22:39:49 +02:00
|
|
|
|
2023-03-10 04:30:09 +01:00
|
|
|
import { PanelService, ServicesManager, Types } from '@ohif/core';
|
|
|
|
|
|
2023-06-07 15:05:35 +02:00
|
|
|
import Button from '../Button';
|
|
|
|
|
import Icon from '../Icon';
|
|
|
|
|
import IconButton from '../IconButton';
|
|
|
|
|
import Tooltip from '../Tooltip';
|
2022-11-16 20:19:52 +01:00
|
|
|
|
|
|
|
|
import 'swiper/css';
|
|
|
|
|
import 'swiper/css/navigation';
|
|
|
|
|
import './style.css';
|
2020-04-06 22:39:49 +02:00
|
|
|
|
2020-04-07 19:03:47 +02:00
|
|
|
const borderSize = 4;
|
2020-04-14 00:35:32 +02:00
|
|
|
const expandedWidth = 248;
|
2022-11-16 20:19:52 +01:00
|
|
|
const collapsedWidth = 25;
|
2020-04-07 19:03:47 +02:00
|
|
|
|
2020-04-06 22:39:49 +02:00
|
|
|
const baseStyle = {
|
2020-04-07 19:03:47 +02:00
|
|
|
maxWidth: `${expandedWidth}px`,
|
|
|
|
|
width: `${expandedWidth}px`,
|
2020-04-06 22:39:49 +02:00
|
|
|
};
|
|
|
|
|
|
2020-04-08 17:00:08 +02:00
|
|
|
const collapsedHideWidth = expandedWidth - collapsedWidth - borderSize;
|
2020-04-07 19:03:47 +02:00
|
|
|
const styleMap = {
|
2020-04-06 22:39:49 +02:00
|
|
|
open: {
|
2020-04-07 19:03:47 +02:00
|
|
|
left: { marginLeft: '0px' },
|
|
|
|
|
right: { marginRight: '0px' },
|
2020-04-06 22:39:49 +02:00
|
|
|
},
|
|
|
|
|
closed: {
|
2020-04-08 17:00:08 +02:00
|
|
|
left: { marginLeft: `-${collapsedHideWidth}px` },
|
|
|
|
|
right: { marginRight: `-${collapsedHideWidth}px` },
|
2020-04-06 22:39:49 +02:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2020-04-08 17:00:08 +02:00
|
|
|
const baseClasses =
|
2022-11-16 20:19:52 +01:00
|
|
|
'transition-all duration-300 ease-in-out h-100 bg-black border-black justify-start box-content flex flex-col';
|
2020-04-07 19:03:47 +02:00
|
|
|
|
|
|
|
|
const classesMap = {
|
|
|
|
|
open: {
|
2022-11-16 20:19:52 +01:00
|
|
|
left: `mr-1`,
|
|
|
|
|
right: `ml-1`,
|
2020-04-07 19:03:47 +02:00
|
|
|
},
|
|
|
|
|
closed: {
|
2022-11-16 20:19:52 +01:00
|
|
|
left: `mr-2 items-end`,
|
|
|
|
|
right: `ml-2 items-start`,
|
2020-04-07 19:03:47 +02:00
|
|
|
},
|
|
|
|
|
};
|
2020-04-06 22:39:49 +02:00
|
|
|
|
2022-11-16 20:19:52 +01:00
|
|
|
const openStateIconName = {
|
2020-04-08 18:00:07 +02:00
|
|
|
left: 'push-left',
|
|
|
|
|
right: 'push-right',
|
2020-04-07 21:59:48 +02:00
|
|
|
};
|
|
|
|
|
|
2020-04-10 01:31:21 +02:00
|
|
|
const position = {
|
|
|
|
|
left: {
|
|
|
|
|
right: 5,
|
|
|
|
|
},
|
|
|
|
|
right: {
|
|
|
|
|
left: 5,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2020-04-06 22:39:49 +02:00
|
|
|
const SidePanel = ({
|
2023-03-10 04:30:09 +01:00
|
|
|
servicesManager,
|
2020-04-06 22:39:49 +02:00
|
|
|
side,
|
|
|
|
|
className,
|
2022-11-16 20:19:52 +01:00
|
|
|
activeTabIndex: activeTabIndexProp,
|
|
|
|
|
tabs,
|
2020-04-06 22:39:49 +02:00
|
|
|
}) => {
|
2023-06-07 15:05:35 +02:00
|
|
|
const panelService: PanelService = servicesManager?.services?.panelService;
|
2023-03-10 04:30:09 +01:00
|
|
|
|
2021-06-15 16:56:33 +02:00
|
|
|
const { t } = useTranslation('SidePanel');
|
|
|
|
|
|
2023-03-10 04:30:09 +01:00
|
|
|
// Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM.
|
|
|
|
|
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
|
|
|
|
|
const [hasBeenOpened, setHasBeenOpened] = useState(
|
|
|
|
|
activeTabIndexProp !== null
|
|
|
|
|
);
|
2022-11-16 20:19:52 +01:00
|
|
|
const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
|
2023-03-10 04:30:09 +01:00
|
|
|
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0);
|
2022-11-16 20:19:52 +01:00
|
|
|
const swiperRef = useRef() as any;
|
|
|
|
|
const [swiper, setSwiper] = useState<any>();
|
|
|
|
|
|
|
|
|
|
const prevRef = React.useRef();
|
|
|
|
|
const nextRef = React.useRef();
|
2020-04-23 04:13:08 +02:00
|
|
|
|
2022-11-16 20:19:52 +01:00
|
|
|
const openStatus = panelOpen ? 'open' : 'closed';
|
2020-04-07 19:03:47 +02:00
|
|
|
const style = Object.assign({}, styleMap[openStatus][side], baseStyle);
|
|
|
|
|
|
2022-11-16 20:19:52 +01:00
|
|
|
const ActiveComponent = tabs[activeTabIndex].content;
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (panelOpen && swiper) {
|
|
|
|
|
swiper.slideTo(activeTabIndex, 500);
|
|
|
|
|
}
|
|
|
|
|
}, [panelOpen, swiper]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (swiper) {
|
|
|
|
|
swiper.params.navigation.prevEl = prevRef.current;
|
|
|
|
|
swiper.params.navigation.nextEl = nextRef.current;
|
|
|
|
|
swiper.navigation.init();
|
|
|
|
|
swiper.navigation.update();
|
|
|
|
|
}
|
|
|
|
|
}, [swiper]);
|
|
|
|
|
|
2023-03-10 04:30:09 +01:00
|
|
|
const updatePanelOpen = useCallback((panelOpen: boolean) => {
|
|
|
|
|
setPanelOpen(panelOpen);
|
|
|
|
|
if (panelOpen) {
|
|
|
|
|
setHasBeenOpened(true);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const updateActiveTabIndex = useCallback(
|
|
|
|
|
(activeTabIndex: number) => {
|
|
|
|
|
setActiveTabIndex(activeTabIndex);
|
|
|
|
|
updatePanelOpen(true);
|
|
|
|
|
},
|
|
|
|
|
[updatePanelOpen]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2023-06-07 15:05:35 +02:00
|
|
|
if (panelService) {
|
|
|
|
|
const activatePanelSubscription = panelService.subscribe(
|
|
|
|
|
panelService.EVENTS.ACTIVATE_PANEL,
|
|
|
|
|
(activatePanelEvent: Types.ActivatePanelEvent) => {
|
|
|
|
|
if (!hasBeenOpened || activatePanelEvent.forceActive) {
|
|
|
|
|
const tabIndex = tabs.findIndex(
|
|
|
|
|
tab => tab.id === activatePanelEvent.panelId
|
|
|
|
|
);
|
|
|
|
|
if (tabIndex !== -1) {
|
|
|
|
|
updateActiveTabIndex(tabIndex);
|
|
|
|
|
}
|
2023-03-10 04:30:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-06-07 15:05:35 +02:00
|
|
|
);
|
2023-03-10 04:30:09 +01:00
|
|
|
|
2023-06-07 15:05:35 +02:00
|
|
|
return () => {
|
|
|
|
|
activatePanelSubscription.unsubscribe();
|
|
|
|
|
};
|
|
|
|
|
}
|
2023-03-10 04:30:09 +01:00
|
|
|
}, [tabs, hasBeenOpened, panelService, updateActiveTabIndex]);
|
|
|
|
|
|
2022-11-16 20:19:52 +01:00
|
|
|
const getCloseStateComponent = () => {
|
|
|
|
|
const _childComponents = Array.isArray(tabs) ? tabs : [tabs];
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div
|
|
|
|
|
className={classnames(
|
|
|
|
|
'bg-secondary-dark h-[28px] flex items-center w-full rounded-md cursor-pointer',
|
|
|
|
|
side === 'left' ? 'pr-2 justify-end' : 'pl-2 justify-start'
|
|
|
|
|
)}
|
2020-04-23 04:13:08 +02:00
|
|
|
onClick={() => {
|
2023-03-10 04:30:09 +01:00
|
|
|
updatePanelOpen(prev => !prev);
|
2020-04-23 04:13:08 +02:00
|
|
|
}}
|
2022-11-16 20:19:52 +01:00
|
|
|
data-cy={`side-panel-header-${side}`}
|
2020-04-23 04:13:08 +02:00
|
|
|
>
|
|
|
|
|
<Icon
|
2022-11-16 20:19:52 +01:00
|
|
|
name={'navigation-panel-right-reveal'}
|
|
|
|
|
className={classnames(
|
|
|
|
|
'text-primary-active',
|
|
|
|
|
side === 'left' && 'transform rotate-180'
|
|
|
|
|
)}
|
2020-04-23 04:13:08 +02:00
|
|
|
/>
|
2022-11-16 20:19:52 +01:00
|
|
|
</div>
|
|
|
|
|
<div className={classnames('flex flex-col space-y-3 mt-3')}>
|
|
|
|
|
{_childComponents.map((childComponent, index) => (
|
|
|
|
|
<Tooltip
|
|
|
|
|
position={side === 'left' ? 'right' : 'left'}
|
|
|
|
|
key={index}
|
|
|
|
|
content={`${childComponent.label}`}
|
|
|
|
|
className={classnames(
|
|
|
|
|
'flex items-center',
|
|
|
|
|
side === 'left' ? 'justify-end ' : 'justify-start '
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<IconButton
|
|
|
|
|
id={`${childComponent.name}-btn`}
|
|
|
|
|
variant="text"
|
|
|
|
|
color="inherit"
|
|
|
|
|
size="initial"
|
|
|
|
|
className="text-primary-active"
|
|
|
|
|
onClick={() => {
|
2023-03-10 04:30:09 +01:00
|
|
|
updateActiveTabIndex(index);
|
2022-11-16 20:19:52 +01:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon
|
|
|
|
|
name={childComponent.iconName}
|
|
|
|
|
className="text-primary-active"
|
|
|
|
|
style={{
|
|
|
|
|
width: '22px',
|
|
|
|
|
height: '22px',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
2020-04-23 04:13:08 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={classnames(
|
|
|
|
|
className,
|
|
|
|
|
baseClasses,
|
|
|
|
|
classesMap[openStatus][side]
|
|
|
|
|
)}
|
|
|
|
|
style={style}
|
|
|
|
|
>
|
2022-11-16 20:19:52 +01:00
|
|
|
{panelOpen ? (
|
2020-04-23 04:13:08 +02:00
|
|
|
<React.Fragment>
|
2022-11-16 20:19:52 +01:00
|
|
|
{/** Panel Header with Arrow and Close Actions */}
|
|
|
|
|
<div
|
|
|
|
|
className={classnames(
|
2023-01-25 16:14:12 +01:00
|
|
|
'flex flex-static px-[10px] bg-primary-dark h-9 cursor-pointer',
|
2022-11-16 20:19:52 +01:00
|
|
|
tabs.length === 1 && 'mb-1'
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => {
|
2023-03-10 04:30:09 +01:00
|
|
|
updatePanelOpen(prev => !prev);
|
2022-11-16 20:19:52 +01:00
|
|
|
// slideToActivePanel();
|
|
|
|
|
}}
|
|
|
|
|
data-cy={`side-panel-header-${side}`}
|
|
|
|
|
>
|
2020-04-13 19:53:36 +02:00
|
|
|
<Button
|
|
|
|
|
variant="text"
|
|
|
|
|
color="inherit"
|
2022-09-20 02:02:53 +02:00
|
|
|
border="none"
|
2020-04-13 19:53:36 +02:00
|
|
|
rounded="none"
|
2023-01-25 16:14:12 +01:00
|
|
|
className="flex flex-row flex-static items-center px-3 relative w-full"
|
2022-11-16 20:19:52 +01:00
|
|
|
name={tabs.length === 1 ? `${tabs[activeTabIndex].name}` : ''}
|
2020-04-13 19:53:36 +02:00
|
|
|
>
|
|
|
|
|
<Icon
|
2022-11-16 20:19:52 +01:00
|
|
|
name={openStateIconName[side]}
|
2020-04-13 19:53:36 +02:00
|
|
|
className={classnames(
|
|
|
|
|
'text-primary-active absolute',
|
|
|
|
|
side === 'left' && 'order-last'
|
|
|
|
|
)}
|
|
|
|
|
style={{ ...position[side] }}
|
|
|
|
|
/>
|
2022-11-16 20:19:52 +01:00
|
|
|
{/* Todo: ass secondary label here */}
|
|
|
|
|
<span className="text-primary-active">
|
2023-02-24 19:07:16 +01:00
|
|
|
{tabs.length === 1 && (t(tabs[activeTabIndex].label) as string)}
|
2020-04-13 19:53:36 +02:00
|
|
|
</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2022-11-16 20:19:52 +01:00
|
|
|
{tabs.length > 1 &&
|
|
|
|
|
_getMoreThanOneTabLayout(
|
|
|
|
|
swiperRef,
|
|
|
|
|
setSwiper,
|
|
|
|
|
prevRef,
|
|
|
|
|
nextRef,
|
|
|
|
|
tabs,
|
|
|
|
|
activeTabIndex,
|
2023-03-10 04:30:09 +01:00
|
|
|
updateActiveTabIndex
|
2022-11-16 20:19:52 +01:00
|
|
|
)}
|
|
|
|
|
{/** carousel navigation with the arrows */}
|
|
|
|
|
{/** only show carousel nav if tabs are more than 3 tabs */}
|
|
|
|
|
{tabs.length > 3 && (
|
|
|
|
|
<div className="text-primary-active w-full flex justify-end gap-2 bg-primary-dark py-1 px-2">
|
|
|
|
|
<button ref={prevRef} className="swiper-button-prev-custom">
|
|
|
|
|
<Icon
|
|
|
|
|
name={'icon-prev'}
|
|
|
|
|
className={classnames('text-primary-active')}
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
<button ref={nextRef} className="swiper-button-next-custom">
|
|
|
|
|
<Icon
|
|
|
|
|
name={'icon-next'}
|
|
|
|
|
className={classnames('text-primary-active')}
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<ActiveComponent />
|
2020-04-23 04:13:08 +02:00
|
|
|
</React.Fragment>
|
|
|
|
|
) : (
|
2022-11-16 20:19:52 +01:00
|
|
|
<React.Fragment>{getCloseStateComponent()}</React.Fragment>
|
2020-04-07 19:03:47 +02:00
|
|
|
)}
|
2020-04-06 22:39:49 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
SidePanel.defaultProps = {
|
2020-04-23 04:13:08 +02:00
|
|
|
defaultComponentOpen: null,
|
2020-04-06 22:39:49 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
SidePanel.propTypes = {
|
2023-03-10 04:30:09 +01:00
|
|
|
servicesManager: PropTypes.instanceOf(ServicesManager),
|
2020-04-06 22:39:49 +02:00
|
|
|
side: PropTypes.oneOf(['left', 'right']).isRequired,
|
|
|
|
|
className: PropTypes.string,
|
2022-11-16 20:19:52 +01:00
|
|
|
activeTabIndex: PropTypes.number,
|
|
|
|
|
tabs: PropTypes.oneOfType([
|
2020-04-23 04:13:08 +02:00
|
|
|
PropTypes.arrayOf(
|
|
|
|
|
PropTypes.shape({
|
|
|
|
|
iconName: PropTypes.string.isRequired,
|
|
|
|
|
iconLabel: PropTypes.string.isRequired,
|
|
|
|
|
name: PropTypes.string.isRequired,
|
|
|
|
|
label: PropTypes.string.isRequired,
|
2020-07-24 12:51:57 +02:00
|
|
|
content: PropTypes.func, // TODO: Should be node, but it keeps complaining?
|
2020-04-23 04:13:08 +02:00
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
]),
|
2020-04-06 22:39:49 +02:00
|
|
|
};
|
|
|
|
|
|
2022-11-16 20:19:52 +01:00
|
|
|
function _getMoreThanOneTabLayout(
|
|
|
|
|
swiperRef: any,
|
|
|
|
|
setSwiper: React.Dispatch<any>,
|
|
|
|
|
prevRef: React.MutableRefObject<undefined>,
|
|
|
|
|
nextRef: React.MutableRefObject<undefined>,
|
|
|
|
|
tabs: any,
|
|
|
|
|
activeTabIndex: any,
|
2023-03-10 04:30:09 +01:00
|
|
|
updateActiveTabIndex
|
2022-11-16 20:19:52 +01:00
|
|
|
) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
2023-01-25 16:14:12 +01:00
|
|
|
className="flex-static collapse-sidebar relative"
|
2022-11-16 20:19:52 +01:00
|
|
|
style={{
|
|
|
|
|
backgroundColor: '#06081f',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className="w-full">
|
|
|
|
|
<Swiper
|
|
|
|
|
onInit={(core: SwiperCore) => {
|
|
|
|
|
swiperRef.current = core.el;
|
|
|
|
|
}}
|
|
|
|
|
simulateTouch={false}
|
|
|
|
|
modules={[Navigation, Pagination, Scrollbar, A11y, Controller]}
|
|
|
|
|
slidesPerView={3}
|
|
|
|
|
spaceBetween={5}
|
|
|
|
|
onSwiper={swiper => setSwiper(swiper)}
|
|
|
|
|
navigation={{
|
|
|
|
|
prevEl: prevRef?.current,
|
|
|
|
|
nextEl: nextRef?.current,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{tabs.map((obj, index) => (
|
|
|
|
|
<SwiperSlide key={index}>
|
|
|
|
|
<div
|
|
|
|
|
className={classnames(
|
|
|
|
|
index === activeTabIndex
|
|
|
|
|
? 'bg-secondary-main text-white'
|
|
|
|
|
: 'text-aqua-pale',
|
|
|
|
|
'flex cursor-pointer px-4 py-1 rounded-[4px] flex-col justify-center items-center text-center hover:text-white'
|
|
|
|
|
)}
|
|
|
|
|
key={index}
|
|
|
|
|
onClick={() => {
|
2023-03-10 04:30:09 +01:00
|
|
|
updateActiveTabIndex(index);
|
2022-11-16 20:19:52 +01:00
|
|
|
}}
|
|
|
|
|
data-cy={`${obj.name}-btn`}
|
|
|
|
|
>
|
|
|
|
|
<span>
|
|
|
|
|
<Icon
|
|
|
|
|
name={obj.iconName}
|
|
|
|
|
className={classnames(
|
|
|
|
|
index === activeTabIndex
|
|
|
|
|
? 'text-white'
|
|
|
|
|
: 'text-primary-active'
|
|
|
|
|
)}
|
|
|
|
|
style={{
|
|
|
|
|
width: '22px',
|
|
|
|
|
height: '22px',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-[10px] select-none font-medium whitespace-nowrap mt-[5px]">
|
|
|
|
|
{obj.label}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</SwiperSlide>
|
|
|
|
|
))}
|
|
|
|
|
</Swiper>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-06 22:39:49 +02:00
|
|
|
export default SidePanel;
|