import React, { useState, createContext, useContext, useCallback, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import Draggable from 'react-draggable'; import classNames from 'classnames'; /* * This is a workaround to import things from ohif/core as docz does * not allow us to access window element and @ohif/core does use it once * we import to instanciate cornerstone */ import guid from './../../../core/src/utils/guid'; import './DialogProvider.css'; const DialogContext = createContext(null); export const useDialog = () => useContext(DialogContext); const DialogProvider = ({ children, service }) => { const [isDragging, setIsDragging] = useState(false); const [dialogs, setDialogs] = useState([]); const [lastDialogId, setLastDialogId] = useState(null); const [lastDialogPosition, setLastDialogPosition] = useState(null); const [centerPositions, setCenterPositions] = useState([]); useEffect(() => { setCenterPositions( dialogs.map(dialog => ({ id: dialog.id, ...getCenterPosition(dialog.id), })) ); }, [dialogs]); const getCenterPosition = id => { const root = document.querySelector('#root'); const centerX = root.offsetLeft + root.offsetWidth / 2; const centerY = root.offsetTop + root.offsetHeight / 2; const item = document.querySelector(`#draggableItem-${id}`); const itemBounds = item.getBoundingClientRect(); return { x: centerX - itemBounds.width / 2, y: centerY - itemBounds.height / 2, }; }; /** * UI Dialog * * @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 {boolean} centralize Center the dialog on the screen. * @property {boolean} preservePosition Use last position instead of default. * @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at. * @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. */ /** * 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; let dialogId = id; if (!dialogId) { dialogId = guid(); } setDialogs(dialogs => [...dialogs, { ...props, id: dialogId }]); setLastDialogId(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 }) => setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id)), [] ); /** * Dismisses all dialogs. * * @returns void */ const dismissAll = () => { 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 = useCallback(id => { setDialogs(dialogs => { const topDialog = dialogs.find(dialog => dialog.id === id); return topDialog ? [...dialogs.filter(dialog => dialog.id !== id), topDialog] : dialogs; }); }, []); /** * Sets the implementation of a dialog service that can be used by extensions. * * @returns void */ useEffect(() => { if (service) { service.setServiceImplementation({ create, dismiss, dismissAll }); } }, [create, dismiss, service]); useEffect(() => _bringToFront(lastDialogId), [_bringToFront, lastDialogId]); const renderDialogs = () => dialogs.map(dialog => { const { id, content: DialogContent, contentProps, defaultPosition, centralize = false, preservePosition = true, isDraggable = true, onStart, onStop, onDrag, onClickOutside, showOverlay, } = dialog; let position = (preservePosition && lastDialogPosition) || defaultPosition; if (centralize) { position = centerPositions.find(position => position.id === id); } const dragableItem = () => ( { const e = event || (typeof window !== 'undefined' && 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); } }} >
_bringToFront(id)} >
); const withOverlay = component => { const background = 'bg-black bg-opacity-50'; const overlay = 'fixed z-50 left-0 top-0 w-full h-full overflow-auto'; return (
{component}
); }; let result = dragableItem(); if (showOverlay) { result = withOverlay(result); } if (typeof onClickOutside === 'function') { result = ( {result} ); } return result; }); /** * Update the last dialog position to be used as the new default position. * * @returns void */ const _updateLastDialogPosition = dialogId => { const draggableItemBounds = document .querySelector(`#draggableItem-${dialogId}`) .getBoundingClientRect(); setLastDialogPosition({ x: draggableItemBounds.x, y: draggableItemBounds.y, }); }; const onKeyDownHandler = event => { if (event.key === 'Escape') { dismissAll(); } }; const validCallback = callback => callback && typeof callback === 'function'; return ( {!isEmpty() && (
{renderDialogs()}
)} {children}
); }; /** * * 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 ( ); }; }; DialogProvider.defaultProps = { service: null, }; DialogProvider.propTypes = { children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node, PropTypes.func]) .isRequired, service: PropTypes.shape({ setServiceImplementation: PropTypes.func, }), }; export default DialogProvider; function OutsideAlerter(props) { const wrapperRef = useRef(null); useEffect(() => { /** * Alert if clicked on outside of element */ function handleInteractionOutside(event) { if (wrapperRef.current && !wrapperRef.current.contains(event.target)) { props.onClickOutside(); } } // Bind the event listener document.addEventListener('mousedown', handleInteractionOutside); document.addEventListener('touchstart', handleInteractionOutside); return () => { // Unbind the event listener on clean up document.removeEventListener('mousedown', handleInteractionOutside); document.removeEventListener('touchstart', handleInteractionOutside); }; }, [wrapperRef]); return
{props.children}
; }