useMouseSelection.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import { useState, useCallback, type RefObject } from 'react';
  2. import { useKeyboardStore } from '@/lib/store/keyboard';
  3. import { useCanvasStore } from '@/lib/store';
  4. import type { PPTElement } from '@/lib/types/slides';
  5. import { getElementRange } from '@/lib/utils/element';
  6. export function useMouseSelection(
  7. elementListRef: React.RefObject<PPTElement[]>,
  8. viewportRef: RefObject<HTMLElement | null>,
  9. ) {
  10. const [mouseSelectionVisible, setMouseSelectionVisible] = useState(false);
  11. const [mouseSelectionQuadrant, setMouseSelectionQuadrant] = useState(1);
  12. const [mouseSelection, setMouseSelection] = useState({
  13. top: 0,
  14. left: 0,
  15. width: 0,
  16. height: 0,
  17. });
  18. const canvasScale = useCanvasStore.use.canvasScale();
  19. const hiddenElementIdList = useCanvasStore.use.hiddenElementIdList();
  20. const setActiveElementIdList = useCanvasStore.use.setActiveElementIdList();
  21. const ctrlOrShiftKeyActive = useKeyboardStore((state) => state.ctrlOrShiftKeyActive());
  22. // Update mouse selection range
  23. const updateMouseSelection = useCallback(
  24. (e: React.MouseEvent) => {
  25. if (!viewportRef.current) return;
  26. let isMouseDown = true;
  27. const viewportRect = viewportRef.current.getBoundingClientRect();
  28. const minSelectionRange = 5;
  29. const startPageX = e.pageX;
  30. const startPageY = e.pageY;
  31. const left = (startPageX - viewportRect.x) / canvasScale;
  32. const top = (startPageY - viewportRect.y) / canvasScale;
  33. // Initialize selection start position and defaults
  34. setMouseSelection({
  35. top: top,
  36. left: left,
  37. width: 0,
  38. height: 0,
  39. });
  40. setMouseSelectionVisible(false);
  41. setMouseSelectionQuadrant(4);
  42. const handleMouseMove = (e: MouseEvent) => {
  43. if (!isMouseDown) return;
  44. const currentPageX = e.pageX;
  45. const currentPageY = e.pageY;
  46. const offsetWidth = (currentPageX - startPageX) / canvasScale;
  47. const offsetHeight = (currentPageY - startPageY) / canvasScale;
  48. const width = Math.abs(offsetWidth);
  49. const height = Math.abs(offsetHeight);
  50. if (width < minSelectionRange || height < minSelectionRange) return;
  51. // Determine mouse selection (movement) direction
  52. // Classified by quadrant position, e.g. bottom-right is quadrant 4
  53. let quadrant = 0;
  54. if (offsetWidth > 0 && offsetHeight > 0) quadrant = 4;
  55. else if (offsetWidth < 0 && offsetHeight < 0) quadrant = 2;
  56. else if (offsetWidth > 0 && offsetHeight < 0) quadrant = 1;
  57. else if (offsetWidth < 0 && offsetHeight > 0) quadrant = 3;
  58. // Update selection range
  59. setMouseSelection((prev) => ({
  60. ...prev,
  61. width: width,
  62. height: height,
  63. }));
  64. setMouseSelectionVisible(true);
  65. setMouseSelectionQuadrant(quadrant);
  66. };
  67. const handleMouseUp = () => {
  68. document.onmousemove = null;
  69. document.onmouseup = null;
  70. isMouseDown = false;
  71. // Check which canvas elements are within the mouse selection range and set them as selected
  72. let inRangeElementList: PPTElement[] = [];
  73. for (const element of elementListRef.current) {
  74. const mouseSelectionLeft = mouseSelection.left;
  75. const mouseSelectionTop = mouseSelection.top;
  76. const mouseSelectionWidth = mouseSelection.width;
  77. const mouseSelectionHeight = mouseSelection.height;
  78. const { minX, maxX, minY, maxY } = getElementRange(element);
  79. // Inclusion check differs for each quadrant direction
  80. let isInclude = false;
  81. if (ctrlOrShiftKeyActive) {
  82. if (mouseSelectionQuadrant === 4) {
  83. isInclude =
  84. maxX > mouseSelectionLeft &&
  85. minX < mouseSelectionLeft + mouseSelectionWidth &&
  86. maxY > mouseSelectionTop &&
  87. minY < mouseSelectionTop + mouseSelectionHeight;
  88. } else if (mouseSelectionQuadrant === 2) {
  89. isInclude =
  90. maxX > mouseSelectionLeft - mouseSelectionWidth &&
  91. minX < mouseSelectionLeft - mouseSelectionWidth + mouseSelectionWidth &&
  92. maxY > mouseSelectionTop - mouseSelectionHeight &&
  93. minY < mouseSelectionTop - mouseSelectionHeight + mouseSelectionHeight;
  94. } else if (mouseSelectionQuadrant === 1) {
  95. isInclude =
  96. maxX > mouseSelectionLeft &&
  97. minX < mouseSelectionLeft + mouseSelectionWidth &&
  98. maxY > mouseSelectionTop - mouseSelectionHeight &&
  99. minY < mouseSelectionTop - mouseSelectionHeight + mouseSelectionHeight;
  100. } else if (mouseSelectionQuadrant === 3) {
  101. isInclude =
  102. maxX > mouseSelectionLeft - mouseSelectionWidth &&
  103. minX < mouseSelectionLeft - mouseSelectionWidth + mouseSelectionWidth &&
  104. maxY > mouseSelectionTop &&
  105. minY < mouseSelectionTop + mouseSelectionHeight;
  106. }
  107. } else {
  108. if (mouseSelectionQuadrant === 4) {
  109. isInclude =
  110. minX > mouseSelectionLeft &&
  111. maxX < mouseSelectionLeft + mouseSelectionWidth &&
  112. minY > mouseSelectionTop &&
  113. maxY < mouseSelectionTop + mouseSelectionHeight;
  114. } else if (mouseSelectionQuadrant === 2) {
  115. isInclude =
  116. minX > mouseSelectionLeft - mouseSelectionWidth &&
  117. maxX < mouseSelectionLeft - mouseSelectionWidth + mouseSelectionWidth &&
  118. minY > mouseSelectionTop - mouseSelectionHeight &&
  119. maxY < mouseSelectionTop - mouseSelectionHeight + mouseSelectionHeight;
  120. } else if (mouseSelectionQuadrant === 1) {
  121. isInclude =
  122. minX > mouseSelectionLeft &&
  123. maxX < mouseSelectionLeft + mouseSelectionWidth &&
  124. minY > mouseSelectionTop - mouseSelectionHeight &&
  125. maxY < mouseSelectionTop - mouseSelectionHeight + mouseSelectionHeight;
  126. } else if (mouseSelectionQuadrant === 3) {
  127. isInclude =
  128. minX > mouseSelectionLeft - mouseSelectionWidth &&
  129. maxX < mouseSelectionLeft - mouseSelectionWidth + mouseSelectionWidth &&
  130. minY > mouseSelectionTop &&
  131. maxY < mouseSelectionTop + mouseSelectionHeight;
  132. }
  133. }
  134. // Locked or hidden elements should not be selected even if within range
  135. if (isInclude && !element.lock && !hiddenElementIdList.includes(element.id))
  136. inRangeElementList.push(element);
  137. }
  138. // If grouped elements are in range, all members of the group must be in range to be selected
  139. inRangeElementList = inRangeElementList.filter((inRangeElement) => {
  140. if (inRangeElement.groupId) {
  141. const inRangeElementIdList = inRangeElementList.map(
  142. (inRangeElement) => inRangeElement.id,
  143. );
  144. const groupElementList = elementListRef.current.filter(
  145. (element) => element.groupId === inRangeElement.groupId,
  146. );
  147. return groupElementList.every((groupElement) =>
  148. inRangeElementIdList.includes(groupElement.id),
  149. );
  150. }
  151. return true;
  152. });
  153. const inRangeElementIdList = inRangeElementList.map((inRangeElement) => inRangeElement.id);
  154. setActiveElementIdList(inRangeElementIdList);
  155. setMouseSelectionVisible(false);
  156. };
  157. document.onmousemove = handleMouseMove;
  158. document.onmouseup = handleMouseUp;
  159. },
  160. // eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally excludes mouseSelection state to avoid infinite re-creation
  161. [
  162. viewportRef,
  163. canvasScale,
  164. ctrlOrShiftKeyActive,
  165. hiddenElementIdList,
  166. elementListRef,
  167. setActiveElementIdList,
  168. ],
  169. );
  170. return {
  171. mouseSelection,
  172. mouseSelectionVisible,
  173. mouseSelectionQuadrant,
  174. updateMouseSelection,
  175. };
  176. }