import { useContext, useState, useRef } from 'react'; import * as d3 from 'd3'; import { observer } from 'mobx-react'; import { action } from 'mobx'; import ElementContext from '../utils/ElementContext'; import useCallbackRef from '../utils/useCallbackRef'; import { Graph, GRAPH_AREA_DRAGGING_EVENT, GRAPH_AREA_SELECTED_EVENT, isGraph, ModifierKey } from '../types'; import Point from '../geom/Point'; export type AreaSelectionRef = (node: SVGGElement | null) => void; // Used to send events prevented by d3.zoom to the document allowing modals, dropdowns, etc, to close const propagateAreaSelectionMouseEvent = (e: Event): void => { document.dispatchEvent(new MouseEvent(e.type, e)); }; export const useAreaSelection = (modifiers: ModifierKey[] = ['ctrlKey']): WithAreaSelectionProps => { const element = useContext(ElementContext); const [draggingState, setDraggingState] = useState>({}); if (!isGraph(element)) { throw new Error('useAreaSelection must be used within the scope of a Graph'); } const elementRef = useRef(element); elementRef.current = element; const areaSelectionRef = useCallbackRef((node: SVGGElement | null) => { if (node) { // TODO fix any type const $svg = d3.select(node.ownerSVGElement) as any; if (node && node.ownerSVGElement) { node.ownerSVGElement.addEventListener('mousedown', propagateAreaSelectionMouseEvent); node.ownerSVGElement.addEventListener('click', propagateAreaSelectionMouseEvent); } const drag = d3 .drag() .on( 'start', action((event: d3.D3DragEvent) => { const { offsetX, offsetY } = event.sourceEvent instanceof MouseEvent ? event.sourceEvent : { offsetX: 0, offsetY: 0 }; const { width: maxX, height: maxY } = elementRef.current.getDimensions(); const startPoint = new Point(Math.min(Math.max(offsetX, 0), maxX), Math.min(Math.max(offsetY, 0), maxY)); const modifier = modifiers.find((m) => event.sourceEvent[m]); setDraggingState({ modifier, isAreaSelectDragging: true, areaSelectDragStart: startPoint, areaSelectDragEnd: startPoint }); elementRef.current .getController() .fireEvent(GRAPH_AREA_DRAGGING_EVENT, { graph: elementRef.current, isDragging: true }); }) ) .on( 'drag', action((event: d3.D3DragEvent) => { const { offsetX, offsetY } = event.sourceEvent instanceof MouseEvent ? event.sourceEvent : { offsetX: 0, offsetY: 0 }; const { width: maxX, height: maxY } = elementRef.current.getDimensions(); setDraggingState((prev) => ({ ...prev, areaSelectDragEnd: new Point(Math.min(Math.max(offsetX, 0), maxX), Math.min(Math.max(offsetY, 0), maxY)) })); }) ) .on( 'end', action(() => { setDraggingState((prev) => { elementRef.current.getController().fireEvent(GRAPH_AREA_SELECTED_EVENT, { graph: elementRef.current, modifier: prev.modifier, startPoint: prev.areaSelectDragStart, endPoint: prev.areaSelectDragEnd }); return { isAreaSelectDragging: false }; }); elementRef.current .getController() .fireEvent(GRAPH_AREA_DRAGGING_EVENT, { graph: elementRef.current, isDragging: false }); }) ) .filter((event: React.MouseEvent) => modifiers.find((m) => event[m]) && !event.button); drag($svg); } return () => { if (node) { // remove all drag listeners d3.select(node.ownerSVGElement).on('.drag', null); if (node.ownerSVGElement) { node.ownerSVGElement.removeEventListener('mousedown', propagateAreaSelectionMouseEvent); node.ownerSVGElement.removeEventListener('click', propagateAreaSelectionMouseEvent); } } }; }); return { areaSelectionRef, ...draggingState }; }; export interface WithAreaSelectionProps { areaSelectionRef?: AreaSelectionRef; modifier?: ModifierKey; isAreaSelectDragging?: boolean; areaSelectDragStart?: Point; areaSelectDragEnd?: Point; } export const withAreaSelection = (modifier: ModifierKey[] = ['ctrlKey']) =>

(WrappedComponent: React.ComponentType

) => { const Component: React.FunctionComponent> = (props) => { const areaSelectionProps = useAreaSelection(modifier); return ; }; Component.displayName = `withAreaSelection(${WrappedComponent.displayName || WrappedComponent.name})`; return observer(Component); };