Nested toolbar w/ "sticky" when clicked
This commit is contained in:
parent
217d848f9f
commit
8c1a844558
41
extensions/default/src/ViewerLayout/NestedToolbar.jsx
Normal file
41
extensions/default/src/ViewerLayout/NestedToolbar.jsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { ToolbarButton } from '@ohif/ui';
|
||||||
|
|
||||||
|
function NestedToolbar({ children }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function closeNestedToolbar() {
|
||||||
|
if (isOpen) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('click', closeNestedToolbar);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('click', closeNestedToolbar);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const dropdownContent = isOpen ? children : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
id="More"
|
||||||
|
label="More"
|
||||||
|
icon="tool-more-menu"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}}
|
||||||
|
dropdownContent={dropdownContent}
|
||||||
|
isActive={isOpen}
|
||||||
|
type="primary"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NestedToolbar.propTypes = {
|
||||||
|
children: PropTypes.any.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NestedToolbar;
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { SidePanel, ToolbarButton } from '@ohif/ui';
|
import { SidePanel } from '@ohif/ui';
|
||||||
import Header from './Header.jsx';
|
import Header from './Header.jsx';
|
||||||
|
import NestedToolbar from './NestedToolbar.jsx';
|
||||||
|
|
||||||
function ViewerLayout({
|
function ViewerLayout({
|
||||||
// From Extension Module Params
|
// From Extension Module Params
|
||||||
@ -79,12 +80,26 @@ function ViewerLayout({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header>
|
<Header>
|
||||||
{/* relative, flex, justify-center */}
|
<div className="relative flex justify-center">
|
||||||
<div className="flex">
|
|
||||||
{toolbars.primary.map(toolDef => {
|
{toolbars.primary.map(toolDef => {
|
||||||
const { id, Component, componentProps } = toolDef;
|
const isNested = Array.isArray(toolDef);
|
||||||
|
|
||||||
return <Component key={id} id={id} {...componentProps} />;
|
if (!isNested) {
|
||||||
|
const { id, Component, componentProps } = toolDef;
|
||||||
|
|
||||||
|
return <Component key={id} id={id} {...componentProps} />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<NestedToolbar>
|
||||||
|
<div className="flex">
|
||||||
|
{toolDef.map(x => {
|
||||||
|
const { id, Component, componentProps } = x;
|
||||||
|
return <Component key={id} id={id} {...componentProps} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</NestedToolbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@ -55,17 +55,17 @@ export default function mode({ modeConfiguration }) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Could import layout selector here from org.ohif.default (when it exists!)
|
// Could import layout selector here from org.ohif.default (when it exists!)
|
||||||
ToolBarService.setToolBarLayout([
|
// ToolBarService.setToolBarLayout([
|
||||||
// Primary
|
// // Primary
|
||||||
{
|
// {
|
||||||
tools: ['Zoom', 'Levels', 'Pan', 'Capture', 'Layout'],
|
// tools: ['Zoom', 'Levels', 'Pan', 'Capture', 'Layout'],
|
||||||
moreTools: ['Zoom'],
|
// moreTools: ['Zoom'],
|
||||||
},
|
// },
|
||||||
// Secondary
|
// // Secondary
|
||||||
{
|
// {
|
||||||
tools: ['Annotate', 'Bidirectional', 'Ellipse', 'Length'],
|
// tools: ['Annotate', 'Bidirectional', 'Ellipse', 'Length'],
|
||||||
},
|
// },
|
||||||
]);
|
// ]);
|
||||||
},
|
},
|
||||||
layoutTemplate: ({ routeProps }) => {
|
layoutTemplate: ({ routeProps }) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -34,10 +34,10 @@ export default function mode({ modeConfiguration }) {
|
|||||||
'Zoom',
|
'Zoom',
|
||||||
'Wwwc',
|
'Wwwc',
|
||||||
'Pan',
|
'Pan',
|
||||||
'Capture', // toggle --> command to open capture modal? needs to know when it should be off? (promise?)
|
'Capture',
|
||||||
'Layout', // toggle --> command to open layout dialog? needs to know when it should be off? (promise?)
|
// 'Layout', // toggle --> command to open layout dialog? needs to know when it should be off? (promise?)
|
||||||
'Divider',
|
'Divider',
|
||||||
'Zoom', // ? array of arrays?
|
['Zoom', 'Wwwc'],
|
||||||
]);
|
]);
|
||||||
ToolBarService.createButtonSection('secondary', [
|
ToolBarService.createButtonSection('secondary', [
|
||||||
'Annotate',
|
'Annotate',
|
||||||
|
|||||||
@ -4,7 +4,7 @@ export default [
|
|||||||
id: 'Divider',
|
id: 'Divider',
|
||||||
type: 'ohif.divider',
|
type: 'ohif.divider',
|
||||||
},
|
},
|
||||||
// Primary
|
// ~~ Primary
|
||||||
{
|
{
|
||||||
id: 'Zoom',
|
id: 'Zoom',
|
||||||
type: 'ohif.radioGroup',
|
type: 'ohif.radioGroup',
|
||||||
@ -50,7 +50,19 @@ export default [
|
|||||||
type: 'primary',
|
type: 'primary',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Secondary
|
{
|
||||||
|
id: 'Capture',
|
||||||
|
type: 'ohif.action',
|
||||||
|
props: {
|
||||||
|
icon: 'tool-capture',
|
||||||
|
label: 'Capture',
|
||||||
|
commandName: 'showDownloadViewportModal',
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Layout
|
||||||
|
// Expanded/Nested?
|
||||||
|
// ~~ Secondary
|
||||||
{
|
{
|
||||||
id: 'Annotate',
|
id: 'Annotate',
|
||||||
type: 'ohif.radioGroup',
|
type: 'ohif.radioGroup',
|
||||||
@ -63,8 +75,7 @@ export default [
|
|||||||
label: 'Annotate',
|
label: 'Annotate',
|
||||||
commandName: 'setToolActive',
|
commandName: 'setToolActive',
|
||||||
commandOptions: { toolName: 'ArrowAnnotate' },
|
commandOptions: { toolName: 'ArrowAnnotate' },
|
||||||
type: 'secondary', // Purely for background color/hover styles :|
|
type: 'secondary',
|
||||||
// Layout Template can set this?
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -63,61 +63,40 @@ export default class ToolBarService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getButtonSection(key) {
|
getButtonSection(key) {
|
||||||
const buttonSection = this.buttonSections[key];
|
const buttonSectionIds = this.buttonSections[key];
|
||||||
const buttonsInSection = [];
|
const buttonsInSection = [];
|
||||||
|
|
||||||
if (!buttonSection) {
|
if (!buttonSectionIds) {
|
||||||
return buttonsInSection;
|
return buttonsInSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
buttonSection.forEach(btnId => {
|
buttonSectionIds.forEach(btnIdOrArray => {
|
||||||
const button = this.buttons[btnId];
|
const isNested = Array.isArray(btnIdOrArray);
|
||||||
if (button) {
|
|
||||||
buttonsInSection.push(button);
|
if (isNested) {
|
||||||
|
const btnIds = btnIdOrArray;
|
||||||
|
const nestedButtons = [];
|
||||||
|
|
||||||
|
btnIds.forEach(nestedBtnId => {
|
||||||
|
const nestedBtn = this.buttons[nestedBtnId];
|
||||||
|
const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key);
|
||||||
|
|
||||||
|
nestedButtons.push(mappedNestedBtn);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nestedButtons.length) {
|
||||||
|
buttonsInSection.push(nestedButtons);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`${btnId} is not a registered button.`);
|
const btnId = btnIdOrArray;
|
||||||
|
const btn = this.buttons[btnId];
|
||||||
|
const mappedBtn = this._mapButtonToDisplay(btn, key);
|
||||||
|
|
||||||
|
buttonsInSection.push(mappedBtn);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mappedButtonSection = buttonsInSection.map(bt => {
|
return buttonsInSection;
|
||||||
const { id, type, component, props } = bt;
|
|
||||||
const buttonType = this._buttonTypes()[type];
|
|
||||||
|
|
||||||
// Filter out & warn
|
|
||||||
if (!buttonType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClick = evt => {
|
|
||||||
console.warn(
|
|
||||||
`Handling click for: BTN::${id}`,
|
|
||||||
evt,
|
|
||||||
bt,
|
|
||||||
buttonType,
|
|
||||||
btnSection
|
|
||||||
);
|
|
||||||
const btnSection = key;
|
|
||||||
if (buttonType.clickHandler) {
|
|
||||||
buttonType.clickHandler(evt, bt, btnSection);
|
|
||||||
}
|
|
||||||
if (bt.props.onClick) {
|
|
||||||
bt.onClick(evt, bt, btnSection);
|
|
||||||
}
|
|
||||||
if (bt.props.clickHandler) {
|
|
||||||
bt.clickHandler(evt, bt, btnSection);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._trySetButtonActive(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
Component: component || buttonType.defaultComponent,
|
|
||||||
componentProps: Object.assign({}, props, { onClick }), //
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return mappedButtonSection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,4 +131,38 @@ export default class ToolBarService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} btn
|
||||||
|
* @param {*} btnSection
|
||||||
|
*/
|
||||||
|
_mapButtonToDisplay(btn, btnSection) {
|
||||||
|
const { id, type, component, props } = btn;
|
||||||
|
const buttonType = this._buttonTypes()[type];
|
||||||
|
|
||||||
|
if (!buttonType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = evt => {
|
||||||
|
if (buttonType.clickHandler) {
|
||||||
|
buttonType.clickHandler(evt, btn, btnSection);
|
||||||
|
}
|
||||||
|
if (btn.props.onClick) {
|
||||||
|
btn.onClick(evt, btn, btnSection);
|
||||||
|
}
|
||||||
|
if (btn.props.clickHandler) {
|
||||||
|
btn.clickHandler(evt, btn, btnSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._trySetButtonActive(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
Component: component || buttonType.defaultComponent,
|
||||||
|
componentProps: Object.assign({}, props, { onClick }), //
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ const ToolbarButton = ({
|
|||||||
return (
|
return (
|
||||||
<div key={id}>
|
<div key={id}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isSticky={shouldShowDropdown}
|
||||||
content={shouldShowDropdown ? dropdownContent : label}
|
content={shouldShowDropdown ? dropdownContent : label}
|
||||||
tight={shouldShowDropdown}
|
tight={shouldShowDropdown}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const arrowPositionStyle = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Tooltip = ({ position, content, tight, children }) => {
|
const Tooltip = ({ content, isSticky, position, tight, children }) => {
|
||||||
const [isActive, setIsActive] = useState(false);
|
const [isActive, setIsActive] = useState(false);
|
||||||
|
|
||||||
const handleMouseOver = () => {
|
const handleMouseOver = () => {
|
||||||
@ -39,6 +39,8 @@ const Tooltip = ({ position, content, tight, children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isOpen = isSticky || isActive;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative "
|
className="relative "
|
||||||
@ -51,8 +53,8 @@ const Tooltip = ({ position, content, tight, children }) => {
|
|||||||
{children}
|
{children}
|
||||||
<div
|
<div
|
||||||
className={classnames(`tooltip tooltip-${position}`, {
|
className={classnames(`tooltip tooltip-${position}`, {
|
||||||
block: isActive,
|
block: isOpen,
|
||||||
hidden: !isActive,
|
hidden: !isOpen,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -65,7 +67,7 @@ const Tooltip = ({ position, content, tight, children }) => {
|
|||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
<svg
|
<svg
|
||||||
className="absolute text-primary-dark h-4 stroke-secondary-main"
|
className="absolute h-4 text-primary-dark stroke-secondary-main"
|
||||||
style={arrowPositionStyle[position]}
|
style={arrowPositionStyle[position]}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -80,11 +82,12 @@ const Tooltip = ({ position, content, tight, children }) => {
|
|||||||
|
|
||||||
Tooltip.defaultProps = {
|
Tooltip.defaultProps = {
|
||||||
tight: false,
|
tight: false,
|
||||||
|
isSticky: false,
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
};
|
};
|
||||||
|
|
||||||
Tooltip.propTypes = {
|
Tooltip.propTypes = {
|
||||||
tight: PropTypes.bool,
|
content: PropTypes.node.isRequired,
|
||||||
position: PropTypes.oneOf([
|
position: PropTypes.oneOf([
|
||||||
'bottom',
|
'bottom',
|
||||||
'bottom-left',
|
'bottom-left',
|
||||||
@ -92,8 +95,9 @@ Tooltip.propTypes = {
|
|||||||
'left',
|
'left',
|
||||||
'right',
|
'right',
|
||||||
]),
|
]),
|
||||||
|
isSticky: PropTypes.bool,
|
||||||
|
tight: PropTypes.bool,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
content: PropTypes.node.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Tooltip;
|
export default Tooltip;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
.tooltip {
|
.tooltip {
|
||||||
@apply z-10 absolute;
|
@apply absolute z-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TOOLTIP WORKAROUND FOR ARROW UP */
|
/* TOOLTIP WORKAROUND FOR ARROW UP */
|
||||||
.tooltip.tooltip-bottom .tooltip-box::before {
|
.tooltip.tooltip-bottom .tooltip-box::before {
|
||||||
@apply bg-primary-dark absolute z-10;
|
@apply absolute z-10 bg-primary-dark;
|
||||||
content: '';
|
content: '';
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
@ -44,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tooltip.tooltip-right .tooltip-box::before {
|
.tooltip.tooltip-right .tooltip-box::before {
|
||||||
@apply bg-primary-dark absolute z-10;
|
@apply absolute z-10 bg-primary-dark;
|
||||||
content: '';
|
content: '';
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
@ -54,7 +54,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tooltip.tooltip-left .tooltip-box::before {
|
.tooltip.tooltip-left .tooltip-box::before {
|
||||||
@apply bg-primary-dark absolute z-10;
|
@apply absolute z-10 bg-primary-dark;
|
||||||
content: '';
|
content: '';
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
@ -64,7 +64,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tooltip.tooltip-bottom-right .tooltip-box::before {
|
.tooltip.tooltip-bottom-right .tooltip-box::before {
|
||||||
@apply bg-primary-dark absolute z-10;
|
@apply absolute z-10 bg-primary-dark;
|
||||||
content: '';
|
content: '';
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
@ -73,19 +73,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tooltip.tooltip-bottom-left .tooltip-box::before {
|
.tooltip.tooltip-bottom-left .tooltip-box::before {
|
||||||
@apply bg-primary-dark absolute z-10;
|
@apply absolute z-10 bg-primary-dark;
|
||||||
content: '';
|
content: '';
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
left: 5px;
|
left: 5px;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: REMOVE BELOW CLASS "showTooltipOnHover" AFTER TOOLTIP REPLACEMENT */
|
|
||||||
.showTooltipOnHover .tooltip {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showTooltipOnHover:hover .tooltip {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -184,7 +184,7 @@ const ViewportActionBar = ({ studyData, onSeriesChange }) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="relative flex justify-end showTooltipOnHover">
|
<div className="relative flex justify-end">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon name="profile" className="w-5 text-white" />
|
<Icon name="profile" className="w-5 text-white" />
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@ -37,7 +37,6 @@ import Thumbnail from './Thumbnail';
|
|||||||
import ThumbnailNoImage from './ThumbnailNoImage';
|
import ThumbnailNoImage from './ThumbnailNoImage';
|
||||||
import ThumbnailTracked from './ThumbnailTracked';
|
import ThumbnailTracked from './ThumbnailTracked';
|
||||||
import ThumbnailList from './ThumbnailList';
|
import ThumbnailList from './ThumbnailList';
|
||||||
import Toolbar from './Toolbar';
|
|
||||||
import ToolbarButton from './ToolbarButton';
|
import ToolbarButton from './ToolbarButton';
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip from './Tooltip';
|
||||||
import Typography from './Typography';
|
import Typography from './Typography';
|
||||||
@ -87,7 +86,6 @@ export {
|
|||||||
ThumbnailNoImage,
|
ThumbnailNoImage,
|
||||||
ThumbnailTracked,
|
ThumbnailTracked,
|
||||||
ThumbnailList,
|
ThumbnailList,
|
||||||
Toolbar,
|
|
||||||
ToolbarButton,
|
ToolbarButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user