import React from 'react' import { useGridContext, useGridSelector } from '../../context/grid-context' import type { DragStartEvent, DragMoveEvent } from '@dnd-kit/core' import { DndContext, DragOverlay, useSensor, useSensors, defaultDropAnimation, } from '@dnd-kit/core' import { TouchSensor, MouseSensor, KeyboardSensor } from './sensors' import type { CollisionDetails } from './collision' import { collisionFactory } from './collision' import { useGridComponents } from '../../context/grid-components-context' import { modifiersFactory } from './modifiers' import isEqual from 'lodash.isequal' import { coordinatesFactory } from './coordinates' import { measuringFactory } from './measuring' import { useAnnouncements } from './announcements' import styled from 'styled-components' import { useColumnMove } from './column' import { DRAG_TYPE_HEADER_CELL, DRAG_TYPE_HEADER_GROUP_CELL } from './constants' import { Portal } from '@planview/pv-uikit' export type GridDragControllerProps = { children: React.ReactNode } const dropAnimationWithoutHiding = { ...defaultDropAnimation, duration: 0, sideEffects: null, } const HOVER_TO_EXPAND_DELAY = 700 const NO_COLLISION: CollisionDetails = { parentId: null, prevId: null, prevInsertId: null, level: 0, operation: 'between', } const StyledDragOverlay = styled(DragOverlay)<{ theme: { zindex: number } }>` z-index: ${(props) => props.theme.zindex} !important; ` const Backdrop = styled.div` position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: all; ` export const GridDragController = ({ children }: GridDragControllerProps) => { const grid = useGridContext() const { GridDragOverlay, GridHeaderDragOverlay } = useGridComponents() const rowHeight = useGridSelector(grid.selectors.selectRowHeight) const { onMoveStart, onMove, onMoveEnd, onMoveCancel, minLeft, maxLeft } = useColumnMove() const { parentId } = useGridSelector( grid.selectors.selectDragCollisionDetails ) React.useEffect(() => { if (parentId) { const rowId = parentId const state = grid.getState() const expandable = grid.selectors.selectIsRowExpandable( state, rowId ) const expanded = grid.selectors.selectIsRowExpanded(state, rowId) if (expandable && !expanded) { const timer = window.setTimeout(() => { grid.api.tree.expand(rowId, true) }, HOVER_TO_EXPAND_DELAY) return () => window.clearTimeout(timer) } } }, [grid, parentId]) const mouseSensor = useSensor(MouseSensor, { activationConstraint: { distance: 5, }, }) const touchSensor = useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 10, }, }) const keyboardSensor = useSensor(KeyboardSensor, { coordinateGetter: coordinatesFactory(rowHeight), }) const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor) const gridRowIntersection = React.useMemo( () => collisionFactory(grid), [grid] ) const { snapCenterToCursor, restrictToHorizontalAxis, rubberBand } = React.useMemo(() => modifiersFactory(grid), [grid]) const measuringConfig = React.useMemo(() => measuringFactory(grid), [grid]) const [draggingHeader, setDraggingHeader] = React.useState(false) const onDragStart = React.useCallback( (e: DragStartEvent) => { const type = e.active.data.current ? e.active.data.current.type : '' if ( type === DRAG_TYPE_HEADER_CELL || type === DRAG_TYPE_HEADER_GROUP_CELL ) { onMoveStart(e.active.id as string, e) setDraggingHeader(true) } else { grid.api.navigation.disable() grid.api.drag.start(e.active.id) } }, [grid.api.drag, grid.api.navigation, onMoveStart] ) const onDragMove = React.useCallback( (e: DragMoveEvent) => { const state = grid.getState() const dragDetails = grid.selectors.selectDragCollisionDetails(state) const collisionData = e.collisions?.[0]?.data as CollisionDetails if (!isEqual(dragDetails, collisionData)) { const details: CollisionDetails = collisionData ?? NO_COLLISION grid.api.drag.move(details) } }, [grid] ) const onDragEndHeader = React.useCallback(() => { onMoveEnd() setDraggingHeader(false) }, [onMoveEnd]) const onDragCancelHeader = React.useCallback(() => { onMoveCancel() setDraggingHeader(false) }, [onMoveCancel]) const onDragEnd = React.useCallback(() => { grid.api.drag.complete() grid.api.navigation.enable() }, [grid.api.drag, grid.api.navigation]) const onDragCancel = React.useCallback(() => { grid.api.drag.cancel() grid.api.navigation.enable() }, [grid.api.drag, grid.api.navigation]) const { announcements, screenReaderInstructions } = useAnnouncements() return ( {children} {draggingHeader && } ) }