mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
holy cow
This commit is contained in:
@@ -245,6 +245,7 @@ export function useElementInteraction({
|
||||
element: TimelineElement;
|
||||
track: TimelineTrack;
|
||||
}) => {
|
||||
event.stopPropagation();
|
||||
mouseDownLocationRef.current = { x: event.clientX, y: event.clientY };
|
||||
|
||||
const isRightClick = event.button === 2;
|
||||
|
||||
@@ -11,7 +11,6 @@ interface UseTimelineInteractionsProps {
|
||||
zoomLevel: number;
|
||||
duration: number;
|
||||
isSelecting: boolean;
|
||||
justFinishedSelecting: boolean;
|
||||
clearSelectedElements: () => void;
|
||||
seek: (time: number) => void;
|
||||
}
|
||||
@@ -23,7 +22,6 @@ export function useTimelineInteractions({
|
||||
zoomLevel,
|
||||
duration,
|
||||
isSelecting,
|
||||
justFinishedSelecting,
|
||||
clearSelectedElements,
|
||||
seek,
|
||||
}: UseTimelineInteractionsProps) {
|
||||
@@ -71,7 +69,7 @@ export function useTimelineInteractions({
|
||||
|
||||
if (deltaX > 5 || deltaY > 5 || deltaTime > 500) return false;
|
||||
|
||||
if (isSelecting || justFinishedSelecting) return false;
|
||||
if (isSelecting) return false;
|
||||
|
||||
if (target.closest(".timeline-element")) return false;
|
||||
|
||||
@@ -84,7 +82,7 @@ export function useTimelineInteractions({
|
||||
|
||||
return true;
|
||||
},
|
||||
[isSelecting, justFinishedSelecting, clearSelectedElements, playheadRef],
|
||||
[isSelecting, clearSelectedElements, playheadRef],
|
||||
);
|
||||
|
||||
const handleTimelineSeek = useCallback(
|
||||
|
||||
@@ -91,25 +91,6 @@ export function useTimelinePlayhead({
|
||||
const fps = activeProject.settings.fps;
|
||||
const time = snapTimeToFrame({ time: rawTime, fps });
|
||||
|
||||
// debug logging
|
||||
if (rawX < 0 || x !== rawX) {
|
||||
console.log(
|
||||
"PLAYHEAD DEBUG:",
|
||||
JSON.stringify({
|
||||
mouseX: event.clientX,
|
||||
rulerLeft: rect.left,
|
||||
rawX,
|
||||
constrainedX: x,
|
||||
timelineContentWidth,
|
||||
rawTime,
|
||||
finalTime: time,
|
||||
duration,
|
||||
zoomLevel,
|
||||
playheadPx: time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
setScrubTime(time);
|
||||
seek(time); // update video preview in real time
|
||||
|
||||
|
||||
@@ -35,14 +35,14 @@ export function useTimelineZoom({
|
||||
|
||||
// pinch-zoom (ctrl/meta + wheel)
|
||||
if (isZoomGesture) {
|
||||
event.preventDefault();
|
||||
const zoomMultiplier = event.deltaY > 0 ? 1 / 1.1 : 1.1;
|
||||
setZoomLevel((prev) =>
|
||||
Math.max(
|
||||
setZoomLevel((prev) => {
|
||||
const nextZoom = Math.max(
|
||||
TIMELINE_CONSTANTS.ZOOM_MIN,
|
||||
Math.min(TIMELINE_CONSTANTS.ZOOM_MAX, prev * zoomMultiplier),
|
||||
),
|
||||
);
|
||||
);
|
||||
return nextZoom;
|
||||
});
|
||||
// for horizontal scrolling (when shift is held or horizontal wheel movement),
|
||||
// let the event bubble up to allow ScrollArea to handle it
|
||||
return;
|
||||
@@ -51,25 +51,25 @@ export function useTimelineZoom({
|
||||
|
||||
// prevent browser zoom in the timeline
|
||||
useEffect(() => {
|
||||
const preventZoom = ({
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
target,
|
||||
preventDefault,
|
||||
}: WheelEvent) => {
|
||||
if (
|
||||
isInTimeline &&
|
||||
(ctrlKey || metaKey) &&
|
||||
containerRef.current?.contains(target as Node)
|
||||
) {
|
||||
preventDefault();
|
||||
const preventZoom = (event: WheelEvent) => {
|
||||
const isZoomKeyPressed = event.ctrlKey || event.metaKey;
|
||||
const isInContainer = containerRef.current?.contains(
|
||||
event.target as Node,
|
||||
);
|
||||
const shouldPrevent =
|
||||
isInTimeline && isZoomKeyPressed && Boolean(isInContainer);
|
||||
if (shouldPrevent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("wheel", preventZoom, { passive: false });
|
||||
document.addEventListener("wheel", preventZoom, {
|
||||
passive: false,
|
||||
capture: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("wheel", preventZoom);
|
||||
document.removeEventListener("wheel", preventZoom, { capture: true });
|
||||
};
|
||||
}, [isInTimeline, containerRef]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { useEffect, useRef, useCallback } from "react";
|
||||
import {
|
||||
TAction,
|
||||
TActionFunc,
|
||||
@@ -14,7 +14,7 @@ export function useActionHandler<A extends TAction>(
|
||||
isActive: TActionHandlerOptions,
|
||||
) {
|
||||
const handlerRef = useRef(handler);
|
||||
const [isBound, setIsBound] = useState(false);
|
||||
const isBoundRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
handlerRef.current = handler;
|
||||
@@ -32,36 +32,34 @@ export function useActionHandler<A extends TAction>(
|
||||
isActive === undefined ||
|
||||
(typeof isActive === "boolean" ? isActive : isActive.current);
|
||||
|
||||
if (shouldBind && !isBound) {
|
||||
if (shouldBind && !isBoundRef.current) {
|
||||
bindAction(action, stableHandler);
|
||||
setIsBound(true);
|
||||
} else if (!shouldBind && isBound) {
|
||||
isBoundRef.current = true;
|
||||
} else if (!shouldBind && isBoundRef.current) {
|
||||
unbindAction(action, stableHandler);
|
||||
setIsBound(false);
|
||||
isBoundRef.current = false;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (isBound) {
|
||||
unbindAction(action, stableHandler);
|
||||
setIsBound(false);
|
||||
}
|
||||
unbindAction(action, stableHandler);
|
||||
isBoundRef.current = false;
|
||||
};
|
||||
}, [action, stableHandler, isActive, isBound]);
|
||||
}, [action, stableHandler, isActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive && typeof isActive === "object" && "current" in isActive) {
|
||||
const interval = setInterval(() => {
|
||||
const shouldBind = isActive.current;
|
||||
if (shouldBind !== isBound) {
|
||||
if (shouldBind !== isBoundRef.current) {
|
||||
if (shouldBind) {
|
||||
bindAction(action, stableHandler);
|
||||
} else {
|
||||
unbindAction(action, stableHandler);
|
||||
}
|
||||
setIsBound(shouldBind);
|
||||
isBoundRef.current = shouldBind;
|
||||
}
|
||||
}, 100);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [action, stableHandler, isActive, isBound]);
|
||||
}, [action, stableHandler, isActive]);
|
||||
}
|
||||
|
||||
@@ -137,6 +137,38 @@ export function useEditorActions() {
|
||||
undefined,
|
||||
);
|
||||
|
||||
useActionHandler(
|
||||
"split-selected-left",
|
||||
() => {
|
||||
const splitElementIds = editor.timeline.splitElements({
|
||||
elements: selectedElements,
|
||||
splitTime: editor.playback.getCurrentTime(),
|
||||
retainSide: "left",
|
||||
});
|
||||
|
||||
if (splitElementIds.length === 0) {
|
||||
toast.error("Playhead must be positioned over the selected element(s)");
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
useActionHandler(
|
||||
"split-selected-right",
|
||||
() => {
|
||||
const splitElementIds = editor.timeline.splitElements({
|
||||
elements: selectedElements,
|
||||
splitTime: editor.playback.getCurrentTime(),
|
||||
retainSide: "right",
|
||||
});
|
||||
|
||||
if (splitElementIds.length === 0) {
|
||||
toast.error("Playhead must be positioned over the selected element(s)");
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
useActionHandler(
|
||||
"delete-selected",
|
||||
() => {
|
||||
|
||||
@@ -4,22 +4,27 @@ interface UseScrollSyncProps {
|
||||
rulerScrollRef: React.RefObject<HTMLDivElement>;
|
||||
tracksScrollRef: React.RefObject<HTMLDivElement>;
|
||||
trackLabelsScrollRef?: React.RefObject<HTMLDivElement>;
|
||||
bookmarksScrollRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export function useScrollSync({
|
||||
rulerScrollRef,
|
||||
tracksScrollRef,
|
||||
trackLabelsScrollRef,
|
||||
bookmarksScrollRef,
|
||||
}: UseScrollSyncProps) {
|
||||
const isUpdatingRef = useRef(false);
|
||||
const lastRulerSync = useRef(0);
|
||||
const lastTracksSync = useRef(0);
|
||||
const lastVerticalSync = useRef(0);
|
||||
const lastBookmarksSync = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const rulerViewport = rulerScrollRef.current;
|
||||
const tracksViewport = tracksScrollRef.current;
|
||||
const trackLabelsViewport = trackLabelsScrollRef?.current;
|
||||
const bookmarksViewport = bookmarksScrollRef?.current;
|
||||
let handleBookmarksScroll: (() => void) | null = null;
|
||||
|
||||
if (!rulerViewport || !tracksViewport) return;
|
||||
|
||||
@@ -29,6 +34,9 @@ export function useScrollSync({
|
||||
lastRulerSync.current = now;
|
||||
isUpdatingRef.current = true;
|
||||
tracksViewport.scrollLeft = rulerViewport.scrollLeft;
|
||||
if (bookmarksViewport) {
|
||||
bookmarksViewport.scrollLeft = rulerViewport.scrollLeft;
|
||||
}
|
||||
isUpdatingRef.current = false;
|
||||
};
|
||||
|
||||
@@ -38,12 +46,30 @@ export function useScrollSync({
|
||||
lastTracksSync.current = now;
|
||||
isUpdatingRef.current = true;
|
||||
rulerViewport.scrollLeft = tracksViewport.scrollLeft;
|
||||
if (bookmarksViewport) {
|
||||
bookmarksViewport.scrollLeft = tracksViewport.scrollLeft;
|
||||
}
|
||||
isUpdatingRef.current = false;
|
||||
};
|
||||
|
||||
rulerViewport.addEventListener("scroll", handleRulerScroll);
|
||||
tracksViewport.addEventListener("scroll", handleTracksScroll);
|
||||
|
||||
if (bookmarksViewport) {
|
||||
handleBookmarksScroll = () => {
|
||||
const now = Date.now();
|
||||
if (isUpdatingRef.current || now - lastBookmarksSync.current < 16)
|
||||
return;
|
||||
lastBookmarksSync.current = now;
|
||||
isUpdatingRef.current = true;
|
||||
tracksViewport.scrollLeft = bookmarksViewport.scrollLeft;
|
||||
rulerViewport.scrollLeft = bookmarksViewport.scrollLeft;
|
||||
isUpdatingRef.current = false;
|
||||
};
|
||||
|
||||
bookmarksViewport.addEventListener("scroll", handleBookmarksScroll);
|
||||
}
|
||||
|
||||
if (trackLabelsViewport) {
|
||||
const handleTrackLabelsScroll = () => {
|
||||
const now = Date.now();
|
||||
@@ -71,6 +97,12 @@ export function useScrollSync({
|
||||
return () => {
|
||||
rulerViewport.removeEventListener("scroll", handleRulerScroll);
|
||||
tracksViewport.removeEventListener("scroll", handleTracksScroll);
|
||||
if (bookmarksViewport && handleBookmarksScroll) {
|
||||
bookmarksViewport.removeEventListener(
|
||||
"scroll",
|
||||
handleBookmarksScroll,
|
||||
);
|
||||
}
|
||||
trackLabelsViewport.removeEventListener(
|
||||
"scroll",
|
||||
handleTrackLabelsScroll,
|
||||
@@ -85,8 +117,14 @@ export function useScrollSync({
|
||||
return () => {
|
||||
rulerViewport.removeEventListener("scroll", handleRulerScroll);
|
||||
tracksViewport.removeEventListener("scroll", handleTracksScroll);
|
||||
if (bookmarksViewport && handleBookmarksScroll) {
|
||||
bookmarksViewport.removeEventListener("scroll", handleBookmarksScroll);
|
||||
}
|
||||
};
|
||||
}, [rulerScrollRef, tracksScrollRef, trackLabelsScrollRef]);
|
||||
}, [
|
||||
rulerScrollRef,
|
||||
tracksScrollRef,
|
||||
trackLabelsScrollRef,
|
||||
bookmarksScrollRef,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
import { getCumulativeHeightBefore, getTrackHeight } from "@/lib/timeline";
|
||||
import { useEditor } from "./use-editor";
|
||||
|
||||
interface UseSelectionBoxProps {
|
||||
containerRef: React.RefObject<HTMLElement>;
|
||||
playheadRef?: React.RefObject<HTMLElement>;
|
||||
onSelectionComplete: (
|
||||
elements: { trackId: string; elementId: string }[]
|
||||
elements: { trackId: string; elementId: string }[],
|
||||
) => void;
|
||||
isEnabled?: boolean;
|
||||
tracksScrollRef: React.RefObject<HTMLDivElement>;
|
||||
zoomLevel: number;
|
||||
}
|
||||
|
||||
interface SelectionBoxState {
|
||||
@@ -15,169 +19,182 @@ interface SelectionBoxState {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
interface SelectionRectangle {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
function getNormalizedRectangle({
|
||||
startPos,
|
||||
endPos,
|
||||
}: {
|
||||
startPos: { x: number; y: number };
|
||||
endPos: { x: number; y: number };
|
||||
}): SelectionRectangle {
|
||||
return {
|
||||
left: Math.min(startPos.x, endPos.x),
|
||||
top: Math.min(startPos.y, endPos.y),
|
||||
right: Math.max(startPos.x, endPos.x),
|
||||
bottom: Math.max(startPos.y, endPos.y),
|
||||
};
|
||||
}
|
||||
|
||||
function getSelectionRectangleInContent({
|
||||
container,
|
||||
scrollContainer,
|
||||
startPos,
|
||||
endPos,
|
||||
}: {
|
||||
container: HTMLElement;
|
||||
scrollContainer: HTMLDivElement | null;
|
||||
startPos: { x: number; y: number };
|
||||
endPos: { x: number; y: number };
|
||||
}): SelectionRectangle {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const scrollLeft = scrollContainer?.scrollLeft ?? 0;
|
||||
const scrollTop = scrollContainer?.scrollTop ?? 0;
|
||||
|
||||
const adjustedStart = {
|
||||
x: startPos.x - containerRect.left + scrollLeft,
|
||||
y: startPos.y - containerRect.top + scrollTop,
|
||||
};
|
||||
const adjustedEnd = {
|
||||
x: endPos.x - containerRect.left + scrollLeft,
|
||||
y: endPos.y - containerRect.top + scrollTop,
|
||||
};
|
||||
|
||||
return getNormalizedRectangle({
|
||||
startPos: adjustedStart,
|
||||
endPos: adjustedEnd,
|
||||
});
|
||||
}
|
||||
|
||||
function isRectangleIntersecting({
|
||||
elementRectangle,
|
||||
selectionRectangle,
|
||||
}: {
|
||||
elementRectangle: SelectionRectangle;
|
||||
selectionRectangle: SelectionRectangle;
|
||||
}): boolean {
|
||||
return !(
|
||||
elementRectangle.right < selectionRectangle.left ||
|
||||
elementRectangle.left > selectionRectangle.right ||
|
||||
elementRectangle.bottom < selectionRectangle.top ||
|
||||
elementRectangle.top > selectionRectangle.bottom
|
||||
);
|
||||
}
|
||||
|
||||
export function useSelectionBox({
|
||||
containerRef,
|
||||
playheadRef,
|
||||
onSelectionComplete,
|
||||
isEnabled = true,
|
||||
tracksScrollRef,
|
||||
zoomLevel,
|
||||
}: UseSelectionBoxProps) {
|
||||
const editor = useEditor();
|
||||
const tracks = editor.timeline.getTracks();
|
||||
const [selectionBox, setSelectionBox] = useState<SelectionBoxState | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
const [justFinishedSelecting, setJustFinishedSelecting] = useState(false);
|
||||
|
||||
// Mouse down handler to start selection
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
({ clientX, clientY }: 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;
|
||||
}
|
||||
// Don't start selection when clicking in the ruler area - this interferes with playhead dragging
|
||||
if ((e.target as HTMLElement).closest("[data-ruler-area]")) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectionBox({
|
||||
startPos: { x: e.clientX, y: e.clientY },
|
||||
currentPos: { x: e.clientX, y: e.clientY },
|
||||
isActive: false, // Will become active when mouse moves
|
||||
startPos: { x: clientX, y: clientY },
|
||||
currentPos: { x: clientX, y: clientY },
|
||||
isActive: false,
|
||||
});
|
||||
},
|
||||
[isEnabled, playheadRef]
|
||||
[isEnabled],
|
||||
);
|
||||
|
||||
// Function to select elements within the selection box
|
||||
const selectElementsInBox = useCallback(
|
||||
(startPos: { x: number; y: number }, endPos: { x: number; y: number }) => {
|
||||
({
|
||||
startPos,
|
||||
endPos,
|
||||
}: {
|
||||
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 selectionRectangle = getSelectionRectangleInContent({
|
||||
container,
|
||||
scrollContainer: tracksScrollRef.current,
|
||||
startPos,
|
||||
endPos,
|
||||
});
|
||||
const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
||||
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,
|
||||
};
|
||||
for (const [trackIndex, track] of tracks.entries()) {
|
||||
const trackTop = getCumulativeHeightBefore({
|
||||
tracks,
|
||||
trackIndex,
|
||||
});
|
||||
const trackHeight = getTrackHeight({ type: track.type });
|
||||
const elementTop = trackTop;
|
||||
const elementBottom = trackTop + trackHeight;
|
||||
|
||||
const selectionAbsolute = {
|
||||
left: startPos.x,
|
||||
top: startPos.y,
|
||||
right: endPos.x,
|
||||
bottom: endPos.y,
|
||||
};
|
||||
for (const element of track.elements) {
|
||||
const elementLeft = element.startTime * pixelsPerSecond;
|
||||
const elementRight = elementLeft + element.duration * pixelsPerSecond;
|
||||
|
||||
// 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 elementRectangle = {
|
||||
left: elementLeft,
|
||||
top: elementTop,
|
||||
right: elementRight,
|
||||
bottom: elementBottom,
|
||||
};
|
||||
|
||||
const elementId = element.getAttribute("data-element-id");
|
||||
const trackId = element.getAttribute("data-track-id");
|
||||
const intersects = isRectangleIntersecting({
|
||||
elementRectangle,
|
||||
selectionRectangle,
|
||||
});
|
||||
|
||||
// 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 });
|
||||
if (intersects) {
|
||||
selectedElements.push({
|
||||
trackId: track.id,
|
||||
elementId: element.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Always call the callback - with elements or empty array to clear selection
|
||||
console.log(
|
||||
JSON.stringify({ selectElementsInBox: selectedElements.length })
|
||||
);
|
||||
}
|
||||
onSelectionComplete(selectedElements);
|
||||
},
|
||||
[containerRef, onSelectionComplete]
|
||||
[containerRef, onSelectionComplete, tracks, tracksScrollRef, zoomLevel],
|
||||
);
|
||||
|
||||
// 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 handleMouseMove = ({ clientX, clientY }: MouseEvent) => {
|
||||
const deltaX = Math.abs(clientX - selectionBox.startPos.x);
|
||||
const deltaY = Math.abs(clientY - selectionBox.startPos.y);
|
||||
const shouldActivate = deltaX > 5 || deltaY > 5;
|
||||
|
||||
const newSelectionBox = {
|
||||
...selectionBox,
|
||||
currentPos: { x: e.clientX, y: e.clientY },
|
||||
currentPos: { x: clientX, y: clientY },
|
||||
isActive: shouldActivate || selectionBox.isActive,
|
||||
};
|
||||
|
||||
setSelectionBox(newSelectionBox);
|
||||
|
||||
// Real-time visual feedback: update selection as we drag
|
||||
if (newSelectionBox.isActive) {
|
||||
selectElementsInBox(
|
||||
newSelectionBox.startPos,
|
||||
newSelectionBox.currentPos
|
||||
);
|
||||
selectElementsInBox({
|
||||
startPos: newSelectionBox.startPos,
|
||||
endPos: 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);
|
||||
};
|
||||
|
||||
@@ -210,6 +227,5 @@ export function useSelectionBox({
|
||||
selectionBox,
|
||||
handleMouseDown,
|
||||
isSelecting: selectionBox?.isActive || false,
|
||||
justFinishedSelecting,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user