import React, { useRef } from "react" import { useDrag, useDrop } from "react-dnd" /** Payload tracked while dragging a reorderable list item */ interface ReorderableDragItem { id: any constrainTo: string height: number } export interface ReorderableListItemComponentProps { /** the item from reorderable list */ item: any /** Call with (id, beforeId) */ onPutBefore: any /** Call with (id, afterId) */ onPutAfter: any /** Called when drag is complete */ onEndDrag: any /** index of the current item */ index: number /** function to render the current item, passed by ReorderableListComponent */ renderItem: any /** the ID of the list where reordering is constrained to */ constrainTo: string getItemId: any } /** Combine a v16 drag/drop ref-callback with any existing ref on a React element. */ function combineRefs( element: React.ReactElement, refCallback: (el: any) => void ): React.ReactElement { const existingRef: any = (element.props as any).ref ?? (element as any).ref const combined = (el: any) => { refCallback(el) if (typeof existingRef === "function") { existingRef(el) } else if (existingRef && typeof existingRef === "object") { (existingRef as React.MutableRefObject).current = el } } return React.cloneElement(element, { ref: combined } as any) } /** * Reorderable list item using react-dnd hooks. Exposes legacy-style "connect" * callbacks to the consumer's `renderItem` callback so that consumers can wrap * arbitrary elements with drag-source / drag-preview / drop-target refs * without knowing about the underlying hooks API. */ export default function ReorderableListItemComponent(props: ReorderableListItemComponentProps) { const domNodeRef = useRef(null) const [, drag, preview] = useDrag(() => ({ type: "form-item", item: () => ({ id: props.getItemId(props.item), constrainTo: props.constrainTo, // Save height of dragged component height: domNodeRef.current ? domNodeRef.current.getBoundingClientRect().height : 0 }), isDragging: (monitor) => props.getItemId(props.item) === monitor.getItem().id, end: () => props.onEndDrag() }), [props.item, props.constrainTo, props.getItemId, props.onEndDrag]) const [, drop] = useDrop(() => ({ accept: "form-item", canDrop: (item) => props.constrainTo === item.constrainTo, hover: (item, monitor) => { const myId = props.getItemId(props.item) if (item.id === myId) return if (props.constrainTo !== item.constrainTo) return if (!domNodeRef.current) return const hoverBoundingRect = domNodeRef.current.getBoundingClientRect() const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 const clientOffset = monitor.getClientOffset() if (!clientOffset) return const hoverClientY = clientOffset.y - hoverBoundingRect.top // If in top half, and within the height of the dragged item if (hoverClientY < hoverMiddleY && hoverClientY < item.height) { props.onPutBefore(myId, item.id) return } // If in bottom half, and within the height of the dragged item if (hoverClientY > hoverMiddleY && hoverClientY > hoverBoundingRect.height - item.height) { props.onPutAfter(myId, item.id) return } } }), [props.item, props.constrainTo, props.getItemId, props.onPutBefore, props.onPutAfter]) // Backward-compatible v6-style "connect" callbacks for the renderItem callback. const connectDragSource = (element: React.ReactElement) => combineRefs(element, (el) => drag(el)) const connectDragPreview = (element: React.ReactElement) => combineRefs(element, (el) => preview(el)) const connectDropTarget = (element: React.ReactElement) => combineRefs(element, (el) => { domNodeRef.current = el drop(el) }) return props.renderItem( props.item, props.index, connectDragSource, connectDragPreview, connectDropTarget ) }