# All files / utils useDragEvent.ts

18.51% Statements 10/54
5.55% Branches 1/18
20% Functions 1/5
22.72% Lines 10/44

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    1x
    1x

    1x

    1x
    4x
    4x

    4x

    4x

    4x

    4x

     import * as React from 'react'
    import {useRef, useCallback} from 'react'
    import {useForceUpdate} from './useForceUpdate'

    // Minimum number of pixels a user must drag before we decide whether the
    // action is a vertical or horizontal drag
    const MIN_DRAG_DELTA = 3

    /**
     * Because dragging and mouse clicks are non trivial, this implementation of DragEvent consumes the mouseDown event;
     * which means that 'onClick' doesn't work for anything that uses these events.
     *
     * We can compensate for that by having a listener for 'onMouseUpEnd' for the consumer of this event (see Brush)
     *
     * But in order to differentiate between a drag and a click, we need to pair up the mouseDowns with the MouseUps and
     * only allow 'legal' mouseUps with the mouseActionState.
     *
     * A 'mouseUp' is only legal if it can be paired with a previous "mouseDown".
     *
     * If a 'mouseUp' happens after another 'mouseUp' (without a "mouseDown" in between)
     * then it is not legal and the 'mouseActionState' will be null.
     *
     * The consumer (Brush, in this example) keeps track of whether or not the drag happens.
     *
     * We need differentiation because:
     *
     * The issue is that when the drag is over, both the onBrushEnd (x or y) and then the
     * onMouseUpEnd handlers get triggered; because of multiple mouseUp events.
     *
     * We want one or the other, not both. (a response to a range drag or to a single click)
     *
     * Each time there is a drag, there is ONE mouseDownEvent, then mousemove events, then one MouseUp Event that
     * triggers the onBrushEnd.
     *
     * Most of the time, there are additional onMouseUpEvents after that. (we will call them phantom mouseUps)
     *
     * We only want the first onMouseUp event to be legal.
     * So we check that the previous 'mouseActionState' was a mouseDownEvent ('mouseDownHappened'), and if so set the mouse state to a 'mouseUpHappened'.
     *
     * When the next mouseUp Event happens (without a proper, previous matching mouseDown Event),
     * then mouseActionState will be set to null.
     *
     * When the singleclick happens, it will check if the mouseActionState says 'mouseDownHappened'; if so; then it is a legal
     * mouseUp; and the mouseActionState will be set to 'mouseUpHappened'.  If the mouse state is *anything* other then ''mouseDownHappened', then
     * the mouseActionState is set to null.
     *
     * The consumer checks the mouseActionState, and only fires the event if the mouseActionState is 'mouseUpHappened'
     *
     * And thus we have some history and can pair mouse ups with mouse downs and assure that phantom clicks
     * do not cause wonky behavior.
     *
     * Removing the phantom clicks entirely is a MUCH harder problem; because this is a clicker used by people with events.
     *
     * We are putting the mouseEvent into the event so that the consumer for the 'onMouseUpEnd' can use it, since that consumer
     * is a mouseListener and needs the mouseEvent information.
     *
     * isShiftDown is added for mouse down events
     */

    export interface DragEvent {
      type: 'drag' | 'dragend'
      initialX: number
      initialY: number
      direction: 'x' | 'y' | null
      x: number
      y: number
      mouseActionState: 'mouseUpHappened' | 'mouseDownHappened' | null
      mouseEvent?: React.MouseEvent
      isShiftDown?: boolean
    }

    interface UseDragEventProps {
      onMouseDown: (e: React.MouseEvent<Element, MouseEvent>) => any
    }

    export const useDragEvent = (): [DragEvent | null, UseDragEventProps] => {
      const dragEventRef = useRef<DragEvent | null>(null)
      const forceUpdate = useForceUpdate()

      const onMouseDown = useCallback(
        (mouseDownEvent: React.MouseEvent<Element, MouseEvent>) => {
          mouseDownEvent.stopPropagation()

          const el = mouseDownEvent.currentTarget

          const getXYCoords = baseEvent => {
            const {left, top} = el.getBoundingClientRect()

            return [baseEvent.pageX - left, baseEvent.pageY - top]
          }

          const onMouseMove = mouseMoveEvent => {
            const [x, y] = getXYCoords(mouseMoveEvent)

            const {initialX, initialY} = dragEventRef.current
            let {direction} = dragEventRef.current

            Iif (!direction) {
              const dx = Math.abs(x - initialX)
              const dy = Math.abs(y - initialY)

              if (dx >= dy && dx > MIN_DRAG_DELTA) {
                direction = 'x'
              } else Iif (dy > MIN_DRAG_DELTA) {
                direction = 'y'
              }
            }

            dragEventRef.current = {
              ...dragEventRef.current,
              type: 'drag',
              direction,
              x,
              y,
            }

            forceUpdate()
          }

          const onMouseUp = mouseUpEvent => {
            document.removeEventListener('mousemove', onMouseMove)
            document.removeEventListener('mouseup', onMouseUp)

            const [x, y] = getXYCoords(mouseUpEvent)

            let mouseActionState = null

            Iif (dragEventRef?.current?.mouseActionState === 'mouseDownHappened') {
              mouseActionState = 'mouseUpHappened'
            }

            dragEventRef.current = {
              ...dragEventRef.current,
              type: 'dragend',
              mouseActionState,
              x,
              y,
              mouseEvent: mouseUpEvent,
            }

            forceUpdate()
          }

          document.addEventListener('mousemove', onMouseMove)
          document.addEventListener('mouseup', onMouseUp)

          const [x, y] = getXYCoords(mouseDownEvent)
          const isShiftDown = mouseDownEvent.getModifierState('Shift')

          // TODO:  even though the 'isShiftDown' gets reset with each mousedown,
          // incase other mouse events/triggers/callbacks want to use the shift key, make sure to set
          // it to false in all the other places where events are emitted to reset it properly!
          dragEventRef.current = {
            type: 'drag',
            initialX: x,
            initialY: y,
            x,
            y,
            direction: null,
            mouseActionState: 'mouseDownHappened',
            isShiftDown,
          }

          forceUpdate()
        },
        []
      )

      const {current: dragEvent} = dragEventRef

      Iif (dragEvent && dragEvent.type === 'dragend') {
        // 'dragEnd' events should be emitted exactly once at the end of a drag
        dragEventRef.current = null
      }

      return [dragEvent, {onMouseDown}]
    }