import { isEqual } from 'lodash' import * as React from 'react' import * as ReactDOM from 'react-dom' import { v4 } from 'uuid' import { IConfig, ILink, INode, IOnLinkCancel, IOnLinkComplete, IOnLinkMove, IOnLinkStart, IOnPortPositionChange, IPort, IPosition, ISelectedOrHovered, } from '../../' import CanvasContext from '../Canvas/CanvasContext' import { IPortDefaultProps, PortDefault } from './Port.default' /** Construct the composed path by traversing parentElements */ const composedPath = (el: HTMLElement | null) => { const path: HTMLElement[] = [] while (el) { path.push(el) el = el.parentElement } return path } export interface IPortWrapperProps { config: IConfig style?: object offset: IPosition selected: ISelectedOrHovered | undefined hovered: ISelectedOrHovered | undefined selectedLink: ILink | undefined hoveredLink: ILink | undefined port: IPort node: INode onPortPositionChange: IOnPortPositionChange Component: React.FunctionComponent // Link handlers onLinkStart: IOnLinkStart onLinkMove: IOnLinkMove onLinkCancel: IOnLinkCancel onLinkComplete: IOnLinkComplete } export class PortWrapper extends React.Component { public static contextType = CanvasContext public context!: React.ContextType private nodeRef = React.createRef() public componentDidUpdate (prevProps: IPortWrapperProps) { // Update port position after a re-render if node.size has changed if (!isEqual(this.props.node.size, prevProps.node.size)) { const el = ReactDOM.findDOMNode(this.nodeRef.current) as HTMLInputElement if (el) { // Ports component should be positions absolute // Factor this in so we get position relative to the node const nodesEl = el.parentElement ? el.parentElement : { offsetLeft: 0, offsetTop: 0 } // update port position after node size has been determined this.props.onPortPositionChange({ config: this.props.config, node: this.props.node, port: this.props.port, el, nodesEl }) } } } public onMouseDown = (startEvent: React.MouseEvent) => { const { offset, node, port, onLinkStart, onLinkCancel, onLinkComplete, onLinkMove, config } = this.props const linkId = v4() const fromNodeId = node.id const fromPortId = port.id // Create the move handler // This will update the position as the mouse moves const mouseMoveHandler = (e: MouseEvent) => { const { offsetX, offsetY } = this.context onLinkMove({ config, linkId, startEvent, fromNodeId, fromPortId, toPosition: { x: e.clientX - offsetX - offset.x, y: e.clientY - offsetY - offset.y, }, }) } // Create and bind the mouse up handler // This is used to check if the link is complete or cancelled const mouseUpHandler = (e: MouseEvent) => { // We traverse up the event path until we find an element with 'data-port-id' and data-node-id' // e.toElement cannot be used because it may be a child element of the port const path = composedPath(e.target as HTMLElement) const portEl = path.find((el) => { const toPortId = el.getAttribute && el.getAttribute('data-port-id') const toNodeId = el.getAttribute && el.getAttribute('data-node-id') return !!(toPortId && toNodeId) }) // If both node-id and port-id are defined as data attributes, we are mouse-upping // on another port. Run the success handler if (portEl) { const toPortId = portEl.getAttribute('data-port-id') as string const toNodeId = portEl.getAttribute('data-node-id') as string onLinkComplete({ config, linkId, startEvent, fromNodeId, fromPortId, toNodeId, toPortId }) } else { onLinkCancel({ config, linkId, startEvent, fromNodeId, fromPortId }) } // Remove the listeners if the link is complete or canceled window.removeEventListener('mouseup', mouseUpHandler, false) window.removeEventListener('mousemove', mouseMoveHandler, false) } // Add listeners window.addEventListener('mouseup', mouseUpHandler, false) window.addEventListener('mousemove', mouseMoveHandler, false) // Notify state of link start onLinkStart({ config, linkId, startEvent, fromNodeId, fromPortId }) // Prevent default and stop propagation to prevent text selection startEvent.preventDefault() startEvent.stopPropagation() } public render () { const { selected, selectedLink, hovered, hoveredLink, style, port, node, Component = PortDefault, config, } = this.props return (
) } }