Spaces:
No application file
No application file
| import { useCallback } from 'react'; | |
| import { uniq } from 'lodash'; | |
| import { useCanvasStore } from '@/lib/store'; | |
| import { useKeyboardStore } from '@/lib/store/keyboard'; | |
| import type { PPTElement } from '@/lib/types/slides'; | |
| /** | |
| * Hook for handling element selection in Canvas | |
| * Supports single selection, multi-selection (Ctrl/Shift), and group selection | |
| */ | |
| export function useSelectElement( | |
| elementListRef: React.RefObject<PPTElement[]>, | |
| moveElement: (e: React.MouseEvent | React.TouchEvent, element: PPTElement) => void, | |
| ) { | |
| const activeElementIdList = useCanvasStore.use.activeElementIdList(); | |
| const activeGroupElementId = useCanvasStore.use.activeGroupElementId(); | |
| const handleElementId = useCanvasStore.use.handleElementId(); | |
| const editorAreaFocus = useCanvasStore.use.editorAreaFocus(); | |
| const setActiveElementIdList = useCanvasStore.use.setActiveElementIdList(); | |
| const setHandleElementId = useCanvasStore.use.setHandleElementId(); | |
| const setActiveGroupElementId = useCanvasStore.use.setActiveGroupElementId(); | |
| const setEditorAreaFocus = useCanvasStore.use.setEditorAreaFocus(); | |
| const ctrlOrShiftKeyActive = useKeyboardStore((state) => state.ctrlOrShiftKeyActive()); | |
| // Select element | |
| // startMove indicates whether to enter move state after selection | |
| const selectElement = useCallback( | |
| (e: React.MouseEvent | React.TouchEvent, element: PPTElement, startMove = true) => { | |
| if (!editorAreaFocus) setEditorAreaFocus(true); | |
| // If the target element is not currently selected, set it as selected | |
| // If Ctrl or Shift is held, enter multi-select mode: add target to current selection; otherwise select only the target | |
| // If the target is a group member, also select the other members of that group | |
| if (!activeElementIdList.includes(element.id)) { | |
| let newActiveIdList: string[] = []; | |
| if (ctrlOrShiftKeyActive) { | |
| newActiveIdList = [...activeElementIdList, element.id]; | |
| } else { | |
| newActiveIdList = [element.id]; | |
| } | |
| if (element.groupId) { | |
| const groupMembersId: string[] = []; | |
| elementListRef.current.forEach((el: PPTElement) => { | |
| if (el.groupId === element.groupId) groupMembersId.push(el.id); | |
| }); | |
| newActiveIdList = [...newActiveIdList, ...groupMembersId]; | |
| } | |
| setActiveElementIdList(uniq(newActiveIdList)); | |
| setHandleElementId(element.id); | |
| } | |
| // If the target element is already selected with Ctrl/Shift held, deselect it | |
| // Unless it's the last selected element, or the group it belongs to is the last selected group | |
| // If the target is a group member, also deselect other members of that group | |
| else if (ctrlOrShiftKeyActive) { | |
| let newActiveIdList: string[] = []; | |
| if (element.groupId) { | |
| const groupMembersId: string[] = []; | |
| elementListRef.current.forEach((el: PPTElement) => { | |
| if (el.groupId === element.groupId) groupMembersId.push(el.id); | |
| }); | |
| newActiveIdList = activeElementIdList.filter((id) => !groupMembersId.includes(id)); | |
| } else { | |
| newActiveIdList = activeElementIdList.filter((id) => id !== element.id); | |
| } | |
| if (newActiveIdList.length > 0) { | |
| setActiveElementIdList(newActiveIdList); | |
| } | |
| } | |
| // If the target is already selected but not the current handle element, make it the handle element | |
| else if (handleElementId !== element.id) { | |
| setHandleElementId(element.id); | |
| } | |
| // If the target is already the handle element, clicking again sets it as the active group element | |
| else if (activeGroupElementId !== element.id) { | |
| const startPageX = | |
| e.nativeEvent instanceof MouseEvent | |
| ? e.nativeEvent.pageX | |
| : 'changedTouches' in e | |
| ? e.changedTouches[0].pageX | |
| : 0; | |
| const startPageY = | |
| e.nativeEvent instanceof MouseEvent | |
| ? e.nativeEvent.pageY | |
| : 'changedTouches' in e | |
| ? e.changedTouches[0].pageY | |
| : 0; | |
| const target = e.target as HTMLElement; | |
| const handleMouseUp = (e: MouseEvent) => { | |
| const currentPageX = e.pageX; | |
| const currentPageY = e.pageY; | |
| if (startPageX === currentPageX && startPageY === currentPageY) { | |
| setActiveGroupElementId(element.id); | |
| target.onmouseup = null; | |
| } | |
| }; | |
| target.onmouseup = handleMouseUp; | |
| } | |
| if (startMove) moveElement(e, element); | |
| }, | |
| // eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally excludes elementListRef (stable ref) to avoid infinite re-creation | |
| [ | |
| editorAreaFocus, | |
| activeElementIdList, | |
| ctrlOrShiftKeyActive, | |
| handleElementId, | |
| activeGroupElementId, | |
| setEditorAreaFocus, | |
| setActiveElementIdList, | |
| setHandleElementId, | |
| setActiveGroupElementId, | |
| moveElement, | |
| ], | |
| ); | |
| return { | |
| selectElement, | |
| }; | |
| } | |