diff options
Diffstat (limited to 'apps/web/src/selection')
| -rw-r--r-- | apps/web/src/selection/hooks/use-box-select.ts | 85 | ||||
| -rw-r--r-- | apps/web/src/selection/hooks/use-selection-scope.ts | 19 | ||||
| -rw-r--r-- | apps/web/src/selection/selectable-item.tsx | 10 | ||||
| -rw-r--r-- | apps/web/src/selection/selectable-surface.tsx | 26 | ||||
| -rw-r--r-- | apps/web/src/selection/selection-box.tsx | 47 | ||||
| -rw-r--r-- | apps/web/src/selection/types.ts | 7 |
6 files changed, 110 insertions, 84 deletions
diff --git a/apps/web/src/selection/hooks/use-box-select.ts b/apps/web/src/selection/hooks/use-box-select.ts index 90a584b5..3bbff14e 100644 --- a/apps/web/src/selection/hooks/use-box-select.ts +++ b/apps/web/src/selection/hooks/use-box-select.ts @@ -4,15 +4,40 @@ import { useCallback, useEffect, useRef, useState } from "react"; import type { BoxSelectionSnapshot, ResolveIntersections, + SelectionBoxBounds, } from "@/selection/types"; interface SelectionBoxState<TId> extends BoxSelectionSnapshot<TId> { startPos: { x: number; y: number }; currentPos: { x: number; y: number }; + bounds: SelectionBoxBounds | null; isActive: boolean; isAdditive: boolean; } +function getSelectionBoxBounds({ + container, + startPos, + currentPos, +}: { + container: HTMLElement; + startPos: { x: number; y: number }; + currentPos: { x: number; y: number }; +}): SelectionBoxBounds { + const containerRect = container.getBoundingClientRect(); + const startX = startPos.x - containerRect.left; + const startY = startPos.y - containerRect.top; + const currentX = currentPos.x - containerRect.left; + const currentY = currentPos.y - containerRect.top; + + return { + left: Math.min(startX, currentX), + top: Math.min(startY, currentY), + width: Math.abs(currentX - startX), + height: Math.abs(currentY - startY), + }; +} + export function useBoxSelect<TId>({ containerRef, resolveIntersections, @@ -40,33 +65,43 @@ export function useBoxSelect<TId>({ const [selectionBox, setSelectionBox] = useState<SelectionBoxState<TId> | null>(null); const justFinishedSelectingRef = useRef(false); - const shouldStartSelectionCheck = shouldStartSelection ?? (() => true); - const getIsAdditiveSelectionCheck = - getIsAdditiveSelection ?? - ((event: React.MouseEvent<Element>) => event.ctrlKey || event.metaKey); const handleMouseDown = useCallback( (event: React.MouseEvent<Element>) => { - const canStartSelection = shouldStartSelectionCheck(event); + const canStartSelection = shouldStartSelection + ? shouldStartSelection(event) + : true; if (!isEnabled || event.button !== 0 || !canStartSelection) { return; } + const startPos = { x: event.clientX, y: event.clientY }; + const container = containerRef.current; setSelectionBox({ - startPos: { x: event.clientX, y: event.clientY }, - currentPos: { x: event.clientX, y: event.clientY }, + startPos, + currentPos: startPos, + bounds: container + ? getSelectionBoxBounds({ + container, + startPos, + currentPos: startPos, + }) + : null, isActive: false, - isAdditive: getIsAdditiveSelectionCheck(event), + isAdditive: getIsAdditiveSelection + ? getIsAdditiveSelection(event) + : event.ctrlKey || event.metaKey, initialSelectedIds: selectedIds, initialAnchorId: anchorId, }); }, [ anchorId, - getIsAdditiveSelectionCheck, + containerRef, + getIsAdditiveSelection, isEnabled, selectedIds, - shouldStartSelectionCheck, + shouldStartSelection, ], ); @@ -98,11 +133,20 @@ export function useBoxSelect<TId>({ } const handleMouseMove = ({ clientX, clientY }: MouseEvent) => { + const currentPos = { x: clientX, y: clientY }; const deltaX = Math.abs(clientX - selectionBox.startPos.x); const deltaY = Math.abs(clientY - selectionBox.startPos.y); + const container = containerRef.current; const nextSelectionBox = { ...selectionBox, - currentPos: { x: clientX, y: clientY }, + currentPos, + bounds: container + ? getSelectionBoxBounds({ + container, + startPos: selectionBox.startPos, + currentPos, + }) + : null, isActive: deltaX > 5 || deltaY > 5 || selectionBox.isActive, }; @@ -133,26 +177,26 @@ export function useBoxSelect<TId>({ window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; - }, [selectionBox, updateSelection]); + }, [containerRef, selectionBox, updateSelection]); useEffect(() => { if (!selectionBox) { return; } + const container = containerRef.current; const previousBodyUserSelect = document.body.style.userSelect; - const previousContainerUserSelect = - containerRef.current?.style.userSelect ?? ""; + const previousContainerUserSelect = container?.style.userSelect ?? ""; document.body.style.userSelect = "none"; - if (containerRef.current) { - containerRef.current.style.userSelect = "none"; + if (container) { + container.style.userSelect = "none"; } return () => { document.body.style.userSelect = previousBodyUserSelect; - if (containerRef.current) { - containerRef.current.style.userSelect = previousContainerUserSelect; + if (container) { + container.style.userSelect = previousContainerUserSelect; } }; }, [containerRef, selectionBox]); @@ -162,7 +206,10 @@ export function useBoxSelect<TId>({ }, []); return { - selectionBox, + selectionBox: + selectionBox?.isActive && selectionBox.bounds + ? { bounds: selectionBox.bounds } + : null, handleMouseDown, isSelecting: selectionBox?.isActive ?? false, shouldIgnoreClick, diff --git a/apps/web/src/selection/hooks/use-selection-scope.ts b/apps/web/src/selection/hooks/use-selection-scope.ts index ad4d2ce8..7fddab08 100644 --- a/apps/web/src/selection/hooks/use-selection-scope.ts +++ b/apps/web/src/selection/hooks/use-selection-scope.ts @@ -1,28 +1,25 @@ -import { useEffect, useRef } from "react";
+import { useEffect, useState } from "react";
+import { useCommittedRef } from "@/hooks/use-committed-ref";
import { useSelectionContext } from "@/selection/context";
import { activateScope, type ScopeEntry } from "@/selection/scope";
export function useSelectionScope() {
const { selectedIds, clearSelection } = useSelectionContext();
- const hasSelectionRef = useRef(selectedIds.length > 0);
- const clearSelectionRef = useRef(clearSelection);
const hasSelection = selectedIds.length > 0;
-
- hasSelectionRef.current = hasSelection;
- clearSelectionRef.current = clearSelection;
-
- const entryRef = useRef<ScopeEntry>({
+ const hasSelectionRef = useCommittedRef(hasSelection);
+ const clearSelectionRef = useCommittedRef(clearSelection);
+ const [entry] = useState<ScopeEntry>(() => ({
hasSelection: () => hasSelectionRef.current,
clear: () => {
clearSelectionRef.current();
},
- });
+ }));
useEffect(() => {
if (!hasSelection) {
return;
}
- return activateScope({ entry: entryRef.current });
- }, [hasSelection]);
+ return activateScope({ entry });
+ }, [entry, hasSelection]);
}
diff --git a/apps/web/src/selection/selectable-item.tsx b/apps/web/src/selection/selectable-item.tsx index cc9e1dbf..9735c4a8 100644 --- a/apps/web/src/selection/selectable-item.tsx +++ b/apps/web/src/selection/selectable-item.tsx @@ -6,7 +6,13 @@ import { SELECTABLE_ITEM_ATTRIBUTE } from "@/selection/attributes"; import type { SelectableItemProps } from "@/selection/types";
import { cn } from "@/utils/ui";
-function setForwardedRef<T>(ref: React.ForwardedRef<T>, value: T | null) {
+function setForwardedRef<T>({
+ ref,
+ value,
+}: {
+ ref: React.ForwardedRef<T>;
+ value: T | null;
+}) {
if (typeof ref === "function") {
ref(value);
return;
@@ -55,7 +61,7 @@ export const SelectableItem = forwardRef<HTMLDivElement, SelectableItemProps>( const handleRef = useCallback(
(element: HTMLDivElement | null) => {
registerItem(id, element);
- setForwardedRef(forwardedRef, element);
+ setForwardedRef({ ref: forwardedRef, value: element });
},
[forwardedRef, id, registerItem],
);
diff --git a/apps/web/src/selection/selectable-surface.tsx b/apps/web/src/selection/selectable-surface.tsx index 24166daf..12682ba8 100644 --- a/apps/web/src/selection/selectable-surface.tsx +++ b/apps/web/src/selection/selectable-surface.tsx @@ -167,14 +167,15 @@ export function SelectableSurface({ [],
);
- const { selectionBox, handleMouseDown, shouldIgnoreClick } = useBoxSelect({
- containerRef,
- resolveIntersections,
- selectedIds: selectionState.selectedIds,
- anchorId: selectionState.anchorId,
- onSelectionChange: handleBoxSelectionChange,
- shouldStartSelection,
- });
+ const { selectionBox, handleMouseDown, isSelecting, shouldIgnoreClick } =
+ useBoxSelect({
+ containerRef,
+ resolveIntersections,
+ selectedIds: selectionState.selectedIds,
+ anchorId: selectionState.anchorId,
+ onSelectionChange: handleBoxSelectionChange,
+ shouldStartSelection,
+ });
const handleBackgroundClick = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
@@ -236,7 +237,7 @@ export function SelectableSurface({ return () => clearTimeout(timer);
}, [getItemElement, onRevealComplete, revealId]);
- const isBoxSelecting = selectionBox?.isActive ?? false;
+ const isBoxSelecting = isSelecting;
const contextValue = useMemo(() => {
return {
@@ -276,12 +277,7 @@ export function SelectableSurface({ onKeyDown={handleBackgroundKeyDown}
>
{children}
- <SelectionBox
- startPos={selectionBox?.startPos ?? null}
- currentPos={selectionBox?.currentPos ?? null}
- containerRef={containerRef}
- isActive={selectionBox?.isActive ?? false}
- />
+ <SelectionBox bounds={selectionBox?.bounds ?? null} />
</div>
</SelectionContext.Provider>
);
diff --git a/apps/web/src/selection/selection-box.tsx b/apps/web/src/selection/selection-box.tsx index e30f574d..2f2bf933 100644 --- a/apps/web/src/selection/selection-box.tsx +++ b/apps/web/src/selection/selection-box.tsx @@ -1,49 +1,22 @@ "use client";
-import { useMemo } from "react";
+import type { SelectionBoxBounds } from "@/selection/types";
interface SelectionBoxProps {
- startPos: { x: number; y: number } | null;
- currentPos: { x: number; y: number } | null;
- containerRef: React.RefObject<HTMLElement | null>;
- isActive: boolean;
+ bounds: SelectionBoxBounds | null;
}
-export function SelectionBox({
- startPos,
- currentPos,
- containerRef,
- isActive,
-}: SelectionBoxProps) {
- const selectionBoxStyle = useMemo(() => {
- if (!isActive || !startPos || !currentPos || !containerRef.current) {
- return null;
- }
-
- const containerRect = containerRef.current.getBoundingClientRect();
- const startX = startPos.x - containerRect.left;
- const startY = startPos.y - containerRect.top;
- const currentX = currentPos.x - containerRect.left;
- const currentY = currentPos.y - containerRect.top;
-
- const left = Math.min(startX, currentX);
- const top = Math.min(startY, currentY);
- const width = Math.abs(currentX - startX);
- const height = Math.abs(currentY - startY);
-
- return {
- left: `${left}px`,
- top: `${top}px`,
- width: `${width}px`,
- height: `${height}px`,
- };
- }, [containerRef, currentPos, isActive, startPos]);
-
- if (!selectionBoxStyle) return null;
+export function SelectionBox({ bounds }: SelectionBoxProps) {
+ if (!bounds) return null;
return (
<div
- style={selectionBoxStyle}
+ style={{
+ left: `${bounds.left}px`,
+ top: `${bounds.top}px`,
+ width: `${bounds.width}px`,
+ height: `${bounds.height}px`,
+ }}
className="border-foreground/50 bg-foreground/5 pointer-events-none absolute z-50 border"
/>
);
diff --git a/apps/web/src/selection/types.ts b/apps/web/src/selection/types.ts index 31520788..a797d6c0 100644 --- a/apps/web/src/selection/types.ts +++ b/apps/web/src/selection/types.ts @@ -8,6 +8,13 @@ export interface BoxSelectionSnapshot<TId = string> { initialAnchorId: TId | null;
}
+export interface SelectionBoxBounds {
+ left: number;
+ top: number;
+ width: number;
+ height: number;
+}
+
export interface BoxSelectionChange<TId = string>
extends BoxSelectionSnapshot<TId> {
intersectedIds: TId[];
|
