useCursorPosition.ts 4.0 KB

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