import { isValidElement, useState, useMemo, useRef, useCallback } from 'react'; import { observer } from 'mobx-react'; import { css } from '@patternfly/react-styles'; import styles from '../css/topology-components'; import { hullPath } from '../utils/svg-utils'; import DefaultCreateConnector from '../components/DefaultCreateConnector'; import Point from '../geom/Point'; import Layer from '../components/layers/Layer'; import { ContextMenu, ContextMenuItem } from '../components/contextmenu'; import { AnchorEnd, Graph, GraphElement, isGraph, isNode, LabelPosition, Node } from '../types'; import { DragEvent, DragObjectWithType, DragOperationWithType, DragSourceMonitor, DragSourceSpec, DragSpecOperationType } from './dnd-types'; import { useDndDrag } from './useDndDrag'; import { TOP_LAYER } from '../const'; import { useCombineRefs, useHover } from '../utils'; export const CREATE_CONNECTOR_OPERATION = '#createconnector#'; export const CREATE_CONNECTOR_DROP_TYPE = '#createConnector#'; export interface ConnectorChoice { label: string; } export interface CreateConnectorOptions { handleAngle?: number; handleAngleTop?: number; handleLength?: number; dragItem?: DragObjectWithType; dragOperation?: DragOperationWithType; hideConnectorMenu?: boolean; } interface ConnectorComponentProps { startPoint: Point; endPoint: Point; hints: string[]; dragging: boolean; hover?: boolean; } type CreateConnectorRenderer = React.ComponentType; type OnCreateResult = ConnectorChoice[] | void | undefined | null | React.ReactElement[]; type CreateConnectorWidgetProps = { element: Node; onKeepAlive: (isAlive: boolean) => void; onCreate: ( element: Node, target: Node | Graph, event: DragEvent, dropHints?: string[] | undefined, choice?: ConnectorChoice ) => Promise | OnCreateResult; ConnectorComponent: CreateConnectorRenderer; contextMenuClass?: string; } & CreateConnectorOptions; interface CollectProps { event?: DragEvent; dragging: boolean; hints?: string[] | undefined; } interface PromptData { element: Node; target: Node | Graph; event: DragEvent; choices: ConnectorChoice[] | React.ReactElement[]; } const isReactElementArray = (choices: ConnectorChoice[] | React.ReactElement[]): choices is React.ReactElement[] => isValidElement(choices[0]); const DEFAULT_HANDLE_ANGLE = Math.PI / 180; const DEFAULT_HANDLE_ANGLE_TOP = 1.5 * Math.PI; const DEFAULT_HANDLE_LENGTH = 32; const CreateConnectorWidget: React.FunctionComponent = observer((props) => { const { element, onKeepAlive, onCreate, ConnectorComponent, handleAngle = DEFAULT_HANDLE_ANGLE, handleAngleTop = DEFAULT_HANDLE_ANGLE_TOP, handleLength = DEFAULT_HANDLE_LENGTH, contextMenuClass, dragItem, dragOperation, hideConnectorMenu } = props; const [prompt, setPrompt] = useState(null); const [active, setActive] = useState(false); const hintsRef = useRef(undefined); const spec = useMemo(() => { const dragSourceSpec: DragSourceSpec< DragObjectWithType, DragSpecOperationType, GraphElement, CollectProps, CreateConnectorWidgetProps > = { item: dragItem || { type: CREATE_CONNECTOR_DROP_TYPE }, operation: dragOperation || { type: CREATE_CONNECTOR_OPERATION }, begin: (monitor: DragSourceMonitor, dragProps: any) => { setActive(true); return dragProps.element; }, drag: (event: DragEvent, monitor: DragSourceMonitor, p: CreateConnectorWidgetProps) => { p.element.raise(); }, end: async (dropResult: GraphElement, monitor: DragSourceMonitor, dragProps: CreateConnectorWidgetProps) => { const event = monitor.getDragEvent(); if ((isNode(dropResult) || isGraph(dropResult)) && event) { const choices = await dragProps.onCreate(dragProps.element, dropResult, event, monitor.getDropHints()); if (choices && choices.length && !hideConnectorMenu) { setPrompt({ element: dragProps.element, target: dropResult, event, choices }); return; } } setActive(false); dragProps.onKeepAlive(false); }, collect: (monitor) => ({ dragging: !!monitor.getItem(), event: monitor.isDragging() ? monitor.getDragEvent() : undefined, hints: monitor.getDropHints() }) }; return dragSourceSpec; }, [setActive, dragItem, dragOperation, hideConnectorMenu]); const [{ dragging, event, hints }, dragRef] = useDndDrag(spec, props); const [hover, hoverRef] = useHover(); const refs = useCombineRefs(dragRef, hoverRef); if (!active && dragging && !event) { // another connector is dragging right now return null; } if (dragging) { // store the latest hints hintsRef.current = hints; } const dragEvent = prompt ? prompt.event : event; let startPoint: Point; let endPoint: Point; if (dragEvent) { endPoint = new Point(dragEvent.x, dragEvent.y); startPoint = element.getAnchor(AnchorEnd.source).getLocation(endPoint); } else { const bounds = element.getBounds(); const isRightLabel = element.getLabelPosition() === LabelPosition.right; const referencePoint = isRightLabel ? new Point(bounds.x + bounds.width / 2, bounds.y) : new Point(bounds.right(), Math.tan(handleAngle) * (bounds.width / 2) + bounds.y + bounds.height / 2); startPoint = element.getAnchor(AnchorEnd.source).getLocation(referencePoint); endPoint = new Point( Math.cos(isRightLabel ? handleAngleTop : handleAngle) * handleLength + startPoint.x, Math.sin(isRightLabel ? handleAngleTop : handleAngle) * handleLength + startPoint.y ); } // bring into the coordinate space of the element element.translateFromParent(startPoint); element.translateFromParent(endPoint); return ( <> onKeepAlive(true) : undefined} onMouseLeave={!active ? () => onKeepAlive(false) : undefined} > {prompt && ( { setActive(false); onKeepAlive(false); }} > {isReactElementArray(prompt.choices) ? prompt.choices : prompt.choices.map((c: ConnectorChoice) => ( { onCreate(prompt.element, prompt.target, prompt.event, hintsRef.current, c); }} > {c.label} ))} )} ); }); interface ElementProps { element: GraphElement; } export interface WithCreateConnectorProps { onShowCreateConnector?: () => void; onHideCreateConnector?: () => void; } export const withCreateConnector =

( onCreate: React.ComponentProps['onCreate'], ConnectorComponent: CreateConnectorRenderer = DefaultCreateConnector, contextMenuClass?: string, options?: CreateConnectorOptions ) => (WrappedComponent: React.ComponentType

) => { const Component: React.FunctionComponent> = (props) => { const [show, setShow] = useState(false); const [alive, setKeepAlive] = useState(false); const onShowCreateConnector = useCallback(() => setShow(true), []); const onHideCreateConnector = useCallback(() => setShow(false), []); const onKeepAlive = useCallback((isAlive: boolean) => setKeepAlive(isAlive), [setKeepAlive]); return ( <> {(show || alive) && ( )} ); }; Component.displayName = `withCreateConnector(${WrappedComponent.displayName || WrappedComponent.name})`; return observer(Component); };