import { useTheme } from '@mui/material'; import { type CollisionDetection, DndContext, type DragEndEvent, type DraggableSyntheticListeners, type DragOverEvent, DragOverlay, type DragStartEvent, KeyboardSensor, MouseSensor, type Over, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { SortableContext, type SortingStrategy } from '@dnd-kit/sortable'; import { equals, find, indexOf, isNil, move, not, path, pick, pipe, pluck, propEq } from 'ramda'; import { type RefObject, useEffect, useState } from 'react'; import Item from './Item'; import SortableItem from './SortableItem'; interface ContentProps { attributes: Record; index: number; isDragging: boolean; itemRef: RefObject; listeners: DraggableSyntheticListeners; style: React.CSSProperties; } export interface RootComponentProps { children: React.ReactNode; isInDragOverlay?: boolean; } export interface DragEnd { event: DragEndEvent; items: Array; } const DefaultRootComponent = ({ children }: RootComponentProps): JSX.Element => children as JSX.Element; interface Props { Content: ({ isDragging, listeners, attributes, style, itemRef, index, ...other }: ContentProps & T) => JSX.Element | null; RootComponent?: ({ children, isInDragOverlay }: RootComponentProps) => JSX.Element; additionalProps?: Array; collisionDetection: CollisionDetection; getDisableItemCondition?: (item: T) => boolean; getDisableOverItemSortableCondition?: (overItem: Over) => boolean; itemProps: Array; items: Array; memoProps?: Array; onDragEnd?: (props: DragEnd) => void; onDragOver?: (event: DragOverEvent) => void; onDragStart?: (event: DragStartEvent) => void; sortingStrategy: SortingStrategy; updateSortableItemsOnItemsChange?: boolean; } const propertyToFilterItemsOn = 'id'; const SortableItems = ({ items, onDragStart, onDragEnd, onDragOver, collisionDetection, sortingStrategy, itemProps, memoProps = [], additionalProps, RootComponent = DefaultRootComponent, Content, getDisableItemCondition = (): boolean => false, getDisableOverItemSortableCondition = (): boolean => false, updateSortableItemsOnItemsChange = false }: Props): JSX.Element => { const getItemsIds = (): Array => pluck(propertyToFilterItemsOn, items); const [activeId, setActiveId] = useState(null); const [sortableItemsIds, setSortableItemsIds] = useState(getItemsIds()); const sensors = useSensors( useSensor(MouseSensor), useSensor(PointerSensor), useSensor(KeyboardSensor, { keyboardCodes: { cancel: ['Escape'], end: ['Space', 'Enter'], start: [ 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter', 'Space' ] }, scrollBehavior: 'smooth' }) ); const theme = useTheme(); const isOverItemDisabled = (overItem: Over | null): boolean => { return (overItem && getDisableOverItemSortableCondition(overItem)) || false; }; const dragStart = (event: DragStartEvent): void => { onDragStart?.(event); setActiveId(path(['active', propertyToFilterItemsOn], event) as string); }; const dragCancel = (): void => setActiveId(null); const dragEnd = (event: DragEndEvent): void => { const overId = path(['over', propertyToFilterItemsOn], event); if ( pipe(isNil, not)(overId) && pipe(equals(activeId), not)(overId as string | null) && not(isOverItemDisabled(event.over)) ) { const oldIndex = indexOf(activeId, sortableItemsIds); const newIndex = indexOf(overId, sortableItemsIds); const newItemsOrder = move(oldIndex, newIndex, sortableItemsIds); setSortableItemsIds(newItemsOrder); onDragEnd?.({ event, items: newItemsOrder }); } setActiveId(null); }; const dragOver = (event: DragOverEvent): void => { onDragOver?.(event); }; const getItemById = (id: string | null): T | undefined => find(propEq(id, propertyToFilterItemsOn), items); const activeItem = getItemById(activeId) as Record; useEffect(() => { if (not(updateSortableItemsOnItemsChange)) { return; } setSortableItemsIds(getItemsIds()); }, [updateSortableItemsOnItemsChange, items]); return ( {sortableItemsIds.map((sortableItemId, index) => { const item = getItemById(sortableItemId) as | Record | undefined; if (isNil(item)) { return null; } return ( not(getDisableItemCondition(item as T)) && ( ) ); })} {activeId ? ( ) : null} ); }; export default SortableItems;