import React, { useEffect, useRef } from "react"; import styled from "styled-components"; import { editingCellContext } from "../context/EditorContext"; import { useScrollerRef } from "../context/ScrollerContext"; import { isSelectingContext, selectionContext, } from "../context/SelectionContext"; import { stickyColumnContext } from "../context/StickyColumnContext"; import { useSelectionKeyHandlers } from "../hooks/useSelectionKeyHandlers"; import { useSelectionMouseMove } from "../hooks/useSelectionMouseMove"; import { useSelectionScroller } from "../hooks/useSelectionScroller"; /** * This component renders the selection box and enables the selection box to be * manipulated with the keyboard and mouse. It also triggers scrolling when the * selection moves. */ export const Selection = ({ numStickyColumns, headers, rowHeight, }: { numStickyColumns: number; headers: { height: number }[]; rowHeight: number; }) => { const selection = selectionContext.useState(); const setIsSelecting = isSelectingContext.useSetter(); const isSelecting = isSelectingContext.useState(); const isSelectingRef = isSelectingContext.useRef(); const setSelection = selectionContext.useSetter(); const primarySelectionRef = useRef(null); const scrollerRef = useScrollerRef(); const topLeftRef = useRef(null); const bottomRightRef = useRef(null); const editingCell = editingCellContext.useState(); const stickyColumnWidths = stickyColumnContext.useState(); useSelectionScroller({ topLeftRef, bottomRightRef, headers }); useSelectionMouseMove(); useSelectionKeyHandlers(); useEffect(() => { const listener = (e: MouseEvent) => { if (isSelectingRef.current) { setIsSelecting(false); } else if (e.target && !scrollerRef.current?.contains(e.target as Node)) { setSelection(null); } }; document.addEventListener("click", listener); return () => document.removeEventListener("click", listener); }, []); const createPositionStyle = ({ start, height, width, }: { start: { x: number; y: number }; height: number; width: number; }): React.CSSProperties => ({ gridColumn: `${start.x + 1} / ${start.x + 1 + width}`, gridRow: `${1 + headers.length} / ${1 + headers.length + height}`, transform: `translate(-1px, ${start.y * rowHeight - 1}px)`, }); if (!selection) { return null; } // The styles on these components change very often. For performance reasons, // we can't use styled-components return ( <> {selection.width > 1 || selection.height > 1 ? ( <> {/* The selection square for the sticky cells has to be a different component */} {selection.start.x < numStickyColumns ? ( acc + v, 0), zIndex: 3, position: "sticky", }} isSelecting={isSelecting} hasRightBorder={ selection.start.x + selection.width <= numStickyColumns } /> ) : null} ) : null} {/* Render invisible squares so we can easily tell the position of the selection box on the screen */} acc + v, 0) : undefined, ...createPositionStyle({ start: selection.primary, height: 1, width: 1, }), zIndex: selection.primary.x === editingCell?.x && selection.primary.y === editingCell?.y ? 5 : selection.primary.x < numStickyColumns ? 3 : 2, }} /> ); }; const InvisibleSquare = styled.div` height: 100%; width: 100%; position: absolute; user-select: none; pointer-events: none; `; const SelectionSquare = styled.div<{ isSelecting: boolean; hasRightBorder?: boolean; }>` background: ${({ theme }) => theme.selection.secondary.backgroundColor}; user-select: none; pointer-events: none; border: ${({ isSelecting, theme }) => isSelecting ? "0" : `${theme.selection.secondary.borderWidth}px solid ${theme.selection.secondary.borderColor}`}; ${({ hasRightBorder = true }) => (hasRightBorder ? "" : "border-right: 0;")} height: calc(100% + 1px); width: calc(100% + 1px); box-sizing: border-box; position: absolute; `; const PrimarySelectionSquare = styled.div` user-select: none; pointer-events: none; box-sizing: border-box; height: calc(100% + 1px); width: calc(100% + 1px); border: ${({ theme }) => `${theme.selection.primary.borderWidth}px solid ${theme.selection.primary.borderColor}`}; position: absolute; `;