diff options
Diffstat (limited to 'apps/web/src/lib/selection')
| -rw-r--r-- | apps/web/src/lib/selection/attributes.ts | 1 | ||||
| -rw-r--r-- | apps/web/src/lib/selection/context.tsx | 52 | ||||
| -rw-r--r-- | apps/web/src/lib/selection/editor-selection.ts | 25 | ||||
| -rw-r--r-- | apps/web/src/lib/selection/hit-testing.ts | 50 | ||||
| -rw-r--r-- | apps/web/src/lib/selection/hooks/use-box-select.ts | 170 | ||||
| -rw-r--r-- | apps/web/src/lib/selection/hooks/use-selection-scope.ts | 28 | ||||
| -rw-r--r-- | apps/web/src/lib/selection/index.ts | 4 | ||||
| -rw-r--r-- | apps/web/src/lib/selection/scope.ts | 30 | ||||
| -rw-r--r-- | apps/web/src/lib/selection/selectable-item.tsx | 107 | ||||
| -rw-r--r-- | apps/web/src/lib/selection/selectable-surface.tsx | 288 | ||||
| -rw-r--r-- | apps/web/src/lib/selection/selection-box.tsx | 50 | ||||
| -rw-r--r-- | apps/web/src/lib/selection/state.ts | 165 | ||||
| -rw-r--r-- | apps/web/src/lib/selection/types.ts | 39 |
13 files changed, 0 insertions, 1009 deletions
diff --git a/apps/web/src/lib/selection/attributes.ts b/apps/web/src/lib/selection/attributes.ts deleted file mode 100644 index a1aee842..00000000 --- a/apps/web/src/lib/selection/attributes.ts +++ /dev/null @@ -1 +0,0 @@ -export const SELECTABLE_ITEM_ATTRIBUTE = "data-selectable-item"; diff --git a/apps/web/src/lib/selection/context.tsx b/apps/web/src/lib/selection/context.tsx deleted file mode 100644 index 8e87aa40..00000000 --- a/apps/web/src/lib/selection/context.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client";
-
-import { createContext, useContext } from "react";
-
-interface SelectionContextValue {
- selectedIds: string[];
- anchorId: string | null;
- highlightedId: string | null;
- isBoxSelecting: boolean;
- isSelected: (id: string) => boolean;
- clearSelection: () => void;
- handleItemClick: ({
- event,
- id,
- }: {
- event: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>;
- id: string;
- }) => void;
- handleItemMouseDown: ({
- event,
- id,
- }: {
- event: React.MouseEvent<HTMLDivElement>;
- id: string;
- }) => void;
- registerItem: (id: string, element: HTMLElement | null) => void;
-}
-
-export const SelectionContext = createContext<SelectionContextValue | null>(null);
-
-export function useSelectionContext() {
- const context = useContext(SelectionContext);
-
- if (!context) {
- throw new Error("useSelectionContext must be used within SelectableSurface");
- }
-
- return context;
-}
-
-export function useSelection() {
- const { selectedIds, anchorId, highlightedId, isSelected, clearSelection } =
- useSelectionContext();
-
- return {
- selectedIds,
- anchorId,
- highlightedId,
- isSelected,
- clearSelection,
- };
-}
diff --git a/apps/web/src/lib/selection/editor-selection.ts b/apps/web/src/lib/selection/editor-selection.ts deleted file mode 100644 index e9563b97..00000000 --- a/apps/web/src/lib/selection/editor-selection.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { SelectedKeyframeRef } from "@/lib/animation/types"; -import type { ElementRef } from "@/lib/timeline/types"; - -export interface SelectedMaskPointSelection { - trackId: string; - elementId: string; - maskId: string; - pointIds: string[]; -} - -export interface EditorSelectionSnapshot { - selectedElements: ElementRef[]; - selectedKeyframes: SelectedKeyframeRef[]; - keyframeSelectionAnchor: SelectedKeyframeRef | null; - selectedMaskPoints: SelectedMaskPointSelection | null; -} - -export interface EditorSelectionPatch { - selectedElements?: ElementRef[]; - selectedKeyframes?: SelectedKeyframeRef[]; - keyframeSelectionAnchor?: SelectedKeyframeRef | null; - selectedMaskPoints?: SelectedMaskPointSelection | null; -} - -export type EditorSelectionKind = "mask-points" | "keyframes" | "elements"; diff --git a/apps/web/src/lib/selection/hit-testing.ts b/apps/web/src/lib/selection/hit-testing.ts deleted file mode 100644 index d8186238..00000000 --- a/apps/web/src/lib/selection/hit-testing.ts +++ /dev/null @@ -1,50 +0,0 @@ -function isIntersecting({
- selectionRect,
- itemRect,
-}: {
- selectionRect: DOMRect;
- itemRect: DOMRect;
-}) {
- return !(
- itemRect.right < selectionRect.left ||
- itemRect.left > selectionRect.right ||
- itemRect.bottom < selectionRect.top ||
- itemRect.top > selectionRect.bottom
- );
-}
-
-function createSelectionRect({
- startPos,
- currentPos,
-}: {
- startPos: { x: number; y: number };
- currentPos: { x: number; y: number };
-}) {
- return new DOMRect(
- Math.min(startPos.x, currentPos.x),
- Math.min(startPos.y, currentPos.y),
- Math.abs(currentPos.x - startPos.x),
- Math.abs(currentPos.y - startPos.y),
- );
-}
-
-export function resolveElementIntersections({
- startPos,
- currentPos,
- elements,
-}: {
- startPos: { x: number; y: number };
- currentPos: { x: number; y: number };
- elements: Map<string, HTMLElement>;
-}) {
- const selectionRect = createSelectionRect({ startPos, currentPos });
-
- return [...elements.entries()]
- .filter(([, element]) =>
- isIntersecting({
- selectionRect,
- itemRect: element.getBoundingClientRect(),
- }),
- )
- .map(([id]) => id);
-}
diff --git a/apps/web/src/lib/selection/hooks/use-box-select.ts b/apps/web/src/lib/selection/hooks/use-box-select.ts deleted file mode 100644 index ab8464ec..00000000 --- a/apps/web/src/lib/selection/hooks/use-box-select.ts +++ /dev/null @@ -1,170 +0,0 @@ -"use client";
-
-import { useCallback, useEffect, useRef, useState } from "react";
-import type {
- BoxSelectionSnapshot,
- ResolveIntersections,
-} from "@/lib/selection/types";
-
-interface SelectionBoxState<TId> extends BoxSelectionSnapshot<TId> {
- startPos: { x: number; y: number };
- currentPos: { x: number; y: number };
- isActive: boolean;
- isAdditive: boolean;
-}
-
-export function useBoxSelect<TId>({
- containerRef,
- resolveIntersections,
- selectedIds,
- anchorId,
- onSelectionChange,
- shouldStartSelection,
- getIsAdditiveSelection,
- isEnabled = true,
-}: {
- containerRef: React.RefObject<HTMLElement | null>;
- resolveIntersections: ResolveIntersections<TId>;
- selectedIds: TId[];
- anchorId: TId | null;
- onSelectionChange: (state: {
- intersectedIds: TId[];
- initialSelectedIds: TId[];
- initialAnchorId: TId | null;
- isAdditive: boolean;
- }) => void;
- shouldStartSelection?: (event: React.MouseEvent<Element>) => boolean;
- getIsAdditiveSelection?: (event: React.MouseEvent<Element>) => boolean;
- isEnabled?: boolean;
-}) {
- 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);
- if (!isEnabled || event.button !== 0 || !canStartSelection) {
- return;
- }
-
- setSelectionBox({
- startPos: { x: event.clientX, y: event.clientY },
- currentPos: { x: event.clientX, y: event.clientY },
- isActive: false,
- isAdditive: getIsAdditiveSelectionCheck(event),
- initialSelectedIds: selectedIds,
- initialAnchorId: anchorId,
- });
- },
- [
- anchorId,
- getIsAdditiveSelectionCheck,
- isEnabled,
- selectedIds,
- shouldStartSelectionCheck,
- ],
- );
-
- const updateSelection = useCallback(
- ({
- startPos,
- currentPos,
- isAdditive,
- initialSelectedIds,
- initialAnchorId,
- }: SelectionBoxState<TId>) => {
- const intersectedIds = resolveIntersections({
- startPos,
- currentPos,
- });
- onSelectionChange({
- intersectedIds,
- initialSelectedIds,
- initialAnchorId,
- isAdditive,
- });
- },
- [onSelectionChange, resolveIntersections],
- );
-
- useEffect(() => {
- if (!selectionBox) {
- return;
- }
-
- const handleMouseMove = ({ clientX, clientY }: MouseEvent) => {
- const deltaX = Math.abs(clientX - selectionBox.startPos.x);
- const deltaY = Math.abs(clientY - selectionBox.startPos.y);
- const nextSelectionBox = {
- ...selectionBox,
- currentPos: { x: clientX, y: clientY },
- isActive: deltaX > 5 || deltaY > 5 || selectionBox.isActive,
- };
-
- setSelectionBox(nextSelectionBox);
-
- if (!nextSelectionBox.isActive) {
- return;
- }
-
- updateSelection(nextSelectionBox);
- };
-
- const handleMouseUp = () => {
- if (selectionBox.isActive) {
- justFinishedSelectingRef.current = true;
- requestAnimationFrame(() => {
- justFinishedSelectingRef.current = false;
- });
- }
-
- setSelectionBox(null);
- };
-
- window.addEventListener("mousemove", handleMouseMove);
- window.addEventListener("mouseup", handleMouseUp);
-
- return () => {
- window.removeEventListener("mousemove", handleMouseMove);
- window.removeEventListener("mouseup", handleMouseUp);
- };
- }, [selectionBox, updateSelection]);
-
- useEffect(() => {
- if (!selectionBox) {
- return;
- }
-
- const previousBodyUserSelect = document.body.style.userSelect;
- const previousContainerUserSelect = containerRef.current?.style.userSelect ?? "";
-
- document.body.style.userSelect = "none";
- if (containerRef.current) {
- containerRef.current.style.userSelect = "none";
- }
-
- return () => {
- document.body.style.userSelect = previousBodyUserSelect;
- if (containerRef.current) {
- containerRef.current.style.userSelect = previousContainerUserSelect;
- }
- };
- }, [containerRef, selectionBox]);
-
- const shouldIgnoreClick = useCallback(() => {
- return justFinishedSelectingRef.current;
- }, []);
-
- return {
- selectionBox,
- handleMouseDown,
- isSelecting: selectionBox?.isActive ?? false,
- shouldIgnoreClick,
- };
-}
diff --git a/apps/web/src/lib/selection/hooks/use-selection-scope.ts b/apps/web/src/lib/selection/hooks/use-selection-scope.ts deleted file mode 100644 index a7090c5d..00000000 --- a/apps/web/src/lib/selection/hooks/use-selection-scope.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useEffect, useRef } from "react";
-import { useSelectionContext } from "@/lib/selection/context";
-import { activateScope, type ScopeEntry } from "@/lib/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>({
- hasSelection: () => hasSelectionRef.current,
- clear: () => {
- clearSelectionRef.current();
- },
- });
-
- useEffect(() => {
- if (!hasSelection) {
- return;
- }
-
- return activateScope({ entry: entryRef.current });
- }, [hasSelection]);
-}
diff --git a/apps/web/src/lib/selection/index.ts b/apps/web/src/lib/selection/index.ts deleted file mode 100644 index b1ded820..00000000 --- a/apps/web/src/lib/selection/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { SelectableItem } from "@/lib/selection/selectable-item";
-export { SelectableSurface } from "@/lib/selection/selectable-surface";
-export { useSelection } from "@/lib/selection/context";
-export { useSelectionScope } from "@/lib/selection/hooks/use-selection-scope";
diff --git a/apps/web/src/lib/selection/scope.ts b/apps/web/src/lib/selection/scope.ts deleted file mode 100644 index 22c7710c..00000000 --- a/apps/web/src/lib/selection/scope.ts +++ /dev/null @@ -1,30 +0,0 @@ -export type ScopeEntry = { - hasSelection: () => boolean; - clear: () => void; - clearActive?: () => void; -}; - -let activeScope: ScopeEntry | null = null; - -export function activateScope({ entry }: { entry: ScopeEntry }): () => void { - if (activeScope && activeScope !== entry) { - activeScope.clear(); - } - - activeScope = entry; - - return () => { - if (activeScope === entry) { - activeScope = null; - } - }; -} - -export function clearActiveScope(): boolean { - if (!activeScope?.hasSelection()) { - return false; - } - - (activeScope.clearActive ?? activeScope.clear)(); - return true; -} diff --git a/apps/web/src/lib/selection/selectable-item.tsx b/apps/web/src/lib/selection/selectable-item.tsx deleted file mode 100644 index 03264753..00000000 --- a/apps/web/src/lib/selection/selectable-item.tsx +++ /dev/null @@ -1,107 +0,0 @@ -"use client";
-
-import { forwardRef, useCallback } from "react";
-import { useSelectionContext } from "@/lib/selection/context";
-import { SELECTABLE_ITEM_ATTRIBUTE } from "@/lib/selection/attributes";
-import type { SelectableItemProps } from "@/lib/selection/types";
-import { cn } from "@/utils/ui";
-
-function setForwardedRef<T>(ref: React.ForwardedRef<T>, value: T | null) {
- if (typeof ref === "function") {
- ref(value);
- return;
- }
-
- if (ref) {
- ref.current = value;
- }
-}
-
-export const SelectableItem = forwardRef<HTMLDivElement, SelectableItemProps>(
- function SelectableItem(
- {
- id,
- children,
- className,
- onClick,
- onKeyDown,
- onMouseDown,
- onContextMenu,
- tabIndex,
- ...rest
- }: SelectableItemProps,
- forwardedRef,
- ) {
- const {
- highlightedId,
- isBoxSelecting,
- isSelected,
- handleItemClick,
- handleItemMouseDown,
- registerItem,
- } = useSelectionContext();
- const isItemSelected = isSelected(id);
- const isItemHighlighted = highlightedId === id;
- const stateClassName = cn(
- "relative",
- isBoxSelecting && "pointer-events-none",
- isItemSelected && "ring-1 ring-primary rounded-sm bg-primary/10",
- isItemHighlighted &&
- (isItemSelected
- ? "rounded-sm shadow-[0_0_0_1px_hsl(var(--primary))]"
- : "ring-1 ring-primary/60 rounded-sm bg-primary/5"),
- );
-
- const handleRef = useCallback(
- (element: HTMLDivElement | null) => {
- registerItem(id, element);
- setForwardedRef(forwardedRef, element);
- },
- [forwardedRef, id, registerItem],
- );
-
- return (
- <div
- ref={handleRef}
- className={cn(stateClassName, className)}
- {...{ [SELECTABLE_ITEM_ATTRIBUTE]: "true" }}
- role="option"
- aria-selected={isItemSelected}
- tabIndex={tabIndex ?? 0}
- onClick={(event) => {
- onClick?.(event);
- if (event.defaultPrevented) {
- return;
- }
-
- handleItemClick({ event, id });
- }}
- onMouseDown={(event) => {
- onMouseDown?.(event);
- if (event.defaultPrevented) {
- return;
- }
-
- handleItemMouseDown({ event, id });
- }}
- onKeyDown={(event) => {
- onKeyDown?.(event);
- if (event.defaultPrevented || event.target !== event.currentTarget) {
- return;
- }
-
- if (event.key !== "Enter" && event.key !== " ") {
- return;
- }
-
- event.preventDefault();
- handleItemClick({ event, id });
- }}
- onContextMenu={onContextMenu}
- {...rest}
- >
- {children}
- </div>
- );
- },
-);
diff --git a/apps/web/src/lib/selection/selectable-surface.tsx b/apps/web/src/lib/selection/selectable-surface.tsx deleted file mode 100644 index 012bc486..00000000 --- a/apps/web/src/lib/selection/selectable-surface.tsx +++ /dev/null @@ -1,288 +0,0 @@ -"use client";
-
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { SelectionBox } from "@/lib/selection/selection-box";
-import { SelectionContext } from "@/lib/selection/context";
-import { SELECTABLE_ITEM_ATTRIBUTE } from "@/lib/selection/attributes";
-import { resolveElementIntersections } from "@/lib/selection/hit-testing";
-import {
- applyBoxSelection,
- clearSelection,
- isSelected as getIsSelected,
- pruneSelection,
- replaceSelection,
- selectRange,
- toggleSelection,
-} from "@/lib/selection/state";
-import type {
- SelectableSurfaceProps,
- SelectionState,
-} from "@/lib/selection/types";
-import { useBoxSelect } from "@/lib/selection/hooks/use-box-select";
-import { cn } from "@/utils/ui";
-
-export function SelectableSurface({
- orderedIds,
- children,
- className,
- ariaLabel = "Selectable items",
- revealId = null,
- onRevealComplete,
- onSelectionChange,
-}: SelectableSurfaceProps) {
- const [selectionState, setSelectionState] = useState<SelectionState>(() =>
- clearSelection(),
- );
- const [highlightedId, setHighlightedId] = useState<string | null>(null);
- const containerRef = useRef<HTMLDivElement>(null);
- const itemElementsRef = useRef<Map<string, HTMLElement>>(new Map());
-
- const registerItem = useCallback(
- (id: string, element: HTMLElement | null) => {
- if (element) {
- itemElementsRef.current.set(id, element);
- } else {
- itemElementsRef.current.delete(id);
- }
- },
- [],
- );
-
- const getSelectableElements = useCallback(() => {
- return itemElementsRef.current;
- }, []);
-
- const getItemElement = useCallback((id: string) => {
- return itemElementsRef.current.get(id) ?? null;
- }, []);
- const shouldStartSelection = useCallback(
- (event: React.MouseEvent<Element>) => {
- return !(
- event.target instanceof Element &&
- event.target.closest(`[${SELECTABLE_ITEM_ATTRIBUTE}='true']`)
- );
- },
- [],
- );
- const resolveIntersections = useCallback(
- ({
- startPos,
- currentPos,
- }: {
- startPos: { x: number; y: number };
- currentPos: { x: number; y: number };
- }) => {
- return resolveElementIntersections({
- startPos,
- currentPos,
- elements: getSelectableElements(),
- });
- },
- [getSelectableElements],
- );
-
- const clearSelectionState = useCallback(() => {
- setSelectionState(clearSelection());
- }, []);
-
- const selectedIdSet = useMemo(
- () => new Set(selectionState.selectedIds),
- [selectionState.selectedIds],
- );
-
- const isSelected = useCallback(
- (id: string) => selectedIdSet.has(id),
- [selectedIdSet],
- );
-
- const handleItemClick = useCallback(
- ({
- event,
- id,
- }: {
- event:
- | React.MouseEvent<HTMLDivElement>
- | React.KeyboardEvent<HTMLDivElement>;
- id: string;
- }) => {
- setSelectionState((state) => {
- const isToggleSelection = event.ctrlKey || event.metaKey;
-
- if (event.shiftKey) {
- return selectRange({
- state,
- orderedIds,
- targetId: id,
- isAdditive: isToggleSelection,
- });
- }
-
- if (isToggleSelection) {
- return toggleSelection({
- state,
- id,
- });
- }
-
- return replaceSelection({
- ids: [id],
- anchorId: id,
- });
- });
- },
- [orderedIds],
- );
-
- const selectUnselectedItem = useCallback((id: string) => {
- setSelectionState((state) => {
- if (getIsSelected({ state, id })) {
- return state;
- }
-
- return replaceSelection({ ids: [id], anchorId: id });
- });
- }, []);
-
- const handleItemMouseDown = useCallback(
- ({
- event,
- id,
- }: {
- event: React.MouseEvent<HTMLDivElement>;
- id: string;
- }) => {
- if (event.button !== 2) {
- return;
- }
-
- selectUnselectedItem(id);
- },
- [selectUnselectedItem],
- );
-
- const handleBoxSelectionChange = useCallback(
- (change: Parameters<typeof applyBoxSelection>[0]) => {
- setSelectionState(applyBoxSelection(change));
- },
- [],
- );
-
- const { selectionBox, handleMouseDown, shouldIgnoreClick } = useBoxSelect({
- containerRef,
- resolveIntersections,
- selectedIds: selectionState.selectedIds,
- anchorId: selectionState.anchorId,
- onSelectionChange: handleBoxSelectionChange,
- shouldStartSelection,
- });
-
- const handleBackgroundClick = useCallback(
- (event: React.MouseEvent<HTMLDivElement>) => {
- const clickedSelectableItem =
- event.target instanceof Element &&
- event.target.closest(`[${SELECTABLE_ITEM_ATTRIBUTE}='true']`);
- const isIgnoringClick = shouldIgnoreClick();
- if (clickedSelectableItem || isIgnoringClick) {
- return;
- }
-
- clearSelectionState();
- },
- [clearSelectionState, shouldIgnoreClick],
- );
-
- const handleBackgroundKeyDown = useCallback(
- (event: React.KeyboardEvent<HTMLDivElement>) => {
- if (event.target !== event.currentTarget) {
- return;
- }
-
- if (event.key !== "Enter" && event.key !== " " && event.key !== "Escape") {
- return;
- }
-
- event.preventDefault();
- clearSelectionState();
- },
- [clearSelectionState],
- );
-
- useEffect(() => {
- setSelectionState((state) =>
- pruneSelection({
- state,
- orderedIds,
- }),
- );
- }, [orderedIds]);
-
- useEffect(() => {
- onSelectionChange?.(selectionState);
- }, [onSelectionChange, selectionState]);
-
- useEffect(() => {
- if (!revealId) {
- return;
- }
-
- setHighlightedId(revealId);
- getItemElement(revealId)?.scrollIntoView({ block: "center" });
-
- const timer = setTimeout(() => {
- setHighlightedId(null);
- onRevealComplete?.();
- }, 1500);
-
- return () => clearTimeout(timer);
- }, [getItemElement, onRevealComplete, revealId]);
-
- const isBoxSelecting = selectionBox?.isActive ?? false;
-
- const contextValue = useMemo(() => {
- return {
- selectedIds: selectionState.selectedIds,
- anchorId: selectionState.anchorId,
- highlightedId,
- isBoxSelecting,
- isSelected,
- clearSelection: clearSelectionState,
- handleItemClick,
- handleItemMouseDown,
- registerItem,
- };
- }, [
- clearSelectionState,
- handleItemClick,
- handleItemMouseDown,
- highlightedId,
- isBoxSelecting,
- isSelected,
- registerItem,
- selectionState.anchorId,
- selectionState.selectedIds,
- ]);
-
- return (
- <SelectionContext.Provider value={contextValue}>
- <div
- ref={containerRef}
- className={cn("relative min-h-full", className)}
- role="listbox"
- aria-label={ariaLabel}
- aria-multiselectable="true"
- tabIndex={0}
- onMouseDown={handleMouseDown}
- onClick={handleBackgroundClick}
- onKeyDown={handleBackgroundKeyDown}
- >
- {children}
- <SelectionBox
- startPos={selectionBox?.startPos ?? null}
- currentPos={selectionBox?.currentPos ?? null}
- containerRef={containerRef}
- isActive={selectionBox?.isActive ?? false}
- />
- </div>
- </SelectionContext.Provider>
- );
-}
diff --git a/apps/web/src/lib/selection/selection-box.tsx b/apps/web/src/lib/selection/selection-box.tsx deleted file mode 100644 index e30f574d..00000000 --- a/apps/web/src/lib/selection/selection-box.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client";
-
-import { useMemo } from "react";
-
-interface SelectionBoxProps {
- startPos: { x: number; y: number } | null;
- currentPos: { x: number; y: number } | null;
- containerRef: React.RefObject<HTMLElement | null>;
- isActive: boolean;
-}
-
-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;
-
- return (
- <div
- style={selectionBoxStyle}
- className="border-foreground/50 bg-foreground/5 pointer-events-none absolute z-50 border"
- />
- );
-}
diff --git a/apps/web/src/lib/selection/state.ts b/apps/web/src/lib/selection/state.ts deleted file mode 100644 index b0ba3089..00000000 --- a/apps/web/src/lib/selection/state.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type {
- BoxSelectionChange,
- SelectionState,
-} from "@/lib/selection/types";
-
-function dedupeIds({ ids }: { ids: string[] }) {
- return [...new Set(ids)];
-}
-
-function getRangeIds({
- orderedIds,
- anchorId,
- targetId,
-}: {
- orderedIds: string[];
- anchorId: string;
- targetId: string;
-}) {
- const anchorIndex = orderedIds.indexOf(anchorId);
- const targetIndex = orderedIds.indexOf(targetId);
-
- if (anchorIndex === -1 || targetIndex === -1) {
- return [targetId];
- }
-
- const rangeStart = Math.min(anchorIndex, targetIndex);
- const rangeEnd = Math.max(anchorIndex, targetIndex);
- return orderedIds.slice(rangeStart, rangeEnd + 1);
-}
-
-export function replaceSelection({
- ids,
- anchorId,
-}: {
- ids: string[];
- anchorId?: string | null;
-}): SelectionState {
- const selectedIds = dedupeIds({ ids });
- return {
- selectedIds,
- anchorId: anchorId ?? selectedIds[selectedIds.length - 1] ?? null,
- };
-}
-
-export function clearSelection(): SelectionState {
- return {
- selectedIds: [],
- anchorId: null,
- };
-}
-
-export function pruneSelection({
- state,
- orderedIds,
-}: {
- state: SelectionState;
- orderedIds: string[];
-}): SelectionState {
- const validIds = new Set(orderedIds);
- const selectedIds = state.selectedIds.filter((id) => validIds.has(id));
- const anchorId =
- state.anchorId && validIds.has(state.anchorId)
- ? state.anchorId
- : selectedIds[selectedIds.length - 1] ?? null;
- const isUnchanged =
- selectedIds.length === state.selectedIds.length &&
- anchorId === state.anchorId;
-
- if (isUnchanged) {
- return state;
- }
-
- return {
- selectedIds,
- anchorId,
- };
-}
-
-export function isSelected({
- state,
- id,
-}: {
- state: SelectionState;
- id: string;
-}) {
- return state.selectedIds.includes(id);
-}
-
-export function toggleSelection({
- state,
- id,
-}: {
- state: SelectionState;
- id: string;
-}): SelectionState {
- if (isSelected({ state, id })) {
- const selectedIds = state.selectedIds.filter((selectedId) => selectedId !== id);
- const anchorId =
- state.anchorId === id
- ? selectedIds[selectedIds.length - 1] ?? null
- : state.anchorId;
- return replaceSelection({
- ids: selectedIds,
- anchorId,
- });
- }
-
- return replaceSelection({
- ids: [...state.selectedIds, id],
- anchorId: id,
- });
-}
-
-export function selectRange({
- state,
- orderedIds,
- targetId,
- isAdditive,
-}: {
- state: SelectionState;
- orderedIds: string[];
- targetId: string;
- isAdditive: boolean;
-}): SelectionState {
- const anchorId =
- state.anchorId ??
- state.selectedIds[state.selectedIds.length - 1] ??
- targetId;
- const rangeIds = getRangeIds({
- orderedIds,
- anchorId,
- targetId,
- });
- const selectedIds = isAdditive
- ? dedupeIds({
- ids: [...state.selectedIds, ...rangeIds],
- })
- : rangeIds;
-
- return replaceSelection({
- ids: selectedIds,
- anchorId,
- });
-}
-
-export function applyBoxSelection({
- intersectedIds,
- initialSelectedIds,
- initialAnchorId,
- isAdditive,
-}: BoxSelectionChange): SelectionState {
- const selectedIds = isAdditive
- ? dedupeIds({
- ids: [...initialSelectedIds, ...intersectedIds],
- })
- : intersectedIds;
- const anchorId = isAdditive
- ? initialAnchorId ?? intersectedIds[intersectedIds.length - 1] ?? null
- : intersectedIds[intersectedIds.length - 1] ?? null;
-
- return replaceSelection({
- ids: selectedIds,
- anchorId,
- });
-}
diff --git a/apps/web/src/lib/selection/types.ts b/apps/web/src/lib/selection/types.ts deleted file mode 100644 index 31520788..00000000 --- a/apps/web/src/lib/selection/types.ts +++ /dev/null @@ -1,39 +0,0 @@ -export interface SelectionState {
- selectedIds: string[];
- anchorId: string | null;
-}
-
-export interface BoxSelectionSnapshot<TId = string> {
- initialSelectedIds: TId[];
- initialAnchorId: TId | null;
-}
-
-export interface BoxSelectionChange<TId = string>
- extends BoxSelectionSnapshot<TId> {
- intersectedIds: TId[];
- isAdditive: boolean;
-}
-
-export type ResolveIntersections<TId = string> = ({
- startPos,
- currentPos,
-}: {
- startPos: { x: number; y: number };
- currentPos: { x: number; y: number };
-}) => TId[];
-
-export interface SelectableSurfaceProps {
- orderedIds: string[];
- children: React.ReactNode;
- className?: string;
- ariaLabel?: string;
- revealId?: string | null;
- onRevealComplete?: () => void;
- onSelectionChange?: (state: SelectionState) => void;
-}
-
-export interface SelectableItemProps
- extends React.HTMLAttributes<HTMLDivElement> {
- id: string;
- children: React.ReactNode;
-}
|
