| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- import { useState, useCallback, type RefObject } from 'react';
- import { useKeyboardStore } from '@/lib/store/keyboard';
- import { useCanvasStore } from '@/lib/store';
- import type { PPTElement } from '@/lib/types/slides';
- import { getElementRange } from '@/lib/utils/element';
- export function useMouseSelection(
- elementListRef: React.RefObject<PPTElement[]>,
- viewportRef: RefObject<HTMLElement | null>,
- ) {
- const [mouseSelectionVisible, setMouseSelectionVisible] = useState(false);
- const [mouseSelectionQuadrant, setMouseSelectionQuadrant] = useState(1);
- const [mouseSelection, setMouseSelection] = useState({
- top: 0,
- left: 0,
- width: 0,
- height: 0,
- });
- const canvasScale = useCanvasStore.use.canvasScale();
- const hiddenElementIdList = useCanvasStore.use.hiddenElementIdList();
- const setActiveElementIdList = useCanvasStore.use.setActiveElementIdList();
- const ctrlOrShiftKeyActive = useKeyboardStore((state) => state.ctrlOrShiftKeyActive());
- // Update mouse selection range
- const updateMouseSelection = useCallback(
- (e: React.MouseEvent) => {
- if (!viewportRef.current) return;
- let isMouseDown = true;
- const viewportRect = viewportRef.current.getBoundingClientRect();
- const minSelectionRange = 5;
- const startPageX = e.pageX;
- const startPageY = e.pageY;
- const left = (startPageX - viewportRect.x) / canvasScale;
- const top = (startPageY - viewportRect.y) / canvasScale;
- // Initialize selection start position and defaults
- setMouseSelection({
- top: top,
- left: left,
- width: 0,
- height: 0,
- });
- setMouseSelectionVisible(false);
- setMouseSelectionQuadrant(4);
- const handleMouseMove = (e: MouseEvent) => {
- if (!isMouseDown) return;
- const currentPageX = e.pageX;
- const currentPageY = e.pageY;
- const offsetWidth = (currentPageX - startPageX) / canvasScale;
- const offsetHeight = (currentPageY - startPageY) / canvasScale;
- const width = Math.abs(offsetWidth);
- const height = Math.abs(offsetHeight);
- if (width < minSelectionRange || height < minSelectionRange) return;
- // Determine mouse selection (movement) direction
- // Classified by quadrant position, e.g. bottom-right is quadrant 4
- let quadrant = 0;
- if (offsetWidth > 0 && offsetHeight > 0) quadrant = 4;
- else if (offsetWidth < 0 && offsetHeight < 0) quadrant = 2;
- else if (offsetWidth > 0 && offsetHeight < 0) quadrant = 1;
- else if (offsetWidth < 0 && offsetHeight > 0) quadrant = 3;
- // Update selection range
- setMouseSelection((prev) => ({
- ...prev,
- width: width,
- height: height,
- }));
- setMouseSelectionVisible(true);
- setMouseSelectionQuadrant(quadrant);
- };
- const handleMouseUp = () => {
- document.onmousemove = null;
- document.onmouseup = null;
- isMouseDown = false;
- // Check which canvas elements are within the mouse selection range and set them as selected
- let inRangeElementList: PPTElement[] = [];
- for (const element of elementListRef.current) {
- const mouseSelectionLeft = mouseSelection.left;
- const mouseSelectionTop = mouseSelection.top;
- const mouseSelectionWidth = mouseSelection.width;
- const mouseSelectionHeight = mouseSelection.height;
- const { minX, maxX, minY, maxY } = getElementRange(element);
- // Inclusion check differs for each quadrant direction
- let isInclude = false;
- if (ctrlOrShiftKeyActive) {
- if (mouseSelectionQuadrant === 4) {
- isInclude =
- maxX > mouseSelectionLeft &&
- minX < mouseSelectionLeft + mouseSelectionWidth &&
- maxY > mouseSelectionTop &&
- minY < mouseSelectionTop + mouseSelectionHeight;
- } else if (mouseSelectionQuadrant === 2) {
- isInclude =
- maxX > mouseSelectionLeft - mouseSelectionWidth &&
- minX < mouseSelectionLeft - mouseSelectionWidth + mouseSelectionWidth &&
- maxY > mouseSelectionTop - mouseSelectionHeight &&
- minY < mouseSelectionTop - mouseSelectionHeight + mouseSelectionHeight;
- } else if (mouseSelectionQuadrant === 1) {
- isInclude =
- maxX > mouseSelectionLeft &&
- minX < mouseSelectionLeft + mouseSelectionWidth &&
- maxY > mouseSelectionTop - mouseSelectionHeight &&
- minY < mouseSelectionTop - mouseSelectionHeight + mouseSelectionHeight;
- } else if (mouseSelectionQuadrant === 3) {
- isInclude =
- maxX > mouseSelectionLeft - mouseSelectionWidth &&
- minX < mouseSelectionLeft - mouseSelectionWidth + mouseSelectionWidth &&
- maxY > mouseSelectionTop &&
- minY < mouseSelectionTop + mouseSelectionHeight;
- }
- } else {
- if (mouseSelectionQuadrant === 4) {
- isInclude =
- minX > mouseSelectionLeft &&
- maxX < mouseSelectionLeft + mouseSelectionWidth &&
- minY > mouseSelectionTop &&
- maxY < mouseSelectionTop + mouseSelectionHeight;
- } else if (mouseSelectionQuadrant === 2) {
- isInclude =
- minX > mouseSelectionLeft - mouseSelectionWidth &&
- maxX < mouseSelectionLeft - mouseSelectionWidth + mouseSelectionWidth &&
- minY > mouseSelectionTop - mouseSelectionHeight &&
- maxY < mouseSelectionTop - mouseSelectionHeight + mouseSelectionHeight;
- } else if (mouseSelectionQuadrant === 1) {
- isInclude =
- minX > mouseSelectionLeft &&
- maxX < mouseSelectionLeft + mouseSelectionWidth &&
- minY > mouseSelectionTop - mouseSelectionHeight &&
- maxY < mouseSelectionTop - mouseSelectionHeight + mouseSelectionHeight;
- } else if (mouseSelectionQuadrant === 3) {
- isInclude =
- minX > mouseSelectionLeft - mouseSelectionWidth &&
- maxX < mouseSelectionLeft - mouseSelectionWidth + mouseSelectionWidth &&
- minY > mouseSelectionTop &&
- maxY < mouseSelectionTop + mouseSelectionHeight;
- }
- }
- // Locked or hidden elements should not be selected even if within range
- if (isInclude && !element.lock && !hiddenElementIdList.includes(element.id))
- inRangeElementList.push(element);
- }
- // If grouped elements are in range, all members of the group must be in range to be selected
- inRangeElementList = inRangeElementList.filter((inRangeElement) => {
- if (inRangeElement.groupId) {
- const inRangeElementIdList = inRangeElementList.map(
- (inRangeElement) => inRangeElement.id,
- );
- const groupElementList = elementListRef.current.filter(
- (element) => element.groupId === inRangeElement.groupId,
- );
- return groupElementList.every((groupElement) =>
- inRangeElementIdList.includes(groupElement.id),
- );
- }
- return true;
- });
- const inRangeElementIdList = inRangeElementList.map((inRangeElement) => inRangeElement.id);
- setActiveElementIdList(inRangeElementIdList);
- setMouseSelectionVisible(false);
- };
- document.onmousemove = handleMouseMove;
- document.onmouseup = handleMouseUp;
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally excludes mouseSelection state to avoid infinite re-creation
- [
- viewportRef,
- canvasScale,
- ctrlOrShiftKeyActive,
- hiddenElementIdList,
- elementListRef,
- setActiveElementIdList,
- ],
- );
- return {
- mouseSelection,
- mouseSelectionVisible,
- mouseSelectionQuadrant,
- updateMouseSelection,
- };
- }
|