useCursorPosition.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { useState, useRef, useCallback, useEffect } from 'react';
  2. import { fromEvent, Subscription } from 'rxjs';
  3. import { throttleTime } from 'rxjs/operators';
  4. import { getAbsoluteCoordinate } from '../helpers/position';
  5. const defaultTime = 35;
  6. type CursorPosition = {
  7. x: number | null;
  8. y: number | null;
  9. clientX: number | null;
  10. clientY: number | null;
  11. screenX: number | null;
  12. screenY: number | null;
  13. };
  14. type UseCursorPositionType = (
  15. throttleTime?: number,
  16. ) => [
  17. CursorPosition,
  18. (element: HTMLElement | SVGSVGElement | SVGCircleElement | null) => void,
  19. ];
  20. const initialState = {
  21. x: null,
  22. y: null,
  23. clientX: null,
  24. clientY: null,
  25. screenX: null,
  26. screenY: null,
  27. };
  28. const useCursorPosition: UseCursorPositionType = (time = defaultTime) => {
  29. const [state, setState] = useState<CursorPosition>(initialState);
  30. const [element, setElement] = useState<
  31. HTMLElement | SVGSVGElement | SVGCircleElement | null
  32. >(null);
  33. const entered = useRef(false);
  34. const isDown = useRef(false);
  35. const onMouseMoveEvent = useCallback(
  36. (e: MouseEvent | Event): void => {
  37. if (element) {
  38. const { clientX, clientY, screenX, screenY } = e as MouseEvent;
  39. const coordinate = getAbsoluteCoordinate(element, e as MouseEvent);
  40. setState({
  41. x: coordinate.x,
  42. y: coordinate.y,
  43. clientX,
  44. clientY,
  45. screenX,
  46. screenY,
  47. });
  48. }
  49. },
  50. [element],
  51. );
  52. const onTouchMoveEvent = useCallback(
  53. (e: TouchEvent | Event): void => {
  54. if (element) {
  55. const {
  56. clientX,
  57. clientY,
  58. screenX,
  59. screenY,
  60. } = (e as TouchEvent).touches[0];
  61. const coordinate = getAbsoluteCoordinate(element, e as TouchEvent);
  62. setState({
  63. x: coordinate.x,
  64. y: coordinate.y,
  65. clientX,
  66. clientY,
  67. screenX,
  68. screenY,
  69. });
  70. }
  71. },
  72. [element],
  73. );
  74. useEffect(() => {
  75. let mouseSubscription: Subscription | null = null;
  76. let touchSubscription: Subscription | null = null;
  77. const onEnter = (): void => {
  78. entered.current = true;
  79. };
  80. const onLeave = (): void => {
  81. entered.current = false;
  82. setState(initialState);
  83. };
  84. const onDown = (e: Event): void => {
  85. entered.current = true;
  86. isDown.current = true;
  87. onMouseMoveEvent(e);
  88. };
  89. const onTouch = (e: Event): void => {
  90. entered.current = true;
  91. isDown.current = true;
  92. onTouchMoveEvent(e);
  93. };
  94. const onUp = (): void => {
  95. entered.current = false;
  96. isDown.current = false;
  97. setState(initialState);
  98. if (mouseSubscription) mouseSubscription.unsubscribe();
  99. if (touchSubscription) touchSubscription.unsubscribe();
  100. };
  101. if (element) {
  102. const addEvent = element.addEventListener.bind(element);
  103. touchSubscription = fromEvent(element, 'touchmove')
  104. .pipe(throttleTime(time))
  105. .subscribe(onTouchMoveEvent);
  106. addEvent('touchstart', onTouch);
  107. addEvent('touchend', onUp);
  108. mouseSubscription = fromEvent(element, 'mousemove')
  109. .pipe(throttleTime(time))
  110. .subscribe(onMouseMoveEvent);
  111. addEvent('mouseenter', onEnter);
  112. addEvent('mouseleave', onLeave);
  113. addEvent('mousedown', onDown);
  114. addEvent('mouseup', onUp);
  115. }
  116. return (): void => {
  117. if (element) {
  118. const removeEvent = element.removeEventListener.bind(element);
  119. removeEvent('touchstart', onTouch);
  120. removeEvent('touchend', onUp);
  121. removeEvent('mouseenter', onEnter);
  122. removeEvent('mouseleave', onLeave);
  123. removeEvent('mousedown', onDown);
  124. removeEvent('mouseup', onUp);
  125. }
  126. };
  127. }, [element]);
  128. return [state, setElement];
  129. };
  130. export default useCursorPosition;