useSelectElement.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import { useCallback } from 'react';
  2. import { uniq } from 'lodash';
  3. import { useCanvasStore } from '@/lib/store';
  4. import { useKeyboardStore } from '@/lib/store/keyboard';
  5. import type { PPTElement } from '@/lib/types/slides';
  6. /**
  7. * Hook for handling element selection in Canvas
  8. * Supports single selection, multi-selection (Ctrl/Shift), and group selection
  9. */
  10. export function useSelectElement(
  11. elementListRef: React.RefObject<PPTElement[]>,
  12. moveElement: (e: React.MouseEvent | React.TouchEvent, element: PPTElement) => void,
  13. ) {
  14. const activeElementIdList = useCanvasStore.use.activeElementIdList();
  15. const activeGroupElementId = useCanvasStore.use.activeGroupElementId();
  16. const handleElementId = useCanvasStore.use.handleElementId();
  17. const editorAreaFocus = useCanvasStore.use.editorAreaFocus();
  18. const setActiveElementIdList = useCanvasStore.use.setActiveElementIdList();
  19. const setHandleElementId = useCanvasStore.use.setHandleElementId();
  20. const setActiveGroupElementId = useCanvasStore.use.setActiveGroupElementId();
  21. const setEditorAreaFocus = useCanvasStore.use.setEditorAreaFocus();
  22. const ctrlOrShiftKeyActive = useKeyboardStore((state) => state.ctrlOrShiftKeyActive());
  23. // Select element
  24. // startMove indicates whether to enter move state after selection
  25. const selectElement = useCallback(
  26. (e: React.MouseEvent | React.TouchEvent, element: PPTElement, startMove = true) => {
  27. if (!editorAreaFocus) setEditorAreaFocus(true);
  28. // If the target element is not currently selected, set it as selected
  29. // If Ctrl or Shift is held, enter multi-select mode: add target to current selection; otherwise select only the target
  30. // If the target is a group member, also select the other members of that group
  31. if (!activeElementIdList.includes(element.id)) {
  32. let newActiveIdList: string[] = [];
  33. if (ctrlOrShiftKeyActive) {
  34. newActiveIdList = [...activeElementIdList, element.id];
  35. } else {
  36. newActiveIdList = [element.id];
  37. }
  38. if (element.groupId) {
  39. const groupMembersId: string[] = [];
  40. elementListRef.current.forEach((el: PPTElement) => {
  41. if (el.groupId === element.groupId) groupMembersId.push(el.id);
  42. });
  43. newActiveIdList = [...newActiveIdList, ...groupMembersId];
  44. }
  45. setActiveElementIdList(uniq(newActiveIdList));
  46. setHandleElementId(element.id);
  47. }
  48. // If the target element is already selected with Ctrl/Shift held, deselect it
  49. // Unless it's the last selected element, or the group it belongs to is the last selected group
  50. // If the target is a group member, also deselect other members of that group
  51. else if (ctrlOrShiftKeyActive) {
  52. let newActiveIdList: string[] = [];
  53. if (element.groupId) {
  54. const groupMembersId: string[] = [];
  55. elementListRef.current.forEach((el: PPTElement) => {
  56. if (el.groupId === element.groupId) groupMembersId.push(el.id);
  57. });
  58. newActiveIdList = activeElementIdList.filter((id) => !groupMembersId.includes(id));
  59. } else {
  60. newActiveIdList = activeElementIdList.filter((id) => id !== element.id);
  61. }
  62. if (newActiveIdList.length > 0) {
  63. setActiveElementIdList(newActiveIdList);
  64. }
  65. }
  66. // If the target is already selected but not the current handle element, make it the handle element
  67. else if (handleElementId !== element.id) {
  68. setHandleElementId(element.id);
  69. }
  70. // If the target is already the handle element, clicking again sets it as the active group element
  71. else if (activeGroupElementId !== element.id) {
  72. const startPageX =
  73. e.nativeEvent instanceof MouseEvent
  74. ? e.nativeEvent.pageX
  75. : 'changedTouches' in e
  76. ? e.changedTouches[0].pageX
  77. : 0;
  78. const startPageY =
  79. e.nativeEvent instanceof MouseEvent
  80. ? e.nativeEvent.pageY
  81. : 'changedTouches' in e
  82. ? e.changedTouches[0].pageY
  83. : 0;
  84. const target = e.target as HTMLElement;
  85. const handleMouseUp = (e: MouseEvent) => {
  86. const currentPageX = e.pageX;
  87. const currentPageY = e.pageY;
  88. if (startPageX === currentPageX && startPageY === currentPageY) {
  89. setActiveGroupElementId(element.id);
  90. target.onmouseup = null;
  91. }
  92. };
  93. target.onmouseup = handleMouseUp;
  94. }
  95. if (startMove) moveElement(e, element);
  96. },
  97. // eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally excludes elementListRef (stable ref) to avoid infinite re-creation
  98. [
  99. editorAreaFocus,
  100. activeElementIdList,
  101. ctrlOrShiftKeyActive,
  102. handleElementId,
  103. activeGroupElementId,
  104. setEditorAreaFocus,
  105. setActiveElementIdList,
  106. setHandleElementId,
  107. setActiveGroupElementId,
  108. moveElement,
  109. ],
  110. );
  111. return {
  112. selectElement,
  113. };
  114. }