feat: Add new annotate tool using new dialog service (#1211)
* Testing dialog * Refactor modal and add dialog component to simple dialog * Use existent dialog * Expect event from getText callback (annotation) * Bump cornerstone-tools version * Use simple dialog * bump cornersotne version * CR Update: Update dialgo method names and default values * CR Update: Bump overlay and show only if theres at least one overlay prop set to true * CR Update: Use percentage over view height in draggable area * Fix broken test vtk checks WWWC tool * Comment test (non-deterministic)
This commit is contained in:
1 parent
1334ba8eb0
commit
8fd3af1e13
18 files changed
+372
-254
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TextInput } from '@ohif/ui';
|
||||
|
||||
import './SimpleDialog.styl';
|
||||
|
||||
@@ -21,6 +22,31 @@ class SimpleDialog extends Component {
|
||||
rootClass: '',
|
||||
};
|
||||
|
||||
static InputDialog = ({ onSubmit, defaultValue, title, label, onClose }) => {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
|
||||
const onSubmitHandler = () => {
|
||||
onSubmit(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="InputDialog">
|
||||
<SimpleDialog
|
||||
headerTitle={title}
|
||||
onClose={onClose}
|
||||
onConfirm={onSubmitHandler}
|
||||
>
|
||||
<TextInput
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={event => setValue(event.target.value)}
|
||||
label={label}
|
||||
/>
|
||||
</SimpleDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
@import './../../design/styles/common/button.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
.InputDialog
|
||||
.simpleDialog
|
||||
position: relative
|
||||
|
||||
.simpleDialog
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
|
||||
@@ -34,49 +34,45 @@ const DialogProvider = ({ children, service }) => {
|
||||
}, [create, dismiss, service]);
|
||||
|
||||
/**
|
||||
* Creates a dialog and return its id.
|
||||
* UI Dialog
|
||||
*
|
||||
* @returns id
|
||||
* @typedef {Object} DialogProps
|
||||
* @property {string} id The dialog id.
|
||||
* @property {DialogContent} content The dialog content.
|
||||
* @property {Object} contentProps The dialog content props.
|
||||
* @property {boolean} isDraggable Controls if dialog content is draggable or not.
|
||||
* @property {boolean} showOverlay Controls dialog overlay.
|
||||
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
|
||||
* @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input.
|
||||
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
|
||||
* @property {Function} onStop Called when dragging stops.
|
||||
* @property {Function} onDrag Called while dragging.
|
||||
*/
|
||||
const create = useCallback(
|
||||
({
|
||||
id,
|
||||
content,
|
||||
onSubmit,
|
||||
onClose,
|
||||
onDrag,
|
||||
onStop,
|
||||
isDraggable,
|
||||
defaultPosition,
|
||||
position,
|
||||
}) => {
|
||||
let dialogId = id;
|
||||
if (!dialogId) {
|
||||
dialogId = utils.guid();
|
||||
}
|
||||
|
||||
const newDialog = {
|
||||
id: dialogId,
|
||||
content,
|
||||
onSubmit,
|
||||
onClose,
|
||||
onDrag,
|
||||
onStop,
|
||||
isDraggable,
|
||||
defaultPosition,
|
||||
position,
|
||||
};
|
||||
/**
|
||||
* Creates a new dialog and return its id.
|
||||
*
|
||||
* @param {DialogProps} props The dialog props.
|
||||
* @returns The new dialog id.
|
||||
*/
|
||||
const create = useCallback(props => {
|
||||
const { id } = props;
|
||||
|
||||
setDialogs(dialogs => [...dialogs, newDialog]);
|
||||
let dialogId = id;
|
||||
if (!dialogId) {
|
||||
dialogId = utils.guid();
|
||||
}
|
||||
|
||||
return dialogId;
|
||||
},
|
||||
[]
|
||||
);
|
||||
setDialogs(dialogs => [...dialogs, { ...props, id: dialogId }]);
|
||||
|
||||
return dialogId;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Dismisses the dialog with a given id.
|
||||
*
|
||||
* @param {Object} props -
|
||||
* @property {string} props.id The dialog id.
|
||||
* @returns void
|
||||
*/
|
||||
const dismiss = useCallback(({ id }) => {
|
||||
@@ -92,18 +88,102 @@ const DialogProvider = ({ children, service }) => {
|
||||
setDialogs([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicate if there are no dialogs present.
|
||||
*
|
||||
* @returns True if no dialogs are present.
|
||||
*/
|
||||
const isEmpty = () => dialogs && dialogs.length < 1;
|
||||
|
||||
/**
|
||||
* Moves the dialog to the foreground if clicked.
|
||||
*
|
||||
* @param {string} id The dialog id.
|
||||
* @returns void
|
||||
*/
|
||||
const _bringToFront = id => {
|
||||
setDialogs(dialogs => {
|
||||
const topDialog = dialogs.find(dialog => dialog.id === id);
|
||||
return topDialog
|
||||
? [...dialogs.filter(dialog => dialog.id !== id), topDialog]
|
||||
: [];
|
||||
});
|
||||
};
|
||||
|
||||
const renderDialogs = () =>
|
||||
dialogs.map(dialog => {
|
||||
const {
|
||||
id,
|
||||
content: DialogContent,
|
||||
contentProps,
|
||||
position,
|
||||
defaultPosition,
|
||||
isDraggable = true,
|
||||
onStart,
|
||||
onStop,
|
||||
onDrag,
|
||||
} = dialog;
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={id}
|
||||
disabled={!isDraggable}
|
||||
position={position}
|
||||
defaultPosition={lastDialogPosition || defaultPosition}
|
||||
bounds="parent"
|
||||
onStart={event => {
|
||||
const e = event || window.event;
|
||||
const target = e.target || e.srcElement;
|
||||
const BLACKLIST = [
|
||||
'SVG',
|
||||
'BUTTON',
|
||||
'PATH',
|
||||
'INPUT',
|
||||
'SPAN',
|
||||
'LABEL',
|
||||
];
|
||||
if (BLACKLIST.includes(target.tagName.toUpperCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validCallback(onStart)) {
|
||||
return onStart(event);
|
||||
}
|
||||
}}
|
||||
onStop={event => {
|
||||
setIsDragging(false);
|
||||
|
||||
if (validCallback(onStop)) {
|
||||
return onStop(event);
|
||||
}
|
||||
}}
|
||||
onDrag={event => {
|
||||
setIsDragging(true);
|
||||
_bringToFront(id);
|
||||
_updateLastDialogPosition(id);
|
||||
|
||||
if (validCallback(onDrag)) {
|
||||
return onDrag(event);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id={`draggableItem-${id}`}
|
||||
className={classNames('DraggableItem', isDragging && 'dragging')}
|
||||
style={{ zIndex: '999', position: 'absolute' }}
|
||||
onClick={() => _bringToFront(id)}
|
||||
>
|
||||
<DialogContent {...dialog} {...contentProps} />
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Update the last dialog position to be used as the new default position.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
const _reorder = id => {
|
||||
setDialogs(dialogs => [
|
||||
...dialogs.filter(dialog => dialog.id !== id),
|
||||
dialogs.find(dialog => dialog.id === id),
|
||||
]);
|
||||
};
|
||||
|
||||
const _updateLastDialogPosition = dialogId => {
|
||||
const draggableItemBounds = document
|
||||
.querySelector(`#draggableItem-${dialogId}`)
|
||||
@@ -114,70 +194,36 @@ const DialogProvider = ({ children, service }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const validCallback = callback => callback && typeof callback === 'function';
|
||||
|
||||
return (
|
||||
<DialogContext.Provider value={{ create, dismiss, dismissAll, dialogs }}>
|
||||
<DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}>
|
||||
<div className="DraggableArea">
|
||||
{dialogs.map(dialog => {
|
||||
const {
|
||||
id,
|
||||
content: Dialog,
|
||||
position /* Position of the dialog. {{x: 0, y: 0}} */,
|
||||
defaultPosition,
|
||||
isDraggable = true,
|
||||
onStart = () => {},
|
||||
onStop = () => {},
|
||||
onDrag = () => {},
|
||||
} = dialog;
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={id}
|
||||
disabled={!isDraggable}
|
||||
position={position}
|
||||
defaultPosition={lastDialogPosition || defaultPosition}
|
||||
bounds="parent"
|
||||
onStart={event => {
|
||||
const e = event || window.event;
|
||||
const target = e.target || e.srcElement;
|
||||
const BLACKLIST = ['SVG', 'BUTTON', 'PATH', 'INPUT'];
|
||||
if (BLACKLIST.includes(target.tagName.toUpperCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
onStart(event);
|
||||
}}
|
||||
onStop={event => {
|
||||
onStop(event);
|
||||
setIsDragging(false);
|
||||
return;
|
||||
}}
|
||||
onDrag={event => {
|
||||
setIsDragging(true);
|
||||
_reorder(id);
|
||||
_updateLastDialogPosition(id);
|
||||
onDrag(event);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id={`draggableItem-${id}`}
|
||||
className={classNames(
|
||||
'DraggableItem',
|
||||
isDragging && 'dragging'
|
||||
)}
|
||||
style={{ zIndex: '999', position: 'absolute' }}
|
||||
onClick={() => _reorder(id)}
|
||||
>
|
||||
<Dialog {...dialog} />
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{dialogs.some(dialog => dialog.showOverlay) ? (
|
||||
<div className="Overlay active">{renderDialogs()}</div>
|
||||
) : (
|
||||
renderDialogs()
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</DialogContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* High Order Component to use the dialog methods through a Class Component
|
||||
*
|
||||
*/
|
||||
export const withDialog = Component => {
|
||||
return function WrappedComponent(props) {
|
||||
const { create, dismiss, dismissAll, isEmpty } = useDialog();
|
||||
return (
|
||||
<Component {...props} dialog={{ create, dismiss, dismissAll, isEmpty }} />
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
DialogProvider.defaultProps = {
|
||||
service: null,
|
||||
};
|
||||
@@ -193,16 +239,4 @@ DialogProvider.propTypes = {
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* High Order Component to use the dialog methods through a Class Component
|
||||
*
|
||||
*/
|
||||
export const withDialog = Component => {
|
||||
return function WrappedComponent(props) {
|
||||
const { create, dismiss, dismissAll } = useDialog();
|
||||
return <Component {...props} dialog={{ create, dismiss, dismissAll }} />;
|
||||
};
|
||||
};
|
||||
|
||||
export default DialogProvider;
|
||||
@@ -6,7 +6,17 @@
|
||||
div
|
||||
cursor: grabbing !important
|
||||
|
||||
.DraggableArea
|
||||
width: 100vw
|
||||
height: 100vh
|
||||
.DraggableArea, .Overlay
|
||||
width: 100%
|
||||
height: 100%
|
||||
position: absolute
|
||||
|
||||
.Overlay.active
|
||||
position: fixed
|
||||
z-index: 999
|
||||
left: 0
|
||||
top: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
overflow: auto
|
||||
background: rgba(0,0,0,.1)
|
||||
@@ -13,14 +13,28 @@ const { Provider } = ModalContext;
|
||||
|
||||
export const useModal = () => useContext(ModalContext);
|
||||
|
||||
/**
|
||||
* UI Modal
|
||||
*
|
||||
* @typedef {Object} ModalProps
|
||||
* @property {ReactElement|HTMLElement} [content=null] Modal content.
|
||||
* @property {Object} [contentProps=null] Modal content props.
|
||||
* @property {boolean} [shouldCloseOnEsc=false] Modal is dismissible via the esc key.
|
||||
* @property {boolean} [isOpen=true] Make the Modal visible or hidden.
|
||||
* @property {boolean} [closeButton=true] Should the modal body render the close button.
|
||||
* @property {string} [title=null] Should the modal render the title independently of the body content.
|
||||
* @property {string} [customClassName=null] The custom class to style the modal.
|
||||
*/
|
||||
|
||||
const ModalProvider = ({ children, modal: Modal, service }) => {
|
||||
const DEFAULT_OPTIONS = {
|
||||
component: null /* The component instance inside the modal. */,
|
||||
shouldCloseOnEsc: false /* Modal is dismissible via the esc key. */,
|
||||
isOpen: true /* Make the Modal visible or hidden. */,
|
||||
closeButton: true /* Should the modal body render the close button. */,
|
||||
title: null /* Should the modal render the title independently of the body content. */,
|
||||
customClassName: '' /* The custom class to style the modal. */,
|
||||
content: null,
|
||||
contentProps: null,
|
||||
shouldCloseOnEsc: false,
|
||||
isOpen: true,
|
||||
closeButton: true,
|
||||
title: null,
|
||||
customClassName: '',
|
||||
};
|
||||
|
||||
const [options, setOptions] = useState(DEFAULT_OPTIONS);
|
||||
@@ -39,13 +53,12 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
|
||||
/**
|
||||
* Show the modal and override its configuration props.
|
||||
*
|
||||
* @param {ModalProps} props { content, contentProps, shouldCloseOnEsc, isOpen, closeButton, title, customClassName }
|
||||
* @returns void
|
||||
*/
|
||||
const show = useCallback(
|
||||
(component, props = {}) =>
|
||||
setOptions(Object.assign({}, options, props, { component })),
|
||||
[options]
|
||||
);
|
||||
const show = useCallback(props => setOptions({ ...options, ...props }), [
|
||||
options,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Hide the modal and set its properties to default.
|
||||
@@ -56,23 +69,28 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
|
||||
DEFAULT_OPTIONS,
|
||||
]);
|
||||
|
||||
const { component: Component } = options;
|
||||
const {
|
||||
content: ModalContent,
|
||||
contentProps,
|
||||
isOpen,
|
||||
title,
|
||||
customClassName,
|
||||
shouldCloseOnEsc,
|
||||
closeButton,
|
||||
} = options;
|
||||
|
||||
return (
|
||||
<Provider value={{ show, hide }}>
|
||||
{options.component && (
|
||||
{ModalContent && (
|
||||
<Modal
|
||||
className={classNames(
|
||||
options.customClassName,
|
||||
options.component.className
|
||||
)}
|
||||
shouldCloseOnEsc={options.keyboard}
|
||||
isOpen={options.isOpen}
|
||||
title={options.title}
|
||||
closeButton={options.closeButton}
|
||||
className={classNames(customClassName, ModalContent.className)}
|
||||
shouldCloseOnEsc={shouldCloseOnEsc}
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
closeButton={closeButton}
|
||||
onClose={hide}
|
||||
>
|
||||
<Component {...options} show={show} hide={hide} />
|
||||
<ModalContent {...contentProps} show={show} hide={hide} />
|
||||
</Modal>
|
||||
)}
|
||||
{children}
|
||||
@@ -80,6 +98,18 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Higher Order Component to use the modal methods through a Class Component.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const withModal = Component => {
|
||||
return function WrappedComponent(props) {
|
||||
const { show, hide } = useModal();
|
||||
return <Component {...props} modal={{ show, hide }} />;
|
||||
};
|
||||
};
|
||||
|
||||
ModalProvider.defaultProps = {
|
||||
service: null,
|
||||
};
|
||||
@@ -99,18 +129,6 @@ ModalProvider.propTypes = {
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Higher Order Component to use the modal methods through a Class Component.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const withModal = Component => {
|
||||
return function WrappedComponent(props) {
|
||||
const { show, hide } = useModal();
|
||||
return <Component {...props} modal={{ show, hide }} />;
|
||||
};
|
||||
};
|
||||
|
||||
export default ModalProvider;
|
||||
|
||||
export const ModalConsumer = ModalContext.Consumer;
|
||||
Reference in new issue
Block a user