diff options
Diffstat (limited to 'apps/web/src/lib/selection/selectable-surface.tsx')
| -rw-r--r-- | apps/web/src/lib/selection/selectable-surface.tsx | 288 |
1 files changed, 0 insertions, 288 deletions
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>
- );
-}
|
