import { Component } from 'react'; import type { MouseEventHandler, MouseEvent } from 'react'; import { scaleLinear, drag, select } from 'd3'; import { Map } from 'immutable'; import invariant from 'invariant'; import { Node, Port, Position, Size } from '../../api'; import { GRID_SIZE, PORT_SINK, PORT_SOURCE } from '../../constants/flowdesigner.constants'; import { PortRecordMap, Position as PositionType, Size as SizeType, NodeRecord, Id, } from '../../customTypings/index.d'; export const ABSTRACT_NODE_INVARIANT = ` should not be used without giving it a children ex: `; /** * calculate the position of each ports for a given node information * @param ports * @param nodePosition * @param nodeSize */ function calculatePortPosition( ports: PortRecordMap, nodePosition: PositionType, nodeSize: SizeType, ) { let portsWithPosition = Map(); const emitterPorts = ports.filter(port => Port.getTopology(port) === PORT_SOURCE); const sinkPorts = ports.filter(port => Port.getTopology(port) === PORT_SINK); const range = [ Position.getYCoordinate(nodePosition), Position.getYCoordinate(nodePosition) + Size.getHeight(nodeSize), ]; const scaleYEmitter = scaleLinear() .domain([0, emitterPorts.size + 1]) .range(range); const scaleYSink = scaleLinear() .domain([0, sinkPorts.size + 1]) .range(range); let emitterNumber = 0; let sinkNumber = 0; emitterPorts .sort((a, b) => { if (Port.getIndex(a) < Port.getIndex(b)) { return -1; } if (Port.getIndex(a) > Port.getIndex(b)) { return 1; } return 0; }) .forEach(port => { emitterNumber += 1; const position = Position.create( Position.getXCoordinate(nodePosition) + Size.getWidth(nodeSize), scaleYEmitter(emitterNumber), ); portsWithPosition = portsWithPosition.set(Port.getId(port), Port.setPosition(port, position)); }); sinkPorts .sort((a, b) => { if (Port.getIndex(a) < Port.getIndex(b)) { return -1; } if (Port.getIndex(a) > Port.getIndex(b)) { return 1; } return 0; }) .forEach(port => { sinkNumber += 1; const position = Position.create( Position.getXCoordinate(nodePosition), scaleYSink(sinkNumber), ); portsWithPosition = portsWithPosition.set(Port.getId(port), Port.setPosition(port, position)); }); return portsWithPosition; } type Props = { node: NodeRecord; startMoveNodeTo: (nodeId: Id, nodePosition: PositionType) => void; moveNodeTo: (nodeId: Id, nodePosition: PositionType) => void; moveNodeToEnd: (nodeId: Id, nodePosition: PositionType) => void; snapToGrid?: boolean; onDragStart?: (event: any) => void; onDrag?: (event: any) => void; onDragEnd?: (event: any) => void; onClick?: MouseEventHandler; onDoubleClick?: MouseEventHandler; children?: any; }; class AbstractNode extends Component { static calculatePortPosition = calculatePortPosition; d3Node: any; nodeElement: any; squaredDeltaDrag = 0; constructor(props: Props) { super(props); this.onClick = this.onClick.bind(this); this.onDoubleClick = this.onDoubleClick.bind(this); this.onDragStart = this.onDragStart.bind(this); this.onDrag = this.onDrag.bind(this); this.onDragEnd = this.onDragEnd.bind(this); this.renderContent = this.renderContent.bind(this); this.getEventPosition = this.getEventPosition.bind(this); } componentDidMount() { this.d3Node = select(this.nodeElement); this.d3Node.data([this.props.node.getPosition()]); this.d3Node.call( drag().on('start', this.onDragStart).on('drag', this.onDrag).on('end', this.onDragEnd), ); } componentDidUpdate(prevProps: Props) { if (prevProps.node !== this.props.node) { const nextPosition = Node.getPosition(this.props.node); if (nextPosition !== Node.getPosition(prevProps.node)) { this.d3Node.data([nextPosition]); } } } shouldComponentUpdate(nextProps: Props) { return nextProps !== this.props; } componentWillUnmount() { this.d3Node.remove(); } onClick(clickEvent: MouseEvent) { if (this.props.onClick) { this.props.onClick(clickEvent); } } onDoubleClick(clickEvent: MouseEvent) { if (this.props.onDoubleClick) { this.props.onDoubleClick(clickEvent); } } onDragStart(event: any) { this.squaredDeltaDrag = 0; const position = { x: event.x, y: event.y, }; this.props.startMoveNodeTo(this.props.node.id, position); if (this.props.onDragStart) { this.props.onDragStart(event); } } onDrag(event: any) { this.squaredDeltaDrag += event.dx * event.dx + event.dy * event.dy; const position = { x: event.x, y: event.y, movementX: event.sourceEvent.movementX, movementY: event.sourceEvent.movementY, }; this.props.moveNodeTo(this.props.node.id, position); if (this.props.onDrag) { this.props.onDrag(position); } } onDragEnd(event: any) { // Ok this is pretty specific // for a chrome windows bug // where d3 inhibit onCLick propagation // if there is any delta between down and up of the mouse // here we add a tolerance, so the underlying click doesn't // get smooshed if the user do not initiate drag if (this.squaredDeltaDrag < 1) { select(window).on('click.drag', null); } const position = this.getEventPosition(event); this.props.moveNodeToEnd(this.props.node.id, position); this.d3Node.data([position]); if (this.props.onDragEnd) { this.props.onDragEnd(position); } } getEventPosition(event: any) { if (this.props.snapToGrid) { return { x: event.x - (event.x % GRID_SIZE), y: event.y - (event.y % GRID_SIZE), }; } return { x: event.x, y: event.y }; } renderContent() { if (this.props.children) { return this.props.children; } invariant(false, ABSTRACT_NODE_INVARIANT); return null; } render() { const { node } = this.props; const { x, y } = Node.getPosition(node); const transform = `translate(${x}, ${y})`; return ( { this.nodeElement = c; }} onClick={this.onClick} onDoubleClick={this.onDoubleClick} data-testid={`group.${node.id}`} > {this.renderContent()} ); } } export default AbstractNode;