feat: selection box
This commit is contained in:
58
apps/web/src/components/editor/selection-box.tsx
Normal file
58
apps/web/src/components/editor/selection-box.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
interface SelectionBoxProps {
|
||||||
|
startPos: { x: number; y: number } | null;
|
||||||
|
currentPos: { x: number; y: number } | null;
|
||||||
|
containerRef: React.RefObject<HTMLElement>;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectionBox({
|
||||||
|
startPos,
|
||||||
|
currentPos,
|
||||||
|
containerRef,
|
||||||
|
isActive,
|
||||||
|
}: SelectionBoxProps) {
|
||||||
|
const selectionBoxRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive || !startPos || !currentPos || !containerRef.current) return;
|
||||||
|
|
||||||
|
const container = containerRef.current;
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Calculate relative positions within the container
|
||||||
|
const startX = startPos.x - containerRect.left;
|
||||||
|
const startY = startPos.y - containerRect.top;
|
||||||
|
const currentX = currentPos.x - containerRect.left;
|
||||||
|
const currentY = currentPos.y - containerRect.top;
|
||||||
|
|
||||||
|
// Calculate the selection rectangle bounds
|
||||||
|
const left = Math.min(startX, currentX);
|
||||||
|
const top = Math.min(startY, currentY);
|
||||||
|
const width = Math.abs(currentX - startX);
|
||||||
|
const height = Math.abs(currentY - startY);
|
||||||
|
|
||||||
|
// Update the selection box position and size
|
||||||
|
if (selectionBoxRef.current) {
|
||||||
|
selectionBoxRef.current.style.left = `${left}px`;
|
||||||
|
selectionBoxRef.current.style.top = `${top}px`;
|
||||||
|
selectionBoxRef.current.style.width = `${width}px`;
|
||||||
|
selectionBoxRef.current.style.height = `${height}px`;
|
||||||
|
}
|
||||||
|
}, [startPos, currentPos, isActive, containerRef]);
|
||||||
|
|
||||||
|
if (!isActive || !startPos || !currentPos) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={selectionBoxRef}
|
||||||
|
className="absolute pointer-events-none z-50"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "hsl(var(--foreground) / 0.1)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -339,6 +339,8 @@ export function TimelineElement({
|
|||||||
left: `${elementLeft}px`,
|
left: `${elementLeft}px`,
|
||||||
width: `${elementWidth}px`,
|
width: `${elementWidth}px`,
|
||||||
}}
|
}}
|
||||||
|
data-element-id={element.id}
|
||||||
|
data-track-id={track.id}
|
||||||
onMouseMove={resizing ? handleResizeMove : undefined}
|
onMouseMove={resizing ? handleResizeMove : undefined}
|
||||||
onMouseUp={resizing ? handleResizeEnd : undefined}
|
onMouseUp={resizing ? handleResizeEnd : undefined}
|
||||||
onMouseLeave={resizing ? handleResizeEnd : undefined}
|
onMouseLeave={resizing ? handleResizeEnd : undefined}
|
||||||
|
@ -48,6 +48,8 @@ import {
|
|||||||
TimelinePlayhead,
|
TimelinePlayhead,
|
||||||
useTimelinePlayheadRuler,
|
useTimelinePlayheadRuler,
|
||||||
} from "./timeline-playhead";
|
} from "./timeline-playhead";
|
||||||
|
import { SelectionBox } from "./selection-box";
|
||||||
|
import { useSelectionBox } from "@/hooks/use-selection-box";
|
||||||
import type { DragData, TimelineTrack } from "@/types/timeline";
|
import type { DragData, TimelineTrack } from "@/types/timeline";
|
||||||
import {
|
import {
|
||||||
getTrackHeight,
|
getTrackHeight,
|
||||||
@ -103,15 +105,7 @@ export function Timeline() {
|
|||||||
isInTimeline,
|
isInTimeline,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Marquee selection state
|
// Old marquee selection removed - using new SelectionBox component instead
|
||||||
const [marquee, setMarquee] = useState<{
|
|
||||||
startX: number;
|
|
||||||
startY: number;
|
|
||||||
endX: number;
|
|
||||||
endY: number;
|
|
||||||
active: boolean;
|
|
||||||
additive: boolean;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
// Dynamic timeline width calculation based on playhead position and duration
|
// Dynamic timeline width calculation based on playhead position and duration
|
||||||
const dynamicTimelineWidth = Math.max(
|
const dynamicTimelineWidth = Math.max(
|
||||||
@ -141,9 +135,40 @@ export function Timeline() {
|
|||||||
playheadRef,
|
playheadRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Selection box functionality
|
||||||
|
const tracksContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const {
|
||||||
|
selectionBox,
|
||||||
|
handleMouseDown: handleSelectionMouseDown,
|
||||||
|
isSelecting,
|
||||||
|
justFinishedSelecting,
|
||||||
|
} = useSelectionBox({
|
||||||
|
containerRef: tracksContainerRef,
|
||||||
|
playheadRef,
|
||||||
|
onSelectionComplete: (elements) => {
|
||||||
|
console.log(JSON.stringify({ onSelectionComplete: elements.length }));
|
||||||
|
setSelectedElements(elements);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Timeline content click to seek handler
|
// Timeline content click to seek handler
|
||||||
const handleTimelineContentClick = useCallback(
|
const handleTimelineContentClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
timelineClick: {
|
||||||
|
isSelecting,
|
||||||
|
justFinishedSelecting,
|
||||||
|
willReturn: isSelecting || justFinishedSelecting,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't seek if this was a selection box operation
|
||||||
|
if (isSelecting || justFinishedSelecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Don't seek if clicking on timeline elements, but still deselect
|
// Don't seek if clicking on timeline elements, but still deselect
|
||||||
if ((e.target as HTMLElement).closest(".timeline-element")) {
|
if ((e.target as HTMLElement).closest(".timeline-element")) {
|
||||||
return;
|
return;
|
||||||
@ -161,6 +186,7 @@ export function Timeline() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear selected elements when clicking empty timeline area
|
// Clear selected elements when clicking empty timeline area
|
||||||
|
console.log(JSON.stringify({ clearingSelectedElements: true }));
|
||||||
clearSelectedElements();
|
clearSelectedElements();
|
||||||
|
|
||||||
// Determine if we're clicking in ruler or tracks area
|
// Determine if we're clicking in ruler or tracks area
|
||||||
@ -209,6 +235,8 @@ export function Timeline() {
|
|||||||
rulerScrollRef,
|
rulerScrollRef,
|
||||||
tracksScrollRef,
|
tracksScrollRef,
|
||||||
clearSelectedElements,
|
clearSelectedElements,
|
||||||
|
isSelecting,
|
||||||
|
justFinishedSelecting,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -283,107 +311,7 @@ export function Timeline() {
|
|||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [redo]);
|
}, [redo]);
|
||||||
|
|
||||||
// Mouse down on timeline background to start marquee
|
// Old marquee system removed - using new SelectionBox component instead
|
||||||
const handleTimelineMouseDown = (e: React.MouseEvent) => {
|
|
||||||
if (e.target === e.currentTarget && e.button === 0) {
|
|
||||||
setMarquee({
|
|
||||||
startX: e.clientX,
|
|
||||||
startY: e.clientY,
|
|
||||||
endX: e.clientX,
|
|
||||||
endY: e.clientY,
|
|
||||||
active: true,
|
|
||||||
additive: e.metaKey || e.ctrlKey || e.shiftKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mouse move to update marquee
|
|
||||||
useEffect(() => {
|
|
||||||
if (!marquee || !marquee.active) return;
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
|
||||||
setMarquee(
|
|
||||||
(prev) => prev && { ...prev, endX: e.clientX, endY: e.clientY }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const handleMouseUp = (e: MouseEvent) => {
|
|
||||||
setMarquee(
|
|
||||||
(prev) =>
|
|
||||||
prev && { ...prev, endX: e.clientX, endY: e.clientY, active: false }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
window.addEventListener("mousemove", handleMouseMove);
|
|
||||||
window.addEventListener("mouseup", handleMouseUp);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
window.removeEventListener("mouseup", handleMouseUp);
|
|
||||||
};
|
|
||||||
}, [marquee]);
|
|
||||||
|
|
||||||
// On marquee end, select elements in box
|
|
||||||
useEffect(() => {
|
|
||||||
if (!marquee || marquee.active) return;
|
|
||||||
const timeline = timelineRef.current;
|
|
||||||
if (!timeline) return;
|
|
||||||
const rect = timeline.getBoundingClientRect();
|
|
||||||
const x1 = Math.min(marquee.startX, marquee.endX) - rect.left;
|
|
||||||
const x2 = Math.max(marquee.startX, marquee.endX) - rect.left;
|
|
||||||
const y1 = Math.min(marquee.startY, marquee.endY) - rect.top;
|
|
||||||
const y2 = Math.max(marquee.startY, marquee.endY) - rect.top;
|
|
||||||
// Validation: skip if too small
|
|
||||||
if (Math.abs(x2 - x1) < 5 || Math.abs(y2 - y1) < 5) {
|
|
||||||
setMarquee(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Clamp to timeline bounds
|
|
||||||
const clamp = (val: number, min: number, max: number) =>
|
|
||||||
Math.max(min, Math.min(max, val));
|
|
||||||
const bx1 = clamp(x1, 0, rect.width);
|
|
||||||
const bx2 = clamp(x2, 0, rect.width);
|
|
||||||
const by1 = clamp(y1, 0, rect.height);
|
|
||||||
const by2 = clamp(y2, 0, rect.height);
|
|
||||||
let newSelection: { trackId: string; elementId: string }[] = [];
|
|
||||||
tracks.forEach((track, trackIdx) => {
|
|
||||||
track.elements.forEach((element) => {
|
|
||||||
const clipLeft =
|
|
||||||
element.startTime * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
|
||||||
const clipTop = getCumulativeHeightBefore(tracks, trackIdx);
|
|
||||||
const clipBottom = clipTop + getTrackHeight(track.type);
|
|
||||||
const clipRight = clipLeft + getTrackHeight(track.type);
|
|
||||||
if (
|
|
||||||
bx1 < clipRight &&
|
|
||||||
bx2 > clipLeft &&
|
|
||||||
by1 < clipBottom &&
|
|
||||||
by2 > clipTop
|
|
||||||
) {
|
|
||||||
newSelection.push({ trackId: track.id, elementId: element.id });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (newSelection.length > 0) {
|
|
||||||
if (marquee.additive) {
|
|
||||||
const selectedSet = new Set(
|
|
||||||
selectedElements.map((c) => c.trackId + ":" + c.elementId)
|
|
||||||
);
|
|
||||||
newSelection = [
|
|
||||||
...selectedElements,
|
|
||||||
...newSelection.filter(
|
|
||||||
(c) => !selectedSet.has(c.trackId + ":" + c.elementId)
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
setSelectedElements(newSelection);
|
|
||||||
} else if (!marquee.additive) {
|
|
||||||
clearSelectedElements();
|
|
||||||
}
|
|
||||||
setMarquee(null);
|
|
||||||
}, [
|
|
||||||
marquee,
|
|
||||||
tracks,
|
|
||||||
zoomLevel,
|
|
||||||
selectedElements,
|
|
||||||
setSelectedElements,
|
|
||||||
clearSelectedElements,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleDragEnter = (e: React.DragEvent) => {
|
const handleDragEnter = (e: React.DragEvent) => {
|
||||||
// When something is dragged over the timeline, show overlay
|
// When something is dragged over the timeline, show overlay
|
||||||
@ -898,6 +826,7 @@ export function Timeline() {
|
|||||||
<div
|
<div
|
||||||
className="flex-1 relative overflow-hidden h-4"
|
className="flex-1 relative overflow-hidden h-4"
|
||||||
onWheel={handleWheel}
|
onWheel={handleWheel}
|
||||||
|
onMouseDown={handleSelectionMouseDown}
|
||||||
onClick={handleTimelineContentClick}
|
onClick={handleTimelineContentClick}
|
||||||
data-ruler-area
|
data-ruler-area
|
||||||
>
|
>
|
||||||
@ -1016,8 +945,16 @@ export function Timeline() {
|
|||||||
<div
|
<div
|
||||||
className="flex-1 relative overflow-hidden"
|
className="flex-1 relative overflow-hidden"
|
||||||
onWheel={handleWheel}
|
onWheel={handleWheel}
|
||||||
|
onMouseDown={handleSelectionMouseDown}
|
||||||
onClick={handleTimelineContentClick}
|
onClick={handleTimelineContentClick}
|
||||||
|
ref={tracksContainerRef}
|
||||||
>
|
>
|
||||||
|
<SelectionBox
|
||||||
|
startPos={selectionBox?.startPos || null}
|
||||||
|
currentPos={selectionBox?.currentPos || null}
|
||||||
|
containerRef={tracksContainerRef}
|
||||||
|
isActive={selectionBox?.isActive || false}
|
||||||
|
/>
|
||||||
<ScrollArea className="w-full h-full" ref={tracksScrollRef}>
|
<ScrollArea className="w-full h-full" ref={tracksScrollRef}>
|
||||||
<div
|
<div
|
||||||
className="relative flex-1"
|
className="relative flex-1"
|
||||||
@ -1025,7 +962,6 @@ export function Timeline() {
|
|||||||
height: `${Math.max(200, Math.min(800, getTotalTracksHeight(tracks)))}px`,
|
height: `${Math.max(200, Math.min(800, getTotalTracksHeight(tracks)))}px`,
|
||||||
width: `${dynamicTimelineWidth}px`,
|
width: `${dynamicTimelineWidth}px`,
|
||||||
}}
|
}}
|
||||||
onMouseDown={handleTimelineMouseDown}
|
|
||||||
>
|
>
|
||||||
{tracks.length === 0 ? (
|
{tracks.length === 0 ? (
|
||||||
<div></div>
|
<div></div>
|
||||||
|
195
apps/web/src/hooks/use-selection-box.ts
Normal file
195
apps/web/src/hooks/use-selection-box.ts
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
|
interface UseSelectionBoxProps {
|
||||||
|
containerRef: React.RefObject<HTMLElement>;
|
||||||
|
playheadRef?: React.RefObject<HTMLElement>;
|
||||||
|
onSelectionComplete: (
|
||||||
|
elements: { trackId: string; elementId: string }[]
|
||||||
|
) => void;
|
||||||
|
isEnabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectionBoxState {
|
||||||
|
startPos: { x: number; y: number };
|
||||||
|
currentPos: { x: number; y: number };
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSelectionBox({
|
||||||
|
containerRef,
|
||||||
|
playheadRef,
|
||||||
|
onSelectionComplete,
|
||||||
|
isEnabled = true,
|
||||||
|
}: UseSelectionBoxProps) {
|
||||||
|
const [selectionBox, setSelectionBox] = useState<SelectionBoxState | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [justFinishedSelecting, setJustFinishedSelecting] = useState(false);
|
||||||
|
|
||||||
|
// Mouse down handler to start selection
|
||||||
|
const handleMouseDown = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
if (!isEnabled) return;
|
||||||
|
|
||||||
|
// Only start selection on empty space clicks
|
||||||
|
if ((e.target as HTMLElement).closest(".timeline-element")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (playheadRef?.current?.contains(e.target as Node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((e.target as HTMLElement).closest("[data-track-labels]")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectionBox({
|
||||||
|
startPos: { x: e.clientX, y: e.clientY },
|
||||||
|
currentPos: { x: e.clientX, y: e.clientY },
|
||||||
|
isActive: false, // Will become active when mouse moves
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[isEnabled, playheadRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Function to select elements within the selection box
|
||||||
|
const selectElementsInBox = useCallback(
|
||||||
|
(startPos: { x: number; y: number }, endPos: { x: number; y: number }) => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const container = containerRef.current;
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Calculate selection rectangle in container coordinates
|
||||||
|
const startX = startPos.x - containerRect.left;
|
||||||
|
const startY = startPos.y - containerRect.top;
|
||||||
|
const endX = endPos.x - containerRect.left;
|
||||||
|
const endY = endPos.y - containerRect.top;
|
||||||
|
|
||||||
|
const selectionRect = {
|
||||||
|
left: Math.min(startX, endX),
|
||||||
|
top: Math.min(startY, endY),
|
||||||
|
right: Math.max(startX, endX),
|
||||||
|
bottom: Math.max(startY, endY),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find all timeline elements within the selection rectangle
|
||||||
|
const timelineElements = container.querySelectorAll(".timeline-element");
|
||||||
|
|
||||||
|
const selectedElements: { trackId: string; elementId: string }[] = [];
|
||||||
|
|
||||||
|
timelineElements.forEach((element) => {
|
||||||
|
const elementRect = element.getBoundingClientRect();
|
||||||
|
// Use absolute coordinates for more accurate intersection detection
|
||||||
|
const elementAbsolute = {
|
||||||
|
left: elementRect.left,
|
||||||
|
top: elementRect.top,
|
||||||
|
right: elementRect.right,
|
||||||
|
bottom: elementRect.bottom,
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectionAbsolute = {
|
||||||
|
left: startPos.x,
|
||||||
|
top: startPos.y,
|
||||||
|
right: endPos.x,
|
||||||
|
bottom: endPos.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize selection rectangle (handle dragging in any direction)
|
||||||
|
const normalizedSelection = {
|
||||||
|
left: Math.min(selectionAbsolute.left, selectionAbsolute.right),
|
||||||
|
top: Math.min(selectionAbsolute.top, selectionAbsolute.bottom),
|
||||||
|
right: Math.max(selectionAbsolute.left, selectionAbsolute.right),
|
||||||
|
bottom: Math.max(selectionAbsolute.top, selectionAbsolute.bottom),
|
||||||
|
};
|
||||||
|
|
||||||
|
const elementId = element.getAttribute("data-element-id");
|
||||||
|
const trackId = element.getAttribute("data-track-id");
|
||||||
|
|
||||||
|
// Check if element intersects with selection rectangle (any overlap)
|
||||||
|
// Using absolute coordinates for more precise detection
|
||||||
|
const intersects = !(
|
||||||
|
elementAbsolute.right < normalizedSelection.left ||
|
||||||
|
elementAbsolute.left > normalizedSelection.right ||
|
||||||
|
elementAbsolute.bottom < normalizedSelection.top ||
|
||||||
|
elementAbsolute.top > normalizedSelection.bottom
|
||||||
|
);
|
||||||
|
|
||||||
|
if (intersects && elementId && trackId) {
|
||||||
|
selectedElements.push({ trackId, elementId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Always call the callback - with elements or empty array to clear selection
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({ selectElementsInBox: selectedElements.length })
|
||||||
|
);
|
||||||
|
onSelectionComplete(selectedElements);
|
||||||
|
},
|
||||||
|
[containerRef, onSelectionComplete]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Effect to track selection box movement
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectionBox) return;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
const deltaX = Math.abs(e.clientX - selectionBox.startPos.x);
|
||||||
|
const deltaY = Math.abs(e.clientY - selectionBox.startPos.y);
|
||||||
|
|
||||||
|
// Start selection if mouse moved more than 5px
|
||||||
|
const shouldActivate = deltaX > 5 || deltaY > 5;
|
||||||
|
|
||||||
|
const newSelectionBox = {
|
||||||
|
...selectionBox,
|
||||||
|
currentPos: { x: e.clientX, y: e.clientY },
|
||||||
|
isActive: shouldActivate || selectionBox.isActive,
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectionBox(newSelectionBox);
|
||||||
|
|
||||||
|
// Real-time visual feedback: update selection as we drag
|
||||||
|
if (newSelectionBox.isActive) {
|
||||||
|
selectElementsInBox(
|
||||||
|
newSelectionBox.startPos,
|
||||||
|
newSelectionBox.currentPos
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({ mouseUp: { wasActive: selectionBox?.isActive } })
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we had an active selection, mark that we just finished selecting
|
||||||
|
if (selectionBox?.isActive) {
|
||||||
|
console.log(JSON.stringify({ settingJustFinishedSelecting: true }));
|
||||||
|
setJustFinishedSelecting(true);
|
||||||
|
// Clear the flag after a short delay to allow click events to check it
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(JSON.stringify({ clearingJustFinishedSelecting: true }));
|
||||||
|
setJustFinishedSelecting(false);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't call selectElementsInBox again - real-time selection already handled it
|
||||||
|
// Just clean up the selection box visual
|
||||||
|
setSelectionBox(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
|
window.addEventListener("mouseup", handleMouseUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
window.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
};
|
||||||
|
}, [selectionBox, selectElementsInBox]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectionBox,
|
||||||
|
handleMouseDown,
|
||||||
|
isSelecting: selectionBox?.isActive || false,
|
||||||
|
justFinishedSelecting,
|
||||||
|
};
|
||||||
|
}
|
Reference in New Issue
Block a user