import { inject , InjectionKey , onMounted , onUnmounted , provide , Ref , watch } from "vue" ; import { Emitter } from "mitt" ; import { nonNull , Reactive , ReadonlyRef } from "../common/common" ; import { Events , NodePositions , Position , EdgeEvent } from "../common/types" ; import { entriesOf , MapUtil } from "../common/utility" ; import { EdgeStates , NodeStates } from "./state" ; type NodeEventHandler < T extends Event = PointerEvent > = ( node : string , event : T ) => void ; type EdgeEventHandler < T extends Event = PointerEvent > = ( edge : string , event : T ) => void ; type EdgesEventHandler < T extends Event = PointerEvent > = ( edges : string [ ] , event : T ) => void ; interface MouseEventHandlers { selectedNodes : Reactive < Set < string > > ; hoveredNodes : Reactive < Set < string > > ; selectedEdges : Reactive < Set < string > > ; hoveredEdges : Reactive < Set < string > > ; handleNodePointerDownEvent : NodeEventHandler ; handleNodePointerOverEvent : NodeEventHandler ; handleNodePointerOutEvent : NodeEventHandler ; handleNodeContextMenu : NodeEventHandler < MouseEvent > ; handleEdgePointerDownEvent : EdgeEventHandler ; handleEdgePointerOverEvent : EdgeEventHandler ; handleEdgePointerOutEvent : EdgeEventHandler ; handleEdgeContextMenu : EdgeEventHandler < MouseEvent > ; handleEdgesPointerDownEvent : EdgesEventHandler ; handleEdgesPointerOverEvent : EdgesEventHandler ; handleEdgesPointerOutEvent : EdgesEventHandler ; handleEdgesContextMenu : EdgesEventHandler < MouseEvent > ; } const mouseEventHandlersKey = Symbol ( "mouseEventHandlers" ) as InjectionKey < MouseEventHandlers > ; const MOVE_DETECTION_THRESHOLD = 3 ; interface NodePointerState { pointerId : number ; nodeId : string ; moveCounter : number ; dragBasePosition : Position ; nodeBasePosition : Position ; latestPosition : Position ; } interface EdgePointerState { pointerId : number ; edgeId : string | string [ ] ; } interface State { container : { moveCounter : number ; pointerCounter : number ; } ; nodePointers : Map < number , NodePointerState > ; follow : { followedPointerId : number ; nodeBasePositions : { [ name ] : Position ; } ; } ; hoveredNodes : Reactive < Set < string > > ; hoveredNodesPre : Set < string > ; hoveredEdges : Reactive < Set < string > > ; edgePointers : Map < number , EdgePointerState > ; edgePointerPeekCount : number ; } type PointerPosition = Pick < PointerEvent , "pageX" | "pageY" | "pointerId" > ; function _unwrapNodePosition ( nodes , node ) { const pos = nodes [ node ] ?? { x : 0 , y : 0 } ; return { ... pos } ; } function _makeEdgeEventObject < T extends Event > ( edge , event ) { if ( edge instanceof Array ) { return { edges : edge , event , summarized : true , } ; } else { return { edge , edges : [ edge ] , event , summarized : false , } ; } } export function provideMouseOperation ( container , nodePositions , zoomLevel , nodeStates , edgeStates , selectedNodes , selectedEdges , hoveredNodes , hoveredEdges , emitter ) { onMounted ( ( ) => { container . value ?. addEventListener ( "pointerdown" , handleContainerPointerDownEvent , { passive : true , } ) ; container . value ?. addEventListener ( "contextmenu" , handleContainerContextMenuEvent , { passive : false , } ) ; } ) ; onUnmounted ( ( ) => { container . value ?. removeEventListener ( "pointerdown" , handleContainerPointerDownEvent ) ; container . value ?. removeEventListener ( "contextmenu" , handleContainerContextMenuEvent ) ; } ) ; const state = { container : { moveCounter : 0 , pointerCounter : 0 , } , nodePointers : new Map ( ) , follow : { followedPointerId : - 1 , nodeBasePositions : { } , } , hoveredNodes , hoveredNodesPre : new Set ( ) , hoveredEdges , edgePointers : new Map ( ) , edgePointerPeekCount : 0 , } ; const containerPointerHandlers = { pointermove : handleContainerPointerMoveEvent , pointerup : handleContainerPointerUpEvent , pointercancel : handleContainerPointerUpEvent , } ; const nodePointerHandlers = { pointermove : handleNodePointerMoveEvent , pointerup : handleNodePointerUpEvent , pointercancel : handleNodePointerCancelEvent , } ; const edgePointerHandlers = { pointerup : handleEdgePointerUpEvent , pointercancel : handleEdgePointerCancelEvent , } ; function handleContainerPointerDownEvent ( _ ) { state . container . moveCounter = 0 ; if ( state . container . pointerCounter === 0 ) { entriesOf ( containerPointerHandlers ) . forEach ( ( [ ev , handler ] ) => { container . value ?. addEventListener ( ev , handler , { passive : true } ) ; } ) ; } state . container . pointerCounter ++ ; } function handleContainerPointerMoveEvent ( _ ) { state . container . moveCounter ++ ; } function handleContainerPointerUpEvent ( event ) { state . container . pointerCounter -- ; if ( state . container . pointerCounter === 0 ) { entriesOf ( containerPointerHandlers ) . forEach ( ( [ ev , handler ] ) => { container . value ?. removeEventListener ( ev , handler ) ; } ) ; if ( state . container . moveCounter <= MOVE_DETECTION_THRESHOLD ) { if ( event . shiftKey && ( selectedNodes . size > 0 || selectedEdges . size > 0 ) ) { return ; } selectedNodes . clear ( ) ; selectedEdges . clear ( ) ; emitter . emit ( "view:click" , { event } ) ; } } } function handleContainerContextMenuEvent ( event ) { emitter . emit ( "view:contextmenu" , { event } ) ; if ( state . container . pointerCounter > 0 ) { state . container . pointerCounter = 0 ; entriesOf ( containerPointerHandlers ) . forEach ( ( [ ev , handler ] ) => { container . value ?. removeEventListener ( ev , handler ) ; } ) ; } } function _updateFollowNodes ( pointerState ) { const isFollowed = state . follow . followedPointerId === pointerState . pointerId ; const isSelectedNode = selectedNodes . has ( pointerState . nodeId ) ; const removed = ! ( pointerState . pointerId in state . nodePointers ) ; if ( ( isFollowed && removed ) || ( isFollowed && ! isSelectedNode ) ) { const candidate = MapUtil . valueOf ( state . nodePointers ) . find ( p => selectedNodes . has ( p . nodeId ) ) ; if ( ! candidate ) { state . follow = { followedPointerId : - 1 , nodeBasePositions : { } } ; return ; } pointerState = candidate ; state . follow . followedPointerId = pointerState . pointerId ; } else { const followed = state . nodePointers . get ( state . follow . followedPointerId ) ; if ( ! followed ) { state . follow = { followedPointerId : - 1 , nodeBasePositions : { } } ; return ; } pointerState = followed ; } if ( isFollowed || isSelectedNode ) { const userGrabs = MapUtil . valueOf ( state . nodePointers ) . map ( n => n . nodeId ) ; state . follow . nodeBasePositions = Object . fromEntries ( Array . from ( selectedNodes ) . filter ( n => ! userGrabs . includes ( n ) ) . filter ( n => nodeStates [ n ] ?. draggable ) . map ( n => [ n , _unwrapNodePosition ( nodePositions , n ) ] ) ) ; pointerState . dragBasePosition = { ... pointerState . latestPosition } ; pointerState . nodeBasePosition = _unwrapNodePosition ( nodePositions , pointerState . nodeId ) ; } } watch ( selectedNodes , ( ) => { const pointerState = state . nodePointers . get ( state . follow . followedPointerId ) ; if ( pointerState ) { _updateFollowNodes ( pointerState ) ; } } ) ; function _calculateNodeNewPosition ( pointerState , event ) { const dx = pointerState . dragBasePosition . x - event . pageX ; const dy = pointerState . dragBasePosition . y - event . pageY ; const positions = state . follow . followedPointerId == pointerState . pointerId ? { [ pointerState . nodeId ] : pointerState . nodeBasePosition , ... state . follow . nodeBasePositions , } : { [ pointerState . nodeId ] : pointerState . nodeBasePosition } ; const z = zoomLevel . value ; return Object . fromEntries ( Object . entries ( positions ) . map ( ( [ node , pos ] ) => [ node , { x : pos . x - dx / z , y : pos . y - dy / z , } , ] ) ) ; } function handleNodeClickEvent ( node , event ) { if ( event . shiftKey && selectedEdges . size > 0 ) { return ; } selectedEdges . clear ( ) ; const selectable = nodeStates [ node ] ?. selectable ?? false ; if ( selectable ) { const isTouchAnySelectedNode = MapUtil . valueOf ( state . nodePointers ) . filter ( p => p . pointerId != event . pointerId && selectedNodes . has ( p . nodeId ) ) . length > 0 ; if ( event . shiftKey || isTouchAnySelectedNode ) { if ( selectedNodes . has ( node ) ) { selectedNodes . delete ( node ) ; } else if ( ! ( typeof selectable === "number" && selectedNodes . size >= selectable ) ) { selectedNodes . add ( node ) ; } } else if ( ! selectedNodes . has ( node ) ) { selectedNodes . clear ( ) ; selectedNodes . add ( node ) ; } } emitter . emit ( "node:click" , { node , event } ) ; } function handleNodePointerMoveEvent ( event ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; const pointerState = state . nodePointers . get ( event . pointerId ) ; if ( ! pointerState ) { return ; } pointerState . latestPosition = { x : event . pageX , y : event . pageY } ; pointerState . moveCounter ++ ; if ( pointerState . moveCounter <= MOVE_DETECTION_THRESHOLD ) { return ; } if ( ! nodeStates [ pointerState . nodeId ] ?. draggable ) { return ; } if ( pointerState . moveCounter === MOVE_DETECTION_THRESHOLD + 1 ) { const draggingNodes = _calculateNodeNewPosition ( pointerState , { pointerId : pointerState . pointerId , pageX : pointerState . dragBasePosition . x , pageY : pointerState . dragBasePosition . y , } ) ; emitter . emit ( "node:dragstart" , draggingNodes ) ; } const draggingNodes = _calculateNodeNewPosition ( pointerState , event ) ; emitter . emit ( "node:pointermove" , draggingNodes ) ; } function handleNodePointerCancelEvent ( event ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; let pointerState = state . nodePointers . get ( event . pointerId ) ; if ( ! pointerState ) { return ; } for ( pointerState of state . nodePointers . values ( ) ) { const node = pointerState . nodeId ; const isMoved = pointerState . moveCounter > MOVE_DETECTION_THRESHOLD ; if ( isMoved ) { const draggingNodes = _calculateNodeNewPosition ( pointerState , { pointerId : pointerState . pointerId , pageX : pointerState . latestPosition . x , pageY : pointerState . latestPosition . y , } ) ; emitter . emit ( "node:dragend" , draggingNodes ) ; } emitter . emit ( "node:pointerup" , { node , event } ) ; } state . nodePointers . clear ( ) ; state . follow = { followedPointerId : - 1 , nodeBasePositions : { } } ; entriesOf ( nodePointerHandlers ) . forEach ( ( [ ev , handler ] ) => { document . removeEventListener ( ev , handler ) ; } ) ; emitter . emit ( "view:mode" , "default" ) ; } function handleNodePointerUpEvent ( event ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; const pointerState = state . nodePointers . get ( event . pointerId ) ; if ( ! pointerState ) { return ; } state . nodePointers . delete ( event . pointerId ) ; const node = pointerState . nodeId ; const isMoved = pointerState . moveCounter > MOVE_DETECTION_THRESHOLD ; if ( isMoved ) { if ( nodeStates [ pointerState . nodeId ] ?. draggable ) { const draggingNodes = _calculateNodeNewPosition ( pointerState , event ) ; emitter . emit ( "node:dragend" , draggingNodes ) ; emitter . emit ( "node:pointerup" , { node , event } ) ; } } else { emitter . emit ( "node:pointerup" , { node , event } ) ; handleNodeClickEvent ( node , event ) ; } if ( state . nodePointers . size == 0 ) { state . follow = { followedPointerId : - 1 , nodeBasePositions : { } } ; entriesOf ( nodePointerHandlers ) . forEach ( ( [ ev , handler ] ) => { document . removeEventListener ( ev , handler ) ; } ) ; emitter . emit ( "view:mode" , "default" ) ; } else { _updateFollowNodes ( pointerState ) ; } state . hoveredNodes . clear ( ) ; state . hoveredNodesPre . forEach ( state . hoveredNodes . add , state . hoveredNodes ) ; } function handleNodePointerDownEvent ( node , event ) { if ( event . button == 2 ) { return ; } event . preventDefault ( ) ; event . stopPropagation ( ) ; if ( state . edgePointers . size !== 0 ) { return ; } if ( state . nodePointers . size == 0 ) { emitter . emit ( "view:mode" , "node" ) ; entriesOf ( nodePointerHandlers ) . forEach ( ( [ ev , handler ] ) => { document . addEventListener ( ev , handler ) ; } ) ; } const pointerState = { pointerId : event . pointerId , nodeId : node , moveCounter : 0 , nodeBasePosition : _unwrapNodePosition ( nodePositions , node ) , dragBasePosition : { x : event . pageX , y : event . pageY } , latestPosition : { x : event . pageX , y : event . pageY } , } ; state . nodePointers . set ( event . pointerId , pointerState ) ; if ( selectedNodes . has ( node ) ) { if ( state . follow . followedPointerId < 0 ) { state . follow . followedPointerId = event . pointerId ; _updateFollowNodes ( pointerState ) ; } else { delete state . follow . nodeBasePositions [ pointerState . nodeId ] ; } } emitter . emit ( "node:pointerdown" , { node , event } ) ; } function handleNodePointerOverEvent ( node , event ) { state . hoveredNodesPre . add ( node ) ; if ( state . nodePointers . size > 0 ) { return ; } state . hoveredNodes . add ( node ) ; emitter . emit ( "node:pointerover" , { node , event } ) ; } function handleNodePointerOutEvent ( node , event ) { state . hoveredNodesPre . delete ( node ) ; if ( state . nodePointers . size > 0 ) { return ; } state . hoveredNodes . delete ( node ) ; emitter . emit ( "node:pointerout" , { node , event } ) ; } function handleNodeContextMenu ( node , event ) { emitter . emit ( "node:contextmenu" , { node , event } ) ; } function handleEdgePointerDownEvent ( edge , event ) { if ( event . button == 2 ) { return ; } event . preventDefault ( ) ; event . stopPropagation ( ) ; if ( state . nodePointers . size !== 0 ) { return ; } if ( state . edgePointers . size == 0 ) { emitter . emit ( "view:mode" , "edge" ) ; entriesOf ( edgePointerHandlers ) . forEach ( ( [ ev , handler ] ) => { document . addEventListener ( ev , handler ) ; } ) ; state . edgePointerPeekCount = 0 ; } state . edgePointerPeekCount ++ ; const pointerState = { pointerId : event . pointerId , edgeId : edge , } ; state . edgePointers . set ( event . pointerId , pointerState ) ; emitter . emit ( "edge:pointerdown" , _makeEdgeEventObject ( edge , event ) ) ; } function handleEdgePointerUpEvent ( event ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; const pointerState = state . edgePointers . get ( event . pointerId ) ; if ( ! pointerState ) { return ; } state . edgePointers . delete ( event . pointerId ) ; const edge = pointerState . edgeId ; emitter . emit ( "edge:pointerup" , _makeEdgeEventObject ( edge , event ) ) ; if ( state . edgePointers . size > 0 || state . edgePointerPeekCount === 1 ) { handleEdgeClickEvent ( edge , event ) ; } if ( state . edgePointers . size === 0 ) { state . edgePointerPeekCount = 0 ; entriesOf ( edgePointerHandlers ) . forEach ( ( [ ev , handler ] ) => { document . removeEventListener ( ev , handler ) ; } ) ; emitter . emit ( "view:mode" , "default" ) ; } } function handleEdgePointerCancelEvent ( event ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; const pointerState = state . edgePointers . get ( event . pointerId ) ; if ( ! pointerState ) { return ; } for ( const pointerState of state . edgePointers . values ( ) ) { const edge = pointerState . edgeId ; emitter . emit ( "edge:pointerup" , _makeEdgeEventObject ( edge , event ) ) ; } state . edgePointers . clear ( ) ; state . edgePointerPeekCount = 0 ; entriesOf ( edgePointerHandlers ) . forEach ( ( [ ev , handler ] ) => { document . removeEventListener ( ev , handler ) ; } ) ; emitter . emit ( "view:mode" , "default" ) ; } function handleEdgeClickEvent ( edge , event ) { if ( event . shiftKey && selectedNodes . size > 0 ) { return ; } selectedNodes . clear ( ) ; const edges = edge instanceof Array ? edge : [ edge ] ; const isTouchAnySelectedEdge = MapUtil . valueOf ( state . edgePointers ) . filter ( p => { const edges = p . edgeId instanceof Array ? p . edgeId : [ p . edgeId ] ; return p . pointerId != event . pointerId && edges . every ( edge => selectedEdges . has ( edge ) ) ; } ) . length > 0 ; if ( edge instanceof Array ) { const selectableEdges = edges . find ( edge => edgeStates [ edge ] ?. selectable ) ; if ( selectableEdges ) { if ( event . shiftKey || isTouchAnySelectedEdge ) { if ( edges . some ( edge => selectedEdges . has ( edge ) ) ) { edges . forEach ( edge => selectedEdges . delete ( edge ) ) ; } else { edges . forEach ( edge => { const selectable = edgeStates [ edge ] ?. selectable ; if ( ! ( typeof selectable === "number" && selectedEdges . size >= selectable ) ) { selectedEdges . add ( edge ) ; } } ) ; } } else { selectedEdges . clear ( ) ; edges . forEach ( edge => selectedEdges . add ( edge ) ) ; } } } else { const selectable = edgeStates [ edge ] ?. selectable ; if ( selectable ) { if ( event . shiftKey || isTouchAnySelectedEdge ) { if ( selectedEdges . has ( edge ) ) { selectedEdges . delete ( edge ) ; } else if ( ! ( typeof selectable === "number" && selectedEdges . size >= selectable ) ) { selectedEdges . add ( edge ) ; } } else { selectedEdges . clear ( ) ; selectedEdges . add ( edge ) ; } } } emitter . emit ( "edge:click" , _makeEdgeEventObject ( edge , event ) ) ; } function handleEdgePointerOverEvent ( edge , event ) { state . hoveredEdges . add ( edge ) ; emitter . emit ( "edge:pointerover" , _makeEdgeEventObject ( edge , event ) ) ; } function handleEdgePointerOutEvent ( edge , event ) { state . hoveredEdges . delete ( edge ) ; emitter . emit ( "edge:pointerout" , _makeEdgeEventObject ( edge , event ) ) ; } function handleEdgeContextMenu ( edge , event ) { emitter . emit ( "edge:contextmenu" , _makeEdgeEventObject ( edge , event ) ) ; } function handleEdgesPointerDownEvent ( edges , event ) { if ( event . button == 2 ) { return ; } event . preventDefault ( ) ; event . stopPropagation ( ) ; if ( state . nodePointers . size !== 0 ) { return ; } if ( state . edgePointers . size == 0 ) { emitter . emit ( "view:mode" , "edge" ) ; entriesOf ( edgePointerHandlers ) . forEach ( ( [ ev , handler ] ) => { document . addEventListener ( ev , handler ) ; } ) ; state . edgePointerPeekCount = 0 ; } state . edgePointerPeekCount ++ ; const pointerState = { pointerId : event . pointerId , edgeId : edges , } ; state . edgePointers . set ( event . pointerId , pointerState ) ; emitter . emit ( "edge:pointerdown" , _makeEdgeEventObject ( edges , event ) ) ; } function handleEdgesPointerOverEvent ( edges , event ) { edges . forEach ( edge => state . hoveredEdges . add ( edge ) ) ; emitter . emit ( "edge:pointerover" , _makeEdgeEventObject ( edges , event ) ) ; } function handleEdgesPointerOutEvent ( edges , event ) { edges . forEach ( edge => state . hoveredEdges . delete ( edge ) ) ; emitter . emit ( "edge:pointerout" , _makeEdgeEventObject ( edges , event ) ) ; } function handleEdgesContextMenu ( edges , event ) { emitter . emit ( "edge:contextmenu" , _makeEdgeEventObject ( edges , event ) ) ; } const provides = < MouseEventHandlers > { selectedNodes , hoveredNodes , selectedEdges , hoveredEdges , handleNodePointerDownEvent , handleNodePointerOverEvent , handleNodePointerOutEvent , handleNodeContextMenu , handleEdgePointerDownEvent , handleEdgePointerOverEvent , handleEdgePointerOutEvent , handleEdgeContextMenu , handleEdgesPointerDownEvent , handleEdgesPointerOverEvent , handleEdgesPointerOutEvent , handleEdgesContextMenu , } ; provide ( mouseEventHandlersKey , provides ) ; return provides ; } export function useMouseOperation ( ) { return nonNull ( inject ( mouseEventHandlersKey ) , "mouseEventHandlers" ) ; }