1
0

useBoundsEvents.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import * as React from 'react'
  2. import { TLSelectionHandle, TLTargetType } from '@tldraw/core'
  3. import { DOUBLE_CLICK_DURATION } from '../constants'
  4. import type { TLReactCustomEvents } from '../types'
  5. import { useRendererContext } from './useRendererContext'
  6. export function useBoundsEvents(handle: TLSelectionHandle) {
  7. const { callbacks } = useRendererContext()
  8. const rDoubleClickTimer = React.useRef<number>(-1)
  9. const events = React.useMemo(() => {
  10. const onPointerMove: TLReactCustomEvents['pointer'] = e => {
  11. const { order = 0 } = e
  12. if (order) return
  13. callbacks.onPointerMove?.({ type: TLTargetType.Selection, handle, order }, e)
  14. e.order = order + 1
  15. }
  16. const onPointerDown: TLReactCustomEvents['pointer'] = e => {
  17. const { order = 0 } = e
  18. if (order) return
  19. // See note at bottom of the file
  20. const elm = loopToHtmlElement(e.currentTarget)
  21. elm.setPointerCapture(e.pointerId)
  22. elm.addEventListener('pointerup', onPointerUp)
  23. callbacks.onPointerDown?.({ type: TLTargetType.Selection, handle, order }, e)
  24. e.order = order + 1
  25. }
  26. const onPointerUp = (e: PointerEvent & { order?: number }) => {
  27. const { order = 0 } = e
  28. if (order) return
  29. const elm = e.target as HTMLElement
  30. elm.removeEventListener('pointerup', onPointerUp)
  31. elm.releasePointerCapture(e.pointerId)
  32. callbacks.onPointerUp?.({ type: TLTargetType.Selection, handle, order }, e)
  33. const now = Date.now()
  34. const elapsed = now - rDoubleClickTimer.current
  35. if (elapsed > DOUBLE_CLICK_DURATION) {
  36. rDoubleClickTimer.current = now
  37. } else {
  38. if (elapsed <= DOUBLE_CLICK_DURATION) {
  39. callbacks.onDoubleClick?.({ type: TLTargetType.Selection, handle, order }, e)
  40. rDoubleClickTimer.current = -1
  41. }
  42. }
  43. e.order = order + 1
  44. }
  45. const onPointerEnter: TLReactCustomEvents['pointer'] = e => {
  46. const { order = 0 } = e
  47. if (order) return
  48. callbacks.onPointerEnter?.({ type: TLTargetType.Selection, handle, order }, e)
  49. e.order = order + 1
  50. }
  51. const onPointerLeave: TLReactCustomEvents['pointer'] = e => {
  52. const { order = 0 } = e
  53. if (order) return
  54. callbacks.onPointerLeave?.({ type: TLTargetType.Selection, handle, order }, e)
  55. e.order = order + 1
  56. }
  57. const onKeyDown: TLReactCustomEvents['keyboard'] = e => {
  58. callbacks.onKeyDown?.({ type: TLTargetType.Selection, handle, order: -1 }, e)
  59. }
  60. const onKeyUp: TLReactCustomEvents['keyboard'] = e => {
  61. callbacks.onKeyUp?.({ type: TLTargetType.Selection, handle, order: -1 }, e)
  62. }
  63. return {
  64. onPointerDown,
  65. onPointerMove,
  66. // onPointerUp,
  67. onPointerEnter,
  68. onPointerLeave,
  69. onKeyDown,
  70. onKeyUp,
  71. }
  72. }, [callbacks])
  73. return events
  74. }
  75. function loopToHtmlElement(elm: Element): HTMLElement {
  76. if (elm.namespaceURI?.endsWith('svg')) {
  77. if (elm.parentElement) return loopToHtmlElement(elm.parentElement)
  78. else throw Error('Could not find a parent element of an HTML type!')
  79. }
  80. return elm as HTMLElement
  81. }
  82. /*
  83. There are a few hacks here in facilitate double clicking and pointer
  84. capture on elements.
  85. The events in this file are possibly set on individual SVG elements,
  86. such as handles or corner handles, rather than on HTML elements or
  87. SVGSVGElements. Raw SVG elements do not support pointerCapture in
  88. most cases, meaning that in order for pointer capture to work, we
  89. need to crawl up the DOM tree to find the nearest HTML element. Then,
  90. in order for that element to also call the `onPointerUp` event from
  91. this file, we need to manually set that event on that element and
  92. later remove it when the pointerup occurs. This is a potential leak
  93. if the user clicks on a handle but the pointerup does not fire for
  94. whatever reason.
  95. */